Skip to content

Commit a44fd4c

Browse files
V48 Gate 3 (specification-implementation): cooperative run cancel + sandbox host durability
Spec (NOTES #26 + parity matrix #26–31; QA runbook cancel/sandbox): - Exhaustive cancel law (row authority, cooperative abort, sandbox stop, UI) - Sandbox deposit: ephemeral boxes, OIDC/token auth fail-closed, sandboxId on context Implementation: - POST /api/executions/[runId]/cancel + execution-cancel helper - Deposit route polls cancel; sandbox shouldAbort + non-persistent create - VercelSandboxPipelineHost abort/cancel outcome; assertVercelSandboxAuthAvailable - Deposit UI Cancel run control + deposit_synthesis_cancelled analytics - Tests for cancel API, host abort, harness result shape, provisioning
1 parent f2ffb6e commit a44fd4c

16 files changed

Lines changed: 1007 additions & 98 deletions

BITCODE_SPEC_V48_NOTES.md

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,13 +1015,13 @@ is named in spec for closure):
10151015
- **Config.** `BITCODE_DEPOSIT_SYNTHESIS_PIPELINE` removed (full SDIVF pipeline only;
10161016
Validation — measurement + quality — never skipped).
10171017

1018-
### Gate-3 #25 — SandboxHost in-box deposit dispatch (Garrett, 2026-06-27)
1018+
### Gate-3 #25 — SandboxHost in-box deposit dispatch (Garrett, 2026-06-27; cancel+auth hardened 2026-07-08)
10191019

10201020
SandboxHost deposit runs the synthesis pipeline INSIDE the box (the asset-pack
10211021
harness already runs `assetPackPipeline` in-box for read-fit via a runner that imports
10221022
the live packages and executes the full SDIVF). Deposit reuses that mechanism:
10231023

1024-
- **Harness deposit mode.** `buildAssetPackSandboxHarness` accepts `synthesisMode`
1024+
- **Harness deposit mode.** `buildAssetPackSandboxHarness` accepts `synthesizeMode`
10251025
(`'deposit' | 'read'`) + the deposit STEERING (obfuscations, protected-IP exclusions,
10261026
demand context); these flow into `PipelineHarnessManifest` and the in-box runner
10271027
INPUT (`synthesizeMode`, steering). The in-box pipeline resolves deposit mode
@@ -1031,19 +1031,66 @@ the live packages and executes the full SDIVF). Deposit reuses that mechanism:
10311031
surfaces the synthesized `implementation:options` in `evidence.json` as
10321032
`depositOptions` (source-safe option metadata; the measure-agent's absolutes are
10331033
already attached by the Validation phase IN the box).
1034-
- **Route dispatch.** When the configured HostKind is `sandbox`, the deposit route
1035-
dispatches the deposit harness (a git `source` for the revision) via
1036-
`VercelSandboxPipelineHost.runHarness` (provider Vercel) instead of the in-process
1037-
InlineHost path; it reads `evidence.depositOptions`, runs the SAME pure projection
1038-
(`validateDepositSynthesisOptions` + `buildRealDepositAssetPackOptionSynthesis`) and
1039-
persists the deposit option synthesis to the execution row — identical to the inline
1040-
path. Source-safe telemetry streams from the harness telemetry artifact to
1041-
`execution_events`.
1042-
- **Parity of result.** InlineHost (in current box) and SandboxHost (in the provisioned
1043-
box) produce the SAME deposit option synthesis; only WHERE the pipeline runs differs.
1044-
Real-sandbox execution is verified against deployed sandbox infra (`@vercel/sandbox`,
1045-
`VERCEL_*`, git in the box image); the harness plan-building + the route dispatch +
1046-
the option projection are unit-tested with a mocked host.
1034+
- **Ephemeral boxes.** Deposit one-shots create sandboxes with `persistent: false`
1035+
(no snapshot storage on stop). Auth: prefer `VERCEL_OIDC_TOKEN` (local: `vercel env
1036+
pull`; automatic on Vercel); fallback `VERCEL_TOKEN` + `VERCEL_TEAM_ID` +
1037+
`VERCEL_PROJECT_ID`. `assertVercelSandboxAuthAvailable` fails closed before create.
1038+
- **Route dispatch.** When `BITCODE_PIPELINE_HOST=sandbox`, the deposit route
1039+
dispatches via `runDepositInBoxHarness``VercelSandboxPipelineHost.runHarness`
1040+
instead of InlineHost provision + in-process SDIVF; it reads `evidence.depositOptions`,
1041+
runs the SAME pure projection (`validateDepositSynthesisOptions` +
1042+
`buildRealDepositAssetPackOptionSynthesis`) and persists the deposit option
1043+
synthesis. `resolveDepositPipelineHost` is **inline-only** (throws if hostKind is
1044+
sandbox — callers must use the harness path). On `sandbox-created`, the route
1045+
persists `context.sandboxId` on the running execution row for cancel.
1046+
- **Cooperative cancel.** See Gate-3 #26.
1047+
- **Parity of result.** InlineHost and SandboxHost produce the SAME deposit option
1048+
synthesis; only WHERE the pipeline runs differs. Real-sandbox execution is verified
1049+
against deployed sandbox infra; harness plan-building + route dispatch + projection
1050+
are unit-tested with a mocked host.
1051+
1052+
### Gate-3 #26 — Cooperative run cancel (Garrett, 2026-07-08)
1053+
1054+
Deposit (and any agentic) runs are cancelable without killing mid-token LLM streams.
1055+
1056+
**Law**
1057+
1058+
1. **User authority.** A depositor who owns the `executions` row may cancel while
1059+
`status === 'running'`. Terminal rows (`completed` | `failed` | `interrupted` |
1060+
`cancelled`) are not re-cancelled (already-cancelled is idempotent 200).
1061+
2. **Row is authority.** Cancel writes `executions.status = 'cancelled'`,
1062+
`completed_at`, source-safe `error.message`, and `context.cancelledAt` /
1063+
`context.cancelReason`. Background workers MUST NOT overwrite a cancelled row
1064+
with `failed` or `completed`.
1065+
3. **Event.** A source-safe `execution_events` status row (`message`, `cancelled: true`)
1066+
is inserted so the deposit telemetry accordion shows the cancel.
1067+
4. **Cooperative abort.** Background deposit `runSynthesis` polls
1068+
`assertExecutionNotCancelled` between major stages (auth, provision, pipeline,
1069+
validate). Sandbox detached command polls call `shouldAbort` each interval and
1070+
return exit code 130 / outcome `'cancelled'`, then `sandbox.stop()`.
1071+
5. **Sandbox stop.** If `context.sandboxId` is present, cancel best-effort stops the
1072+
box via `Sandbox.get` + `stop` (in addition to harness `finally` stop).
1073+
6. **UI.** While a synthesis is running, Telemetry shows **Cancel run**; on success
1074+
the detail shows a Cancelled badge and analytics fires
1075+
`deposit_synthesis_cancelled` (duration only — no identifiers).
1076+
7. **Not cancel.** Orphan sweep still maps silent stuck rows → `interrupted` (not
1077+
cancelled). Deleting activity anchors is unrelated.
1078+
1079+
**API**
1080+
1081+
- `POST /api/executions/[runId]/cancel` body optional `{ reason?: string }` (≤280 chars)
1082+
- Auth: session user must own the row
1083+
- Responses: 200 cancelled (or already cancelled), 401 unauthenticated, 404 not
1084+
found/not owned, 409 not running
1085+
1086+
**Implementation symbols**
1087+
1088+
- `uapi/lib/execution-cancel.ts``cancelUserExecution`, `assertExecutionNotCancelled`,
1089+
`ExecutionCancelledError`, `isExecutionCancelled`
1090+
- `uapi/app/api/executions/[runId]/cancel/route.ts`
1091+
- Deposit route cooperative checks + sandbox `shouldAbort`
1092+
- `VercelSandboxPipelineHost` `shouldAbort` + `sandbox-cancelled` events
1093+
- `DepositPageClient` Cancel run control
10471094

10481095
### Gate-3 depositing PARITY MATRIX (gate-closure audit; Garrett, 2026-06-27)
10491096

@@ -1077,17 +1124,26 @@ Parity: ✅ specified + implemented + tested · 🟦 specified + implemented as
10771124
| 22 | HostKind selection (configured, not env) | `selectDepositHostKind` / `resolveDepositPipelineHost` | depositSourceProvisioning.test ||
10781125
| 23 | full inventory (sources+samples) + fail-closed exclusions | `provisionDepositSourceInventory` / `applyExclusionsToInventory` | depositSourceProvisioning.test, asset-packs-synthesis.test ||
10791126
| 24 | deposit provisions full checkout via Host | deposit route + `InlineHost` path | depositSynthesizeOptionsRoute.test ||
1080-
| 25 | SandboxHost IN-BOX deposit dispatch (run the pipeline in the box) | harness deposit mode (`synthesizeMode` + steering → in-box runner; `depositOptions` in evidence) + `runDepositInBoxHarness` + route hostKind branch | asset-pack-harness.test, depositSourceProvisioning.test, depositSynthesizeOptionsRoute.test |* |
1127+
| 25 | SandboxHost IN-BOX deposit dispatch (run the pipeline in the box) | harness deposit mode (`synthesizeMode` + steering → in-box runner; `depositOptions` in evidence) + `runDepositInBoxHarness` + route hostKind branch; ephemeral `persistent:false`; OIDC/token auth fail-closed | asset-pack-harness.test, depositSourceProvisioning.test, depositSynthesizeOptionsRoute.test, vercel-sandbox-host.test |* |
1128+
| 26 | Cooperative run cancel | `POST /api/executions/[runId]/cancel` + `execution-cancel.ts` + deposit route polls + sandbox `shouldAbort` + UI Cancel run + `deposit_synthesis_cancelled` analytics | executionCancelRoute.test, vercel-sandbox-host.test (abort), depositPageClient (cancel control) ||
1129+
| 27 | Run configuration locked above telemetry in run detail | `DepositPageClient` config above telemetry; `disabled` when `synthesisRunId` set | depositPageClient.test ||
1130+
| 28 | Formal absolutes fail-closed (no placeholder catalog fallback) | `validateDepositSynthesisOptions` requires `absolutes[]` | agent-measure-absolutes.test, asset-packs-synthesis.test ||
1131+
| 29 | Map-tree LLM token rollup | `sumLlmTokensFromExecutionTree` | asset-packs-synthesis-pipeline.test ||
1132+
| 30 | Full deposit SDIVF integration under test (boundary LLM mock) | `BITCODE_ENABLE_ASSET_PACK_SDIVF_RUNTIME_IN_TEST` + deposit-sdivf-pipeline-integration.test | deposit-sdivf-pipeline-integration.test ||
1133+
| 31 | Deposit implementation agent key (telemetry clarity) | `implementation:deposit-asset-pack-synthesis` | synthesize-asset-packs-phase-rosters.test ||
10811134

10821135
* = wired + unit-tested with a mocked host (harness plan-building, the dispatch, the
10831136
option projection); real in-sandbox execution is verified against deployed sandbox infra.
10841137

10851138
**Open items for full gate-3 depositing closure (deployment + verification, not code):**
10861139
- **Deployment config**: the Vercel sandbox provider's runtime deps (`@vercel/sandbox`,
1087-
`VERCEL_*`, git in the box image); the AWS provider (#20) beyond the stub.
1140+
OIDC or `VERCEL_TOKEN`+team+project, git in the box image); set
1141+
`BITCODE_PIPELINE_HOST=sandbox` on prod for durable in-box runs; the AWS provider
1142+
(#20) beyond the stub.
10881143
- **Live verification**: a real deposit run — on the InlineHost (in-process) and on the
1089-
SandboxHost (in-box) — confirming measured sizes + the decision panel end-to-end and
1090-
that both hosts produce the same deposit option synthesis.
1144+
SandboxHost (in-box) — confirming measured sizes + the decision panel end-to-end,
1145+
that both hosts produce the same deposit option synthesis, and that Cancel run stops
1146+
a live sandbox (Observability → Sandboxes).
10911147

10921148
## Non-goals during V48 opening
10931149

BITCODE_V48_QA.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -635,14 +635,23 @@ cd uapi && pnpm exec jest depositSourceProvisioning depositSynthesizeOptionsRout
635635

636636
**5. #25 SandboxHost in-box dispatch (deployment-conditional)**
637637
- Local default is inline. To exercise the sandbox path: `BITCODE_PIPELINE_HOST=sandbox`
638-
+ `BITCODE_SANDBOX_PROVIDER=vercel` + the sandbox infra (`@vercel/sandbox`, `VERCEL_OIDC_TOKEN`
639-
or `VERCEL_TOKEN`/`VERCEL_TEAM_ID`/`VERCEL_PROJECT_ID`, git in the box image).
638+
+ sandbox auth (`VERCEL_OIDC_TOKEN` via `vercel env pull`, or
639+
`VERCEL_TOKEN`/`VERCEL_TEAM_ID`/`VERCEL_PROJECT_ID`) + `@vercel/sandbox` + git in the box image.
640+
Missing auth must fail closed with a clear message (not a silent hang).
641+
- Deposit boxes are **non-persistent** (`persistent: false` — no snapshot billing).
640642
- Log: `Dispatching deposit synthesis to the sandbox host (in-box) for {repo}@{ref}…` then
641643
`sandbox: sandbox-create-started` / `sandbox-created` / `command-started` / … / `sandbox-stopped`.
642-
- PASS: the box clones + runs the deposit SDIVF in-box; `evidence.depositOptions` returns;
643-
the SAME deposit option synthesis persists as an inline run of the same repo/revision
644-
(modulo run-to-run LLM variance). If the sandbox infra isn't deployed, mark N/A — this is
645-
the single deployment-pending item.
644+
On create, `executions.context.sandboxId` must be set while status is still `running`.
645+
- PASS: the box clones + runs the deposit SDIVF in-box; `evidence.depositOptions` returns with
646+
formal absolutes; the SAME deposit option synthesis persists as an inline run of the same
647+
repo/revision (modulo run-to-run LLM variance). If the sandbox infra isn't deployed, mark N/A.
648+
649+
**5b. #26 Cooperative cancel**
650+
- Start a synthesis (inline or sandbox). While Telemetry shows **Cancel run**, click it.
651+
- PASS: `POST /api/executions/<runId>/cancel` → 200; row `status=cancelled`; UI Cancelled badge;
652+
background does not flip the row to `failed`; sandbox path stops the box (Observability →
653+
Sandboxes, or `sandbox: sandbox-cancelled` / `sandbox-stopped` status lines). Completing a
654+
run that finished before cancel must remain `completed` (409 not_running on cancel).
646655

647656
**6. QA record** (fill per run)
648657

@@ -657,6 +666,7 @@ cd uapi && pnpm exec jest depositSourceProvisioning depositSynthesizeOptionsRout
657666
| Source-safety — no leak | | | |
658667
| Automated baseline green | | | |
659668
| #25 sandbox in-box (or N/A) | | | |
669+
| #26 cancel run | | | |
660670

661671
**Pulling a run's persisted output** (for the COPY targets) — Supabase SQL editor:
662672
```sql

packages/pipeline-hosts/src/__tests__/vercel-sandbox-host.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,53 @@ class DetachedFakeSandbox extends FakeSandbox {
6969
}
7070

7171
describe('VercelSandboxPipelineHost', () => {
72+
it('aborts a detached poll when shouldAbort becomes true and returns cancelled', async () => {
73+
let polls = 0;
74+
class NeverExitSandbox extends DetachedFakeSandbox {
75+
async readFileToBuffer(file: { path: string }): Promise<Buffer | null> {
76+
// Never produce an exit code so the poll continues until shouldAbort.
77+
if (file.path.includes('exit')) return null;
78+
return super.readFileToBuffer(file);
79+
}
80+
}
81+
const fakeSandbox = new NeverExitSandbox();
82+
const host = new VercelSandboxPipelineHost({
83+
sandboxFactory: {
84+
create: async () => fakeSandbox,
85+
},
86+
shouldAbort: async () => {
87+
polls += 1;
88+
return polls >= 2;
89+
},
90+
});
91+
const plan = buildAssetPackSandboxHarness({
92+
read: { id: 'read-1', prompt: 'Read.' },
93+
deposit: { id: 'deposit-1' },
94+
sourceRevision: {
95+
repositoryFullName: 'engineeredsoftware/ENGI',
96+
branch: 'main',
97+
commit: '31bbc0c5227b6b3aed5d107fd8507d35ec22970a',
98+
},
99+
});
100+
plan.commands = [
101+
{
102+
label: 'detached-run',
103+
cmd: 'sh',
104+
args: ['-lc', 'long command'],
105+
detached: true,
106+
exitCodePath: '.bitcode/pipeline-harness/pipeline.exit-code',
107+
stdoutPath: '.bitcode/pipeline-harness/pipeline.stdout.log',
108+
stderrPath: '.bitcode/pipeline-harness/pipeline.stderr.log',
109+
pollIntervalMs: 5,
110+
maxWaitMs: 5_000,
111+
},
112+
];
113+
114+
const result = await host.runHarness(plan);
115+
expect(result.outcome).toBe('cancelled');
116+
expect(fakeSandbox.stopped).toBe(true);
117+
});
118+
72119
it('creates the sandbox, writes harness files, runs commands, reads artifacts, and stops', async () => {
73120
const fakeSandbox = new FakeSandbox();
74121
const createOptions: SandboxCreateOptions[] = [];

packages/pipeline-hosts/src/asset-pack-harness.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ export interface BuildAssetPackSandboxHarnessOptions {
5353
protectedIpExclusions?: string[];
5454
demandContext?: string[];
5555
};
56+
/**
57+
* When false, create a non-persistent (ephemeral) sandbox — no snapshot on stop.
58+
* Deposit one-shot synthesis defaults to false when synthesizeMode is deposit.
59+
*/
60+
persistent?: boolean;
5661
}
5762

5863
export function buildAssetPackSandboxHarness(
@@ -113,6 +118,13 @@ export function buildAssetPackSandboxHarness(
113118
timeout: options.timeoutMs ?? DEFAULT_LONG_TIMEOUT_MS,
114119
networkPolicy: options.networkPolicy ?? 'allow-all',
115120
source: options.source,
121+
// Deposit one-shots are ephemeral; read/QA harness may opt into persistence.
122+
persistent:
123+
typeof options.persistent === 'boolean'
124+
? options.persistent
125+
: options.synthesizeMode === 'deposit'
126+
? false
127+
: undefined,
116128
},
117129
manifest,
118130
files: [

packages/pipeline-hosts/src/types.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,13 @@ export interface SandboxCreateOptions {
169169
teamId?: string;
170170
projectId?: string;
171171
token?: string;
172+
/**
173+
* When false, the sandbox is ephemeral (no snapshot storage on stop).
174+
* Deposit one-shot synthesis should prefer non-persistent boxes.
175+
*/
176+
persistent?: boolean;
177+
/** Optional stable name for Sandbox.get / resume (unique per project). */
178+
name?: string;
172179
}
173180

174181
export interface PipelineHarnessFile {
@@ -247,6 +254,14 @@ export interface SandboxSession {
247254

248255
export interface SandboxFactory {
249256
create(options: SandboxCreateOptions): Promise<SandboxSession>;
257+
/** Optional: resume/stop an existing sandbox by id or name (SDK-dependent). */
258+
get?(options: {
259+
sandboxId?: string;
260+
name?: string;
261+
teamId?: string;
262+
projectId?: string;
263+
token?: string;
264+
}): Promise<SandboxSession>;
250265
}
251266

252267
export type PipelineHarnessHostEvent =
@@ -262,6 +277,12 @@ export type PipelineHarnessHostEvent =
262277
sandboxId?: string;
263278
status?: string;
264279
}
280+
| {
281+
type: 'sandbox-cancelled';
282+
timestamp: string;
283+
sandboxId?: string;
284+
reason?: string;
285+
}
265286
| {
266287
type: 'harness-files-written';
267288
timestamp: string;
@@ -326,6 +347,6 @@ export interface PipelineHarnessRunResult {
326347
evidence: unknown | null;
327348
telemetry: string | null;
328349
};
329-
outcome: 'completed' | 'failed';
350+
outcome: 'completed' | 'failed' | 'cancelled';
330351
stopped: boolean;
331352
}

0 commit comments

Comments
 (0)