diff --git a/apps/server/src/features/agents/orchestration/__tests__/agent-service-gate.test.ts b/apps/server/src/features/agents/orchestration/__tests__/agent-service-gate.test.ts index 0aed63b67..4bbe3fee3 100644 --- a/apps/server/src/features/agents/orchestration/__tests__/agent-service-gate.test.ts +++ b/apps/server/src/features/agents/orchestration/__tests__/agent-service-gate.test.ts @@ -401,7 +401,7 @@ describe("AgentService internal MCP provider allowlist", () => { "Call delegate_task exactly once to hand this review to Codex. Do not call any other tool.", "Do not create an Mcode thread", ])("activates thread control whatever the message says: %s", async (content) => { - const { svc, threadControlMcp, providerStub } = buildService(); + const { svc, threadControlMcp } = buildService(); await svc.sendMessage({ threadId: THREAD_ID, @@ -418,9 +418,6 @@ describe("AgentService internal MCP provider allowlist", () => { sourceThreadId: THREAD_ID, permissionMode: "supervised", })); - expect(providerStub.sendTurn).toHaveBeenCalledWith(expect.objectContaining({ - threadControlEligible: true, - })); }); it("activates full thread control for a default command on a persisted full thread", async () => { diff --git a/apps/server/src/features/agents/orchestration/__tests__/turn-steer-delivery.test.ts b/apps/server/src/features/agents/orchestration/__tests__/turn-steer-delivery.test.ts index 021a2c810..c137f7ce5 100644 --- a/apps/server/src/features/agents/orchestration/__tests__/turn-steer-delivery.test.ts +++ b/apps/server/src/features/agents/orchestration/__tests__/turn-steer-delivery.test.ts @@ -50,7 +50,7 @@ class FakeTurnRuntime implements TurnSteerRuntime { turnExecutionId: this.running.turnExecutionId, workspaceId: "workspace", providerId: this.providerId, - threadControlEligible: false, + holdsDelegationLease: false, steer: this.running.steerable ? (input) => { this.sent.push(input); return this.respond(input); } : null, }; } diff --git a/apps/server/src/features/agents/orchestration/turn-runtime-controller.ts b/apps/server/src/features/agents/orchestration/turn-runtime-controller.ts index dad3d2a23..aa2ebe37a 100644 --- a/apps/server/src/features/agents/orchestration/turn-runtime-controller.ts +++ b/apps/server/src/features/agents/orchestration/turn-runtime-controller.ts @@ -329,7 +329,7 @@ export class TurnRuntimeController implements TurnLifecycleControl, TurnRuntimeE turnExecutionId: turn.turnExecutionId, workspaceId: owned.prepared.request.workspaceId, providerId: provider.id, - threadControlEligible: owned.prepared.request.threadControlEligible === true, + holdsDelegationLease: owned.prepared.threadControl !== null, steer: isTurnSteerCapable(provider) ? (input) => provider.steerTurn({ ...turn, ...input }) : null, }; } diff --git a/apps/server/src/features/agents/planning/__tests__/native-plan-questions.test.ts b/apps/server/src/features/agents/planning/__tests__/native-plan-questions.test.ts index 1390d1643..4526efa9a 100644 --- a/apps/server/src/features/agents/planning/__tests__/native-plan-questions.test.ts +++ b/apps/server/src/features/agents/planning/__tests__/native-plan-questions.test.ts @@ -107,7 +107,7 @@ async function fixture(interactionMode: "plan" | "build" = "plan") { const request: TurnRequest<"codex"> = { threadId: thread.id, turnId: "turn", turnExecutionId: "execution", sessionId: `mcode-${thread.id}`, workspaceId: workspace.id, cwd: process.cwd(), message: "Plan a greeting", model: "gpt-6.1-sol", permissionMode: "full", approvalReviewMode: "manual", interactionMode, - providerOptions: {}, threadControlEligible: false, + providerOptions: {}, ...(interactionMode === "plan" ? { planTurn: { kind: "planning", planFilePath: null } } : {}) }; const ask = async () => { await provider.sendTurn(request); diff --git a/apps/server/src/features/agents/steering/turn-steer-runtime-port.ts b/apps/server/src/features/agents/steering/turn-steer-runtime-port.ts index c6cd68eba..ea372fbdb 100644 --- a/apps/server/src/features/agents/steering/turn-steer-runtime-port.ts +++ b/apps/server/src/features/agents/steering/turn-steer-runtime-port.ts @@ -16,8 +16,8 @@ export interface SteerableTurn { /** Where the turn's mentions resolve, as for `agent.send`. */ readonly workspaceId: string; readonly providerId: ProviderId; - /** Whether the turn holds the thread-control lease, so a message steered into it can be acted on with task tools. */ - readonly threadControlEligible: boolean; + /** Whether the turn holds the Delegation lease, so a message steered into it can be acted on with task tools. */ + readonly holdsDelegationLease: boolean; readonly steer: ((input: TurnSteerMessage) => Promise) | null; } diff --git a/apps/server/src/features/agents/turns/turn-admission-dispatch-coordinator.ts b/apps/server/src/features/agents/turns/turn-admission-dispatch-coordinator.ts index 6d5f3a48f..69930e342 100644 --- a/apps/server/src/features/agents/turns/turn-admission-dispatch-coordinator.ts +++ b/apps/server/src/features/agents/turns/turn-admission-dispatch-coordinator.ts @@ -1205,7 +1205,6 @@ export class TurnAdmissionDispatchCoordinator { attachments: attachments.persisted.length > 0 ? attachments.persisted : undefined, reasoningLevel: prepared.command.reasoningLevel, ...guardrails, - threadControlEligible: supportsInternalThreadControl(prepared.providerId), resumeFrom: this.resumeFrom(prepared, attachments), ...(prepared.command.resumeRequired ? { resumeRequired: true } : {}), providerOptions: this.providerOptions(prepared, settings), diff --git a/apps/server/src/features/thread-control/delegation/coordinator-wake-service.ts b/apps/server/src/features/thread-control/delegation/coordinator-wake-service.ts index 5bdcd4389..c1fd76f2f 100644 --- a/apps/server/src/features/thread-control/delegation/coordinator-wake-service.ts +++ b/apps/server/src/features/thread-control/delegation/coordinator-wake-service.ts @@ -110,7 +110,7 @@ export class CoordinatorWakeService { const turn = this.deps.runtime.runningTurn(coordinatorId); if (turn) { // A turn without the lease cannot call task_status for detail, so the result waits for a turn that can. - if (!turn.steer || !turn.threadControlEligible || this.noSteer.get(coordinatorId) === turn.turnExecutionId) return; + if (!turn.steer || !turn.holdsDelegationLease || this.noSteer.get(coordinatorId) === turn.turnExecutionId) return; await this.deliverSteer(coordinatorId, turn.turnExecutionId); return; } diff --git a/apps/server/src/features/thread-control/delegation/delegated-task-service.test.ts b/apps/server/src/features/thread-control/delegation/delegated-task-service.test.ts index 9cf0da96f..c281da363 100644 --- a/apps/server/src/features/thread-control/delegation/delegated-task-service.test.ts +++ b/apps/server/src/features/thread-control/delegation/delegated-task-service.test.ts @@ -257,7 +257,7 @@ async function wakeHarness(f: Fixture) { const turn = f.canonical.loadLatestTurn(threadId); if (!turn?.executionId) return null; return { turnExecutionId: turn.executionId, workspaceId: f.workspace.id, providerId: "codex", - threadControlEligible: state.leased, steer: state.steerable ? async () => state.response : null }; + holdsDelegationLease: state.leased, steer: state.steerable ? async () => state.response : null }; }; const runtime: TurnSteerRuntime = { runningTurn, recordDelivered: async (message) => { await f.messages.create(message.threadId, "user", message.content, 3, undefined, undefined, undefined, diff --git a/packages/contracts/src/providers/interfaces.ts b/packages/contracts/src/providers/interfaces.ts index 6783498ed..e790cd732 100644 --- a/packages/contracts/src/providers/interfaces.ts +++ b/packages/contracts/src/providers/interfaces.ts @@ -129,8 +129,6 @@ export interface TurnRequest

{ maxBudgetUsd?: number; /** Max agent turns for this Turn. Undefined or 0 disables. */ maxTurns?: number; - /** Whether the server grants this turn Mcode thread control. */ - threadControlEligible?: boolean; /** * SDK session id to resume from. When defined the Provider resumes that * session; when undefined it starts fresh. Replaces the previous diff --git a/packages/providers/src/__tests__/codex/codex-native-plan.test.ts b/packages/providers/src/__tests__/codex/codex-native-plan.test.ts index 55ad4d410..31e65c64b 100644 --- a/packages/providers/src/__tests__/codex/codex-native-plan.test.ts +++ b/packages/providers/src/__tests__/codex/codex-native-plan.test.ts @@ -63,7 +63,7 @@ const turn: TurnRequest<"codex"> = { turnId: "turn", turnExecutionId: "execution", sessionId: "mcode-native-plan", threadId: "native-plan", workspaceId: "fixture", cwd: process.cwd(), message: "Plan a greeting", model: "gpt-6.1-sol", reasoningLevel: "low", permissionMode: "full", approvalReviewMode: "manual", interactionMode: "plan", - providerOptions: {}, threadControlEligible: false, planTurn: { kind: "planning", planFilePath: null }, + providerOptions: {}, planTurn: { kind: "planning", planFilePath: null }, }; function fixture( diff --git a/packages/providers/src/__tests__/codex/codex-provider-first-turn.test.ts b/packages/providers/src/__tests__/codex/codex-provider-first-turn.test.ts index 31e82e1c4..a1336a319 100644 --- a/packages/providers/src/__tests__/codex/codex-provider-first-turn.test.ts +++ b/packages/providers/src/__tests__/codex/codex-provider-first-turn.test.ts @@ -496,9 +496,9 @@ describe("CodexProvider first turn on new session", () => { await provider.stopSession(sessionId); }); - it("attaches for Browser alone when the turn is not thread-control eligible", async () => { - const attach = vi.fn().mockResolvedValue(mcodeAttachment({ browser: true, threadControl: false })); - const provider = makeProvider(undefined, configuredLease(), { attach }); + it("attaches the Delegation toolkit whatever the user's wording, without a Browser grant", async () => { + const attach = vi.fn().mockResolvedValue(mcodeAttachment({ browser: false, threadControl: true })); + const provider = makeProvider(undefined, new BrowserAutomationSessionLease(), { attach }); const send = provider.sendTurn({ turnId: "test-turn", @@ -506,19 +506,18 @@ describe("CodexProvider first turn on new session", () => { sessionId, workspaceId: "workspace-test", threadId, - message: "inspect the page", + message: "Farm this out to Claude", cwd: process.cwd(), model: "gpt-5.4", interactionMode: "build", providerOptions: {}, permissionMode: "supervised", - threadControlEligible: false, }); await vi.waitFor(() => expect(appServers.at(-1)).toBeDefined()); emitMcpStartup(appServers.at(-1)!, "mcode", "ready"); await send; - expect(attach).toHaveBeenCalledExactlyOnceWith({ providerId: "codex", sessionId, threadId, threadControl: false }); + expect(attach).toHaveBeenCalledExactlyOnceWith({ providerId: "codex", sessionId, threadId, threadControl: true }); await provider.stopSession(sessionId); }); @@ -622,7 +621,6 @@ describe("CodexProvider first turn on new session", () => { interactionMode: "build", providerOptions: {}, permissionMode: "supervised", - threadControlEligible: false, }); const server = appServers.at(-1)!; @@ -2808,71 +2806,6 @@ describe("CodexProvider first turn on new session", () => { await provider.stopSession(sessionId); }); - it("omits the mcode server for an ineligible turn without a Browser lease", async () => { - const attach = vi.fn().mockResolvedValue(mcodeAttachment()); - const provider = makeProvider(undefined, new BrowserAutomationSessionLease(), { attach }); - - await provider.sendTurn({ - turnId: "test-turn", - turnExecutionId: "ineligible-execution", - sessionId: "mcode-ineligible", - workspaceId: "workspace-test", - threadId: "ineligible", - message: "Spawn a provider-native child", - cwd: process.cwd(), - model: "gpt-5.4", - interactionMode: "build", - providerOptions: {}, - permissionMode: "supervised", - threadControlEligible: false, - }); - - const server = appServers.at(-1)!; - expect(attach).not.toHaveBeenCalled(); - expect(server.options.configOverrides).toEqual([]); - expect(server.spawnedEnv).not.toHaveProperty("MCODE_MCP_TOKEN"); - expect(server.options.instructionSections?.map((section) => section.key)).not.toContain("mcode_thread_control"); - await provider.stopSession("mcode-ineligible"); - }); - - it("restarts a pooled session when Mcode thread-control eligibility changes", async () => { - const attach = vi.fn().mockResolvedValue(mcodeAttachment({ browser: false, threadControl: true })); - const provider = makeProvider(undefined, new BrowserAutomationSessionLease(), { attach }); - - const request = { - turnId: "test-turn", - turnExecutionId: "eligible-execution", - sessionId: "mcode-eligibility-transition", - workspaceId: "workspace-test", - threadId: "eligibility-transition", - message: "Create an Mcode thread named child", - cwd: process.cwd(), - model: "gpt-5.4", - interactionMode: "build" as const, - providerOptions: {}, - permissionMode: "supervised", - }; - const firstSend = provider.sendTurn({ ...request, threadControlEligible: true }); - await vi.waitFor(() => expect(appServers.at(-1)).toBeDefined()); - emitMcpStartup(appServers[0]!, "mcode", "ready"); - await firstSend; - await provider.sendTurn({ - ...request, - turnExecutionId: "ineligible-execution", - message: "Spawn a provider-native child named child", - threadControlEligible: false, - }); - - expect(appServers).toHaveLength(2); - expect(appServers[0]?.options.configOverrides).toEqual([ - 'mcp_servers.mcode.url="http://127.0.0.1:19400/mcp/mcode"', - 'mcp_servers.mcode.bearer_token_env_var="MCODE_MCP_TOKEN"', - ]); - expect(appServers[1]?.options.configOverrides).toEqual([]); - expect(attach).toHaveBeenCalledTimes(1); - await provider.stopSession("mcode-eligibility-transition"); - }); - it("restarts a pooled session and attaches again once its Browser credential expires", async () => { const attach = vi.fn().mockImplementation(async () => mcodeAttachment()); const provider = makeProvider(undefined, configuredLease(), { attach }); diff --git a/packages/providers/src/__tests__/codex/codex-provider-lifecycle.test.ts b/packages/providers/src/__tests__/codex/codex-provider-lifecycle.test.ts index 66fa8ddb5..ea3e59344 100644 --- a/packages/providers/src/__tests__/codex/codex-provider-lifecycle.test.ts +++ b/packages/providers/src/__tests__/codex/codex-provider-lifecycle.test.ts @@ -77,7 +77,6 @@ const request: TurnRequest<"codex"> = { turnId: "turn-1", turnExecutionId: "execution-1", sessionId: "mcode-lifecycle", threadId: "lifecycle", workspaceId: "fixture", cwd: process.cwd(), message: "hello", model: "gpt-5.4", permissionMode: "supervised", approvalReviewMode: "manual", interactionMode: "build", providerOptions: {}, - threadControlEligible: false, }; function createProvider() { diff --git a/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts b/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts index 8be540fa3..817a217f3 100644 --- a/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts +++ b/packages/providers/src/__tests__/codex/codex-provider-test-fixture.ts @@ -10,7 +10,6 @@ import type { ProviderMcodeAttachment, ProviderMcodeAttachRequest, } from "../../host-ports.js"; -import type { TurnRequest } from "@mcode/contracts"; interface TestBrowserScope extends ProviderBrowserLeaseRequest {} @@ -215,12 +214,4 @@ export class CodexProvider extends PackageCodexProvider { super(host, codexPorts, 10 * 60 * 1_000); this.settingsService = delegatedSettingsService; } - - /** Preserve legacy fixture behavior unless a test explicitly exercises the gate. */ - override sendTurn(request: TurnRequest<"codex">): Promise { - return super.sendTurn({ - ...request, - threadControlEligible: request.threadControlEligible ?? true, - }); - } } diff --git a/packages/providers/src/host-ports.ts b/packages/providers/src/host-ports.ts index 4fc054796..c0ebe52f2 100644 --- a/packages/providers/src/host-ports.ts +++ b/packages/providers/src/host-ports.ts @@ -92,7 +92,7 @@ export interface ProviderMcodeAttachRequest { /** Must equal the Browser lease `mcodeSessionId` for this session. */ sessionId: string; threadId: string; - /** Whether the thread-control toolkit is listed; Codex passes its per-turn eligibility. */ + /** Whether the Delegation toolkit is listed; every adapter that supports it passes true. */ threadControl: boolean; } diff --git a/packages/providers/src/private/codex/codex-provider.ts b/packages/providers/src/private/codex/codex-provider.ts index ed8e3def4..f954c7005 100644 --- a/packages/providers/src/private/codex/codex-provider.ts +++ b/packages/providers/src/private/codex/codex-provider.ts @@ -320,8 +320,6 @@ interface CodexSessionState { workspaceId: string; /** Browser permission class fixed to the provider process at spawn. */ browserPermissionCapability: "observe" | "interact" | "privileged"; - /** Whether this session was started with the Mcode thread-control toolkit. */ - threadControlEligible: boolean; } /** One pending codex approval bridged into the Phase 1 permission flow. */ @@ -349,7 +347,6 @@ interface PreparedCodexTurn { browserPermissionCapability: "observe" | "interact" | "privileged"; input: CodexTurnInput; turnOptions: CodexTurnOptions; - threadControlEligible: boolean; } interface CodexMcodeSetup { @@ -366,9 +363,8 @@ interface CodexSpawnContext { resumeFrom: string | undefined; sandbox: SandboxMode; approvalPolicy: "never" | "on-request"; - pendingSpawn: { input: CodexTurnInput; turnOptions: CodexTurnOptions; turnExecutionId: string; threadControlEligible: boolean; resumeRequired: boolean } | undefined; + pendingSpawn: { input: CodexTurnInput; turnOptions: CodexTurnOptions; turnExecutionId: string; resumeRequired: boolean } | undefined; stagedExecutionId: string | undefined; - threadControlEligible: boolean; browserAccess: { stage: BrowserAutomationSessionLeaseStage; workspaceId: string; permissionCapability: "observe" | "interact" | "privileged" } | undefined; mcode: ProviderMcodeAttachment | null; mcodeSetupError: string | undefined; @@ -741,7 +737,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv turnExecutionId: string; turnId: string; deliveryAttempt: number; - threadControlEligible: boolean; resumeRequired: boolean; signal: AbortSignal; } @@ -1182,7 +1177,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv sandbox: request.permissionMode === "full" ? "danger-full-access" : "workspace-write", browserPermissionCapability: providerBrowserPermissionCapability(request.permissionMode, request.interactionMode), turnOptions: this.codexTurnOptions(request, settings.fastMode, nativePlan), - threadControlEligible: request.threadControlEligible === true, }; } @@ -1255,7 +1249,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv return undefined; } if (existing && existing.server.isAlive && existing.sandboxMode !== turn.sandbox) this.restartCodexSessionForSandbox(existing, turn); - if (this.requiresThreadControlSessionRestart(existing, turn)) return this.restartCodexSessionForThreadControl(existing, turn); return existing; } @@ -1273,18 +1266,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv }); } - private requiresThreadControlSessionRestart(existing: CodexSessionState | undefined, turn: PreparedCodexTurn): existing is CodexSessionState { - return Boolean(existing && existing.server.isAlive && existing.threadControlEligible !== turn.threadControlEligible); - } - - private async restartCodexSessionForThreadControl(existing: CodexSessionState, turn: PreparedCodexTurn): Promise { - const sessionId = turn.request.sessionId; - logger.info("Codex session restarted due to Mcode thread-control eligibility change", { sessionId, from: existing.threadControlEligible, to: turn.threadControlEligible }); - this.drainPending((entry) => entry.sessionId === sessionId); - await this.runtime.stop(sessionId); - return undefined; - } - private isReusableCodexSession(existing: CodexSessionState | undefined, sandbox: string): existing is CodexSessionState { return Boolean(existing && existing.server.isAlive && existing.sandboxMode === sandbox); } @@ -1301,7 +1282,7 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv private stageCodexTurn(turn: PreparedCodexTurn): void { const { request } = turn; const sessionId = request.sessionId; - this.pendingSpawnTurns.set(sessionId, { input: turn.input, turnOptions: turn.turnOptions, turnExecutionId: request.turnExecutionId, turnId: request.turnId, deliveryAttempt: request.deliveryAttempt ?? 1, threadControlEligible: turn.threadControlEligible, resumeRequired: request.resumeRequired === true, signal: turn.signal }); + this.pendingSpawnTurns.set(sessionId, { input: turn.input, turnOptions: turn.turnOptions, turnExecutionId: request.turnExecutionId, turnId: request.turnId, deliveryAttempt: request.deliveryAttempt ?? 1, resumeRequired: request.resumeRequired === true, signal: turn.signal }); if (!this.host.browser.isConfigured()) return; const stage = this.host.browser.stage({ providerId: this.id, providerSessionId: request.resumeFrom ?? sessionId, mcodeSessionId: sessionId, threadId: request.threadId, workspaceId: request.workspaceId, permissionCapability: turn.browserPermissionCapability }); const previous = this.pendingBrowserAccess.get(sessionId); @@ -1472,12 +1453,11 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv private async prepareCodexSpawn(args: SpawnArgs): Promise { const settings = await this.codexPorts.settings.get(); const pendingSpawn = this.pendingSpawnTurns.get(args.sessionId); - const threadControlEligible = pendingSpawn?.threadControlEligible === true; const browserAccess = this.pendingBrowserAccess.get(args.sessionId); const issuedGrant = browserAccess ? this.host.browser.issue(browserAccess.stage) : null; this.pendingBrowserAccess.delete(args.sessionId); const closeMcode = this.codexMcodeSessionCloser(args.sessionId); - const mcodeSetup = await this.attachCodexMcode(args, threadControlEligible, issuedGrant !== null, closeMcode); + const mcodeSetup = await this.attachCodexMcode(args, closeMcode); const browserGrant = this.releaseUnlistedBrowserGrant(issuedGrant, mcodeSetup.mcode); const spawnEnv = { ...args.env }; if (mcodeSetup.mcode) spawnEnv[CODEX_MCODE_TOKEN_ENV] = mcodeSetup.mcode.token; @@ -1492,7 +1472,6 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv approvalPolicy: args.permissionMode === "full" ? "never" : "on-request", pendingSpawn, stagedExecutionId: pendingSpawn?.turnExecutionId, - threadControlEligible, browserAccess, mcode: mcodeSetup.mcode, mcodeSetupError: mcodeSetup.setupError, @@ -1509,13 +1488,10 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv */ private async attachCodexMcode( args: SpawnArgs, - threadControlEligible: boolean, - hasBrowserGrant: boolean, close: () => Promise, ): Promise { - if (!threadControlEligible && !hasBrowserGrant) return { mcode: null, setupError: undefined }; try { - const mcode = await this.host.mcode.attach({ providerId: this.id, sessionId: args.sessionId, threadId: args.threadId, threadControl: threadControlEligible }); + const mcode = await this.host.mcode.attach({ providerId: this.id, sessionId: args.sessionId, threadId: args.threadId, threadControl: true }); if (!mcode) logger.warn("Codex mcode MCP server is unavailable; continuing without Mcode tools", { threadId: args.threadId }); return { mcode, setupError: undefined }; } catch (error) { @@ -1863,7 +1839,7 @@ export class CodexProvider extends NodeEvents.EventEmitter implements IAgentProv nextChildGeneration: 0, pendingChildEvents: [], deliveredChildEventKeys: new Set(), workspaceId: context.browserAccess?.workspaceId ?? "unknown-workspace", browserPermissionCapability: context.browserAccess?.permissionCapability ?? "interact", - threadControlEligible: context.threadControlEligible, childMetadataFetches: new Set(), + childMetadataFetches: new Set(), }; if (context.browserGrant) { state.browserCredential = { credentialId: context.browserGrant.credentialId, expiresAt: context.browserGrant.expiresAt };