/* * mnd8t reference executor — customer-side, runs OUTSIDE the mnd8t trust * boundary with YOUR execution credentials. mnd8t never sees them. * * Running it: * 1. npm install @mnd8t/sdk * 2. Replace the "@mandate/wallet-adapters" import below with your own * provider call, or keep it if you are running inside a clone of the * mnd8t repository (pnpm --filter @mandate/reference-executor start), * where the simulated and EVM testnet adapters are available. * 3. MANDATE_API_KEY=mdt_test_... MANDATE_API_URL=https://api.your-workspace.example \ * npx tsx reference-executor.ts * * Sandbox keys never touch real money, and live-money execution is disabled * in the service. See /docs/wallet-adapters and /docs/known-limitations. */ /** * Customer-side reference executor (CR-001-05). * * This process represents the CUSTOMER, not Mandate. It runs outside the * Mandate SaaS trust boundary, holds the customer's own execution * credentials, and demonstrates the required flow: * * 1. submit a decision intent (or receive one from your agent); * 2. receive the signed authorisation artifact; * 3. verify the artifact signature locally against the well-known key; * 4. claim the artifact (atomic, single use); * 5. execute through the customer's own wallet/provider; * 6. report the outcome back to Mandate's confirmation API. * * Environment (all customer-held; Mandate never sees these): * MANDATE_API_KEY customer's Mandate API key * MANDATE_API_URL default http://localhost:4000 * EXECUTOR_REFERENCE instance name, default reference-executor-1 * ENABLE_REFERENCE_TESTNET_EXECUTOR 'true' to use the EVM testnet adapter * WALLET_TESTNET_RPC_URL / WALLET_TESTNET_CHAIN_ID / * WALLET_TESTNET_PRIVATE_KEY / WALLET_TESTNET_TOKEN_ADDRESS * testnet credentials, TESTNET FUNDS ONLY * * Without testnet configuration the simulated adapter is used, so the full * loop runs locally with zero credentials. */ import { MandateClient, verifyReceiptSignature } from "@mnd8t/sdk"; import { EvmTestnetAdapter, SimulatedWalletAdapter, type WalletAdapter, } from "@mandate/wallet-adapters"; const client = new MandateClient({ apiKey: process.env.MANDATE_API_KEY ?? "", baseUrl: process.env.MANDATE_API_URL ?? "http://localhost:4000", }); const baseUrl = (process.env.MANDATE_API_URL ?? "http://localhost:4000").replace(/\/$/, ""); const executorReference = process.env.EXECUTOR_REFERENCE ?? "reference-executor-1"; const useTestnet = process.env.ENABLE_REFERENCE_TESTNET_EXECUTOR === "true"; const adapter: WalletAdapter = useTestnet ? new EvmTestnetAdapter({ enabled: true, rpcUrl: process.env.WALLET_TESTNET_RPC_URL ?? "", chainId: Number(process.env.WALLET_TESTNET_CHAIN_ID ?? 84532), privateKey: process.env.WALLET_TESTNET_PRIVATE_KEY ?? "", tokenAddress: process.env.WALLET_TESTNET_TOKEN_ADDRESS || undefined, }) : new SimulatedWalletAdapter(); console.log(`[executor] ${executorReference} using adapter ${adapter.providerName}`); const connection = await adapter.validateConnection({}); if (!connection.ok) { console.error(`[executor] provider connection failed: ${connection.detail}`); process.exit(1); } // ── 1. Ask Mandate for authority ───────────────────────────────────────── const destination = process.env.WALLET_TESTNET_DESTINATION ?? "0x1111111111111111111111111111111111111111"; const decision = await client.decisions.authorize({ agent_external_id: process.env.MANDATE_AGENT_EXTERNAL_ID ?? "procurement_agent_12", external_reference: `executor_demo_${Date.now()}`, action_type: "PAYMENT", amount_minor: 2_500, policy_amount_minor: 2_500, policy_currency: "GBP", asset: "USDC", counterparty_external_id: "supplier_acme_cloud", destination, purpose: "cloud_infrastructure", } as never); console.log(`[executor] decision: ${decision.effective_decision} (${decision.reason_codes.join(", ")})`); const artifactInfo = (decision as unknown as { authorisation_artifact: { id: string } | null }).authorisation_artifact; if (decision.effective_decision !== "APPROVE" || !artifactInfo) { console.log("[executor] no artifact issued — nothing to execute. Stopping."); process.exit(0); } // ── 2. Fetch and locally verify the artifact ───────────────────────────── const envelope = (await fetchJson(`/v1/authorisation-artifacts/${artifactInfo.id}`)) as { id: string; artifact: Record; }; const keysDoc = (await (await fetch(`${baseUrl}/.well-known/mandate-keys.json`)).json()) as { keys: Array<{ key_id: string; public_key_pem: string }>; }; const keyId = (envelope.artifact["signature"] as { key_id?: string } | undefined)?.key_id; const key = keysDoc.keys.find((k) => k.key_id === keyId) ?? keysDoc.keys[0]; const verification = await verifyReceiptSignature(envelope.artifact, key!.public_key_pem); if (!verification.valid) { console.error(`[executor] REFUSING to execute: artifact signature invalid (${verification.reason})`); process.exit(1); } console.log("[executor] artifact signature verified locally ✓"); // Bindings check: executor only acts on what the artifact says. const a = envelope.artifact as { amount_minor: number; asset: string; destination: string | null; action_type: string; expires_at: string; }; if (new Date(a.expires_at) < new Date()) { console.error("[executor] artifact expired before claim; requesting nothing."); process.exit(1); } // ── 3. Claim (atomic, single use) ──────────────────────────────────────── const claim = (await fetchJson(`/v1/authorisation-artifacts/${envelope.id}/claim`, { executor_reference: executorReference, })) as { status: string }; console.log(`[executor] artifact claimed (${claim.status})`); // ── 4. Execute with CUSTOMER credentials ───────────────────────────────── const execution = await adapter.execute( { intentId: decision.id, organisationId: String(envelope.artifact["organisation_id"]), amountMinor: a.amount_minor, asset: a.asset, destination: a.destination ?? destination, counterpartyExternalId: (envelope.artifact["counterparty_reference"] as string) ?? null, externalReference: (envelope.artifact["external_client_reference"] as string) ?? null, }, { receiptId: envelope.id, receiptHash: "", decisionStatus: "APPROVED" }, ); console.log(`[executor] execution: ${execution.status} ${execution.externalReference ?? ""} — ${execution.detail}`); if (execution.status === "FAILED" || !execution.externalReference) { // /fail is the failure path: it says the money definitely did not move, and // it is the only call that releases a hold once the artifact was claimed. // (/cancel is refused while a claim is unaccounted for — see the policy // model.) Cancelling afterwards is this example abandoning the action // rather than reissuing a fresh artifact and retrying. await fetchJson(`/v1/authorisation-artifacts/${envelope.id}/fail`, { reason_code: "PROVIDER_REJECTED", reported_by: "CUSTOMER_EXECUTOR", }); await client.decisions.cancel(decision.id, "execution failed at provider"); console.log("[executor] failure reported with /fail; budget released, decision abandoned."); process.exit(1); } // ── 5. Poll until final, then report the outcome to Mandate ────────────── let status: import("@mandate/wallet-adapters").WalletExecutionStatus = execution.status; while (status === "PENDING") { await new Promise((resolve) => setTimeout(resolve, 3000)); status = await adapter.getStatus(execution.externalReference); console.log(`[executor] provider status: ${status}`); } if (status === "CONFIRMED") { await fetchJson(`/v1/decision-intents/${decision.id}/confirm`, { provider: adapter.providerName, external_reference: execution.externalReference, authorisation_artifact_id: envelope.id, reported_by: "CUSTOMER_EXECUTOR", verification: { method: useTestnet ? "NETWORK_LOOKUP" : "PROVIDER_LOOKUP", verified: true }, }); console.log("[executor] outcome reported to Mandate — budget consumed, evidence chain updated."); } else { await fetchJson(`/v1/authorisation-artifacts/${envelope.id}/fail`, { reason_code: "EXECUTION_FAILED_ONCHAIN", reported_by: "CUSTOMER_EXECUTOR", }); await client.decisions.cancel(decision.id, "execution failed on-chain"); console.log("[executor] on-chain failure reported with /fail; budget released, decision abandoned."); } async function fetchJson(path: string, body?: unknown): Promise { const res = await fetch(`${baseUrl}${path}`, { method: body === undefined ? "GET" : "POST", headers: { authorization: `Bearer ${process.env.MANDATE_API_KEY}`, "content-type": "application/json", "idempotency-key": `executor-${crypto.randomUUID()}`, }, body: body === undefined ? undefined : JSON.stringify(body), }); const json = (await res.json()) as unknown; if (!res.ok) { throw new Error(`${path} failed: ${JSON.stringify(json)}`); } return json; }