diff --git a/config/scripts/vitest-sqlite-runtime-files.mjs b/config/scripts/vitest-sqlite-runtime-files.mjs index 40a668f58ebf..a0d4c30c8e97 100644 --- a/config/scripts/vitest-sqlite-runtime-files.mjs +++ b/config/scripts/vitest-sqlite-runtime-files.mjs @@ -207,6 +207,7 @@ export const SQLITE_RUNTIME_INCLUDE = [ 'src/main/runtime/agent-session-record-options.test.ts', 'src/main/runtime/agent-session-record-store-tab-table.test.ts', 'src/main/runtime/agent-session-record-store.test.ts', + 'src/main/runtime/agent-session-permission-backfill.test.ts', 'src/main/runtime/agent-session-released-reservation-replay.test.ts', 'src/main/runtime/agent-session-runtime-attribution.test.ts', 'src/main/runtime/agent-session-store-transactions.test.ts', diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-host.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-host.test.ts index 02bed410f400..631ff303cbd6 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-host.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-startup-attempt-host.test.ts @@ -91,7 +91,7 @@ async function startHost(): Promise { ...(readAcquisitionOptions ? { readAcquisitionOptions } : {}) }, modelCatalog: { - read: vi.fn(), + read: vi.fn(async () => ({ origin: 'unknown' as const })), recordLiveListing: vi.fn(), prewarm: vi.fn(async () => {}), stop: vi.fn(), @@ -637,3 +637,24 @@ describe('the attempt an acquire runs under', () => { ) }) }) + +describe('a chat after startup permission backfill', () => { + it.each(['ask', 'full-access'] as const)( + 'keeps %s before and after the first start, regardless of later Arguments', + async (mode) => { + await host.close(SESSION, 'evict') + await store.backfillChatPermissionModes(() => mode === 'full-access') + expect((await host.readOptions(SESSION)).current.permissionMode).toBe(mode) + await store.backfillChatPermissionModes(() => mode === 'ask') + await restartWith(publishFirst) + await heldBehindStart('hello') + expect(acquire.mock.calls.at(-1)![0].permissionMode).toBe(mode) + await host.handleAdapterEvent(startedEvent()) + await vi.advanceTimersByTimeAsync(50) + await host.flushStreamedEvents(SESSION) + expect(store.getRecord(SESSION)?.options?.permissionMode).toBe(mode) + await store.backfillChatPermissionModes(() => mode === 'ask') + expect(store.getRecord(SESSION)?.options?.permissionMode).toBe(mode) + } + ) +}) diff --git a/src/main/runtime/agent-session-permission-backfill.test.ts b/src/main/runtime/agent-session-permission-backfill.test.ts new file mode 100644 index 000000000000..f7bc9bda36af --- /dev/null +++ b/src/main/runtime/agent-session-permission-backfill.test.ts @@ -0,0 +1,203 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { codexProviderHandle } from '../../shared/agent-session-provider-handle-encoding' +import { foundAgentSessionRecord } from './agent-session-record-founding' +import { + openTestAgentSessionRecordStore, + seedTestAgentSessionRecordStore, + seedTestAgentSessionStoreFromNewerBuild +} from './agent-session-record-store-test-harness' +import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support' + +let root: string +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'orca-permission-backfill-')) +}) +afterEach(async () => { + await rm(root, { recursive: true, force: true }) +}) + +function record(provider: string, options?: Record) { + return foundAgentSessionRecord( + { + sessionId: `session-${provider}`, + provider, + location: { + executionHostId: 'local', + workspaceId: 'folder-1', + workspaceKind: 'folder', + wslDistro: null + }, + accountHome: { + variable: provider === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME', + path: '/account' + }, + ...(options ? { options } : {}) + }, + { claimKeyId: 'key-1', now: 1 } + ) +} + +describe('existing chat permission backfill', () => { + it.each(['full-access', 'ask'] as const)( + "saves %s from the agent's own Arguments for old Claude and Codex rows, keeping other options", + async (mode) => { + await seedTestAgentSessionRecordStore(root, { + records: [record('claude', { model: 'opus', effort: 'high' }), record('codex')] + }) + const store = await openTestAgentSessionRecordStore(root) + const beforeClaude = store.getRecord('session-claude') + const beforeCodex = store.getRecord('session-codex') + await store.backfillChatPermissionModes(() => mode === 'full-access') + expect(store.getRecord('session-claude')?.options).toEqual({ + model: 'opus', + effort: 'high', + permissionMode: mode + }) + expect(store.getRecord('session-codex')?.options).toEqual({ permissionMode: mode }) + expect(store.getRecord('session-claude')).toEqual({ + ...beforeClaude, + options: { model: 'opus', effort: 'high', permissionMode: mode } + }) + expect(store.getRecord('session-codex')).toEqual({ + ...beforeCodex, + options: { permissionMode: mode } + }) + const reopened = await openTestAgentSessionRecordStore(root) + expect(reopened.getRecord('session-claude')?.options).toEqual( + store.getRecord('session-claude')?.options + ) + expect(reopened.getRecord('session-codex')?.options).toEqual( + store.getRecord('session-codex')?.options + ) + } + ) + + it.each(['ask', 'full-access', 'invalid-saved-mode'] as const)( + 'never replaces saved %s or options on other providers', + async (mode) => { + await seedTestAgentSessionRecordStore(root, { + records: [ + record('claude', { permissionMode: mode }), + record('codex', { permissionMode: mode }), + record('grok', { model: 'grok' }) + ] + }) + const store = await openTestAgentSessionRecordStore(root) + const before = store.listRecords() + await store.backfillChatPermissionModes(() => mode !== 'full-access') + expect(store.listRecords()).toEqual(before) + expect(store.getRecord('session-grok')?.options).toEqual({ model: 'grok' }) + } + ) + + it('does no row write on its second pass, even with changed Arguments', async () => { + await seedTestAgentSessionRecordStore(root, { records: [record('claude'), record('codex')] }) + const store = await openTestAgentSessionRecordStore(root) + await store.backfillChatPermissionModes(() => true) + const before = store.listRecords() + const db = openTestJournalHostDatabase(root).db + db.exec( + "CREATE TRIGGER refuse_backfill_write BEFORE UPDATE ON agent_session_records BEGIN SELECT RAISE(ABORT, 'unexpected second write'); END" + ) + await expect(store.backfillChatPermissionModes(() => false)).resolves.toBeUndefined() + expect(store.listRecords()).toEqual(before) + expect(store.getRecord('session-claude')).toBe(before.find((row) => row.provider === 'claude')) + expect(store.getRecord('session-codex')?.options?.permissionMode).toBe('full-access') + }) + + it('skips a store written by a newer build without an error or in-memory change', async () => { + await seedTestAgentSessionRecordStore(root, { records: [record('claude')] }) + await seedTestAgentSessionStoreFromNewerBuild(root) + const store = await openTestAgentSessionRecordStore(root) + expect(store.readOnly).toBe(true) + expect(store.getRecord('session-claude')).not.toBeNull() + await expect(store.backfillChatPermissionModes(() => true)).resolves.toBeUndefined() + expect(store.getRecord('session-claude')?.options).toBeUndefined() + }) + + it.each([ + ['claude', 'full-access', 'ask'], + ['codex', 'ask', 'full-access'] + ] as const)( + "fills each row from its own agent's Arguments when only %s skips prompts", + async (bypassing, claude, codex) => { + await seedTestAgentSessionRecordStore(root, { + records: [record('claude'), record('codex'), record('grok')] + }) + const store = await openTestAgentSessionRecordStore(root) + await store.backfillChatPermissionModes((provider) => provider === bypassing) + expect(store.getRecord('session-claude')?.options).toEqual({ permissionMode: claude }) + expect(store.getRecord('session-codex')?.options).toEqual({ permissionMode: codex }) + expect(store.getRecord('session-grok')?.options).toBeUndefined() + } + ) + + it('preserves the existing owner identity, fence, timestamps and provider history', async () => { + const old = record('codex', { model: 'old-model', effort: 'high' }) + await seedTestAgentSessionRecordStore(root, { + records: [ + { + ...old, + lease: { + ...old.lease, + runtimeFence: 7, + claimStatus: 'reserved', + handoffStage: 'new-owner-proving', + reservedSpawnToken: 'old-spawn', + ownerProcess: { + hostId: 'local', + pid: 42, + processStartTimeMs: 1, + spawnToken: 'old-spawn' + } + }, + providerHandleChain: [ + { + linkId: 'old-link', + handle: codexProviderHandle('old-thread'), + origin: 'created', + mintedAtFence: 6, + observedAt: 1 + } + ] + } + ] + }) + const store = await openTestAgentSessionRecordStore(root) + const before = store.getRecord('session-codex') + expect(before).not.toBeNull() + await store.backfillChatPermissionModes(() => true) + expect(store.getRecord('session-codex')).toEqual({ + ...before, + options: { + model: 'old-model', + effort: 'high', + permissionMode: 'full-access' + } + }) + }) + + it('rolls back every row if saving fails, then succeeds on the next startup pass', async () => { + await seedTestAgentSessionRecordStore(root, { records: [record('claude'), record('codex')] }) + const store = await openTestAgentSessionRecordStore(root) + const db = openTestJournalHostDatabase(root).db + db.exec( + "CREATE TRIGGER refuse_backfill_write BEFORE UPDATE ON agent_session_records WHEN NEW.session_id = 'session-codex' BEGIN SELECT RAISE(ABORT, 'disk full'); END" + ) + await expect(store.backfillChatPermissionModes(() => true)).rejects.toThrow('disk full') + expect(store.listRecords().every((row) => row.options === undefined)).toBe(true) + expect( + (await openTestAgentSessionRecordStore(root)) + .listRecords() + .every((row) => row.options === undefined) + ).toBe(true) + db.exec('DROP TRIGGER refuse_backfill_write') + await store.backfillChatPermissionModes(() => true) + expect(store.listRecords().every((row) => row.options?.permissionMode === 'full-access')).toBe( + true + ) + }) +}) diff --git a/src/main/runtime/agent-session-permission-backfill.ts b/src/main/runtime/agent-session-permission-backfill.ts new file mode 100644 index 000000000000..ca7088d75374 --- /dev/null +++ b/src/main/runtime/agent-session-permission-backfill.ts @@ -0,0 +1,32 @@ +import { + AGENT_CHAT_PERMISSION_MODE_OPTION_ID, + newChatPermissionMode +} from '../../shared/agent-chat-permission-mode' +import type { AgentSessionStoreTransactions } from './agent-session-store-transactions' + +/** One transaction as the chat host installs; before modes, chats ran their own agent's Arguments. */ +export function backfillAgentSessionPermissionModes( + store: AgentSessionStoreTransactions, + ranWithFullAccess: (provider: string) => boolean +): Promise { + if (store.readOnly) { + return Promise.resolve() + } + return store.transact((draft) => { + for (const [sessionId, record] of draft.records) { + if (record.options?.[AGENT_CHAT_PERMISSION_MODE_OPTION_ID] !== undefined) { + continue + } + const permissionMode = newChatPermissionMode( + record.provider, + ranWithFullAccess(record.provider) ? 'full-access' : 'ask' + ) + if (permissionMode) { + draft.records.set(sessionId, { + ...record, + options: { ...record.options, [AGENT_CHAT_PERMISSION_MODE_OPTION_ID]: permissionMode } + }) + } + } + }) +} diff --git a/src/main/runtime/agent-session-record-store.test.ts b/src/main/runtime/agent-session-record-store.test.ts index cc0a83cc55f2..b3f19910cb4d 100644 --- a/src/main/runtime/agent-session-record-store.test.ts +++ b/src/main/runtime/agent-session-record-store.test.ts @@ -286,6 +286,21 @@ describe('acquisition path', () => { ) ).rejects.toThrow('agent_session_conflict') }) + + it("takes request options only for a record it creates, so an existing chat's are kept", async () => { + const store = await open() + await establishOwner(store, { options: { permissionMode: 'ask' } }) + await store.reserveOwner( + reserveRequest({ + options: { permissionMode: 'full-access' }, + expectedFence: 1, + probe: { outcome: 'pid-absent' }, + operation: { callerKey: 'client-1', operationId: operationId(), fingerprint: 'fp-2' } + }) + ) + expect(store.getRecord('session-alpha')?.lease.runtimeFence).toBe(2) + expect(store.getRecord('session-alpha')?.options).toEqual({ permissionMode: 'ask' }) + }) }) describe('concurrent claims', () => { diff --git a/src/main/runtime/agent-session-record-store.ts b/src/main/runtime/agent-session-record-store.ts index 5601d2a2e00c..02a1ab2f63a4 100644 --- a/src/main/runtime/agent-session-record-store.ts +++ b/src/main/runtime/agent-session-record-store.ts @@ -1,6 +1,7 @@ /** Durable single-writer session records and their operation ledger, as rows in the host's chat * journal database. */ +import { backfillAgentSessionPermissionModes } from './agent-session-permission-backfill' import { agentSessionRefusalError } from '../../shared/agent-session-wire-refusals' import { createAgentSessionConversationReceipts } from './agent-session-conversation-receipts' import { commitConversationCommandRecord } from './agent-session-conversation-command-record' @@ -115,6 +116,9 @@ export class AgentSessionRecordStore { return this.transactions.readOnly } + backfillChatPermissionModes = (ranWithFullAccess: (provider: string) => boolean): Promise => + backfillAgentSessionPermissionModes(this.transactions, ranWithFullAccess) + getRecord = (sessionId: string): AgentSessionRecord | null => this.state.records.get(sessionId) ?? null diff --git a/src/main/runtime/claude-structured-session-integration.test.ts b/src/main/runtime/claude-structured-session-integration.test.ts index dedde5033497..45baed003b13 100644 --- a/src/main/runtime/claude-structured-session-integration.test.ts +++ b/src/main/runtime/claude-structured-session-integration.test.ts @@ -229,6 +229,7 @@ beforeEach(async () => { getRuntimeId: () => 'runtime-1', getClientSettings: () => ({ experimentalNativeChat: true }), getStructuredAgentSessionCreateSupport: async () => ({ supported: true }), + structuredAgentSessionLaunchSeedOptions: () => undefined, resolveStructuredAgentSessionCreateIntent: async (input: { envelope: unknown }) => ({ ...ensureParams(1), envelope: input.envelope, diff --git a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts index dd81e303b41d..f3442ce6f6e8 100644 --- a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts +++ b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts @@ -37,6 +37,9 @@ describe('execution host structured launch settings wiring', () => { settings.agentDefaultArgs = { claude: '--model second', codex: '' } expect(await deps?.resolveLaunchArgs?.('claude')).toEqual(['--model', 'second']) expect(await deps?.resolveLaunchArgs?.('codex')).toEqual([]) + settings.agentDefaultArgs = { claude: '--dangerously-skip-permissions', codex: '' } + expect(deps?.resolveAgentFullAccess?.('claude')).toBe(true) + expect(deps?.resolveAgentFullAccess?.('codex')).toBe(false) const notRunnable = expect.objectContaining({ reason: 'agentCommandNotRunnable' }) settings.agentCmdOverrides = { claude: 'wrapper --arg' } expect(() => deps?.resolveClaudeCommand?.()).toThrow(notRunnable) diff --git a/src/main/runtime/rpc/methods/structured-agent-session-create-handler.ts b/src/main/runtime/rpc/methods/structured-agent-session-create-handler.ts index a917eff7eef4..335648ecd87d 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-create-handler.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-create-handler.ts @@ -1,3 +1,4 @@ +import { AGENT_CHAT_PERMISSION_MODE_OPTION_ID } from '../../../../shared/agent-chat-permission-mode' import type { z } from 'zod' import { agentSessionRefusalError } from '../../../../shared/agent-session-wire-refusals' import { agentSessionFingerprintConflict } from '../../../../shared/agent-session-mutation-envelope' @@ -32,7 +33,16 @@ async function resolveClientSuppliedAttach(params: z.infer, reason: 'hostUnsupported' }) } - const attachParams: AgentSessionAttachParams = params + // Reservation applies options only to a record it creates, so an existing chat is never reseeded. + const permissionMode = ctx.runtime.structuredAgentSessionLaunchSeedOptions(params.agent)?.[ + AGENT_CHAT_PERMISSION_MODE_OPTION_ID + ] + const attachParams: AgentSessionAttachParams = { + ...params, + ...(permissionMode + ? { options: { [AGENT_CHAT_PERMISSION_MODE_OPTION_ID]: permissionMode } } + : {}) + } return { host, attachParams } } diff --git a/src/main/runtime/rpc/methods/structured-agent-session-permission-create.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-permission-create.test.ts new file mode 100644 index 000000000000..46a9b75fcd75 --- /dev/null +++ b/src/main/runtime/rpc/methods/structured-agent-session-permission-create.test.ts @@ -0,0 +1,42 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + attachParams, + call, + clearStructuredHostStub, + hostCalls, + installStructuredHostStub, + STRUCTURED_CLIENT +} from './structured-agent-session-rpc.test-fixture' + +beforeEach(() => { + installStructuredHostStub() +}) +afterEach(() => { + clearStructuredHostStub() +}) + +describe('legacy attach-shaped permission creation', () => { + it.each(['agentSession.create', 'agentSession.ensure'] as const)( + '%s gives a new record only the host permission seed', + async (method) => { + const seed = vi.fn(() => ({ + permissionMode: 'full-access', + model: 'host-model', + effort: 'high' + })) + const params = attachParams() + expect( + await call(method, params, STRUCTURED_CLIENT, { + structuredAgentSessionLaunchSeedOptions: seed + }) + ).toMatchObject({ ok: true }) + expect(hostCalls.attach).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + options: { permissionMode: 'full-access' }, + envelope: params.envelope + }) + ) + } + ) +}) diff --git a/src/main/runtime/structured-agent-session-integration.test.ts b/src/main/runtime/structured-agent-session-integration.test.ts index deaee6763c34..6f6b38378cd2 100644 --- a/src/main/runtime/structured-agent-session-integration.test.ts +++ b/src/main/runtime/structured-agent-session-integration.test.ts @@ -330,6 +330,7 @@ beforeEach(async () => { getRuntimeId: () => 'runtime-1', getClientSettings: () => ({ experimentalNativeChat: true }), getStructuredAgentSessionCreateSupport: async () => ({ supported: true }), + structuredAgentSessionLaunchSeedOptions: () => undefined, resolveStructuredAgentSessionCreateIntent: async () => { const { envelope: _envelope, diff --git a/src/main/runtime/structured-agent-session-runtime.test.ts b/src/main/runtime/structured-agent-session-runtime.test.ts index 947b73dfdd0c..c48b1d466746 100644 --- a/src/main/runtime/structured-agent-session-runtime.test.ts +++ b/src/main/runtime/structured-agent-session-runtime.test.ts @@ -23,6 +23,10 @@ import { hasPersistedStructuredAgentSessionStore, stopStructuredAgentSessionRuntime } from './structured-agent-session-runtime' +import { seedTestAgentSessionRecordStore } from './agent-session-record-store-test-harness' +import { foundAgentSessionRecord } from './agent-session-record-founding' +import { openTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support' +import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support' import { createStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger' const HOST_ID = 'local' @@ -220,6 +224,85 @@ describe('structured agent-session runtime install', () => { vi.restoreAllMocks() }) + function oldChat(provider: 'claude' | 'codex'): AgentSessionRecord { + return foundAgentSessionRecord( + { + sessionId: `old-${provider}`, + provider, + location: { + executionHostId: HOST_ID, + workspaceId: 'folder-1', + workspaceKind: 'folder', + wslDistro: null + }, + accountHome: { + variable: provider === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME', + path: '/account' + } + }, + { claimKeyId: 'key-1', now: 1 } + ) + } + + async function installOver( + fullAccessAgents: readonly string[], + logger = recordingStructuredAgentSessionLogger().logger + ) { + return await ensureStructuredAgentSessionHost({ + logger, + stateDirectory: stateDirectory!, + hostId: HOST_ID, + claimKeyId: 'key-1', + resolveWorkspacePath: async () => stateDirectory!, + resolveLaunchArgs: () => [], + resolveClaudeAuthPolicy: () => ({ account: 'managed' }), + resolveAgentFullAccess: (agent) => fullAccessAgents.includes(agent), + resolveEnvironment: async () => ({}) + }) + } + + it.each([false, true])( + 'backfills before the host is readable and keeps startup nonfatal on a failed write (%s)', + async (failWrite) => { + stateDirectory = await mkdtemp(join(tmpdir(), 'orca-structured-runtime-backfill-')) + await seedTestAgentSessionRecordStore(stateDirectory, { records: [oldChat('codex')] }) + if (failWrite) { + openTestJournalHostDatabase(stateDirectory).db.exec( + "CREATE TRIGGER fail_permission_backfill BEFORE UPDATE ON agent_session_records BEGIN SELECT RAISE(ABORT, 'disk full'); END" + ) + } + const logged = recordingStructuredAgentSessionLogger() + const host = await installOver(['codex'], logged.logger) + expect(host.deps.store.getRecord('old-codex')?.options?.permissionMode).toBe( + failWrite ? undefined : 'full-access' + ) + expect((await host.readOptions('old-codex')).current.permissionMode).toBe( + failWrite ? 'ask' : 'full-access' + ) + if (failWrite) { + expect(logged.scopes()).toContain('permission-mode-backfill') + } else { + expect(logged.scopes()).not.toContain('permission-mode-backfill') + } + } + ) + + it.each([ + [['claude'], 'full-access', 'ask'], + [['codex'], 'ask', 'full-access'] + ] as const)( + "fills each old chat from its own agent's arguments when %j skip prompts", + async (fullAccessAgents, claude, codex) => { + stateDirectory = await mkdtemp(join(tmpdir(), 'orca-structured-runtime-backfill-')) + await seedTestAgentSessionRecordStore(stateDirectory, { + records: [oldChat('claude'), oldChat('codex')] + }) + const host = await installOver(fullAccessAgents) + expect(host.deps.store.getRecord('old-claude')?.options?.permissionMode).toBe(claude) + expect(host.deps.store.getRecord('old-codex')?.options?.permissionMode).toBe(codex) + } + ) + it('holds stop until the model catalog has written its coalesced save', async () => { stateDirectory = await mkdtemp(join(tmpdir(), 'orca-structured-runtime-')) await ensureStructuredAgentSessionHost({ diff --git a/src/main/runtime/structured-agent-session-runtime.ts b/src/main/runtime/structured-agent-session-runtime.ts index 11afc2d16f46..34a68277adc8 100644 --- a/src/main/runtime/structured-agent-session-runtime.ts +++ b/src/main/runtime/structured-agent-session-runtime.ts @@ -121,7 +121,7 @@ export type StructuredAgentSessionRuntimeDeps = { resolveLaunchArgs: (provider: AgentSessionRecord['provider']) => Promise | string[] /** Required, and asserted at install time — an absent policy must not degrade to a guess. */ resolveClaudeAuthPolicy: () => Promise | ClaudeStructuredAuthPolicy - /** Agent Permissions for an ACP agent; Claude and Codex chats run their own permission mode. */ + /** Agent Permissions for an ACP agent; also fills Claude and Codex chats saved before modes. */ resolveAgentFullAccess?: (agent: string) => boolean /** The user's per-agent environment overlay from settings, laid over every agent's base env. */ resolveAgentLaunchEnv?: (agent: string) => Record @@ -263,6 +263,18 @@ async function install(deps: StructuredAgentSessionRuntimeDeps): Promise 'runtime-1', getClientSettings: () => ({ experimentalStructuredNativeChat: true }), getStructuredAgentSessionCreateSupport: async () => ({ supported: true }), + structuredAgentSessionLaunchSeedOptions: () => undefined, resolveStructuredAgentSessionCreateIntent: async () => { const { envelope: _envelope,