@phnx-labs/agents-cli 1.22.49 → 1.22.51
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 +229 -0
- package/README.md +10 -0
- package/dist/bootstrap.js +1 -1
- package/dist/commands/accounts.js +1 -13
- package/dist/commands/browser.d.ts +10 -0
- package/dist/commands/browser.js +77 -2
- package/dist/commands/config.js +20 -0
- package/dist/commands/daemon-test-harness.d.ts +42 -0
- package/dist/commands/daemon-test-harness.js +90 -0
- package/dist/commands/device-config-test-harness.d.ts +11 -0
- package/dist/commands/device-config-test-harness.js +59 -0
- package/dist/commands/exec.js +24 -1
- package/dist/commands/feedback.js +1 -1
- package/dist/commands/menubar.js +6 -4
- package/dist/commands/share.js +9 -4
- package/dist/commands/teams.d.ts +12 -0
- package/dist/commands/teams.js +44 -67
- package/dist/commands/traces.js +36 -4
- package/dist/lib/account-provider-registry.d.ts +9 -0
- package/dist/lib/account-provider-registry.js +20 -0
- package/dist/lib/accounting/account-pool-collect.d.ts +34 -0
- package/dist/lib/accounting/account-pool-collect.js +71 -0
- package/dist/lib/accounting/account-pool.d.ts +46 -0
- package/dist/lib/accounting/account-pool.js +33 -0
- package/dist/lib/accounting/capacity.d.ts +24 -0
- package/dist/lib/accounting/capacity.js +30 -0
- package/dist/lib/accounting/rotate.d.ts +11 -18
- package/dist/lib/accounting/rotate.js +6 -24
- package/dist/lib/claude-statusline.d.ts +9 -0
- package/dist/lib/claude-statusline.js +45 -4
- package/dist/lib/computer/ssh-tunnel.d.ts +7 -6
- package/dist/lib/computer/ssh-tunnel.js +13 -8
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +9 -2
- package/dist/lib/device-config.js +23 -0
- package/dist/lib/factory/snapshot.d.ts +1 -1
- package/dist/lib/factory/snapshot.js +1 -1
- package/dist/lib/helper-download.d.ts +12 -2
- package/dist/lib/helper-download.js +12 -2
- package/dist/lib/installations/migrate.js +4 -4
- package/dist/lib/installations/store.js +6 -3
- package/dist/lib/installations/versions.js +48 -46
- package/dist/lib/menubar/install-menubar.d.ts +71 -8
- package/dist/lib/menubar/install-menubar.js +183 -24
- package/dist/lib/share/publish.d.ts +8 -3
- package/dist/lib/share/publish.js +6 -3
- package/dist/lib/share/worker-template.js +239 -22
- package/dist/lib/star-nudge.d.ts +2 -2
- package/dist/lib/star-nudge.js +2 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +2 -2
- package/dist/lib/teams/agents.d.ts +14 -12
- package/dist/lib/teams/agents.js +188 -73
- package/dist/lib/teams/api.d.ts +6 -1
- package/dist/lib/teams/api.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/teams/scheduler.js +26 -8
- package/dist/lib/traces/sync.d.ts +30 -0
- package/dist/lib/traces/sync.js +103 -10
- package/package.json +3 -3
|
@@ -21,7 +21,9 @@ export interface PublishOptions {
|
|
|
21
21
|
/**
|
|
22
22
|
* Server-enforced visibility (RUSH-3135). `public` (default) is listed in
|
|
23
23
|
* the gallery; `unlisted` is a capability URL (GET still 200, X-Robots-Tag:
|
|
24
|
-
* noindex, hidden from gallery/list). `
|
|
24
|
+
* noindex, hidden from gallery/list). `me` requires a Phoenix session and is
|
|
25
|
+
* visible only to the signed-in owner; `org` requires the same and is visible
|
|
26
|
+
* to members of the same Phoenix organization.
|
|
25
27
|
*/
|
|
26
28
|
visibility?: ShareVisibility;
|
|
27
29
|
/**
|
|
@@ -80,7 +82,7 @@ export interface PublishOptions {
|
|
|
80
82
|
/** DI seam for tests — override provenance auto-capture (agent/session/host/repo/date). */
|
|
81
83
|
provenance?: ShareProvenance;
|
|
82
84
|
}
|
|
83
|
-
export type ShareVisibility = 'public' | 'unlisted';
|
|
85
|
+
export type ShareVisibility = 'public' | 'unlisted' | 'me' | 'org';
|
|
84
86
|
export interface PublishResult {
|
|
85
87
|
url: string;
|
|
86
88
|
expiresAt?: string;
|
|
@@ -94,7 +96,10 @@ export interface PublishResult {
|
|
|
94
96
|
/** Whether `label` came from `--label` or was auto-derived. */
|
|
95
97
|
labelSource?: 'explicit' | 'derived';
|
|
96
98
|
}
|
|
97
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* `--unlisted` / `{ unlisted: true }` map to `unlisted`; `--visibility me|org`
|
|
101
|
+
* passes through; otherwise `visibility` (default public).
|
|
102
|
+
*/
|
|
98
103
|
export declare function resolveShareVisibility(opts?: {
|
|
99
104
|
visibility?: ShareVisibility;
|
|
100
105
|
unlisted?: boolean;
|
|
@@ -17,12 +17,15 @@ import { captureCover, OG_WIDTH, OG_HEIGHT, OG_SCALE } from './capture.js';
|
|
|
17
17
|
import { deriveMeta, injectOgMeta } from './og.js';
|
|
18
18
|
import { injectAnalyticsBeacon } from './analytics.js';
|
|
19
19
|
import { prepareShareHtml } from './html.js';
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* `--unlisted` / `{ unlisted: true }` map to `unlisted`; `--visibility me|org`
|
|
22
|
+
* passes through; otherwise `visibility` (default public).
|
|
23
|
+
*/
|
|
21
24
|
export function resolveShareVisibility(opts = {}) {
|
|
22
25
|
if (opts.unlisted === true)
|
|
23
26
|
return 'unlisted';
|
|
24
|
-
if (opts.visibility === 'unlisted')
|
|
25
|
-
return
|
|
27
|
+
if (opts.visibility === 'unlisted' || opts.visibility === 'me' || opts.visibility === 'org')
|
|
28
|
+
return opts.visibility;
|
|
26
29
|
return 'public';
|
|
27
30
|
}
|
|
28
31
|
/**
|
|
@@ -15,12 +15,17 @@
|
|
|
15
15
|
// A managed deployment sets PHOENIX_ID_BASE; BYO sets WRITE_TOKEN; the
|
|
16
16
|
// platform endpoint may set both. Fail loud (401) when neither authenticates.
|
|
17
17
|
// Writes the body to R2 via the BUCKET binding, storing visibility
|
|
18
|
-
// (public|unlisted
|
|
19
|
-
// provenance (agent/session/host/repo/date), a label, and
|
|
20
|
-
// entries in object metadata.
|
|
21
|
-
//
|
|
18
|
+
// (public|unlisted|me|org), owner, org_domain (org only), an optional
|
|
19
|
+
// expires-at, plus provenance (agent/session/host/repo/date), a label, and
|
|
20
|
+
// any `--meta` entries in object metadata. me/org require a Phoenix
|
|
21
|
+
// identity (BYO WRITE_TOKEN cannot publish them). org from a public inbox
|
|
22
|
+
// domain is 400. Overwriting an existing slug first copies the current
|
|
23
|
+
// object to <slug>/rev-<ts>-<rand> (revision history) unless
|
|
22
24
|
// x-share-no-revision is set.
|
|
23
|
-
// - GET /<username>/<slug> — public
|
|
25
|
+
// - GET /<username>/<slug> — public|unlisted are anonymous; me requires the
|
|
26
|
+
// Phoenix owner, org requires a same-domain Phoenix identity (Bearer, then
|
|
27
|
+
// HMAC cookie, then phoenix_ticket). Unauthenticated me/org 302s to
|
|
28
|
+
// Phoenix login (or 401 JSON if PHOENIX_ID_BASE is unset). 410s (and lazily
|
|
24
29
|
// deletes) once its expiry has passed. A bucket lifecycle rule is the durable
|
|
25
30
|
// sweeper; this is the immediate gate.
|
|
26
31
|
// - GET /<username>/<slug>?revisions=json — machine-readable history of the
|
|
@@ -78,6 +83,19 @@ export default {
|
|
|
78
83
|
const vis = normalizeVisibility(request.headers.get('x-share-visibility'));
|
|
79
84
|
if (vis.error) return vis.error;
|
|
80
85
|
const visibility = vis.value;
|
|
86
|
+
if (visibility === 'me' || visibility === 'org') {
|
|
87
|
+
const phoenixBase = typeof env.PHOENIX_ID_BASE === 'string' ? env.PHOENIX_ID_BASE.replace(/\\/+$/, '') : '';
|
|
88
|
+
if (auth.kind !== 'phoenix' || !phoenixBase) {
|
|
89
|
+
return json({ error: 'visibility me/org requires Phoenix identity' }, 400);
|
|
90
|
+
}
|
|
91
|
+
if (visibility === 'org') {
|
|
92
|
+
const domain = emailDomain(auth.email);
|
|
93
|
+
if (!domain) return json({ error: 'org visibility requires a verified email domain' }, 400);
|
|
94
|
+
if (PUBLIC_INBOX_DOMAINS.indexOf(domain) !== -1) {
|
|
95
|
+
return json({ error: 'org visibility cannot use a public email domain', domain: domain }, 400);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
81
99
|
const segments = path.split('/').filter(Boolean);
|
|
82
100
|
if (auth.kind === 'phoenix') {
|
|
83
101
|
const expected = phoenixHandle(auth);
|
|
@@ -90,7 +108,8 @@ export default {
|
|
|
90
108
|
const expiresAt = request.headers.get('x-share-expires-at') || '';
|
|
91
109
|
// 'unlisted' hides the page from the public gallery + JSON listing while
|
|
92
110
|
// keeping the direct URL world-readable (capability URL, not secret).
|
|
93
|
-
//
|
|
111
|
+
// me/org are also hidden from gallery/listing; GET is identity-gated
|
|
112
|
+
// and always sends X-Robots-Tag: noindex (same as unlisted).
|
|
94
113
|
const contentType = request.headers.get('content-type') || 'text/html; charset=utf-8';
|
|
95
114
|
// Provenance (RUSH-2683): captured client-side from the exec env/git/clock,
|
|
96
115
|
// never invented here — a header is simply absent when the CLI had nothing
|
|
@@ -131,6 +150,7 @@ export default {
|
|
|
131
150
|
? auth.owner
|
|
132
151
|
: (env.SHARE_NAMESPACE || segments[0] || 'byo');
|
|
133
152
|
if (owner) customMetadata['owner'] = owner;
|
|
153
|
+
if (visibility === 'org') customMetadata['org_domain'] = emailDomain(auth.email);
|
|
134
154
|
if (agent) customMetadata['agent'] = agent;
|
|
135
155
|
if (session) customMetadata['session'] = session;
|
|
136
156
|
if (host) customMetadata['host'] = host;
|
|
@@ -194,9 +214,22 @@ export default {
|
|
|
194
214
|
|
|
195
215
|
// Revision history for one canonical <user>/<slug> key (RUSH-2683). Checked
|
|
196
216
|
// before the plain object GET below so the query param routes even though
|
|
197
|
-
// the canonical key itself resolves to a real object.
|
|
217
|
+
// the canonical key itself resolves to a real object. me/org use the same
|
|
218
|
+
// identity gate as the page GET — listing session/host/repo on an unauthed
|
|
219
|
+
// ?revisions=json must not leak that the page exists.
|
|
198
220
|
if (segments.length === 2 && url.searchParams.get('revisions') === 'json') {
|
|
199
|
-
|
|
221
|
+
const canonical = await env.BUCKET.get(path);
|
|
222
|
+
if (canonical) {
|
|
223
|
+
const denied = await gateRestrictedGet(request, env, url, canonical);
|
|
224
|
+
if (denied) return denied;
|
|
225
|
+
}
|
|
226
|
+
return renderRevisions(
|
|
227
|
+
env.BUCKET,
|
|
228
|
+
url.origin,
|
|
229
|
+
path,
|
|
230
|
+
request.method,
|
|
231
|
+
canonical && isIdentityGated((canonical.customMetadata && canonical.customMetadata.visibility) || 'public'),
|
|
232
|
+
);
|
|
200
233
|
}
|
|
201
234
|
|
|
202
235
|
const obj = await env.BUCKET.get(path);
|
|
@@ -206,13 +239,19 @@ export default {
|
|
|
206
239
|
await env.BUCKET.delete(path);
|
|
207
240
|
return new Response('gone — this link has expired', { status: 410, headers: { 'content-type': 'text/plain' } });
|
|
208
241
|
}
|
|
242
|
+
const denied = await gateRestrictedGet(request, env, url, obj);
|
|
243
|
+
if (denied) return denied;
|
|
244
|
+
const visibility = (obj.customMetadata && obj.customMetadata.visibility) || 'public';
|
|
209
245
|
const headers = new Headers();
|
|
210
246
|
obj.writeHttpMetadata(headers);
|
|
211
247
|
headers.set('etag', obj.httpEtag);
|
|
212
248
|
if (!headers.has('content-type')) headers.set('content-type', 'text/html; charset=utf-8');
|
|
213
|
-
|
|
214
|
-
|
|
249
|
+
if (visibility === 'me' || visibility === 'org') {
|
|
250
|
+
headers.set('cache-control', 'private, no-store');
|
|
215
251
|
headers.set('X-Robots-Tag', 'noindex');
|
|
252
|
+
} else {
|
|
253
|
+
headers.set('cache-control', 'public, max-age=60');
|
|
254
|
+
if (visibility === 'unlisted') headers.set('X-Robots-Tag', 'noindex');
|
|
216
255
|
}
|
|
217
256
|
return new Response(request.method === 'HEAD' ? null : obj.body, { status: 200, headers });
|
|
218
257
|
}
|
|
@@ -267,8 +306,8 @@ async function renderGallery(bucket, origin, user, method) {
|
|
|
267
306
|
const activeObjects = objects.filter(o => {
|
|
268
307
|
if (isRevisionKey(o.key)) return false;
|
|
269
308
|
if (o.key.endsWith('.png')) return false;
|
|
270
|
-
// Unlisted pages are reachable by direct URL only — never on the gallery.
|
|
271
|
-
if (o.customMetadata && o.customMetadata.visibility
|
|
309
|
+
// Unlisted / me / org pages are reachable by direct URL only — never on the gallery.
|
|
310
|
+
if (isHiddenFromGallery(o.customMetadata && o.customMetadata.visibility)) return false;
|
|
272
311
|
const expiresAt = o.customMetadata && o.customMetadata['expires-at'];
|
|
273
312
|
return !(expiresAt && Date.now() > Date.parse(expiresAt));
|
|
274
313
|
});
|
|
@@ -330,10 +369,10 @@ async function renderListing(bucket, origin, user, method) {
|
|
|
330
369
|
const items = objects
|
|
331
370
|
.filter(o => {
|
|
332
371
|
// Mirror the gallery: hide revisions, sibling .png OG covers, expired
|
|
333
|
-
// pages, and unlisted pages — the listing is the machine-readable public surface.
|
|
372
|
+
// pages, and unlisted/me/org pages — the listing is the machine-readable public surface.
|
|
334
373
|
if (isRevisionKey(o.key)) return false;
|
|
335
374
|
if (o.key.endsWith('.png')) return false;
|
|
336
|
-
if (o.customMetadata && o.customMetadata.visibility
|
|
375
|
+
if (isHiddenFromGallery(o.customMetadata && o.customMetadata.visibility)) return false;
|
|
337
376
|
const expiresAt = o.customMetadata && o.customMetadata['expires-at'];
|
|
338
377
|
return !(expiresAt && now > Date.parse(expiresAt));
|
|
339
378
|
})
|
|
@@ -364,7 +403,7 @@ async function renderListing(bucket, origin, user, method) {
|
|
|
364
403
|
});
|
|
365
404
|
}
|
|
366
405
|
|
|
367
|
-
async function renderRevisions(bucket, origin, key, method) {
|
|
406
|
+
async function renderRevisions(bucket, origin, key, method, identityGated) {
|
|
368
407
|
const objects = [];
|
|
369
408
|
let cursor;
|
|
370
409
|
do {
|
|
@@ -390,12 +429,15 @@ async function renderRevisions(bucket, origin, key, method) {
|
|
|
390
429
|
// Newest first — the most recently replaced version leads.
|
|
391
430
|
items.sort((a, b) => (a.uploadedAt < b.uploadedAt ? 1 : a.uploadedAt > b.uploadedAt ? -1 : 0));
|
|
392
431
|
|
|
432
|
+
const cacheHeaders = identityGated
|
|
433
|
+
? { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'private, no-store', 'X-Robots-Tag': 'noindex' }
|
|
434
|
+
: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'public, max-age=30' };
|
|
393
435
|
if (method === 'HEAD') {
|
|
394
|
-
return new Response(null, { status: 200, headers:
|
|
436
|
+
return new Response(null, { status: 200, headers: cacheHeaders });
|
|
395
437
|
}
|
|
396
438
|
return new Response(JSON.stringify({ key, count: items.length, revisions: items }), {
|
|
397
439
|
status: 200,
|
|
398
|
-
headers:
|
|
440
|
+
headers: cacheHeaders,
|
|
399
441
|
});
|
|
400
442
|
}
|
|
401
443
|
|
|
@@ -404,7 +446,10 @@ async function renderRevisions(bucket, origin, key, method) {
|
|
|
404
446
|
// before it ever reaches this Worker; see RESERVED_META_KEYS in publish.ts).
|
|
405
447
|
// One list, reused both to strip a same-named --meta collision on write and
|
|
406
448
|
// to split arbitrary --meta entries back out on read.
|
|
407
|
-
var RESERVED_METADATA_KEYS = ['expires-at', 'visibility', 'owner', 'agent', 'session', 'host', 'repo', 'date', 'label', 'label-source'];
|
|
449
|
+
var RESERVED_METADATA_KEYS = ['expires-at', 'visibility', 'owner', 'org_domain', 'agent', 'session', 'host', 'repo', 'date', 'label', 'label-source'];
|
|
450
|
+
var PUBLIC_INBOX_DOMAINS = ['gmail.com', 'googlemail.com', 'outlook.com', 'hotmail.com', 'live.com', 'icloud.com', 'me.com'];
|
|
451
|
+
var SHARE_COOKIE = '__Host-phoenix_share';
|
|
452
|
+
var SHARE_COOKIE_MAX_AGE = 604800;
|
|
408
453
|
|
|
409
454
|
// Everything in customMetadata that ISN'T one of the reserved provenance/label
|
|
410
455
|
// keys above — i.e. the caller's own \`--meta key=value\` entries. Surfaced on
|
|
@@ -495,13 +540,185 @@ async function claimHandle(bucket, handle, userId) {
|
|
|
495
540
|
return {};
|
|
496
541
|
}
|
|
497
542
|
|
|
498
|
-
// P1 visibility: public | unlisted. 'org' (and anything else) is 400 — org is Phase 2.
|
|
499
543
|
function normalizeVisibility(raw) {
|
|
500
544
|
const v = (raw || '').trim().toLowerCase();
|
|
501
545
|
if (!v || v === 'public') return { value: 'public' };
|
|
502
|
-
if (v === 'unlisted') return { value:
|
|
503
|
-
|
|
504
|
-
|
|
546
|
+
if (v === 'unlisted' || v === 'me' || v === 'org') return { value: v };
|
|
547
|
+
return { error: json({ error: 'visibility must be public, unlisted, me, or org' }, 400) };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function emailDomain(email) {
|
|
551
|
+
if (!email) return '';
|
|
552
|
+
const at = String(email).lastIndexOf('@');
|
|
553
|
+
if (at < 0) return '';
|
|
554
|
+
return String(email).slice(at + 1).toLowerCase();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isHiddenFromGallery(visibility) {
|
|
558
|
+
return visibility === 'unlisted' || visibility === 'me' || visibility === 'org';
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function isIdentityGated(visibility) {
|
|
562
|
+
return visibility === 'me' || visibility === 'org';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function gateRestrictedGet(request, env, url, obj) {
|
|
566
|
+
const visibility = (obj.customMetadata && obj.customMetadata.visibility) || 'public';
|
|
567
|
+
if (!isIdentityGated(visibility)) return null;
|
|
568
|
+
const viewer = await resolveViewer(request, env, url);
|
|
569
|
+
if (viewer.redirect) return viewer.redirect;
|
|
570
|
+
if (viewer.error) return viewer.error;
|
|
571
|
+
if (!viewer.identity) return bounceToLogin(url, env);
|
|
572
|
+
if (!viewerMayRead(visibility, obj.customMetadata, viewer.identity)) {
|
|
573
|
+
return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function viewerMayRead(visibility, meta, identity) {
|
|
579
|
+
if (visibility === 'me') {
|
|
580
|
+
const owner = meta && meta.owner;
|
|
581
|
+
return !!owner && owner === identity.userId;
|
|
582
|
+
}
|
|
583
|
+
if (visibility === 'org') {
|
|
584
|
+
const stamped = meta && meta.org_domain;
|
|
585
|
+
if (!stamped) return false;
|
|
586
|
+
return emailDomain(identity.email) === String(stamped).toLowerCase();
|
|
587
|
+
}
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function bounceToLogin(url, env) {
|
|
592
|
+
const base = typeof env.PHOENIX_ID_BASE === 'string' ? env.PHOENIX_ID_BASE.replace(/\\/+$/, '') : '';
|
|
593
|
+
if (!base) return json({ error: 'phoenix login is not configured' }, 401);
|
|
594
|
+
return new Response(null, {
|
|
595
|
+
status: 302,
|
|
596
|
+
headers: { Location: base + '/login?return=' + encodeURIComponent(url.toString()) },
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function readCookie(request, name) {
|
|
601
|
+
const header = request.headers.get('Cookie') || '';
|
|
602
|
+
const parts = header.split(';');
|
|
603
|
+
for (let i = 0; i < parts.length; i++) {
|
|
604
|
+
const p = parts[i];
|
|
605
|
+
const eq = p.indexOf('=');
|
|
606
|
+
if (eq < 0) continue;
|
|
607
|
+
if (p.slice(0, eq).trim() === name) return p.slice(eq + 1).trim();
|
|
608
|
+
}
|
|
609
|
+
return '';
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function toB64Url(s) {
|
|
613
|
+
const bytes = new TextEncoder().encode(s);
|
|
614
|
+
let bin = '';
|
|
615
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
616
|
+
return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function fromB64Url(s) {
|
|
620
|
+
const pad = s.length % 4 === 0 ? '' : '='.repeat(4 - (s.length % 4));
|
|
621
|
+
const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + pad;
|
|
622
|
+
const bin = atob(b64);
|
|
623
|
+
const bytes = new Uint8Array(bin.length);
|
|
624
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
625
|
+
return new TextDecoder().decode(bytes);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function bufToHex(buf) {
|
|
629
|
+
return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function hmacHex(secret, data) {
|
|
633
|
+
const key = await crypto.subtle.importKey(
|
|
634
|
+
'raw',
|
|
635
|
+
new TextEncoder().encode(secret),
|
|
636
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
637
|
+
false,
|
|
638
|
+
['sign'],
|
|
639
|
+
);
|
|
640
|
+
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data));
|
|
641
|
+
return bufToHex(sig);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function signShareCookie(identity, env) {
|
|
645
|
+
const secret = typeof env.WRITE_TOKEN === 'string' ? env.WRITE_TOKEN : '';
|
|
646
|
+
if (!secret) return '';
|
|
647
|
+
const exp = Math.floor(Date.now() / 1000) + SHARE_COOKIE_MAX_AGE;
|
|
648
|
+
const payload = identity.userId + '|' + (identity.email || '') + '|' + exp;
|
|
649
|
+
const sig = await hmacHex(secret, payload);
|
|
650
|
+
const value = toB64Url(payload) + '.' + sig;
|
|
651
|
+
return SHARE_COOKIE + '=' + value + '; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=' + SHARE_COOKIE_MAX_AGE;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function identityFromCookie(request, env) {
|
|
655
|
+
const secret = typeof env.WRITE_TOKEN === 'string' ? env.WRITE_TOKEN : '';
|
|
656
|
+
if (!secret) return null;
|
|
657
|
+
const raw = readCookie(request, SHARE_COOKIE);
|
|
658
|
+
if (!raw) return null;
|
|
659
|
+
const dot = raw.lastIndexOf('.');
|
|
660
|
+
if (dot < 1) return null;
|
|
661
|
+
let payload;
|
|
662
|
+
try {
|
|
663
|
+
payload = fromB64Url(raw.slice(0, dot));
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const sig = raw.slice(dot + 1);
|
|
668
|
+
const expected = await hmacHex(secret, payload);
|
|
669
|
+
if (!safeEqual(sig, expected)) return null;
|
|
670
|
+
const first = payload.indexOf('|');
|
|
671
|
+
const last = payload.lastIndexOf('|');
|
|
672
|
+
if (first < 0 || last <= first) return null;
|
|
673
|
+
const userId = payload.slice(0, first);
|
|
674
|
+
const email = payload.slice(first + 1, last);
|
|
675
|
+
const cookieExp = Number(payload.slice(last + 1));
|
|
676
|
+
if (!userId || !Number.isFinite(cookieExp) || Math.floor(Date.now() / 1000) > cookieExp) return null;
|
|
677
|
+
return { userId: userId, email: email };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async function redeemTicket(ticket, env) {
|
|
681
|
+
const base = typeof env.PHOENIX_ID_BASE === 'string' ? env.PHOENIX_ID_BASE.replace(/\\/+$/, '') : '';
|
|
682
|
+
if (!base || !ticket) return null;
|
|
683
|
+
let res;
|
|
684
|
+
try {
|
|
685
|
+
res = await fetch(base + '/api/v1/auth/ticket', {
|
|
686
|
+
method: 'POST',
|
|
687
|
+
headers: { 'content-type': 'application/json' },
|
|
688
|
+
body: JSON.stringify({ ticket: ticket }),
|
|
689
|
+
});
|
|
690
|
+
} catch {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
if (!res.ok) return null;
|
|
694
|
+
let body;
|
|
695
|
+
try {
|
|
696
|
+
body = await res.json();
|
|
697
|
+
} catch {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
if (!body || typeof body.userId !== 'string' || !body.userId) return null;
|
|
701
|
+
return { userId: body.userId, email: typeof body.email === 'string' ? body.email : '' };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function resolveViewer(request, env, url) {
|
|
705
|
+
const claims = await hooks.verifyPhoenixToken(request, env);
|
|
706
|
+
if (claims && typeof claims.userId === 'string' && claims.userId) {
|
|
707
|
+
return { identity: { userId: claims.userId, email: typeof claims.email === 'string' ? claims.email : '' } };
|
|
708
|
+
}
|
|
709
|
+
const cookieId = await identityFromCookie(request, env);
|
|
710
|
+
if (cookieId) return { identity: cookieId };
|
|
711
|
+
const ticket = url.searchParams.get('phoenix_ticket');
|
|
712
|
+
if (!ticket) return {};
|
|
713
|
+
const redeemed = await redeemTicket(ticket, env);
|
|
714
|
+
if (!redeemed) return { error: json({ error: 'invalid ticket' }, 401) };
|
|
715
|
+
const cookie = await signShareCookie(redeemed, env);
|
|
716
|
+
if (!cookie) return { error: json({ error: 'phoenix login is not configured' }, 401) };
|
|
717
|
+
url.searchParams.delete('phoenix_ticket');
|
|
718
|
+
const headers = new Headers();
|
|
719
|
+
headers.set('Location', url.toString());
|
|
720
|
+
headers.append('Set-Cookie', cookie);
|
|
721
|
+
return { redirect: new Response(null, { status: 302, headers: headers }) };
|
|
505
722
|
}
|
|
506
723
|
|
|
507
724
|
async function defaultVerifyPhoenixToken(request, env) {
|
package/dist/lib/star-nudge.d.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* repeats. It is a plain inline line — not a toast, not a nag — and stays out
|
|
9
9
|
* of the way of non-interactive, CI, quiet, and JSON output.
|
|
10
10
|
*/
|
|
11
|
-
/** Canonical GitHub repo the nudge points at. */
|
|
12
|
-
export declare const REPO_URL = "https://github.com/phnx-labs/
|
|
11
|
+
/** Canonical GitHub repo the nudge points at (renamed from agents-cli). */
|
|
12
|
+
export declare const REPO_URL = "https://github.com/phnx-labs/agi-cli";
|
|
13
13
|
/** Has the star nudge already been shown on this machine? */
|
|
14
14
|
export declare function hasShownStarNudge(): boolean;
|
|
15
15
|
/** Inputs to the pure show/skip decision (kept side-effect free for testing). */
|
package/dist/lib/star-nudge.js
CHANGED
|
@@ -12,8 +12,8 @@ import * as fs from 'fs';
|
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import { getRuntimeStateDir } from './state.js';
|
|
15
|
-
/** Canonical GitHub repo the nudge points at. */
|
|
16
|
-
export const REPO_URL = 'https://github.com/phnx-labs/
|
|
15
|
+
/** Canonical GitHub repo the nudge points at (renamed from agents-cli). */
|
|
16
|
+
export const REPO_URL = 'https://github.com/phnx-labs/agi-cli';
|
|
17
17
|
/** Sentinel written the first (and only) time the nudge is shown. */
|
|
18
18
|
function nudgeSentinelPath() {
|
|
19
19
|
return path.join(getRuntimeStateDir(), 'star-nudge-shown');
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare function getHumansFilePath(): string;
|
|
|
31
31
|
* file against `schema/agents-yaml.schema.json`. Exported so
|
|
32
32
|
* `lib/devices/config-migration.ts` rewrites a device doc with the same header.
|
|
33
33
|
*/
|
|
34
|
-
export declare const META_HEADER = "# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/
|
|
34
|
+
export declare const META_HEADER = "# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agi-cli\n# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agi-cli/main/cli/schema/agents-yaml.schema.json\n\n";
|
|
35
35
|
/** Root of the system data directory (~/.agents/.system/). */
|
|
36
36
|
export declare function getAgentsDir(): string;
|
|
37
37
|
/** Root of the system data directory (~/.agents/.system/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -167,8 +167,8 @@ const USER_PROMPTCUTS_FILE = path.join(USER_AGENTS_DIR, 'hooks', 'promptcuts.yam
|
|
|
167
167
|
*/
|
|
168
168
|
export const META_HEADER = `# agents-cli metadata
|
|
169
169
|
# Auto-generated - do not edit manually
|
|
170
|
-
# https://github.com/phnx-labs/
|
|
171
|
-
# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/
|
|
170
|
+
# https://github.com/phnx-labs/agi-cli
|
|
171
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agi-cli/main/cli/schema/agents-yaml.schema.json
|
|
172
172
|
|
|
173
173
|
`;
|
|
174
174
|
// ─── Root getters ─────────────────────────────────────────────────────────────
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { captureProcessStartTime } from '../platform/index.js';
|
|
2
2
|
import { AgentType } from './parsers.js';
|
|
3
|
-
import { NoViableDeviceError } from './scheduler.js';
|
|
4
3
|
/**
|
|
5
4
|
* Compute the Lowest Common Ancestor (LCA) of multiple file paths.
|
|
6
5
|
* Returns the deepest common directory shared by all paths.
|
|
@@ -27,6 +26,16 @@ export declare enum AgentStatus {
|
|
|
27
26
|
export declare const TERMINAL_STATUSES: ReadonlySet<AgentStatus>;
|
|
28
27
|
/** True when a teammate has reached a terminal (completed/failed/stopped) status. */
|
|
29
28
|
export declare function isTerminalStatus(status: AgentStatus): boolean;
|
|
29
|
+
export type TeammateFailureStage = 'placement' | 'spawn' | 'execution' | 'dependency' | 'cloud';
|
|
30
|
+
/** Durable evidence observed at a concrete teammate lifecycle boundary. */
|
|
31
|
+
export interface TeammateFailure {
|
|
32
|
+
stage: TeammateFailureStage;
|
|
33
|
+
code: string;
|
|
34
|
+
message: string;
|
|
35
|
+
exit_code: number | null;
|
|
36
|
+
retryable: boolean;
|
|
37
|
+
observed_at: string;
|
|
38
|
+
}
|
|
30
39
|
/**
|
|
31
40
|
* One remote teammate's liveness, resolved by a single host probe. Three states,
|
|
32
41
|
* kept distinct on purpose (RUSH-2366):
|
|
@@ -201,6 +210,7 @@ export declare class AgentProcess {
|
|
|
201
210
|
remotePid: number | null;
|
|
202
211
|
remoteLog: string | null;
|
|
203
212
|
remoteExit: string | null;
|
|
213
|
+
failure: TeammateFailure | null;
|
|
204
214
|
remoteLogOffset: number;
|
|
205
215
|
remotePollSnapshot: RemoteLivenessSnapshot | null;
|
|
206
216
|
private eventsCache;
|
|
@@ -227,6 +237,7 @@ export declare class AgentProcess {
|
|
|
227
237
|
cloud_session_id: string | null;
|
|
228
238
|
cloud_repo: string | null;
|
|
229
239
|
cloud_branch: string | null;
|
|
240
|
+
failure: TeammateFailure | null;
|
|
230
241
|
agent_dir: string;
|
|
231
242
|
cwd: string | null;
|
|
232
243
|
}>;
|
|
@@ -432,6 +443,8 @@ export declare class AgentManager {
|
|
|
432
443
|
*/
|
|
433
444
|
setCloudDispatcher(fn: CloudDispatchFn | null): void;
|
|
434
445
|
registerAgent(agent: AgentProcess): void;
|
|
446
|
+
private failAgent;
|
|
447
|
+
private deferAgent;
|
|
435
448
|
/**
|
|
436
449
|
* Scan the agents dir for meta.json files not already in the in-memory
|
|
437
450
|
* cache and load them. Needed when another process (e.g. a Planner
|
|
@@ -553,16 +566,6 @@ export declare class AgentManager {
|
|
|
553
566
|
private maybeSchedulePlacement;
|
|
554
567
|
/** Human label of a teammate's agent for the placement fail-loud message. */
|
|
555
568
|
private placementAgentLabel;
|
|
556
|
-
/**
|
|
557
|
-
* Pre-flight the team pool at `teams start` (RUSH-2002): probe the pool and
|
|
558
|
-
* confirm each distinct agent among the pending, unpinned, pooled teammates
|
|
559
|
-
* has at least one viable device. Returns the first {@link NoViableDeviceError}
|
|
560
|
-
* so the command can fail loud BEFORE launching a wave, rather than silently
|
|
561
|
-
* leaving teammates stranded pending. A poolless team (no `--devices`) or a
|
|
562
|
-
* probe that could not gather positive evidence returns null — fail-loud fires
|
|
563
|
-
* only on proof that no device can run the agent, never on a probe miss.
|
|
564
|
-
*/
|
|
565
|
-
preflightPlacement(taskName: string): Promise<NoViableDeviceError | null>;
|
|
566
569
|
/**
|
|
567
570
|
* One-ssh-per-host batched liveness/exit pre-pass for a team's remote teammates.
|
|
568
571
|
* The supervisor calls this each wave BEFORE listByTask() so the per-teammate
|
|
@@ -659,6 +662,5 @@ export declare class AgentManager {
|
|
|
659
662
|
alreadyStopped: string[];
|
|
660
663
|
}>;
|
|
661
664
|
stop(agentId: string): Promise<boolean>;
|
|
662
|
-
private cleanupPartialAgent;
|
|
663
665
|
private cleanupOldAgents;
|
|
664
666
|
}
|