Skip to content

Commit fb6a214

Browse files
committed
fix: align the input-cache digest across control plane and runner
The two ends computed the SAME contract differently — the runner joined (storage session, id) with a literal NUL byte, the control plane with a space — so every pushed object landed under a name no lookup could ever produce. Both unit suites passed because each was self-consistent; the route test and the priming test used one side only. A live execution reported 'No such file' for files that had been delivered successfully. Both now use an explicit \u0000 escape (no raw NUL in source, which also made grep treat the file as binary), and both suites assert the same golden vector so the contract cannot silently drift again.
1 parent 51694a2 commit fb6a214

6 files changed

Lines changed: 59 additions & 1 deletion

File tree

api/src/session-inputs.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,24 @@ async function makeBatch(
4040
return tar.stdout;
4141
}
4242

43+
/**
44+
* Cross-component contract. The control plane names batch members with ITS
45+
* implementation of this digest and the runner looks entries up with THIS one;
46+
* nothing else ties them together, so both suites assert the same vector.
47+
*
48+
* They silently diverged once — one side used a literal NUL separator, the
49+
* other a space — and every push landed in the cache under names no lookup
50+
* would ever produce. Both unit suites passed (each self-consistent) and only
51+
* a live execution showed it, as "file not found" for files that had been
52+
* delivered successfully.
53+
*/
54+
const GOLDEN_KEY_SID_1_FILE_1 = 'a995f1e7977466c5636419d21582e0b44420c44d2d7e2660b13aa4d4b4667d90';
55+
4356
describe('pushed input cache', () => {
57+
test('digests match the wire contract the control plane names members with', () => {
58+
expect(inputCacheKey('sid-1', 'file-1')).toBe(GOLDEN_KEY_SID_1_FILE_1);
59+
});
60+
4461
test('lives outside the workspace root so the reaper cannot eat it', async () => {
4562
/* Regression: the cache was originally a dot-directory INSIDE
4663
* SANDBOX_WORKSPACE_ROOT, where the stale-workspace reaper treats every

api/src/session-inputs.ts

5 Bytes
Binary file not shown.

service/src/probecheck.tmp.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { AwsLambdaMicrovmClient } from './runtime-session/lambda-client-aws';
2+
import { probeInputs, pushInputs } from './runtime-session/checkpoint';
3+
import { buildInputBatch } from './runtime-session/files';
4+
import { normalizeMicrovmEndpoint } from './sandbox-backend/lambda-microvm';
5+
6+
const [microvmId, endpoint] = process.argv.slice(2);
7+
const client = new AwsLambdaMicrovmClient({ region: 'us-east-1' });
8+
const cfg = { port: 8080, authTokenTtlSeconds: 300, maxBytes: 512 * 1024 * 1024, timeoutMs: 30_000 };
9+
const mintToken = () => client.createMicrovmAuthToken({ microvmId, port: 8080, ttlSeconds: 300 });
10+
const base = normalizeMicrovmEndpoint(endpoint);
11+
const refs = [{ storage_session_id: 'AP-6YQGnfCXsIbT1F00zs', id: 'V_WL-92AoQdSZWdN85fkt' }];
12+
13+
console.log('probe #1:', JSON.stringify(await probeInputs({ mintToken, endpointBase: base }, refs, cfg)));
14+
const batch = await buildInputBatch(
15+
[{ storage_session_id: 'AP-6YQGnfCXsIbT1F00zs', id: 'V_WL-92AoQdSZWdN85fkt', name: 'probe.csv' }],
16+
{ timeoutMs: 30_000, maxBytes: 1024 * 1024 },
17+
);
18+
console.log('batch bytes:', batch?.data.length);
19+
await pushInputs({ mintToken, endpointBase: base }, batch!.data, cfg);
20+
console.log('push: ok');
21+
console.log('probe #2:', JSON.stringify(await probeInputs({ mintToken, endpointBase: base }, refs, cfg)));

service/src/runtime-session/files.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ async function membersOf(data: Buffer): Promise<string[]> {
5656
}
5757
}
5858

59+
/**
60+
* Cross-component contract — see the twin assertion in the runner's
61+
* session-inputs.test.ts. Members are named here and looked up there, so a
62+
* silent divergence makes every delivery land under names the runner can never
63+
* find. That happened (NUL vs space separator) and only live execution caught
64+
* it; this vector is what makes it a test failure instead.
65+
*/
66+
const GOLDEN_KEY_SID_1_FILE_1 = 'a995f1e7977466c5636419d21582e0b44420c44d2d7e2660b13aa4d4b4667d90';
67+
68+
describe('inputCacheKey', () => {
69+
test('matches the digest the runner resolves cache entries with', () => {
70+
expect(inputCacheKey('sid-1', 'file-1')).toBe(GOLDEN_KEY_SID_1_FILE_1);
71+
});
72+
});
73+
5974
describe('sessionFileRefs', () => {
6075
test('collapses the same object requested under multiple names', () => {
6176
/* Identity is (storage session, id): one delivery, and priming writes it

service/src/runtime-session/files.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export interface SessionFileRef {
4444
/** Mirrors the runner's `inputCacheKey` (api/src/session-inputs.ts): both ends
4545
* ship in the same image, so the digest is a hard-coded contract. */
4646
export function inputCacheKey(storageSessionId: string, id: string): string {
47-
return crypto.createHash('sha256').update(`${storageSessionId} ${id}`, 'utf8').digest('hex');
47+
return crypto.createHash('sha256').update(`${storageSessionId}\u0000${id}`, 'utf8').digest('hex');
4848
}
4949

5050
/** The by-reference subset of the payload's files (inline `content` entries

service/src/sandbox-backend/lambda-microvm.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,11 @@ export class LambdaMicrovmSandboxBackend implements SandboxBackend {
638638
refs.map((ref) => ({ storage_session_id: ref.storage_session_id, id: ref.id })),
639639
this.config.checkpoint,
640640
);
641+
logger.info('Session input delivery', {
642+
microvmId: vm.microvmId,
643+
refs: refs.length,
644+
missing: missing.length,
645+
});
641646
if (missing.length === 0) return;
642647
const wanted = new Set(missing.map((ref) => `${ref.storage_session_id}/${ref.id}`));
643648
const batch = await buildInputBatch(

0 commit comments

Comments
 (0)