From 6f8344cdff67d3ead031c656575594abf956ff19 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:59:06 -0700 Subject: [PATCH] fix(orchestration): refuse title evidence when publishing identity for routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilt on current main so this carries the hardened parser from #16148 and the corrected resolver from #16157 (authority-scoped run keys, no order-dependent duplicate resolution). Applies the resolver's new `minimumSource` floor at the publisher. What this publishes authorizes an action — routing decides which real agent pane receives a message — so ranking title last is not enough; the floor removes it from consideration entirely, and no amount of reordering by a caller can bring it back. The trade, stated because it is a real capability loss: a hook-less agent over SSH that Orca did not launch, and whose foreground process the host cannot read, is no longer addressable by @agent. Accepted because a message delivered into the wrong agent's prompt is unrecoverable while an undelivered one is visible — the sender sees zero recipients. Whether real panes actually carry launch/foreground evidence is the open question, and is what live validation must answer. --- src/shared/published-pane-agent-identity.test.ts | 15 +++++++++++---- src/shared/published-pane-agent-identity.ts | 13 +++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/shared/published-pane-agent-identity.test.ts b/src/shared/published-pane-agent-identity.test.ts index 2a0a5ef3418..acca2033b53 100644 --- a/src/shared/published-pane-agent-identity.test.ts +++ b/src/shared/published-pane-agent-identity.test.ts @@ -30,10 +30,17 @@ describe('resolvePublishedPaneAgentIdentity', () => { expect(resolve({ launchAgent: 'claude', foregroundAgent: 'codex' })).toBe('codex') }) - it('falls back to the title when the host has no process or launch evidence', () => { - // Hook-less agents over SSH surface only a decorated title; dropping it would make those - // panes unaddressable. It is last, not absent. - expect(resolve({ title: '✳ Claude Code' })).toBe('claude') + it('publishes nothing from a title alone, even an unambiguous one', () => { + // Deliberate, and the sharpest trade in this change. What this publishes AUTHORIZES ACTIONS: + // orchestration routing uses it to pick which real agent pane receives a message. A title is + // a decoration channel, and a stale one outlives the agent that wrote it, so a pane whose only + // evidence is a parsed string publishes no identity and every action consumer fails closed. + // + // The cost: a hook-less agent over SSH that Orca did not launch, and whose foreground process + // the host cannot read, is not addressable by @agent. That is a real capability loss, accepted + // because a message delivered into the wrong agent's prompt is not recoverable and an + // undelivered one is — the sender sees zero recipients. + expect(resolve({ title: '✳ Claude Code' })).toBeUndefined() }) it('publishes nothing when the title names no agent unambiguously', () => { diff --git a/src/shared/published-pane-agent-identity.ts b/src/shared/published-pane-agent-identity.ts index 1531c447dbd..47a6e2c8bd0 100644 --- a/src/shared/published-pane-agent-identity.ts +++ b/src/shared/published-pane-agent-identity.ts @@ -8,10 +8,10 @@ import type { TuiAgent } from './tui-agent' * Kept out of the runtime class so it can be tested without one, and so routing, delivery and the * UI all read the same decision instead of each re-deriving it. * - * Title is included but ranks last, and contributes only when the evidence parser finds an - * unambiguous name. A task title that merely mentions an agent yields no title evidence at all, - * which is the whole point: "Switch Claude and Codex off the load balancer… - grok" is a Grok - * pane, and used to receive both `@claude` and `@codex`. + * Title is NOT consulted. What this publishes authorizes actions, and a terminal title is a + * decoration channel a user can type any agent's name into — "Switch Claude and Codex off the + * load balancer… - grok" is a Grok pane that used to receive both `@claude` and `@codex`. + * Identity therefore comes only from launch and foreground-process evidence the host owns. * * Returns undefined when nothing is known, and absence is published as absence. A caller that * authorizes an action must fail closed on it rather than falling back to parsing the title. @@ -24,6 +24,11 @@ export function resolvePublishedPaneAgentIdentity(args: { const titleAgent = args.title ? collectAgentTitleEvidence(args.title).agent : null return ( resolvePaneAgentIdentity({ + // Why the floor: what this publishes authorizes actions — orchestration routing decides + // which real agent pane receives a message. Ranking title last makes a bad delivery + // unlikely; refusing to see it makes one impossible. A pane whose only evidence is a + // parsed string publishes no identity, and every action consumer fails closed on absence. + minimumSource: 'launch', evidence: [ ...(args.foregroundAgent ? [{ source: 'process' as const, agent: args.foregroundAgent }]