@phnx-labs/agents-cli 1.22.53 → 1.22.55
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 +208 -0
- package/README.md +59 -9
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +219 -173
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +10 -8
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +2 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +7 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/routines.js +31 -2
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +64 -20
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +79 -3
- package/dist/commands/share.js +347 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +18 -2
- package/dist/commands/status.js +14 -0
- package/dist/commands/view.d.ts +11 -1
- package/dist/commands/view.js +35 -7
- package/dist/lib/account-registry.js +15 -3
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +68 -1
- package/dist/lib/accounting/usage.js +116 -10
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/cdp.d.ts +1 -1
- package/dist/lib/browser/cdp.js +1 -1
- package/dist/lib/browser/ffmpeg.d.ts +12 -0
- package/dist/lib/browser/ffmpeg.js +184 -0
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/browser/service.js +119 -25
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
- package/dist/lib/daemon/browser-task-reap-service.js +26 -0
- package/dist/lib/daemon/daemon.js +87 -176
- package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
- package/dist/lib/daemon/heartbeat-service.js +26 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
- package/dist/lib/daemon/monitor-engine-service.js +15 -7
- package/dist/lib/daemon/runner.d.ts +15 -0
- package/dist/lib/daemon/runner.js +23 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
- package/dist/lib/daemon/secrets-broker-service.js +17 -32
- package/dist/lib/daemon/service.d.ts +2 -2
- package/dist/lib/daemon/service.js +1 -1
- package/dist/lib/daemon/session-state-service.d.ts +21 -0
- package/dist/lib/daemon/session-state-service.js +34 -0
- package/dist/lib/daemon/supervisor.d.ts +17 -7
- package/dist/lib/daemon/supervisor.js +87 -14
- package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
- package/dist/lib/daemon/tmux-reap-service.js +28 -0
- package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
- package/dist/lib/daemon/webhook-receiver-service.js +17 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +25 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +5 -5
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/harness-inventory.js +5 -2
- package/dist/lib/exec.d.ts +28 -0
- package/dist/lib/exec.js +73 -7
- package/dist/lib/feed/feed.d.ts +1 -1
- package/dist/lib/feed/feed.js +23 -1
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.d.ts +5 -1
- package/dist/lib/monitors/engine.js +13 -4
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/scheduling/routines.d.ts +47 -0
- package/dist/lib/scheduling/routines.js +70 -1
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.js +1 -4
- package/dist/lib/session/db.d.ts +41 -5
- package/dist/lib/session/db.js +132 -30
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +119 -25
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/share/capture.js +11 -2
- package/dist/lib/share/publish.d.ts +56 -5
- package/dist/lib/share/publish.js +126 -18
- package/dist/lib/share/worker-template.d.ts +3 -12
- package/dist/lib/share/worker-template.js +860 -59
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +16 -0
- package/dist/lib/state.js +178 -46
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +7 -0
- package/package.json +9 -2
|
@@ -59,6 +59,14 @@ export function usageNoCredentialError(agent) {
|
|
|
59
59
|
export function usageExpiredCredentialError(agent) {
|
|
60
60
|
return `${agent} credential expired — re-auth this account (a usage read never refreshes it).`;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Kimi-specific expired-credential wording. A normal Kimi launch refreshes its
|
|
64
|
+
* own OAuth access token, so the recovery action for an expired Kimi credential
|
|
65
|
+
* is to run Kimi once — not to re-auth through agents-cli (RUSH-3198).
|
|
66
|
+
*/
|
|
67
|
+
export function usageExpiredKimiCredentialError() {
|
|
68
|
+
return `Kimi credential expired — run Kimi once to refresh it (a usage read never refreshes it).`;
|
|
69
|
+
}
|
|
62
70
|
export function usageRejectedError(agent, status) {
|
|
63
71
|
return status === 429
|
|
64
72
|
? `${agent} is rate-limiting the usage endpoint for this machine (HTTP 429).`
|
|
@@ -84,6 +92,31 @@ export function usageHeadlessScopeError(agent = 'Claude') {
|
|
|
84
92
|
export function isUsageHeadlessScopeError(error) {
|
|
85
93
|
return typeof error === 'string' && error.includes(USAGE_HEADLESS_SCOPE_MARKER);
|
|
86
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Canonical phrase for a Claude account the usage reader holds no usable
|
|
97
|
+
* credential for. Distinct from {@link USAGE_HEADLESS_SCOPE_MARKER}, which
|
|
98
|
+
* means a setup-token WAS read and the endpoint refused its scope.
|
|
99
|
+
*/
|
|
100
|
+
export const USAGE_NO_USAGE_CREDENTIAL_MARKER = 'usage unavailable (no usage credential)';
|
|
101
|
+
/**
|
|
102
|
+
* Claude's own no-credential message. The shared
|
|
103
|
+
* {@link usageNoCredentialError} offers "sign in" as the remedy, which holds
|
|
104
|
+
* for Kimi/Droid/Cursor — their CLIs rotate a readable token on the next launch
|
|
105
|
+
* — and is false for Claude: the usage read deliberately never touches the
|
|
106
|
+
* interactive login (RUSH-1822), so an account that IS signed in reads as
|
|
107
|
+
* unreadable here and signing in again changes nothing. Naming only the second
|
|
108
|
+
* remedy would send the operator to `claude setup-token`, whose token then hits
|
|
109
|
+
* the `user:profile` scope gap (RUSH-2392) — the loop reported in #2987 — so
|
|
110
|
+
* this message states both constraints and that the account still runs.
|
|
111
|
+
*/
|
|
112
|
+
export function usageNoClaudeUsageCredentialError() {
|
|
113
|
+
return (`Claude ${USAGE_NO_USAGE_CREDENTIAL_MARKER} — a usage read never uses your login ` +
|
|
114
|
+
'(RUSH-1822); a setup-token cannot read usage (RUSH-2392). The account still runs.');
|
|
115
|
+
}
|
|
116
|
+
/** True when an error string is the Claude no-usage-credential state (#2987). */
|
|
117
|
+
export function isUsageNoUsageCredentialError(error) {
|
|
118
|
+
return typeof error === 'string' && error.includes(USAGE_NO_USAGE_CREDENTIAL_MARKER);
|
|
119
|
+
}
|
|
87
120
|
/**
|
|
88
121
|
* Detect Anthropic's usage-endpoint scope denial: HTTP 403 whose body names
|
|
89
122
|
* `user:profile` (or "scope requirement"). A bare 403 without that body stays
|
|
@@ -130,6 +163,35 @@ export function usageUnreachableError(agent, cause) {
|
|
|
130
163
|
*/
|
|
131
164
|
export const USAGE_NO_RECENT_USAGE_MARKER = 'no usage recorded yet';
|
|
132
165
|
export const USAGE_BENIGN_STATE = Symbol('usageBenignState');
|
|
166
|
+
/**
|
|
167
|
+
* Sentinel `UsageInfo.error` for a read-only lookup whose cache held nothing
|
|
168
|
+
* (`getUsageInfoForIdentity`). No request was made and nothing failed — the
|
|
169
|
+
* daemon simply has not collected this account yet. It was an unclassified
|
|
170
|
+
* literal, so `classifyUsageErrorKind` fell through to `'rejected'` and
|
|
171
|
+
* `agents view` printed the generic "usage unavailable" for a cold cache,
|
|
172
|
+
* which reads as a failure the operator should chase (#2987). The string value
|
|
173
|
+
* is unchanged; callers that already compare against `'stale'` keep working.
|
|
174
|
+
*/
|
|
175
|
+
export const USAGE_NOT_COLLECTED_MARKER = 'stale';
|
|
176
|
+
/**
|
|
177
|
+
* Human-facing form of a `UsageInfo.error` for a machine/JSON consumer
|
|
178
|
+
* (`agents view --json`'s `usageError`). Every error string this module
|
|
179
|
+
* constructs is already a full human sentence EXCEPT the internal
|
|
180
|
+
* {@link USAGE_NOT_COLLECTED_MARKER} (`'stale'`) sentinel, which a read-only
|
|
181
|
+
* lookup returns for a never-cached account when `--refresh` was not passed. That
|
|
182
|
+
* value is an internal cache signal, not an error message, and leaking it verbatim
|
|
183
|
+
* contradicts the field's "human-readable" contract (PHNX-3348). Map it to a
|
|
184
|
+
* plain-language, actionable string and pass every genuine error through
|
|
185
|
+
* unchanged. Returns `null` when there is no error.
|
|
186
|
+
*/
|
|
187
|
+
export function usageErrorForDisplay(error) {
|
|
188
|
+
if (!error)
|
|
189
|
+
return null;
|
|
190
|
+
if (error === USAGE_NOT_COLLECTED_MARKER) {
|
|
191
|
+
return 'Usage not collected yet — run `agents view --refresh` to fetch it.';
|
|
192
|
+
}
|
|
193
|
+
return error;
|
|
194
|
+
}
|
|
133
195
|
/**
|
|
134
196
|
* Shared error-classification + 429 backoff for a networked usage fetch whose
|
|
135
197
|
* only signal is an HTTP status (or none at all, on a network failure) —
|
|
@@ -155,8 +217,12 @@ export function classifyUsageFetchFailure(agent, agentId, status, retryAfterHead
|
|
|
155
217
|
export function classifyUsageErrorKind(error) {
|
|
156
218
|
if (!error)
|
|
157
219
|
return null;
|
|
220
|
+
if (error === USAGE_NOT_COLLECTED_MARKER)
|
|
221
|
+
return 'not-collected';
|
|
158
222
|
if (isUsageHeadlessScopeError(error))
|
|
159
223
|
return 'headless-scope';
|
|
224
|
+
if (isUsageNoUsageCredentialError(error))
|
|
225
|
+
return 'no-usage-credential';
|
|
160
226
|
if (error.startsWith('No readable '))
|
|
161
227
|
return 'no-credential';
|
|
162
228
|
if (error.includes('credential expired'))
|
|
@@ -342,12 +408,13 @@ export async function getUsageInfoForIdentity(input, opts) {
|
|
|
342
408
|
// cache file holds every account without collision.
|
|
343
409
|
if (!usageKey) {
|
|
344
410
|
if (readOnly)
|
|
345
|
-
return { snapshot: null, error:
|
|
411
|
+
return { snapshot: null, error: USAGE_NOT_COLLECTED_MARKER };
|
|
346
412
|
return getUsageInfo(input.agentId, {
|
|
347
413
|
home: input.home,
|
|
348
414
|
cliVersion: input.cliVersion,
|
|
349
415
|
organizationId: input.info.organizationId,
|
|
350
416
|
fileOnly: opts?.fileOnly,
|
|
417
|
+
allowInteractiveLogin: opts?.allowInteractiveLogin,
|
|
351
418
|
});
|
|
352
419
|
}
|
|
353
420
|
const cached = readClaudeUsageCache(usageKey);
|
|
@@ -361,21 +428,24 @@ export async function getUsageInfoForIdentity(input, opts) {
|
|
|
361
428
|
// it. A stale-or-absent snapshot is handled downstream by the router's own
|
|
362
429
|
// freshness guard (`isUsageVerified` in rotate.ts), which routes around a
|
|
363
430
|
// number it can't confirm rather than trusting an old one — so returning a
|
|
364
|
-
// stale snapshot here is safe, and an absent one reports
|
|
431
|
+
// stale snapshot here is safe, and an absent one reports
|
|
432
|
+
// {@link USAGE_NOT_COLLECTED_MARKER}.
|
|
365
433
|
if (readOnly) {
|
|
366
434
|
if (cached)
|
|
367
435
|
return { snapshot: cached, error: null };
|
|
368
|
-
return { snapshot: null, error:
|
|
436
|
+
return { snapshot: null, error: USAGE_NOT_COLLECTED_MARKER };
|
|
369
437
|
}
|
|
370
438
|
// Explicit refresh: block on the shared device collector.
|
|
371
|
-
return fetchLiveUsageDeduped(input, usageKey, cached, opts?.fileOnly === true
|
|
439
|
+
return fetchLiveUsageDeduped(input, usageKey, cached, opts?.fileOnly === true, {
|
|
440
|
+
allowInteractiveLogin: opts?.allowInteractiveLogin === true,
|
|
441
|
+
});
|
|
372
442
|
}
|
|
373
443
|
/**
|
|
374
444
|
* Single-flight live usage fetch per usage key. Concurrent callers (view +
|
|
375
445
|
* rotation, or two rows sharing an account) await the same promise rather than
|
|
376
446
|
* opening duplicate HTTP requests that then time out and pile up.
|
|
377
447
|
*/
|
|
378
|
-
async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
|
|
448
|
+
async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly, opts) {
|
|
379
449
|
const existing = inFlightLiveFetches.get(usageKey);
|
|
380
450
|
if (existing)
|
|
381
451
|
return existing;
|
|
@@ -398,6 +468,7 @@ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
|
|
|
398
468
|
// throttled account cannot park the whole provider (RUSH-3036).
|
|
399
469
|
usageScope: usageKey,
|
|
400
470
|
fileOnly,
|
|
471
|
+
allowInteractiveLogin: opts?.allowInteractiveLogin === true,
|
|
401
472
|
});
|
|
402
473
|
if (usage.snapshot) {
|
|
403
474
|
if (!usage.snapshot.capturedAt || usage.snapshot.capturedAt.getTime() <= previousCapturedAt) {
|
|
@@ -460,15 +531,28 @@ function formatUsageErrorKindLabel(kind, detail) {
|
|
|
460
531
|
switch (kind) {
|
|
461
532
|
case 'no-credential':
|
|
462
533
|
return 'sign in / provision token';
|
|
534
|
+
// Both of these are permanent for the account as configured, and both used
|
|
535
|
+
// to render as the generic bucket — which reads as a transient failure and
|
|
536
|
+
// sends operators back to `claude setup-token` for a remedy that cannot
|
|
537
|
+
// work (#2987). Name the state instead.
|
|
538
|
+
case 'no-usage-credential':
|
|
539
|
+
return USAGE_NO_USAGE_CREDENTIAL_MARKER;
|
|
540
|
+
case 'headless-scope':
|
|
541
|
+
return USAGE_HEADLESS_SCOPE_MARKER;
|
|
463
542
|
case 'expired-credential':
|
|
543
|
+
// Kimi refreshes its own credential on a normal launch; the recovery hint
|
|
544
|
+
// is embedded in the error string so the label matches the exact action.
|
|
545
|
+
if (detail?.includes('run Kimi once'))
|
|
546
|
+
return 'run Kimi once';
|
|
464
547
|
return 're-auth for usage';
|
|
548
|
+
case 'not-collected':
|
|
549
|
+
return 'usage pending';
|
|
465
550
|
case 'rate-limited': {
|
|
466
551
|
const retryHint = detail?.match(/not retrying for (.+)\.$/)?.[1] ?? null;
|
|
467
552
|
return retryHint ? `rate-limited (retry ~${retryHint})` : 'rate-limited';
|
|
468
553
|
}
|
|
469
554
|
case 'rejected':
|
|
470
555
|
case 'unreachable':
|
|
471
|
-
case 'headless-scope':
|
|
472
556
|
case null:
|
|
473
557
|
case undefined:
|
|
474
558
|
default:
|
|
@@ -761,12 +845,24 @@ async function getClaudeUsageInfo(options) {
|
|
|
761
845
|
// the interactive login (reading that ACL-bound token and firing it at the
|
|
762
846
|
// usage API is what got it revoked — RUSH-1822). No setup-token => null =>
|
|
763
847
|
// "usage pending". fileOnly additionally forbids the ACL keychain path.
|
|
848
|
+
//
|
|
849
|
+
// allowInteractiveLogin is the one sanctioned exception (USAGE-READ-1/2): a
|
|
850
|
+
// foreground human `agents view` on a `personal` device MAY fall through to
|
|
851
|
+
// the interactive login when no setup-token exists, because that login is the
|
|
852
|
+
// only credential carrying the `user:profile` scope the usage endpoint
|
|
853
|
+
// requires (the setup-token is user:inference → 403, RUSH-2392). It is unset
|
|
854
|
+
// for every background caller, so the RUSH-1822 guarantee is untouched there.
|
|
764
855
|
const oauth = await loadClaudeOauth(options?.home, {
|
|
765
856
|
accessTokenCache: true,
|
|
766
857
|
fileOnly: options?.fileOnly === true,
|
|
858
|
+
allowInteractiveLogin: options?.allowInteractiveLogin === true,
|
|
767
859
|
});
|
|
768
860
|
if (!oauth?.accessToken) {
|
|
769
|
-
|
|
861
|
+
// NOT the shared no-credential message: "sign in" is not a remedy here.
|
|
862
|
+
// The account this reads for is usually signed in already — the reader is
|
|
863
|
+
// forbidden from touching that login (RUSH-1822) — so the shared wording
|
|
864
|
+
// asked the operator to redo the one thing they had already done (#2987).
|
|
865
|
+
return { snapshot: null, error: usageNoClaudeUsageCredentialError() };
|
|
770
866
|
}
|
|
771
867
|
const requestedOrgId = normalizeString(options?.organizationId);
|
|
772
868
|
const liveOrgId = normalizeString(oauth.organizationUuid);
|
|
@@ -887,7 +983,7 @@ async function getKimiUsageInfo(options) {
|
|
|
887
983
|
}
|
|
888
984
|
const expiresAt = typeof cred?.expires_at === 'number' ? cred.expires_at : null;
|
|
889
985
|
if (expiresAt !== null && Date.now() / 1000 >= expiresAt) {
|
|
890
|
-
return { snapshot: null, error:
|
|
986
|
+
return { snapshot: null, error: usageExpiredKimiCredentialError() };
|
|
891
987
|
}
|
|
892
988
|
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
893
989
|
// usage-backoff.ts). No request at all while the window is open.
|
|
@@ -1450,7 +1546,7 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1450
1546
|
// the source of truth if it has actually been revoked.
|
|
1451
1547
|
return { accessToken: setupToken };
|
|
1452
1548
|
}
|
|
1453
|
-
// No provisioned setup-token
|
|
1549
|
+
// No provisioned setup-token. A read-only usage/health probe MUST NOT fall
|
|
1454
1550
|
// through to Claude Code's interactive login credential. The daemon's usage
|
|
1455
1551
|
// (~60s) and auth-health (~3min) warms would otherwise read the ACL-bound
|
|
1456
1552
|
// OAuth token and transmit it to api.anthropic.com/api/oauth/usage — an
|
|
@@ -1460,7 +1556,17 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1460
1556
|
// (docs/secrets.md). Report unprovisioned (-> probe
|
|
1461
1557
|
// token 'missing' -> auth-health 'unconfigured', benign for rotation); seed a
|
|
1462
1558
|
// setup-token via the mint-auth path to restore usage/probe for the account.
|
|
1463
|
-
|
|
1559
|
+
//
|
|
1560
|
+
// The single sanctioned exception (USAGE-READ-1/2): a foreground human
|
|
1561
|
+
// `agents view` on a `personal` device sets allowInteractiveLogin, and only
|
|
1562
|
+
// then do we fall through to the interactive-login read below — the one
|
|
1563
|
+
// credential carrying `user:profile`, which the usage endpoint requires. This
|
|
1564
|
+
// is a human running one command, not an unattended loop, so it is not the
|
|
1565
|
+
// revocation risk RUSH-1822 fixed. Every background caller leaves the flag
|
|
1566
|
+
// unset and still returns null here.
|
|
1567
|
+
if (opts?.allowInteractiveLogin !== true) {
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1464
1570
|
}
|
|
1465
1571
|
// Full-credential callers (isClaudeAuthValid -> getClaudeAccessToken)
|
|
1466
1572
|
// legitimately read the interactive login to run/refresh Claude. Rush Cloud
|
|
@@ -376,8 +376,11 @@ export declare function resolveOpenCodeAccountId(base: string): string | undefin
|
|
|
376
376
|
* this file is not authoritative, and probing the Keychain would raise an
|
|
377
377
|
* authorization sheet per installed version on every `agents run` — the reason
|
|
378
378
|
* rotation stopped calling `isClaudeAuthValid` at all. Off macOS the file IS the
|
|
379
|
-
* only store, so a token-less file is proof of signed-out
|
|
380
|
-
*
|
|
379
|
+
* only store, so a missing or token-less file is proof of signed-out — unless a
|
|
380
|
+
* Linux setup-token (`.claude/.oauth_token`) is present, which the shim exports
|
|
381
|
+
* as `CLAUDE_CODE_OAUTH_TOKEN` and can authenticate the run without
|
|
382
|
+
* `.credentials.json`. `platform` is a parameter so both branches are testable
|
|
383
|
+
* on any host.
|
|
381
384
|
*
|
|
382
385
|
* Sync, no Keychain, no network — safe on the `agents run` hot path.
|
|
383
386
|
*/
|
|
@@ -1897,14 +1897,29 @@ function museAuthEmail(value, depth = 0) {
|
|
|
1897
1897
|
* this file is not authoritative, and probing the Keychain would raise an
|
|
1898
1898
|
* authorization sheet per installed version on every `agents run` — the reason
|
|
1899
1899
|
* rotation stopped calling `isClaudeAuthValid` at all. Off macOS the file IS the
|
|
1900
|
-
* only store, so a token-less file is proof of signed-out
|
|
1901
|
-
*
|
|
1900
|
+
* only store, so a missing or token-less file is proof of signed-out — unless a
|
|
1901
|
+
* Linux setup-token (`.claude/.oauth_token`) is present, which the shim exports
|
|
1902
|
+
* as `CLAUDE_CODE_OAUTH_TOKEN` and can authenticate the run without
|
|
1903
|
+
* `.credentials.json`. `platform` is a parameter so both branches are testable
|
|
1904
|
+
* on any host.
|
|
1902
1905
|
*
|
|
1903
1906
|
* Sync, no Keychain, no network — safe on the `agents run` hot path.
|
|
1904
1907
|
*/
|
|
1905
1908
|
export function isClaudeCredentialFileBlank(base, platform = process.platform) {
|
|
1906
1909
|
if (platform === 'darwin')
|
|
1907
1910
|
return false;
|
|
1911
|
+
// A per-version setup-token is a real credential on Linux even when
|
|
1912
|
+
// `.credentials.json` was never written (the shim's `$CLAUDE_CONFIG_DIR/.oauth_token`
|
|
1913
|
+
// fallback). Treat it as signed-in so rotation does not skip a worker that
|
|
1914
|
+
// authenticates from an attached setup-token.
|
|
1915
|
+
try {
|
|
1916
|
+
const token = fs.readFileSync(path.join(base, '.claude', '.oauth_token'), 'utf-8').trim();
|
|
1917
|
+
if (token.length > 0)
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
catch {
|
|
1921
|
+
/* absent — fall through to the credentials.json floor */
|
|
1922
|
+
}
|
|
1908
1923
|
try {
|
|
1909
1924
|
const raw = fs.readFileSync(path.join(base, '.claude', '.credentials.json'), 'utf-8');
|
|
1910
1925
|
const oauth = JSON.parse(raw).claudeAiOauth;
|
|
@@ -1913,11 +1928,14 @@ export function isClaudeCredentialFileBlank(base, platform = process.platform) {
|
|
|
1913
1928
|
const nonEmpty = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
1914
1929
|
return !nonEmpty(oauth.accessToken) && !nonEmpty(oauth.refreshToken);
|
|
1915
1930
|
}
|
|
1916
|
-
catch {
|
|
1917
|
-
//
|
|
1918
|
-
//
|
|
1919
|
-
//
|
|
1920
|
-
//
|
|
1931
|
+
catch (err) {
|
|
1932
|
+
// Off macOS the file IS the store. Missing it means this home cannot
|
|
1933
|
+
// authenticate (a newly installed default with leftover `.claude.json`
|
|
1934
|
+
// oauthAccount is the PHNX-2685 false-healthy case). A corrupt file is
|
|
1935
|
+
// not positive evidence of a blank credential — leave the existing
|
|
1936
|
+
// signal alone rather than declaring a working install signed out.
|
|
1937
|
+
if (err.code === 'ENOENT')
|
|
1938
|
+
return true;
|
|
1921
1939
|
return false;
|
|
1922
1940
|
}
|
|
1923
1941
|
}
|
|
@@ -73,7 +73,10 @@ export function registerMixCommands(parent) {
|
|
|
73
73
|
.option('--days <n>', 'Days of history to include', '7')
|
|
74
74
|
.option('--json', 'Emit JSON instead of tables')
|
|
75
75
|
.action(function summary() {
|
|
76
|
-
|
|
76
|
+
// optsWithGlobals(): --json collides by name with the `insights` parent, so
|
|
77
|
+
// commander binds it to the parent and this.opts() never sees it. Merging
|
|
78
|
+
// ancestor opts is what the per-recipe leaves below already do.
|
|
79
|
+
const o = this.optsWithGlobals();
|
|
77
80
|
renderMixDashboard(parseMixDays(o.days), Boolean(o.json), banner);
|
|
78
81
|
});
|
|
79
82
|
setHelpSections(mix, {
|
|
@@ -104,7 +107,8 @@ export function registerMixCommands(parent) {
|
|
|
104
107
|
parent.command('recipes')
|
|
105
108
|
.description('List baked mix-recipe ids')
|
|
106
109
|
.option('--json', 'Emit JSON')
|
|
107
|
-
.action((
|
|
110
|
+
.action(function recipes() {
|
|
111
|
+
const o = this.optsWithGlobals();
|
|
108
112
|
const list = listRecipes();
|
|
109
113
|
if (o.json) {
|
|
110
114
|
console.log(JSON.stringify(list, null, 2));
|
|
@@ -122,7 +126,8 @@ export function registerMixCommands(parent) {
|
|
|
122
126
|
.option('--days <n>', 'Days of history', '7')
|
|
123
127
|
.option('--limit <n>', 'Max rows', '40')
|
|
124
128
|
.option('--json', 'Emit JSON')
|
|
125
|
-
.action((
|
|
129
|
+
.action(function query() {
|
|
130
|
+
const o = this.optsWithGlobals();
|
|
126
131
|
const win = analyticsWindow(parseMixDays(o.days));
|
|
127
132
|
const kind = o.kind && USAGE_KINDS.includes(o.kind)
|
|
128
133
|
? o.kind
|
|
@@ -165,8 +170,9 @@ export function registerMixCommands(parent) {
|
|
|
165
170
|
.option('--days <n>', 'Days of history', '7')
|
|
166
171
|
.option('--json', 'Emit JSON')
|
|
167
172
|
.action(function recipeAction() {
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
// optsWithGlobals() merges the `insights` parent opts, so the name-colliding
|
|
174
|
+
// --json/--since reach this leaf (see the sibling commands above).
|
|
175
|
+
const o = this.optsWithGlobals();
|
|
170
176
|
const win = analyticsWindow(parseMixDays(o.days));
|
|
171
177
|
const section = runRecipe(id, win);
|
|
172
178
|
if (o.json) {
|
|
@@ -187,7 +193,7 @@ export function registerMixCommands(parent) {
|
|
|
187
193
|
.option('--days <n>', 'Days of history to include', '7')
|
|
188
194
|
.option('--json', 'Emit JSON instead of tables')
|
|
189
195
|
.action(function summary() {
|
|
190
|
-
const o = this.
|
|
196
|
+
const o = this.optsWithGlobals();
|
|
191
197
|
renderMixDashboard(parseMixDays(o.days), Boolean(o.json), banner);
|
|
192
198
|
});
|
|
193
199
|
setHelpSections(trends, {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-class setup-token mint + seed (PHNX-2364).
|
|
3
|
+
*
|
|
4
|
+
* Closes the mint-auth manual recipe: drive `claude setup-token` through the
|
|
5
|
+
* same injectable PTY driver `agents fleet login` uses, capture a well-formed
|
|
6
|
+
* `sk-ant-oat01-…` token (the #1767 ANSI-banner guard), and seed BOTH:
|
|
7
|
+
*
|
|
8
|
+
* 1. a named provider account (`agents accounts add` shape, policy never)
|
|
9
|
+
* 2. the reserved FILE-BASED `auth` bundle keyed per-account email, which
|
|
10
|
+
* usage/probe reads (`resolveClaudeSetupToken`)
|
|
11
|
+
*
|
|
12
|
+
* Native rotating OAuth is never copied. Only this non-rotating class is
|
|
13
|
+
* stored and optionally synced. Interactive mint is Claude-only; every other
|
|
14
|
+
* harness fails loud with the command that actually provisions it.
|
|
15
|
+
*/
|
|
16
|
+
import type { AgentId } from './types.js';
|
|
17
|
+
import { type CredentialAccount } from './account-registry.js';
|
|
18
|
+
import { type DriveOptions, type PtyDriver } from './fleet/remote-login.js';
|
|
19
|
+
/** Well-formed Claude setup-token as it appears inside a TTY blob. */
|
|
20
|
+
export declare const CLAUDE_SETUP_TOKEN_CAPTURE_RE: RegExp;
|
|
21
|
+
export interface MintFlow {
|
|
22
|
+
harness: AgentId;
|
|
23
|
+
provider: string;
|
|
24
|
+
auth: 'setup-token';
|
|
25
|
+
/** Interactive mint argv after HOME=… <bin>. Null when stdin-seed only. */
|
|
26
|
+
mintArgs: string[] | null;
|
|
27
|
+
verificationUrlRegex: RegExp;
|
|
28
|
+
tokenCapture: RegExp;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Harnesses that expose an interactive setup-token mint. Native device-code
|
|
32
|
+
* login (codex/droid/kimi/grok) stays on `agents fleet login`; API keys stay
|
|
33
|
+
* on `agents accounts add`. Adding a harness here without a real mint command
|
|
34
|
+
* is a lying table — do not.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MINT_FLOWS: Record<string, MintFlow>;
|
|
37
|
+
export declare function listMintableHarnesses(): AgentId[];
|
|
38
|
+
export declare function getMintFlow(harnessRaw: string): MintFlow;
|
|
39
|
+
export declare function unmintableMessage(harness: string): string;
|
|
40
|
+
/** Strip CSI / Fe ANSI so a #1767 TTY blob can be scanned for a real token. */
|
|
41
|
+
export declare function stripAnsi(text: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Pull a single well-formed Claude setup-token out of a (possibly ANSI-wrapped)
|
|
44
|
+
* screen. Returns null when none is present. Two distinct tokens fail loud —
|
|
45
|
+
* guessing which one to seed is how a banner fragment becomes an auth header.
|
|
46
|
+
*/
|
|
47
|
+
export declare function extractClaudeSetupToken(screen: string): string | null;
|
|
48
|
+
/** First https URL on the screen, trailing punctuation stripped. */
|
|
49
|
+
export declare function extractMintUrl(screen: string, flow: MintFlow): string | undefined;
|
|
50
|
+
export declare function isEmail(value: string): boolean;
|
|
51
|
+
/** Account-name slug of an email (`ada@example.com` → `ada-at-example.com`). */
|
|
52
|
+
export declare function accountNameFromEmail(email: string): string;
|
|
53
|
+
export declare function assertValidSetupToken(token: string): string;
|
|
54
|
+
export interface ResolveMintIdentityInput {
|
|
55
|
+
account?: string;
|
|
56
|
+
email?: string;
|
|
57
|
+
home?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ResolvedMintIdentity {
|
|
60
|
+
accountName: string;
|
|
61
|
+
email: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the named account + the email that keys the reserved `auth` bundle.
|
|
65
|
+
* `--account` that looks like an email is the email; a name needs `--email` or
|
|
66
|
+
* a locally signed-in `.claude.json`. Missing email fails loud — we must not
|
|
67
|
+
* fall back to a bare shared key (that is the multi-account mix-up).
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveMintIdentity(input: ResolveMintIdentityInput): ResolvedMintIdentity;
|
|
70
|
+
/**
|
|
71
|
+
* Write (or rotate) the reserved FILE-BASED `auth` bundle's per-account key.
|
|
72
|
+
* Usage/probe ignores a keychain- or vault-backed bundle of this name, so a
|
|
73
|
+
* wrong backend fails loud instead of looking like a successful mint.
|
|
74
|
+
*/
|
|
75
|
+
export declare function seedReservedAuthToken(email: string, token: string): {
|
|
76
|
+
key: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Create or rotate the named provider account that `agents run --account` and
|
|
80
|
+
* `agents accounts sync` consume. Existing account of a different kind fails
|
|
81
|
+
* loud rather than silently overwriting an API key with a setup-token.
|
|
82
|
+
*/
|
|
83
|
+
export declare function seedNamedAccount(name: string, token: string, flow: MintFlow): CredentialAccount;
|
|
84
|
+
export interface MintDriveHooks {
|
|
85
|
+
driver?: PtyDriver;
|
|
86
|
+
openUrl?: (url: string) => Promise<void>;
|
|
87
|
+
/** Asked once the authorize URL is on screen, when `--code` was not given. */
|
|
88
|
+
readCode?: () => Promise<string | undefined>;
|
|
89
|
+
drive?: DriveOptions;
|
|
90
|
+
}
|
|
91
|
+
export interface DriveMintResult {
|
|
92
|
+
token: string;
|
|
93
|
+
url?: string;
|
|
94
|
+
sessionId: string;
|
|
95
|
+
}
|
|
96
|
+
export interface DriveSetupTokenMintOpts extends MintDriveHooks {
|
|
97
|
+
code?: string;
|
|
98
|
+
/** Suppress stdout progress so `--json` callers get a parseable blob. */
|
|
99
|
+
json?: boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Drive `claude setup-token` in a PTY: scrape the authorize URL, open it,
|
|
103
|
+
* optionally paste `--code`, then capture the token with the #1767 guard.
|
|
104
|
+
* Tears the session down on the way out (success, timeout, or throw).
|
|
105
|
+
*/
|
|
106
|
+
export declare function driveSetupTokenMint(command: string, flow: MintFlow, opts?: DriveSetupTokenMintOpts): Promise<DriveMintResult>;
|
|
107
|
+
export declare function buildMintCommand(flow: MintFlow, bin: string, home: string): string;
|
|
108
|
+
export declare function resolveMintInstallation(harness: AgentId): {
|
|
109
|
+
version: string;
|
|
110
|
+
bin: string;
|
|
111
|
+
home: string;
|
|
112
|
+
};
|
|
113
|
+
export interface MintAndSeedInput {
|
|
114
|
+
harness: string;
|
|
115
|
+
account?: string;
|
|
116
|
+
email?: string;
|
|
117
|
+
token?: string;
|
|
118
|
+
code?: string;
|
|
119
|
+
open?: boolean;
|
|
120
|
+
fleet?: boolean;
|
|
121
|
+
devices?: string[];
|
|
122
|
+
/** Suppress progress prints so `--json` stdout stays machine-parseable. */
|
|
123
|
+
json?: boolean;
|
|
124
|
+
hooks?: MintDriveHooks;
|
|
125
|
+
}
|
|
126
|
+
export interface FleetSyncRow {
|
|
127
|
+
device: string;
|
|
128
|
+
ok: boolean;
|
|
129
|
+
message: string;
|
|
130
|
+
}
|
|
131
|
+
export interface MintAndSeedResult {
|
|
132
|
+
harness: AgentId;
|
|
133
|
+
account: string;
|
|
134
|
+
email: string;
|
|
135
|
+
authBundleKey: string;
|
|
136
|
+
rotated: boolean;
|
|
137
|
+
fleet: FleetSyncRow[];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* End-to-end mint: resolve identity, obtain a token (stdin or PTY drive),
|
|
141
|
+
* seed the named account + reserved auth bundle, optionally sync the fleet.
|
|
142
|
+
* Never returns or logs the token.
|
|
143
|
+
*/
|
|
144
|
+
export declare function mintAndSeed(input: MintAndSeedInput): Promise<MintAndSeedResult>;
|
|
145
|
+
export declare function resolveSyncTargets(fleet: boolean, devices: string[]): Promise<string[]>;
|
|
146
|
+
/** True when a Claude setup-token is already seeded on this box (setup status). */
|
|
147
|
+
export declare function hasMintedSetupToken(): {
|
|
148
|
+
ready: boolean;
|
|
149
|
+
detail: string;
|
|
150
|
+
};
|