deepdebug-local-agent 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/docs/la-protocol-v2.md +70 -16
  2. package/env.example +6 -0
  3. package/package.json +1 -1
  4. package/scripts/sync-la-protocol-v2-contract.mjs +3 -3
  5. package/src/la-protocol-v2/content-handlers/config.js +10 -2
  6. package/src/la-protocol-v2/content-handlers/content-index.js +196 -0
  7. package/src/la-protocol-v2/content-handlers/egress.js +22 -0
  8. package/src/la-protocol-v2/content-handlers/file-list.js +174 -0
  9. package/src/la-protocol-v2/content-handlers/git-diff.js +95 -10
  10. package/src/la-protocol-v2/content-handlers/handler.js +52 -16
  11. package/src/la-protocol-v2/content-handlers/install.js +35 -15
  12. package/src/la-protocol-v2/content-handlers/path-guard.js +120 -10
  13. package/src/la-protocol-v2/content-handlers/ref-registry.js +41 -13
  14. package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +13 -8
  15. package/src/la-protocol-v2/contract/README.md +87 -2
  16. package/src/la-protocol-v2/contract/content.index.schema.json +93 -0
  17. package/src/la-protocol-v2/contract/content.request.schema.json +1 -0
  18. package/src/la-protocol-v2/contract/envelope.schema.json +1 -0
  19. package/src/la-protocol-v2/contract/file.list.body.schema.json +69 -0
  20. package/src/la-protocol-v2/contract/index.json +7 -0
  21. package/src/la-protocol-v2/contract.js +9 -0
  22. package/src/la-protocol-v2/runtime.js +12 -2
  23. package/test/contract/la-protocol-v2/vectors/content.index.vectors.json +3809 -0
  24. package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +215 -0
  25. package/test/contract/la-protocol-v2/vectors/content.response.vectors.json +30 -0
  26. package/test/contract/la-protocol-v2/vectors/file.list.body.vectors.json +1645 -0
  27. package/test/contract/la-protocol-v2/vectors/traces/invalid-content-index-before-ready.json +68 -0
  28. package/test/contract/la-protocol-v2/vectors/traces/valid-content-index.json +161 -0
  29. package/test/la-protocol-v2/content-alignment.test.js +669 -0
  30. package/test/la-protocol-v2/content-deny-list.test.js +138 -0
  31. package/test/la-protocol-v2/content-diff-symlink.test.js +83 -0
  32. package/test/la-protocol-v2/content-file-list.test.js +385 -0
  33. package/test/la-protocol-v2/content-handlers.test.js +55 -61
  34. package/test/la-protocol-v2/content-path-guard.test.js +10 -5
  35. package/test/la-protocol-v2/content-wiring.test.js +39 -14
  36. package/test/la-protocol-v2/contract.test.js +15 -5
  37. package/test/la-protocol-v2/helpers/reference-cp.js +8 -0
  38. package/test/la-protocol-v2/traces.test.js +29 -4
  39. package/tools/interop/LaV2NodeContentInteropTest.java +18 -4
  40. package/tools/interop/LaV2NodeContentRelayE2ETest.java +374 -0
  41. package/tools/interop/LaV2NodeFileListE2ETest.java +369 -0
  42. package/tools/interop/README.md +16 -0
  43. package/tools/interop/la-interop-content-client.mjs +19 -8
  44. package/tools/interop/la-interop-list-client.mjs +93 -0
@@ -27,7 +27,7 @@ async function withRig(opts, body) {
27
27
  // ------------------------------------------------------------------------------------------ file: the happy path
28
28
 
29
29
  test("file: an id minted by the agent is served; the request is acked `accepted` before the answer; content type from the extension", async () => {
30
- await withRig({ dlp: "off" }, async (rig) => {
30
+ await withRig({ dlp: "enforce" }, async (rig) => {
31
31
  write(rig.root(), "docs/readme.md", "# hello\n");
32
32
  write(rig.root(), "src/data.json", '{"a":1}');
33
33
  write(rig.root(), "src/a.txt", "plain");
@@ -43,7 +43,7 @@ test("file: an id minted by the agent is served; the request is acked `accepted`
43
43
  });
44
44
 
45
45
  test("file: 100 KiB goes out in 4 chunks and the CP rebuilds it byte for byte; exactly the size limit is accepted", async () => {
46
- await withRig({ dlp: "off" }, async (rig) => {
46
+ await withRig({ dlp: "enforce" }, async (rig) => {
47
47
  const data = Buffer.from("line of text number x\n".repeat(5000)).subarray(0, 100 * 1024);
48
48
  write(rig.root(), "big.txt", data);
49
49
  const r = await rig.request({ ref: rig.mint("file", "big.txt") });
@@ -59,14 +59,14 @@ test("file: 100 KiB goes out in 4 chunks and the CP rebuilds it byte for byte; e
59
59
  });
60
60
 
61
61
  test("file: unicode names, spaces and a root reached through a symlink work", async () => {
62
- await withRig({ dlp: "off" }, async (rig) => {
62
+ await withRig({ dlp: "enforce" }, async (rig) => {
63
63
  write(rig.root(), "Dir Com Espaço/ficheiro ü ñ 日本.txt", "olá");
64
64
  assert.equal((await rig.request({ ref: rig.mint("file", "Dir Com Espaço/ficheiro ü ñ 日本.txt") })).text, "olá");
65
65
  });
66
66
  });
67
67
 
68
68
  test("file: range is applied to the bytes as served, and past the end is an empty ok", async () => {
69
- await withRig({ dlp: "off" }, async (rig) => {
69
+ await withRig({ dlp: "enforce" }, async (rig) => {
70
70
  write(rig.root(), "r.txt", "0123456789");
71
71
  const ref = rig.mint("file", "r.txt");
72
72
  assert.equal((await rig.request({ ref, range: { offset: 2, length: 3 } })).text, "234");
@@ -79,7 +79,7 @@ test("file: range is applied to the bytes as served, and past the end is an empt
79
79
  // ------------------------------------------------------------------------------------------ refs: opaque ids, nothing is a path
80
80
 
81
81
  test("ref: path-like refs are never interpreted as paths: traversal, absolute, NUL, %2e%2e, unicode tricks -> not_found, nothing echoed", async () => {
82
- await withRig({ dlp: "off" }, async (rig) => {
82
+ await withRig({ dlp: "enforce" }, async (rig) => {
83
83
  const outside = tmpDir(); rig.dirs.push(outside);
84
84
  write(outside, "passwd", "OUTSIDE-CONTENT");
85
85
  write(rig.root(), "src/a.txt", "inside");
@@ -103,7 +103,7 @@ test("ref: path-like refs are never interpreted as paths: traversal, absolute, N
103
103
 
104
104
  test("ref: unknown and expired ids are not_found; an expired id does not come back", async () => {
105
105
  let t = 5_000_000;
106
- await withRig({ dlp: "off", now: () => t, env: { LOCAL_AGENT_CONTENT_REF_TTL_MS: "60000" } }, async (rig) => {
106
+ await withRig({ dlp: "enforce", now: () => t, env: { LOCAL_AGENT_CONTENT_REF_TTL_MS: "60000" } }, async (rig) => {
107
107
  write(rig.root(), "a.txt", "x");
108
108
  const id = rig.mint("file", "a.txt");
109
109
  assert.equal((await rig.request({ ref: id })).status, "ok");
@@ -118,7 +118,7 @@ test("ref: unknown and expired ids are not_found; an expired id does not come ba
118
118
  });
119
119
 
120
120
  test("ref: an id of workspace A does not open anything through workspace B (both registered), nor through an unregistered one", async () => {
121
- await withRig({ dlp: "off" }, async (rig) => {
121
+ await withRig({ dlp: "enforce" }, async (rig) => {
122
122
  write(rig.root("ws-1"), "a.txt", "A-CONTENT");
123
123
  write(rig.root("ws-2"), "a.txt", "B-CONTENT");
124
124
  const idA = rig.mint("file", "a.txt", "ws-1");
@@ -134,7 +134,7 @@ test("ref: an id of workspace A does not open anything through workspace B (both
134
134
 
135
135
  test("ref: a workspace that belongs to another tenant is not served, even with a valid id", async () => {
136
136
  const rootA = tmpDir(); const rootB = tmpDir();
137
- await withRig({ dlp: "off", workspaces: { "ws-1": { root: rootA }, "ws-foreign": { root: rootB, tenantId: "9d1f6a2c-0000-4000-8000-000000000000" } } }, async (rig) => {
137
+ await withRig({ dlp: "enforce", workspaces: { "ws-1": { root: rootA }, "ws-foreign": { root: rootB, tenantId: "9d1f6a2c-0000-4000-8000-000000000000" } } }, async (rig) => {
138
138
  write(rootA, "a.txt", "mine"); write(rootB, "a.txt", "FOREIGN");
139
139
  const id = rig.registry.mint({ workspaceId: "ws-foreign", root: rootB, kind: "file", rel: "a.txt" }).id;
140
140
  assert.equal((await rig.request({ ref: id, workspaceId: "ws-foreign" })).status, "not_found");
@@ -144,7 +144,7 @@ test("ref: a workspace that belongs to another tenant is not served, even with a
144
144
  });
145
145
 
146
146
  test("ref: a frame for another tenant never reaches the handler (the session refuses it as tenant_mismatch)", async () => {
147
- await withRig({ dlp: "off" }, async (rig) => {
147
+ await withRig({ dlp: "enforce" }, async (rig) => {
148
148
  write(rig.root(), "a.txt", "x");
149
149
  const f = rig.cp.frameWith("content.request", { kind: "file", ref: rig.mint("file", "a.txt"), actor: { userId: "u-1", role: "member" } }, { requestId: uuid() });
150
150
  f.tenantId = "9d1f6a2c-0000-4000-8000-000000000000";
@@ -156,7 +156,7 @@ test("ref: a frame for another tenant never reaches the handler (the session ref
156
156
  });
157
157
 
158
158
  test("ref: a workspace re-opened on another directory invalidates the ids minted for the old root", async () => {
159
- await withRig({ dlp: "off" }, async (rig) => {
159
+ await withRig({ dlp: "enforce" }, async (rig) => {
160
160
  write(rig.root(), "a.txt", "old root");
161
161
  const id = rig.mint("file", "a.txt");
162
162
  const other = tmpDir(); rig.dirs.push(other);
@@ -171,7 +171,7 @@ test("ref: a workspace re-opened on another directory invalidates the ids minted
171
171
  // ------------------------------------------------------------------------------------------ path checks at read time
172
172
 
173
173
  test("file: sensitive files are forbidden by name (an id for them cannot be minted) and by link (a harmless name pointing to .env)", async () => {
174
- await withRig({ dlp: "off" }, async (rig) => {
174
+ await withRig({ dlp: "enforce" }, async (rig) => {
175
175
  write(rig.root(), ".env", "DB_PASSWORD=hunter2hunter2");
176
176
  write(rig.root(), "certs/server.pem", "PEMDATA");
177
177
  write(rig.root(), ".git/config", "[remote]\n\turl = https://user:tok@host/r.git");
@@ -189,7 +189,7 @@ test("file: sensitive files are forbidden by name (an id for them cannot be mint
189
189
  });
190
190
 
191
191
  test("file: the deny list is configurable (LOCAL_AGENT_CONTENT_DENY_GLOBS) and applied at read time too", async () => {
192
- await withRig({ dlp: "off", env: { LOCAL_AGENT_CONTENT_DENY_GLOBS: "private-notes/, *.hush" } }, async (rig) => {
192
+ await withRig({ dlp: "enforce", env: { LOCAL_AGENT_CONTENT_DENY_GLOBS: "private-notes/, *.hush" } }, async (rig) => {
193
193
  write(rig.root(), "private-notes/n.txt", "n"); write(rig.root(), "a.hush", "h");
194
194
  assert.equal(rig.mint("file", "private-notes/n.txt"), null);
195
195
  assert.equal(rig.mint("file", "a.hush"), null);
@@ -202,7 +202,7 @@ test("file: the deny list is configurable (LOCAL_AGENT_CONTENT_DENY_GLOBS) and a
202
202
  });
203
203
 
204
204
  test("file: a symlink that leaves the workspace (swapped in after the id was minted) is forbidden and its target is never read", async () => {
205
- await withRig({ dlp: "off" }, async (rig) => {
205
+ await withRig({ dlp: "enforce" }, async (rig) => {
206
206
  const outside = tmpDir(); rig.dirs.push(outside);
207
207
  write(outside, "secret.txt", "OUTSIDE-SECRET-CONTENT");
208
208
  write(outside, "d/x.txt", "OUTSIDE-DIR-CONTENT");
@@ -221,7 +221,7 @@ test("file: a symlink that leaves the workspace (swapped in after the id was min
221
221
  });
222
222
 
223
223
  test("file: missing file, a directory and an id minted for a path that never existed are not_found", async () => {
224
- await withRig({ dlp: "off" }, async (rig) => {
224
+ await withRig({ dlp: "enforce" }, async (rig) => {
225
225
  write(rig.root(), "d/x.txt", "x");
226
226
  assert.equal((await rig.request({ ref: rig.mint("file", "nope.txt") })).status, "not_found");
227
227
  assert.equal((await rig.request({ ref: rig.mint("file", "d") })).status, "not_found");
@@ -229,7 +229,7 @@ test("file: missing file, a directory and an id minted for a path that never exi
229
229
  });
230
230
 
231
231
  test("file: a file above the limit is payload_too_large WITHOUT being read (3 GiB sparse file answered at once)", async () => {
232
- await withRig({ dlp: "off" }, async (rig) => {
232
+ await withRig({ dlp: "enforce" }, async (rig) => {
233
233
  write(rig.root(), "huge.bin", "");
234
234
  fs.truncateSync(path.join(rig.root(), "huge.bin"), 3 * 1024 ** 3);
235
235
  const t0 = Date.now();
@@ -238,7 +238,7 @@ test("file: a file above the limit is payload_too_large WITHOUT being read (3 Gi
238
238
  assert.ok(Date.now() - t0 < 1500);
239
239
  write(rig.root(), "over.txt", Buffer.alloc(rig.config.maxBytes + 1, 0x61));
240
240
  assert.equal((await rig.request({ ref: rig.mint("file", "over.txt") })).errorCode, "payload_too_large");
241
- const small = await setup({ dlp: "off", env: { LOCAL_AGENT_CONTENT_MAX_BYTES: "4096" } });
241
+ const small = await setup({ dlp: "enforce", env: { LOCAL_AGENT_CONTENT_MAX_BYTES: "4096" } });
242
242
  try {
243
243
  write(small.root(), "f.txt", Buffer.alloc(4097, 0x61));
244
244
  assert.equal((await small.request({ ref: small.mint("file", "f.txt") })).errorCode, "payload_too_large");
@@ -248,19 +248,16 @@ test("file: a file above the limit is payload_too_large WITHOUT being read (3 Gi
248
248
 
249
249
  // ------------------------------------------------------------------------------------------ binary
250
250
 
251
- test("binary: with the DLP off or warn it is served as octet-stream; in enforce it cannot be inspected and is withheld", async () => {
251
+ test("binary: in enforce it cannot be inspected and is withheld (forbidden); invalid UTF-8 counts as binary", async () => {
252
252
  const bin = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0, 0, 0, 1]), crypto.randomBytes(5000)]);
253
- for (const [dlp, want] of [["off", "ok"], ["warn", "ok"], ["enforce", "forbidden"]]) {
254
- await withRig({ dlp }, async (rig) => {
255
- write(rig.root(), "logo.png", bin);
256
- write(rig.root(), "latin1.txt", Buffer.from([0x63, 0x61, 0x66, 0xe9, 0xff, 0xfe]));
257
- const r = await rig.request({ ref: rig.mint("file", "logo.png") });
258
- assert.equal(r.status, want, dlp);
259
- if (want === "ok") { assert.equal(r.contentType, "application/octet-stream"); assert.ok(r.body.equals(bin)); } else assert.equal(r.body.length, 0);
260
- const l = await rig.request({ ref: rig.mint("file", "latin1.txt") });
261
- assert.equal(l.status, want, "invalid UTF-8 is binary for the DLP");
262
- });
263
- }
253
+ await withRig({ dlp: "enforce" }, async (rig) => {
254
+ write(rig.root(), "logo.png", bin);
255
+ write(rig.root(), "latin1.txt", Buffer.from([0x63, 0x61, 0x66, 0xe9, 0xff, 0xfe]));
256
+ const r = await rig.request({ ref: rig.mint("file", "logo.png") });
257
+ assert.equal(r.status, "forbidden");
258
+ assert.equal(r.body.length, 0);
259
+ assert.equal((await rig.request({ ref: rig.mint("file", "latin1.txt") })).status, "forbidden");
260
+ });
264
261
  });
265
262
 
266
263
  // ------------------------------------------------------------------------------------------ DLP
@@ -303,23 +300,17 @@ test("DLP enforce: a secret that would straddle two chunks is found (the whole t
303
300
  });
304
301
  });
305
302
 
306
- test("DLP warn: content goes as read (the existing meaning of warn) and the hits are only counted in the local audit", async () => {
307
- await withRig({ dlp: "warn" }, async (rig) => {
308
- write(rig.root(), "c.txt", `k=${GH}\n`);
309
- const r = await rig.request({ ref: rig.mint("file", "c.txt") });
310
- assert.equal(r.text, `k=${GH}\n`);
311
- const rec = rig.handler.audit.tail(1)[0];
312
- assert.deepEqual([rec.dlpMode, rec.dlpHits], ["warn", 1]);
313
- assert.deepEqual(rec.dlpRules, { github_token: 1 });
314
- });
315
- });
316
-
317
- test("DLP off: the content goes as read; the agent warns once at install time (see the install test), the audit says dlpMode off", async () => {
318
- await withRig({ dlp: "off" }, async (rig) => {
319
- write(rig.root(), "c.txt", `k=${GH}\n`);
320
- assert.equal((await rig.request({ ref: rig.mint("file", "c.txt") })).text, `k=${GH}\n`);
321
- assert.equal(rig.handler.audit.tail(1)[0].dlpMode, "off");
322
- });
303
+ test("DLP warn and off (F3-07b): no content is served at all, the answer is protocol_unsupported and nothing was read", async () => {
304
+ for (const dlp of ["warn", "off"]) {
305
+ await withRig({ dlp }, async (rig) => {
306
+ write(rig.root(), "c.txt", `k=${GH}\n`);
307
+ const r = await rig.request({ ref: rig.mint("file", "c.txt") });
308
+ assert.deepEqual([r.status, r.errorCode, r.body.length], ["error", "protocol_unsupported", 0], dlp);
309
+ assert.equal(r.text.includes(GH), false);
310
+ const rec = rig.handler.audit.tail(1)[0];
311
+ assert.deepEqual([rec.status, rec.errorCode, rec.bytes], ["error", "protocol_unsupported", 0]);
312
+ });
313
+ }
323
314
  });
324
315
 
325
316
  test("DLP enforce: a hit in LOCAL_AGENT_DLP_BLOCK_CLASSES withholds the whole file (forbidden, no body)", async () => {
@@ -351,13 +342,16 @@ test("DLP enforce: masking that makes the text grow past the limit is payload_to
351
342
  });
352
343
  });
353
344
 
354
- test("DLP: the mode is read from LOCAL_AGENT_DLP_MODE at each request, like the rest of the agent", async () => {
355
- await withRig({ dlp: "off" }, async (rig) => {
345
+ test("DLP: the mode is read from LOCAL_AGENT_DLP_MODE at each request; leaving enforce stops serving at once (no opt-out), going back resumes", async () => {
346
+ await withRig({ dlp: "enforce" }, async (rig) => {
356
347
  write(rig.root(), "c.txt", `k=${GH}\n`);
357
348
  const id = rig.mint("file", "c.txt");
358
- assert.equal((await rig.request({ ref: id })).text.includes(GH), true);
359
- rig.env.LOCAL_AGENT_DLP_MODE = "enforce";
360
349
  assert.equal((await rig.request({ ref: id })).text.includes(GH), false);
350
+ rig.env.LOCAL_AGENT_DLP_MODE = "off";
351
+ const off = await rig.request({ ref: id });
352
+ assert.deepEqual([off.status, off.errorCode, off.text], ["error", "protocol_unsupported", ""]);
353
+ rig.env.LOCAL_AGENT_DLP_MODE = "enforce";
354
+ assert.equal((await rig.request({ ref: id })).status, "ok");
361
355
  });
362
356
  });
363
357
 
@@ -388,7 +382,7 @@ test("every other kind of the allowlist answers `error` + protocol_unsupported (
388
382
  });
389
383
 
390
384
  test("an id minted for a file is not valid as a diff, and the other way round", async () => {
391
- await withRig({ dlp: "off" }, async (rig) => {
385
+ await withRig({ dlp: "enforce" }, async (rig) => {
392
386
  write(rig.root(), "a.txt", "x");
393
387
  assert.equal((await rig.request({ kind: "diff", ref: rig.mint("file", "a.txt") })).status, "not_found");
394
388
  assert.equal((await rig.request({ kind: "file", ref: rig.mint("diff", null) })).status, "not_found");
@@ -433,7 +427,7 @@ test("diff: `worktree` is the work tree against HEAD; sensitive files are left o
433
427
  });
434
428
 
435
429
  test("diff: a minted id can ask for one file; a sensitive path cannot be minted; an unchanged file is an empty ok", async () => {
436
- await withRig({ dlp: "off" }, async (rig) => {
430
+ await withRig({ dlp: "enforce" }, async (rig) => {
437
431
  repoWithChanges(rig.root());
438
432
  const one = await rig.request({ kind: "diff", ref: rig.mint("diff", "src/app.js") });
439
433
  assert.equal(one.status, "ok");
@@ -448,7 +442,7 @@ test("diff: a minted id can ask for one file; a sensitive path cannot be minted;
448
442
  });
449
443
 
450
444
  test("diff: not a repository, or a repository without commits -> not_found; another workspace's id does not diff this one", async () => {
451
- await withRig({ dlp: "off" }, async (rig) => {
445
+ await withRig({ dlp: "enforce" }, async (rig) => {
452
446
  write(rig.root("ws-1"), "a.txt", "x");
453
447
  assert.equal((await rig.request({ kind: "diff", ref: "worktree" })).status, "not_found");
454
448
  git(rig.root("ws-1"), "init", "-q", "-b", "main");
@@ -461,7 +455,7 @@ test("diff: not a repository, or a repository without commits -> not_found; anot
461
455
  });
462
456
 
463
457
  test("diff: a workspace inside a bigger repository only shows its own subtree", async () => {
464
- await withRig({ dlp: "off" }, async (rig) => {
458
+ await withRig({ dlp: "enforce" }, async (rig) => {
465
459
  const top = rig.root();
466
460
  write(top, "pkg/in.txt", "1\n"); write(top, "outside.txt", "1\n");
467
461
  git(top, "init", "-q", "-b", "main"); git(top, "add", "-A"); git(top, "commit", "-q", "-m", "i");
@@ -475,7 +469,7 @@ test("diff: a workspace inside a bigger repository only shows its own subtree",
475
469
  });
476
470
 
477
471
  test("diff: repository config cannot run anything (fsmonitor, external diff, textconv) and GIT_DIR in the agent's env is ignored", async () => {
478
- await withRig({ dlp: "off" }, async (rig) => {
472
+ await withRig({ dlp: "enforce" }, async (rig) => {
479
473
  const root = rig.root();
480
474
  repoWithChanges(root);
481
475
  const marker = path.join(tmpDir(), "PWNED"); rig.dirs.push(path.dirname(marker));
@@ -498,7 +492,7 @@ test("diff: repository config cannot run anything (fsmonitor, external diff, tex
498
492
  });
499
493
 
500
494
  test("diff: above the size limit is payload_too_large (git is stopped while it streams)", async () => {
501
- await withRig({ dlp: "off", env: { LOCAL_AGENT_CONTENT_MAX_BYTES: "20000" } }, async (rig) => {
495
+ await withRig({ dlp: "enforce", env: { LOCAL_AGENT_CONTENT_MAX_BYTES: "20000" } }, async (rig) => {
502
496
  write(rig.root(), "big.txt", "a\n");
503
497
  git(rig.root(), "init", "-q", "-b", "main"); git(rig.root(), "add", "-A"); git(rig.root(), "commit", "-q", "-m", "i");
504
498
  write(rig.root(), "big.txt", "change number x\n".repeat(5000));
@@ -510,7 +504,7 @@ test("diff: above the size limit is payload_too_large (git is stopped while it s
510
504
  // ------------------------------------------------------------------------------------------ cancellation, limits, backpressure
511
505
 
512
506
  test("cancel in the middle of a stream: no further chunk is read or sent, the slot and the memory come back, the audit says cancelled", async () => {
513
- await withRig({ dlp: "off" }, async (rig) => {
507
+ await withRig({ dlp: "enforce" }, async (rig) => {
514
508
  write(rig.root(), "big.txt", Buffer.alloc(rig.config.maxBytes, 0x61));
515
509
  const requestId = uuid();
516
510
  rig.cp.silent = true; // the CP never acks: 8 chunks fit the window, the rest waits (backpressure)
@@ -534,7 +528,7 @@ test("cancel in the middle of a stream: no further chunk is read or sent, the sl
534
528
  });
535
529
 
536
530
  test("the request deadline (LOCAL_AGENT_CONTENT_TOTAL_MS) stops a stream the CP does not consume", async () => {
537
- await withRig({ dlp: "off", env: { LOCAL_AGENT_CONTENT_TOTAL_MS: "1000" } }, async (rig) => {
531
+ await withRig({ dlp: "enforce", env: { LOCAL_AGENT_CONTENT_TOTAL_MS: "1000" } }, async (rig) => {
538
532
  write(rig.root(), "big.txt", Buffer.alloc(rig.config.maxBytes, 0x61));
539
533
  rig.cp.silent = true;
540
534
  const requestId = uuid();
@@ -548,7 +542,7 @@ test("the request deadline (LOCAL_AGENT_CONTENT_TOTAL_MS) stops a stream the CP
548
542
  });
549
543
 
550
544
  test("backpressure: with a slow consumer the window of the session holds (at most 8 chunks unacked) and the file still arrives intact", async () => {
551
- await withRig({ dlp: "off", cp: {} }, async (rig) => {
545
+ await withRig({ dlp: "enforce", cp: {} }, async (rig) => {
552
546
  const data = crypto.randomBytes(Math.floor(rig.config.maxBytes / 2)).toString("base64").slice(0, Math.floor(rig.config.maxBytes / 2));
553
547
  write(rig.root(), "big.txt", data);
554
548
  rig.cp.ackDelayMs = 20;
@@ -563,7 +557,7 @@ test("backpressure: with a slow consumer the window of the session holds (at mos
563
557
  });
564
558
 
565
559
  test("limits: above LOCAL_AGENT_CONTENT_MAX_CONCURRENT the extra request gets rate_limited (a code, no content); the first one is not disturbed", async () => {
566
- await withRig({ dlp: "off", env: { LOCAL_AGENT_CONTENT_MAX_CONCURRENT: "1", LOCAL_AGENT_CONTENT_MAX_INFLIGHT_BYTES: String(64 * 1024 * 1024) } }, async (rig) => {
560
+ await withRig({ dlp: "enforce", env: { LOCAL_AGENT_CONTENT_MAX_CONCURRENT: "1", LOCAL_AGENT_CONTENT_MAX_INFLIGHT_BYTES: String(64 * 1024 * 1024) } }, async (rig) => {
567
561
  write(rig.root(), "big.txt", Buffer.alloc(rig.config.maxBytes, 0x61));
568
562
  write(rig.root(), "small.txt", "s");
569
563
  rig.cp.ackDelayMs = 40;
@@ -581,7 +575,7 @@ test("limits: above LOCAL_AGENT_CONTENT_MAX_CONCURRENT the extra request gets ra
581
575
  });
582
576
 
583
577
  test("limits: the memory reservation (LOCAL_AGENT_CONTENT_MAX_INFLIGHT_BYTES) is enforced too", async () => {
584
- await withRig({ dlp: "off", env: { LOCAL_AGENT_CONTENT_MAX_BYTES: "4096", LOCAL_AGENT_CONTENT_MAX_INFLIGHT_BYTES: "16384", LOCAL_AGENT_CONTENT_MAX_CONCURRENT: "8" } }, async (rig) => {
578
+ await withRig({ dlp: "enforce", env: { LOCAL_AGENT_CONTENT_MAX_BYTES: "4096", LOCAL_AGENT_CONTENT_MAX_INFLIGHT_BYTES: "16384", LOCAL_AGENT_CONTENT_MAX_CONCURRENT: "8" } }, async (rig) => {
585
579
  assert.equal(rig.config.maxInFlightBytes, 16384); // each request reserves 2 x 4096: two fit
586
580
  write(rig.root(), "a.txt", "a".repeat(4096));
587
581
  rig.cp.ackDelayMs = 100;
@@ -625,7 +619,7 @@ test("nothing but codes in errors, audit and logs: no path, no ref, no content,
625
619
  });
626
620
 
627
621
  test("a request without requestId is refused with a rejected ack, not left hanging", async () => {
628
- await withRig({ dlp: "off" }, async (rig) => {
622
+ await withRig({ dlp: "enforce" }, async (rig) => {
629
623
  const before = rig.handler.stats.received;
630
624
  assert.deepEqual(rig.handler.handle({ params: { kind: "file", ref: "x" }, workspaceId: "ws-1" }), { reject: "schema_violation" });
631
625
  assert.equal(rig.handler.stats.received, before + 1);
@@ -41,7 +41,7 @@ test("deny list: secrets by name, keys, credentials, VCS internals (case-insensi
41
41
  });
42
42
 
43
43
  test("deny list: ordinary files are not caught (no false positives on look-alikes)", () => {
44
- for (const p of ["src/environment.js", "README.md", "src/keyboard.js", "src/monkey.js", "docs/pem-format.md", "environment/readme.txt", "src/secretary.js", "gitignore.txt", "src/.github/workflows/ci.yml", "src/auth/credentials.js"]) {
44
+ for (const p of ["src/environment.js", "README.md", "src/keyboard.js", "src/monkey.js", "docs/pem-format.md", "environment/readme.txt", "src/secretary.js", "gitignore.txt", "src/.github/workflows/ci.yml", "src/auth/credential-store.js", "src/kubernetes.js", "docs/kube-notes.md"]) {
45
45
  assert.equal(isSensitivePath(p), false, p);
46
46
  }
47
47
  });
@@ -156,12 +156,17 @@ test("readRegular: size cap before reading, exact bytes, the file swapped for a
156
156
 
157
157
  // ------------------------------------------------------------------------------------------ ids
158
158
 
159
- test("ids: 128 random bits, not derived from the path, a fresh id per mint", () => {
159
+ test("ids (F3-07b): 43 base64url chars, no prefix, 256 random bits, not derived from the path, a fresh id per mint; the F3-07 form cr1_+22 is gone", () => {
160
160
  const reg = new ContentRefRegistry({ tenantId: "t1" });
161
161
  const a = reg.mint({ workspaceId: "w", root: "/r", kind: "file", rel: "src/a.txt" });
162
162
  const b = reg.mint({ workspaceId: "w", root: "/r", kind: "file", rel: "src/a.txt" });
163
163
  assert.match(a.id, REF_PATTERN);
164
- assert.equal(a.id.length, 4 + 22);
164
+ assert.match(a.id, /^[A-Za-z0-9_-]{43}$/);
165
+ assert.equal(a.id.length, 43);
166
+ assert.equal(a.id.startsWith("cr1_"), false);
167
+ assert.equal(Buffer.from(a.id, "base64url").length, 32, "32 random bytes");
168
+ assert.equal(reg.resolve("cr1_" + "A".repeat(22), { workspaceId: "w", root: "/r", kind: "file" }), null);
169
+ assert.equal(REF_PATTERN.test("worktree"), false, "the symbolic ref is not an id");
165
170
  assert.notEqual(a.id, b.id);
166
171
  assert.equal(a.id.includes("a.txt") || a.id.includes("src"), false);
167
172
  const ids = new Set(Array.from({ length: 2000 }, () => reg.mint({ workspaceId: "w", root: "/r", kind: "file", rel: "x" }).id));
@@ -171,13 +176,13 @@ test("ids: 128 random bits, not derived from the path, a fresh id per mint", ()
171
176
  test("ids: resolve only for the same tenant, workspace, root and kind; anything else is null (indistinguishable from unknown)", () => {
172
177
  const reg = new ContentRefRegistry({ tenantId: "t1" });
173
178
  const { id } = reg.mint({ workspaceId: "w1", root: "/r1", kind: "file", rel: "src/a.txt" });
174
- assert.deepEqual(reg.resolve(id, { workspaceId: "w1", root: "/r1", kind: "file" }), { rel: "src/a.txt" });
179
+ assert.deepEqual(reg.resolve(id, { workspaceId: "w1", root: "/r1", kind: "file" }), { rel: "src/a.txt", after: null });
175
180
  assert.equal(reg.resolve(id, { workspaceId: "w2", root: "/r1", kind: "file" }), null, "other workspace");
176
181
  assert.equal(reg.resolve(id, { workspaceId: "w1", root: "/r2", kind: "file" }), null, "same workspace id, re-opened on another root");
177
182
  assert.equal(reg.resolve(id, { workspaceId: "w1", root: "/r1", kind: "diff" }), null, "other kind");
178
183
  const other = new ContentRefRegistry({ tenantId: "t2" });
179
184
  assert.equal(other.resolve(id, { workspaceId: "w1", root: "/r1", kind: "file" }), null, "other tenant's registry");
180
- for (const junk of ["", "x", "cr1_", "cr1_short", `${id}0`, `${id.slice(0, -1)}!`, "../etc/passwd", "/etc/passwd", "src/a.txt", "%2e%2e", "a\u0000b", 42, null, undefined, {}]) {
185
+ for (const junk of ["", "x", "cr1_", "cr1_short", "worktree", "cr1_" + "A".repeat(22), `${id}0`, `${id.slice(0, -1)}!`, "../etc/passwd", "/etc/passwd", "src/a.txt", "%2e%2e", "a\u0000b", 42, null, undefined, {}]) {
181
186
  assert.equal(reg.resolve(junk, { workspaceId: "w1", root: "/r1", kind: "file" }), null);
182
187
  }
183
188
  });
@@ -40,9 +40,9 @@ test("config: every number is clamped to a sane range; a bad value falls back to
40
40
 
41
41
  // ------------------------------------------------------------------------------------------ in process (runtime.js)
42
42
 
43
- async function linkFor(cp, { env, root, tenantWs } = {}) {
43
+ async function linkFor(cp, { env, root, tenantWs, errors = [] } = {}) {
44
44
  const logs = [];
45
- const log = { info: (m) => logs.push(m), log: (m) => logs.push(m), warn: (m) => logs.push(m) };
45
+ const log = { info: (m) => logs.push(m), log: (m) => logs.push(m), warn: (m) => logs.push(m), error: (m) => errors.push(m) };
46
46
  const wsManager = { list: () => [{ id: "ws-1", root, tenantId: tenantWs ?? TENANT }] };
47
47
  const link = await buildV2Link({ tenantId: TENANT, agentId: AGENT, signing: signingFor({ keyId: cp.keyId, publicKeyRaw: cp.publicKeyRaw }, "enforce"), wsManager,
48
48
  getWorkspaceRoot: () => root, log, env });
@@ -60,7 +60,7 @@ test("flag off (unset or any non-true value): content.request is acked `rejected
60
60
  const { link } = await linkFor(cp, { root, env: { LOCAL_AGENT_CONTENT_HANDLERS: v } });
61
61
  assert.equal(link.content, undefined);
62
62
  assert.deepEqual(cp.registers[0].capabilities, ["chunk.base64", "protocol.v2", "v1.commands"]);
63
- cp.sendData("content.request", { kind: "file", ref: "cr1_" + "A".repeat(22), actor: ACTOR }, { workspaceId: "ws-1", requestId: uuid() });
63
+ cp.sendData("content.request", { kind: "file", ref: "A".repeat(43), actor: ACTOR }, { workspaceId: "ws-1", requestId: uuid() });
64
64
  await waitFor(() => cp.acksFromLa.length > 0);
65
65
  assert.deepEqual(cp.acksFromLa[0], { ackSeq: 1, status: "rejected", errorCode: "protocol_unsupported" });
66
66
  assert.equal(cp.received.filter((f) => f.type === "content.response").length, 0);
@@ -69,14 +69,14 @@ test("flag off (unset or any non-true value): content.request is acked `rejected
69
69
  }
70
70
  });
71
71
 
72
- test("flag on: the handler is installed, the register announces content.file and content.diff, mintRef gives the hook for future inventories", async () => {
72
+ test("flag on: the handler is installed, the register announces content.file, content.file.list and content.diff, mintRef is the hook the listing mints through", async () => {
73
73
  const root = tmpDir(); write(root, "src/a.txt", "hello"); write(root, ".env", "X=1");
74
74
  const cp = new ReferenceCp();
75
75
  const { link, logs } = await linkFor(cp, { root, env: { LOCAL_AGENT_CONTENT_HANDLERS: "true", LOCAL_AGENT_DLP_MODE: "enforce" } });
76
- assert.deepEqual(cp.registers[0].capabilities, ["chunk.base64", "content.diff", "content.file", "protocol.v2", "v1.commands"]);
76
+ assert.deepEqual(cp.registers[0].capabilities, ["chunk.base64", "content.diff", "content.file", "content.file.list", "protocol.v2", "v1.commands"]);
77
77
  assert.equal(logs.some((l) => l.includes("not enforce")), false, "enforce: no warning");
78
78
  const m = link.content.mintRef({ workspaceId: "ws-1", kind: "file", rel: "src/a.txt" });
79
- assert.match(m.id, /^cr1_[A-Za-z0-9_-]{22}$/);
79
+ assert.match(m.id, /^[A-Za-z0-9_-]{43}$/);
80
80
  assert.equal(link.content.mintRef({ workspaceId: "ws-1", kind: "file", rel: ".env" }), null);
81
81
  assert.equal(link.content.mintRef({ workspaceId: "ws-unknown", kind: "file", rel: "src/a.txt" }), null);
82
82
  const requestId = uuid();
@@ -88,13 +88,38 @@ test("flag on: the handler is installed, the register announces content.file and
88
88
  fs.rmSync(root, { recursive: true, force: true });
89
89
  });
90
90
 
91
- test("flag on with the DLP not in enforce: the agent says so at install time (the operator chose, but it is visible)", async () => {
92
- const root = tmpDir();
93
- const cp = new ReferenceCp();
94
- const { link, logs } = await linkFor(cp, { root, env: { LOCAL_AGENT_CONTENT_HANDLERS: "true" } });
95
- assert.ok(logs.some((l) => l.includes("LOCAL_AGENT_DLP_MODE is not enforce")));
96
- link.close();
97
- fs.rmSync(root, { recursive: true, force: true });
91
+ test("flag on x DLP mode (F3-07b): only enforce registers the handlers; off, warn, unset and unknown values do not (ERROR at startup, no content capability, protocol_unsupported)", async () => {
92
+ for (const mode of [undefined, "off", "warn", "enforce-ish", "ENFORCE"]) {
93
+ const root = tmpDir(); write(root, "a.txt", "x");
94
+ const cp = new ReferenceCp();
95
+ const errors = [];
96
+ const env = { LOCAL_AGENT_CONTENT_HANDLERS: "true", ...(mode === undefined ? {} : { LOCAL_AGENT_DLP_MODE: mode }) };
97
+ const { link, logs } = await linkFor(cp, { root, env, errors });
98
+ const enforce = mode === "ENFORCE"; // normalizeMode of the DLP module is case-insensitive: the agent and the DLP agree on what enforce means
99
+ assert.equal(!!link.content, enforce, String(mode));
100
+ assert.equal(cp.registers.at(-1).capabilities.includes("content.file"), enforce, String(mode));
101
+ assert.equal(cp.registers.at(-1).capabilities.includes("content.diff"), enforce, String(mode));
102
+ if (!enforce) {
103
+ assert.ok(errors.some((l) => l.includes("ERROR") && l.includes("LOCAL_AGENT_DLP_MODE") && l.includes("NOT registered")), String(mode));
104
+ const requestId = uuid();
105
+ cp.sendData("content.request", { kind: "diff", ref: "worktree", actor: ACTOR }, { workspaceId: "ws-1", requestId });
106
+ await waitFor(() => cp.acksFromLa.some((a) => a.status === "rejected"));
107
+ assert.deepEqual(cp.acksFromLa.filter((a) => a.status === "rejected").map((a) => a.errorCode), ["protocol_unsupported"], String(mode));
108
+ assert.equal(cp.received.some((f) => f.type === "content.response"), false, "no content.response, no file read");
109
+ assert.equal(logs.concat(errors).some((l) => l.includes(root)), false, "no path in the logs");
110
+ }
111
+ link.close();
112
+ fs.rmSync(root, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ test("installContentHandlers itself refuses to install without DLP enforce (defence in depth, no opt-out)", async () => {
117
+ const { installContentHandlers } = await import("../../src/la-protocol-v2/content-handlers/install.js");
118
+ const cfg = resolveContentHandlersConfig({ LOCAL_AGENT_CONTENT_HANDLERS: "true" }, () => {});
119
+ for (const mode of [undefined, "off", "warn"]) {
120
+ assert.throws(() => installContentHandlers({ link: { session: {}, close() {} }, tenantId: TENANT, wsManager: null, getWorkspaceRoot: () => null, config: cfg,
121
+ env: mode ? { LOCAL_AGENT_DLP_MODE: mode } : {} }), /DLP_MODE=enforce/);
122
+ }
98
123
  });
99
124
 
100
125
  test("flag on: a workspace of another tenant (per the agent's own registry) is not served", async () => {
@@ -113,7 +138,7 @@ test("flag on: a workspace of another tenant (per the agent's own registry) is n
113
138
  test("closing the link while a stream is in flight cancels it (no work left behind)", async () => {
114
139
  const root = tmpDir(); write(root, "big.txt", Buffer.alloc(900_000, 0x61));
115
140
  const cp = new ReferenceCp();
116
- const { link } = await linkFor(cp, { root, env: { LOCAL_AGENT_CONTENT_HANDLERS: "true", LOCAL_AGENT_DLP_MODE: "off" } });
141
+ const { link } = await linkFor(cp, { root, env: { LOCAL_AGENT_CONTENT_HANDLERS: "true", LOCAL_AGENT_DLP_MODE: "enforce" } });
117
142
  const id = link.content.mintRef({ workspaceId: "ws-1", kind: "file", rel: "big.txt" }).id;
118
143
  cp.silent = true;
119
144
  cp.sendData("content.request", { kind: "file", ref: id, actor: ACTOR }, { workspaceId: "ws-1", requestId: uuid() });
@@ -52,9 +52,18 @@ test("vectors: the suite is complete (every type has a file, plus cross-type)",
52
52
 
53
53
  let valid = 0;
54
54
  let invalid = 0;
55
+ let bodyVectors = 0;
55
56
  for (const file of vectorFiles) {
56
57
  const doc = JSON.parse(fs.readFileSync(path.join(VECTORS, file), "utf8"));
57
58
  for (const v of doc.vectors) {
59
+ // F3-07c: `file.list.body.vectors.json` holds bodies (the decoded data of a content.response), not frames: they name their schema
60
+ if (typeof doc.schema === "string" && doc.schema.endsWith(".body.schema.json")) {
61
+ test(`vector ${file.replace(".vectors.json", "")} :: ${v.name}`, () => {
62
+ assert.equal(contract.validateBody(doc.schema, v.message), v.expect.valid === true);
63
+ bodyVectors++;
64
+ });
65
+ continue;
66
+ }
58
67
  test(`vector ${file.replace(".vectors.json", "")} :: ${v.name}`, () => {
59
68
  const r = contract.validateFrame(v.message);
60
69
  if (v.expect.valid) {
@@ -73,10 +82,11 @@ for (const file of vectorFiles) {
73
82
  }
74
83
  }
75
84
 
76
- test("vectors: counts (134 = 24 valid + 110 invalid in this copy)", () => {
77
- assert.equal(valid + invalid, 134);
78
- assert.equal(valid, 24);
79
- assert.equal(invalid, 110);
85
+ test("vectors: counts (176 frames = 34 valid + 142 invalid in this copy, plus the bodies of file.list)", () => {
86
+ assert.equal(valid + invalid, 176);
87
+ assert.equal(valid, 34);
88
+ assert.equal(invalid, 142);
89
+ assert.equal(bodyVectors, 54, "file.list.body.vectors.json (F3-07c): 54 bodies");
80
90
  });
81
91
 
82
92
  // ------------------------------------------------------------------------------------------------ tables
@@ -99,7 +109,7 @@ test("error codes: fatal and retryable sets mirror the gateway (LaV2ErrorCode)",
99
109
 
100
110
  test("allowlist: the LA receives only cp->la/both types and sends only la->cp/both types; v1 commands are not types", () => {
101
111
  assert.deepEqual([...contract.inboundTypes].sort(), ["ack", "approval.decision", "config.update", "content.request", "error", "job.assign", "job.cancel", "ready", "scan.upload", "webhook.forward"]);
102
- assert.deepEqual([...contract.outboundTypes].sort(), ["ack", "alert", "audit.event", "content.response", "error", "heartbeat", "hello", "incident.candidate", "job.event", "job.result", "log.stats", "register", "scan.uploaded"]);
112
+ assert.deepEqual([...contract.outboundTypes].sort(), ["ack", "alert", "audit.event", "content.index", "content.response", "error", "heartbeat", "hello", "incident.candidate", "job.event", "job.result", "log.stats", "register", "scan.uploaded"]);
103
113
  for (const v1 of ["mcp.execute-command", "workspace.open", "health", "shell.exec", "__proto__", "constructor", "toString"]) {
104
114
  assert.equal(contract.canReceive(v1), false, v1);
105
115
  assert.equal(contract.canSend(v1), false, v1);
@@ -52,6 +52,8 @@ export class ReferenceCp {
52
52
  this.rawIn = [];
53
53
  this.dropAcks = 0; // do not ack the next n accepted LA data frames (simulates lost acks)
54
54
  this.silent = false; // do not ack anything
55
+ this.rejectData = []; // F3-07b: error codes, consumed one per LA data frame: the frame is acked `rejected` with that code (not stored)
56
+ this.rejectedFrames = []; // [type, code] of the frames acked `rejected`
55
57
  this.ackDelayMs = 0; // answer acks late (slow consumer)
56
58
  this.onReceived = () => {};
57
59
  this.contentRequests = new Map(); // requestId -> {chunks:[], done}
@@ -167,6 +169,12 @@ export class ReferenceCp {
167
169
  this.ring.set(seq, msgId);
168
170
  while (this.ring.size > 512) this.ring.delete(this.ring.keys().next().value);
169
171
  if (m.ack !== undefined) this.#ackUpTo(m.ack);
172
+ if (this.rejectData.length > 0) { // the gateway's non-fatal answer: the seq is consumed, the frame is not stored
173
+ const code = this.rejectData.shift();
174
+ this.rejectedFrames.push([m.type, code]);
175
+ this.#sendAck(seq, "rejected", code);
176
+ return null;
177
+ }
170
178
  this.received.push(m);
171
179
  if (m.type === "content.response") {
172
180
  const c = this.contentRequests.get(m.requestId) || { chunks: [], done: false };
@@ -1,4 +1,4 @@
1
- // F3-05b: the 24 whole-session traces of the contract (F3-22), reproduced in Node. "Traces are language
1
+ // F3-05b (+2 of F3-07b, content.index): the 26 whole-session traces of the contract (F3-22), reproduced in Node. "Traces are language
2
2
  // neutral; the LA must reproduce each expect.error." Three things are shown for them:
3
3
  // 1. the reference CP (test helper) reproduces every trace: same error at the same frame (so the model the
4
4
  // other tests use is the model of the contract);
@@ -18,9 +18,9 @@ import { V2Session } from "../../src/la-protocol-v2/session.js";
18
18
  const DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "../contract/la-protocol-v2/vectors/traces");
19
19
  const traces = fs.readdirSync(DIR).filter((f) => f.endsWith(".json")).sort().map((f) => ({ file: f, name: f.replace(".json", ""), ...JSON.parse(fs.readFileSync(path.join(DIR, f), "utf8")) }));
20
20
 
21
- test("the suite has all 24 traces (20 invalid, 4 valid)", () => {
22
- assert.equal(traces.length, 24);
23
- assert.equal(traces.filter((t) => t.expect.ok).length, 4);
21
+ test("the suite has all 26 traces (21 invalid, 5 valid)", () => {
22
+ assert.equal(traces.length, 26);
23
+ assert.equal(traces.filter((t) => t.expect.ok).length, 5);
24
24
  });
25
25
 
26
26
  const textOf = (fr) => {
@@ -223,6 +223,31 @@ test("valid-content-chunks-in-order: the three chunks of the trace go out in ord
223
223
  env.session.close();
224
224
  });
225
225
 
226
+ test("valid-content-index: the two inventories of the trace go out in order after ready and the CP accepts them", async () => {
227
+ const t = traces.find((x) => x.name === "valid-content-index");
228
+ const cp = new ReferenceCp({ tenantId: t.session.tenantId, agentId: t.session.agentId });
229
+ const env = inMemory(cp, { tenantId: t.session.tenantId, agentId: t.session.agentId }).start();
230
+ await waitFor(() => env.session.state === "ready");
231
+ for (const fr of LA_FRAMES(t).filter((f) => f.frame.type === "content.index")) {
232
+ await env.session.send("content.index", fr.frame.payload, { workspaceId: fr.frame.workspaceId });
233
+ }
234
+ assert.deepEqual(cp.received.map((f) => [f.type, f.seq]), [["content.index", 1], ["content.index", 2]]);
235
+ assert.deepEqual(cp.errorsSent, []);
236
+ env.session.close();
237
+ });
238
+
239
+ test("invalid-content-index-before-ready: the LA never produces it (no data before ready: not_ready, nothing written)", async () => {
240
+ const cp = new ReferenceCp();
241
+ const sent = [];
242
+ const env = inMemory(cp);
243
+ env.session.attach({ send: (x) => sent.push(JSON.parse(x)), close() {}, bufferedAmount: () => 0 });
244
+ const t = traces.find((x) => x.name === "invalid-content-index-before-ready");
245
+ const bad = t.frames[t.expect.atFrame].frame;
246
+ await assert.rejects(env.session.enqueue("content.index", bad.payload, { workspaceId: bad.workspaceId }), (e) => e.code === "not_ready");
247
+ assert.deepEqual(sent.map((f) => f.type), ["register", "hello"]);
248
+ env.session.close();
249
+ });
250
+
226
251
  test("valid-retransmit-is-idempotent: an ack lost -> the same frame again -> the CP re-acks and counts one event", async () => {
227
252
  const cp = new ReferenceCp({ limits: { ackTimeoutMs: 100 } });
228
253
  const env = inMemory(cp).start();