Skip to content

Commit 383ec97

Browse files
wip v28
1 parent aa60bf4 commit 383ec97

19 files changed

Lines changed: 861 additions & 25 deletions

BITCODE_SPEC_V28_NOTES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ artifacts, and database readback. Local development authenticates through
128128
OIDC automatically; external hosts need `VERCEL_TOKEN`, `VERCEL_TEAM_ID`, and
129129
`VERCEL_PROJECT_ID`.
130130

131+
The staging API surface for this harness is
132+
`POST /api/pipeline-harness/asset-pack`. It is authenticated by the normal
133+
Supabase session, disabled on production deployments unless
134+
`BITCODE_ENABLE_PIPELINE_HARNESS_API=1`, and returns server-sent events for host
135+
lifecycle, command, artifact, completion, and failure visibility. For private
136+
repository sources it may reuse the authenticated user's GitHub installation
137+
token as clone credentials; the token must never be returned in streamed events.
138+
131139
The minimum commercial telemetry record for a Read/Fit pipeline execution is:
132140

133141
- one run row in `pipeline_runs` or `deliverable_pipeline_runs`;

BITCODE_V28_QA.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,34 @@ BITCODE_PIPELINE_STRUCTURED_DB=1 \
11421142
BITCODE_SANDBOX_ENV_KEYS=SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY,OPENAI_API_KEY
11431143
```
11441144
1145+
On Vercel staging/preview, prefer the deployed streaming trigger because it
1146+
uses Vercel automatic OIDC instead of a local Vercel token:
1147+
1148+
```bash
1149+
curl -N "$BITCODE_UAPI_URL/api/pipeline-harness/asset-pack" \
1150+
-H "Content-Type: application/json" \
1151+
-H "Cookie: $BITCODE_STAGING_SESSION_COOKIE" \
1152+
--data '{
1153+
"repositoryFullName": "engineeredsoftware/ENGI",
1154+
"sourceBranch": "main",
1155+
"sourceCommit": "31bbc0c5227b6b3aed5d107fd8507d35ec22970a",
1156+
"sourceGitUrl": "https://github.com/engineeredsoftware/ENGI.git",
1157+
"readId": "1c4f1f50-ac8b-4d83-8d2d-dc9c96d238b0",
1158+
"depositId": "3f68d845-d910-41ef-835a-89cf0103ac0a",
1159+
"depositAssetId": "asset_repository-revision-deposit-engineeredsoftware-engi_aece31322e",
1160+
"depositHasWalletOrAttestationProof": true,
1161+
"depositHasAssetMeasurementEvidence": true
1162+
}'
1163+
```
1164+
1165+
The route streams `harness-started`, `harness-event`, `harness-completed`, and
1166+
`harness-failed` events. It is authenticated by the normal Supabase session and
1167+
is disabled on production deployments unless
1168+
`BITCODE_ENABLE_PIPELINE_HARNESS_API=1`. Private repository clone credentials
1169+
come from explicit source token env when present, otherwise from the
1170+
authenticated user's GitHub installation token; these credentials must not
1171+
appear in the streamed events.
1172+
11451173
After either harness run:
11461174
11471175
1. Save the command JSON summary, sandbox id, command exit codes, and artifact

packages/pipeline-hosts/README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ leaving enough telemetry for SQL readback.
99

1010
- Firecracker microVM isolation on Amazon Linux 2023.
1111
- Default working directory: `/vercel/sandbox`.
12-
- Default runtime: `node24`; documented runtime options include `node26`,
13-
`node24`, `node22`, and `python3.13`.
12+
- Default runtime: `node24`; documented runtime options are `node24`,
13+
`node22`, and `python3.13`.
1414
- The sandbox user is `vercel-sandbox` and can use `sudo`.
1515
- Filesystem state is ephemeral. Evidence must be exported before stop, then
1616
persisted to Bitcode storage or database projections.
@@ -89,6 +89,29 @@ BITCODE_SANDBOX_DEPOSIT_HAS_MEASUREMENT=1 \
8989
pnpm -C packages/pipeline-hosts run qa:asset-pack:sandbox
9090
```
9191

92+
On a deployed Vercel preview/staging runtime, trigger the same harness through
93+
the authenticated streaming route so the server can use Vercel's automatic
94+
Sandbox OIDC. The route also reuses the authenticated user's GitHub installation
95+
token for private repository clone credentials when no explicit source token is
96+
configured:
97+
98+
```bash
99+
curl -N "$BITCODE_UAPI_URL/api/pipeline-harness/asset-pack" \
100+
-H "Content-Type: application/json" \
101+
-H "Cookie: $BITCODE_STAGING_SESSION_COOKIE" \
102+
--data '{
103+
"repositoryFullName": "engineeredsoftware/ENGI",
104+
"sourceBranch": "main",
105+
"sourceCommit": "31bbc0c5227b6b3aed5d107fd8507d35ec22970a",
106+
"sourceGitUrl": "https://github.com/engineeredsoftware/ENGI.git",
107+
"readId": "1c4f1f50-ac8b-4d83-8d2d-dc9c96d238b0",
108+
"depositId": "3f68d845-d910-41ef-835a-89cf0103ac0a",
109+
"depositAssetId": "asset_repository-revision-deposit-engineeredsoftware-engi_aece31322e",
110+
"depositHasWalletOrAttestationProof": true,
111+
"depositHasAssetMeasurementEvidence": true
112+
}'
113+
```
114+
92115
After the run, execute:
93116

94117
```bash

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { buildAssetPackSandboxHarness } from '../asset-pack-harness';
2+
import ts from 'typescript';
23

34
const baseOptions = {
45
read: {
@@ -35,6 +36,7 @@ describe('asset-pack sandbox harness plan', () => {
3536
evidence: '.bitcode/pipeline-harness/evidence.json',
3637
telemetry: '.bitcode/pipeline-harness/telemetry.jsonl',
3738
});
39+
expect(plan.manifest.expectedEvidenceTables).toContain('deliverable_pipeline_events');
3840
});
3941

4042
it('requires a repository source before planning the real pipeline mode', () => {
@@ -71,4 +73,22 @@ describe('asset-pack sandbox harness plan', () => {
7173
'asset-pack-pipeline-run',
7274
]);
7375
});
76+
77+
it('generates a syntactically valid live pipeline runner', () => {
78+
const plan = buildAssetPackSandboxHarness({
79+
...baseOptions,
80+
mode: 'asset_pack_pipeline',
81+
assumeRepositoryPresent: true,
82+
});
83+
const liveRunner = plan.files.find((file) => file.path.endsWith('run-live-asset-pack-pipeline.ts'));
84+
const diagnostics = ts.transpileModule(liveRunner?.content.toString('utf8') || '', {
85+
compilerOptions: {
86+
module: ts.ModuleKind.CommonJS,
87+
target: ts.ScriptTarget.ES2022,
88+
},
89+
reportDiagnostics: true,
90+
}).diagnostics || [];
91+
92+
expect(diagnostics.filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)).toEqual([]);
93+
});
7494
});

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { buildAssetPackSandboxHarness } from '../asset-pack-harness';
22
import { VercelSandboxPipelineHost } from '../vercel-sandbox-host';
33
import type {
44
PipelineHarnessFile,
5+
PipelineHarnessHostEvent,
56
SandboxCommandResult,
67
SandboxCreateOptions,
78
SandboxFactory,
@@ -80,4 +81,110 @@ describe('VercelSandboxPipelineHost', () => {
8081
expect(result.stopped).toBe(true);
8182
expect(fakeSandbox.stopped).toBe(true);
8283
});
84+
85+
it('emits host lifecycle events for streaming harness observers', async () => {
86+
const fakeSandbox = new FakeSandbox();
87+
const events: PipelineHarnessHostEvent[] = [];
88+
const factory: SandboxFactory = {
89+
create: async () => fakeSandbox,
90+
};
91+
const host = new VercelSandboxPipelineHost({
92+
sandboxFactory: factory,
93+
onEvent: (event) => {
94+
events.push(event);
95+
},
96+
});
97+
const plan = buildAssetPackSandboxHarness({
98+
read: {
99+
id: 'read-1',
100+
prompt: 'Read the deposited repository revision.',
101+
},
102+
deposit: {
103+
id: 'deposit-1',
104+
},
105+
sourceRevision: {
106+
repositoryFullName: 'engineeredsoftware/ENGI',
107+
branch: 'main',
108+
commit: '31bbc0c5227b6b3aed5d107fd8507d35ec22970a',
109+
},
110+
});
111+
112+
await host.runHarness(plan);
113+
114+
expect(events.map((event) => event.type)).toEqual([
115+
'sandbox-create-started',
116+
'sandbox-created',
117+
'harness-files-written',
118+
'command-started',
119+
'command-completed',
120+
'command-started',
121+
'command-completed',
122+
'artifacts-read',
123+
'sandbox-stopped',
124+
]);
125+
expect(events.find((event) => event.type === 'command-completed')).toMatchObject({
126+
stdoutLength: expect.any(Number),
127+
stderrLength: expect.any(Number),
128+
});
129+
});
130+
131+
it('passes access-token auth fields to Sandbox.create when OIDC is unavailable', async () => {
132+
const previous = {
133+
VERCEL_TOKEN: process.env.VERCEL_TOKEN,
134+
VERCEL_TEAM_ID: process.env.VERCEL_TEAM_ID,
135+
VERCEL_PROJECT_ID: process.env.VERCEL_PROJECT_ID,
136+
VERCEL_OIDC_TOKEN: process.env.VERCEL_OIDC_TOKEN,
137+
};
138+
process.env.VERCEL_TOKEN = 'test-token';
139+
process.env.VERCEL_TEAM_ID = 'team_test';
140+
process.env.VERCEL_PROJECT_ID = 'prj_test';
141+
delete process.env.VERCEL_OIDC_TOKEN;
142+
143+
try {
144+
const fakeSandbox = new FakeSandbox();
145+
const createOptions: SandboxCreateOptions[] = [];
146+
const factory: SandboxFactory = {
147+
create: async (options) => {
148+
createOptions.push(options);
149+
return fakeSandbox;
150+
},
151+
};
152+
const host = new VercelSandboxPipelineHost({ sandboxFactory: factory });
153+
const plan = buildAssetPackSandboxHarness({
154+
read: {
155+
id: 'read-1',
156+
prompt: 'Read the deposited repository revision.',
157+
},
158+
deposit: {
159+
id: 'deposit-1',
160+
},
161+
sourceRevision: {
162+
repositoryFullName: 'engineeredsoftware/ENGI',
163+
branch: 'main',
164+
commit: '31bbc0c5227b6b3aed5d107fd8507d35ec22970a',
165+
},
166+
});
167+
168+
await host.runHarness(plan);
169+
170+
expect(createOptions[0]).toMatchObject({
171+
token: 'test-token',
172+
teamId: 'team_test',
173+
projectId: 'prj_test',
174+
});
175+
} finally {
176+
restoreEnv('VERCEL_TOKEN', previous.VERCEL_TOKEN);
177+
restoreEnv('VERCEL_TEAM_ID', previous.VERCEL_TEAM_ID);
178+
restoreEnv('VERCEL_PROJECT_ID', previous.VERCEL_PROJECT_ID);
179+
restoreEnv('VERCEL_OIDC_TOKEN', previous.VERCEL_OIDC_TOKEN);
180+
}
181+
});
83182
});
183+
184+
function restoreEnv(key: string, value: string | undefined): void {
185+
if (value === undefined) {
186+
delete process.env[key];
187+
} else {
188+
process.env[key] = value;
189+
}
190+
}

0 commit comments

Comments
 (0)