Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions cmd/onecli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
"net/url"
"os"
Expand Down Expand Up @@ -95,6 +96,9 @@ func (c *RunCmd) Run(out *output.Writer) error {
}
}

// Write credential stubs for connected apps (non-fatal on failure).
writeCredentialStubs(client, out)

// Build child environment.
env := buildChildEnv(os.Environ(), cfg.Env, caPath)

Expand Down Expand Up @@ -322,6 +326,77 @@ func buildSkillContent(secrets []api.Secret) string {
return strings.Replace(gatewaySkill, "{{SERVICES_SECTION}}", sb.String(), 1)
}

// credentialSentinel is the placeholder value used in credential stub files.
// Files containing this value are safe to overwrite; files without it contain
// real credentials and must not be touched.
const credentialSentinel = "onecli-managed"

// writeCredentialStubs fetches connected apps and writes credential stub files
// for apps that define them. Existing files with real credentials (no sentinel)
// are never overwritten.
func writeCredentialStubs(client *api.Client, out *output.Writer) {
apps, err := client.ListApps(newContext())
if err != nil {
return
}
for _, app := range apps {
if app.Connection == nil || app.Connection.Status != "connected" {
continue
}
for _, stub := range app.CredentialStubs {
if stub.Path == "" {
continue
}
safeWriteStub(stub, out)
}
}
}

// safeWriteStub writes a single credential stub file. It refuses to overwrite
// files that don't contain the "onecli-managed" sentinel, and skips the write
// when the on-disk content already matches.
func safeWriteStub(stub api.CredentialStub, out *output.Writer) {
path := expandTilde(stub.Path)

content, err := json.MarshalIndent(stub.Content, "", " ")
if err != nil {
return
}
content = append(content, '\n')

existing, readErr := os.ReadFile(path)
if readErr == nil {
if bytes.Equal(existing, content) {
return
}
if !bytes.Contains(existing, []byte(credentialSentinel)) {
out.Stderr(fmt.Sprintf("onecli: skipping %s (real credentials detected)", stub.Path))
return
}
}

if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
out.Stderr(fmt.Sprintf("onecli: warning: could not create directory for %s: %v", stub.Path, err))
return
}
if err := os.WriteFile(path, content, 0o600); err != nil {
out.Stderr(fmt.Sprintf("onecli: warning: could not write credential stub %s: %v", stub.Path, err))
return
}
}

// expandTilde replaces a leading ~ with the user's home directory.
func expandTilde(path string) string {
if !strings.HasPrefix(path, "~/") {
return path
}
home, err := os.UserHomeDir()
if err != nil {
return path
}
return filepath.Join(home, path[2:])
}

// maybeInstallGatewaySkill installs the OneCLI gateway skill file if it is
// missing or stale. agentName is used in user-facing messages.
func maybeInstallGatewaySkill(out *output.Writer, agentName, baseDir, content string) {
Expand Down
24 changes: 16 additions & 8 deletions internal/api/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@ import (
"net/http"
)

// CredentialStub is a stub file that provisioners write so MCP servers
// can boot. The gateway replaces sentinel values at request time.
type CredentialStub struct {
Path string `json:"path"`
Content map[string]any `json:"content"`
}

// App represents an app from the /api/apps endpoints.
type App struct {
ID string `json:"id"`
Name string `json:"name"`
Available bool `json:"available"`
ConnectionType string `json:"connectionType"`
Configurable bool `json:"configurable"`
Config *AppConfig `json:"config"`
Connection *AppConnection `json:"connection"`
Hint string `json:"hint,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Available bool `json:"available"`
ConnectionType string `json:"connectionType"`
Configurable bool `json:"configurable"`
Config *AppConfig `json:"config"`
Connection *AppConnection `json:"connection"`
CredentialStubs []CredentialStub `json:"credentialStubs,omitempty"`
Hint string `json:"hint,omitempty"`
}

// AppConfig is the BYOC credential configuration status.
Expand Down
Loading