Skip to content

Agent Launch, CLI Probing & Managed Accounts

dazeb edited this page Sep 17, 2026 · 1 revision

Agent Launch, CLI Probing & Managed Accounts

This slice is the launch contract between termsprawl and the external agent CLIs it hosts. It answers four separate questions:

  1. What agent preset exists? src/shared/agents/config.ts declares ids, commands, titles, and capability flags.
  2. Where is the executable? src/core/command-resolver.ts turns claude, gemini, druk, etc. into absolute paths, including the GUI-minimal-PATH fallback.
  3. What can this installed CLI actually do? src/core/agent-tool-launch.ts probes --help/--version, selects a launch adapter, and src/core/agent-cli.ts contains pure help-text predicates for Claude-specific behavior.
  4. Which credentials does the spawn use? src/core/agent-accounts.ts isolates managed accounts into <userData>/accounts/<id> and strips inherited auth environment variables.

The files here are core/shared helpers: mostly Electron-free and Node-filesystem/child-process based. The actual PTY/tool-server caller is not in this excerpt, so the call order below describes how the exported APIs are designed to compose, not a complete main-process trace.

Runtime path

The launch flow is deliberately staged. Registry data cannot know whether a CLI is installed; probing cannot know whether the user picked a managed account; account isolation must happen at spawn time, after executable resolution and argument construction.

flowchart TD
  A[AgentConfig from AGENT_REGISTRY] --> B[agentCommand / config.command]
  B --> C[findExecutable]
  C -->|resolved absolute path| D[probeAgent: --help and --version]
  C -->|null| E[unresolvedNotice / missingCommandExec]
  D --> F[selectLaunchAdapter]
  F --> G[prepareToolLaunch]
  H[installToolRuntime: client.mjs + termsprawlctl] --> G
  I[Active managed account] --> J[claudeConfigEnv + stripAuthEnv]
  J --> G
  K[claude --help text] --> L[claudeSupportsPermissionMode / claudeLoginArgs]
  G --> M[shell command + env + IntegrationStatus]
Loading

Key nodes:

  • AgentConfig.command is only a binary name. It must pass through findExecutable before spawning.
  • probeAgent executes the resolved binary directly with execFileSync, not through a shell.
  • selectLaunchAdapter uses both the executable basename and the probed help text; a preset name alone is explicitly insufficient.
  • installToolRuntime produces the launcher that prepareToolLaunch writes into MCP config or injects through environment variables.
  • Managed account handling is separate from launch construction and happens through CLAUDE_CONFIG_DIR plus auth-env stripping.

File responsibilities and collaboration

src/shared/agents/config.ts — preset registry

This is the single source of truth for agent presets. AgentId is a closed union: claude, codex, gemini, grok, openclaude, custom. AGENT_REGISTRY maps each id to an AgentConfig with display name, node title, command, enabled flag, integration metadata, and an AgentCapabilities record.

Important boundaries:

  • command is resolved to an absolute path at spawn time; the registry intentionally stores the logical name.
  • gemini is kept as the command/id even though the displayed product is Antigravity. Persisted canvas nodes keep their agent identity, and executable resolution handles agy/antigravity aliases later.
  • custom is a template, not an enabled preset.
  • capabilities is a declaration, not runtime probing. For example, gemini, grok, and openclaude have hooks: false because no hook normalizer exists for them in the hook-server path. Flipping those flags requires adding the normalizer/installer, not just changing booleans.
  • integration.transcriptReader is currently claude-jsonl only for Claude.
  • Helpers agentIds, agentConfig, agentName, agentTitle, and agentCommand are the read API; callers should not reach into the object shape directly.

src/core/command-resolver.ts — executable resolution and missing-command handling

This module exists because desktop-launched apps often inherit a minimal PATH. findExecutable(name, home) checks, in order:

  1. Already-absolute names are returned as-is.
  2. Each non-empty directory in process.env.PATH, split on :.
  3. Known user-local install directories: ~/.local/bin, ~/.druk/bin, ~/bin.
  4. Legacy aliases from COMMAND_ALIASES; currently only gemini → agy, antigravity.

resolveCommandLine preserves trailing arguments: it resolves only the first token, then reattaches the rest. If the token cannot be resolved, it returns the original line so the shell can report the failure.

The failure path is split into two helpers:

  • unresolvedNotice returns a user-facing explanation for a missing first token, or null if the line is empty, absolute, or resolvable.
  • missingCommandExec wraps that notice in exec /bin/sh -c ...; exit 1 so the message is printed by a non-interactive shell instead of being parsed by the interactive line editor as unmatched quotes or continuation prompts.

This resolver is POSIX-oriented in the fragment: it assumes :-separated PATH and /bin/sh. There is no Windows PATHEXT or ; handling here.

src/core/agent-tool-launch.ts — probing, adapter selection, and spawn argument construction

This is the runtime compatibility layer. It collaborates with command-resolver and the agent-tools module. probeAgent(command) resolves the executable, caches by absolute executable path, then reads --help and --version with a 3s timeout, 256 KiB max buffer, and stderr ignored. Failures become empty strings; version becomes unknown.

The cache is process-local and keyed only by executable path. A binary replaced at the same path will not be re-probed until the process restarts.

selectLaunchAdapter(probe) maps observed help text to a launch strategy:

Observed executable/help Adapter kind Native MCP Instruction route
claude with --mcp-config and --append-system-prompt claude-mcp yes --append-system-prompt
codex with --config and mcp in help codex-mcp yes none selected
agy / antigravity / gemini with --prompt-interactive <name>-cli no --prompt-interactive
gemini / agy / antigravity / grok / openclaude with [PROMPT] or [prompt] <name>-cli no initial prompt argument
anything else <name>-unverified no none

Precedence matters: the --prompt-interactive branch is checked before the generic [PROMPT] branch.

installToolRuntime(directory, executable, bundle) creates a private runtime directory, writes client.mjs from the supplied bundle, and writes an executable termsprawlctl launcher that runs the provided executable with ELECTRON_RUN_AS_NODE=1 against client.mjs. Directory modes are 0700; runtime and config files are 0600.

prepareToolLaunch(options) is the spawn builder. It:

  • Validates an optional customInstructionFlag against ^--[a-z][a-z-]*$.
  • Creates the runtime directory and writes one skill folder per TOOL_GUIDES topic as skills/termsprawl-<topic>/SKILL.md.
  • For claude-mcp, writes mcp.json with a termsprawl MCP server entry and appends --mcp-config <path>.
  • For codex-mcp, appends -c mcp_servers.termsprawl.command=... and -c mcp_servers.termsprawl.args=....
  • Appends either adapter.instructionFlag <orientation>, a bare orientation prompt for initialPrompt, or nothing.
  • Returns a shell command string, environment entries (TERMSPRAWL_SESSION_FILE, TERMSPRAWL_CTL), and an IntegrationStatus.

The returned command is constructed as shellQuote(probe.executable) + commandTail + quoted args. Therefore commandTail is inserted raw; the caller is responsible for making it safe. The adapter’s optional custom instruction flag is the only part validated locally.

The status state is derived from adapter availability:

  • Native MCP adapters produce state: 'needs-setup' with reason “Waiting for the agent to connect to MCP”.
  • CLI fallback adapters with an instruction route produce state: 'cli-fallback' and a warning that shell tool access depends on agent permissions.
  • Unverified adapters produce state: 'needs-setup' and tell the user to read generated skills and configure explicitly.

TOOL_GUIDES and IntegrationStatus come from ./agent-tools, which is outside this excerpt.

src/core/agent-cli.ts — pure Claude help-text predicates

This small module is intentionally Electron-free and testable against fake claude --help output. It does not spawn anything. It interprets help text produced by a probe:

  • claudeSupportsPermissionMode(helpText) returns whether --permission-mode appears.
  • claudeLoginArgs(helpText) returns ['auth', 'login'] if auth login appears, ['/login'] if /login appears, otherwise [].
  • claudeLoginCommand(helpText) builds claude auth login, claude /login, or bare claude.

These predicates are useful when a caller has help text, but the fragments do not show a direct call site wiring probeAgent().help into this module. Treat that as a composition point to check before modifying login or permission-mode behavior.

src/core/agent-accounts.ts — isolated config-dir accounts

Managed accounts are directories under <userData>/accounts/<id>. The app does not store tokens; it only creates the directory that the agent CLI treats as its config home. For Claude, that is CLAUDE_CONFIG_DIR.

The module exposes:

  • accountConfigDir(userData, id) for path derivation.
  • newAccountId() for collision-resistant ids shaped like acc-<base36-time>-<random>.
  • createManagedAccount(userData, label) which creates the directory and returns { id, label, configDir }.
  • deleteManagedAccount(userData, id) which removes the directory recursively and fails open if the directory is already gone.
  • claudeConfigEnv(configDir) which returns { CLAUDE_CONFIG_DIR: configDir }.
  • stripAuthEnv(env) which removes ANTHROPIC_API_KEY, CLAUDE_API_KEY, and ANTHROPIC_AUTH_TOKEN from a copy of the environment.
  • activeAccount(settings) which reads settings.activeAccountId and finds the matching member of settings.accounts; otherwise it returns undefined, meaning the caller should fall back to the default ~/.claude.

The security boundary is important: because the managed config directory must be the only credential source for a managed spawn, inherited auth environment variables are stripped before spawn. A stray API key in the shell cannot leak into the managed account. stripAuthEnv only covers the three listed keys; additional provider variables would need to be added deliberately.

Creating and deleting accounts only manipulates files. Persisting or removing entries from settings.accounts is outside these functions. Deletion also explicitly expects the caller to surface data loss in the UI before removing the directory.

State, edge conditions, and extension points

Key state:

  • Registry state is static in AGENT_REGISTRY.
  • Probe state is process-local in probeCache, keyed by absolute executable path.
  • Account state is split between the filesystem (<userData>/accounts/<id>) and settings (activeAccountId, accounts).
  • Launch state is returned per call as { command, env, status }.

Important edge conditions:

  • GUI-launched apps may not see user install directories on PATH; the resolver compensates with ~/.local/bin, ~/.druk/bin, and ~/bin.
  • Absolute command names bypass all resolution.
  • Legacy aliases are only consulted if the original name is not found.
  • Missing preset commands should use unresolvedNotice and missingCommandExec, not raw injection into an interactive shell.
  • probeAgent failures are silent empty strings; adapter selection will fall back to *-unverified.
  • selectLaunchAdapter is heuristic and help-text dependent. A CLI rename or help-text change can move it to a different branch.
  • prepareToolLaunch writes files with restrictive modes and expects a secure launcher path.
  • commandTail is not quoted by prepareToolLaunch; only the resolved executable and generated args are quoted.
  • Managed-account deletion is fail-open and does not clean settings.
  • The managed-account implementation is v1 Claude-only for config-dir semantics. The creation/id/directory helpers are generic, but claudeConfigEnv and AgentAccount.agentId scope are not.

Extension points:

  • Add a new agent by extending AgentId, adding an AgentConfig entry, declaring honest capabilities, and deciding whether command-resolver needs an alias.
  • If hooks: true is set for an agent, a matching hook normalizer and installer must exist; the registry comments explicitly call this out.
  • Add a new runtime adapter by extending selectLaunchAdapter and teaching prepareToolLaunch how to write or inject that adapter’s MCP/instruction configuration.
  • Add new CLI probes in agent-cli.ts as pure functions over help text; keep them spawn-free and fixture-testable.
  • Add new managed-account auth variables to AUTH_ENV only when the isolation boundary is understood.
  • Add a new command alias in COMMAND_ALIASES when a CLI rename must not break persisted project files.

Limits of this excerpt: the actual PTY spawn, settings persistence, hook server, and agent-tools guide/status definitions are not included here. The behavior above is limited to the five provided source fragments.

Sources: src/shared/agents/config.ts, src/core/agent-cli.ts, src/core/command-resolver.ts, src/core/agent-tool-launch.ts, src/core/agent-accounts.ts

termsprawl

App Shell & Platform Foundations

Canvas, Nodes & Renderer State

Terminals & Session Continuity

Persistence, Projects & Files

Agent Runtime & Tooling

Chat Nodes & Model Providers

Git & Source Control

Embedded Browser Nodes

Server Edition

Relay & Remote Access

Integrations & Secondary Surfaces

Settings, Updates & Maintenance

Clone this wiki locally