agent-dag 1.35.5 → 1.35.7

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.
@@ -40,7 +40,7 @@
40
40
  document.documentElement.setAttribute("data-theme", stored === "light" ? "light" : "dark");
41
41
  })();
42
42
  </script>
43
- <script type="module" crossorigin src="/assets/index-Cit6gUld.js"></script>
43
+ <script type="module" crossorigin src="/assets/index-D789Qv7-.js"></script>
44
44
  <link rel="stylesheet" crossorigin href="/assets/index-D0TVjNSt.css">
45
45
  </head>
46
46
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.35.5",
3
+ "version": "1.35.7",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1336,7 +1336,125 @@ async function writeResume(res, frame) {
1336
1336
  });
1337
1337
  }
1338
1338
 
1339
+ // What a redacted occurrence of the deck's token is replaced with. A visible
1340
+ // marker rather than an empty string, because the reader of a mangled Bash
1341
+ // output deserves to know the deck did it and why, and because the UI renders
1342
+ // payload strings verbatim — a silent hole would look like a truncated tool
1343
+ // response and get debugged as one.
1344
+ const TOKEN_REDACTED = "[redacted: ccdeck token]";
1345
+
1346
+ /**
1347
+ * Take the deck's own token back out of an event, before anything stores it.
1348
+ *
1349
+ * #366 made HOOK_TOKEN the one credential separating "may read" from "may
1350
+ * mutate", and there is a path that copies it into a store needing no
1351
+ * credential to read. `POST /api/event` is deliberately open — hook/hook.js is
1352
+ * installed outside this package, into the user's ~/.claude, so requiring a
1353
+ * credential there would silence every session whose hook predates that change
1354
+ * — while `GET /api/events`, `GET /events` and events.jsonl are all readable
1355
+ * without one. So an agent session that merely LOOKS at the discovery file puts
1356
+ * the token into the ring buffer, and that is not an exotic act: `cat
1357
+ * ~/.claude/agent-dag/<pid>.json`, a `Read` of it, or a plain `grep -r
1358
+ * ~/.claude` all do it, and CC records both the tool input and the tool
1359
+ * response. From the buffer it is served to exactly the two callers #366 was
1360
+ * written for — the other local UID and the sandboxed subprocess — neither of
1361
+ * which can open the discovery file itself but both of which reach the API. For
1362
+ * them this turned "cannot mutate" back into "can mutate", the token read out
1363
+ * of the event log being the whole of the new gate. What keeps those two out of
1364
+ * the discovery file itself — a mode bit on Unix, the profile directory's ACL
1365
+ * on Windows, where the chmod is a no-op — is spelled out at presentsDeckToken.
1366
+ *
1367
+ * Why this sits in pushEvent and not in handleEventIngest, which is where the
1368
+ * report first put it. Events reach the buffer through more than one door:
1369
+ * handleEventIngest is the hook's, emitCodexEvent is the Codex rollout
1370
+ * watcher's — those events never pass through an HTTP handler at all —
1371
+ * replayLog is the boot replay's, `/api/clear` pushes its own marker, and six
1372
+ * enrichment sites push synthetic events off the back of a transcript read.
1373
+ * Redacting at the ingest handler covers one of those and leaves the next route
1374
+ * somebody adds uncovered. pushEvent is the one point every event passes
1375
+ * through exactly once, so the check is written once and cannot be forgotten.
1376
+ *
1377
+ * Why a walk over the string values rather than
1378
+ * `JSON.stringify(payload).includes(token)`, which is the obvious one-liner.
1379
+ * Measured rather than assumed, on 4.7k real payloads out of a 21MB
1380
+ * events.jsonl (mean 5KB serialized): the walk costs 1.3–3.6 µs per event and
1381
+ * serialize-then-search costs 14–48 µs, the same order as the JSON.parse the
1382
+ * ingest path already pays for the body. Serializing here would also defeat the
1383
+ * deliberate optimization below, which skips serializing ENTIRELY when nothing
1384
+ * is subscribed and nothing is being logged — a headless deck, and the boot
1385
+ * replay of a log that only rotates at 50MB. The walk allocates nothing at all
1386
+ * until it finds something.
1387
+ *
1388
+ * Iterative rather than recursive on purpose. The payload arrives from
1389
+ * JSON.parse of a body capped at 5MB, which is free to nest as deeply as it
1390
+ * likes, and a recursive scan would be a new way to overflow the stack inside
1391
+ * the request listener — where nothing catches it.
1392
+ *
1393
+ * Scope is the token and nothing else in the discovery file. pid, port,
1394
+ * workspace, the log path and startedAt are not credentials — /api/health
1395
+ * already answers with the workspace path — and redacting the workspace would
1396
+ * mangle the `cwd` of every honest event the deck draws.
1397
+ *
1398
+ * What this does NOT catch, stated plainly so nobody mistakes it for more. A
1399
+ * token split across two string fields, or across two events, survives — but
1400
+ * every substring search has that shape, and the halves are individually
1401
+ * worthless: secretEquals is byte-exact, so a partial token authenticates
1402
+ * nothing. Case is not folded for the same reason; HOOK_TOKEN is lowercase hex
1403
+ * and an uppercased copy would not authenticate either.
1404
+ *
1405
+ * And this is not a new oracle. A caller who can POST here and read
1406
+ * /api/events can post a guess and watch whether it comes back redacted, which
1407
+ * tells them precisely what `x-ccdeck-token: <guess>` on any protected route
1408
+ * already tells them — against 256 bits with no structure to search.
1409
+ *
1410
+ * Returns the payload, because a top-level JSON string is a legal body for
1411
+ * `POST /api/event` and a primitive cannot be redacted in place.
1412
+ */
1413
+ function redactDeckToken(raw) {
1414
+ // Read at call time, not at definition time: HOOK_TOKEN is declared far
1415
+ // below this line and nothing calls pushEvent during module evaluation.
1416
+ const token = HOOK_TOKEN;
1417
+ if (typeof raw === "string") return raw.includes(token) ? raw.replaceAll(token, TOKEN_REDACTED) : raw;
1418
+ if (raw === null || typeof raw !== "object") return raw;
1419
+
1420
+ const stack = [raw];
1421
+ while (stack.length > 0) {
1422
+ const node = stack.pop();
1423
+ // Arrays and objects are walked separately because an indexed loop over an
1424
+ // array is markedly cheaper than `for…in` over one, and this runs on every
1425
+ // event: a single PostToolUse response can be an array of thousands.
1426
+ if (Array.isArray(node)) {
1427
+ for (let i = 0; i < node.length; i++) {
1428
+ const v = node[i];
1429
+ if (typeof v === "string") {
1430
+ if (v.includes(token)) node[i] = v.replaceAll(token, TOKEN_REDACTED);
1431
+ } else if (v !== null && typeof v === "object") stack.push(v);
1432
+ }
1433
+ } else {
1434
+ // `for…in` is safe on these: they come from JSON.parse or from an object
1435
+ // literal in this file, so the prototype chain is Object.prototype, which
1436
+ // has nothing enumerable. A `"__proto__"` key in the posted JSON becomes
1437
+ // an OWN data property — JSON.parse does not run the setter — so it both
1438
+ // enumerates here and takes the assignment below as an ordinary write,
1439
+ // leaving the prototype alone.
1440
+ for (const k in node) {
1441
+ const v = node[k];
1442
+ if (typeof v === "string") {
1443
+ if (v.includes(token)) node[k] = v.replaceAll(token, TOKEN_REDACTED);
1444
+ } else if (v !== null && typeof v === "object") stack.push(v);
1445
+ }
1446
+ }
1447
+ }
1448
+ return raw;
1449
+ }
1450
+
1339
1451
  function pushEvent(raw, source, opts = {}) {
1452
+ // First, before anything below can see it: the deck's own credential does not
1453
+ // belong in a store that is served without one. Every entry point to the
1454
+ // buffer, the SSE fan-out and events.jsonl passes through here, so this is
1455
+ // the single line that keeps it out of all three. See redactDeckToken.
1456
+ raw = redactDeckToken(raw);
1457
+
1340
1458
  // Synchronous enrichment: if we already know this session's model, stamp
1341
1459
  // it on the payload so the client's recursive scanner picks it up.
1342
1460
  if (raw && typeof raw === "object" && raw.session_id && !raw.model) {