ecc590f344
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.
138 lines
7.1 KiB
Markdown
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}`).
|