Files
fleetd/docs/Worker-Startup-and-Trust.md
Dai Ha ecc590f344 CB-632 unit 5: rename the daemon and classes in the docs prose
Part of #145 (CB-632). Documentation only, plus one internal literal.

Unit 1 renamed the package and classes, which left every doc describing
classes that no longer exist. This fixes the prose across README.md,
docs/ and bridged/docs/ -- 18 files.

Renamed: dev.ltms.bridged -> dev.ltms.fleet, the five class names, and
"bridged" where it names the daemon as a product rather than a path.

Also renamed two literals, because a doc that disagrees with the code is
worse than one that is out of date:

  - bridged-local-noauth -> fleetd-local-noauth. A placeholder apiKey
    OpenCodeLauncher sends when a profile resolves no token, to a local
    endpoint that does not check it. No test asserts the old string.
  - the vnd.ltms.bridged.* media type in the M4 design doc. It appears
    in no Java file, so nothing implements it yet.

Deliberately NOT renamed, because each is still literally true today and
changes only at the cutover:

  - paths: bridged/, bridged.yaml, bridged.example.yaml, bridged.jar,
    .bridged-worktrees, deploy/dev.ltms.bridged.plist,
    scripts/redeploy-bridged.sh, bridged-launchd-wrapper.sh
  - bridged_* metric names -- renaming these after the monitoring is
    wired would break dashboard continuity, so they move before it is
  - bridge_* MCP tool names, which answer alongside fleet_* on purpose
  - BRIDGED_* environment variables, read by a file outside this repo

Method note: perl, not sed. BSD sed has no \b and no lookaround, and a
word-boundary expression there fails silently. The prose replace uses
(?<![\w./-])bridged(?![\w./-]) so it cannot touch a path or an
identifier, then every remaining hit was read by hand.

Verified: mvn clean install green, 51 classes, 878 tests, 0 failures.
2026-08-23 06:46:34 +02:00

7.1 KiB

Worker startup: working directory & the folder-trust prompt

When fleetd spawns a worker, the worker CLI may show an interactive startup prompt before it is ready to accept a task — most importantly a "Do you trust the files in this folder?" dialog. An unattended worker parked on that prompt never becomes injectable: the status-gated injector waits for idle/blocked, the task is never delivered, and (worst case) a stray Enter answers the dialog wrong. How this is handled depends on two things:

  1. The worker's working directory — which folder the CLI is asked to trust.
  2. Which CLI launches the worker — each has its own first-run/trust behaviour.

This doc pins the current assumption (ccs is the launcher), how its trust prompt works, the rule that a worker inherits the primary's directory (never $HOME), and how other CLIs differ.

flowchart TD
    A["fleet_spawn / POST /workers"] --> B{"explicit cwd?<br/>(profile cwd or spawn arg)"}
    B -->|"yes — told otherwise"| C["use that cwd"]
    B -->|"no"| D{"caller PID resolvable?<br/>(MCP peer PID)"}
    D -->|"yes"| E["cwd = the primary's cwd<br/>lsof -a -p PID -d cwd"]
    D -->|"no (REST / off-host)"| F["cwd = fleetd daemon cwd<br/>(never $HOME by assumption)"]
    C --> G["ensureWorkspace → tab.create → agent.start {cwd}"]
    E --> G
    F --> G
    G --> H{"does the CLI trust this folder?"}
    H -->|"yes"| I["worker reaches its prompt → injectable"]
    H -->|"no"| J["worker BLOCKS on the trust dialog<br/>never injectable"]
    classDef good fill:#2f855a,stroke:#22543d,color:#ffffff;
    classDef bad fill:#9b2c2c,stroke:#63171b,color:#ffffff;
    class I good
    class J bad

Figure 1 — spawn resolves a working directory, then the CLI's trust check gates readiness.

Working directory: inherit the primary's path

Rule: a worker opens the same directory the primary (main) session is working in, unless told otherwise. Never assume $HOME. If the primary is in /Users/you/LTMS/claude-bridge, its workers open there too — so delegated tasks share the same relative paths and the same (already-trusted) project folder.

The herdr seam. An agent.start pane does not inherit its tab's or workspace's cwd — it starts in $HOME unless told otherwise. herdr's agent.start honours an (undocumented) cwd param, verified live: setting it roots the worker process at that directory. So the worker's cwd is threaded onto agent.start {…, cwd}, not the placement step (workspace.create/tab.create cwd only affect the seed shell, which the bridge closes).

Resolution order (first match wins):

# Source When
1 Explicit cwd — a per-profile cwd: in config, or a spawn argument "told otherwise" — pin a fixed workdir
2 The primary's cwd, auto-detected from the fleet_spawn caller normal MCP spawn from the primary
3 The fleetd daemon's own cwd REST spawn / off-host caller — never $HOME

The primary's cwd (source 2) is discoverable with no new plumbing: fleetd already resolves the MCP caller's loopback peer PID for connection identity (ConnectionIdentity → LsofPeerPidLookup); the same PID yields its cwd via lsof -a -p <pid> -d cwd -Fn (the n… line). The primary maps to no worker pane (it is not a worker), but its PID and cwd are still readable.

sequenceDiagram
    participant P as "Primary (main)"
    participant B as "fleetd"
    participant O as "OS (lsof)"
    participant H as "herdr"
    P->>B: "fleet_spawn {profile} (no cwd)"
    B->>O: "peer PID for this connection's port"
    O-->>B: "pid"
    B->>O: "cwd of pid (lsof -d cwd)"
    O-->>B: "/Users/you/LTMS/claude-bridge"
    B->>H: "tab.create (placement)"
    B->>H: "agent.start {argv, env, tab_id, cwd}"
    H-->>B: "worker in the primary's directory"

Figure 2 — a no-cwd spawn inherits the primary's directory from the caller's PID.

Status: implemented (CB-112). fleetd threads the resolved cwd onto agent.start {cwd} (verified: the worker process is rooted there), keeping the single shared worker space. On an MCP fleet_spawn the primary's cwd is auto-detected from the caller's PID; over REST (no MCP caller) it is the explicit cwd param else the daemon's cwd. Both placements (tab and legacy pane) carry it, since it rides agent.start.

Assumed launcher: ccs (Claude Code)

For now the fleet assumes ccs (Claude Code under the hood) as the worker CLI — argv: ["ccs", "<profile>"]. Its startup gate is the folder-trust dialog.

How ccs/Claude Code decides whether to prompt

Trust is recorded per-directory, per config dir. Each ccs profile is an isolated instance with its own config dir (~/.ccs/instances/<profile>/) and its own .claude.json:

// ~/.ccs/instances/<profile>/.claude.json
"projects": {
  "/Users/you/LTMS/claude-bridge": { "hasTrustDialogAccepted": true },   // trusted → no prompt
  "/Users/you":                     { "hasTrustDialogAccepted": false }  // untrusted → prompts
}

The worker prompts iff its cwd is not marked hasTrustDialogAccepted: true for that instance. This is why the directory rule above matters: land workers in the primary's project folder and you grant trust once per profile, instead of scattering trust across $HOME and ad-hoc dirs.

Clearing the prompt (ranked)

  1. Inherit the primary's project dir (the rule above) and trust that folder once per profile.
  2. Pre-trust interactively: run ccs <profile> in the target folder and accept — persists hasTrustDialogAccepted: true for that path in the instance's .claude.json.
  3. Set the flag directly (scriptable, no interaction): set projects["<cwd>"].hasTrustDialogAccepted = true in ~/.ccs/instances/<profile>/.claude.json.
  4. Do not reach for --dangerously-skip-permissions — it disables all permission gating, not just this dialog, which defeats running off-subscription workers autonomously.

Other CLIs: different launchers, different prompts

ccs is the current assumption, not a hard dependency — a worker profile's argv can be any CLI. Each launcher has its own first-run/trust gate, so the "clear the prompt" step is CLI-specific and belongs with the profile, not hard-coded:

Launcher (argv) Startup gate How to clear it
ccs <profile> (Claude Code) Folder-trust dialog hasTrustDialogAccepted per project in the instance's .claude.json (above)
Other Claude-compatible runtimes via ccs (codex, gemini, cursor, …) Each has its own first-run / trust / login prompt Per-runtime; document per launcher as it is adopted
A bare command (bash -c …, mechanics probe) None n/a — used for non-interactive smoke tests

When adding a new launcher, capture its startup-prompt behaviour here (what blocks, and the non-interactive way to satisfy it) so a spawned worker of that kind reaches an injectable prompt unattended.

See also

  • docs/MCP-Contract.md — the tool surface (fleet_spawn, fleet_profiles, …).
  • wiki/2-Message-Server.md — the herdr agent.* / workspace.* schema (workspace.create {cwd}).