refactor(orchestration): resolve Run callers through one principal resolver (2/6) - #19949
brennanb2025 wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
resolveCallerPrincipalresolver — newcaller-principal.tscollapses every credential form (term handle,structworker_bearer, declared session fields) into one shape and enforces the bearer-only session invariant.- Run method conversion —
runCreate/runUse/runCurrentconsumeprincipalId+binding+waiterHandleswhole and never branch on principal kind. - Principal-aware DB layer —
isEquivalentPrincipal, therunCoordinatorBindinglegacy normalizer, and principal-scoped bind/unbind/lookup (bindRun,createRun,runsBoundToPrincipal,unbindOtherRunsForPrincipal). resolveRunScopere-routed — now resolves throughresolveCallerPrincipal, threading dormantcallerAgentSessionId/callerRuntimeFencefields for later PRs.- Tests — 595 added lines across 4 spec files pinning the cross-kind invariant, bearer-only authentication, fence-0 acceptance, and remint stability.
The resolver layer is sound: declared session fields never authenticate (bearer possession is the gate), the pane↔session non-bridging invariant holds in both argument orders, and the reversal of from from requiredString to OptionalString plus the two new optional fields is a Rule 1 wire-compatible change under docs/reference/remote-wire-compatibility.md.
ℹ️ resolveRunScope now routes non-Run methods through the session resolver
resolveRunScope is shared by check/send/reply/gate/dispatch and the message methods, and it now calls resolveCallerPrincipal with newly added but as-yet-unpopulated callerAgentSessionId/callerRuntimeFence fields. A structworker_ handle passed as --from to any of those methods today therefore resolves as a session principal (bearer-gated, no corroboration forwarded) instead of failing with stable_pane_required. That is safe — the bearer is the real gate and corroboration is only a mismatch refusal — but it is a behavior expansion on methods the PR title describes as out of scope, so it is worth confirming the sequencing is intended rather than landing inside the check/send/reply conversions.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Index-backed principal lookups —
runsBoundToPrincipalnow serves pane principals from the exactidx_runs_coordinator_principalpartial index plus the existingidx_runs_coordinator_pane_leafexpression index (dedup by run id, then rowid sort), and session/unparseable principals from the exact index only, replacing the prior full scan. - New partial index —
idx_runs_coordinator_principal(coordinator_principal IS NOT NULL AND legacy = 0) is created idempotently inbackfillPrincipalColumns, which runs after migration and on every open. - Query-plan pinning — a new test asserts both lookups resolve to an index (no
SCAN runs), and the migration revert now drops the index before dropping the column.
The rewrite is equivalence-preserving: a pane: principal is only ever persisted alongside a non-null coordinator_pane_key sharing its leaf (every write path derives the principal from the pane key via principalFromPaneKey or the resolver's binding), so the IS NOT NULL pre-filter cannot drop a run the old full scan would have returned, and isEquivalentPrincipal remains the authoritative final gate for cross-kind and malformed values.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
|
Review status: PASS on pushed head
|
|
Electron QA status (branch
|
|
Superseded by #22555 (stacked on #22522), part of a fresh orchestration stack for structured-session coordinators. The design changed after this PR was written. There is no bearer credential and no fence; all agents run on one machine as one user, so the identity is the Orca session id injected into the agent's environment. #22555 resolves every caller once, at the dispatch entry, to one orchestration actor (
This PR's one-resolver principle carries over. Closing in favour of #22555. |

Summary
Second of six PRs replacing orchestration's terminal-shaped actor identity with a principal. PR 1 added the columns; this PR introduces the single place a credential becomes a principal, and converts the Run methods to it.
resolveCallerPrincipal(newcaller-principal.ts) takes any credential form — a--fromterminal handle with hook evidence, astructworker_bearer, or declared session values — and returns one shape: principal,principalId,ownerGeneration,hostScope,workspaceId, an opaquebindingfor DB writers, andwaiterHandles. Methods never inspect the principal's kind.Why one resolver
The closed PR this stack supersedes branched per method on
if (params.agentSessionId)at 21 call sites and shipped six defects that way — skipped prior-run waiter cancellation, a bypassed takeover guard, and changed error text among them. Here the only occurrence of that expression in the entire orchestration method tree is a comment naming it as the defect class the module prevents.runCreate/runUse/runCurrentexecute an identical statement sequence for both principal kinds.The security invariant
A declared
agentSessionId/runtimeFencenever authenticates. Session ids are public —structuredAgentSessionTabIdreturnsstructured-agent-session-${sessionId}and tab ids carry them in plain text — and a fence is a small integer. Accepting a declared pair would let any RPC caller impersonate any native session, and later PRs convertingcheck/send/replywould inherit it as mailbox theft.Session-kind resolution therefore requires possession of an unguessable bearer. Declared values are corroboration only: a mismatch against the bearer's real identity is a refusal, never an acceptance. A standalone declared session id resolves to
consumer_fenced.Pinned at resolver level and method level, and verified by ablation: re-introducing the declared-id acceptance and bypassing bearer possession reddens exactly two tests, one at each level. Note the defense is layered — removing the explicit refusal alone does not open the hole, because the handle-to-identity lookup is the real gate.
Second invariant:
isEquivalentPrincipalnever bridges pane↔session in either direction. A session id is recoverable from a structured pane key's tab half, so a fabricatedpane:structured-agent-session-<realId>:<attacker-leaf>must not matchsession:<realId>— asserted in both argument orders. PR 1's backfill does derive session-from-pane, but only as a one-time server-side pass over rows the host wrote; the module doc states that derivation is legal exactly there and never at request time.A latent bug fixed
The fence schema is
z.number().int().min(0). The closed PR used.positive(), which refuses fence0— a legitimate value (agent-session-record.tsvalidatesruntimeFence >= 0), so freshly leased sessions would have been silently locked out. Pinned by a test at both levels.Notable deviation from the plan
The plan described
attestDeclaredCalleras a no-op for sessions. It isn't: the session path runs the same evidence assertion as panes. A strict no-op would let a caller attested asterm_Xpresent a stolen structworker handle without triggering the mismatch throw, which is weaker than today'srunUse. The implemented form is byte-compatible with current behavior and strictly safer, with a pinning test.Validation
orchestration+rpc+shared, 1093 files): 11,020 passed. One failure inremote-runtime-shared-control-connectionis a known load-timing flake — its failure count varies between identical runs on an unchanged tree, it passes in isolation on both this branch and its base, and no import path connects it to anything here.pnpm-lock.yamlabsent from every commit and from the branch range.Stacking
Based on
brennanb2025/principals-01-columns(#19943), notmain. Review that first.