agent-dag 1.35.6 → 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.
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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.
|
|
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": {
|
package/src/server/index.mjs
CHANGED
|
@@ -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) {
|