From 7604daa242fedefebfcfb003c7431fb34a65cc82 Mon Sep 17 00:00:00 2001 From: Terence Tan Date: Thu, 16 Jul 2026 00:14:17 +0800 Subject: [PATCH 1/3] fix: persist sandbox before asynchronous bootstrap (cherry picked from commit d0438e08e8c1eca695824a3e2a245cd16c5811b1) --- docs/onecomputer/azure-e2e-runbook.md | 255 ++++++++++++++++++ packages/api/src/routes/sandboxes.ts | 68 ++++- .../sandbox-providers/kasm-local-provider.ts | 17 +- .../src/services/sandbox-providers/types.ts | 9 + 4 files changed, 339 insertions(+), 10 deletions(-) create mode 100644 docs/onecomputer/azure-e2e-runbook.md diff --git a/docs/onecomputer/azure-e2e-runbook.md b/docs/onecomputer/azure-e2e-runbook.md new file mode 100644 index 0000000..f3af3db --- /dev/null +++ b/docs/onecomputer/azure-e2e-runbook.md @@ -0,0 +1,255 @@ +# ONEComputer × OpenVTC Azure E2E runbook + +## Acceptance target + +The release candidate is complete only when one recorded Azure run proves: + +1. A company admin configures an organization-wide `manual_approval` policy for + `POST graph.microsoft.com/v1.0/me/sendMail`. +2. An employee creates and opens their own Kasm sandbox. +3. Claude initiates the governed Outlook action through the ONEComputer gateway. +4. The gateway holds the HTTP request and creates a durable approval record. +5. OpenVTC alerts the manager with the action digest and human-readable preview. +6. OpenVTC delivers the manager an `auth/step-up/approve-request/0.1` through + the configured TSP/DIDComm transport; the separate wallet signs + `auth/step-up/approve-response/0.2` with the manager's key. +7. ONEComputer verifies the signed response, delegated manager authorization, + exact subject/session/challenge/recipient binding, and the gateway releases + the request once. +8. Unsigned, stale, altered, denied, timed-out, and replayed responses do not run. + +## Promotion path + +Code moves through these gates in order: + +```text +local branch -> local tests -> local runtime E2E -> Gitea -> CI -> Azure deploy -> Azure E2E +``` + +The Azure VM must deploy a specific green Git commit. Editing application source +directly on the VM is not a deployment method. + +## Sandbox lifecycle gate + +`POST /v1/sandboxes` must return a tenant-owned sandbox ID in `provisioning` +state before optional desktop or Claude bootstrap begins. The API persists that +row first, then performs bootstrap asynchronously and updates it to `started` +or `failed`. A caller must be able to use `DELETE /v1/sandboxes/:id` while +bootstrap is running; deletion must be idempotent and remove the provider +container plus the persisted row. Bootstrap failure must trigger the same +provider cleanup before the row is marked `failed`. + +Before a release can use the sandbox path as evidence, run both: + +1. normal create → `provisioning` → `started` → exec → delete; and +2. create → immediate delete during bootstrap → no surviving container, + bootstrap process, or sandbox row. + +Do not accept a manually removed container, an empty provider list without a +matching ownership-row check, or a UI-only status as lifecycle proof. + +## Azure source deployment + +- portal: `https://onecomputer-openvtc.eastus2.cloudapp.azure.com` +- internal portal/API: `http://127.0.0.1:10254` +- internal gateway: `http://127.0.0.1:10255` + +The public origin terminates TLS in nginx. HTTP redirects to HTTPS. The +approval device is an external OpenVTC wallet/VTA surface; the ONEComputer +portal is read-only for approval status. The certificate is managed by the +VM's Certbot systemd timer. + +Deploy the Gitea branch and run the hosted trust-loop gate: + +```bash +bash scripts/onecomputer/deploy-azure-vm.sh codex/azure-e2e-openvtc +GATEWAY_API_URL=http://127.0.0.1:10255 \ + pnpm exec dotenv -e .env -- \ + pnpm --filter @onecli/api e2e:openvtc-release +``` + +To execute the approved action against Microsoft Graph (this sends a real proof +email to the configured tenant recipient), add `E2E_GRAPH_LIVE=1`. A passing +live run ends with `upstream: "microsoft-graph"` and `upstreamStatus: 202`. + +### Azure OpenVTC DIDComm wallet gate + +The Azure staging VM now runs a separate OpenVTC manager wallet service and a +separate DIDComm bridge. The manager private material is outside the +ONEComputer checkout and the portal has no approval endpoint. The release gate +uses the deployed wallet service, not the older in-process test-wallet fixture: + +```bash +sudo bash -c 'set -a; source /etc/openvtc/wallet.env; set +a +runuser -u azureuser -- /opt/node22/bin/node \ + /home/azureuser/work/onecomputer/packages/api/node_modules/.bin/tsx \ + /home/azureuser/work/onecomputer/packages/api/src/scripts/e2e-openvtc-didcomm-release.ts' +``` + +The staging mediator uses authenticated DIDComm direct delivery because the +pinned mediator build does not implement coordinate mediation and the existing +manager account lacks the forwarded-message ACL. The wallet and bridge skip +enrollment in staging. This is a development-only posture; the promotion gate +remains a closed mediator with explicit ACL/keylist provisioning and forwarded +delivery. + +For the hosted Claude run, provision the short-lived Graph AppConnection from +the VM's Azure app credentials. The script encrypts the acquired token with +`SECRET_ENCRYPTION_KEY`, attaches it to the default employee agent, and never +prints or copies the token: + +```bash +cd /home/azureuser/work/onecomputer/packages/api +set -a; source ../../.env; set +a +/opt/node22/bin/node node_modules/tsx/dist/cli.mjs \ + src/scripts/provision-staging-graph-connection.ts +``` + +Gateway-created holds automatically dispatch their OpenVTC Trust Task when +`OPENVTC_TRANSPORT_BINDING=didcomm`; no manager action in the ONEComputer UI or +manual notification POST is part of the acceptance path. The explicit wallet +CLI remains the only approval action: + +```bash +sudo bash -c 'set -a; source /etc/openvtc/wallet.env; set +a +runuser -u azureuser -- /opt/node22/bin/node \ + /usr/local/lib/openvtc/openvtc-wallet.mjs approve ' +``` + +The legacy standalone wallet fixture below is retained only for local protocol +diagnostics and must not be presented as evidence of the Azure OpenVTC path. + +Start it on the VM using the API workspace: + +```bash +OPENVTC_TEST_WALLET_PORT=18100 \ +OPENVTC_TEST_WALLET_SEED_FILE=/run/user/$UID/onecomputer-test-wallet.seed \ +OPENVTC_APPROVAL_API_URL=http://127.0.0.1:10254/v1/openvtc-approvals \ +pnpm --filter @onecli/api wallet:openvtc-test +``` + +Generate the seed file with mode `0600`, keep it outside Git, and delete it +after the run. Do not pass a manager seed in a command-line argument or commit +it to the VM checkout. + +Before starting ONEComputer for the gate, configure the printed wallet DID as +`OPENVTC_APPROVER_DID`, set `OPENVTC_TRANSPORT_BINDING=rest`, and point +`OPENVTC_TASK_ENDPOINT_URL` at `http://127.0.0.1:18100/trust-tasks`. Restart +the web and gateway services together so the API and gateway use the same +`OPENVTC_RP_DID` and approver configuration. The E2E harness then: + +1. creates the real gateway hold; +2. queues the RP-signed request to the separate wallet process; +3. asserts the wallet received the alert and exact manager target; +4. submits an unsigned response and expects rejection; +5. calls the wallet's explicit `/approve/` endpoint; +6. waits for the gateway to release the controlled upstream or Graph request; +7. calls the wallet approval again and expects replay rejection. + +This proves the Azure hold → external wallet → signed response → one-time +release boundary. The promotion gate is not complete until the same contract is +run through the real OpenVTC VTA/mediator/TSP-or-DIDComm transport and the +fixture is removed from the production path. + +The service runtime reads root `.env`; the Next.js build also reads +`apps/web/.env.local`. `AUTH_URL`, `NEXTAUTH_URL`, and `PUBLIC_WEB_ORIGIN` must +all use the public HTTPS origin in the deployed environment. + +Kasm desktops bind noVNC to loopback. Deployment renders TLS nginx bridges for +ports `16901` through `16910`; keep the Azure NSG and UFW range aligned. The +`onecomputer-litellm-bridge` service exposes the host-only LiteLLM listener on +the Docker bridge only, allowing `host.docker.internal` from sandbox containers +without publishing the model service to the internet. + +The hosted seed currently pre-provisions the migration-harness identities into +Demo Corp. On first SSO session, the account's immutable subject replaces the +placeholder external ID while its intended org role is retained. + +Entra remains a temporary migration harness. It must not be treated as the +OpenVTC approval authority or as a substitute for the separate wallet. The +production E2E requires a manager DID with VMC/M-DID/trust-registry +authorization and an external OpenVTC wallet/VTA key. + +## Recorded browser acceptance run + +Use the public HTTPS portal and real Entra identities in this order: + +1. Sign in as `demo.admin@giniresearch.onmicrosoft.com`; in the organization + policy view configure a company-wide `manual_approval` rule for `POST` + `graph.microsoft.com` `sendMail`. +2. Sign in as `demo.member@giniresearch.onmicrosoft.com`; open the employee's + `alex-azure-e2e` Kasm desktop and invoke the governed Outlook-send action + from Claude. Record the sandbox ID and newly-created approval ID. +3. The manager's separate OpenVTC wallet/VTA receives the signed + `approve-request`, verifies the RP proof, displays the action, and signs the + canonical `approve-response/0.2`. The ONEComputer portal only displays the + resulting status; it cannot approve the action. +4. Confirm the held request is released once, Graph returns `202`, the approval + shows verified evidence, and a second approval attempt is rejected. + +The current Azure evidence confirms the sandbox has Claude Code `2.1.207`, a +working authenticated LiteLLM model catalog, and a valid public TLS desktop +endpoint. The manager/employee phone enrollments are intentionally the last +interactive prerequisite. + +The Azure feature branch contains the reusable staging provisioning script for +the `microsoft-graph` AppConnection. Its Graph access token is encrypted in +the control-plane database and the Rust gateway injects it only for +`graph.microsoft.com`; it must never be copied into the Kasm container. The +recorded Claude-originated live run is documented in +[`e2e-evidence-2026-07-12.md`](./e2e-evidence-2026-07-12.md) and returned Graph +`202` after the external wallet proof was verified. + +## Local source stack + +The reproducible local stack uses isolated ports so it cannot be confused with +an old container deployment: + +- portal/API: `http://127.0.0.1:11254` +- gateway: `http://127.0.0.1:11255` +- PostgreSQL: `127.0.0.1:5433` +- Kasm desktops: `https://127.0.0.1:17901+` + +Create `.env` from `.env.example`, pin a stable gateway signing seed, then run: + +```bash +pnpm install +pnpm e2e:local:start +pnpm e2e:local:status +pnpm exec dotenv -e .env -- pnpm --filter @onecli/api e2e:openvtc-release +pnpm e2e:local:stop +``` + +`scripts/onecomputer/local-e2e.sh` applies migrations, seeds Demo Corp, builds +the gateway from the current checkout, and starts the portal and gateway from +that same source tree. On Homebrew systems where only the `rustup` executable is +on `PATH`, it resolves the actual Cargo and Rust compiler through `rustup which`. + +The `e2e:openvtc-release` gate is a controlled wallet-boundary harness. It is +stronger than the former in-process signer because the private key is held by a +separate wallet process, but it is not evidence of production VTA/mobile +delivery until it uses the real OpenVTC VTA/mediator transport. + +## Cryptographic release invariant + +An `approved` database status is not authorization. The external OpenVTC wallet +signs a canonical `auth/step-up/approve-response/0.2` binding: + +- protocol/version; +- employee identity as `payload.subject`; +- approval ID as `payload.sessionId`; +- random request nonce as `payload.challenge`; +- decision; +- manager DID as `issuer` and `proof.verificationMethod`; +- the ONEComputer RP DID as `recipient`. + +The API resolves the manager's OpenVTC identity and company role, persists the +raw signed response, and only then emits the gateway-verifiable decision +credential. The gateway independently verifies the Data Integrity proof and +denies an approved status that lacks valid signed evidence or exact binding. + +## Evidence to retain + +Every candidate run should retain the Git SHA, policy rule, sandbox ID, gateway +approval ID, OpenVTC task hash, manager DID, signature verification result, +Graph request ID, audit events, and negative-case results. diff --git a/packages/api/src/routes/sandboxes.ts b/packages/api/src/routes/sandboxes.ts index e2ba67f..d858fa6 100644 --- a/packages/api/src/routes/sandboxes.ts +++ b/packages/api/src/routes/sandboxes.ts @@ -48,7 +48,14 @@ const mergeSandboxInfo = async (row: { const provider = getSandboxProvider(); try { const live = await provider.getSandbox(row.id); - return { ...live, name: row.name }; + const pending = row.status === "provisioning" || row.status === "failed"; + return { + ...live, + name: row.name, + state: pending ? row.status : live.state, + bootstrapped: pending ? false : live.bootstrapped, + desktopReady: pending ? false : live.desktopReady, + }; } catch { // Provider can't find it (stopped/removed) — return the persisted view. return { @@ -88,10 +95,9 @@ export const sandboxRoutes = () => { return c.json(sandboxes); }); - // POST /sandboxes — create a sandbox from the default snapshot. Body: - // { "name": string }. Real call, not a mock: polls until started then runs - // the bootstrap that installs the Claude Code CLI. Persists a Sandbox row - // with ownerId = auth.userId after the provider creates it. + // POST /sandboxes — allocate and persist a sandbox from the default snapshot. + // Long desktop/Claude bootstrap runs only after the ownership record exists, + // so DELETE can address an in-flight provider resource after a disconnect. app.post("/", async (c) => { const auth = c.get("auth"); const ability = c.get("ability"); @@ -106,9 +112,10 @@ export const sandboxRoutes = () => { where: { projectId, isDefault: true }, select: { accessToken: true }, }); + const provider = getSandboxProvider(); const sandbox = await withAudit( () => - getSandboxProvider().createSandbox(name, { + provider.createSandbox(name, { gatewayAgentToken: agent?.accessToken ?? undefined, }), (result) => ({ @@ -144,6 +151,55 @@ export const sandboxRoutes = () => { status: sandbox.state, }, }); + if (provider.bootstrapSandbox) { + void withAudit( + () => + provider.bootstrapSandbox!(sandbox.id, { + gatewayAgentToken: agent?.accessToken ?? undefined, + }), + (result) => ({ + organizationId: auth.organizationId, + userId: auth.userId, + userEmail: auth.userEmail, + action: AUDIT_ACTIONS.UPDATE, + service: AUDIT_SERVICES.SANDBOX, + source: AUDIT_SOURCE.API, + metadata: { + sandboxId: result.id, + state: result.state, + operation: "bootstrap", + }, + }), + ) + .then(async (ready) => { + await db.sandbox + .update({ + where: { id: sandbox.id }, + data: { status: ready.state }, + }) + .catch(() => undefined); + }) + .catch(async () => { + await withAudit( + () => provider.deleteSandbox(sandbox.id), + () => ({ + organizationId: auth.organizationId, + userId: auth.userId, + userEmail: auth.userEmail, + action: AUDIT_ACTIONS.DELETE, + service: AUDIT_SERVICES.SANDBOX, + source: AUDIT_SOURCE.API, + metadata: { + sandboxId: sandbox.id, + operation: "bootstrap-cleanup", + }, + }), + ).catch(() => undefined); + await db.sandbox + .update({ where: { id: sandbox.id }, data: { status: "failed" } }) + .catch(() => undefined); + }); + } return c.json(sandbox, 201); }); diff --git a/packages/api/src/services/sandbox-providers/kasm-local-provider.ts b/packages/api/src/services/sandbox-providers/kasm-local-provider.ts index 48bfad6..f109a66 100644 --- a/packages/api/src/services/sandbox-providers/kasm-local-provider.ts +++ b/packages/api/src/services/sandbox-providers/kasm-local-provider.ts @@ -537,10 +537,7 @@ export async function execInSandbox( } export const kasmLocalProvider: SandboxProvider = { - async createSandbox( - name: string, - options?: SandboxRuntimeOptions, - ): Promise { + async createSandbox(name: string): Promise { const id = name .toLowerCase() @@ -576,6 +573,18 @@ export const kasmLocalProvider: SandboxProvider = { IMAGE, ]); await new Promise((r) => setTimeout(r, 4000)); + const info = normalize(await inspectContainer(id)); + return { + ...info, + state: "provisioning", + bootstrapped: false, + desktopReady: false, + }; + }, + async bootstrapSandbox( + id: string, + options?: SandboxRuntimeOptions, + ): Promise { const install = await installClaudeDesktopAndCode(id); await ensureClaudeDesktopGatewayLoopback(id); await configureClaudeCodeProxy(id); diff --git a/packages/api/src/services/sandbox-providers/types.ts b/packages/api/src/services/sandbox-providers/types.ts index 1330b8c..7115301 100644 --- a/packages/api/src/services/sandbox-providers/types.ts +++ b/packages/api/src/services/sandbox-providers/types.ts @@ -38,6 +38,15 @@ export interface SandboxProvider { name: string, options?: SandboxRuntimeOptions, ): Promise; + /** + * Optional long-running setup after a stable provider identity has been + * persisted. Create must return before this work starts so cancellation can + * always address the sandbox by ID. + */ + bootstrapSandbox?( + id: string, + options?: SandboxRuntimeOptions, + ): Promise; listSandboxes(): Promise; getSandbox(id: string): Promise; execInSandbox(id: string, command: string): Promise; From 2b06853d4622460d60bf0ebb9cd366b3577ab439 Mon Sep 17 00:00:00 2001 From: Terence Tan Date: Thu, 16 Jul 2026 00:17:00 +0800 Subject: [PATCH 2/3] test: cover sandbox provisioning lifecycle (cherry picked from commit ebd0aa80f65ddbf780c1367b0f1aeb92c3f2fe8d) --- packages/api/src/routes/sandboxes.test.ts | 150 ++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 packages/api/src/routes/sandboxes.test.ts diff --git a/packages/api/src/routes/sandboxes.test.ts b/packages/api/src/routes/sandboxes.test.ts new file mode 100644 index 0000000..6da002d --- /dev/null +++ b/packages/api/src/routes/sandboxes.test.ts @@ -0,0 +1,150 @@ +import { Hono } from "hono"; +import type { MiddlewareHandler } from "hono"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => { + const auditOrder: string[] = []; + const provider = { + createSandbox: vi.fn(async () => { + auditOrder.push("create"); + return { + id: "sandbox-provisioning", + name: "sandbox-provisioning", + state: "provisioning", + provider: "kasm-local" as const, + bootstrapped: false, + }; + }), + bootstrapSandbox: vi.fn(async () => { + auditOrder.push("bootstrap"); + return { + id: "sandbox-provisioning", + name: "sandbox-provisioning", + state: "started", + provider: "kasm-local" as const, + bootstrapped: true, + }; + }), + listSandboxes: vi.fn(async () => []), + getSandbox: vi.fn(), + execInSandbox: vi.fn(), + deleteSandbox: vi.fn(async () => undefined), + getSandboxDesktop: vi.fn(), + restartSandboxDesktop: vi.fn(), + ensureVisualRuntime: vi.fn(), + captureScreenshot: vi.fn(), + }; + const db = { + agent: { findFirst: vi.fn(async () => ({ accessToken: "gateway-token" })) }, + sandbox: { + upsert: vi.fn(async () => { + auditOrder.push("persist"); + }), + update: vi.fn(async () => undefined), + findMany: vi.fn(async () => []), + findFirst: vi.fn(), + delete: vi.fn(), + }, + }; + return { auditOrder, provider, db }; +}); + +vi.mock("@onecli/db", () => ({ db: mocks.db })); +vi.mock("../services/sandbox-providers", () => ({ + getSandboxProvider: () => mocks.provider, +})); +vi.mock("../middleware/auth", () => { + const authMiddleware: MiddlewareHandler = async (c, next) => { + c.set("auth", { + userId: "user-1", + userEmail: "user@example.com", + organizationId: "org-1", + projectId: "project-1", + role: "admin", + }); + await next(); + }; + return { authMiddleware, requireProjectId: () => "project-1" }; +}); +vi.mock("../middleware/ability", () => ({ + withAbility: (async (_c, next) => next()) as MiddlewareHandler, + requireAbility: vi.fn(), +})); +vi.mock("../lib/ability", () => ({ subject: vi.fn() })); +vi.mock("../services/audit-service", () => ({ + withAudit: async (operation: () => Promise) => operation(), + AUDIT_ACTIONS: { + CREATE: "create", + UPDATE: "update", + DELETE: "delete", + CONNECT: "connect", + }, + AUDIT_SERVICES: { SANDBOX: "sandbox" }, + AUDIT_SOURCE: { API: "api" }, +})); +vi.mock("../services/governed-action-service", () => ({ + triggerGovernedAction: vi.fn(), +})); + +afterEach(() => { + mocks.auditOrder.splice(0); + vi.clearAllMocks(); +}); + +describe("sandbox provisioning lifecycle", () => { + it("persists a provisioning identity before starting long bootstrap", async () => { + const { sandboxRoutes } = await import("./sandboxes"); + const app = new Hono(); + app.route("/v1/sandboxes", sandboxRoutes()); + + const response = await app.request("/v1/sandboxes", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "Lifecycle proof" }), + }); + + expect(response.status).toBe(201); + await expect(response.json()).resolves.toMatchObject({ + id: "sandbox-provisioning", + state: "provisioning", + }); + await vi.waitFor(() => + expect(mocks.provider.bootstrapSandbox).toHaveBeenCalledOnce(), + ); + expect(mocks.auditOrder).toEqual(["create", "persist", "bootstrap"]); + await vi.waitFor(() => + expect(mocks.db.sandbox.update).toHaveBeenCalledWith({ + where: { id: "sandbox-provisioning" }, + data: { status: "started" }, + }), + ); + }); + + it("removes the provider resource when bootstrap fails", async () => { + mocks.provider.bootstrapSandbox.mockRejectedValueOnce( + new Error("bootstrap failed"), + ); + const { sandboxRoutes } = await import("./sandboxes"); + const app = new Hono(); + app.route("/v1/sandboxes", sandboxRoutes()); + + const response = await app.request("/v1/sandboxes", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: "Failure proof" }), + }); + + expect(response.status).toBe(201); + await vi.waitFor(() => + expect(mocks.provider.deleteSandbox).toHaveBeenCalledWith( + "sandbox-provisioning", + ), + ); + await vi.waitFor(() => + expect(mocks.db.sandbox.update).toHaveBeenCalledWith({ + where: { id: "sandbox-provisioning" }, + data: { status: "failed" }, + }), + ); + }); +}); From b2c10aa19f39b5566b3de516c79424cd00672817 Mon Sep 17 00:00:00 2001 From: Terence Tan Date: Thu, 16 Jul 2026 00:50:14 +0800 Subject: [PATCH 3/3] chore: restore API lint baseline --- .../scripts/e2e-openvtc-didcomm-release.ts | 19 +++++++++++++------ .../sandbox-providers/daytona-provider.ts | 12 +++--------- turbo.json | 15 ++++++++++++++- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/api/src/scripts/e2e-openvtc-didcomm-release.ts b/packages/api/src/scripts/e2e-openvtc-didcomm-release.ts index 90cbff8..31efcca 100644 --- a/packages/api/src/scripts/e2e-openvtc-didcomm-release.ts +++ b/packages/api/src/scripts/e2e-openvtc-didcomm-release.ts @@ -21,6 +21,8 @@ const WALLET_SCRIPT = "/usr/local/lib/openvtc/openvtc-wallet.mjs"; const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; const runCurl = ( token: string, @@ -89,7 +91,10 @@ const waitForPendingWalletRecord = async (approvalId: string) => { await access(path, constants.R_OK); return { path, - record: JSON.parse(await readFile(path, "utf8")) as Record, + record: JSON.parse(await readFile(path, "utf8")) as Record< + string, + unknown + >, }; } catch { await wait(250); @@ -225,8 +230,8 @@ const main = async () => { const candidateContext = candidate?.context as { _vti?: { stepUpRequest?: { - payload?: Record; - document?: Record; + payload?: Record; + document?: Record; }; }; } | null; @@ -248,8 +253,8 @@ const main = async () => { const context = approval.context as { _vti?: { stepUpRequest?: { - payload?: Record; - document?: Record; + payload?: Record; + document?: Record; }; }; }; @@ -289,7 +294,9 @@ const main = async () => { const verified = await getApprovalByBridgeId(approval.id); const completed = JSON.parse( await readFile(pending.path, "utf8"), - ) as Record; + ) as Record; + if (!isRecord(completed.response)) + throw new Error("wallet completion lacks a structured response"); const replay = await postWalletDecision(approval.id, completed.response); const result = { diff --git a/packages/api/src/services/sandbox-providers/daytona-provider.ts b/packages/api/src/services/sandbox-providers/daytona-provider.ts index 3d3036b..9d9f7c4 100644 --- a/packages/api/src/services/sandbox-providers/daytona-provider.ts +++ b/packages/api/src/services/sandbox-providers/daytona-provider.ts @@ -1,9 +1,5 @@ import * as daytona from "../daytona-service"; -import type { - SandboxInfo, - SandboxProvider, - SandboxRuntimeOptions, -} from "./types"; +import type { SandboxInfo, SandboxProvider } from "./types"; const normalize = (sandbox: daytona.SandboxInfo): SandboxInfo => ({ ...sandbox, @@ -11,13 +7,11 @@ const normalize = (sandbox: daytona.SandboxInfo): SandboxInfo => ({ }); export const daytonaProvider: SandboxProvider = { - createSandbox: async (name, _options?: SandboxRuntimeOptions) => - normalize(await daytona.createSandbox(name)), + createSandbox: async (name) => normalize(await daytona.createSandbox(name)), listSandboxes: async () => (await daytona.listSandboxes()).map(normalize), getSandbox: async (id) => normalize(await daytona.getSandbox(id)), execInSandbox: daytona.execInSandbox, deleteSandbox: daytona.deleteSandbox, getSandboxDesktop: daytona.getSandboxDesktop, - restartSandboxDesktop: async (id, _options?: SandboxRuntimeOptions) => - daytona.restartSandboxDesktop(id), + restartSandboxDesktop: async (id) => daytona.restartSandboxDesktop(id), }; diff --git a/turbo.json b/turbo.json index bfd70d0..cf50330 100644 --- a/turbo.json +++ b/turbo.json @@ -126,7 +126,20 @@ "AZURE_AD_CLIENT_ID", "AZURE_AD_CLIENT_SECRET", "AZURE_AD_TENANT_ID", - "E2E_GRAPH_LIVE" + "E2E_GRAPH_LIVE", + "OPENVTC_APPROVAL_API_URL", + "OPENVTC_WALLET_PENDING_DIR", + "OPENVTC_WALLET_SCRIPT", + "OPENVTC_TRANSPORT_BINDING", + "OPENVTC_TASK_ENDPOINT_URL", + "OPENVTC_TASK_ENDPOINT_BEARER", + "OPENVTC_TEST_WALLET_URL", + "OPENVTC_TEST_WALLET_HOST", + "OPENVTC_TEST_WALLET_PORT", + "OPENVTC_TEST_WALLET_SEED", + "OPENVTC_TEST_WALLET_SEED_FILE", + "ONECOMPUTER_E2E_PROJECT_ID", + "ONECOMPUTER_E2E_AGENT" ], "tasks": { "build": {