From 1d845e495c05911165a16ffb3d537974a9e33388 Mon Sep 17 00:00:00 2001 From: chinawch007 Date: Mon, 14 Sep 2026 20:19:32 +0800 Subject: [PATCH 1/3] refactor(runtime): remove obsolete permission mode compatibility path Remove SessionManager.setPermissionMode, its legacy-store fallback, and helpers used only by that path. Production Desktop and CLI permission changes continue through the versioned configuration authority. Migrate concurrency, Deep Research cleanup, and pending Interaction tests to transitionSessionConfiguration. Document the paired optional Store capabilities and verify missing capabilities reject without fallback writes. Fixes #4795 Generated-by: Codex --- .../src/__tests__/session-manager.test.ts | 152 ++++++++++++------ packages/runtime/src/session-manager.ts | 97 ++--------- 2 files changed, 117 insertions(+), 132 deletions(-) diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts index 584126dad2..b770330613 100644 --- a/packages/runtime/src/__tests__/session-manager.test.ts +++ b/packages/runtime/src/__tests__/session-manager.test.ts @@ -4697,25 +4697,12 @@ describe('SessionManager manual compaction and quiescent session changes', () => }); describe('SessionManager permission mode updates', () => { - for (const route of ['direct', 'legacy'] as const) { + for (const route of ['direct', 'configuration'] as const) { test(`serializes concurrent ${route} boundary commits before they can become narrowing`, { timeout: 10_000, }, async (t) => { const root = await mkdtemp(join(tmpdir(), 'maka-boundary-commit-race-')); const store = createSessionStore(root); - // Hide optional capabilities from Runtime, without changing SQLite's own - // internal method calls, to exercise the legacy SessionStore contract. - const runtimeStore = - route === 'legacy' - ? new Proxy(store, { - get(target, key) { - if (key === 'readHeaderRecordSnapshot' || key === 'updateSessionConfiguration') - return undefined; - const value = Reflect.get(target, key, target); - return typeof value === 'function' ? value.bind(target) : value; - }, - }) - : store; const gate = makeGate(); t.after(async () => { gate.release(); @@ -4727,7 +4714,7 @@ describe('SessionManager permission mode updates', () => { let backend: TestBackend | undefined; backends.register('ai-sdk', (ctx) => (backend = new TestBackend(ctx, gate))); const manager = new SessionManager({ - store: runtimeStore, + store, backends, newId: nextId(), now: nextNow(979), @@ -4748,10 +4735,20 @@ describe('SessionManager permission mode updates', () => { } as never, }); const session = await manager.createSession(makeInput({ permissionMode: 'explore' })); - const update = (bypass: boolean) => - route === 'direct' - ? manager.setExecutionBoundaryKind(session.id, bypass ? 'bypass' : 'managed') - : manager.setPermissionMode(session.id, bypass ? 'bypass' : 'ask'); + const update = async (bypass: boolean) => { + if (route === 'direct') { + return manager.setExecutionBoundaryKind(session.id, bypass ? 'bypass' : 'managed'); + } + const current = await store.readHeaderRecordSnapshot(session.id); + return manager.transitionSessionConfiguration(session.id, { + expectedRevision: current.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(current.header, { + permissionMode: bypass ? 'bypass' : 'ask', + }), + }); + }; const turn = manager .sendMessage(session.id, { turnId: 'turn-racing', text: 'keep running' }) [Symbol.asyncIterator](); @@ -4766,8 +4763,17 @@ describe('SessionManager permission mode updates', () => { ); const conflict = results[1]; assert.ok(conflict?.status === 'rejected'); - assert.ok(conflict.reason instanceof SessionConfigurationTransitionError); - assert.strictEqual(conflict.reason.code, 'operation_conflict'); + // Either the configuration revision or the boundary revision can fence + // the stale request, depending on when its snapshot was observed. + if ( + route === 'configuration' && + conflict.reason instanceof SessionConfigurationRevisionConflictError + ) { + assert.strictEqual(conflict.reason.actualRevision, conflict.reason.expectedRevision + 1); + } else { + assert.ok(conflict.reason instanceof SessionConfigurationTransitionError); + assert.strictEqual(conflict.reason.code, 'operation_conflict'); + } assert.deepStrictEqual(await store.readExecutionBoundary(session.id), { kind: 'bypass', revision: 1, @@ -5529,11 +5535,17 @@ describe('SessionManager permission mode updates', () => { ['turn-2', 'completed'], ], ); - const summary = await manager.setPermissionMode(session.id, 'bypass'); - assert.strictEqual(summary.permissionMode, 'bypass'); + const afterTurns = await store.readHeaderRecordSnapshot(session.id); + const unchanged = await manager.transitionSessionConfiguration(session.id, { + expectedRevision: afterTurns.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(afterTurns.header, { permissionMode: 'bypass' }), + }); + assert.deepStrictEqual(unchanged, afterTurns); }); - test('the setPermissionMode wrapper delegates deep research cleanup to configuration authority', async () => { + test('configuration authority removes only the deep research label when leaving Explore', async () => { const store = new VersionedConfigurationMemorySessionStore(); const backends = new BackendRegistry(); backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); @@ -5545,29 +5557,63 @@ describe('SessionManager permission mode updates', () => { }), ); - const summary = await manager.setPermissionMode(session.id, 'ask'); + const current = await store.readHeaderRecordSnapshot(session.id); + const next = await manager.transitionSessionConfiguration(session.id, { + expectedRevision: current.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(current.header, { permissionMode: 'ask' }), + }); - assert.strictEqual(summary.permissionMode, 'ask'); - assert.deepStrictEqual(summary.labels, ['kept']); - assert.deepStrictEqual((await store.readHeader(session.id)).labels, ['kept']); + assert.strictEqual(next.header.permissionMode, 'ask'); + assert.strictEqual(next.revision, current.revision + 1); + assert.deepStrictEqual(next.header.labels, ['kept']); + const persisted = await store.readHeaderRecordSnapshot(session.id); + assert.deepStrictEqual(persisted.header, next.header); + assert.strictEqual(persisted.revision, next.revision); }); - test('temporarily preserves setPermissionMode for legacy SessionStore implementations', async () => { - const store = new MemorySessionStore(); - const manager = new SessionManager({ - store, - backends: new BackendRegistry(), - newId: nextId(), - now: nextNow(6_100), - }); - const session = await manager.createSession(makeInput({ permissionMode: 'ask' })); - - const summary = await manager.setPermissionMode(session.id, 'bypass'); + for (const missing of [ + ['readHeaderRecordSnapshot'], + ['updateSessionConfiguration'], + ['readHeaderRecordSnapshot', 'updateSessionConfiguration'], + ] as const) { + test(`configuration changes require Store capabilities: missing ${missing.join(', ')}`, async () => { + const store = new VersionedConfigurationMemorySessionStore(); + const manager = new SessionManager({ + store, + backends: new BackendRegistry(), + newId: nextId(), + now: nextNow(6_100), + }); + const session = await manager.createSession(makeInput({ permissionMode: 'ask' })); + const readSnapshot = store.readHeaderRecordSnapshot.bind(store); + const current = await readSnapshot(session.id); + const boundary = await store.readExecutionBoundary(session.id); + for (const capability of missing) { + Object.defineProperty(store, capability, { value: undefined }); + } + store.updateHeader = async () => assert.fail('Must not fall back to header writes'); + store.setExecutionBoundaryKind = async () => + assert.fail('Must not fall back to boundary writes'); - assert.strictEqual(summary.permissionMode, 'bypass'); - assert.strictEqual((await store.readHeader(session.id)).permissionMode, 'bypass'); - assert.strictEqual((await store.readExecutionBoundary(session.id)).kind, 'bypass'); - }); + await assert.rejects( + manager.transitionSessionConfiguration(session.id, { + expectedRevision: current.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(current.header, { permissionMode: 'bypass' }), + }), + (error: unknown) => { + assert.ok(error instanceof SessionConfigurationTransitionError); + assert.strictEqual(error.code, 'operation_unavailable'); + return true; + }, + ); + assert.deepStrictEqual(await readSnapshot(session.id), current); + assert.deepStrictEqual(await store.readExecutionBoundary(session.id), boundary); + }); + } test('starts a new turn without workspace identity when safety inspection fails', async () => { const store = new MemorySessionStore(); @@ -11337,8 +11383,24 @@ describe('SessionManager permission mode updates', () => { assert.strictEqual((await store.readHeader(session.id)).status, 'waiting_for_user'); const [run] = await runStore.listSessionInvocations(session.id); assert.strictEqual(run?.terminalEvent, undefined); - await expectRejects(manager.setPermissionMode(session.id, 'bypass'), /pending Interaction/); - assert.strictEqual((await store.readHeader(session.id)).permissionMode, 'ask'); + const current = await store.readHeaderRecordSnapshot(session.id); + const boundary = await store.readExecutionBoundary(session.id); + await assert.rejects( + manager.transitionSessionConfiguration(session.id, { + expectedRevision: current.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(current.header, { permissionMode: 'bypass' }), + }), + (error: unknown) => { + assert.ok(error instanceof SessionConfigurationTransitionError); + assert.strictEqual(error.code, 'session_busy'); + assert.match(error.message, /pending Interaction/); + return true; + }, + ); + assert.deepStrictEqual(await store.readHeaderRecordSnapshot(session.id), current); + assert.deepStrictEqual(await store.readExecutionBoundary(session.id), boundary); await manager.respondToSandboxBoundary(session.id, { requestId: 'boundary-1', diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts index 214c6efc2c..b56ac40a4a 100644 --- a/packages/runtime/src/session-manager.ts +++ b/packages/runtime/src/session-manager.ts @@ -648,7 +648,17 @@ export interface SessionStore { patch: SessionHeaderPatch, expectedRevision: number, ): Promise; + /** + * Versioned configuration authority requires both this method and + * updateSessionConfiguration. Stores may omit these capabilities when they + * do not support configuration changes; Runtime rejects those operations + * with operation_unavailable rather than falling back to unversioned writes. + */ readHeaderRecordSnapshot?(sessionId: string): Promise; + /** + * Atomically check the expected revision and commit configuration, execution + * boundary and the new revision. Requires readHeaderRecordSnapshot. + */ updateSessionConfiguration?( sessionId: string, input: SessionConfigurationStoreUpdate, @@ -1640,64 +1650,6 @@ export class SessionManager { return this.runtimeKernel.listActiveInteractions?.(sessionId) ?? []; } - async setPermissionMode(sessionId: string, mode: PermissionMode): Promise { - const readHeaderRecordSnapshot = this.deps.store.readHeaderRecordSnapshot?.bind( - this.deps.store, - ); - if (!readHeaderRecordSnapshot || !this.deps.store.updateSessionConfiguration) { - // Temporary compatibility bridge for SessionStore embeddings that predate - // versioned configuration authority. A follow-up PR will shortly remove - // setPermissionMode and this redundant fallback after callers migrate. - return this.setPermissionModeWithLegacyStore(sessionId, mode); - } - const current = await readHeaderRecordSnapshot(sessionId); - const next = await this.transitionSessionConfiguration(sessionId, { - expectedRevision: current.revision, - clearConnectionBlock: false, - permissionModeOnly: true, - configuration: sessionConfigurationWithPermissionMode(current.header, mode), - }); - return headerToSummary(next.header); - } - - private async setPermissionModeWithLegacyStore( - sessionId: string, - mode: PermissionMode, - ): Promise { - const previous = await this.deps.store.readHeader(sessionId); - const boundary = await this.deps.store.readExecutionBoundary(sessionId); - const leavingDeepResearch = isDeepResearchSession(previous.labels) && mode !== 'explore'; - if ( - previous.permissionMode === mode && - executionBoundaryMatchesPermissionMode(boundary, mode) && - !leavingDeepResearch - ) { - return headerToSummary(previous); - } - - const labels = leavingDeepResearch - ? previous.labels.filter((label) => label !== DEEP_RESEARCH_SESSION_LABEL) - : previous.labels; - const kind = mode === 'bypass' ? 'bypass' : 'managed'; - await this.commitExecutionBoundaryTransition(sessionId, boundary, mode, async () => { - const current = await this.deps.store.readHeader(sessionId); - if (current.status === 'waiting_for_user') { - throw new SessionConfigurationTransitionError( - 'session_busy', - 'Session has a pending Interaction', - ); - } - return () => - this.deps.store.setExecutionBoundaryKind(sessionId, kind, { - permissionMode: mode, - labels, - }); - }); - const next = await this.deps.store.readHeader(sessionId); - this.runtimeKernel.updateCachedHeader(sessionId, next); - return headerToSummary(next); - } - async setExecutionBoundaryKind( sessionId: string, kind: 'managed' | 'bypass', @@ -5243,24 +5195,6 @@ function claimedAgentGraphIntentResult( }; } -function sessionConfigurationWithPermissionMode( - header: SessionHeader, - permissionMode: PermissionMode, -): SessionConfigurationTransitionRequest['configuration'] { - return { - backend: header.backend, - executorId: header.executorId, - llmConnectionId: header.llmConnectionId, - llmConnectionSlug: header.llmConnectionSlug, - connectionLocked: header.connectionLocked, - model: header.model, - thinkingLevel: header.thinkingLevel, - permissionMode, - collaborationMode: header.collaborationMode ?? 'agent', - orchestrationMode: header.orchestrationMode ?? 'default', - }; -} - function sessionConfigurationMatchesExceptPermissionMode( header: SessionHeader, configuration: SessionConfigurationTransitionRequest['configuration'], @@ -5288,17 +5222,6 @@ function sessionConfigurationMatches( ); } -function executionBoundaryMatchesPermissionMode( - boundary: ExecutionBoundary, - mode: PermissionMode, -): boolean { - if (mode === 'bypass') return boundary.kind === 'bypass'; - if (boundary.kind !== 'managed') return false; - return mode === 'explore' - ? boundary.profile.name === 'read-only' - : boundary.profile.name !== 'read-only'; -} - function narrowsExecutionAuthority( boundary: ExecutionBoundary, nextPermissionMode: PermissionMode, From b844456f09fc8c3463a744052b36323fbc6dc237 Mon Sep 17 00:00:00 2001 From: chinawch007 Date: Tue, 15 Sep 2026 17:26:05 +0800 Subject: [PATCH 2/3] refactor(runtime): finish permission authority cleanup Remove the unused SessionManager boundary setter and its unversioned Store requirement. Migrate descendant revocation and admission checks to configuration authority and remove obsolete direct-route fixtures. Split stale configuration revisions from gated concurrent boundary conflicts, asserting each error precisely. Simplify capability tests and explain Runtime optionality alongside the atomic Store contract. Refs #4795 Generated-by: Codex --- .../runtime-event-read-model.test.ts | 4 - .../runtime-kernel-interaction.test.ts | 3 - .../session-manager-terminal-ledger.test.ts | 4 - .../src/__tests__/session-manager.test.ts | 585 ++++++++---------- packages/runtime/src/session-manager.ts | 59 +- 5 files changed, 275 insertions(+), 380 deletions(-) diff --git a/packages/runtime/src/__tests__/runtime-event-read-model.test.ts b/packages/runtime/src/__tests__/runtime-event-read-model.test.ts index ca9813b87c..a63f993a52 100644 --- a/packages/runtime/src/__tests__/runtime-event-read-model.test.ts +++ b/packages/runtime/src/__tests__/runtime-event-read-model.test.ts @@ -2588,10 +2588,6 @@ class ReadOnlyStore implements SessionStore { throw new Error('not implemented'); } - async setExecutionBoundaryKind(): Promise { - throw new Error('not implemented'); - } - async readExecutionBoundary(): Promise { throw new Error('not implemented'); } diff --git a/packages/runtime/src/__tests__/runtime-kernel-interaction.test.ts b/packages/runtime/src/__tests__/runtime-kernel-interaction.test.ts index dbc0d047ff..6c5e99557e 100644 --- a/packages/runtime/src/__tests__/runtime-kernel-interaction.test.ts +++ b/packages/runtime/src/__tests__/runtime-kernel-interaction.test.ts @@ -615,9 +615,6 @@ function memoryStore(): SessionStore { return { create: async () => header, createSubagent: async () => ({ header, created: false }), - setExecutionBoundaryKind: async () => { - throw new Error('not implemented'); - }, readExecutionBoundary: async () => { throw new Error('not implemented'); }, diff --git a/packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts b/packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts index 46729664d0..00ebeffdfa 100644 --- a/packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts +++ b/packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts @@ -2305,10 +2305,6 @@ class TinySessionStore implements SessionStore { return clone(header); } - async setExecutionBoundaryKind(): Promise { - throw new Error('not implemented'); - } - async readExecutionBoundary(): Promise { throw new Error('not implemented'); } diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts index b770330613..fb85d78d26 100644 --- a/packages/runtime/src/__tests__/session-manager.test.ts +++ b/packages/runtime/src/__tests__/session-manager.test.ts @@ -4697,110 +4697,150 @@ describe('SessionManager manual compaction and quiescent session changes', () => }); describe('SessionManager permission mode updates', () => { - for (const route of ['direct', 'configuration'] as const) { - test(`serializes concurrent ${route} boundary commits before they can become narrowing`, { - timeout: 10_000, - }, async (t) => { - const root = await mkdtemp(join(tmpdir(), 'maka-boundary-commit-race-')); - const store = createSessionStore(root); - const gate = makeGate(); - t.after(async () => { - gate.release(); - await store.close?.(); - await rm(root, { recursive: true, force: true }); + test('configuration authority rejects a stale revision without changing the committed grant', async () => { + const store = new VersionedConfigurationMemorySessionStore(); + const manager = new SessionManager({ + store, + backends: new BackendRegistry(), + newId: nextId(), + now: nextNow(978), + }); + const session = await manager.createSession(makeInput({ permissionMode: 'explore' })); + const original = await store.readHeaderRecordSnapshot(session.id); + const update = (permissionMode: PermissionMode) => + manager.transitionSessionConfiguration(session.id, { + expectedRevision: original.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(original.header, { permissionMode }), }); - const calls: string[] = []; - const backends = new BackendRegistry(); - let backend: TestBackend | undefined; - backends.register('ai-sdk', (ctx) => (backend = new TestBackend(ctx, gate))); - const manager = new SessionManager({ - store, - backends, - newId: nextId(), - now: nextNow(979), - shellRuns: { - async terminateSession(sessionId: string) { - calls.push(`terminate:${sessionId}`); - return { sessionId, token: Symbol('test') }; - }, - async commitSessionClose() { - calls.push('commit'); - }, - rollbackSessionClose() { - calls.push('rollback'); - }, - resumeSession(sessionId: string) { - calls.push(`resume:${sessionId}`); - }, - } as never, + + await update('bypass'); + const committed = await store.readHeaderRecordSnapshot(session.id); + const boundary = await store.readExecutionBoundary(session.id); + assert.strictEqual(committed.revision, original.revision + 1); + assert.strictEqual(committed.header.permissionMode, 'bypass'); + await assert.rejects(update('ask'), (error: unknown) => { + assert.ok(error instanceof SessionConfigurationRevisionConflictError); + assert.strictEqual(error.expectedRevision, original.revision); + assert.strictEqual(error.actualRevision, committed.revision); + return true; + }); + assert.deepStrictEqual(await store.readHeaderRecordSnapshot(session.id), committed); + assert.deepStrictEqual(await store.readExecutionBoundary(session.id), boundary); + }); + + test('serializes configuration commits that observed the same execution boundary', { + timeout: 10_000, + }, async (t) => { + const root = await mkdtemp(join(tmpdir(), 'maka-boundary-commit-race-')); + const store = createSessionStore(root); + const gate = makeGate(); + const bothBoundariesRead = makeGate(); + const releaseBoundaryReads = makeGate(); + const readExecutionBoundary = store.readExecutionBoundary.bind(store); + t.after(async () => { + releaseBoundaryReads.release(); + gate.release(); + await store.close?.(); + await rm(root, { recursive: true, force: true }); + }); + const calls: string[] = []; + const backends = new BackendRegistry(); + let backend: TestBackend | undefined; + backends.register('ai-sdk', (ctx) => (backend = new TestBackend(ctx, gate))); + const manager = new SessionManager({ + store, + backends, + newId: nextId(), + now: nextNow(979), + shellRuns: { + async terminateSession(sessionId: string) { + calls.push(`terminate:${sessionId}`); + return { sessionId, token: Symbol('test') }; + }, + async commitSessionClose() { + calls.push('commit'); + }, + rollbackSessionClose() { + calls.push('rollback'); + }, + resumeSession(sessionId: string) { + calls.push(`resume:${sessionId}`); + }, + } as never, + }); + const session = await manager.createSession(makeInput({ permissionMode: 'explore' })); + const update = (snapshot: VersionedSessionHeader, permissionMode: PermissionMode) => + manager.transitionSessionConfiguration(session.id, { + expectedRevision: snapshot.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(snapshot.header, { permissionMode }), }); - const session = await manager.createSession(makeInput({ permissionMode: 'explore' })); - const update = async (bypass: boolean) => { - if (route === 'direct') { - return manager.setExecutionBoundaryKind(session.id, bypass ? 'bypass' : 'managed'); + const turn = manager + .sendMessage(session.id, { turnId: 'turn-racing', text: 'keep running' }) + [Symbol.asyncIterator](); + try { + await turn.next(); + const snapshot = await store.readHeaderRecordSnapshot(session.id); + let boundaryReads = 0; + store.readExecutionBoundary = async (sessionId) => { + const boundary = await readExecutionBoundary(sessionId); + if (++boundaryReads <= 2) { + if (boundaryReads === 2) bothBoundariesRead.release(); + await releaseBoundaryReads.promise; } - const current = await store.readHeaderRecordSnapshot(session.id); - return manager.transitionSessionConfiguration(session.id, { - expectedRevision: current.revision, - clearConnectionBlock: false, - permissionModeOnly: true, - configuration: configurationForHeader(current.header, { - permissionMode: bypass ? 'bypass' : 'ask', - }), - }); + return boundary; }; - const turn = manager - .sendMessage(session.id, { turnId: 'turn-racing', text: 'keep running' }) - [Symbol.asyncIterator](); - try { - await turn.next(); - // Both requests initially observe Explore. The second must not reuse - // that classification after the first has committed Bypass. - const results = await Promise.allSettled([update(true), update(false)]); - assert.deepStrictEqual( - results.map((result) => result.status), - ['fulfilled', 'rejected'], - ); - const conflict = results[1]; - assert.ok(conflict?.status === 'rejected'); - // Either the configuration revision or the boundary revision can fence - // the stale request, depending on when its snapshot was observed. - if ( - route === 'configuration' && - conflict.reason instanceof SessionConfigurationRevisionConflictError - ) { - assert.strictEqual(conflict.reason.actualRevision, conflict.reason.expectedRevision + 1); - } else { - assert.ok(conflict.reason instanceof SessionConfigurationTransitionError); - assert.strictEqual(conflict.reason.code, 'operation_conflict'); - } - assert.deepStrictEqual(await store.readExecutionBoundary(session.id), { - kind: 'bypass', - revision: 1, - }); - assert.strictEqual((await store.readHeader(session.id)).permissionMode, 'bypass'); - assert.deepStrictEqual(manager.runningTurnIds(session.id), ['turn-racing']); - assert.strictEqual(backend?.stopCalls, 0); - assert.deepStrictEqual(calls, []); - // A fresh retry is now correctly classified as narrowing. - await assert.rejects(update(false), (error: unknown) => { - assert.ok(error instanceof SessionConfigurationTransitionError); - assert.strictEqual(error.code, 'session_busy'); - return true; - }); - } finally { - gate.release(); - while (!(await turn.next()).done) {} - } - // The conflict released the mutation lane; idle narrowing still revokes shells. - await update(false); - assert.strictEqual((await store.readHeader(session.id)).permissionMode, 'ask'); - assert.deepStrictEqual(calls, [`terminate:${session.id}`, 'commit', `resume:${session.id}`]); - }); - } + // A shared configuration snapshot alone does not fix which fence wins. + // Hold both initial boundary reads until each has observed Explore, so + // the second commit hits the boundary revision fence before its CAS. + const pending = Promise.allSettled([update(snapshot, 'bypass'), update(snapshot, 'ask')]); + await bothBoundariesRead.promise; + releaseBoundaryReads.release(); + const results = await pending; + store.readExecutionBoundary = readExecutionBoundary; + assert.deepStrictEqual( + results.map((result) => result.status), + ['fulfilled', 'rejected'], + ); + const conflict = results[1]; + assert.ok(conflict?.status === 'rejected'); + assert.ok(conflict.reason instanceof SessionConfigurationTransitionError); + assert.strictEqual(conflict.reason.code, 'operation_conflict'); + assert.match(conflict.reason.message, /execution boundary changed/); + const committed = await store.readHeaderRecordSnapshot(session.id); + assert.strictEqual(committed.revision, snapshot.revision + 1); + assert.strictEqual(committed.header.permissionMode, 'bypass'); + assert.deepStrictEqual(await store.readExecutionBoundary(session.id), { + kind: 'bypass', + revision: 1, + }); + assert.deepStrictEqual(manager.runningTurnIds(session.id), ['turn-racing']); + assert.strictEqual(backend?.stopCalls, 0); + assert.deepStrictEqual(calls, []); + // A fresh retry sees Bypass and is now correctly classified as narrowing. + await assert.rejects(update(committed, 'ask'), (error: unknown) => { + assert.ok(error instanceof SessionConfigurationTransitionError); + assert.strictEqual(error.code, 'session_busy'); + return true; + }); + assert.deepStrictEqual(await store.readHeaderRecordSnapshot(session.id), committed); + } finally { + store.readExecutionBoundary = readExecutionBoundary; + releaseBoundaryReads.release(); + gate.release(); + while (!(await turn.next()).done) {} + } + // The conflict released the mutation lane; idle narrowing still revokes shells. + await update(await store.readHeaderRecordSnapshot(session.id), 'ask'); + assert.strictEqual((await store.readHeader(session.id)).permissionMode, 'ask'); + assert.deepStrictEqual(calls, [`terminate:${session.id}`, 'commit', `resume:${session.id}`]); + }); test('rejects unprotected boundary commits when admission mutation authority is unavailable', async () => { - const store = new MemorySessionStore(); + const store = new VersionedConfigurationMemorySessionStore(); const kernel = new DelegatingRuntimeKernel(); Object.defineProperty(kernel, 'runSessionAdmissionMutation', { value: undefined }); const manager = new SessionManager({ @@ -4811,8 +4851,14 @@ describe('SessionManager permission mode updates', () => { now: nextNow(979), }); const session = await manager.createSession(makeInput({ permissionMode: 'explore' })); + const current = await store.readHeaderRecordSnapshot(session.id); await assert.rejects( - manager.setExecutionBoundaryKind(session.id, 'bypass'), + manager.transitionSessionConfiguration(session.id, { + expectedRevision: current.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(current.header, { permissionMode: 'bypass' }), + }), (error: unknown) => { assert.ok(error instanceof SessionConfigurationTransitionError); assert.strictEqual(error.code, 'operation_unavailable'); @@ -5214,143 +5260,126 @@ describe('SessionManager permission mode updates', () => { assert.strictEqual((await store.readHeader(session.id)).permissionMode, 'explore'); }); - for (const route of ['configuration', 'boundary'] as const) { - for (const grant of ['read', 'write', 'network'] as const) { - test(`restoring Explore revokes an approved ${grant} through the durable ${route} path`, async (t) => { - const root = await mkdtemp(join(tmpdir(), 'maka-explore-read-revocation-')); - const store = createSessionStore(root); - t.after(async () => { - await store.close?.(); - await rm(root, { recursive: true, force: true }); - }); - const gate = makeGate(); - const calls: string[] = []; - const backends = new BackendRegistry(); - const runStore = new MemoryAgentRunStore(); - backends.register('ai-sdk', (ctx) => new TestBackend(ctx, gate)); - const manager = new SessionManager({ - store, - runStore, - runtimeEventStore: runStore, - backends, - newId: nextId(), - now: nextNow(988), - shellRuns: { - async terminateSession(sessionId: string) { - calls.push(`terminate:${sessionId}`); - return { sessionId, token: Symbol('test') }; - }, - async commitSessionClose() { - calls.push('commit'); - }, - rollbackSessionClose() { - calls.push('rollback'); - }, - resumeSession(sessionId: string) { - calls.push(`resume:${sessionId}`); - }, - } as never, - }); - const workspaceRoot = join(root, 'workspace'); - const outsidePath = join(root, 'approved', 'input.txt'); - const session = await manager.createSession( - makeInput({ permissionMode: 'explore', cwd: workspaceRoot }), - ); - const updatePermissionMode = async (permissionMode: PermissionMode) => { - const current = await store.readHeaderRecordSnapshot(session.id); - return manager.transitionSessionConfiguration(session.id, { - expectedRevision: current.revision, - clearConnectionBlock: false, - permissionModeOnly: true, - configuration: configurationForHeader(current.header, { permissionMode }), - }); - }; - await store.createSandboxBoundaryRequest({ - sessionId: session.id, - requestId: 'approved-expansion', - turnId: 'turn-approval', - runId: 'run-approval', - expansion: - grant === 'network' - ? { network: { enabled: true } } - : { - filesystem: { entries: [{ path: outsidePath, access: grant, scope: 'exact' }] }, - }, - justification: 'Approve one specific sandbox expansion.', + for (const grant of ['read', 'write', 'network'] as const) { + test(`restoring Explore revokes an approved ${grant} through configuration authority`, async (t) => { + const root = await mkdtemp(join(tmpdir(), 'maka-explore-read-revocation-')); + const store = createSessionStore(root); + t.after(async () => { + await store.close?.(); + await rm(root, { recursive: true, force: true }); + }); + const gate = makeGate(); + const calls: string[] = []; + const backends = new BackendRegistry(); + const runStore = new MemoryAgentRunStore(); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx, gate)); + const manager = new SessionManager({ + store, + runStore, + runtimeEventStore: runStore, + backends, + newId: nextId(), + now: nextNow(988), + shellRuns: { + async terminateSession(sessionId: string) { + calls.push(`terminate:${sessionId}`); + return { sessionId, token: Symbol('test') }; + }, + async commitSessionClose() { + calls.push('commit'); + }, + rollbackSessionClose() { + calls.push('rollback'); + }, + resumeSession(sessionId: string) { + calls.push(`resume:${sessionId}`); + }, + } as never, + }); + const workspaceRoot = join(root, 'workspace'); + const outsidePath = join(root, 'approved', 'input.txt'); + const session = await manager.createSession( + makeInput({ permissionMode: 'explore', cwd: workspaceRoot }), + ); + const updatePermissionMode = async (permissionMode: PermissionMode) => { + const current = await store.readHeaderRecordSnapshot(session.id); + return manager.transitionSessionConfiguration(session.id, { + expectedRevision: current.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(current.header, { permissionMode }), }); - const settlement = await store.settleSandboxBoundaryRequest({ - sessionId: session.id, - requestId: 'approved-expansion', - decision: 'allow', + }; + await store.createSandboxBoundaryRequest({ + sessionId: session.id, + requestId: 'approved-expansion', + turnId: 'turn-approval', + runId: 'run-approval', + expansion: + grant === 'network' + ? { network: { enabled: true } } + : { + filesystem: { entries: [{ path: outsidePath, access: grant, scope: 'exact' }] }, + }, + justification: 'Approve one specific sandbox expansion.', + }); + const settlement = await store.settleSandboxBoundaryRequest({ + sessionId: session.id, + requestId: 'approved-expansion', + decision: 'allow', + }); + assert.strictEqual(settlement.request.status, 'approved'); + await updatePermissionMode('ask'); + const restoreExplore = () => updatePermissionMode('explore'); + const expanded = await store.readExecutionBoundary(session.id); + assert.strictEqual(expanded.kind, 'managed'); + if (expanded.kind !== 'managed') throw new Error('Expected a managed boundary'); + assert.strictEqual(expanded.profile.name, 'read-only'); + assert.strictEqual(isReadOnlyPermissionProfile(expanded.profile), grant === 'read'); + assert.strictEqual( + expanded.profile.network.kind, + grant === 'network' ? 'enabled' : 'restricted', + ); + assert.strictEqual( + canReadPath(expanded.profile, outsidePath, { workspaceRoots: [workspaceRoot] }), + grant !== 'network', + ); + assert.deepStrictEqual(calls, []); + + const activeTurn = manager + .sendMessage(session.id, { turnId: 'turn-expanded-read', text: 'keep reading' }) + [Symbol.asyncIterator](); + try { + await activeTurn.next(); + await assert.rejects(restoreExplore(), (error: unknown) => { + assert.ok(error instanceof SessionConfigurationTransitionError); + assert.strictEqual(error.code, 'session_busy'); + return true; }); - assert.strictEqual(settlement.request.status, 'approved'); - if (route === 'configuration') await updatePermissionMode('ask'); - const restoreExplore = () => - route === 'configuration' - ? updatePermissionMode('explore') - : manager.setExecutionBoundaryKind(session.id, 'managed'); - const expanded = await store.readExecutionBoundary(session.id); - assert.strictEqual(expanded.kind, 'managed'); - if (expanded.kind !== 'managed') throw new Error('Expected a managed boundary'); - assert.strictEqual(expanded.profile.name, 'read-only'); - assert.strictEqual(isReadOnlyPermissionProfile(expanded.profile), grant === 'read'); - assert.strictEqual( - expanded.profile.network.kind, - grant === 'network' ? 'enabled' : 'restricted', - ); - assert.strictEqual( - canReadPath(expanded.profile, outsidePath, { workspaceRoots: [workspaceRoot] }), - grant !== 'network', - ); + assert.deepStrictEqual(await store.readExecutionBoundary(session.id), expanded); + assert.strictEqual((await store.readHeader(session.id)).permissionMode, 'ask'); assert.deepStrictEqual(calls, []); + } finally { + gate.release(); + while (!(await activeTurn.next()).done) {} + } - const activeTurn = manager - .sendMessage(session.id, { turnId: 'turn-expanded-read', text: 'keep reading' }) - [Symbol.asyncIterator](); - try { - await activeTurn.next(); - await assert.rejects(restoreExplore(), (error: unknown) => { - if (route === 'boundary') { - assert.ok(error instanceof SessionConfigurationTransitionError); - assert.strictEqual(error.code, 'session_busy'); - return true; - } - assert.ok(error instanceof SessionConfigurationTransitionError); - assert.strictEqual(error.code, 'session_busy'); - return true; - }); - assert.deepStrictEqual(await store.readExecutionBoundary(session.id), expanded); - assert.strictEqual( - (await store.readHeader(session.id)).permissionMode, - route === 'configuration' ? 'ask' : 'explore', - ); - assert.deepStrictEqual(calls, []); - } finally { - gate.release(); - while (!(await activeTurn.next()).done) {} - } - - await restoreExplore(); - assert.deepStrictEqual(calls, [ - `terminate:${session.id}`, - 'commit', - `resume:${session.id}`, - ]); - const narrowed = await store.readExecutionBoundary(session.id); - assert.strictEqual(narrowed.kind, 'managed'); - if (narrowed.kind !== 'managed') throw new Error('Expected a managed boundary'); - assert.deepStrictEqual(narrowed.profile, createReadOnlyPermissionProfile()); - assert.strictEqual( - canReadPath(narrowed.profile, outsidePath, { workspaceRoots: [workspaceRoot] }), - false, - ); - assert.strictEqual((await store.readHeader(session.id)).permissionMode, 'explore'); - }); - } + await restoreExplore(); + assert.deepStrictEqual(calls, [`terminate:${session.id}`, 'commit', `resume:${session.id}`]); + const narrowed = await store.readExecutionBoundary(session.id); + assert.strictEqual(narrowed.kind, 'managed'); + if (narrowed.kind !== 'managed') throw new Error('Expected a managed boundary'); + assert.deepStrictEqual(narrowed.profile, createReadOnlyPermissionProfile()); + assert.strictEqual( + canReadPath(narrowed.profile, outsidePath, { workspaceRoots: [workspaceRoot] }), + false, + ); + assert.strictEqual((await store.readHeader(session.id)).permissionMode, 'explore'); + }); } - test('revokes descendant background shell authority through the direct boundary API', async () => { - const store = new AtomicBoundaryMemorySessionStore(); + test('configuration narrowing revokes descendant background shell authority', async () => { + const store = new VersionedConfigurationMemorySessionStore(); const calls: string[] = []; const backends = new BackendRegistry(); backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); @@ -5446,7 +5475,13 @@ describe('SessionManager permission mode updates', () => { ); store.disposeCount = 0; - await manager.setExecutionBoundaryKind(session.id, 'managed'); + const current = await store.readHeaderRecordSnapshot(session.id); + await manager.transitionSessionConfiguration(session.id, { + expectedRevision: current.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(current.header, { permissionMode: 'ask' }), + }); assert.deepStrictEqual(calls, [ `terminate:${session.id}`, @@ -5573,12 +5608,8 @@ describe('SessionManager permission mode updates', () => { assert.strictEqual(persisted.revision, next.revision); }); - for (const missing of [ - ['readHeaderRecordSnapshot'], - ['updateSessionConfiguration'], - ['readHeaderRecordSnapshot', 'updateSessionConfiguration'], - ] as const) { - test(`configuration changes require Store capabilities: missing ${missing.join(', ')}`, async () => { + for (const missing of ['readHeaderRecordSnapshot', 'updateSessionConfiguration'] as const) { + test(`configuration changes require Store capability: ${missing}`, async () => { const store = new VersionedConfigurationMemorySessionStore(); const manager = new SessionManager({ store, @@ -5590,12 +5621,7 @@ describe('SessionManager permission mode updates', () => { const readSnapshot = store.readHeaderRecordSnapshot.bind(store); const current = await readSnapshot(session.id); const boundary = await store.readExecutionBoundary(session.id); - for (const capability of missing) { - Object.defineProperty(store, capability, { value: undefined }); - } - store.updateHeader = async () => assert.fail('Must not fall back to header writes'); - store.setExecutionBoundaryKind = async () => - assert.fail('Must not fall back to boundary writes'); + Object.defineProperty(store, missing, { value: undefined }); await assert.rejects( manager.transitionSessionConfiguration(session.id, { @@ -14235,79 +14261,6 @@ class VersionedConfigurationMemorySessionStore extends MemorySessionStore { } } -class AtomicBoundaryMemorySessionStore extends MemorySessionStore { - failAppends = false; - readonly boundaryCalls: Array<{ - sessionId: string; - kind: 'managed' | 'bypass'; - projection: - | { - permissionMode: SessionHeader['permissionMode']; - labels?: readonly string[]; - } - | undefined; - }> = []; - private readonly boundaries = new Map(); - private projectingBoundary = false; - - forceBoundary(sessionId: string, boundary: ExecutionBoundary): void { - this.boundaries.set(sessionId, boundary); - } - - override async readExecutionBoundary(sessionId: string): Promise { - return this.boundaries.get(sessionId) ?? super.readExecutionBoundary(sessionId); - } - - async setExecutionBoundaryKind( - sessionId: string, - kind: 'managed' | 'bypass', - projection?: { - permissionMode: SessionHeader['permissionMode']; - labels?: readonly string[]; - }, - ) { - this.boundaryCalls.push({ sessionId, kind, projection }); - const current = await this.readHeader(sessionId); - const permissionMode = - projection?.permissionMode ?? - (kind === 'bypass' - ? 'bypass' - : current.permissionMode === 'bypass' - ? 'ask' - : current.permissionMode); - this.projectingBoundary = true; - try { - await super.updateHeader(sessionId, { - permissionMode, - ...(projection?.labels ? { labels: [...projection.labels] } : {}), - }); - } finally { - this.projectingBoundary = false; - } - const boundary = { - ...createGenesisExecutionBoundary(permissionMode), - revision: 1, - }; - this.boundaries.set(sessionId, boundary); - return boundary; - } - - override async updateHeader( - sessionId: string, - patch: Partial, - ): Promise { - if (!this.projectingBoundary && Object.hasOwn(patch, 'permissionMode')) { - throw new Error('permissionMode must be projected by the boundary transition'); - } - return super.updateHeader(sessionId, patch); - } - - override async appendMessage(sessionId: string, message: StoredMessage): Promise { - if (this.failAppends) throw new Error('audit append failed'); - return super.appendMessage(sessionId, message); - } -} - class MemoryAgentRunStore implements AgentRunStore, RuntimeEventStore, RuntimeContinuationAuthorityStore { diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts index b56ac40a4a..0cac2beb3d 100644 --- a/packages/runtime/src/session-manager.ts +++ b/packages/runtime/src/session-manager.ts @@ -613,14 +613,6 @@ export interface SessionStore { settleSandboxBoundaryRequest?( input: SettleSandboxBoundaryRequest, ): Promise; - setExecutionBoundaryKind( - sessionId: string, - kind: 'managed' | 'bypass', - projection?: { - permissionMode: SessionHeader['permissionMode']; - labels?: readonly string[]; - }, - ): Promise; createAgentGraphOperator?( input: CreateSessionInput, request: AgentGraphOperatorProvisionRequest, @@ -649,16 +641,14 @@ export interface SessionStore { expectedRevision: number, ): Promise; /** - * Versioned configuration authority requires both this method and - * updateSessionConfiguration. Stores may omit these capabilities when they - * do not support configuration changes; Runtime rejects those operations - * with operation_unavailable rather than falling back to unversioned writes. + * Configuration changes require both readHeaderRecordSnapshot and + * updateSessionConfiguration. Production SessionAuthorityStore requires both; + * Runtime keeps them optional for stores that do not mutate configuration, + * such as execution-only test fixtures. Missing either makes changes unavailable, + * without an unversioned fallback. Implementations must atomically check the + * expected revision and commit configuration, execution boundary and revision. */ readHeaderRecordSnapshot?(sessionId: string): Promise; - /** - * Atomically check the expected revision and commit configuration, execution - * boundary and the new revision. Requires readHeaderRecordSnapshot. - */ updateSessionConfiguration?( sessionId: string, input: SessionConfigurationStoreUpdate, @@ -1650,43 +1640,6 @@ export class SessionManager { return this.runtimeKernel.listActiveInteractions?.(sessionId) ?? []; } - async setExecutionBoundaryKind( - sessionId: string, - kind: 'managed' | 'bypass', - ): Promise { - const current = await this.deps.store.readExecutionBoundary(sessionId); - const header = await this.deps.store.readHeader(sessionId); - // Managed includes Explore. Match Storage's default projection, then pass - // it explicitly so classification and commit describe the same transition. - const permissionMode = - kind === 'bypass' - ? 'bypass' - : header.permissionMode === 'bypass' - ? 'ask' - : header.permissionMode; - const narrows = narrowsExecutionAuthority(current, permissionMode); - if (narrows && this.runtimeKernel.hasActiveRuns(sessionId)) { - throw new SessionConfigurationTransitionError( - 'session_busy', - 'Execution boundary cannot change while a Turn is running', - ); - } - if (header.status === 'waiting_for_user') { - throw new SessionConfigurationTransitionError( - 'session_busy', - 'Execution boundary cannot change while an Interaction is pending', - ); - } - const boundary = await this.commitExecutionBoundaryTransition( - sessionId, - current, - permissionMode, - async () => () => - this.deps.store.setExecutionBoundaryKind(sessionId, kind, { permissionMode }), - ); - return boundary; - } - private async commitExecutionBoundaryTransition( sessionId: string, current: ExecutionBoundary, From 239da078497455f5cba1aeeeffe80d54a860c77b Mon Sep 17 00:00:00 2001 From: chinawch007 Date: Fri, 18 Sep 2026 21:29:08 +0800 Subject: [PATCH 3/3] test(runtime): preserve permission configuration invariants Preserve executorId when migrating permission changes to configuration authority and cover widening with an active plugin-executor Turn. Reject permissionMode patches through ordinary and versioned header updates in the configuration Store double. Keep header and boundary projection inside configuration commits and remove the unused memory boundary setter and duplicate header write. Verify the executor regression fails without the fix and injected header permission writes are rejected. All 560 affected tests and the prescribed lint, format, build, typecheck, and knip checks pass. Refs #4795 Generated-by: Codex --- .../src/__tests__/session-manager.test.ts | 86 +++++++++++-------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts index fb85d78d26..d998634dae 100644 --- a/packages/runtime/src/__tests__/session-manager.test.ts +++ b/packages/runtime/src/__tests__/session-manager.test.ts @@ -4697,6 +4697,39 @@ describe('SessionManager manual compaction and quiescent session changes', () => }); describe('SessionManager permission mode updates', () => { + test('preserves the plugin executor when widening permission with an active turn', async () => { + const store = new VersionedConfigurationMemorySessionStore(); + const kernel = new DelegatingRuntimeKernel(); + const manager = new SessionManager({ + store, + backends: new BackendRegistry(), + newId: nextId(), + now: nextNow(977), + runtimeKernel: kernel, + }); + const session = await manager.createSession( + makeInput({ permissionMode: 'explore', executorId: 'codex' }), + ); + const current = await store.readHeaderRecordSnapshot(session.id); + kernel.activeRuns = true; + + const next = await manager.transitionSessionConfiguration(session.id, { + expectedRevision: current.revision, + clearConnectionBlock: false, + permissionModeOnly: true, + configuration: configurationForHeader(current.header, { permissionMode: 'bypass' }), + }); + + assert.strictEqual(next.header.backend, 'plugin-executor'); + assert.strictEqual(next.header.executorId, 'codex'); + assert.strictEqual(next.header.permissionMode, 'bypass'); + assert.strictEqual(next.revision, current.revision + 1); + assert.deepStrictEqual((await store.readHeaderRecordSnapshot(session.id)).header, next.header); + assert.strictEqual((await store.readExecutionBoundary(session.id)).kind, 'bypass'); + assert.deepStrictEqual(kernel.stopped, []); + assert.deepStrictEqual(kernel.disposed, []); + }); + test('configuration authority rejects a stale revision without changing the committed grant', async () => { const store = new VersionedConfigurationMemorySessionStore(); const manager = new SessionManager({ @@ -13861,7 +13894,7 @@ class CheckpointRecorderContractProbeBackend implements AgentBackend { class MemorySessionStore implements SessionStore { private headers = new Map(); private messages = new Map(); - private executionBoundaries = new Map(); + protected readonly executionBoundaries = new Map(); private sandboxBoundaryRequests = new Map(); readonly failReadMessagesFor = new Set(); readonly failNextReadMessagesFor = new Map(); @@ -13994,31 +14027,6 @@ class MemorySessionStore implements SessionStore { return header; } - async setExecutionBoundaryKind( - sessionId: string, - kind: 'managed' | 'bypass', - projection?: { - permissionMode: SessionHeader['permissionMode']; - labels?: readonly string[]; - }, - ) { - const current = await this.readHeader(sessionId); - const permissionMode = - projection?.permissionMode ?? - (kind === 'bypass' - ? 'bypass' - : current.permissionMode === 'bypass' - ? 'ask' - : current.permissionMode); - await this.updateHeader(sessionId, { - permissionMode, - ...(projection?.labels ? { labels: [...projection.labels] } : {}), - }); - const boundary = createGenesisExecutionBoundary(permissionMode); - this.executionBoundaries.set(sessionId, boundary); - return boundary; - } - async readExecutionBoundary(sessionId: string): Promise { const boundary = this.executionBoundaries.get(sessionId); if (!boundary) throw new Error(`Unknown session ${sessionId}`); @@ -14208,6 +14216,16 @@ class VersionedConfigurationMemorySessionStore extends MemorySessionStore { }; } + override async updateHeader( + sessionId: string, + patch: Partial, + ): Promise { + if (Object.hasOwn(patch, 'permissionMode')) { + throw new Error('permissionMode must be projected by the configuration transition'); + } + return super.updateHeader(sessionId, patch); + } + async updateSessionConfiguration( sessionId: string, input: SessionConfigurationStoreUpdate, @@ -14222,14 +14240,7 @@ class VersionedConfigurationMemorySessionStore extends MemorySessionStore { if (revision !== input.expectedVersion) { throw new Error('injected configuration revision conflict'); } - await super.setExecutionBoundaryKind( - sessionId, - input.configuration.permissionMode === 'bypass' ? 'bypass' : 'managed', - { - permissionMode: input.configuration.permissionMode, - labels: input.configuration.labels, - }, - ); + // Only configuration authority may project permissionMode into the header. const header = await super.updateHeader(sessionId, { ...input.configuration, labels: [...input.configuration.labels], @@ -14241,6 +14252,10 @@ class VersionedConfigurationMemorySessionStore extends MemorySessionStore { } : {}), }); + this.executionBoundaries.set( + sessionId, + createGenesisExecutionBoundary(input.configuration.permissionMode), + ); this.forcedBoundaries.delete(sessionId); this.revisions.set(sessionId, revision + 1); return { header, revision: revision + 1, committedAt: revision + 1 }; @@ -14255,7 +14270,7 @@ class VersionedConfigurationMemorySessionStore extends MemorySessionStore { if (revision !== expectedRevision) { throw new SessionConfigurationRevisionConflictError(expectedRevision, revision); } - const header = await super.updateHeader(sessionId, patch); + const header = await this.updateHeader(sessionId, patch); this.revisions.set(sessionId, revision + 1); return { header, revision: revision + 1, committedAt: revision + 1 }; } @@ -14904,6 +14919,7 @@ function configurationForHeader( ): SessionConfigurationTransitionRequest['configuration'] { return { backend: header.backend, + executorId: header.executorId, ...(header.llmConnectionId === undefined ? {} : { llmConnectionId: header.llmConnectionId }), llmConnectionSlug: header.llmConnectionSlug, connectionLocked: header.connectionLocked,