From 373777174b1bf335225e2cf784fc582f819d00c8 Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 00:48:07 +0800 Subject: [PATCH 01/14] fix(issue-407): elicit exact owner carrier approval --- src/server.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/src/server.ts b/src/server.ts index 57f4b453c..dfe8e8a69 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import { CarrierBindingStore, type CarrierCompletionBinding } from "./carrier-binding.js"; +import { CarrierBindingStore, type CarrierCompletionBinding, type CarrierContract } from "./carrier-binding.js"; import type { ControlPlaneConsumerOptions } from "./control-plane-consumer.js"; import { ControlPlaneOwnershipError } from "./control-plane-ownership.js"; import { @@ -19,7 +19,7 @@ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js"; import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { isInitializeRequest, type ServerNotification, type ServerRequest } from "@modelcontextprotocol/sdk/types.js"; +import { ElicitResultSchema, isInitializeRequest, type ServerNotification, type ServerRequest } from "@modelcontextprotocol/sdk/types.js"; import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js"; import { @@ -4518,9 +4518,73 @@ export function createMcpServer( if (carrierBindings) { const contractSchema=z.object({repository:z.string(),goal:z.string(),role:z.enum(["controller","worker"]),scope:z.array(z.string()),baseRevision:z.string(),operations:z.array(z.enum(["dependency_sync","worktree_write"])),expiresAt:z.string()}).strict(); + const carrierIdentitySchema=z.object({sourceCommit:z.string().regex(/^[a-f0-9]{40,64}$/),buildId:z.string().min(1),capabilityManifestSha256:z.string().regex(/^[a-f0-9]{64}$/)}).strict(); + const ownerContractSchema=z.object({ + repository:z.string().min(1),goal:z.string().min(1),role:z.enum(["controller","worker"]),scope:z.array(z.string()).min(1), + baseRevision:z.string().regex(/^[a-f0-9]{40,64}$/),operations:z.array(z.enum(["dependency_sync","worktree_write","cutover_start"])).min(1), + expiresAt:z.string().datetime(), + cutover:z.object({ + stateRoot:z.string().min(1),attemptKey:z.string().min(1), + currentIdentity:carrierIdentitySchema.extend({serverInstanceId:z.string().min(1)}).strict(), + expectedIdentity:carrierIdentitySchema,expiresAt:z.string().datetime(), + restart:z.object({buildReady:z.object({verifiedBy:z.string().min(1),verifiedAt:z.string().datetime(),evidence:z.string().min(1)}).strict(),actuator:z.literal("launchd-self"),serviceLabel:z.string().min(1),launchdTarget:z.string().min(1)}).strict(), + finish:z.object({workspaceId:z.string().min(1),agentId:z.string().min(1)}).strict(), + }).strict().optional(), + maxDepth:z.number().int().nonnegative().optional(),remainingDepth:z.number().int().nonnegative().optional(), + }).strict(); const registration={annotations:{readOnlyHint:false,destructiveHint:false,idempotentHint:false,openWorldHint:false},_meta:{}}; const result=(value:unknown)=>({content:[textBlock(JSON.stringify(value))]}); - registerAppTool(server,"coordination_pair",{...registration,title:"Request a carrier pairing",description:"Request local Owner approval for new bounded authority. Save the returned pendingId; once approved locally by the Owner via CLI, resume the carrier using coordination_resume with the pendingId.",inputSchema:{}},async(_,extra)=>result(carrierBindings.requestPairing(dependencyConsumerContext(extra)))); + registerAppTool(server,"coordination_pair",{...registration,title:"Request a carrier pairing",description:"Request explicit Owner approval for new bounded authority. Save the returned pendingId; an Owner-facing client may approve the exact contract with coordination_owner_approve before coordination_resume.",inputSchema:{}},async(_,extra)=>result(carrierBindings.requestPairing(dependencyConsumerContext(extra)))); + const ownerApprovalRoots=[...config.allowedRoots,config.worktreeRoot]; + const ownerApprovalPreview=(pendingId:string,contract:CarrierContract,extra:{authInfo?:{clientId:string;scopes:string[]};sessionId?:string},sameSession=true)=>{ + const context=dependencyConsumerContext(extra); + const pending=carrierBindings.pending(pendingId); + if(pending.client_id!==context.clientId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Pairing belongs to a different authenticated client"); + if(sameSession && pending.session_id!==context.sessionId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Owner approval must originate from the session that requested the pairing"); + const canonical=carrierBindings.validateLocalScope(contract,ownerApprovalRoots); + const contractHash=createHash("sha256").update(JSON.stringify(canonical)).digest("hex"); + if(pending.binding_id) { + const approved=carrierBindings.inspectLocal(pending.binding_id); + const approvedHash=createHash("sha256").update(JSON.stringify(approved.contract)).digest("hex"); + if(approvedHash!==contractHash) return {status:"CONTRACT_MISMATCH" as const,pendingId,contractHash,approvedContractHash:approvedHash,expiresAt:new Date(pending.expires_at).toISOString()}; + return {status:"ALREADY_APPROVED_SAME_CONTRACT" as const,pendingId,contractHash,carrierId:approved.id,expiresAt:new Date(pending.expires_at).toISOString(),canonical}; + } + return {status:"PENDING_OWNER_APPROVAL" as const,pendingId,contractHash,expiresAt:new Date(pending.expires_at).toISOString(),canonical}; + }; + registerAppTool(server,"coordination_owner_approval_prepare",{...registration,annotations:{...registration.annotations,readOnlyHint:true,idempotentHint:true},title:"Prepare exact Owner carrier approval",description:"Validate and hash one exact pending carrier contract before human approval. Grants no authority and exposes no credential.",inputSchema:{pendingId:z.string(),contract:ownerContractSchema}},async({pendingId,contract},extra)=>{ + const preview=ownerApprovalPreview(pendingId,contract as CarrierContract,extra); + return result({...preview,canonical:undefined}); + }); + registerAppTool(server,"coordination_owner_approval_status",{...registration,annotations:{...registration.annotations,readOnlyHint:true,idempotentHint:true},title:"Read Owner carrier approval status",description:"Read pending or approved state for one pairing from the authenticated OAuth client. Does not bind the current session or grant authority.",inputSchema:{pendingId:z.string(),contract:ownerContractSchema}},async({pendingId,contract},extra)=>{ + const preview=ownerApprovalPreview(pendingId,contract as CarrierContract,extra,false); + return result({...preview,canonical:undefined}); + }); + registerAppTool(server,"coordination_owner_approve",{...registration,title:"Request explicit Owner carrier approval",description:"Ask the connected human Owner to approve one exact pending carrier contract through MCP elicitation. Tool arguments alone cannot approve it; decline, cancel, unsupported elicitation, drift, or expiry grants no authority.",inputSchema:{pendingId:z.string(),contract:ownerContractSchema}},async({pendingId,contract},extra)=>{ + const exact=contract as CarrierContract; + const preview=ownerApprovalPreview(pendingId,exact,extra); + if(preview.status!=="PENDING_OWNER_APPROVAL") return result({...preview,canonical:undefined}); + let elicited; + try { + elicited=await extra.sendRequest({ + method:"elicitation/create", + params:{ + message:`Approve this exact DevSpace carrier? repository=${preview.canonical.repository}; goal=${preview.canonical.goal}; role=${preview.canonical.role}; base=${preview.canonical.baseRevision}; operations=${preview.canonical.operations.join(",")}; scope=${preview.canonical.scope.join(",")}; expiresAt=${preview.canonical.expiresAt}; contractSha256=${preview.contractHash}`, + requestedSchema:{type:"object",properties:{approve:{type:"boolean",title:"Approve exact bounded carrier"}},required:["approve"]}, + }, + },ElicitResultSchema); + } catch(error) { + const message=error instanceof Error?error.message:String(error); + return {content:[textBlock(`ELICITATION_UNAVAILABLE: ${message}`)],isError:true,structuredContent:{status:"PENDING_OWNER_APPROVAL",pendingId,contractHash:preview.contractHash,code:"ELICITATION_UNAVAILABLE",message}}; + } + const content=elicited.content as {approve?:unknown}|undefined; + if(elicited.action!=="accept" || content?.approve!==true) { + return result({status:elicited.action==="decline"||elicited.action==="accept"?"REJECTED":"PENDING_OWNER_APPROVAL",pendingId,contractHash:preview.contractHash,elicitationAction:elicited.action}); + } + const rechecked=ownerApprovalPreview(pendingId,exact,extra); + if(rechecked.status!=="PENDING_OWNER_APPROVAL" || rechecked.contractHash!==preview.contractHash) throw new ControlPlaneOwnershipError("CAS_CONFLICT","Pairing or exact Owner-approved contract changed before commit"); + const carrier=carrierBindings.approveLocal(pendingId,rechecked.canonical); + return result({status:"APPROVED",pendingId,contractHash:preview.contractHash,carrier}); + }); registerAppTool(server,"coordination_recovery_request",{...registration,title:"Request existing-carrier recovery",description:"Create a pending recovery verifier for this MCP session. This does not create a carrier or grant authority. A host Owner must bind this pendingId to one exact existing carrier with `devspace carrier recover --carrier --version --validity-version --confirm ` before coordination_resume can succeed.",inputSchema:{}},async(_,extra)=>result(carrierBindings.requestPairing(dependencyConsumerContext(extra)))); registerAppTool(server,"coordination_resume",{...registration,title:"Resume a paired carrier",description:"Resume a paired or recovered carrier on the current authenticated MCP session using an Owner-approved pendingId, or verification token.",inputSchema:{pendingId:z.string().optional().describe("The pendingId returned from coordination_pair or coordination_recovery_request once approved by the Owner"),token:z.string().optional().describe("Optional pairing verification token"),credential:z.string().optional().describe("Legacy pairing verification token")}},async(args,extra)=>{ try { From e386b427dcfb835ab0119a484a2c4374d693d885 Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 00:50:43 +0800 Subject: [PATCH 02/14] test(issue-407): cover elicited owner carrier approval --- src/carrier-binding-http.test.ts | 139 +++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/carrier-binding-http.test.ts b/src/carrier-binding-http.test.ts index 9a2378aa0..b1173ae14 100644 --- a/src/carrier-binding-http.test.ts +++ b/src/carrier-binding-http.test.ts @@ -9,6 +9,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { loadConfig } from "./config.js"; import { createServer } from "./server.js"; import { SingleUserOAuthProvider } from "./oauth-provider.js"; @@ -583,3 +584,141 @@ test("approved cutover credential survives fresh MCP sessions for prepare and st rmSync(root, { recursive: true, force: true }); } }); + + +test("Owner-facing elicitation approves exactly one bounded carrier without Terminal fallback",async()=>{ + const root=realpathSync(mkdtempSync(join(tmpdir(),"carrier-owner-elicitation-"))); + const workspace=join(root,"workspace");mkdirSync(workspace); + const config=loadConfig({ + DEVSPACE_CONFIG_DIR:join(root,"config"), + DEVSPACE_STATE_DIR:join(root,"state"), + DEVSPACE_ALLOWED_ROOTS:root, + DEVSPACE_WORKTREE_ROOT:join(root,"worktrees"), + DEVSPACE_SUBAGENTS:"false", + DEVSPACE_TOOL_MODE:"full", + DEVSPACE_PUBLIC_BASE_URL:"http://127.0.0.1:1", + DEVSPACE_OAUTH_OWNER_TOKEN:"test-owner-token-that-is-long-enough", + PORT:"1", + }); + const provider=new SingleUserOAuthProvider(config.oauth,new URL("/mcp",config.publicBaseUrl),config.stateDir); + const oauthClient=await provider.clientsStore.registerClient!({ + redirect_uris:["http://localhost/callback"], + client_name:"owner elicitation fixture", + token_endpoint_auth_method:"none", + }); + let redirect=""; + await provider.authorize( + oauthClient, + {redirectUri:"http://localhost/callback",codeChallenge:"fixture",scopes:config.oauth.scopes,resource:new URL("/mcp",config.publicBaseUrl)}, + {req:{method:"POST",body:{owner_token:config.oauth.ownerToken}},redirect:(_status:number,url:string)=>{redirect=url;}} as never, + ); + const tokens=await provider.exchangeAuthorizationCode(oauthClient,new URL(redirect).searchParams.get("code")!); + const running=createServer(config); + const listener=running.app.listen(0,"127.0.0.1"); + await new Promise((resolve,reject)=>{listener.once("listening",resolve);listener.once("error",reject);}); + const url=new URL(`http://127.0.0.1:${(listener.address() as {port:number}).port}/mcp`); + const clients:Client[]=[]; + let approval:"accept"|"decline"|"cancel"="accept"; + const connect=async(name:string,withElicitation=true)=>{ + const client=new Client( + {name,version:"1"}, + withElicitation?{capabilities:{elicitation:{}}}:undefined, + ); + clients.push(client); + if(withElicitation) { + client.setRequestHandler(ElicitRequestSchema,async(request)=>{ + assert.match(request.params.message,/James3014\/devspace/); + assert.match(request.params.message,/contractSha256=[0-9a-f]{64}/); + if(approval==="cancel") return {action:"cancel" as const}; + if(approval==="decline") return {action:"decline" as const}; + return {action:"accept" as const,content:{approve:true}}; + }); + } + await client.connect(new StreamableHTTPClientTransport(url,{requestInit:{headers:{Authorization:`Bearer ${tokens.access_token}`}}})); + return client; + }; + try { + const owner=await connect("owner-elicitation"); + const pending=data(await owner.callTool({name:"coordination_pair",arguments:{}})); + const contract:CarrierContract={ + repository:"James3014/devspace", + goal:"issue407", + role:"controller", + scope:[workspace], + baseRevision:"a".repeat(40), + operations:["worktree_write"], + expiresAt:new Date(Date.now()+120000).toISOString(), + }; + const prepared=data(await owner.callTool({ + name:"coordination_owner_approval_prepare", + arguments:{pendingId:pending.pendingId,contract}, + })); + assert.equal(prepared.status,"PENDING_OWNER_APPROVAL"); + assert.match(prepared.contractHash,/^[0-9a-f]{64}$/); + + const before=await owner.callTool({name:"coordination_resume",arguments:{pendingId:pending.pendingId}}); + assert.equal(before.isError,true); + + const approved=data(await owner.callTool({ + name:"coordination_owner_approve", + arguments:{pendingId:pending.pendingId,contract}, + })); + assert.equal(approved.status,"APPROVED"); + assert.equal(approved.contractHash,prepared.contractHash); + const carrierId=(approved.carrier as {id:string}).id; + assert.match(carrierId,/^carrier_/); + + const resumed=data(await owner.callTool({name:"coordination_resume",arguments:{pendingId:pending.pendingId}})); + assert.equal(resumed.id,carrierId); + + const replay=data(await owner.callTool({ + name:"coordination_owner_approve", + arguments:{pendingId:pending.pendingId,contract}, + })); + assert.equal(replay.status,"ALREADY_APPROVED_SAME_CONTRACT"); + assert.equal(replay.carrierId,carrierId); + + const mismatch=data(await owner.callTool({ + name:"coordination_owner_approval_status", + arguments:{pendingId:pending.pendingId,contract:{...contract,goal:"issue407-changed"}}, + })); + assert.equal(mismatch.status,"CONTRACT_MISMATCH"); + + const fresh=await connect("fresh-status",false); + const durable=data(await fresh.callTool({ + name:"coordination_owner_approval_status", + arguments:{pendingId:pending.pendingId,contract}, + })); + assert.equal(durable.status,"ALREADY_APPROVED_SAME_CONTRACT"); + assert.equal(durable.carrierId,carrierId); + const hijack=await fresh.callTool({ + name:"coordination_owner_approve", + arguments:{pendingId:pending.pendingId,contract}, + }); + assert.equal(hijack.isError,true); + + approval="decline"; + const declinedPending=data(await owner.callTool({name:"coordination_pair",arguments:{}})); + const declined=data(await owner.callTool({ + name:"coordination_owner_approve", + arguments:{pendingId:declinedPending.pendingId,contract}, + })); + assert.equal(declined.status,"REJECTED"); + assert.equal((await owner.callTool({name:"coordination_resume",arguments:{pendingId:declinedPending.pendingId}})).isError,true); + + approval="cancel"; + const cancelledPending=data(await owner.callTool({name:"coordination_pair",arguments:{}})); + const cancelled=data(await owner.callTool({ + name:"coordination_owner_approve", + arguments:{pendingId:cancelledPending.pendingId,contract}, + })); + assert.equal(cancelled.status,"PENDING_OWNER_APPROVAL"); + assert.equal(cancelled.elicitationAction,"cancel"); + assert.equal((await owner.callTool({name:"coordination_resume",arguments:{pendingId:cancelledPending.pendingId}})).isError,true); + } finally { + for(const client of clients.reverse()) await client.close().catch(()=>{}); + await new Promise(resolve=>listener.close(()=>resolve())); + await running.close(); + rmSync(root,{recursive:true,force:true}); + } +}); From 830d8c748b6976f2d9d06de83785bbd90a9b9d7d Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 00:57:45 +0800 Subject: [PATCH 03/14] test(issue-407): expect canonical repository key --- src/carrier-binding-http.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/carrier-binding-http.test.ts b/src/carrier-binding-http.test.ts index b1173ae14..969a28f51 100644 --- a/src/carrier-binding-http.test.ts +++ b/src/carrier-binding-http.test.ts @@ -627,7 +627,7 @@ test("Owner-facing elicitation approves exactly one bounded carrier without Term clients.push(client); if(withElicitation) { client.setRequestHandler(ElicitRequestSchema,async(request)=>{ - assert.match(request.params.message,/James3014\/devspace/); + assert.match(request.params.message,/james3014\/devspace/); assert.match(request.params.message,/contractSha256=[0-9a-f]{64}/); if(approval==="cancel") return {action:"cancel" as const}; if(approval==="decline") return {action:"decline" as const}; From 17234e4a31912be5caa5f47a9e6ea86c8662fb24 Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:42:36 +0800 Subject: [PATCH 04/14] feat(issue-407): persist owner approval receipts --- src/carrier-owner-approval-receipts.ts | 247 +++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 src/carrier-owner-approval-receipts.ts diff --git a/src/carrier-owner-approval-receipts.ts b/src/carrier-owner-approval-receipts.ts new file mode 100644 index 000000000..bad201f54 --- /dev/null +++ b/src/carrier-owner-approval-receipts.ts @@ -0,0 +1,247 @@ +import type Database from "better-sqlite3"; + +export type OwnerApprovalReceiptStatus = + | "PENDING_OWNER_APPROVAL" + | "APPROVED" + | "REJECTED" + | "EXPIRED" + | "CONTRACT_MISMATCH"; + +export interface OwnerApprovalPairingProjection { + pendingId: string; + clientId: string; + sessionId: string; + expiresAtMs: number; + carrierId?: string; +} + +export interface OwnerApprovalReceiptProjection { + pendingId: string; + clientId: string; + sessionId: string; + contractHash: string; + contractJson: string; + expiresAtMs: number; + status: OwnerApprovalReceiptStatus; + carrierId?: string; + createdAt: string; + updatedAt: string; + decidedAt?: string; +} + +interface ReceiptRow { + pending_id: string; + client_id: string; + session_id: string; + contract_hash: string; + contract_json: string; + expires_at: number; + status: "PENDING" | "APPROVED" | "REJECTED"; + carrier_id: string | null; + created_at: string; + updated_at: string; + decided_at: string | null; +} + +interface PairingRow { + id: string; + client_id: string; + session_id: string; + expires_at: number; + binding_id: string | null; +} + +export class OwnerCarrierApprovalReceiptStore { + constructor( + private readonly sqlite: Database.Database, + private readonly now: () => number = Date.now, + ) {} + + pairing(pendingId: string): OwnerApprovalPairingProjection | undefined { + const row = this.sqlite.prepare( + "select id,client_id,session_id,expires_at,binding_id from carrier_pairings where id=?", + ).get(pendingId) as PairingRow | undefined; + if (!row) return undefined; + return { + pendingId: row.id, + clientId: row.client_id, + sessionId: row.session_id, + expiresAtMs: row.expires_at, + ...(row.binding_id ? { carrierId: row.binding_id } : {}), + }; + } + + read(pendingId: string): OwnerApprovalReceiptProjection | undefined { + const row = this.sqlite.prepare( + "select pending_id,client_id,session_id,contract_hash,contract_json,expires_at,status,carrier_id,created_at,updated_at,decided_at from carrier_owner_approval_receipts where pending_id=?", + ).get(pendingId) as ReceiptRow | undefined; + return row ? this.project(row) : undefined; + } + + ensurePending(input: { + pendingId: string; + clientId: string; + sessionId: string; + contractHash: string; + contractJson: string; + expiresAtMs: number; + }): OwnerApprovalReceiptProjection { + return this.sqlite.transaction(() => { + const existing = this.read(input.pendingId); + if (existing) { + if ( + existing.clientId !== input.clientId || + existing.sessionId !== input.sessionId || + existing.contractHash !== input.contractHash || + existing.contractJson !== input.contractJson + ) { + return { ...existing, status: "CONTRACT_MISMATCH" as const }; + } + return existing; + } + const now = new Date(this.now()).toISOString(); + this.sqlite.prepare( + "insert into carrier_owner_approval_receipts(pending_id,client_id,session_id,contract_hash,contract_json,expires_at,status,carrier_id,created_at,updated_at,decided_at) values(?,?,?,?,?,?,'PENDING',null,?,?,null)", + ).run( + input.pendingId, + input.clientId, + input.sessionId, + input.contractHash, + input.contractJson, + input.expiresAtMs, + now, + now, + ); + return this.read(input.pendingId)!; + }).immediate(); + } + + markRejected(input: { + pendingId: string; + clientId: string; + sessionId: string; + contractHash: string; + }): OwnerApprovalReceiptProjection { + return this.sqlite.transaction(() => { + const current = this.requireExact(input); + if (current.status === "APPROVED" || current.status === "REJECTED" || current.status === "EXPIRED") return current; + const decidedAt = new Date(this.now()).toISOString(); + const changed = this.sqlite.prepare( + "update carrier_owner_approval_receipts set status='REJECTED',updated_at=?,decided_at=? where pending_id=? and status='PENDING' and client_id=? and session_id=? and contract_hash=?", + ).run(decidedAt, decidedAt, input.pendingId, input.clientId, input.sessionId, input.contractHash); + if (changed.changes !== 1) return this.requireExact(input); + return this.read(input.pendingId)!; + }).immediate(); + } + + markApproved(input: { + pendingId: string; + clientId: string; + sessionId: string; + contractHash: string; + carrierId: string; + }): OwnerApprovalReceiptProjection { + return this.sqlite.transaction(() => { + const current = this.requireExact(input); + if (current.status === "APPROVED") { + if (current.carrierId !== input.carrierId) { + return { ...current, status: "CONTRACT_MISMATCH" as const }; + } + return current; + } + if (current.status === "REJECTED" || current.status === "EXPIRED") return current; + const decidedAt = new Date(this.now()).toISOString(); + const changed = this.sqlite.prepare( + "update carrier_owner_approval_receipts set status='APPROVED',carrier_id=?,updated_at=?,decided_at=? where pending_id=? and status='PENDING' and client_id=? and session_id=? and contract_hash=?", + ).run(input.carrierId, decidedAt, decidedAt, input.pendingId, input.clientId, input.sessionId, input.contractHash); + if (changed.changes !== 1) return this.requireExact(input); + return this.read(input.pendingId)!; + }).immediate(); + } + + reconcileApproved(input: { + pendingId: string; + clientId: string; + sessionId: string; + contractHash: string; + contractJson: string; + expiresAtMs: number; + carrierId: string; + }): OwnerApprovalReceiptProjection { + return this.sqlite.transaction(() => { + const existing = this.read(input.pendingId); + if (!existing) { + const now = new Date(this.now()).toISOString(); + this.sqlite.prepare( + "insert into carrier_owner_approval_receipts(pending_id,client_id,session_id,contract_hash,contract_json,expires_at,status,carrier_id,created_at,updated_at,decided_at) values(?,?,?,?,?,?,'APPROVED',?,?,?,?)", + ).run( + input.pendingId, + input.clientId, + input.sessionId, + input.contractHash, + input.contractJson, + input.expiresAtMs, + input.carrierId, + now, + now, + now, + ); + return this.read(input.pendingId)!; + } + if ( + existing.clientId !== input.clientId || + existing.sessionId !== input.sessionId || + existing.contractHash !== input.contractHash || + existing.contractJson !== input.contractJson + ) { + return { ...existing, status: "CONTRACT_MISMATCH" as const }; + } + if (existing.status === "APPROVED" && existing.carrierId === input.carrierId) return existing; + const decidedAt = new Date(this.now()).toISOString(); + this.sqlite.prepare( + "update carrier_owner_approval_receipts set status='APPROVED',carrier_id=?,updated_at=?,decided_at=? where pending_id=?", + ).run(input.carrierId, decidedAt, decidedAt, input.pendingId); + return this.read(input.pendingId)!; + }).immediate(); + } + + private requireExact(input: { + pendingId: string; + clientId: string; + sessionId: string; + contractHash: string; + }): OwnerApprovalReceiptProjection { + const current = this.read(input.pendingId); + if (!current) throw new Error("OWNER_APPROVAL_RECEIPT_MISSING"); + if ( + current.clientId !== input.clientId || + current.sessionId !== input.sessionId || + current.contractHash !== input.contractHash + ) { + return { ...current, status: "CONTRACT_MISMATCH" as const }; + } + return current; + } + + private project(row: ReceiptRow): OwnerApprovalReceiptProjection { + const status: OwnerApprovalReceiptStatus = + row.status === "PENDING" && row.expires_at <= this.now() + ? "EXPIRED" + : row.status === "PENDING" + ? "PENDING_OWNER_APPROVAL" + : row.status; + return { + pendingId: row.pending_id, + clientId: row.client_id, + sessionId: row.session_id, + contractHash: row.contract_hash, + contractJson: row.contract_json, + expiresAtMs: row.expires_at, + status, + ...(row.carrier_id ? { carrierId: row.carrier_id } : {}), + createdAt: row.created_at, + updatedAt: row.updated_at, + ...(row.decided_at ? { decidedAt: row.decided_at } : {}), + }; + } +} From 31ff83c251cbe6b71466ed233fe68cadab05cf50 Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:42:51 +0800 Subject: [PATCH 05/14] feat(issue-407): add approval receipt migration --- src/db/migrations.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/db/migrations.ts b/src/db/migrations.ts index 2eaabe08b..52cd787a5 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -88,6 +88,7 @@ const migrations: Migration[] = [ { version: 23, name: "core-candidate-acquisition-observations", up: migrateCoreCandidateAcquisitionObservations }, { version: 24, name: "core-candidate-acquisition-receipt-path", up: migrateCoreCandidateAcquisitionReceiptPath }, { version: 25, name: "work-resume-registry", up: migrateWorkResumeRegistry }, + { version: 26, name: "carrier-owner-approval-receipts", up: migrateCarrierOwnerApprovalReceipts }, ]; export function migrateDatabase(sqlite: Database.Database): void { @@ -756,3 +757,24 @@ function migrateWorkResumeRegistry(sqlite: Database.Database): void { addColumnIfMissing(sqlite, "work_resume_registry", "effect_key", "text"); addColumnIfMissing(sqlite, "work_resume_registry", "effect_handle", "text"); } + + +function migrateCarrierOwnerApprovalReceipts(sqlite: Database.Database): void { + sqlite.exec(` + create table if not exists carrier_owner_approval_receipts ( + pending_id text primary key, + client_id text not null, + session_id text not null, + contract_hash text not null check(length(contract_hash) = 64), + contract_json text not null, + expires_at integer not null, + status text not null check(status in ('PENDING', 'APPROVED', 'REJECTED')), + carrier_id text, + created_at text not null, + updated_at text not null, + decided_at text + ); + create index if not exists carrier_owner_approval_receipts_client_idx + on carrier_owner_approval_receipts(client_id, updated_at desc); + `); +} From 086e1dee6cf88d8b0577d0fe1084bc0f9dd2c0c5 Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:42:56 +0800 Subject: [PATCH 06/14] test(issue-407): verify approval receipt migration --- src/db/migrations.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/db/migrations.test.ts b/src/db/migrations.test.ts index 26b7c3da8..49dba0234 100644 --- a/src/db/migrations.test.ts +++ b/src/db/migrations.test.ts @@ -120,3 +120,36 @@ test("v22 repairs a database that already recorded the legacy v21 migration", () sqlite.close(); } }); + + +test("v26 adds durable Owner carrier approval receipts without replacing carrier authority", () => { + const sqlite = new Database(":memory:"); + try { + migrateDatabase(sqlite); + const migration = sqlite.prepare( + "select name from devspace_schema_migrations where version = 26", + ).get() as { name: string }; + assert.equal(migration.name, "carrier-owner-approval-receipts"); + const columns = sqlite.prepare( + "pragma table_info(carrier_owner_approval_receipts)", + ).all() as Array<{ name: string }>; + assert.deepEqual( + columns.map((column) => column.name), + [ + "pending_id", + "client_id", + "session_id", + "contract_hash", + "contract_json", + "expires_at", + "status", + "carrier_id", + "created_at", + "updated_at", + "decided_at", + ], + ); + } finally { + sqlite.close(); + } +}); From a850660026c4d08568268c0bc33fec1d02aa5c6e Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:43:46 +0800 Subject: [PATCH 07/14] fix(issue-407): project pending approval expiry --- src/carrier-owner-approval-receipts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/carrier-owner-approval-receipts.ts b/src/carrier-owner-approval-receipts.ts index bad201f54..32575cb2f 100644 --- a/src/carrier-owner-approval-receipts.ts +++ b/src/carrier-owner-approval-receipts.ts @@ -12,6 +12,7 @@ export interface OwnerApprovalPairingProjection { clientId: string; sessionId: string; expiresAtMs: number; + expired: boolean; carrierId?: string; } @@ -67,6 +68,7 @@ export class OwnerCarrierApprovalReceiptStore { clientId: row.client_id, sessionId: row.session_id, expiresAtMs: row.expires_at, + expired: row.expires_at <= this.now(), ...(row.binding_id ? { carrierId: row.binding_id } : {}), }; } From bb6cf1b2184467dea95b6d05bd5faa4ca8f8648c Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:44:23 +0800 Subject: [PATCH 08/14] fix(issue-407): persist elicited approval state --- src/server.ts | 182 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 132 insertions(+), 50 deletions(-) diff --git a/src/server.ts b/src/server.ts index dfe8e8a69..d322040df 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,5 @@ import { CarrierBindingStore, type CarrierCompletionBinding, type CarrierContract } from "./carrier-binding.js"; +import { OwnerCarrierApprovalReceiptStore } from "./carrier-owner-approval-receipts.js"; import type { ControlPlaneConsumerOptions } from "./control-plane-consumer.js"; import { ControlPlaneOwnershipError } from "./control-plane-ownership.js"; import { @@ -3505,6 +3506,7 @@ export function createMcpServer( * Legacy callers without a resumableWork pointer are unaffected. */ workResumeStore?: WorkResumeStore, + ownerApprovalReceipts?: OwnerCarrierApprovalReceiptStore, ): McpServer { const runtimeBuildIdentity = runtimeBuildIdentityContext?.identity ?? describeRuntimeBuildIdentity({ @@ -4535,56 +4537,132 @@ export function createMcpServer( const registration={annotations:{readOnlyHint:false,destructiveHint:false,idempotentHint:false,openWorldHint:false},_meta:{}}; const result=(value:unknown)=>({content:[textBlock(JSON.stringify(value))]}); registerAppTool(server,"coordination_pair",{...registration,title:"Request a carrier pairing",description:"Request explicit Owner approval for new bounded authority. Save the returned pendingId; an Owner-facing client may approve the exact contract with coordination_owner_approve before coordination_resume.",inputSchema:{}},async(_,extra)=>result(carrierBindings.requestPairing(dependencyConsumerContext(extra)))); - const ownerApprovalRoots=[...config.allowedRoots,config.worktreeRoot]; - const ownerApprovalPreview=(pendingId:string,contract:CarrierContract,extra:{authInfo?:{clientId:string;scopes:string[]};sessionId?:string},sameSession=true)=>{ - const context=dependencyConsumerContext(extra); - const pending=carrierBindings.pending(pendingId); - if(pending.client_id!==context.clientId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Pairing belongs to a different authenticated client"); - if(sameSession && pending.session_id!==context.sessionId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Owner approval must originate from the session that requested the pairing"); - const canonical=carrierBindings.validateLocalScope(contract,ownerApprovalRoots); - const contractHash=createHash("sha256").update(JSON.stringify(canonical)).digest("hex"); - if(pending.binding_id) { - const approved=carrierBindings.inspectLocal(pending.binding_id); - const approvedHash=createHash("sha256").update(JSON.stringify(approved.contract)).digest("hex"); - if(approvedHash!==contractHash) return {status:"CONTRACT_MISMATCH" as const,pendingId,contractHash,approvedContractHash:approvedHash,expiresAt:new Date(pending.expires_at).toISOString()}; - return {status:"ALREADY_APPROVED_SAME_CONTRACT" as const,pendingId,contractHash,carrierId:approved.id,expiresAt:new Date(pending.expires_at).toISOString(),canonical}; - } - return {status:"PENDING_OWNER_APPROVAL" as const,pendingId,contractHash,expiresAt:new Date(pending.expires_at).toISOString(),canonical}; - }; - registerAppTool(server,"coordination_owner_approval_prepare",{...registration,annotations:{...registration.annotations,readOnlyHint:true,idempotentHint:true},title:"Prepare exact Owner carrier approval",description:"Validate and hash one exact pending carrier contract before human approval. Grants no authority and exposes no credential.",inputSchema:{pendingId:z.string(),contract:ownerContractSchema}},async({pendingId,contract},extra)=>{ - const preview=ownerApprovalPreview(pendingId,contract as CarrierContract,extra); - return result({...preview,canonical:undefined}); - }); - registerAppTool(server,"coordination_owner_approval_status",{...registration,annotations:{...registration.annotations,readOnlyHint:true,idempotentHint:true},title:"Read Owner carrier approval status",description:"Read pending or approved state for one pairing from the authenticated OAuth client. Does not bind the current session or grant authority.",inputSchema:{pendingId:z.string(),contract:ownerContractSchema}},async({pendingId,contract},extra)=>{ - const preview=ownerApprovalPreview(pendingId,contract as CarrierContract,extra,false); - return result({...preview,canonical:undefined}); - }); - registerAppTool(server,"coordination_owner_approve",{...registration,title:"Request explicit Owner carrier approval",description:"Ask the connected human Owner to approve one exact pending carrier contract through MCP elicitation. Tool arguments alone cannot approve it; decline, cancel, unsupported elicitation, drift, or expiry grants no authority.",inputSchema:{pendingId:z.string(),contract:ownerContractSchema}},async({pendingId,contract},extra)=>{ - const exact=contract as CarrierContract; - const preview=ownerApprovalPreview(pendingId,exact,extra); - if(preview.status!=="PENDING_OWNER_APPROVAL") return result({...preview,canonical:undefined}); - let elicited; - try { - elicited=await extra.sendRequest({ - method:"elicitation/create", - params:{ - message:`Approve this exact DevSpace carrier? repository=${preview.canonical.repository}; goal=${preview.canonical.goal}; role=${preview.canonical.role}; base=${preview.canonical.baseRevision}; operations=${preview.canonical.operations.join(",")}; scope=${preview.canonical.scope.join(",")}; expiresAt=${preview.canonical.expiresAt}; contractSha256=${preview.contractHash}`, - requestedSchema:{type:"object",properties:{approve:{type:"boolean",title:"Approve exact bounded carrier"}},required:["approve"]}, - }, - },ElicitResultSchema); - } catch(error) { - const message=error instanceof Error?error.message:String(error); - return {content:[textBlock(`ELICITATION_UNAVAILABLE: ${message}`)],isError:true,structuredContent:{status:"PENDING_OWNER_APPROVAL",pendingId,contractHash:preview.contractHash,code:"ELICITATION_UNAVAILABLE",message}}; - } - const content=elicited.content as {approve?:unknown}|undefined; - if(elicited.action!=="accept" || content?.approve!==true) { - return result({status:elicited.action==="decline"||elicited.action==="accept"?"REJECTED":"PENDING_OWNER_APPROVAL",pendingId,contractHash:preview.contractHash,elicitationAction:elicited.action}); - } - const rechecked=ownerApprovalPreview(pendingId,exact,extra); - if(rechecked.status!=="PENDING_OWNER_APPROVAL" || rechecked.contractHash!==preview.contractHash) throw new ControlPlaneOwnershipError("CAS_CONFLICT","Pairing or exact Owner-approved contract changed before commit"); - const carrier=carrierBindings.approveLocal(pendingId,rechecked.canonical); - return result({status:"APPROVED",pendingId,contractHash:preview.contractHash,carrier}); - }); + if (ownerApprovalReceipts) { + const ownerApprovalRoots=[...config.allowedRoots,config.worktreeRoot]; + const ownerApprovalPreview=(pendingId:string,contract:CarrierContract,extra:{authInfo?:{clientId:string;scopes:string[]};sessionId?:string},sameSession=true)=>{ + const context=dependencyConsumerContext(extra); + const pending=ownerApprovalReceipts.pairing(pendingId); + if(!pending) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Pairing request not found"); + if(pending.clientId!==context.clientId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Pairing belongs to a different authenticated client"); + if(sameSession && pending.sessionId!==context.sessionId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Owner approval must originate from the session that requested the pairing"); + const canonical=carrierBindings.validateLocalScope(contract,ownerApprovalRoots); + const contractJson=JSON.stringify(canonical); + const contractHash=createHash("sha256").update(contractJson).digest("hex"); + const existingReceipt=ownerApprovalReceipts.read(pendingId); + if(existingReceipt) { + if(existingReceipt.clientId!==context.clientId || existingReceipt.contractHash!==contractHash || existingReceipt.contractJson!==contractJson) { + return {status:"CONTRACT_MISMATCH" as const,pendingId,contractHash,expectedContractHash:existingReceipt.contractHash,expiresAt:new Date(existingReceipt.expiresAtMs).toISOString(),canonical}; + } + if(sameSession && existingReceipt.sessionId!==context.sessionId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Owner approval receipt belongs to a different MCP session"); + } + if(pending.carrierId) { + const approved=carrierBindings.inspectLocal(pending.carrierId); + const approvedJson=JSON.stringify(approved.contract); + const approvedHash=createHash("sha256").update(approvedJson).digest("hex"); + if(approvedHash!==contractHash) return {status:"CONTRACT_MISMATCH" as const,pendingId,contractHash,approvedContractHash:approvedHash,expiresAt:new Date(pending.expiresAtMs).toISOString(),canonical}; + const receipt=ownerApprovalReceipts.reconcileApproved({ + pendingId, + clientId:pending.clientId, + sessionId:pending.sessionId, + contractHash, + contractJson, + expiresAtMs:Math.min(pending.expiresAtMs,Date.parse(canonical.expiresAt)), + carrierId:approved.id, + }); + if(receipt.status==="CONTRACT_MISMATCH") return {status:"CONTRACT_MISMATCH" as const,pendingId,contractHash,expectedContractHash:receipt.contractHash,expiresAt:new Date(receipt.expiresAtMs).toISOString(),canonical}; + return {status:"ALREADY_APPROVED_SAME_CONTRACT" as const,pendingId,contractHash,carrierId:approved.id,expiresAt:new Date(receipt.expiresAtMs).toISOString(),canonical}; + } + if(existingReceipt?.status==="REJECTED") return {status:"REJECTED" as const,pendingId,contractHash,expiresAt:new Date(existingReceipt.expiresAtMs).toISOString(),canonical}; + if(existingReceipt?.status==="EXPIRED" || pending.expired) return {status:"EXPIRED" as const,pendingId,contractHash,expiresAt:new Date(existingReceipt?.expiresAtMs??pending.expiresAtMs).toISOString(),canonical}; + return {status:"PENDING_OWNER_APPROVAL" as const,pendingId,contractHash,contractJson,expiresAt:new Date(Math.min(pending.expiresAtMs,Date.parse(canonical.expiresAt))).toISOString(),expiresAtMs:Math.min(pending.expiresAtMs,Date.parse(canonical.expiresAt)),canonical}; + }; + registerAppTool(server,"coordination_owner_approval_prepare",{...registration,annotations:{...registration.annotations,readOnlyHint:true,idempotentHint:true},title:"Prepare exact Owner carrier approval",description:"Validate and hash one exact pending carrier contract before human approval. Grants no authority, writes no approval receipt, and exposes no credential.",inputSchema:{pendingId:z.string(),contract:ownerContractSchema}},async({pendingId,contract},extra)=>{ + const preview=ownerApprovalPreview(pendingId,contract as CarrierContract,extra); + return result({...preview,canonical:undefined,contractJson:undefined,expiresAtMs:undefined}); + }); + registerAppTool(server,"coordination_owner_approval_status",{...registration,annotations:{...registration.annotations,readOnlyHint:true,idempotentHint:true},title:"Read Owner carrier approval status",description:"Read durable pending/approved/rejected/expired approval state for one pairing from the authenticated OAuth client. Does not bind the current session or grant authority.",inputSchema:{pendingId:z.string()}},async({pendingId},extra)=>{ + const context=dependencyConsumerContext(extra); + const receipt=ownerApprovalReceipts.read(pendingId); + if(receipt) { + if(receipt.clientId!==context.clientId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Approval receipt belongs to a different authenticated client"); + return result({ + status:receipt.status, + pendingId, + contractHash:receipt.contractHash, + carrierId:receipt.carrierId, + expiresAt:new Date(receipt.expiresAtMs).toISOString(), + decidedAt:receipt.decidedAt, + }); + } + const pending=ownerApprovalReceipts.pairing(pendingId); + if(!pending || pending.clientId!==context.clientId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Pairing request not found"); + if(pending.carrierId) { + const approved=carrierBindings.inspectLocal(pending.carrierId); + const contractJson=JSON.stringify(approved.contract); + const contractHash=createHash("sha256").update(contractJson).digest("hex"); + const reconciled=ownerApprovalReceipts.reconcileApproved({ + pendingId, + clientId:pending.clientId, + sessionId:pending.sessionId, + contractHash, + contractJson, + expiresAtMs:Math.min(pending.expiresAtMs,Date.parse(approved.contract.expiresAt)), + carrierId:approved.id, + }); + return result({status:"APPROVED",pendingId,contractHash:reconciled.contractHash,carrierId:approved.id,expiresAt:new Date(reconciled.expiresAtMs).toISOString(),decidedAt:reconciled.decidedAt}); + } + return result({status:pending.expired?"EXPIRED":"PENDING_OWNER_APPROVAL",pendingId,expiresAt:new Date(pending.expiresAtMs).toISOString()}); + }); + registerAppTool(server,"coordination_owner_approve",{...registration,title:"Request explicit Owner carrier approval",description:"Ask the connected human Owner to approve one exact pending carrier contract through MCP elicitation. Tool arguments alone cannot approve it; decline, cancel, unsupported elicitation, drift, or expiry grants no authority.",inputSchema:{pendingId:z.string(),contract:ownerContractSchema}},async({pendingId,contract},extra)=>{ + const exact=contract as CarrierContract; + const preview=ownerApprovalPreview(pendingId,exact,extra); + if(preview.status!=="PENDING_OWNER_APPROVAL") return result({...preview,canonical:undefined,contractJson:undefined,expiresAtMs:undefined}); + const context=dependencyConsumerContext(extra); + const receipt=ownerApprovalReceipts.ensurePending({ + pendingId, + clientId:context.clientId, + sessionId:context.sessionId??"", + contractHash:preview.contractHash, + contractJson:preview.contractJson, + expiresAtMs:preview.expiresAtMs, + }); + if(receipt.status==="CONTRACT_MISMATCH") return result({status:"CONTRACT_MISMATCH",pendingId,contractHash:preview.contractHash,expectedContractHash:receipt.contractHash,expiresAt:new Date(receipt.expiresAtMs).toISOString()}); + if(receipt.status==="REJECTED" || receipt.status==="EXPIRED") return result({status:receipt.status,pendingId,contractHash:receipt.contractHash,expiresAt:new Date(receipt.expiresAtMs).toISOString(),decidedAt:receipt.decidedAt}); + let elicited; + try { + elicited=await extra.sendRequest({ + method:"elicitation/create", + params:{ + message:`Approve this exact DevSpace carrier? repository=${preview.canonical.repository}; goal=${preview.canonical.goal}; role=${preview.canonical.role}; base=${preview.canonical.baseRevision}; operations=${preview.canonical.operations.join(",")}; scope=${preview.canonical.scope.join(",")}; expiresAt=${preview.canonical.expiresAt}; contractSha256=${preview.contractHash}`, + requestedSchema:{type:"object",properties:{approve:{type:"boolean",title:"Approve exact bounded carrier"}},required:["approve"]}, + }, + },ElicitResultSchema); + } catch(error) { + const message=error instanceof Error?error.message:String(error); + return {content:[textBlock(`ELICITATION_UNAVAILABLE: ${message}`)],isError:true,structuredContent:{status:"PENDING_OWNER_APPROVAL",pendingId,contractHash:preview.contractHash,code:"ELICITATION_UNAVAILABLE",message}}; + } + const content=elicited.content as {approve?:unknown}|undefined; + if(elicited.action!=="accept" || content?.approve!==true) { + if(elicited.action==="decline" || elicited.action==="accept") { + const rejected=ownerApprovalReceipts.markRejected({pendingId,clientId:context.clientId,sessionId:context.sessionId??"",contractHash:preview.contractHash}); + return result({status:rejected.status,pendingId,contractHash:rejected.contractHash,expiresAt:new Date(rejected.expiresAtMs).toISOString(),decidedAt:rejected.decidedAt,elicitationAction:elicited.action}); + } + return result({status:"PENDING_OWNER_APPROVAL",pendingId,contractHash:preview.contractHash,elicitationAction:elicited.action}); + } + const rechecked=ownerApprovalPreview(pendingId,exact,extra); + if(rechecked.status!=="PENDING_OWNER_APPROVAL" || rechecked.contractHash!==preview.contractHash) throw new ControlPlaneOwnershipError("CAS_CONFLICT","Pairing or exact Owner-approved contract changed before commit"); + const carrier=carrierBindings.approveLocal(pendingId,rechecked.canonical); + const approvedReceipt=ownerApprovalReceipts.markApproved({ + pendingId, + clientId:context.clientId, + sessionId:context.sessionId??"", + contractHash:preview.contractHash, + carrierId:carrier.id, + }); + if(approvedReceipt.status!=="APPROVED") throw new ControlPlaneOwnershipError("CAS_CONFLICT","Owner approval receipt did not converge after carrier approval"); + return result({status:"APPROVED",pendingId,contractHash:preview.contractHash,carrier}); + }); + } registerAppTool(server,"coordination_recovery_request",{...registration,title:"Request existing-carrier recovery",description:"Create a pending recovery verifier for this MCP session. This does not create a carrier or grant authority. A host Owner must bind this pendingId to one exact existing carrier with `devspace carrier recover --carrier --version --validity-version --confirm ` before coordination_resume can succeed.",inputSchema:{}},async(_,extra)=>result(carrierBindings.requestPairing(dependencyConsumerContext(extra)))); registerAppTool(server,"coordination_resume",{...registration,title:"Resume a paired carrier",description:"Resume a paired or recovered carrier on the current authenticated MCP session using an Owner-approved pendingId, or verification token.",inputSchema:{pendingId:z.string().optional().describe("The pendingId returned from coordination_pair or coordination_recovery_request once approved by the Owner"),token:z.string().optional().describe("Optional pairing verification token"),credential:z.string().optional().describe("Legacy pairing verification token")}},async(args,extra)=>{ try { @@ -8642,6 +8720,9 @@ export function createServer( : undefined, carrierBindings?.ownership, ); + const ownerApprovalReceipts = carrierBindings + ? new OwnerCarrierApprovalReceiptStore(workResumeDb.sqlite, options.carrierClock) + : undefined; const hostOperations = config.hostOperationsEnabled && config.hostOperationExecutable && config.hostOperationExecutableSha256 && config.hostOperationOwnerClientId && config.hostOperationCwd ? new HostOperationRegistrar(durableOperations.store, { enabled: true, @@ -9389,6 +9470,7 @@ export function createServer( coreMutationSessions, undefined, // coreMutationTestOnlyBypass — production never set workResumeStore, + ownerApprovalReceipts, ); const reboundSessionIds = new Set(); From 5635443848a971d8fe35cd3c16baf4d65e986c4e Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:45:05 +0800 Subject: [PATCH 09/14] test(issue-407): persist approval decisions across reconnect --- src/carrier-binding-http.test.ts | 37 ++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/carrier-binding-http.test.ts b/src/carrier-binding-http.test.ts index 969a28f51..1811bbc65 100644 --- a/src/carrier-binding-http.test.ts +++ b/src/carrier-binding-http.test.ts @@ -613,7 +613,8 @@ test("Owner-facing elicitation approves exactly one bounded carrier without Term {req:{method:"POST",body:{owner_token:config.oauth.ownerToken}},redirect:(_status:number,url:string)=>{redirect=url;}} as never, ); const tokens=await provider.exchangeAuthorizationCode(oauthClient,new URL(redirect).searchParams.get("code")!); - const running=createServer(config); + let carrierClock=Date.now(); + const running=createServer(config,{carrierClock:()=>carrierClock}); const listener=running.app.listen(0,"127.0.0.1"); await new Promise((resolve,reject)=>{listener.once("listening",resolve);listener.once("error",reject);}); const url=new URL(`http://127.0.0.1:${(listener.address() as {port:number}).port}/mcp`); @@ -679,7 +680,7 @@ test("Owner-facing elicitation approves exactly one bounded carrier without Term assert.equal(replay.carrierId,carrierId); const mismatch=data(await owner.callTool({ - name:"coordination_owner_approval_status", + name:"coordination_owner_approval_prepare", arguments:{pendingId:pending.pendingId,contract:{...contract,goal:"issue407-changed"}}, })); assert.equal(mismatch.status,"CONTRACT_MISMATCH"); @@ -687,9 +688,9 @@ test("Owner-facing elicitation approves exactly one bounded carrier without Term const fresh=await connect("fresh-status",false); const durable=data(await fresh.callTool({ name:"coordination_owner_approval_status", - arguments:{pendingId:pending.pendingId,contract}, + arguments:{pendingId:pending.pendingId}, })); - assert.equal(durable.status,"ALREADY_APPROVED_SAME_CONTRACT"); + assert.equal(durable.status,"APPROVED"); assert.equal(durable.carrierId,carrierId); const hijack=await fresh.callTool({ name:"coordination_owner_approve", @@ -705,6 +706,12 @@ test("Owner-facing elicitation approves exactly one bounded carrier without Term })); assert.equal(declined.status,"REJECTED"); assert.equal((await owner.callTool({name:"coordination_resume",arguments:{pendingId:declinedPending.pendingId}})).isError,true); + const declinedFresh=await connect("declined-status",false); + const declinedDurable=data(await declinedFresh.callTool({ + name:"coordination_owner_approval_status", + arguments:{pendingId:declinedPending.pendingId}, + })); + assert.equal(declinedDurable.status,"REJECTED"); approval="cancel"; const cancelledPending=data(await owner.callTool({name:"coordination_pair",arguments:{}})); @@ -715,6 +722,28 @@ test("Owner-facing elicitation approves exactly one bounded carrier without Term assert.equal(cancelled.status,"PENDING_OWNER_APPROVAL"); assert.equal(cancelled.elicitationAction,"cancel"); assert.equal((await owner.callTool({name:"coordination_resume",arguments:{pendingId:cancelledPending.pendingId}})).isError,true); + const cancelledFresh=await connect("cancelled-status",false); + const cancelledDurable=data(await cancelledFresh.callTool({ + name:"coordination_owner_approval_status", + arguments:{pendingId:cancelledPending.pendingId}, + })); + assert.equal(cancelledDurable.status,"PENDING_OWNER_APPROVAL"); + + const expiringPending=data(await owner.callTool({name:"coordination_pair",arguments:{}})); + const expiringContract:CarrierContract={...contract,goal:"issue407-expiring",expiresAt:new Date(carrierClock+1000).toISOString()}; + const expiring=data(await owner.callTool({ + name:"coordination_owner_approve", + arguments:{pendingId:expiringPending.pendingId,contract:expiringContract}, + })); + assert.equal(expiring.status,"PENDING_OWNER_APPROVAL"); + carrierClock+=2000; + const expiredFresh=await connect("expired-status",false); + const expired=data(await expiredFresh.callTool({ + name:"coordination_owner_approval_status", + arguments:{pendingId:expiringPending.pendingId}, + })); + assert.equal(expired.status,"EXPIRED"); + assert.equal((await owner.callTool({name:"coordination_resume",arguments:{pendingId:expiringPending.pendingId}})).isError,true); } finally { for(const client of clients.reverse()) await client.close().catch(()=>{}); await new Promise(resolve=>listener.close(()=>resolve())); From f52efd4ae77ded55f1935b677d2bf951e795d7fd Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:46:48 +0800 Subject: [PATCH 10/14] fix(issue-407): reconcile approval receipt from carrier truth --- src/server.ts | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/server.ts b/src/server.ts index d322040df..f1e2bb96e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4539,7 +4539,7 @@ export function createMcpServer( registerAppTool(server,"coordination_pair",{...registration,title:"Request a carrier pairing",description:"Request explicit Owner approval for new bounded authority. Save the returned pendingId; an Owner-facing client may approve the exact contract with coordination_owner_approve before coordination_resume.",inputSchema:{}},async(_,extra)=>result(carrierBindings.requestPairing(dependencyConsumerContext(extra)))); if (ownerApprovalReceipts) { const ownerApprovalRoots=[...config.allowedRoots,config.worktreeRoot]; - const ownerApprovalPreview=(pendingId:string,contract:CarrierContract,extra:{authInfo?:{clientId:string;scopes:string[]};sessionId?:string},sameSession=true)=>{ + const ownerApprovalPreview=(pendingId:string,contract:CarrierContract,extra:{authInfo?:{clientId:string;scopes:string[]};sessionId?:string},sameSession=true,reconcileApproved=false)=>{ const context=dependencyConsumerContext(extra); const pending=ownerApprovalReceipts.pairing(pendingId); if(!pending) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Pairing request not found"); @@ -4560,17 +4560,19 @@ export function createMcpServer( const approvedJson=JSON.stringify(approved.contract); const approvedHash=createHash("sha256").update(approvedJson).digest("hex"); if(approvedHash!==contractHash) return {status:"CONTRACT_MISMATCH" as const,pendingId,contractHash,approvedContractHash:approvedHash,expiresAt:new Date(pending.expiresAtMs).toISOString(),canonical}; - const receipt=ownerApprovalReceipts.reconcileApproved({ - pendingId, - clientId:pending.clientId, - sessionId:pending.sessionId, - contractHash, - contractJson, - expiresAtMs:Math.min(pending.expiresAtMs,Date.parse(canonical.expiresAt)), - carrierId:approved.id, - }); - if(receipt.status==="CONTRACT_MISMATCH") return {status:"CONTRACT_MISMATCH" as const,pendingId,contractHash,expectedContractHash:receipt.contractHash,expiresAt:new Date(receipt.expiresAtMs).toISOString(),canonical}; - return {status:"ALREADY_APPROVED_SAME_CONTRACT" as const,pendingId,contractHash,carrierId:approved.id,expiresAt:new Date(receipt.expiresAtMs).toISOString(),canonical}; + const receipt=reconcileApproved + ? ownerApprovalReceipts.reconcileApproved({ + pendingId, + clientId:pending.clientId, + sessionId:pending.sessionId, + contractHash, + contractJson, + expiresAtMs:Math.min(pending.expiresAtMs,Date.parse(canonical.expiresAt)), + carrierId:approved.id, + }) + : existingReceipt; + if(receipt?.status==="CONTRACT_MISMATCH") return {status:"CONTRACT_MISMATCH" as const,pendingId,contractHash,expectedContractHash:receipt.contractHash,expiresAt:new Date(receipt.expiresAtMs).toISOString(),canonical}; + return {status:"ALREADY_APPROVED_SAME_CONTRACT" as const,pendingId,contractHash,carrierId:approved.id,expiresAt:new Date(receipt?.expiresAtMs??Math.min(pending.expiresAtMs,Date.parse(canonical.expiresAt))).toISOString(),canonical}; } if(existingReceipt?.status==="REJECTED") return {status:"REJECTED" as const,pendingId,contractHash,expiresAt:new Date(existingReceipt.expiresAtMs).toISOString(),canonical}; if(existingReceipt?.status==="EXPIRED" || pending.expired) return {status:"EXPIRED" as const,pendingId,contractHash,expiresAt:new Date(existingReceipt?.expiresAtMs??pending.expiresAtMs).toISOString(),canonical}; @@ -4583,8 +4585,27 @@ export function createMcpServer( registerAppTool(server,"coordination_owner_approval_status",{...registration,annotations:{...registration.annotations,readOnlyHint:true,idempotentHint:true},title:"Read Owner carrier approval status",description:"Read durable pending/approved/rejected/expired approval state for one pairing from the authenticated OAuth client. Does not bind the current session or grant authority.",inputSchema:{pendingId:z.string()}},async({pendingId},extra)=>{ const context=dependencyConsumerContext(extra); const receipt=ownerApprovalReceipts.read(pendingId); + const pending=ownerApprovalReceipts.pairing(pendingId); if(receipt) { if(receipt.clientId!==context.clientId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Approval receipt belongs to a different authenticated client"); + if(pending?.carrierId) { + const approved=carrierBindings.inspectLocal(pending.carrierId); + const approvedJson=JSON.stringify(approved.contract); + const approvedHash=createHash("sha256").update(approvedJson).digest("hex"); + if(approvedHash!==receipt.contractHash || approvedJson!==receipt.contractJson) { + return result({status:"CONTRACT_MISMATCH",pendingId,contractHash:receipt.contractHash,approvedContractHash:approvedHash,expiresAt:new Date(receipt.expiresAtMs).toISOString()}); + } + const reconciled=ownerApprovalReceipts.reconcileApproved({ + pendingId, + clientId:pending.clientId, + sessionId:pending.sessionId, + contractHash:receipt.contractHash, + contractJson:receipt.contractJson, + expiresAtMs:receipt.expiresAtMs, + carrierId:approved.id, + }); + return result({status:"APPROVED",pendingId,contractHash:reconciled.contractHash,carrierId:approved.id,expiresAt:new Date(reconciled.expiresAtMs).toISOString(),decidedAt:reconciled.decidedAt}); + } return result({ status:receipt.status, pendingId, @@ -4594,7 +4615,6 @@ export function createMcpServer( decidedAt:receipt.decidedAt, }); } - const pending=ownerApprovalReceipts.pairing(pendingId); if(!pending || pending.clientId!==context.clientId) throw new ControlPlaneOwnershipError("AUTHORITY_REQUIRED","Pairing request not found"); if(pending.carrierId) { const approved=carrierBindings.inspectLocal(pending.carrierId); @@ -4615,7 +4635,7 @@ export function createMcpServer( }); registerAppTool(server,"coordination_owner_approve",{...registration,title:"Request explicit Owner carrier approval",description:"Ask the connected human Owner to approve one exact pending carrier contract through MCP elicitation. Tool arguments alone cannot approve it; decline, cancel, unsupported elicitation, drift, or expiry grants no authority.",inputSchema:{pendingId:z.string(),contract:ownerContractSchema}},async({pendingId,contract},extra)=>{ const exact=contract as CarrierContract; - const preview=ownerApprovalPreview(pendingId,exact,extra); + const preview=ownerApprovalPreview(pendingId,exact,extra,true,true); if(preview.status!=="PENDING_OWNER_APPROVAL") return result({...preview,canonical:undefined,contractJson:undefined,expiresAtMs:undefined}); const context=dependencyConsumerContext(extra); const receipt=ownerApprovalReceipts.ensurePending({ From 0fccc59bbc99858feb0695e62e793a785b6da463 Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:47:15 +0800 Subject: [PATCH 11/14] test(issue-407): reconcile approval receipt from carrier state --- src/carrier-binding-http.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/carrier-binding-http.test.ts b/src/carrier-binding-http.test.ts index 1811bbc65..a51be4dff 100644 --- a/src/carrier-binding-http.test.ts +++ b/src/carrier-binding-http.test.ts @@ -729,6 +729,30 @@ test("Owner-facing elicitation approves exactly one bounded carrier without Term })); assert.equal(cancelledDurable.status,"PENDING_OWNER_APPROVAL"); + const reconcilePending=data(await owner.callTool({name:"coordination_pair",arguments:{}})); + const reconcileContract:CarrierContract={...contract,goal:"issue407-reconcile"}; + const reconcilePrepared=data(await owner.callTool({ + name:"coordination_owner_approve", + arguments:{pendingId:reconcilePending.pendingId,contract:reconcileContract}, + })); + assert.equal(reconcilePrepared.status,"PENDING_OWNER_APPROVAL"); + const hostApprover=new CarrierBindingStore(config.stateDir,()=>carrierClock); + const externallyApproved=hostApprover.approveLocal(reconcilePending.pendingId,reconcileContract); + hostApprover.close(); + const reconcileFresh=await connect("reconcile-status",false); + const reconciled=data(await reconcileFresh.callTool({ + name:"coordination_owner_approval_status", + arguments:{pendingId:reconcilePending.pendingId}, + })); + assert.equal(reconciled.status,"APPROVED"); + assert.equal(reconciled.carrierId,externallyApproved.id); + const reconcileReplay=data(await owner.callTool({ + name:"coordination_owner_approve", + arguments:{pendingId:reconcilePending.pendingId,contract:reconcileContract}, + })); + assert.equal(reconcileReplay.status,"ALREADY_APPROVED_SAME_CONTRACT"); + assert.equal(reconcileReplay.carrierId,externallyApproved.id); + const expiringPending=data(await owner.callTool({name:"coordination_pair",arguments:{}})); const expiringContract:CarrierContract={...contract,goal:"issue407-expiring",expiresAt:new Date(carrierClock+1000).toISOString()}; const expiring=data(await owner.callTool({ From 6adadf3641add35c477ad12310a01ddfd57fa22b Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:49:09 +0800 Subject: [PATCH 12/14] fix(issue-407): mark receipt reconciliation non-readonly --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index f1e2bb96e..1a85b4df2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4582,7 +4582,7 @@ export function createMcpServer( const preview=ownerApprovalPreview(pendingId,contract as CarrierContract,extra); return result({...preview,canonical:undefined,contractJson:undefined,expiresAtMs:undefined}); }); - registerAppTool(server,"coordination_owner_approval_status",{...registration,annotations:{...registration.annotations,readOnlyHint:true,idempotentHint:true},title:"Read Owner carrier approval status",description:"Read durable pending/approved/rejected/expired approval state for one pairing from the authenticated OAuth client. Does not bind the current session or grant authority.",inputSchema:{pendingId:z.string()}},async({pendingId},extra)=>{ + registerAppTool(server,"coordination_owner_approval_status",{...registration,annotations:{...registration.annotations,readOnlyHint:false,idempotentHint:true},title:"Read/reconcile Owner carrier approval status",description:"Read durable pending/approved/rejected/expired approval state for one pairing from the authenticated OAuth client. May reconcile the derived receipt from authoritative carrier state after a lost acknowledgement; never mints a carrier, binds the current session, or grants authority.",inputSchema:{pendingId:z.string()}},async({pendingId},extra)=>{ const context=dependencyConsumerContext(extra); const receipt=ownerApprovalReceipts.read(pendingId); const pending=ownerApprovalReceipts.pairing(pendingId); From 1ea88afbdba90a824979a6a9916d3fcf48407356 Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:53:00 +0800 Subject: [PATCH 13/14] test(issue-407): advance schema migration expectation --- src/chat-swarm-store.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat-swarm-store.test.ts b/src/chat-swarm-store.test.ts index c4868530d..1d512ecac 100644 --- a/src/chat-swarm-store.test.ts +++ b/src/chat-swarm-store.test.ts @@ -19,7 +19,7 @@ test("RESULT_READY releases worker while retaining immutable result until collec test("restart converts possibly started work to reconciliation but keeps queued work queued", () => { const f = fixture(); let queuedId: string; let runningId: string; try { queuedId = f.store.createTask({ swarmId: f.swarm.id, taskKey: "queued", prompt: "q" }).task.id; runningId = f.store.createTask({ swarmId: f.swarm.id, taskKey: "running", prompt: "r" }).task.id; f.store.claimTask(runningId, f.worker.id); f.store.startTask(runningId, f.worker.id); const attempt = f.store.listAttempts(runningId)[0]!; f.store.close(); f.store = new ChatSwarmStore(f.root); assert.equal(f.store.getTask(runningId)?.lifecycleState, "RUNNING"); f.store.recoverAfterRestart(); assert.equal(f.store.getTask(queuedId)?.lifecycleState, "QUEUED"); assert.equal(f.store.getTask(runningId)?.lifecycleState, "RECONCILE_REQUIRED"); assert.throws(() => f.store.claimTask(runningId, f.worker.id), (e: unknown) => e instanceof ChatSwarmError && e.code === "RECONCILIATION_REQUIRED"); assert.throws(() => f.store.resolveReconciliation(runningId, "REQUEUE"), (e: unknown) => e instanceof ChatSwarmError && e.code === "RECONCILIATION_REQUIRED"); assert.equal(f.store.resolveReconciliation(runningId, "REQUEUE", undefined, { taskId: runningId, attemptId: attempt.id, disposition: "NO_EFFECT", evidenceRef: "restart-probe-1" }).lifecycleState, "QUEUED"); } finally { cleanup(f); } }); -test("migration is idempotent and payload boundaries are enforced", () => { const f = fixture(); try { const sqlite = (f.store as unknown as { sqlite: { prepare: (sql: string) => { get: () => { version: number } } } }).sqlite; assert.equal(sqlite.prepare("select max(version) as version from devspace_schema_migrations").get().version, 25); const prompt = f.store.createTask({ swarmId: f.swarm.id, taskKey: "max", prompt: "x".repeat(MAX_PROMPT_BYTES) }).task; f.store.claimTask(prompt.id, f.worker.id); assert.equal(f.store.submitResult(prompt.id, f.worker.id, "x".repeat(MAX_RESULT_BYTES)).result?.length, MAX_RESULT_BYTES); const overflow = f.store.createTask({ swarmId: f.swarm.id, taskKey: "overflow", prompt: "p" }).task; f.store.claimTask(overflow.id, f.worker.id); assert.throws(() => f.store.submitResult(overflow.id, f.worker.id, "x".repeat(MAX_RESULT_BYTES + 1)), ChatSwarmError); f.store.close(); f.store = new ChatSwarmStore(f.root); f.store.recoverAfterRestart(); assert.equal(f.store.getTask(overflow.id)?.lifecycleState, "RECONCILE_REQUIRED"); } finally { cleanup(f); } }); +test("migration is idempotent and payload boundaries are enforced", () => { const f = fixture(); try { const sqlite = (f.store as unknown as { sqlite: { prepare: (sql: string) => { get: () => { version: number } } } }).sqlite; assert.equal(sqlite.prepare("select max(version) as version from devspace_schema_migrations").get().version, 26); const prompt = f.store.createTask({ swarmId: f.swarm.id, taskKey: "max", prompt: "x".repeat(MAX_PROMPT_BYTES) }).task; f.store.claimTask(prompt.id, f.worker.id); assert.equal(f.store.submitResult(prompt.id, f.worker.id, "x".repeat(MAX_RESULT_BYTES)).result?.length, MAX_RESULT_BYTES); const overflow = f.store.createTask({ swarmId: f.swarm.id, taskKey: "overflow", prompt: "p" }).task; f.store.claimTask(overflow.id, f.worker.id); assert.throws(() => f.store.submitResult(overflow.id, f.worker.id, "x".repeat(MAX_RESULT_BYTES + 1)), ChatSwarmError); f.store.close(); f.store = new ChatSwarmStore(f.root); f.store.recoverAfterRestart(); assert.equal(f.store.getTask(overflow.id)?.lifecycleState, "RECONCILE_REQUIRED"); } finally { cleanup(f); } }); test("unknown persisted task state fails closed", () => { const f = fixture(); try { const sqlite = (f.store as unknown as { sqlite: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).sqlite; const task = f.store.createTask({ swarmId: f.swarm.id, taskKey: "corrupt", prompt: "p" }).task; sqlite.prepare("update chat_swarm_tasks set lifecycle_state=? where id=?").run("CORRUPT", task.id); assert.throws(() => f.store.getTask(task.id), (e: unknown) => e instanceof ChatSwarmError && e.code === "INVALID_STATE"); } finally { cleanup(f); } }); From 7185066896e92513ab47de1f1a2f7aea26515752 Mon Sep 17 00:00:00 2001 From: James3014 Date: Wed, 7 Oct 2026 05:59:08 +0800 Subject: [PATCH 14/14] test(issue-407): advance oauth migration ledger --- src/oauth-store.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/oauth-store.test.ts b/src/oauth-store.test.ts index 0019a305d..3b40e7e65 100644 --- a/src/oauth-store.test.ts +++ b/src/oauth-store.test.ts @@ -66,6 +66,7 @@ async function testDatabaseConfiguration(stateDir: string): Promise { { version: 23, name: "core-candidate-acquisition-observations" }, { version: 24, name: "core-candidate-acquisition-receipt-path" }, { version: 25, name: "work-resume-registry" }, + { version: 26, name: "carrier-owner-approval-receipts" }, ]); } finally { database.close();