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

138 lines
7.1 KiB
Markdown

# 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.
```mermaid
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.
```mermaid
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`:
```jsonc
// ~/.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}`).