fix(orchestration): refuse a session address that gate-list or task-list --run would drop

The CLI entry lets a `session:` address that is not the session's own spelling through
for the host to place, but gate-list and task-list send no caller when --run names the
Run, so `--from session:<other>` was silently ignored. With --run they now refuse it
(consumer_fenced) before any request, the same as a conflicting terminal handle.
This commit is contained in:
Brennan Benson
2026-09-29 01:48:18 -07:00
parent 5deafe9883
commit 5f753af0a7
4 changed files with 53 additions and 6 deletions
@@ -1,6 +1,7 @@
import type { CommandHandler } from '../../dispatch'
import { printResult } from '../../format'
import { getOptionalJsonFlag, getOptionalStringFlag, getRequiredStringFlag } from '../../flags'
import { refuseUnsentSessionAddress } from '../../session-caller-flags'
import { callOrchestrationMutation } from './mutation-request'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
@@ -36,6 +37,9 @@ export const ORCHESTRATION_GATE_HANDLERS: Record<string, CommandHandler> = {
'orchestration gate-list': async ({ flags, client, cwd, json }) => {
const run = getOptionalStringFlag(flags, 'run')
if (run) {
refuseUnsentSessionAddress(flags, 'from')
}
// Why: named runs remain inspectable without a pane; only implicit runs resolve identity.
const from = run ? undefined : await resolveCoordinatorTerminalHandle(flags, cwd, client)
const result = await client.call<{
@@ -3,6 +3,7 @@ import { printResult } from '../../format'
import { getOptionalStringFlag, getRequiredStringFlag } from '../../flags'
import { RuntimeClientError } from '../../runtime-client'
import { abbreviateOrchestrationTasks } from '../../../shared/orchestration-task-summary'
import { refuseUnsentSessionAddress } from '../../session-caller-flags'
import { callOrchestrationMutation } from './mutation-request'
import { resolveCoordinatorTerminalHandle } from './terminal-identity'
@@ -38,6 +39,9 @@ export const ORCHESTRATION_TASK_HANDLERS: Record<string, CommandHandler> = {
'orchestration task-list': async ({ flags, client, cwd, json }) => {
const brief = flags.has('brief')
const run = getOptionalStringFlag(flags, 'run')
if (run) {
refuseUnsentSessionAddress(flags, 'from')
}
const callerTerminalHandle = run
? undefined
: await resolveCoordinatorTerminalHandle(flags, cwd, client)
@@ -360,6 +360,17 @@ describe.each([
await invoke(command, flagMap({ run: 'run_1', from: `session:${SESSION}` }))
expect(callsTo(method)[0]?.[callerParam]).toBeUndefined()
})
it('refuses a session address it would not send for the host to check, before any request', async () => {
// With --run no caller is sent, so a `session:` address the host alone could place (another
// chat's, or this chat's pre-/clear root) would be dropped unchecked.
for (const other of [`session:${ROOT}`, 'session:0b5e2d7c-9a41-4c3e-8f62-7d1a3e5b9c08']) {
await expect(invoke(command, flagMap({ run: 'run_1', from: other }))).rejects.toMatchObject({
code: 'consumer_fenced'
})
}
expect(callMock).not.toHaveBeenCalled()
})
})
describe('the identity a session presents', () => {
+34 -6
View File
@@ -36,16 +36,44 @@ export function refuseConflictingSessionCallerFlags(
!namesInjectedSession(declared, sessionId, env) &&
!sessionAddressForHost(declared, sessionId, env)
) {
throw new RuntimeClientError(
'consumer_fenced',
`This command runs as agent session ${sessionId}, so --${flagName} ${declared} would act as a ` +
`different caller. Drop --${flagName}: this session's orchestration commands already act ` +
`as this session. No request was sent.`
)
throw conflictingCallerFlag(sessionId, flagName, declared)
}
}
}
/**
* For a verb that sends no caller (`--run` names the Run), a session address the host would
* otherwise have checked is refused here, rather than dropped unchecked.
*/
export function refuseUnsentSessionAddress(
flags: ReadonlyMap<string, string | boolean>,
flagName: IdentityFlag,
env: NodeJS.ProcessEnv = process.env
): void {
const sessionId = readInjectedAgentSessionId(env)
const declared = flags.get(flagName)
if (
sessionId &&
typeof declared === 'string' &&
sessionAddressForHost(declared, sessionId, env)
) {
throw conflictingCallerFlag(sessionId, flagName, declared)
}
}
function conflictingCallerFlag(
sessionId: string,
flagName: IdentityFlag,
declared: string
): RuntimeClientError {
return new RuntimeClientError(
'consumer_fenced',
`This command runs as agent session ${sessionId}, so --${flagName} ${declared} would act as a ` +
`different caller. Drop --${flagName}: this session's orchestration commands already act ` +
`as this session. No request was sent.`
)
}
const IDENTITY_FLAGS: readonly IdentityFlag[] = ['from', 'terminal']
/**