Repository navigation
Description
Rooms today have in-process members only: each is bound to a set of workspace documents and answers via retrieval. The larger prize is a member that is a foothold in another environment — a Claude Code session in a different repo, a Codex session on another machine — because that is where the information asymmetry a room depends on is largest. A session holds context that is too large, too live, too private, or too capability-bound to paste into one chat.
Target shape:
you → #api-token-change
Changing the auth token to a JWT. What breaks?
@web 3 call sites assume opaque. src/lib/session.ts:44 parses it as a key.
@mobile We cache in Keychain and never decode — but we pin length at 128.
@infra The ALB caps request headers at 4KB; current p99 is 900 bytes.
Proposed approach: a headless foothold worker
A process run next to a repo that answers a room question by invoking a headless agent CLI (claude -p, codex exec) in that working directory. It captures the foothold — repo, machine, credentials — without needing to interrupt a live session. Reuses AgentWorker and HubClient verbatim, so nothing new goes on the wire.
packages/adapters/src/collab/headless-runtime.ts—HeadlessCliRuntime implements AgentRuntime, injectablespawnFnso it is testable without spawning.apps/web/scripts/collab-foothold.ts— env reader assemblingHubClient+AgentWorker+ the runtime.- Provenance in
AgentTurnResult.meta:cwd,repo.{remote,branch,commit,dirty},hostname,exitCode,durationMs. Commit and dirty refresh per turn — the claim of a foothold answer is "this is what the code says now".
Safety essentials: spawn(command, [...args, prompt], { shell: false }) so an attacker-influenced question lands as one argv element; scrub COLLAB_SECRET from the child env; maxConcurrent: 1 per working directory; time out below the hub's turnTimeoutMs (120s) so completed work is not discarded.
Blocking: hub security floor
No machine outside the operator's trust boundary should join until this lands. Found while planning; all verified against packages/adapters/src/collab/net/.
nodeIdis not in the signed canonical string.signerNodeIdis the unsignedx-collab-nodeheader echoed back, and routes compare it to a body field the same caller controls. Any holder ofCOLLAB_HUB_SECRETcan claim any node identity. Fix: protocol v2 binds it into the MAC; accept v1+v2 during rollout.- No workspace scoping on any hub route. A registered node can read any channel's transcript, append messages under an arbitrary author, and run
start|step|run|pause|resume|takeover|completeon any meeting.Channel.workspaceIdandMeetingConfig.workspaceIdalready exist, so the hub can resolve the owning company without a store-port change — what is missing is node → company, which wants aNodeDirectoryport. - One shared secret for every node. Onboarding an external party means handing it over, and revoking one session rotates everyone's credential. Per-node secrets are what make external participation real.
listKnownNodesbinds any connected node to whichever company's settings page loads first (apps/web/src/server/collab/runtime.ts). This is a live cross-tenant admission hole today, independent of this feature, and worth fixing on its own.GET /collab/v1/healthis unauthenticated and returns the node inventory and every meeting id.
Also worth fixing while in there: servesRemotely never checks lastSeenAt, so a dead node is still elected and stalls a round for the full turn timeout; session.queue grows unboundedly for ghost nodes; and NonceCache.evict returns early below 512 entries and then only removes expired ones, so sustained traffic makes every claim an O(n) scan.
Capabilities gate HTTP actions, not content
Worth stating explicitly, because "ask-only" sounds like more containment than it is. A member holding only an answer capability can still end a meeting by emitting MEETING_COMPLETE (the marker is handed to every worker in the turn context), or redirect the floor by writing @mention under moderated/reactive. Neither is an HTTP request.
Rooms as shipped have neither mechanism — no speaker election, no completion marker, and members never see each other's answers — which is a substantive reason the fan-out shape was chosen first. When deliberation returns, proposesCompletion and mention-nomination need gating on a control capability, and buildTranscriptMessages should mark foreign-origin turns as data rather than instructions.
Alternative considered: MCP server
Lets a live session join from inside its own loop and answer with real conversation history — truest to the goal. Costs: this repo has never shipped a bin, packages/features/src/mcp is an export {} stub whose planned direction is outbound (exposing our engine as tools, the opposite of joining a room), and the agent must choose to poll. Worth doing after the foothold proves the value.
Note the existing agent-knowledge connector deliberately refuses to read session transcripts (projects/, sessions/, history.jsonl are all denied, with a written rationale). Scraping local session files is not the path.
Acceptance
- A second repo on another machine answers a room question with no pasting.
- Each answer carries provenance identifying the repo and commit it came from.
- An instruction planted in a README in that repo cannot read
COLLAB_SECRETor drive room control. - The metric is not answer quality — measurement says that is a wash — but how much context a human did not have to move by hand.
Activity