@phnx-labs/agents-cli 1.22.75 → 1.22.76
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/CHANGELOG.md +117 -0
- package/README.md +20 -8
- package/dist/bootstrap.js +7 -7
- package/dist/cli/command-registry.js +5 -0
- package/dist/commands/artifacts-setup.js +1 -1
- package/dist/commands/artifacts.js +1 -1
- package/dist/commands/auth.js +7 -1
- package/dist/commands/browser.js +104 -10
- package/dist/commands/commands.js +7 -6
- package/dist/commands/config.js +27 -4
- package/dist/commands/cost.js +6 -4
- package/dist/commands/doctor.d.ts +6 -5
- package/dist/commands/doctor.js +32 -274
- package/dist/commands/exec.d.ts +2 -0
- package/dist/commands/exec.js +9 -2
- package/dist/commands/harness.d.ts +1 -0
- package/dist/commands/harness.js +11 -3
- package/dist/commands/hooks.js +7 -6
- package/dist/commands/mcp.js +7 -6
- package/dist/commands/memory.js +7 -7
- package/dist/commands/monitors.js +3 -2
- package/dist/commands/open.d.ts +25 -12
- package/dist/commands/open.js +24 -10
- package/dist/commands/permissions.js +7 -6
- package/dist/commands/plugins.js +21 -17
- package/dist/commands/route.js +33 -16
- package/dist/commands/rules.js +7 -12
- package/dist/commands/sessions-share.js +1 -1
- package/dist/commands/setup-watchdog.js +2 -2
- package/dist/commands/setup.js +22 -1
- package/dist/commands/share.js +26 -10
- package/dist/commands/skills.js +7 -6
- package/dist/commands/subagents.js +7 -6
- package/dist/commands/sync.js +81 -10
- package/dist/commands/view.js +4 -1
- package/dist/commands/watchdog.d.ts +1 -1
- package/dist/commands/watchdog.js +10 -10
- package/dist/commands/webhook.d.ts +4 -0
- package/dist/commands/webhook.js +22 -4
- package/dist/commands/workflows.js +7 -6
- package/dist/lib/accounting/rotate.d.ts +3 -1
- package/dist/lib/accounting/rotate.js +8 -4
- package/dist/lib/auth-health.d.ts +2 -0
- package/dist/lib/auth-health.js +2 -0
- package/dist/lib/browser/chrome.d.ts +21 -0
- package/dist/lib/browser/chrome.js +60 -3
- package/dist/lib/browser/drivers/local.d.ts +21 -0
- package/dist/lib/browser/drivers/local.js +102 -9
- package/dist/lib/browser/profiles.d.ts +29 -1
- package/dist/lib/browser/profiles.js +50 -1
- package/dist/lib/browser/types.d.ts +18 -0
- package/dist/lib/config-keys.d.ts +7 -2
- package/dist/lib/config-keys.js +17 -2
- package/dist/lib/daemon/daemon.js +8 -0
- package/dist/lib/daemon/session-summarizer-service.d.ts +24 -0
- package/dist/lib/daemon/session-summarizer-service.js +39 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/daemon-ticks.d.ts +2 -2
- package/dist/lib/daemon-ticks.js +2 -1
- package/dist/lib/deeplink/register.js +10 -9
- package/dist/lib/deeplink/url.d.ts +4 -4
- package/dist/lib/deeplink/url.js +4 -4
- package/dist/lib/device-config.js +25 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -4
- package/dist/lib/devices/doctor-findings.js +14 -8
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/stats-cache.d.ts +4 -0
- package/dist/lib/devices/stats-cache.js +19 -0
- package/dist/lib/drift-sync.d.ts +3 -1
- package/dist/lib/drift-sync.js +16 -5
- package/dist/lib/exec.d.ts +2 -0
- package/dist/lib/exec.js +16 -1
- package/dist/lib/fleet-shared-state.d.ts +8 -0
- package/dist/lib/heal.d.ts +4 -3
- package/dist/lib/heal.js +5 -4
- package/dist/lib/hosts/ready.d.ts +1 -1
- package/dist/lib/hosts/ready.js +16 -4
- package/dist/lib/hosts/reconnect.js +4 -2
- package/dist/lib/identity/client.d.ts +6 -0
- package/dist/lib/identity/index.d.ts +16 -0
- package/dist/lib/identity/index.js +25 -1
- package/dist/lib/profiles.d.ts +2 -0
- package/dist/lib/profiles.js +28 -9
- package/dist/lib/reconcile-and-repair.d.ts +109 -0
- package/dist/lib/reconcile-and-repair.js +267 -0
- package/dist/lib/routers.d.ts +12 -1
- package/dist/lib/routers.js +30 -1
- package/dist/lib/scheduling/routines.js +8 -2
- package/dist/lib/session/active.d.ts +13 -0
- package/dist/lib/session/db.d.ts +47 -7
- package/dist/lib/session/db.js +114 -12
- package/dist/lib/session/mirror.js +58 -0
- package/dist/lib/session/remote/watch.js +22 -2
- package/dist/lib/session/session-cache.d.ts +19 -0
- package/dist/lib/session/session-cache.js +46 -0
- package/dist/lib/session/types.d.ts +34 -0
- package/dist/lib/share/backend.d.ts +6 -4
- package/dist/lib/share/backend.js +10 -8
- package/dist/lib/share/config.d.ts +4 -3
- package/dist/lib/share/config.js +10 -1
- package/dist/lib/share/delete.d.ts +1 -1
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/html.d.ts +1 -1
- package/dist/lib/share/html.js +1 -1
- package/dist/lib/share/provision.d.ts +1 -1
- package/dist/lib/share/provision.js +2 -2
- package/dist/lib/share/publish.d.ts +23 -7
- package/dist/lib/share/publish.js +58 -12
- package/dist/lib/share/worker-template.js +221 -60
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/state.d.ts +15 -0
- package/dist/lib/state.js +29 -7
- package/dist/lib/summarizer/config.d.ts +46 -0
- package/dist/lib/summarizer/config.js +83 -0
- package/dist/lib/summarizer/pass.d.ts +45 -0
- package/dist/lib/summarizer/pass.js +112 -0
- package/dist/lib/summarizer/summarize.d.ts +68 -0
- package/dist/lib/summarizer/summarize.js +120 -0
- package/dist/lib/teams/agents.d.ts +4 -3
- package/dist/lib/teams/agents.js +12 -4
- package/dist/lib/teams/scheduler.d.ts +4 -2
- package/dist/lib/teams/scheduler.js +6 -6
- package/dist/lib/tmux/session.d.ts +2 -0
- package/dist/lib/tmux/session.js +7 -1
- package/dist/lib/types.d.ts +20 -0
- package/dist/lib/verbs.d.ts +23 -0
- package/dist/lib/verbs.js +24 -0
- package/dist/lib/view-types.d.ts +4 -0
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
|
@@ -151,11 +151,22 @@ export default {
|
|
|
151
151
|
}
|
|
152
152
|
const segments = path.split('/').filter(Boolean);
|
|
153
153
|
if (auth.kind === 'phoenix') {
|
|
154
|
-
|
|
154
|
+
// The caller's handle is normally derived from the email local-part. An
|
|
155
|
+
// explicit x-share-handle (the CLI's --handle, PHNX-3547) lets a Phoenix
|
|
156
|
+
// user choose a DIFFERENT free handle — the escape hatch when their
|
|
157
|
+
// derived handle is taken or they want a vanity namespace. Sanitized to
|
|
158
|
+
// the same [a-z0-9-] shape; claim rules below bind it first-writer-writes,
|
|
159
|
+
// exactly like a derived handle.
|
|
160
|
+
const requestedRaw = request.headers.get('x-share-handle') || '';
|
|
161
|
+
const requested = sanitizeNamespace(requestedRaw);
|
|
162
|
+
if (requestedRaw && (!requested || requested.length > 63)) {
|
|
163
|
+
return json({ error: 'invalid handle', handle: requestedRaw }, 400);
|
|
164
|
+
}
|
|
165
|
+
const expected = requested || phoenixHandle(auth);
|
|
155
166
|
if (!expected || segments[0] !== expected) {
|
|
156
167
|
return json({ error: 'namespace mismatch', owner: expected }, 403);
|
|
157
168
|
}
|
|
158
|
-
const claimed = await claimHandle(env.BUCKET, expected, auth.owner);
|
|
169
|
+
const claimed = await claimHandle(env.BUCKET, expected, auth.owner, auth.email || '');
|
|
159
170
|
if (claimed.error) return claimed.error;
|
|
160
171
|
}
|
|
161
172
|
const expiresAt = request.headers.get('x-share-expires-at') || '';
|
|
@@ -268,11 +279,8 @@ export default {
|
|
|
268
279
|
// publish rate limit, enforced ONLY for a managed Phoenix identity. A BYO
|
|
269
280
|
// WRITE_TOKEN publish writes to the operator's OWN bucket at their own
|
|
270
281
|
// cost, so it skips all four — a deliberate, documented policy, NOT a
|
|
271
|
-
// silent no-op.
|
|
272
|
-
//
|
|
273
|
-
// skips the read entirely (nothing consumes it).
|
|
274
|
-
const needExisting = !noRevision || auth.kind === 'phoenix';
|
|
275
|
-
const existing = needExisting ? await env.BUCKET.get(path) : null;
|
|
282
|
+
// silent no-op.
|
|
283
|
+
//
|
|
276
284
|
// Enforcement measures the REAL request body, never a client-declared size.
|
|
277
285
|
// A spoofed-low content-length must NOT (a) slip an oversized body past the
|
|
278
286
|
// per-file cap, (b) let real bytes exceed the total quota, or — most
|
|
@@ -281,8 +289,10 @@ export default {
|
|
|
281
289
|
// Phoenix write we buffer the body bounded by the plan's per-file cap and
|
|
282
290
|
// reject on the REAL size BEFORE any write; only then do we copy the
|
|
283
291
|
// revision and store the buffered bytes. BYO streams unbuffered (uncapped,
|
|
284
|
-
// its own bucket)
|
|
292
|
+
// its own bucket) — which also means a BYO body cannot be re-read for a
|
|
293
|
+
// retry, so BYO gets a single conditional attempt below.
|
|
285
294
|
let putBody = request.body;
|
|
295
|
+
let realBytes = 0;
|
|
286
296
|
if (auth.kind === 'phoenix') {
|
|
287
297
|
const limits = planLimits((await readUsage(env, auth.owner)).usage.plan);
|
|
288
298
|
// Fast-reject an HONEST oversized content-length without reading the body.
|
|
@@ -290,47 +300,101 @@ export default {
|
|
|
290
300
|
// read below, which measures the truth.
|
|
291
301
|
const declaredLen = parseInt(request.headers.get('content-length') || '', 10);
|
|
292
302
|
if (Number.isFinite(declaredLen) && declaredLen > limits.maxFileBytes) {
|
|
293
|
-
return json({ error: 'file too large', maxBytes: limits.maxFileBytes, gotBytes: declaredLen }, 413);
|
|
303
|
+
return json({ error: 'file too large: max ' + limits.maxFileBytes + ' bytes', maxBytes: limits.maxFileBytes, gotBytes: declaredLen }, 413);
|
|
294
304
|
}
|
|
295
305
|
// readBodyBounded aborts the moment it passes the cap, so a chunked/
|
|
296
306
|
// streaming body can never buffer more than the cap (+ one chunk).
|
|
297
307
|
const read = await readBodyBounded(request, limits.maxFileBytes);
|
|
298
308
|
if (read.oversize) {
|
|
299
|
-
return json({ error: 'file too large', maxBytes: limits.maxFileBytes, gotBytes: read.size }, 413);
|
|
309
|
+
return json({ error: 'file too large: max ' + limits.maxFileBytes + ' bytes', maxBytes: limits.maxFileBytes, gotBytes: read.size }, 413);
|
|
300
310
|
}
|
|
301
|
-
|
|
302
|
-
const existingSize = existing && typeof existing.size === 'number' ? existing.size : 0;
|
|
303
|
-
const newCanonical = !existing;
|
|
304
|
-
// Keeping a revision retains the old canonical bytes AND adds the new
|
|
305
|
-
// ones, so storage grows by the full new size. A no-revision or first
|
|
306
|
-
// publish grows by new minus the bytes it replaces (may be negative on a
|
|
307
|
-
// shrink; the ledger clamps at >= 0).
|
|
308
|
-
const charge = (!noRevision && existing) ? realBytes : realBytes - existingSize;
|
|
309
|
-
const charged = await chargeShareWrite(env, auth, {
|
|
310
|
-
charge: charge,
|
|
311
|
-
newCanonical: newCanonical,
|
|
312
|
-
fileBytes: realBytes,
|
|
313
|
-
countRate: true,
|
|
314
|
-
});
|
|
315
|
-
if (charged.error) return charged.error; // rejected BEFORE any destructive write
|
|
311
|
+
realBytes = read.size;
|
|
316
312
|
putBody = read.bytes;
|
|
317
313
|
}
|
|
318
314
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
315
|
+
// Revision retention + quota charge + canonical write as a BOUNDED
|
|
316
|
+
// compare-and-swap loop (PHNX-3547). The old code read the current object,
|
|
317
|
+
// archived it, then overwrote the canonical key UNCONDITIONALLY: two
|
|
318
|
+
// concurrent republishers both archived the same old version and the
|
|
319
|
+
// loser's new body ended up neither canonical nor retained — silently
|
|
320
|
+
// discarded. R2 has no transactions, so each attempt re-reads the canonical
|
|
321
|
+
// object, archives it as a revision, and overwrites ONLY while the etag
|
|
322
|
+
// still matches the read (onlyIf.etagMatches — the same CAS primitive the
|
|
323
|
+
// PATCH path at :513 and the usage ledger already rely on; R2 returns null
|
|
324
|
+
// on the mismatch instead of storing). A conflicted attempt therefore loses
|
|
325
|
+
// nothing: it re-reads the winner's body, archives THAT as the revision on
|
|
326
|
+
// the next attempt, and lands its own body canonical — both writers survive.
|
|
327
|
+
// Phoenix bytes are buffered above, so retrying is safe; the rate counter
|
|
328
|
+
// and object count advance once (attempt 0) and a conflicted re-charge only
|
|
329
|
+
// bills the growth beyond what this request already paid. A BYO stream is
|
|
330
|
+
// consumed by the first attempt, so it gets one conditional try and a 409
|
|
331
|
+
// asking the caller to retry the whole publish.
|
|
332
|
+
const MAX_PUT_ATTEMPTS = 3;
|
|
333
|
+
let chargedAlready = 0;
|
|
334
|
+
let chargedNewCanonical = false;
|
|
335
|
+
let putResult = null;
|
|
336
|
+
for (let attempt = 0; attempt < MAX_PUT_ATTEMPTS; attempt++) {
|
|
337
|
+
// The current object is needed for BOTH the revision copy and the charge
|
|
338
|
+
// math, and even a first/no-revision publish must read before its
|
|
339
|
+
// conditional write so two concurrent creates cannot both report 200.
|
|
340
|
+
const existing = await env.BUCKET.get(path);
|
|
341
|
+
if (auth.kind === 'phoenix') {
|
|
342
|
+
const existingSize = existing && typeof existing.size === 'number' ? existing.size : 0;
|
|
343
|
+
const newCanonical = !existing;
|
|
344
|
+
// Keeping a revision retains the old canonical bytes AND adds the new
|
|
345
|
+
// ones, so storage grows by the full new size. A no-revision or first
|
|
346
|
+
// publish grows by new minus the bytes it replaces (may be negative on a
|
|
347
|
+
// shrink; the ledger clamps at >= 0).
|
|
348
|
+
const charge = (!noRevision && existing) ? realBytes : realBytes - existingSize;
|
|
349
|
+
// Conflict retry: only the growth beyond what this request already
|
|
350
|
+
// paid. Exact accounting under a lost race is impossible without
|
|
351
|
+
// reconciling the bucket; this stays the ledger's documented
|
|
352
|
+
// best-effort, same as its >= 0 clamp.
|
|
353
|
+
const bill = Math.max(0, charge - chargedAlready);
|
|
354
|
+
chargedAlready += bill;
|
|
355
|
+
const chargeNewCanonical = newCanonical && attempt === 0;
|
|
356
|
+
const charged = await chargeShareWrite(env, auth, {
|
|
357
|
+
charge: bill,
|
|
358
|
+
newCanonical: chargeNewCanonical,
|
|
359
|
+
fileBytes: realBytes,
|
|
360
|
+
countRate: attempt === 0,
|
|
361
|
+
});
|
|
362
|
+
if (charged.error) return charged.error; // rejected BEFORE any destructive write
|
|
363
|
+
if (chargeNewCanonical) chargedNewCanonical = true;
|
|
364
|
+
}
|
|
329
365
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
366
|
+
if (!noRevision && existing) {
|
|
367
|
+
const existingHeaders = new Headers();
|
|
368
|
+
if (typeof existing.writeHttpMetadata === 'function') existing.writeHttpMetadata(existingHeaders);
|
|
369
|
+
const existingContentType = existingHeaders.get('content-type');
|
|
370
|
+
const revKey = path + '/rev-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
|
371
|
+
await env.BUCKET.put(revKey, existing.body, {
|
|
372
|
+
httpMetadata: existingContentType ? { contentType: existingContentType } : undefined,
|
|
373
|
+
customMetadata: existing.customMetadata || {},
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const putOpts = { httpMetadata: { contentType }, customMetadata };
|
|
378
|
+
// Existing objects use the bare R2Object#etag for CAS; missing objects
|
|
379
|
+
// use R2's create-only condition so a concurrent first writer wins loud.
|
|
380
|
+
if (existing && existing.etag) putOpts.onlyIf = { etagMatches: existing.etag };
|
|
381
|
+
else putOpts.onlyIf = { etagDoesNotMatch: '*' };
|
|
382
|
+
putResult = await env.BUCKET.put(path, putBody, putOpts);
|
|
383
|
+
if (putResult !== null) break;
|
|
384
|
+
// A failed create-only condition means another first publisher won.
|
|
385
|
+
// Do not turn that loser into a republish: the caller must see the race.
|
|
386
|
+
if (!existing) break;
|
|
387
|
+
if (auth.kind !== 'phoenix') break; // BYO stream is spent — cannot retry
|
|
388
|
+
}
|
|
389
|
+
if (putResult === null) {
|
|
390
|
+
if (auth.kind === 'phoenix') {
|
|
391
|
+
await refundShareWrite(env, auth.owner, {
|
|
392
|
+
refund: chargedAlready,
|
|
393
|
+
freeCanonical: chargedNewCanonical,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return json({ error: 'publish conflict: another write landed first, retry the publish' }, 409);
|
|
397
|
+
}
|
|
334
398
|
// A managed republish may change the title/description. Invalidate only
|
|
335
399
|
// its generated sibling so the next crawler receives a fresh card; BYO
|
|
336
400
|
// publishes send no OG metadata and keep their explicitly uploaded cover.
|
|
@@ -345,15 +409,36 @@ export default {
|
|
|
345
409
|
if (segments.length < 2) return json({ error: 'metadata edit requires /<username>/<slug>' }, 400);
|
|
346
410
|
if (auth.kind === 'phoenix') {
|
|
347
411
|
const expected = phoenixHandle(auth);
|
|
348
|
-
|
|
412
|
+
const handle = segments[0];
|
|
413
|
+
if (!expected || handle !== expected) {
|
|
414
|
+
// An alternate handle must already have a claim; unlike PUT, PATCH
|
|
415
|
+
// cannot create a new namespace as a side effect.
|
|
416
|
+
const claim = handle ? await env.BUCKET.get('__handles/' + handle) : null;
|
|
417
|
+
if (!claim) return json({ error: 'namespace mismatch', owner: expected }, 403);
|
|
418
|
+
}
|
|
419
|
+
// Use the same ownership path as PUT/DELETE so the same verified email
|
|
420
|
+
// under a new userId transfers the claim and re-stamps old pages before
|
|
421
|
+
// the per-object ownership check below.
|
|
422
|
+
const owned = await assertHandleOwner(env.BUCKET, handle, auth.owner, auth.email || '');
|
|
423
|
+
if (owned.error) return json({ error: 'forbidden' }, 403);
|
|
349
424
|
}
|
|
350
425
|
const existing = await env.BUCKET.get(path);
|
|
351
426
|
if (!existing) return json({ error: 'share not found', key: path }, 404);
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
// the
|
|
355
|
-
//
|
|
356
|
-
|
|
427
|
+
// Ownership was settled above, the same way DELETE settles it: WRITE_TOKEN
|
|
428
|
+
// is the endpoint owner/admin credential, and a Phoenix caller has proven
|
|
429
|
+
// the handle claim (or, pre-claim, that no rival userId owns the prefix).
|
|
430
|
+
// There is deliberately NO per-object owner comparison here. Pages in a
|
|
431
|
+
// claimed namespace can carry a stamp that is not the claim holder's
|
|
432
|
+
// userId — a BYO WRITE_TOKEN publish stamps owner = the namespace, a page
|
|
433
|
+
// from the same human's earlier userId that transferHandle never saw, or
|
|
434
|
+
// a page with no stamp at all — and the claim holder could DELETE every
|
|
435
|
+
// one of them yet was refused a visibility change (403 'forbidden'), which
|
|
436
|
+
// left confidential pages public with takedown as the only remedy. The
|
|
437
|
+
// claim is the authority. The stamp itself is deliberately NOT rewritten:
|
|
438
|
+
// the anonymous lazy-expiry path refunds the STAMPED owner's usage ledger
|
|
439
|
+
// (see the GET expiry branch + refundShareWrite), and a fleet/BYO page was
|
|
440
|
+
// never charged to a Phoenix ledger — re-stamping it to the caller would
|
|
441
|
+
// credit her quota with bytes and a slot she never paid for on expiry.
|
|
357
442
|
|
|
358
443
|
let edit;
|
|
359
444
|
try { edit = await request.json(); } catch { return json({ error: 'PATCH body must be JSON' }, 400); }
|
|
@@ -486,7 +571,7 @@ export default {
|
|
|
486
571
|
const viewer = await resolveViewer(request, env, url);
|
|
487
572
|
if (viewer.redirect) return viewer.redirect;
|
|
488
573
|
if (viewer.error) return viewer.error;
|
|
489
|
-
const denied = gateVisibility(url, env, canonical, viewer.identity || null);
|
|
574
|
+
const denied = await gateVisibility(url, env, canonical, viewer.identity || null);
|
|
490
575
|
if (denied) return denied;
|
|
491
576
|
}
|
|
492
577
|
// A private canonical gates its revision list on the viewer key too
|
|
@@ -524,7 +609,7 @@ export default {
|
|
|
524
609
|
if (!page) return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
|
|
525
610
|
const pageVisibility = (page.customMetadata && page.customMetadata.visibility) || 'public';
|
|
526
611
|
if (viewer.error && isIdentityGated(pageVisibility)) return viewer.error;
|
|
527
|
-
const denied = gateVisibility(url, env, page, viewer.identity || null);
|
|
612
|
+
const denied = await gateVisibility(url, env, page, viewer.identity || null);
|
|
528
613
|
if (denied) return denied;
|
|
529
614
|
// A token-gated page's cover is token-gated too (PHNX-3654): a crawler
|
|
530
615
|
// fetching <slug>.png without the key gets 404, so no preview leaks.
|
|
@@ -534,7 +619,7 @@ export default {
|
|
|
534
619
|
if (existingCover && existingCover.customMetadata['og-source-etag'] === page.etag) {
|
|
535
620
|
const current = await env.BUCKET.get(pagePath);
|
|
536
621
|
if (!current || current.etag !== page.etag) { existingCover = null; continue; }
|
|
537
|
-
const currentDenied = gateVisibility(url, env, current, viewer.identity || null);
|
|
622
|
+
const currentDenied = await gateVisibility(url, env, current, viewer.identity || null);
|
|
538
623
|
if (currentDenied) return currentDenied;
|
|
539
624
|
return new Response(request.method === 'HEAD' ? null : existingCover.body, {
|
|
540
625
|
status: 200,
|
|
@@ -619,7 +704,7 @@ export default {
|
|
|
619
704
|
if (viewer.redirect) return viewer.redirect;
|
|
620
705
|
if (viewer.error && isIdentityGated(visibility)) return viewer.error;
|
|
621
706
|
const identity = viewer.identity || null;
|
|
622
|
-
const denied = gateVisibility(url, env, obj, identity);
|
|
707
|
+
const denied = await gateVisibility(url, env, obj, identity);
|
|
623
708
|
if (denied) return denied;
|
|
624
709
|
// Token-gated read auth (PHNX-3654): a 'private' page is served only to a
|
|
625
710
|
// request carrying the matching viewer key (?k= or Bearer), or to its owner.
|
|
@@ -718,7 +803,16 @@ export default {
|
|
|
718
803
|
if (delSegments[0] && delSegments[0] === uid) {
|
|
719
804
|
// own leftover UUID prefix
|
|
720
805
|
} else if (delSegments[0] && delSegments[0] === handle) {
|
|
721
|
-
const owned = await assertHandleOwner(env.BUCKET, handle, auth.owner);
|
|
806
|
+
const owned = await assertHandleOwner(env.BUCKET, handle, auth.owner, auth.email || '');
|
|
807
|
+
if (owned.error) return owned.error;
|
|
808
|
+
} else if (delSegments[0]) {
|
|
809
|
+
// An explicitly-chosen handle (the CLI's --handle, PHNX-3547): the
|
|
810
|
+
// caller's derived handle differs from the namespace, so the claim is
|
|
811
|
+
// the authority — assertHandleOwner refuses strangers (409) and
|
|
812
|
+
// recovers a same-email account move. No claim at all is a mismatch.
|
|
813
|
+
const claim = await env.BUCKET.get('__handles/' + delSegments[0]);
|
|
814
|
+
if (!claim) return json({ error: 'namespace mismatch', owner: handle || uid }, 403);
|
|
815
|
+
const owned = await assertHandleOwner(env.BUCKET, delSegments[0], auth.owner, auth.email || '');
|
|
722
816
|
if (owned.error) return owned.error;
|
|
723
817
|
} else {
|
|
724
818
|
return json({ error: 'namespace mismatch', owner: handle || uid }, 403);
|
|
@@ -1335,8 +1429,13 @@ function phoenixHandle(auth) {
|
|
|
1335
1429
|
}
|
|
1336
1430
|
|
|
1337
1431
|
// First writer of a handle owns it. Later PUTs from the same userId are fine;
|
|
1338
|
-
// a different userId whose email local-part collides gets 409, not a silent
|
|
1339
|
-
|
|
1432
|
+
// a different userId whose email local-part collides gets 409, not a silent
|
|
1433
|
+
// overwrite — EXCEPT when the verified Phoenix email matches the claim's
|
|
1434
|
+
// recorded email exactly: that is the same human re-authenticated under a new
|
|
1435
|
+
// userId (an account move), and the claim transfers to them instead of
|
|
1436
|
+
// dead-ending (PHNX-3547). The transfer also re-stamps owner on the old
|
|
1437
|
+
// account's objects so PATCH/DELETE keep working.
|
|
1438
|
+
async function assertHandleOwner(bucket, handle, userId, email) {
|
|
1340
1439
|
// The __handles/<handle> claim object is the authoritative first-writer record
|
|
1341
1440
|
// of ownership. When it exists it decides ownership OUTRIGHT: the recorded
|
|
1342
1441
|
// userId may write, anyone else is refused. Consult it FIRST — a stray page
|
|
@@ -1347,8 +1446,17 @@ async function assertHandleOwner(bucket, handle, userId) {
|
|
|
1347
1446
|
const key = '__handles/' + handle;
|
|
1348
1447
|
const existing = await bucket.get(key);
|
|
1349
1448
|
if (existing) {
|
|
1350
|
-
const
|
|
1449
|
+
const meta = existing.customMetadata || {};
|
|
1450
|
+
const claimed = meta.userId;
|
|
1351
1451
|
if (claimed && claimed !== userId) {
|
|
1452
|
+
// Same verified email, different userId → account move, not a rival:
|
|
1453
|
+
// rebind the claim and migrate the old owner's objects. Legacy claims
|
|
1454
|
+
// written before the claim recorded an email cannot prove this and keep
|
|
1455
|
+
// the permanent 409.
|
|
1456
|
+
if (email && meta.email && String(meta.email).toLowerCase() === String(email).toLowerCase()) {
|
|
1457
|
+
await transferHandle(bucket, handle, claimed, userId, email);
|
|
1458
|
+
return {};
|
|
1459
|
+
}
|
|
1352
1460
|
return { error: json({ error: 'handle taken', handle: handle }, 409) };
|
|
1353
1461
|
}
|
|
1354
1462
|
return {};
|
|
@@ -1369,15 +1477,48 @@ async function assertHandleOwner(bucket, handle, userId) {
|
|
|
1369
1477
|
return {};
|
|
1370
1478
|
}
|
|
1371
1479
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1480
|
+
// Account-move recovery (PHNX-3547): the claim's recorded userId held the handle;
|
|
1481
|
+
// the caller proves the SAME verified email under a NEW userId. Rebind the claim
|
|
1482
|
+
// and re-stamp owner on every object the old userId owned under this prefix, so
|
|
1483
|
+
// the moved account keeps full control of its shares. Objects owned by anyone
|
|
1484
|
+
// else (BYO namespace stamps, a pre-claim stray) are left untouched.
|
|
1485
|
+
async function transferHandle(bucket, handle, oldUserId, newUserId, email) {
|
|
1486
|
+
await bucket.put('__handles/' + handle, JSON.stringify({ userId: newUserId, email: email }), {
|
|
1487
|
+
httpMetadata: { contentType: 'application/json' },
|
|
1488
|
+
customMetadata: { userId: newUserId, email: email, visibility: 'unlisted' },
|
|
1489
|
+
});
|
|
1490
|
+
let cursor;
|
|
1491
|
+
do {
|
|
1492
|
+
const list = await bucket.list({ prefix: handle + '/', cursor: cursor, include: ['customMetadata'] });
|
|
1493
|
+
for (const o of list.objects || []) {
|
|
1494
|
+
const owner = o.customMetadata && o.customMetadata.owner;
|
|
1495
|
+
if (!owner || owner !== oldUserId) continue;
|
|
1496
|
+
const obj = await bucket.get(o.key);
|
|
1497
|
+
if (!obj) continue;
|
|
1498
|
+
const headers = new Headers();
|
|
1499
|
+
if (typeof obj.writeHttpMetadata === 'function') obj.writeHttpMetadata(headers);
|
|
1500
|
+
const customMetadata = { ...(obj.customMetadata || {}), owner: newUserId };
|
|
1501
|
+
await bucket.put(o.key, obj.body, {
|
|
1502
|
+
httpMetadata: headers.get('content-type') ? { contentType: headers.get('content-type') } : undefined,
|
|
1503
|
+
customMetadata: customMetadata,
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
cursor = list.truncated ? list.cursor : undefined;
|
|
1507
|
+
} while (cursor);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
async function claimHandle(bucket, handle, userId, email) {
|
|
1511
|
+
const owned = await assertHandleOwner(bucket, handle, userId, email);
|
|
1374
1512
|
if (owned.error) return owned;
|
|
1375
1513
|
const key = '__handles/' + handle;
|
|
1376
1514
|
// Write (or rewrite) so a same-user republish resets object Age against the
|
|
1377
|
-
// bucket's 366-day lifecycle — otherwise the claim can expire while pages stay
|
|
1378
|
-
|
|
1515
|
+
// bucket's 366-day lifecycle — otherwise the claim can expire while pages stay
|
|
1516
|
+
// live. The claim also records the verified email: it is what lets a future
|
|
1517
|
+
// same-email/different-userId request prove an account move and recover the
|
|
1518
|
+
// handle instead of hitting the permanent 409.
|
|
1519
|
+
await bucket.put(key, JSON.stringify({ userId: userId, email: email }), {
|
|
1379
1520
|
httpMetadata: { contentType: 'application/json' },
|
|
1380
|
-
customMetadata: { userId: userId, visibility: 'unlisted' },
|
|
1521
|
+
customMetadata: { userId: userId, email: email || '', visibility: 'unlisted' },
|
|
1381
1522
|
});
|
|
1382
1523
|
return {};
|
|
1383
1524
|
}
|
|
@@ -1423,16 +1564,36 @@ function managedCoverHeaders(visibility) {
|
|
|
1423
1564
|
// Gate a me/org read given the ALREADY-RESOLVED viewer identity. Pure/sync: the
|
|
1424
1565
|
// caller resolves the viewer once (it also needs the identity for the ownership
|
|
1425
1566
|
// check) and both the page GET and the ?revisions=json path share this gate.
|
|
1426
|
-
function gateVisibility(url, env, obj, identity) {
|
|
1567
|
+
async function gateVisibility(url, env, obj, identity) {
|
|
1427
1568
|
const visibility = (obj.customMetadata && obj.customMetadata.visibility) || 'public';
|
|
1428
1569
|
if (!isIdentityGated(visibility)) return null;
|
|
1429
1570
|
if (!identity) return bounceToLogin(url, env);
|
|
1430
1571
|
if (!viewerMayRead(visibility, obj.customMetadata, identity)) {
|
|
1572
|
+
// A 'me' page reads for its stamped owner (the fast path above) OR for the
|
|
1573
|
+
// holder of the namespace's handle claim — the same authority PATCH and
|
|
1574
|
+
// DELETE use. Without this, the claim holder who takes a fleet/BYO-stamped
|
|
1575
|
+
// or pre-stamp page to 'me' (owner = namespace, or none) would be locked
|
|
1576
|
+
// out of her own page: PATCH says 200, GET says 404. The stamp itself stays
|
|
1577
|
+
// untouched (expiry refunds credit the stamped ledger), so the read gate
|
|
1578
|
+
// has to consult the claim rather than the stamp.
|
|
1579
|
+
const handle = decodeURIComponent(url.pathname).split('/').filter(Boolean)[0] || '';
|
|
1580
|
+
if (visibility === 'me' && handle && (await holdsHandleClaim(env.BUCKET, handle, identity.userId))) return null;
|
|
1431
1581
|
return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
|
|
1432
1582
|
}
|
|
1433
1583
|
return null;
|
|
1434
1584
|
}
|
|
1435
1585
|
|
|
1586
|
+
// True when userId is the recorded holder of the __handles/<handle> claim.
|
|
1587
|
+
// Read-only: never transfers or writes a claim (that is claimHandle /
|
|
1588
|
+
// assertHandleOwner's job on the write paths).
|
|
1589
|
+
async function holdsHandleClaim(bucket, handle, userId) {
|
|
1590
|
+
if (!handle || !userId) return false;
|
|
1591
|
+
const claim = await bucket.get('__handles/' + handle);
|
|
1592
|
+
if (!claim) return false;
|
|
1593
|
+
const claimed = claim.customMetadata && claim.customMetadata.userId;
|
|
1594
|
+
return !!claimed && claimed === userId;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1436
1597
|
// SHA-256 hex of a string — the form a 'private' object's stored
|
|
1437
1598
|
// 'viewer-token-hash' takes. Both the PUT (hash-on-store) and the read gate use
|
|
1438
1599
|
// this, so a token minted by the CLI matches byte-for-byte (PHNX-3654).
|
|
@@ -1909,7 +2070,7 @@ async function renderOgCard(input) {
|
|
|
1909
2070
|
props: {
|
|
1910
2071
|
style: { width: '100%', height: '100%', display: 'flex', flexDirection: 'column', background: '#0a0a0a', color: '#f5f5f5', padding: '68px 76px 58px', fontFamily: 'Inter' },
|
|
1911
2072
|
children: [
|
|
1912
|
-
{ type: 'div', props: { style: { display: 'flex', color: '#a3e635', fontFamily: 'JetBrains Mono', fontSize: 25, fontWeight: 600, letterSpacing: '-0.5px' }, children: '
|
|
2073
|
+
{ type: 'div', props: { style: { display: 'flex', color: '#a3e635', fontFamily: 'JetBrains Mono', fontSize: 25, fontWeight: 600, letterSpacing: '-0.5px' }, children: 'share.getrush.ai' } },
|
|
1913
2074
|
{ type: 'div', props: { style: { display: 'flex', flexDirection: 'column', flexGrow: 1, justifyContent: 'center', maxWidth: 1050 }, children: [
|
|
1914
2075
|
{ type: 'div', props: { style: { display: 'flex', fontSize: 66, lineHeight: 1.06, fontWeight: 700, letterSpacing: '-2.8px', maxHeight: 218, overflow: 'hidden' }, children: input.title || 'Shared artifact' } },
|
|
1915
2076
|
input.description ? { type: 'div', props: { style: { display: 'flex', marginTop: 24, color: '#a3a3a3', fontSize: 27, lineHeight: 1.35, maxHeight: 74, overflow: 'hidden' }, children: input.description } } : null,
|
|
@@ -2,9 +2,9 @@ const LOADED_COMMAND_NAMES = [
|
|
|
2
2
|
'accounts', 'auth', 'view', 'inspect', 'feedback', 'commands', 'hooks', 'skills', 'rules', 'memory',
|
|
3
3
|
'permissions', 'mcp', 'clis', 'subagents', 'plugins', 'workflows', 'add', 'use',
|
|
4
4
|
'remove', 'rm', 'purge', 'update', 'prune', 'import', 'registry', 'search', 'install', 'packages',
|
|
5
|
-
'routines', 'monitors', 'projects', 'run', 'open', 'reconnect', 'fork', 'config',
|
|
5
|
+
'routines', 'monitors', 'projects', 'run', '_callback', 'open', 'reconnect', 'fork', 'config',
|
|
6
6
|
'models', 'modes', 'trash', 'restore', 'doctor',
|
|
7
|
-
'route', 'harness', 'harnesses', 'secrets', 'menubar', 'sync',
|
|
7
|
+
'route', 'routes', 'harness', 'harnesses', 'secrets', 'menubar', 'sync',
|
|
8
8
|
'refresh-rules', 'factory', 'insights', 'trace', 'reminders',
|
|
9
9
|
'pty', 'tmux', 'watchdog', 'browser', 'computer', 'logs', 'events',
|
|
10
10
|
'ssh', 'devices', 'fleet', 'repos', 'repo', 'setup', 'uninstall', 'upgrade', 'sessions',
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -319,6 +319,21 @@ export declare function getRuntimeStateDir(): string;
|
|
|
319
319
|
export declare function getCompanionDir(): string;
|
|
320
320
|
/** Path to browser runtime data — chrome-data, pids (~/.agents/.cache/browser/). */
|
|
321
321
|
export declare function getBrowserRuntimeDir(): string;
|
|
322
|
+
/**
|
|
323
|
+
* Path to DURABLE browser-profile data (~/.agents/.history/browser-profiles/).
|
|
324
|
+
*
|
|
325
|
+
* This is the persistent home for an attach-only profile's `--user-data-dir` —
|
|
326
|
+
* where a one-time browser sign-in lives. It sits under `.history` (durable),
|
|
327
|
+
* NOT `.cache` (regenerable), for two reasons the ticket (PHNX-3967) named:
|
|
328
|
+
* - `agents browser profiles remove` sweeps `~/.agents/.cache/browser/<name>*`;
|
|
329
|
+
* a durable dir here survives that so logins are not wiped by a routine cleanup.
|
|
330
|
+
* - A cache wipe or the daemon reaper never touches it, so a signed-in Comet
|
|
331
|
+
* survives quit+relaunch.
|
|
332
|
+
* The user's canonical Comet is launched with this as `--user-data-dir`, and the
|
|
333
|
+
* ownership guard in the local driver compares the running instance's
|
|
334
|
+
* `--user-data-dir` against it to reject a foreign port-squatter.
|
|
335
|
+
*/
|
|
336
|
+
export declare function getBrowserDurableDir(): string;
|
|
322
337
|
/** Path to helper subprocess scratch (~/.agents/.cache/helpers/). */
|
|
323
338
|
export declare function getHelpersDir(): string;
|
|
324
339
|
/**
|
package/dist/lib/state.js
CHANGED
|
@@ -642,6 +642,21 @@ export function getRuntimeStateDir() { return process.env.AGENTS_STATE_DIR ?? RU
|
|
|
642
642
|
export function getCompanionDir() { return COMPANION_CACHE_DIR; }
|
|
643
643
|
/** Path to browser runtime data — chrome-data, pids (~/.agents/.cache/browser/). */
|
|
644
644
|
export function getBrowserRuntimeDir() { return BROWSER_RUNTIME_DIR; }
|
|
645
|
+
/**
|
|
646
|
+
* Path to DURABLE browser-profile data (~/.agents/.history/browser-profiles/).
|
|
647
|
+
*
|
|
648
|
+
* This is the persistent home for an attach-only profile's `--user-data-dir` —
|
|
649
|
+
* where a one-time browser sign-in lives. It sits under `.history` (durable),
|
|
650
|
+
* NOT `.cache` (regenerable), for two reasons the ticket (PHNX-3967) named:
|
|
651
|
+
* - `agents browser profiles remove` sweeps `~/.agents/.cache/browser/<name>*`;
|
|
652
|
+
* a durable dir here survives that so logins are not wiped by a routine cleanup.
|
|
653
|
+
* - A cache wipe or the daemon reaper never touches it, so a signed-in Comet
|
|
654
|
+
* survives quit+relaunch.
|
|
655
|
+
* The user's canonical Comet is launched with this as `--user-data-dir`, and the
|
|
656
|
+
* ownership guard in the local driver compares the running instance's
|
|
657
|
+
* `--user-data-dir` against it to reject a foreign port-squatter.
|
|
658
|
+
*/
|
|
659
|
+
export function getBrowserDurableDir() { return path.join(HISTORY_DIR, 'browser-profiles'); }
|
|
645
660
|
/** Path to helper subprocess scratch (~/.agents/.cache/helpers/). */
|
|
646
661
|
export function getHelpersDir() { return HELPERS_DIR; }
|
|
647
662
|
/**
|
|
@@ -1136,6 +1151,13 @@ function serializeCentral(central) {
|
|
|
1136
1151
|
* read-snapshot-then-separately-lock race that {@link updateMeta} would impose.
|
|
1137
1152
|
*/
|
|
1138
1153
|
export function writeMetaUnlocked(meta) {
|
|
1154
|
+
const writesDeviceRoutines = Object.prototype.hasOwnProperty.call(meta, 'deviceRoutines');
|
|
1155
|
+
const writesDeviceConfig = Object.prototype.hasOwnProperty.call(meta, 'deviceConfig');
|
|
1156
|
+
const writesDeviceBrowser = Object.prototype.hasOwnProperty.call(meta, 'deviceBrowser');
|
|
1157
|
+
const writesDeviceFleet = Object.prototype.hasOwnProperty.call(meta, 'deviceFleet');
|
|
1158
|
+
const writesDeviceHosts = Object.prototype.hasOwnProperty.call(meta, 'deviceHosts');
|
|
1159
|
+
const writesDeviceAccounts = Object.prototype.hasOwnProperty.call(meta, 'deviceAccounts');
|
|
1160
|
+
const writesProjectRoot = Object.prototype.hasOwnProperty.call(meta, 'projectRoot');
|
|
1139
1161
|
// INVARIANT: every key destructured here must also be in BESPOKE_DEVICE_KEYS (and
|
|
1140
1162
|
// vice versa) — a bespoke device key that is classified but NOT pulled out here
|
|
1141
1163
|
// would fall into `central`, and the generic router skips it (BESPOKE_DEVICE_KEY_SET),
|
|
@@ -1191,17 +1213,17 @@ export function writeMetaUnlocked(meta) {
|
|
|
1191
1213
|
delete doc.isolatedAgents;
|
|
1192
1214
|
if (Array.isArray(deviceRoutines))
|
|
1193
1215
|
doc.routines = deviceRoutines;
|
|
1194
|
-
else
|
|
1216
|
+
else if (writesDeviceRoutines)
|
|
1195
1217
|
delete doc.routines;
|
|
1196
1218
|
const hasDeviceConfig = !!deviceConfig && Object.keys(deviceConfig).length > 0;
|
|
1197
1219
|
if (hasDeviceConfig)
|
|
1198
1220
|
doc.config = deviceConfig;
|
|
1199
|
-
else
|
|
1221
|
+
else if (writesDeviceConfig)
|
|
1200
1222
|
delete doc.config;
|
|
1201
1223
|
const hasDeviceBrowser = !!deviceBrowser && Object.keys(deviceBrowser).length > 0;
|
|
1202
1224
|
if (hasDeviceBrowser)
|
|
1203
1225
|
doc.browser = deviceBrowser;
|
|
1204
|
-
else
|
|
1226
|
+
else if (writesDeviceBrowser)
|
|
1205
1227
|
delete doc.browser;
|
|
1206
1228
|
// PHNX-3315 device-scoped fleet/hosts/accounts blocks. Each is this box's OWN
|
|
1207
1229
|
// slice; the effective fleet view is unioned across every device doc at read
|
|
@@ -1219,12 +1241,12 @@ export function writeMetaUnlocked(meta) {
|
|
|
1219
1241
|
df.ignored = fleetIgnored;
|
|
1220
1242
|
doc.fleet = df;
|
|
1221
1243
|
}
|
|
1222
|
-
else
|
|
1244
|
+
else if (writesDeviceFleet)
|
|
1223
1245
|
delete doc.fleet;
|
|
1224
1246
|
const hasDeviceHosts = !!deviceHosts && Object.keys(deviceHosts).length > 0;
|
|
1225
1247
|
if (hasDeviceHosts)
|
|
1226
1248
|
doc.hosts = deviceHosts;
|
|
1227
|
-
else
|
|
1249
|
+
else if (writesDeviceHosts)
|
|
1228
1250
|
delete doc.hosts;
|
|
1229
1251
|
const accountsNative = deviceAccounts?.native && Object.keys(deviceAccounts.native).length > 0
|
|
1230
1252
|
? deviceAccounts.native : undefined;
|
|
@@ -1238,12 +1260,12 @@ export function writeMetaUnlocked(meta) {
|
|
|
1238
1260
|
da.bindings = accountsBindings;
|
|
1239
1261
|
doc.accounts = da;
|
|
1240
1262
|
}
|
|
1241
|
-
else
|
|
1263
|
+
else if (writesDeviceAccounts)
|
|
1242
1264
|
delete doc.accounts;
|
|
1243
1265
|
const hasProjectRoot = typeof projectRoot === 'string' && projectRoot.length > 0;
|
|
1244
1266
|
if (hasProjectRoot)
|
|
1245
1267
|
doc.projectRoot = projectRoot;
|
|
1246
|
-
else
|
|
1268
|
+
else if (writesProjectRoot)
|
|
1247
1269
|
delete doc.projectRoot;
|
|
1248
1270
|
// Generic device-scoped keys (PHNX-3315): any key left in `central` that this
|
|
1249
1271
|
// version classifies as device but does NOT bespoke-route round-trips through
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-summarizer configuration (PHNX-3939).
|
|
3
|
+
*
|
|
4
|
+
* Resolves the three knobs behind the daemon summarizer — enabled / base URL /
|
|
5
|
+
* model — from `agents config` (`summarizer.*`, user-scope in the central
|
|
6
|
+
* agents.yaml) with a per-process env override
|
|
7
|
+
* (`AGENTS_SUMMARIZER_ENABLED` / `AGENTS_SUMMARIZER_BASEURL` /
|
|
8
|
+
* `AGENTS_SUMMARIZER_MODEL`). Off by default: with no config and no env, the
|
|
9
|
+
* summarizer is disabled and makes zero model calls.
|
|
10
|
+
*
|
|
11
|
+
* `isSummarizerReady()` is memoized on a short TTL because the display merge
|
|
12
|
+
* (the watch-stream projections) calls it once per session row — a fresh
|
|
13
|
+
* agents.yaml read per row would defeat the "blazing fast" requirement.
|
|
14
|
+
*/
|
|
15
|
+
export interface SummarizerConfig {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
/** Anthropic-wire base URL (Ollama/vLLM/LiteLLM), or undefined when unconfigured. */
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
/** Model id to request, or undefined when unconfigured. */
|
|
20
|
+
model?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the full summarizer config. Env overrides the stored config key by key;
|
|
24
|
+
* an unset env var falls through to `agents config`, then to the built-in
|
|
25
|
+
* default (disabled). Never throws — a missing/corrupt config reads as unset.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveSummarizerConfig(env?: NodeJS.ProcessEnv): SummarizerConfig;
|
|
28
|
+
/**
|
|
29
|
+
* True only when the summarizer is enabled AND has a base URL + model to call.
|
|
30
|
+
* A configuration that is `enabled` but missing an endpoint cannot produce a
|
|
31
|
+
* summary, so it is treated as unconfigured (the service no-ops, the merge marks
|
|
32
|
+
* `skipped`) rather than erroring on every tick.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isSummarizerRunnable(config: SummarizerConfig): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Memoized "will a summary actually be produced?" check for the hot merge path.
|
|
37
|
+
* Reflects {@link isSummarizerRunnable} — enabled AND a base URL AND a model —
|
|
38
|
+
* NOT just `enabled`, because an enabled-but-unconfigured summarizer computes
|
|
39
|
+
* nothing, so a row with no cached summary must read `skipped`, not a `pending`
|
|
40
|
+
* that never resolves (the exact case: `summarizer.enabled on` set before the
|
|
41
|
+
* endpoint). TTL keeps a config change visible within a few seconds without a
|
|
42
|
+
* per-row agents.yaml read.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isSummarizerReady(nowMs?: number): boolean;
|
|
45
|
+
/** Test seam: drop the memoized ready flag so the next read re-resolves. */
|
|
46
|
+
export declare function resetSummarizerReadyCacheForTest(): void;
|