agent-dag 1.33.72 → 1.33.74
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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>agents-deck</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-8oJEgTRz.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-CF2yG9oG.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/hook/hook.js
CHANGED
|
@@ -119,6 +119,30 @@ function challengeProof(token, nonce) {
|
|
|
119
119
|
return crypto.createHash("sha256").update(`${token}:${nonce}`).digest("hex");
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Must this target answer the challenge before it is handed a payload?
|
|
124
|
+
*
|
|
125
|
+
* Only a deck that advertises a token can be asked to prove it holds one. And
|
|
126
|
+
* hook.js is a single shared file — <claude config dir>/agent-dag/hook.js,
|
|
127
|
+
* installed by whichever deck booted most recently — while running several
|
|
128
|
+
* decks at once is ordinary use. So a hook that knows about the handshake
|
|
129
|
+
* routinely reads discovery files written by decks that predate it, which serve
|
|
130
|
+
* no /api/hook-challenge route at all. Refusing those outright leaves every one
|
|
131
|
+
* of them listening and permanently empty, with its banner still saying it is
|
|
132
|
+
* receiving events.
|
|
133
|
+
*
|
|
134
|
+
* A tokenless file therefore falls back to what shipped before the handshake:
|
|
135
|
+
* pid liveness and nothing else. That is not a weakening of anything — it is
|
|
136
|
+
* the exact risk every release up to 1.33.70 already carried, unchanged — and
|
|
137
|
+
* it costs the hardening nothing, because a file that does carry a token still
|
|
138
|
+
* gets no payload until the port answers correctly. Drop this fallback, and
|
|
139
|
+
* refuse tokenless files again, once no deck older than 1.33.71 is plausibly
|
|
140
|
+
* still running.
|
|
141
|
+
*/
|
|
142
|
+
function requiresProof(d) {
|
|
143
|
+
return typeof d.token === "string" && d.token !== "";
|
|
144
|
+
}
|
|
145
|
+
|
|
122
146
|
// Constant-time compare, purely so a hostile listener cannot walk the expected
|
|
123
147
|
// proof out of us one byte at a time by timing how long we take to hang up. The
|
|
124
148
|
// lengths are public (64 hex chars) and a mismatched one is rejected outright,
|
|
@@ -142,11 +166,15 @@ const POST_TIMEOUT_MS = 1000;
|
|
|
142
166
|
* only once it has. `done` runs exactly once, whatever the outcome — a refused
|
|
143
167
|
* connection, a silent port, a wrong answer and a delivered event all just mean
|
|
144
168
|
* this target is finished.
|
|
169
|
+
*
|
|
170
|
+
* A deck that advertised no token is posted to directly: see requiresProof.
|
|
145
171
|
*/
|
|
146
172
|
function deliver(d, body, done) {
|
|
147
173
|
let settled = false;
|
|
148
174
|
const finish = () => { if (settled) return; settled = true; done(); };
|
|
149
175
|
|
|
176
|
+
if (!requiresProof(d)) return post(d, body, finish);
|
|
177
|
+
|
|
150
178
|
const nonce = crypto.randomBytes(16).toString("hex");
|
|
151
179
|
const want = challengeProof(d.token, nonce);
|
|
152
180
|
|
|
@@ -236,12 +264,9 @@ function main() {
|
|
|
236
264
|
let d;
|
|
237
265
|
try { d = JSON.parse(fs.readFileSync(path.join(DIR, file), "utf8")); } catch { continue; }
|
|
238
266
|
if (typeof d.workspace !== "string" || !d.pid || !d.port) continue;
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// next start, against the cost of trusting an unauthenticated port with
|
|
243
|
-
// every prompt the user types.
|
|
244
|
-
if (typeof d.token !== "string" || d.token === "") continue;
|
|
267
|
+
// A missing token is not a reason to drop the file here — deliver()
|
|
268
|
+
// decides what a target has to prove, and a deck older than the handshake
|
|
269
|
+
// can prove nothing. See requiresProof.
|
|
245
270
|
|
|
246
271
|
if (!isAlive(d.pid)) {
|
|
247
272
|
try { fs.unlinkSync(path.join(DIR, file)); } catch {}
|
|
@@ -275,5 +300,5 @@ function main() {
|
|
|
275
300
|
// the installer writes is `"<node>" "<...>/hook.js" --provider <name>`. Under a
|
|
276
301
|
// require() it exports the matching rule and starts nothing, which is what lets
|
|
277
302
|
// the rule be tested without a 1.5s exit timer in the test runner.
|
|
278
|
-
module.exports = { cwdInWorkspace, foldsCase, challengeProof };
|
|
303
|
+
module.exports = { cwdInWorkspace, foldsCase, challengeProof, requiresProof };
|
|
279
304
|
if (require.main === module) main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.33.
|
|
3
|
+
"version": "1.33.74",
|
|
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": {
|
|
@@ -167,12 +167,34 @@ export function npxRestartSpec(pkgRoot, name = "agents-deck") {
|
|
|
167
167
|
return npxSpecFromMeta(meta, name);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/** The package an upgrade would actually install here — the only package worth
|
|
171
|
+
* asking npm about.
|
|
172
|
+
*
|
|
173
|
+
* The check used to ask about `agents-deck` no matter what the upgrade
|
|
174
|
+
* command installed, so a deck started with `npx ccdeck` compared its version
|
|
175
|
+
* against `agents-deck`'s dist-tag and then handed back `npx -y ccdeck@latest`.
|
|
176
|
+
* Nothing tied the two together. CI publishes the three names one after
|
|
177
|
+
* another, so between the first and the last publish they genuinely disagree,
|
|
178
|
+
* and inside that window the deck offered a version the command could not
|
|
179
|
+
* install — the ETARGET below, with a window measured in publishes rather than
|
|
180
|
+
* in seconds of propagation.
|
|
181
|
+
*
|
|
182
|
+
* Deriving the name from the resolved upgrade spec keeps the two halves
|
|
183
|
+
* consistent by construction: whatever the command will install is what gets
|
|
184
|
+
* asked about, and the per-name marker follows the same name. A global install
|
|
185
|
+
* installs `name` and a checkout installs nothing, so both keep asking about
|
|
186
|
+
* the name this build was published under. */
|
|
187
|
+
export function upgradeName(pkgRoot, name = "agents-deck") {
|
|
188
|
+
if (!isNpxInstall(pkgRoot) || isGitCheckout(pkgRoot)) return name;
|
|
189
|
+
return bareSpecName(npxRestartSpec(pkgRoot, name)) ?? name;
|
|
190
|
+
}
|
|
191
|
+
|
|
170
192
|
/** The exact line the user can paste, for the way THIS copy was installed. */
|
|
171
193
|
export function upgradeCommand(pkgRoot, name = "agents-deck") {
|
|
172
194
|
// A checkout is updated by pulling, and the bundle is built, not shipped —
|
|
173
195
|
// so `npm run build` is part of the answer rather than an afterthought.
|
|
174
196
|
if (isGitCheckout(pkgRoot)) return "git pull && npm run build";
|
|
175
|
-
if (isNpxInstall(pkgRoot)) return `npx -y ${
|
|
197
|
+
if (isNpxInstall(pkgRoot)) return `npx -y ${upgradeName(pkgRoot, name)}@latest`;
|
|
176
198
|
return `npm i -g ${name}@latest`;
|
|
177
199
|
}
|
|
178
200
|
|
|
@@ -200,6 +222,41 @@ async function fetchLatest(name) {
|
|
|
200
222
|
}
|
|
201
223
|
}
|
|
202
224
|
|
|
225
|
+
// A dist-tag is a pointer, and being pointed at is not the same as being
|
|
226
|
+
// installable.
|
|
227
|
+
//
|
|
228
|
+
// npm makes the moved tag visible before the version document has propagated to
|
|
229
|
+
// the replica the installer reads, so for a window `{"latest":"1.33.28"}` and
|
|
230
|
+
// `No matching version found for ccdeck@1.33.28` are both true at the same
|
|
231
|
+
// moment. Reported live: the banner offered v1.33.28, the restart ran
|
|
232
|
+
// `npx -y ccdeck@latest`, npm answered ETARGET, and the deck came back on the
|
|
233
|
+
// version it started with after tearing itself down — under npx a restart is
|
|
234
|
+
// not free, since the worker exits and hands the port over before anything is
|
|
235
|
+
// fetched.
|
|
236
|
+
//
|
|
237
|
+
// So the tag is checked against the version document, which is the same
|
|
238
|
+
// question `npm view <name>@<version> version` asks and the same document the
|
|
239
|
+
// installer resolves against. 404 is the answer this exists for: published
|
|
240
|
+
// tag, unpublished version, try again in five minutes. `ok` is false only when
|
|
241
|
+
// the registry gave no usable answer at all — that is not a licence to
|
|
242
|
+
// announce either, but it is not evidence of an unpublished version.
|
|
243
|
+
async function isPublished(name, version) {
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch(`https://registry.npmjs.org/${name}/${version}`, {
|
|
246
|
+
headers: { accept: "application/json", "user-agent": "agents-deck" },
|
|
247
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
248
|
+
});
|
|
249
|
+
if (res.status === 404) return { ok: true, published: false };
|
|
250
|
+
if (!res.ok) return { ok: false, published: false };
|
|
251
|
+
// The document has to be the one asked for: a registry that answers 200
|
|
252
|
+
// with something else has not shown that this version is resolvable.
|
|
253
|
+
const body = await res.json().catch(() => null);
|
|
254
|
+
return { ok: true, published: body?.version === version };
|
|
255
|
+
} catch {
|
|
256
|
+
return { ok: false, published: false };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
203
260
|
/** Marker file name for a package: `ccdeck` → `.self-update-check-ccdeck`.
|
|
204
261
|
*
|
|
205
262
|
* Pure, and deliberately strict about what reaches the filesystem — a package
|
|
@@ -276,10 +333,27 @@ function writeMarker(name, marker) {
|
|
|
276
333
|
* A success stamps the hour and clears the failure. A failure records only
|
|
277
334
|
* itself, leaving the last real answer and the time it arrived untouched —
|
|
278
335
|
* the deck keeps showing what it knew, and stops claiming it just confirmed
|
|
279
|
-
* it.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
336
|
+
* it.
|
|
337
|
+
*
|
|
338
|
+
* `installable` is the third outcome: npm answered, and what it named is not
|
|
339
|
+
* yet a version anything can install. That is a real answer — `at` moves, the
|
|
340
|
+
* registry was reached — but the version is held in `pending` instead of
|
|
341
|
+
* `version`, so `latest` stays a number the upgrade command can resolve, and
|
|
342
|
+
* `pendingAt` puts the next look on the short window rather than the hour. */
|
|
343
|
+
export function nextMarker({ prev, now, ok, version, installable = true }) {
|
|
344
|
+
if (!ok) {
|
|
345
|
+
return {
|
|
346
|
+
at: prev?.at ?? null,
|
|
347
|
+
version: prev?.version ?? null,
|
|
348
|
+
failedAt: now,
|
|
349
|
+
pending: prev?.pending ?? null,
|
|
350
|
+
pendingAt: prev?.pendingAt ?? null,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (!installable) {
|
|
354
|
+
return { at: now, version: prev?.version ?? null, failedAt: null, pending: version ?? null, pendingAt: now };
|
|
355
|
+
}
|
|
356
|
+
return { at: now, version: version ?? null, failedAt: null, pending: null, pendingAt: null };
|
|
283
357
|
}
|
|
284
358
|
|
|
285
359
|
// Even a per-package marker is shared by every deck running that package, so
|
|
@@ -294,15 +368,20 @@ const _askedThisProcess = new Set();
|
|
|
294
368
|
* banner appear" is the question this feature gets asked, and the rule behind
|
|
295
369
|
* it should be readable in one place.
|
|
296
370
|
*/
|
|
297
|
-
export function checkDue({ at, failedAt, now, first = false, force = false, ttlMs = CHECK_MS, retryMs = RETRY_MS }) {
|
|
371
|
+
export function checkDue({ at, failedAt, pendingAt, now, first = false, force = false, ttlMs = CHECK_MS, retryMs = RETRY_MS }) {
|
|
298
372
|
if (force || first) return true; // explicit ask, or this process's first
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
373
|
+
// Two ways of not having an answer yet, and neither may spend the hour a real
|
|
374
|
+
// answer buys: the last attempt failed, or npm named a version that cannot be
|
|
375
|
+
// installed yet. Both take the short window instead — an unreachable registry
|
|
376
|
+
// must not turn every poll into another request, and a release mid-publish is
|
|
377
|
+
// resolvable minutes later, not an hour later. Answered before `at`, which
|
|
378
|
+
// here is the older, settled check and would otherwise ask again immediately
|
|
379
|
+
// (or, for a pending version, not for another hour).
|
|
380
|
+
const unsettled = [failedAt, pendingAt].filter(t => typeof t === "number");
|
|
381
|
+
if (unsettled.length) {
|
|
382
|
+
const last = Math.max(...unsettled);
|
|
383
|
+
if (last > now) return true; // clock moved; do not wait it out
|
|
384
|
+
return now - last >= retryMs;
|
|
306
385
|
}
|
|
307
386
|
if (typeof at !== "number") return true; // never checked
|
|
308
387
|
if (at > now) return true; // marker from the future: a moved clock
|
|
@@ -319,18 +398,20 @@ async function latestOnNpm(name, now, force = false) {
|
|
|
319
398
|
const key = markerFileName(name);
|
|
320
399
|
const first = !_askedThisProcess.has(key);
|
|
321
400
|
_askedThisProcess.add(key);
|
|
322
|
-
if (!checkDue({ at: m?.at, failedAt: m?.failedAt, now, first, force }))
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
401
|
+
if (!checkDue({ at: m?.at, failedAt: m?.failedAt, pendingAt: m?.pendingAt, now, first, force })) {
|
|
402
|
+
return m?.version ?? null;
|
|
403
|
+
}
|
|
404
|
+
const inflight = _inflight.get(key);
|
|
405
|
+
if (inflight) return inflight;
|
|
406
|
+
const run = runCheck(name, m, now)
|
|
326
407
|
// Record the outcome, not just the moment, and record it against THIS
|
|
327
408
|
// package: only an answer stamps `at`; a failure takes the short retry
|
|
328
409
|
// window instead of the hour, keeps the version we already knew rather
|
|
329
410
|
// than erasing it, and lands in this name's marker rather than spending
|
|
330
411
|
// another package's window on a lookup that was never about it.
|
|
331
|
-
.then((
|
|
332
|
-
writeMarker(name,
|
|
333
|
-
return
|
|
412
|
+
.then((marker) => {
|
|
413
|
+
writeMarker(name, marker);
|
|
414
|
+
return marker.version ?? null;
|
|
334
415
|
})
|
|
335
416
|
.catch(() => m?.version ?? null)
|
|
336
417
|
.finally(() => { _inflight.delete(key); });
|
|
@@ -338,6 +419,20 @@ async function latestOnNpm(name, now, force = false) {
|
|
|
338
419
|
return run;
|
|
339
420
|
}
|
|
340
421
|
|
|
422
|
+
/** One check, as a marker: what npm's dist-tag says, and — only when that is a
|
|
423
|
+
* version this deck has not already confirmed — whether it can be installed.
|
|
424
|
+
*
|
|
425
|
+
* The second request is what keeps the banner honest, and it is skipped in the
|
|
426
|
+
* case that runs all day: a tag that has not moved was confirmed the first
|
|
427
|
+
* time it was seen, so a deck sitting on the current release still costs one
|
|
428
|
+
* ~20-byte GET per check. Confirming costs one more, once per release. */
|
|
429
|
+
async function runCheck(name, prev, now) {
|
|
430
|
+
const { ok, version } = await fetchLatest(name);
|
|
431
|
+
if (!ok || version === prev?.version) return nextMarker({ prev, now, ok, version });
|
|
432
|
+
const probe = await isPublished(name, version);
|
|
433
|
+
return nextMarker({ prev, now, ok, version, installable: probe.ok && probe.published });
|
|
434
|
+
}
|
|
435
|
+
|
|
341
436
|
// ── the notice ───────────────────────────────────────────────────────────────
|
|
342
437
|
|
|
343
438
|
/** Pure: turns three version strings into at most one thing worth saying.
|
|
@@ -526,6 +621,11 @@ export function lastMeaningfulLine(text) {
|
|
|
526
621
|
* registry is unreachable. */
|
|
527
622
|
export async function versionReport({ running, pkgRoot, name = "agents-deck", now = Date.now(), force = false }) {
|
|
528
623
|
const installed = installedVersion(pkgRoot);
|
|
624
|
+
// Asked about the package the command installs, not about the one this build
|
|
625
|
+
// happens to be named after — see upgradeName. Everything registry-shaped in
|
|
626
|
+
// this report is about `target`: the version, the marker it is cached in, and
|
|
627
|
+
// the name the report gives for it.
|
|
628
|
+
const target = upgradeName(pkgRoot, name);
|
|
529
629
|
// Only an explicit opt-out silences the registry.
|
|
530
630
|
//
|
|
531
631
|
// A checkout used to be excluded here too, on the reasoning that its version
|
|
@@ -539,11 +639,11 @@ export async function versionReport({ running, pkgRoot, name = "agents-deck", no
|
|
|
539
639
|
const skipRegistry =
|
|
540
640
|
process.env.AGENTS_DECK_NO_UPDATE_CHECK === "1" ||
|
|
541
641
|
process.env.AGENTS_DECK_NO_INSTALL === "1";
|
|
542
|
-
const latest = skipRegistry ? null : await latestOnNpm(
|
|
543
|
-
const marker = skipRegistry ? null : readMarker(
|
|
642
|
+
const latest = skipRegistry ? null : await latestOnNpm(target, now, force);
|
|
643
|
+
const marker = skipRegistry ? null : readMarker(target);
|
|
544
644
|
const blocked = upgradeBlock(pkgRoot);
|
|
545
645
|
return {
|
|
546
|
-
name,
|
|
646
|
+
name: target,
|
|
547
647
|
running: running ?? null,
|
|
548
648
|
installed,
|
|
549
649
|
latest,
|
|
@@ -556,6 +656,11 @@ export async function versionReport({ running, pkgRoot, name = "agents-deck", no
|
|
|
556
656
|
// most common reason for a missing update button — a proxy, a flaky line,
|
|
557
657
|
// an offline machine — is indistinguishable from being up to date.
|
|
558
658
|
checkFailedAt: marker?.failedAt ?? null,
|
|
659
|
+
// A version npm's dist-tag names that the registry cannot serve yet — the
|
|
660
|
+
// one thing `latest` deliberately will not say, since saying it is what
|
|
661
|
+
// sent a deck into a restart that ended in ETARGET. Reported so the state
|
|
662
|
+
// is visible rather than looking like nothing was published at all.
|
|
663
|
+
latestPending: marker?.pending ?? null,
|
|
559
664
|
checkDisabled: skipRegistry,
|
|
560
665
|
notice: pickNotice({ running, installed, latest }),
|
|
561
666
|
command: upgradeCommand(pkgRoot, name),
|