Skip to content

feat: workstreams W1 — a session gets only its workstream's servers and credentials, and no backend holds a secret - #462

Merged
saucam merged 10 commits into
mainfrom
feat/workstreams-w1
Nov 12, 2026
Merged

saucam merged 10 commits into
mainfrom
feat/workstreams-w1

Conversation

@saucam

@saucam saucam commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #461 (W0). Review that first; this one retargets to main once it merges.

What changes for a user

W1 makes a session's workstream decide what it is given. This is phase 2 of 5 (design). Nothing changes until you configure it, and the server isolation stays off until you set workstreams.isolateGlobalServers: true.

  • Named credentials. credentials[] declares a credential's id and kind (bearer, header, env, oauth), never its value. Values live in a pluggable secret store:
    • env-file (the default) reads CODEOID_CRED_<ID> from the daemon's environment or ~/.codeoid/.env;
    • dir reads one file per id, for example a mounted Kubernetes Secret or a Vault sink.
      Every reference is checked when the config loads.
  • A workstream's env and git identity reach its sessions' backend: for example GH_TOKEN from a credential, and commits as bot@acme.example.
  • MCP isolation (isolateGlobalServers: true). A session reaches built-ins, the servers its workstream names, and its folder's own servers only when the folder belongs to that workstream. A server can be shared across workstreams, with the same or a separate credential each.
    • Claude runs with a strict MCP config: nothing from ~/.claude.json, a .mcp.json, plugins or agents. Every HTTP server is called by the daemon through a bridge. A stdio server that needs no secret is still run by the CLI, inside the sandbox as before.
    • Codex has every server its own configs declare turned off, and its apps and plugins off.
    • Gemini CLI and Qwen are started with only codeoid's server names allowed.
    • opencode turns are refused under isolation for now.
    • No secret on any backend. No backend holds a credential, whether in a header, an env value, a URL or a command line. Servers that need one go through the daemon's relay, or are withheld, and the session is told.
    • Mid-turn revocation. A server removed from a workstream fails on the next call, mid-turn.
  • A second OAuth sign-in to one server, one per workstream (codeoid mcp signin <server> --credential <id>). This is how two Slack workspaces stay apart.
  • Confirmed overrides. Choosing a workstream for a folder another workstream holds exclusively shows what would change. codeoid new asks before going ahead, or --override confirms. The token is single use, bound to the request, needs session:approve, and is audited.
  • The folder record. If a repository's origin later matches another workstream, the next session there must choose. An agent editing .git/config can't silently re-home a folder.
  • codeoid workstream import proposes workstreams from your repositories' remotes and lists your global servers to assign. It writes nothing.

Decisions for reviewers

  • The daemon holds every workstream secret (G1). Backends get references, proxies and bridges. A credential's value is read per request (upstreamFetch), so it never lands in a spec, a settings view, argv or a transcript.
  • No backend can be kept to codeoid's servers? Then it is refused, not trusted. This is opencode for now. Codex fails to start rather than run with a server it can't name.
  • A server config can't name ${CODEOID_CRED_*}. One workstream's secret must not reach a server of another. The rule is deliberately narrow: your own CODEOID_* variables keep working.
  • mcp_oauth_credentials gains a credential slot in its primary key. The table is rebuilt once, in one transaction, and existing rows become the default slot.
  • Not isolated, nothing changes. The relay reaches exactly what it did. There's no strict mode, no new arguments, and the bridges stay as they were.
  • Deferred (design §15): skillDirs (accepted but not applied, with a warning), workstream env on opencode, and MCP prompts and resources through Claude's bridge.

Audit

Two rounds, each with a security lane and a correctness lane. Every fix is folded in with a regression test.

  • Round 1:
    • Codex, the Gemini CLI and Qwen loaded their own MCP configs under isolation.
    • A .mcp.json could read any workstream's credential via ${CODEOID_CRED_*}.
    • A token in a URL or in stdio args counted as secret-free.
    • The folder record stored the chosen home and poisoned later creates.
    • A delegated token could edit an origin and then choose.
    • The relay kept a stale credential.
    • A slot sign-in left that slot's hub client connected.
    • Claude under isolation would have run a repository's stdio server on the host.
  • Round 2:
    • Codex plugins still brought servers. Plugins are now off.
    • Rejecting a changed origin still recorded the new match.
    • A symlinked Codex config.toml (every account overlay) failed isolation.
    • Codex merged a same-named user entry's headers into codeoid's server.
    • The daemon-variable rule was too broad.

Verified

  • Unit: credentials and the secret store, the isolation view, credential references, per-call refusal, Claude strict and bridges, the relay with the session's view, grantFor, Codex isolation, overrides, the folder record, OAuth slots and the slot migration, env reaching the backend, the opencode refusal.
  • Measured against real Codex 0.166: disabling servers by name, placeholder transports, merge semantics, features.plugins.
  • Full daemon suite: 6,092 pass. The 1 failure (inside real bubblewrap > conversations are the session's own) fails identically on main on this machine.
  • e2e: 19 passed, including the new journey: no home under isolation is refused, an override is confirmed with --override, and credentials show by name only.
  • Typecheck (daemon, protocol, core, e2e, web) and lint are clean.
For agents
  • src/daemon/credentials/: schema.ts (credentialDeclSchema, secretStoreSchema, credentialProblems run from RootSchema.superRefine), store.ts (SecretStore, EnvFileSecretStore, DirSecretStore, credentialEnvName).
  • workstreams/grant.ts (McpGrant, grantRefusal), service.ts (grantFor, envFor, #overrideChanges, forCreate(..., required, overridden)), override.ts (OverrideTokens), import.ts (proposeWorkstreams).
  • mcp/session-view.ts: grant option, applyGrant, isolated, relayFor, withheldFromNative, secretFree (URL and args too); registry.ts McpServerView.relayFor/isolated.
  • mcp/types.ts: McpHttpTransport.credential (a reference), isDaemonOwnedEnvName (CODEOID_CRED_); upstream.ts (secrets lookup, McpCredentialMissing); hub.ts (secrets, setPermit, dropTenant with variants); proxy.ts (McpProxyBinding.view, Relay.variant, credential-ref relays); oauth.ts (slotOf, withSlot, slot-aware keyOf, pending slot).
  • store.ts: mcp_oauth_credentials.credential (rebuild migration), workstream_folders.
  • Providers: Claude strictMcpConfig + HTTP bridges (oauth-bridge.ts), stdioOnly; codex isolation.ts (codexIsolationArgs, placeholders, plugins/apps off); acp --allowed-mcp-server-names / qwen allowedMcpServerNames via allowedMcpNames; opencode refused in Session#sendInner.
  • Session: workstreamEnv (merged under extraEnv; GIT_IDENTITY_ENV), mcpGrant, #noteWithheldServers.
  • SessionManager#create: override redeem, workstream_required, the folder record (folderMatch, written after create, only when the match is accepted, never delegated).

🤖 Generated with Claude Code

saucam and others added 7 commits November 12, 2026 09:22
…ore; a home's env and git identity

- credentials[] declares a credential's id and kind (bearer, header, env,
  oauth) — never its value; every reference across the config is checked at
  load (declared once, of a kind that fits)
- credentialStore picks where values live: env-file (CODEOID_CRED_<ID>, kept
  out of every agent subprocess by the CODEOID_ prefix) or dir (one file per
  id: a mounted Kubernetes Secret, a Vault sink; read-only, non-blocking,
  registered as a secret location)
- a session gets its home workstream's env (values read from the store when
  it is built) and git identity; GIT_AUTHOR_*/GIT_COMMITTER_* pass the
  extra-env GIT_ deny, nothing else does; a workstream env name codeoid owns
  for the backend fails the config load

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CP servers, and no backend holds a secret

With workstreams.isolateGlobalServers on (off by default: nothing changes):
- a session's MCP view lists built-ins, the servers its home selects by name,
  and the folder's own servers only when the folder is the home's; no home,
  the conductor's, or an orphaned one: built-ins only — on every backend
- Claude runs with a strict MCP config (no ~/.claude.json, .mcp.json, plugin
  or agent servers of its own); every server it may reach is an in-process
  bridge to the daemon's hub, so it holds no server config, command or secret
- other CLI backends mount natively only servers that need no secret; HTTP
  servers called with a credential go through the daemon's relay, which now
  takes the session's view; a stdio server needing env values is withheld
- a server a workstream selects with a named credential carries a reference;
  the hub and relay read its value from the secret store per request and
  refuse without it
- every hub list and call, and every relay request, is checked against the
  grant as it is now (G4): a removed server fails mid-turn with the reason
- under isolation a user's create in a folder with no home is refused with
  workstream_required; sessions the daemon starts there get built-ins only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… required under isolation

- a create choosing a workstream past another's exclusive claim is refused
  with what an override would change and, when the claimant allows it, a
  token: single use, five minutes, bound to the caller, folder, choice and
  config revision (G3); the confirming retry needs session:approve on an
  undelegated token and is audited (workstream.override); codeoid new shows
  the warning and asks, or --override confirms it
- how each folder last resolved is recorded per tenant: a repository rule that
  now matches another workstream (an edited origin) makes the next create
  choose, and leaves a session the daemon starts there without a home
- under isolation a user's create in a folder with no home is refused with
  the workstreams to choose from

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e server, one per workstream

- mcp_oauth_credentials gains a credential slot in its primary key, rebuilt
  once in one transaction; every existing row is the default slot ('')
- the OAuth engine reads, refreshes, stamps and signs out per slot; a pending
  sign-in carries its slot, so a completion always lands in the slot it began
  for
- a workstream's server with an oauth credential is called with that slot's
  sign-in (on the hub, the Claude bridge and the relay), never the default
- mcp.oauth.begin / disconnect and codeoid mcp signin --credential take a slot,
  refused unless it is declared as an oauth credential for that server

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the docs

- codeoid workstream import proposes workstreams from the repositories on
  disk (grouped by remote owner) and lists Claude's global MCP servers to
  assign; it writes nothing
- skillDirs are accepted but not applied yet, and the daemon says so
- e2e: under isolation a create with no home is refused with the choices; an
  exclusive folder takes only a confirmed override; credentials show by name
- docs/CONFIGURATION.md: credentials, the secret store, what a workstream
  gives its sessions, overrides, moving an existing setup; the design's
  backlog and status

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rvers, no daemon variable in a server config, an honest folder record

Security:
- Codex, the Gemini CLI and Qwen loaded MCP servers of their own under
  isolation. Codex now has every server its user and project configs declare
  turned off (with a placeholder transport, so an untrusted project file still
  starts; measured on codex 0.166) and its apps off; the Gemini CLI and Qwen
  get only codeoid's server names allowed; opencode's turns are refused under
  isolation until its project config can be kept out
- ${CODEOID_*}, ${ZEROID_*}, ${TELEGRAM_*} in a server config resolve empty, and
  such a bearerTokenEnv is ignored: a repo's .mcp.json could read any
  workstream's credential from the daemon's environment
- secretFree also reads the URL (a token in its path or query, user:pass@)
  and the command line: such servers go through the relay or are withheld
- Claude under isolation bridges only HTTP servers; a stdio one needing no
  secret is the CLI's to run inside the session's sandbox, as before (the hub
  would have run a repo's script on the host)
- a delegated token can't edit an origin and then choose the workstream it
  now matches, and never writes the folder record
- the hub's per-call check refuses an unknown session under isolation; a
  relay whose credential changed is closed (the backend initializes again);
  a slot sign-in or sign-out drops that slot's hub clients too
- override tokens go only to callers who can redeem them

Correctness:
- the folder record holds what the folder matched, never a chosen home, and
  is written once the session exists: an explicit choice no longer makes the
  next plain create in that folder refuse
- an override token is spent only once its session exists
- not isolated, the relay reaches exactly what it did before
- a server address change rebuilds the Claude query
- servers withheld from a backend are said in the session, once per set
- --override without --workstream is refused with why

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anged origin sticks, the daemon-variable rule narrowed

- codex plugins bring MCP servers that can't be turned off by name: under
  isolation plugins are off whole (and /etc/codex layers are read too)
- choosing a workstream other than the folder's match (rejecting a changed
  origin included) no longer records the new match; the record changes only
  when the owner accepts the match
- codex's home config.toml is read through a link (account overlays link it;
  isolation failed to start for every codex account); a project's is not
- a codeoid server named like one of codex's own entries is refused: codex
  merges the two, lending codeoid's server that entry's headers or env
- the rule against naming daemon variables in a server config is narrowed to
  workstream credentials (CODEOID_CRED_*): a user's own CODEOID_* variable in
  their own config resolves as before
- an override token is spent up front again: single use under concurrent creates
- tests: codex plugins/links/merged names, the Gemini/Qwen allowlist, the
  opencode refusal, the record after a rejected change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@saucam
saucam force-pushed the feat/workstreams-w1 branch from bd136f7 to a4a0ea7 Compare November 12, 2026 01:23
@saucam
saucam changed the base branch from feat/workstreams-w0 to main November 12, 2026 01:23
@saucam saucam closed this Nov 12, 2026
@saucam saucam reopened this Nov 12, 2026
@saucam

saucam commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author
pr-shepherd: stopped at iteration 1 (test failure, nothing pushed)
  • Failing: daemon-check. 1 of 6239 tests fails: src/daemon/providers/codex/isolation.test.ts:43, "codex under workstream isolation > fails closed: a name it can't write as a key, invalid TOML, or one server declared as both kinds".
  • Passing: web-check, e2e, Socket Security (both).
  • Not one of the known main failures. This test is new in this PR.
  • Cause: CI runs Bun 1.3.13 (from .bun-version). On that version Bun.TOML.parse("[mcp_servers.x\n") returns {"mcp_servers":{"x":{}}} instead of throwing. Bun 1.4.4 throws, so the test passes locally. The fixture isn't invalid TOML for the pinned runtime. The codex config is then treated as declaring server x (it gets disabled) and no fail-closed error is raised.
  • Why I stopped: this is a test failure, and fixing it is a judgment call. One option is a fixture that 1.3.13 also rejects. Another is to bump .bun-version. A third is to decide whether the fail-closed guarantee needs a stricter check than Bun.TOML.parse on 1.3.x.

…er to the runtime parser's leniency

Bun 1.3.13 (the pinned runtime) reads an unclosed header such as
`[mcp_servers.x` as a table where 1.4 refuses it, so on CI a malformed
codex config started codex instead of being refused. tomlStructureProblem
checks every table header (outside strings and open values) and that
nothing is left open, before the parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@saucam

saucam commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

CI is green after one follow-up commit (770753e6). The pinned Bun 1.3.13 parses an unclosed TOML table header such as [mcp_servers.x as a table, where 1.4 refuses it. So on our runtime, a malformed codex config would have started codex instead of being refused. Codex isolation now checks the config's table headers itself (tomlStructureProblem) before it parses. The full unit suite at the top of the stack also passes under 1.3.13.

…web UI (#463)

* feat: workstreams W2 — edit workstreams and credentials live

- workstream.upsert / delete / settings and credential.list / upsert /
  delete / set_value: settings:write on an undelegated token; written to
  config.json through the settings store (the whole config validated, nothing
  written otherwise) and applied to the running daemon at once — new sessions
  and every session's grant follow it, live sessions keep their homes;
  workstreams.changed announces it
- a credential's value goes to .env (0600) and the daemon's environment and
  is never sent back: only whether it is set, and where; an OAuth one points
  at codeoid mcp signin; deleting one a workstream uses is refused
- a session's MCP view fingerprint includes its grant: a workstream edited
  rebuilds that session's backend at its next turn, on every backend

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: workstreams W2 — the web UI: Workstreams and Credentials in Settings, a picker and override confirm in New session, a chip on every session

- Settings → Workstreams: isolation switch, default, the list (claim, share,
  rules, servers, env), an editor that keeps what it doesn't show (the item
  as configured, not bases merged — WorkstreamSummary.config), remove, and
  'check a folder'
- Settings → Credentials: declare by id and kind, set a value from a password
  field (sent once, cleared, never shown back), clear, remove (refused while
  used), OAuth slots point at codeoid mcp signin
- New session: what the folder resolves to and why, an explicit choice; a
  choice past an exclusive folder is offered with what it would change, and
  Start anyway resends with the override token
- a workstream chip in the session header and the session list (orphaned red,
  override amber); the list filter matches workstream names
- live: workstreams.changed refetches what's shown

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: workstreams W2 — the UI journey with screenshots at every viewport; fixes they showed

- e2e: Settings lists and adds a workstream, a credential value is set and
  never shown back, the new-session dialog says what the folder resolves to,
  a choice past an exclusive folder is offered and confirmed, and the chip
  shows it — screenshots at 375x667, 390x844, 768x1024, 1280x800, 1470x830,
  1920x1080 and 1280x300, the override's buttons asserted inside the window
- the Default select no longer overflows a phone's width; the override offer
  scrolls into view as it appears and says what the choice gets accurately

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: workstreams W2 audit round 1 — edits apply both blocks, live env, a header is part of a credential, editor survives refreshes

Daemon:
- an edit applies both the workstreams and the credentials block from the
  file as written: a hand edit since boot is never shadowed by a stale copy
- a session's workstream env is read live: a credential set, swapped or
  cleared, an env entry removed, reaches the backend at its next start, and
  a changed env rebuilds the backend at the next turn like its servers
- a credential's header is part of its hub client and of the per-call check:
  redeclaring it with another header is another credential
- credential.delete takes a strict id and refuses an undeclared one; deleting
  an OAuth slot signs it out; a value set in the daemon's real environment is
  refused (it would win at the next start); an edit's reply needs no
  settings:read; the revision covers credentials too

Web:
- workstream rows are keyed by id: an open editor keeps what was typed when
  the list refreshes (another client's edit, a toggle)
- a forced refresh during a fetch refetches after it, so the newer list wins
- the editor checks env and server lines before sending; failed settings saves
  put the control back; remove and clear confirm, with Cancel, busy-guarded;
  the credential field uses new-password
- New session: the last folder's answer never shows for a new one, a failed
  check says so, the override offer names workstreams as the picker does and
  clears on a mode switch, focus moves to create after choosing the folder's,
  and workstream_required is said in the UI's words with the picker focused

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: workstreams W2 audit round 2 — env changes rebuild a backend without MCP servers, shadowed .env values refused, an OAuth slot's removal signs every tenant out

- A session with no MCP view never rebuilt its backend when its workstream's
  env changed: #applyMcpView returned early. The env check now runs before
  the view check, and #envBuiltWith keeps a sha256, not the values.
- loadDotEnv records the keys the real environment shadowed; set_value
  refuses those too, as it did a key set only in the real environment.
- Removing an OAuth credential forgets its slot for every tenant and URL
  (Store.clearMcpOAuthSlot, McpOAuth.forgetSlot), never the default sign-in.
- Credentials rows keyed by id, so a refresh keeps a typed value.
- New-session dialog: the workstream picker in collaborative mode; pipeline
  runs get UI-worded workstream errors (showCreateError).
- The dialog's deferred focus no longer moves focus already inside it
  (a fast fill typed the workdir into the name: the e2e flake).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s by share policy (#464)

* feat: workstreams W3 — a workstream's folders, a session's read fence, share verdicts for references and moves

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: workstreams W3 — the read fence: sandbox roots and hidden paths from the session's workstream, file tools refused in another workstream's folders

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: workstreams W3 — episodes record their workstream; recall, timeline and get_episode keep to the home and its references

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: workstreams W3 — references at create and mid-session by share policy, moving a session to another workstream

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: lint — no assignment in an expression

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: workstreams W3 — CLI and web UI for references and moves; docs

- codeoid new --ref, codeoid workstream ref add|rm, workstream move
- the session header's chip opens what the session reads and a move; the
  new-session dialog offers what it may read (under isolation)
- a fresh session's sandbox no longer says its directories changed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: lint — plain strings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: a test's memory stub has the session scope W3 registers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: workstreams W3 audit round 1 — every other workstream's folder is fenced, with what the session reads carved out; its own workdir always readable

- fenceFor: no foreign folder is dropped for being nested in the home's
  or holding it; WorkstreamFence.readable (workdir, home, exactly what a
  reference grants) is carved out — the gate checks it, the sandbox hides
  every other entry on the way (carveOut), else the folder whole
- the home is never a workstream's folder, nor a repository top above it
- a session's own workdir stays readable after a move or an override
- extraction: a glob's partial last segment, ACP locations, pi's @ and
  find/ls; a relative path with an unknown cwd is refused
- an added sandbox dir inside a fenced folder is refused and never kept
- reference inputs held to the binding's own bounds; an unreadable stored
  binding seals the session (no home, never moved)
- the session map and the frozen memory index keep to the workstream
- the chip's panel takes focus and follows the window

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: workstreams W3 audit round 2 — monorepos keep to the package, granted paths where they really are, changes at the turn boundary

- a session's repository top is its own only where no other workstream's
  folder holds it; a recorded home widens to its top only where no other
  workstream has a folder inside it; folders are no longer collapsed
- reference paths resolved at grant, and counted only where they really
  are inside the granting workstream (no .., no link planted later)
- Session.setWorkstream saves at once and applies at the next turn's start
- a move adds the old home to readFrom; a home in another workstream's
  folder is judged by that one's share (closed refuses); the claim and the
  bounds are judged again when an ask is answered
- the gate's folder forms are prepared once per fence; a session's own
  folder may be written though a reference holds it
- globs judged at their start and their dir; pi's @ only before a path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: workstreams W4 — the conductor reads across workstreams without a closed one's text, and acts through workers homed where the work belongs (#465)

* feat: workstreams W4 — the conductor reads across workstreams without a closed one's text, and acts through workers homed where the work belongs

- fleet tools name each result's workstream and take a workstream filter;
  a closed workstream's episode text never reaches fleet_find, fleet_summary,
  fleet_recall or a worker's digest (status only)
- fleet_spawn takes workstream: judged before queueing as a create is
  (a folder with none needs one; an exclusive folder is an override the
  approval card says; a closed one's folder never), stored on the task and
  judged again at claim
- every send-class approval card (web, Telegram) names the workstream it
  acts in, with the override warning approving it confirms
- egress stub: planEgress (a message is sent by a worker homed in its workstream)
- the conductor recalls its own episodes under isolation
- the fleet rail docks from md up, so a phone's approval card has the width

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: workstreams W4 audit round 1 — untagged text judged by its session, confirmations tied to the card the owner approved

- untagged episodes are withheld by their session's workstream (live or
  stored), unknown ones while any workstream is closed; a closed one's hits
  are dropped from fleet_find / fleet_recall before anything is counted;
  machine_map leaves out its branches
- a worker's digest is withheld from the conductor only; the conductor's
  tenant-wide task board withholds an orchestrator's closed results
- an override or an ask folder counts as confirmed only by the owner's own
  approval of that very card, once, at that config revision (checked again
  at claim); a delegated token can't approve such a card
- an unnamed spawn keeps the remote-changed guard; a closed workstream's
  worker works only in its own folders
- codeoid approve and the terminal show the card's workstream

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: workstreams W4 audit round 2 — a confirmation is a one-time token on the approved call, checked against exactly what its card showed

- the owner's own approval of a card that needed confirming adds a token to
  that call's input; its handler accepts it once, for the tenant, workdir,
  workstream, conflicting workstream, ask folders and revision the card was
  judged on (no leftover, stale or denied approval confirms anything)
- only the session owner's own token confirms (not another member's)
- the warning names the workstream it passes; the TUI shows it too
- conductor tasks known by the routing rule (no goal), not today's URI
- a stored session judged by its recorded home; answers memoised briefly
- an unnamed spawn's claim judged as when queued; a closed workstream with
  only remote rules may work in a folder it matches

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: the W4 e2e follows the warning that names the workstream passed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@saucam
saucam merged commit 1bb6a7e into main Nov 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants