@phnx-labs/agents-cli 1.22.116 → 1.22.118
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 +247 -0
- package/dist/bootstrap.js +4 -11
- package/dist/commands/browser.d.ts +52 -70
- package/dist/commands/browser.js +192 -3562
- package/dist/commands/daemon.d.ts +39 -2
- package/dist/commands/daemon.js +72 -73
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +104 -12
- package/dist/commands/repo.js +13 -49
- package/dist/commands/route.js +5 -5
- package/dist/commands/routines.js +39 -12
- package/dist/commands/sessions-render.d.ts +1 -1
- package/dist/commands/sessions-render.js +0 -1
- package/dist/commands/sessions.test-fixture.d.ts +0 -1
- package/dist/commands/sessions.test-fixture.js +0 -37
- package/dist/commands/setup-browser.d.ts +17 -12
- package/dist/commands/setup-browser.js +52 -87
- package/dist/commands/setup-preferences.d.ts +0 -17
- package/dist/commands/setup-preferences.js +18 -49
- package/dist/commands/setup-term.d.ts +4 -3
- package/dist/commands/setup-term.js +5 -4
- package/dist/commands/setup.js +11 -11
- package/dist/commands/ssh.js +10 -1
- package/dist/commands/sync.js +6 -44
- package/dist/commands/teams.js +0 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +34 -6
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +47 -2
- package/dist/index.js +107 -26
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-catalog.d.ts +38 -0
- package/dist/lib/account-catalog.js +110 -26
- package/dist/lib/account-provider-registry.js +1 -1
- package/dist/lib/account-registry.d.ts +1 -1
- package/dist/lib/account-registry.js +36 -2
- package/dist/lib/accounting/rotate.js +22 -8
- package/dist/lib/accounting/usage-ingest.d.ts +6 -1
- package/dist/lib/accounting/usage-ingest.js +115 -45
- package/dist/lib/accounting/usage-sync.d.ts +134 -31
- package/dist/lib/accounting/usage-sync.js +227 -66
- package/dist/lib/accounting/usage.d.ts +16 -3
- package/dist/lib/accounting/usage.js +26 -8
- package/dist/lib/accounts/slots.d.ts +8 -0
- package/dist/lib/accounts/slots.js +17 -0
- package/dist/lib/add-dir.js +0 -1
- package/dist/lib/agent-cli-commands.js +0 -1
- package/dist/lib/agent-spec/agents.d.ts +6 -7
- package/dist/lib/agent-spec/agents.js +6 -60
- package/dist/lib/auth-health.d.ts +69 -1
- package/dist/lib/auth-health.js +133 -4
- package/dist/lib/browser/context.d.ts +82 -0
- package/dist/lib/browser/context.js +88 -0
- package/dist/lib/browser/paths.d.ts +25 -0
- package/dist/lib/browser/paths.js +51 -0
- package/dist/lib/browser/record.d.ts +38 -0
- package/dist/lib/browser/record.js +57 -0
- package/dist/lib/browser/sessions-list.js +1 -2
- package/dist/lib/browser-client.d.ts +158 -0
- package/dist/lib/browser-client.js +234 -0
- package/dist/lib/channels/owner-sink.d.ts +1 -18
- package/dist/lib/channels/owner-sink.js +29 -82
- package/dist/lib/channels/providers/rush.d.ts +7 -7
- package/dist/lib/channels/providers/rush.js +127 -57
- package/dist/lib/claude-statusline.d.ts +1 -1
- package/dist/lib/claude-statusline.js +15 -3
- package/dist/lib/commands.js +1 -1
- package/dist/lib/crabbox/lease.js +1 -1
- package/dist/lib/crabbox/runtimes.js +1 -3
- package/dist/lib/daemon/account-state-daemon-service.js +1 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
- package/dist/lib/daemon/auth-sync-service.js +37 -43
- package/dist/lib/daemon/daemon.d.ts +25 -14
- package/dist/lib/daemon/daemon.js +92 -154
- package/dist/lib/daemon/runner.d.ts +2 -2
- package/dist/lib/daemon/runner.js +32 -9
- package/dist/lib/daemon/self-update-service.d.ts +35 -34
- package/dist/lib/daemon/self-update-service.js +40 -36
- package/dist/lib/daemon/service.d.ts +12 -8
- package/dist/lib/daemon/supervisor.d.ts +65 -54
- package/dist/lib/daemon/supervisor.js +136 -171
- package/dist/lib/daemon/testdata/process-view-start.js +5 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
- package/dist/lib/daemon/usage-sync-service.js +46 -78
- package/dist/lib/daemon-health.d.ts +22 -5
- package/dist/lib/daemon-health.js +47 -3
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +0 -10
- package/dist/lib/daemon-ticks.js +4 -2
- package/dist/lib/devices/doctor-findings.js +8 -11
- package/dist/lib/devices/harness-inventory.js +14 -5
- package/dist/lib/exec-account-home.js +5 -0
- package/dist/lib/exec.js +0 -11
- package/dist/lib/feed/events.js +0 -1
- package/dist/lib/feed/tool-activity.js +5 -2
- package/dist/lib/fleet-shared-state.d.ts +45 -0
- package/dist/lib/fleet-shared-state.js +78 -9
- package/dist/lib/fs-atomic.js +25 -0
- package/dist/lib/harness-auth-capabilities.js +0 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hooks/cache.d.ts +7 -0
- package/dist/lib/hooks/cache.js +27 -2
- package/dist/lib/hooks/install.js +7 -1
- package/dist/lib/hosts/ready.d.ts +38 -8
- package/dist/lib/hosts/ready.js +62 -14
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/installations/active-check.d.ts +27 -0
- package/dist/lib/installations/active-check.js +61 -0
- package/dist/lib/installations/migrate.js +6 -3
- package/dist/lib/installations/shims.js +9 -7
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +12 -1
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/models.js +0 -2
- package/dist/lib/open-url.js +28 -54
- package/dist/lib/placement.d.ts +2 -0
- package/dist/lib/placement.js +5 -0
- package/dist/lib/profiles.js +0 -2
- package/dist/lib/refresh.js +1 -1
- package/dist/lib/routine-readiness.d.ts +35 -0
- package/dist/lib/routine-readiness.js +74 -16
- package/dist/lib/rules/compile.js +6 -0
- package/dist/lib/sandbox.js +0 -2
- package/dist/lib/secrets-policy.d.ts +89 -11
- package/dist/lib/secrets-policy.js +175 -28
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +65 -18
- package/dist/lib/session/discover.js +1 -283
- package/dist/lib/session/mirror.d.ts +4 -3
- package/dist/lib/session/mirror.js +4 -3
- package/dist/lib/session/parse.d.ts +0 -2
- package/dist/lib/session/parse.js +1 -138
- package/dist/lib/session/throughput.d.ts +1 -3
- package/dist/lib/session/throughput.js +0 -20
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +2 -2
- package/dist/lib/sessions-client.d.ts +60 -1
- package/dist/lib/sessions-client.js +227 -4
- package/dist/lib/setup-tool-status.js +8 -12
- package/dist/lib/signin-badge.d.ts +3 -2
- package/dist/lib/signin-badge.js +6 -3
- package/dist/lib/smart-launch.js +4 -1
- package/dist/lib/staleness/detectors/commands.d.ts +1 -1
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
- package/dist/lib/staleness/detectors/rules.d.ts +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +1 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.d.ts +1 -1
- package/dist/lib/staleness/writers/hooks.d.ts +1 -1
- package/dist/lib/staleness/writers/mcp.d.ts +1 -1
- package/dist/lib/staleness/writers/permissions.d.ts +1 -1
- package/dist/lib/staleness/writers/plugins.d.ts +1 -1
- package/dist/lib/staleness/writers/rules.d.ts +1 -1
- package/dist/lib/staleness/writers/skills.d.ts +1 -1
- package/dist/lib/staleness/writers/sources.js +5 -0
- package/dist/lib/staleness/writers/subagents.d.ts +1 -1
- package/dist/lib/staleness/writers/workflows.d.ts +1 -1
- package/dist/lib/state.d.ts +6 -4
- package/dist/lib/state.js +7 -17
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +1 -136
- package/dist/lib/teams/placement-probe.js +50 -15
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/types.d.ts +10 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/view-types.d.ts +32 -0
- package/dist/lib/watchdog/read.js +0 -1
- package/dist/lib/watchdog/rotate.js +1 -2
- package/dist/lib/watchdog/runner.js +2 -2
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdogTail.js +0 -31
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -8
- package/dist/browser.d.ts +0 -2
- package/dist/browser.js +0 -17
- package/dist/commands/browser-picker.d.ts +0 -18
- package/dist/commands/browser-picker.js +0 -97
- package/dist/lib/browser/arc-discovery.d.ts +0 -52
- package/dist/lib/browser/arc-discovery.js +0 -197
- package/dist/lib/browser/arc-dom.d.ts +0 -14
- package/dist/lib/browser/arc-dom.js +0 -121
- package/dist/lib/browser/caller-identity.d.ts +0 -40
- package/dist/lib/browser/caller-identity.js +0 -175
- package/dist/lib/browser/cdp.d.ts +0 -51
- package/dist/lib/browser/cdp.js +0 -254
- package/dist/lib/browser/chrome.d.ts +0 -141
- package/dist/lib/browser/chrome.js +0 -704
- package/dist/lib/browser/chromium-discovery.d.ts +0 -28
- package/dist/lib/browser/chromium-discovery.js +0 -96
- package/dist/lib/browser/devices.d.ts +0 -34
- package/dist/lib/browser/devices.js +0 -61
- package/dist/lib/browser/domain-skills.d.ts +0 -71
- package/dist/lib/browser/domain-skills.js +0 -195
- package/dist/lib/browser/drivers/arc.d.ts +0 -40
- package/dist/lib/browser/drivers/arc.js +0 -276
- package/dist/lib/browser/drivers/firefox.d.ts +0 -99
- package/dist/lib/browser/drivers/firefox.js +0 -377
- package/dist/lib/browser/drivers/local.d.ts +0 -55
- package/dist/lib/browser/drivers/local.js +0 -266
- package/dist/lib/browser/drivers/ssh.d.ts +0 -120
- package/dist/lib/browser/drivers/ssh.js +0 -467
- package/dist/lib/browser/editor.d.ts +0 -3
- package/dist/lib/browser/editor.js +0 -50
- package/dist/lib/browser/ffmpeg.d.ts +0 -12
- package/dist/lib/browser/ffmpeg.js +0 -184
- package/dist/lib/browser/firefox-discovery.d.ts +0 -69
- package/dist/lib/browser/firefox-discovery.js +0 -162
- package/dist/lib/browser/har.d.ts +0 -85
- package/dist/lib/browser/har.js +0 -77
- package/dist/lib/browser/hygiene.d.ts +0 -97
- package/dist/lib/browser/hygiene.js +0 -153
- package/dist/lib/browser/index.d.ts +0 -5
- package/dist/lib/browser/index.js +0 -5
- package/dist/lib/browser/input.d.ts +0 -7
- package/dist/lib/browser/input.js +0 -92
- package/dist/lib/browser/ipc.d.ts +0 -204
- package/dist/lib/browser/ipc.js +0 -1453
- package/dist/lib/browser/login-detection.d.ts +0 -87
- package/dist/lib/browser/login-detection.js +0 -274
- package/dist/lib/browser/profiles.d.ts +0 -422
- package/dist/lib/browser/profiles.js +0 -1157
- package/dist/lib/browser/refs.d.ts +0 -89
- package/dist/lib/browser/refs.js +0 -191
- package/dist/lib/browser/registry.d.ts +0 -70
- package/dist/lib/browser/registry.js +0 -252
- package/dist/lib/browser/remote-control.d.ts +0 -59
- package/dist/lib/browser/remote-control.js +0 -85
- package/dist/lib/browser/resolve-target.d.ts +0 -66
- package/dist/lib/browser/resolve-target.js +0 -264
- package/dist/lib/browser/runtime-state.d.ts +0 -291
- package/dist/lib/browser/runtime-state.js +0 -584
- package/dist/lib/browser/secret-ref.d.ts +0 -10
- package/dist/lib/browser/secret-ref.js +0 -14
- package/dist/lib/browser/service.d.ts +0 -812
- package/dist/lib/browser/service.js +0 -4693
- package/dist/lib/browser/stream.d.ts +0 -17
- package/dist/lib/browser/stream.js +0 -72
- package/dist/lib/browser/task-index.d.ts +0 -78
- package/dist/lib/browser/task-index.js +0 -199
- package/dist/lib/browser/types.d.ts +0 -647
- package/dist/lib/browser/types.js +0 -61
- package/dist/lib/browser/upload.d.ts +0 -29
- package/dist/lib/browser/upload.js +0 -298
- package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
- package/dist/lib/daemon/browser-ipc-service.js +0 -46
- package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
- package/dist/lib/daemon/browser-task-reap-service.js +0 -26
- package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
- package/dist/lib/fleet-shared-repo-sync.js +0 -490
package/dist/commands/view.js
CHANGED
|
@@ -39,7 +39,7 @@ import { selectUpdateStrategy } from '../lib/installations/strategies.js';
|
|
|
39
39
|
import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
|
|
40
40
|
import { getCentralRulesFileName } from '../lib/rules/rules.js';
|
|
41
41
|
import { composeRulesFromState } from '../lib/rules/compose.js';
|
|
42
|
-
import { getConfiguredRunStrategy, isLaunchableSignedIn } from '../lib/accounting/rotate.js';
|
|
42
|
+
import { getConfiguredRunStrategy, isLaunchableSignedIn, collectRunCandidates, readinessFromCandidate } from '../lib/accounting/rotate.js';
|
|
43
43
|
import { resolveRunDefaults } from '../lib/run-defaults.js';
|
|
44
44
|
import { resolveConfiguredModel } from '../lib/models.js';
|
|
45
45
|
import { listProfiles, profileExists, profileSummary, readProfile } from '../lib/profiles.js';
|
|
@@ -1272,6 +1272,36 @@ export function parseResourceSections(options, jsonMode) {
|
|
|
1272
1272
|
}
|
|
1273
1273
|
return set;
|
|
1274
1274
|
}
|
|
1275
|
+
/**
|
|
1276
|
+
* The one run-readiness gate for one agent on THIS box (PHNX-4116). Runs the
|
|
1277
|
+
* SAME enumeration the local router picks from — `collectRunCandidates` (native
|
|
1278
|
+
* slots + version homes) through `readinessFromCandidate` — so the answer
|
|
1279
|
+
* `agents view --json` publishes is exactly what a `run` here would find, and a
|
|
1280
|
+
* remote `--device auto` dispatcher can read it instead of re-deriving freshness
|
|
1281
|
+
* on its own box (which drifted: view.ts lists version homes, the router lists
|
|
1282
|
+
* slots). Returns undefined when the agent has no candidates at all (nothing
|
|
1283
|
+
* installed), so the field is simply absent rather than a misleading verdict.
|
|
1284
|
+
*/
|
|
1285
|
+
export async function computeAgentRunReady(agentId) {
|
|
1286
|
+
const candidates = await collectRunCandidates(agentId);
|
|
1287
|
+
if (candidates.length === 0)
|
|
1288
|
+
return undefined;
|
|
1289
|
+
const accounts = candidates.map((c) => {
|
|
1290
|
+
const readiness = readinessFromCandidate(c);
|
|
1291
|
+
const name = c.nativeAccount || c.accountLabel || c.email || c.version;
|
|
1292
|
+
return { name, ready: readiness.ready, reason: readiness.ready ? 'ready' : readiness.reason };
|
|
1293
|
+
});
|
|
1294
|
+
const readyAccount = accounts.find((a) => a.ready);
|
|
1295
|
+
let reason;
|
|
1296
|
+
if (readyAccount) {
|
|
1297
|
+
reason = `ready (${readyAccount.name})`;
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
const reasons = [...new Set(accounts.map((a) => a.reason))];
|
|
1301
|
+
reason = reasons.length === 1 ? `all ${reasons[0]}` : reasons.join(', ');
|
|
1302
|
+
}
|
|
1303
|
+
return { ready: !!readyAccount, reason, accounts };
|
|
1304
|
+
}
|
|
1275
1305
|
/**
|
|
1276
1306
|
* Collect structured info for one or more agents without rendering to the
|
|
1277
1307
|
* terminal. Used by `--json` output and any programmatic consumer (e.g. the
|
|
@@ -1395,6 +1425,15 @@ export async function collectAgentsJson(filterAgentId, resourceSections, opts) {
|
|
|
1395
1425
|
const note = secretsUnavailableNote(catalog);
|
|
1396
1426
|
if (note)
|
|
1397
1427
|
console.error(chalk.yellow(note)); // stderr — never corrupts --json stdout
|
|
1428
|
+
// The one run-readiness gate per agent, computed off the router's own
|
|
1429
|
+
// enumeration (PHNX-4116). Runs in parallel across agents; a harness with
|
|
1430
|
+
// nothing installed yields undefined and the field is omitted.
|
|
1431
|
+
const runReadyByAgent = new Map();
|
|
1432
|
+
await Promise.all(agentsToShow.map(async (agentId) => {
|
|
1433
|
+
const runReady = await computeAgentRunReady(agentId);
|
|
1434
|
+
if (runReady)
|
|
1435
|
+
runReadyByAgent.set(agentId, runReady);
|
|
1436
|
+
}));
|
|
1398
1437
|
const out = [];
|
|
1399
1438
|
for (const agentId of agentsToShow) {
|
|
1400
1439
|
const versions = byAgent.get(agentId) ?? [];
|
|
@@ -1406,7 +1445,13 @@ export async function collectAgentsJson(filterAgentId, resourceSections, opts) {
|
|
|
1406
1445
|
// Project through the public JSON v2 serializer — the internal catalog row
|
|
1407
1446
|
// (identityKey, home, installations) is not the machine contract.
|
|
1408
1447
|
const accounts = accountListJson(catalog.native.filter((row) => row.agent === agentId), catalog.provider.filter((row) => row.harnesses.includes(agentId)), agentId).accounts;
|
|
1409
|
-
out.push({
|
|
1448
|
+
out.push({
|
|
1449
|
+
agent: agentId,
|
|
1450
|
+
versions,
|
|
1451
|
+
accounts,
|
|
1452
|
+
runReady: runReadyByAgent.get(agentId),
|
|
1453
|
+
harnesses: harnesses.filter((h) => h.agent === agentId),
|
|
1454
|
+
});
|
|
1410
1455
|
}
|
|
1411
1456
|
return out;
|
|
1412
1457
|
}
|
package/dist/index.js
CHANGED
|
@@ -78,10 +78,12 @@ if (process.argv[2] === '__claude-statusline') {
|
|
|
78
78
|
const { runClaudeStatusLine } = await import('./lib/claude-statusline.js');
|
|
79
79
|
process.exit(await runClaudeStatusLine());
|
|
80
80
|
}
|
|
81
|
-
// Fleet usage-sync receiver: a headed peer pipes its
|
|
82
|
-
// stdin
|
|
83
|
-
//
|
|
84
|
-
//
|
|
81
|
+
// Fleet usage-sync receiver: a headed peer pipes its daemon-state envelope to
|
|
82
|
+
// stdin; we store it as that peer's file, merge its usage rows newest-wins into
|
|
83
|
+
// this box's cache, and with `--reply` print our own envelope back (PHNX-3392,
|
|
84
|
+
// PHNX-4116). Above the bootstrap line for the same reason as
|
|
85
|
+
// __claude-statusline — no update check, no detached sync fork, and without
|
|
86
|
+
// `--reply` nothing writes to stdout to corrupt the ready probe's view.
|
|
85
87
|
if (process.argv[2] === '__usage-ingest') {
|
|
86
88
|
const { runUsageIngest } = await import('./lib/accounting/usage-ingest.js');
|
|
87
89
|
process.exit(await runUsageIngest());
|
|
@@ -110,9 +112,10 @@ if (process.argv[2] === '__daemon-run') {
|
|
|
110
112
|
// stack to whatever the service manager had wired to stdout and never reaching
|
|
111
113
|
// the daemon's own structured log. Route both into log() so the failure is in
|
|
112
114
|
// logs.jsonl where `agents daemon logs` reads it, then exit non-zero and
|
|
113
|
-
// DELIBERATELY let the supervisor restart us —
|
|
114
|
-
// ThrottleInterval / the unit's
|
|
115
|
-
//
|
|
115
|
+
// DELIBERATELY let the supervisor restart us — paced by the plist's
|
|
116
|
+
// ThrottleInterval / the unit's RestartSec, and never abandoned
|
|
117
|
+
// (StartLimitIntervalSec=0, PHNX-4116). Swallowing here would be the worse
|
|
118
|
+
// failure: a daemon left alive with a dead subsystem.
|
|
116
119
|
const crash = (kind) => (err) => {
|
|
117
120
|
const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
118
121
|
try {
|
|
@@ -137,28 +140,102 @@ if (process.argv[2] === '__daemon-run') {
|
|
|
137
140
|
// Lifecycle verbs (resume/stop/inject/watch/--active/--markdown) fall through.
|
|
138
141
|
if (process.argv[2] === 'sessions') {
|
|
139
142
|
const forwarded = process.argv.slice(3);
|
|
140
|
-
const { isReadQuery, resolveSessionsBin, invocation, SessionsClientError, SESSIONS_INSTALL_HINT, } = await import('./lib/sessions-client.js');
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
143
|
+
const { isReadQuery, usesFilterFlags, usesHostFlag, sessionsBinSupportsFilters, sessionsBinSupportsHost, planDeviceHostRead, resolveSessionsBin, invocation, SessionsClientError, SESSIONS_INSTALL_HINT, } = await import('./lib/sessions-client.js');
|
|
144
|
+
// Resolve the bin up front (a cheap PATH lookup): its presence decides whether
|
|
145
|
+
// we can route at all, and its VERSION decides whether the 0.2.0 filter flags
|
|
146
|
+
// (`--project`/`--since`/`--until`/`--sort`, `@version`, the shorthands) and the
|
|
147
|
+
// 0.2.1 `--host <target>` point-to-one remote read flag are recognized — an
|
|
148
|
+
// older `sessions` would mis-read them as FTS tokens, so those queries stay on
|
|
149
|
+
// the in-repo engine (which implements the same filters and the `--device`
|
|
150
|
+
// fan-out). The two floors are checked independently: a 0.2.0 binary takes the
|
|
151
|
+
// filters but not `--host`.
|
|
152
|
+
let bin = null;
|
|
153
|
+
try {
|
|
154
|
+
bin = resolveSessionsBin();
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
// No standalone on this box: fall through to the in-repo engine below rather
|
|
158
|
+
// than refusing the query. The fast path is an optimization for a box that
|
|
159
|
+
// has `sessions` installed (skips bootstrap and the sessions module); a box
|
|
160
|
+
// without it must still answer `agents sessions --json` exactly as before.
|
|
161
|
+
if (!(err instanceof SessionsClientError && err.code === 'SESSIONS_BIN_MISSING'))
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
const filters = bin !== null && usesFilterFlags(forwarded) ? sessionsBinSupportsFilters(bin) : false;
|
|
165
|
+
const host = bin !== null && usesHostFlag(forwarded) ? sessionsBinSupportsHost(bin) : false;
|
|
166
|
+
// PHNX-4012 local-orchestration collapse: a read query carrying `--device
|
|
167
|
+
// <name>` (and no explicit `--host`) routes through the standalone's
|
|
168
|
+
// point-to-one remote read — `sessions <read args> --host ssh://<resolved>` —
|
|
169
|
+
// instead of the in-repo `--device` peer fan-out, WHEN this box's standalone
|
|
170
|
+
// supports `--host` (>=0.2.1). `planDeviceHostRead` is pure and probes nothing,
|
|
171
|
+
// so a non-device read never pays a `sessions --version` spawn (the probe fires
|
|
172
|
+
// only after a device read is detected). If the PEER lacks the standalone the
|
|
173
|
+
// ssh `bash -lc` command-not-found surfaces as exit 127 — a CAPABILITY gate
|
|
174
|
+
// (same spirit as #3687's version gate, keyed on a specific documented signal),
|
|
175
|
+
// on which we FALL THROUGH to the in-repo `--device` fan-out below so the read
|
|
176
|
+
// still succeeds. Everything else (e.g. 255 unreachable) propagates verbatim.
|
|
177
|
+
// The in-repo fan-out is deliberately KEPT here as the path for un-upgraded
|
|
178
|
+
// peers; its eventual removal is a follow-up gated on fleet-wide 0.2.1 (a
|
|
179
|
+
// separate PHNX-4012 follow-up, see the PR body), not this PR.
|
|
180
|
+
if (bin !== null) {
|
|
181
|
+
const deviceRead = planDeviceHostRead(forwarded, { filters });
|
|
182
|
+
if (deviceRead && sessionsBinSupportsHost(bin)) {
|
|
183
|
+
let target = null;
|
|
184
|
+
try {
|
|
185
|
+
const { resolveRemoteDevice } = await import('./lib/ssh-tunnel.js');
|
|
186
|
+
target = (await resolveRemoteDevice(deviceRead.device, {})).target;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Unknown/unresolvable device — fall through to the in-repo path, which
|
|
190
|
+
// resolves `--device` against the same fleet registry and emits the
|
|
191
|
+
// canonical error; do not duplicate that error here.
|
|
192
|
+
target = null;
|
|
193
|
+
}
|
|
194
|
+
if (target !== null) {
|
|
195
|
+
const { spawnSync } = await import('node:child_process');
|
|
196
|
+
const { command, prefix } = invocation(bin);
|
|
197
|
+
const hostArgs = [...deviceRead.readArgs, '--host', `ssh://${target}`];
|
|
198
|
+
// Capture (not `stdio: 'inherit'`) so a 127 capability miss can fall
|
|
199
|
+
// through silently instead of leaking the peer's command-not-found before
|
|
200
|
+
// the in-repo read re-answers. Reads are bounded, so buffering is fine;
|
|
201
|
+
// force color + geometry through the capture when the caller is at a real
|
|
202
|
+
// terminal and not asking for `--json`, matching the passthrough renderer.
|
|
203
|
+
const childEnv = { ...process.env };
|
|
204
|
+
if (process.stdout.isTTY && !hostArgs.includes('--json')) {
|
|
205
|
+
if (childEnv.FORCE_COLOR === undefined)
|
|
206
|
+
childEnv.FORCE_COLOR = '1';
|
|
207
|
+
if (process.stdout.columns)
|
|
208
|
+
childEnv.COLUMNS = String(process.stdout.columns);
|
|
209
|
+
if (process.stdout.rows)
|
|
210
|
+
childEnv.LINES = String(process.stdout.rows);
|
|
211
|
+
}
|
|
212
|
+
const res = spawnSync(command, [...prefix, ...hostArgs], {
|
|
213
|
+
encoding: 'utf8',
|
|
214
|
+
env: childEnv,
|
|
215
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
216
|
+
});
|
|
217
|
+
if (res.error) {
|
|
218
|
+
process.stderr.write(`Failed to run \`sessions\`: ${res.error.message}\n`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
// Exit 127 = the peer has no standalone `sessions` on PATH (ssh/`bash
|
|
222
|
+
// -lc` command-not-found). Fall through to the in-repo fan-out rather
|
|
223
|
+
// than surfacing a broken read.
|
|
224
|
+
if (res.status !== 127) {
|
|
225
|
+
if (res.stdout)
|
|
226
|
+
process.stdout.write(res.stdout);
|
|
227
|
+
if (res.stderr)
|
|
228
|
+
process.stderr.write(res.stderr);
|
|
229
|
+
process.exit(res.status ?? 1);
|
|
230
|
+
}
|
|
231
|
+
// 127: fall through. `forwarded` still carries `--device`, so the
|
|
232
|
+
// `isReadQuery` check below is false and bootstrap's in-repo path answers.
|
|
159
233
|
}
|
|
160
234
|
}
|
|
235
|
+
}
|
|
236
|
+
if (isReadQuery(forwarded, { filters, host })) {
|
|
161
237
|
if (bin) {
|
|
238
|
+
const { spawnSync } = await import('node:child_process');
|
|
162
239
|
const { command, prefix } = invocation(bin);
|
|
163
240
|
const res = spawnSync(command, [...prefix, ...forwarded], { stdio: 'inherit' });
|
|
164
241
|
if (res.error) {
|
|
@@ -167,6 +244,10 @@ if (process.argv[2] === 'sessions') {
|
|
|
167
244
|
}
|
|
168
245
|
process.exit(res.status ?? 1);
|
|
169
246
|
}
|
|
247
|
+
// A read query on a box with no standalone: hint once, then fall through.
|
|
248
|
+
if (process.env.AGENTS_SESSIONS_FASTPATH_HINT !== '0') {
|
|
249
|
+
process.stderr.write(`agents: standalone \`sessions\` not installed; using the in-process engine (${SESSIONS_INSTALL_HINT})\n`);
|
|
250
|
+
}
|
|
170
251
|
}
|
|
171
252
|
}
|
|
172
253
|
// Full CLI: commander tree, update checks, migrations, parse. Static imports
|
|
@@ -30,8 +30,6 @@ export const NATIVE_ACCOUNT_CAPABILITIES = {
|
|
|
30
30
|
antigravity: { inspection: 'opaque', scope: 'device', status: 'unsupported' },
|
|
31
31
|
droid: { inspection: 'opaque', scope: 'device', status: 'unsupported' },
|
|
32
32
|
opencode: { inspection: 'opaque', scope: 'device', status: 'unsupported' },
|
|
33
|
-
// Discoverable in the list, but not nameable.
|
|
34
|
-
gemini: { inspection: 'email', scope: 'unsupported', status: 'discovery-only' },
|
|
35
33
|
copilot: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
36
34
|
openclaw: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
37
35
|
amp: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
@@ -90,6 +90,19 @@ export interface NativeAccountCatalogRow {
|
|
|
90
90
|
usageSnapshot?: UsageSnapshot | null;
|
|
91
91
|
/** Raw usage fetch error, for headless/unverified labeling alongside the bars. */
|
|
92
92
|
usageError?: string | null;
|
|
93
|
+
/**
|
|
94
|
+
* The token FACT for this account ON THIS BOX (PHNX-4116): the credential kind
|
|
95
|
+
* plus its file date — `sk-ant-oat01 (Sep 16)` for a Claude slot's
|
|
96
|
+
* `.claude/.oauth_token`, `api key (present)` for a provider-key harness — or
|
|
97
|
+
* `no token` when the credential file is absent. A fact, never a verdict word.
|
|
98
|
+
*/
|
|
99
|
+
token: string;
|
|
100
|
+
/**
|
|
101
|
+
* The auth FACT for this account ON THIS BOX (PHNX-4116): `last used ok 12m
|
|
102
|
+
* ago` / `last auth failure 401 Sep 20 14:02` / `rate-limited until 15:00` /
|
|
103
|
+
* `not used on this box yet`, from the recorded run outcome (or a real probe).
|
|
104
|
+
*/
|
|
105
|
+
lastAuth: string;
|
|
93
106
|
fix: string | null;
|
|
94
107
|
}
|
|
95
108
|
export interface AccountDeviceVerdict {
|
|
@@ -155,6 +168,17 @@ export interface AccountListEntryJson {
|
|
|
155
168
|
/** The live usage snapshot backing `usage`, when available — additive, not breaking. */
|
|
156
169
|
usageSnapshot?: UsageSnapshot | null;
|
|
157
170
|
usageError?: string | null;
|
|
171
|
+
/**
|
|
172
|
+
* The token FACT for this account on the box that emitted this JSON (PHNX-4116):
|
|
173
|
+
* `sk-ant-oat01 (Sep 16)` / `api key (present)` / `no token`. A fact, not a verdict.
|
|
174
|
+
*/
|
|
175
|
+
token: string;
|
|
176
|
+
/**
|
|
177
|
+
* The auth FACT for this account on the box that emitted this JSON (PHNX-4116):
|
|
178
|
+
* `last used ok 12m ago` / `last auth failure 401 Sep 20 14:02` / `not used on
|
|
179
|
+
* this box yet`. Reflects the emitting box only; `devices[]` carries the fleet.
|
|
180
|
+
*/
|
|
181
|
+
lastAuth: string;
|
|
158
182
|
fix: string | null;
|
|
159
183
|
}
|
|
160
184
|
interface AccountListJson {
|
|
@@ -184,6 +208,14 @@ type CatalogMeta = Pick<Meta, 'accounts' | 'deviceAccounts'>;
|
|
|
184
208
|
* silently dropped from the list nor shown as connected.
|
|
185
209
|
*/
|
|
186
210
|
export declare function buildNativeCatalog(rows: NativeHomeRow[], meta: CatalogMeta, globalDefault?: (agent: AgentId) => string | null): NativeAccountCatalogRow[];
|
|
211
|
+
/**
|
|
212
|
+
* The token FACT for a Claude account ON THIS BOX (PHNX-4116): the slot/home
|
|
213
|
+
* `.claude/.oauth_token` scheme prefix + its file date, else a present native
|
|
214
|
+
* credential, else `no token`. For non-Claude native rows it reports credential
|
|
215
|
+
* presence generically. `dir` is the slot dir when the account has one, else the
|
|
216
|
+
* local version home. A fact, never a verdict.
|
|
217
|
+
*/
|
|
218
|
+
export declare function readTokenFact(agent: AgentId, dir: string | null): string;
|
|
187
219
|
export declare function toProviderRow(account: CredentialAccount, meta: Pick<Meta, 'accounts'>): ProviderAccountCatalogRow;
|
|
188
220
|
/**
|
|
189
221
|
* Wire the real collectors to the pure builder. This is the canonical
|
|
@@ -215,6 +247,12 @@ export declare const OVERVIEW_MAX_USAGE_WINDOWS = 2;
|
|
|
215
247
|
* cannot drift.
|
|
216
248
|
*/
|
|
217
249
|
export declare const ACCOUNT_LISTING_LEGEND = "* stale usage \u00B7 a healthy account carries no state \u00B7 identity + per-box state: agents accounts list --fleet";
|
|
250
|
+
/**
|
|
251
|
+
* Colour the auth FACT by what it states (PHNX-4116): a recent success is green,
|
|
252
|
+
* a server rejection red, a throttle yellow, and "not used on this box yet" the
|
|
253
|
+
* neutral gray of a fact that carries no alarm.
|
|
254
|
+
*/
|
|
255
|
+
export declare function authFactNote(lastAuth: string): string;
|
|
218
256
|
/** Shared text renderer used by both `accounts list` and account-first `view`. */
|
|
219
257
|
export declare function renderAccountRows(rows: NativeAccountCatalogRow[], opts: {
|
|
220
258
|
heading?: boolean;
|
|
@@ -8,7 +8,9 @@ import { readSlots } from './accounts/slots.js';
|
|
|
8
8
|
import { harnessWorkerIsPerDevice } from './harness-auth-capabilities.js';
|
|
9
9
|
import { hasKeychainTokenSync, isSecretsTransportError } from './secrets-client.js';
|
|
10
10
|
import { isLaunchableSignedIn as isCredentialLaunchable } from './accounting/rotate.js';
|
|
11
|
-
import
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { authCacheKey, formatAuthFact, readAuthHealthCache, slotAuthVersionKey } from './auth-health.js';
|
|
12
14
|
import { readFleetSharedDeviceStates } from './fleet-shared-state.js';
|
|
13
15
|
import { machineId } from './machine-id.js';
|
|
14
16
|
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from './device-config.js';
|
|
@@ -180,15 +182,21 @@ export function buildNativeCatalog(rows, meta, globalDefault = getGlobalDefault)
|
|
|
180
182
|
state: signedIn ? 'connected' : 'reconnect-needed',
|
|
181
183
|
identityLabel: email ?? account?.name ?? group.identityKey,
|
|
182
184
|
provisioning: provisioningFor(group.agent),
|
|
183
|
-
|
|
185
|
+
// A credential is present but nothing has probed or run it here yet — that
|
|
186
|
+
// is `no_evidence` ("we did not look"), not `unverified` (PHNX-4116). The
|
|
187
|
+
// real per-box verdict is recomputed in loadAccountCatalog; the display is
|
|
188
|
+
// a fact (token + run outcomes), never this word.
|
|
189
|
+
verdict: signedIn ? 'no_evidence' : 'missing',
|
|
184
190
|
checkedAt: null,
|
|
185
191
|
devices: [],
|
|
186
192
|
usage: null,
|
|
187
193
|
usageSnapshot: null,
|
|
188
194
|
usageError: null,
|
|
195
|
+
token: 'no token',
|
|
196
|
+
lastAuth: 'not used on this box yet',
|
|
189
197
|
fix: fixFor({
|
|
190
198
|
agent: group.agent,
|
|
191
|
-
verdict: signedIn ? '
|
|
199
|
+
verdict: signedIn ? 'no_evidence' : 'missing',
|
|
192
200
|
name: account?.name,
|
|
193
201
|
version: home,
|
|
194
202
|
provisioning: provisioningFor(group.agent),
|
|
@@ -204,6 +212,54 @@ export function buildNativeCatalog(rows, meta, globalDefault = getGlobalDefault)
|
|
|
204
212
|
function provisioningFor(agent) {
|
|
205
213
|
return harnessWorkerIsPerDevice(agent) ? 'per-device' : 'portable';
|
|
206
214
|
}
|
|
215
|
+
const TOKEN_FACT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
216
|
+
/** `Sep 16` — the credential file's date, the only mutable part of the token fact. */
|
|
217
|
+
function tokenFileDate(mtimeMs) {
|
|
218
|
+
const d = new Date(mtimeMs);
|
|
219
|
+
return `${TOKEN_FACT_MONTHS[d.getMonth()]} ${d.getDate()}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The non-secret scheme prefix of a Claude token — `sk-ant-oat01` for a
|
|
223
|
+
* setup-token, `sk-ant` for another Claude scheme, `token` otherwise. NEVER the
|
|
224
|
+
* secret body: only a fixed, publicly-documented prefix is returned so the fact
|
|
225
|
+
* can be printed and fleet-synced safely (root AGENTS.md §Security).
|
|
226
|
+
*/
|
|
227
|
+
function safeTokenPrefix(raw) {
|
|
228
|
+
const value = raw.trim();
|
|
229
|
+
if (value.startsWith('sk-ant-oat01'))
|
|
230
|
+
return 'sk-ant-oat01';
|
|
231
|
+
if (value.startsWith('sk-ant'))
|
|
232
|
+
return 'sk-ant';
|
|
233
|
+
return 'token';
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The token FACT for a Claude account ON THIS BOX (PHNX-4116): the slot/home
|
|
237
|
+
* `.claude/.oauth_token` scheme prefix + its file date, else a present native
|
|
238
|
+
* credential, else `no token`. For non-Claude native rows it reports credential
|
|
239
|
+
* presence generically. `dir` is the slot dir when the account has one, else the
|
|
240
|
+
* local version home. A fact, never a verdict.
|
|
241
|
+
*/
|
|
242
|
+
export function readTokenFact(agent, dir) {
|
|
243
|
+
if (!dir)
|
|
244
|
+
return 'no token';
|
|
245
|
+
if (agent === 'claude') {
|
|
246
|
+
const oauth = path.join(dir, '.claude', '.oauth_token');
|
|
247
|
+
try {
|
|
248
|
+
const stat = fs.statSync(oauth);
|
|
249
|
+
const prefix = safeTokenPrefix(fs.readFileSync(oauth, 'utf-8'));
|
|
250
|
+
return `${prefix} (${tokenFileDate(stat.mtimeMs)})`;
|
|
251
|
+
}
|
|
252
|
+
catch { /* fall through to the native-credential check */ }
|
|
253
|
+
try {
|
|
254
|
+
const creds = path.join(dir, '.claude', '.credentials.json');
|
|
255
|
+
const stat = fs.statSync(creds);
|
|
256
|
+
return `native login (${tokenFileDate(stat.mtimeMs)})`;
|
|
257
|
+
}
|
|
258
|
+
catch { /* no credential file */ }
|
|
259
|
+
return 'no token';
|
|
260
|
+
}
|
|
261
|
+
return credentialPresence(agent, dir).perVersion ? 'credential present' : 'no token';
|
|
262
|
+
}
|
|
207
263
|
export function toProviderRow(account, meta) {
|
|
208
264
|
const harnesses = ALL_AGENT_IDS.filter((agent) => providerAuthenticatesHarness(account.provider, account.auth, agent));
|
|
209
265
|
const defaults = meta.accounts?.defaults ?? {};
|
|
@@ -286,6 +342,13 @@ export async function loadAccountCatalog() {
|
|
|
286
342
|
const inv = localHome ? inventoryByHome.get(`${row.agent}:${localHome.label}`) : undefined;
|
|
287
343
|
row.usageSnapshot = inv?.snapshot ?? null;
|
|
288
344
|
row.usageError = inv?.usageError ?? null;
|
|
345
|
+
// The per-box FACTS (PHNX-4116): the token file on disk and the recorded run
|
|
346
|
+
// (or probe) outcome — never a verdict word. `cached` is the same row the
|
|
347
|
+
// observation used (slot key first, then the version-home label), so the
|
|
348
|
+
// fact and the internal verdict read the same evidence.
|
|
349
|
+
const credentialDir = slot?.slotDir ?? (localHome ? getVersionHomePath(row.agent, localHome.label) : null);
|
|
350
|
+
row.token = readTokenFact(row.agent, credentialDir);
|
|
351
|
+
row.lastAuth = formatAuthFact(cached ?? null);
|
|
289
352
|
row.fix = fixFor({
|
|
290
353
|
agent: row.agent,
|
|
291
354
|
verdict: row.verdict,
|
|
@@ -326,6 +389,10 @@ function providerListEntry(row, harness) {
|
|
|
326
389
|
checkedAt: null,
|
|
327
390
|
devices: [],
|
|
328
391
|
usage: null,
|
|
392
|
+
// A durable provider credential is a key, not a run-tracked login: state its
|
|
393
|
+
// presence as the token fact; there is no per-run auth evidence to render.
|
|
394
|
+
token: row.verdict === 'ready' ? 'api key (present)' : 'no key',
|
|
395
|
+
lastAuth: 'not used on this box yet',
|
|
329
396
|
fix: row.fix,
|
|
330
397
|
};
|
|
331
398
|
}
|
|
@@ -361,6 +428,8 @@ export function accountListJson(native, providers = [], harness) {
|
|
|
361
428
|
usage: row.usage,
|
|
362
429
|
...(row.usageSnapshot ? { usageSnapshot: row.usageSnapshot } : {}),
|
|
363
430
|
...((() => { const v = usageErrorForDisplay(row.usageError); return v ? { usageError: v } : {}; })()),
|
|
431
|
+
token: row.token,
|
|
432
|
+
lastAuth: row.lastAuth,
|
|
364
433
|
fix: row.fix,
|
|
365
434
|
})),
|
|
366
435
|
...providers.flatMap((row) => providerJsonEntries(row, harness)),
|
|
@@ -403,7 +472,7 @@ export function coverageNote(row, localDevice) {
|
|
|
403
472
|
return absent.length > 0 && absent.length < row.devices.length ? `not on ${absent.join(', ')}` : null;
|
|
404
473
|
}
|
|
405
474
|
const provisioned = row.devices.filter((device) => device.verdict !== 'missing').length;
|
|
406
|
-
const usable = row.devices.filter((device) => device.verdict === 'live' || device.verdict === 'rate_limited' || device.verdict === 'unverified').length;
|
|
475
|
+
const usable = row.devices.filter((device) => device.verdict === 'live' || device.verdict === 'rate_limited' || device.verdict === 'unverified' || device.verdict === 'no_evidence').length;
|
|
407
476
|
if (usable === 0 || usable === provisioned)
|
|
408
477
|
return null;
|
|
409
478
|
return `usable on ${usable} of ${provisioned} boxes`;
|
|
@@ -416,10 +485,19 @@ export const OVERVIEW_MAX_USAGE_WINDOWS = 2;
|
|
|
416
485
|
* cannot drift.
|
|
417
486
|
*/
|
|
418
487
|
export const ACCOUNT_LISTING_LEGEND = '* stale usage · a healthy account carries no state · identity + per-box state: agents accounts list --fleet';
|
|
488
|
+
/**
|
|
489
|
+
* When a usage reading arrived from ANOTHER box's poller over the fleet store,
|
|
490
|
+
* name its origin — `(from zion)` — so the number reads as a synced fact, not a
|
|
491
|
+
* local capture (PHNX-4116). Local captures (statusline/poll) carry no suffix.
|
|
492
|
+
*/
|
|
493
|
+
function usageOriginSuffix(snapshot) {
|
|
494
|
+
const poller = snapshot?.freshness?.source === 'sync' ? snapshot.freshness.poller : undefined;
|
|
495
|
+
return poller ? ` ${chalk.gray(`(from ${poller})`)}` : '';
|
|
496
|
+
}
|
|
419
497
|
function usageText(row, maxWindows) {
|
|
420
498
|
if (row.usageSnapshot) {
|
|
421
499
|
const usageInfo = { snapshot: row.usageSnapshot, error: row.usageError ?? null };
|
|
422
|
-
return formatUsageSummary(null, usageInfo.snapshot, 3, viewUsageSummaryOptions(row.agent, row.state === 'connected', usageInfo, maxWindows));
|
|
500
|
+
return formatUsageSummary(null, usageInfo.snapshot, 3, viewUsageSummaryOptions(row.agent, row.state === 'connected', usageInfo, maxWindows)) + usageOriginSuffix(row.usageSnapshot);
|
|
423
501
|
}
|
|
424
502
|
if (row.usage?.status === 'rate_limited' && (row.usage.usedPercent === null || row.usage.usedPercent === undefined)) {
|
|
425
503
|
return 'limited';
|
|
@@ -432,31 +510,32 @@ function usageText(row, maxWindows) {
|
|
|
432
510
|
return `${renderBar(row.usage.usedPercent, LISTING_USAGE_BAR_LEN)} ${percent}`;
|
|
433
511
|
}
|
|
434
512
|
/**
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* 'out of credits' or 'session-limited (…)' exactly for these `unavailable`
|
|
439
|
-
* reasons; without one, `usageText` prints 'limited' or 'no credits'. A verdict
|
|
440
|
-
* thrown by a maxed WINDOW is deliberately NOT here: the overview caps the cell
|
|
441
|
-
* at `OVERVIEW_MAX_USAGE_WINDOWS`, so the window that tripped it can be hidden
|
|
442
|
-
* behind the `+N` count with no color at all, and the note is the only signal.
|
|
513
|
+
* Colour the auth FACT by what it states (PHNX-4116): a recent success is green,
|
|
514
|
+
* a server rejection red, a throttle yellow, and "not used on this box yet" the
|
|
515
|
+
* neutral gray of a fact that carries no alarm.
|
|
443
516
|
*/
|
|
444
|
-
function
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
517
|
+
export function authFactNote(lastAuth) {
|
|
518
|
+
if (lastAuth.startsWith('last used ok'))
|
|
519
|
+
return chalk.green(lastAuth);
|
|
520
|
+
if (lastAuth.startsWith('last auth failure'))
|
|
521
|
+
return chalk.red(lastAuth);
|
|
522
|
+
if (lastAuth.startsWith('rate-limited'))
|
|
523
|
+
return chalk.yellow(lastAuth);
|
|
524
|
+
return chalk.gray(lastAuth);
|
|
452
525
|
}
|
|
453
526
|
function nativeLine(row, localDevice, maxWindows) {
|
|
454
527
|
return {
|
|
455
528
|
// A discovered login nobody has named is still identified by what it is:
|
|
456
529
|
// the row no longer carries an IDENTITY column, so the identity IS the name.
|
|
457
530
|
name: row.name ?? row.identityLabel,
|
|
531
|
+
// FACTS, in order: the token on disk, what happened when it was last used, a
|
|
532
|
+
// signed-out flag when there is no credential at all, fleet coverage, then the
|
|
533
|
+
// repair. The auth fact already carries a throttle/failure with its time, so
|
|
534
|
+
// there is no separate verdict word (PHNX-4116).
|
|
458
535
|
notes: [
|
|
459
|
-
|
|
536
|
+
chalk.gray(row.token),
|
|
537
|
+
authFactNote(row.lastAuth),
|
|
538
|
+
row.verdict === 'missing' ? chalk.red('signed out') : null,
|
|
460
539
|
coverageNote(row, localDevice),
|
|
461
540
|
row.fix ? chalk.gray(`fix: ${row.fix}`) : null,
|
|
462
541
|
].filter((note) => !!note),
|
|
@@ -565,7 +644,7 @@ export function readSharedAccountVerdicts(userAgentsDir) {
|
|
|
565
644
|
continue;
|
|
566
645
|
if (!['native', 'durable', 'per-device'].includes(row.authMode))
|
|
567
646
|
continue;
|
|
568
|
-
if (!['live', 'expired', 'revoked', 'rate_limited', 'unverified', 'missing'].includes(row.verdict))
|
|
647
|
+
if (!['live', 'expired', 'revoked', 'rate_limited', 'unverified', 'no_evidence', 'missing'].includes(row.verdict))
|
|
569
648
|
continue;
|
|
570
649
|
const key = `${row.harness}:${row.accountId}`;
|
|
571
650
|
const values = out.get(key) ?? [];
|
|
@@ -600,11 +679,14 @@ function normalizeAuthVerdict(verdict, signedIn) {
|
|
|
600
679
|
// result — the daemon never publishes it (probeLocalFleetAuth drops those
|
|
601
680
|
// rows). So it says nothing about the credential on disk; the live signedIn
|
|
602
681
|
// read of the slot decides, exactly as it does when no verdict exists at all.
|
|
682
|
+
// No probe row, or one that says nothing about the credential: a present
|
|
683
|
+
// credential with no evidence either way is `no_evidence`, never a word that
|
|
684
|
+
// claims we looked (PHNX-4116).
|
|
603
685
|
if (verdict === 'unconfigured')
|
|
604
|
-
return signedIn ? '
|
|
686
|
+
return signedIn ? 'no_evidence' : 'missing';
|
|
605
687
|
if (verdict === 'error')
|
|
606
|
-
return signedIn ? '
|
|
607
|
-
return verdict ?? (signedIn ? '
|
|
688
|
+
return signedIn ? 'no_evidence' : 'missing';
|
|
689
|
+
return verdict ?? (signedIn ? 'no_evidence' : 'missing');
|
|
608
690
|
}
|
|
609
691
|
/**
|
|
610
692
|
* Merge the two writers of the same local truth — the T1 slot store and the
|
|
@@ -650,6 +732,8 @@ export function aggregateAccountVerdict(provisioning, devices, localQuota) {
|
|
|
650
732
|
return 'live';
|
|
651
733
|
if (verdicts.includes('unverified'))
|
|
652
734
|
return 'unverified';
|
|
735
|
+
if (verdicts.includes('no_evidence'))
|
|
736
|
+
return 'no_evidence';
|
|
653
737
|
return provisioning === 'per-device' ? 'per-device' : 'missing';
|
|
654
738
|
}
|
|
655
739
|
function newestCheckedAt(devices) {
|
|
@@ -38,7 +38,7 @@ const ADAPTERS = new Map([
|
|
|
38
38
|
['deepinfra', fixed('deepinfra', ['api-key'], { codex: 'OPENAI_API_KEY' }, { codex: { OPENAI_BASE_URL: 'https://api.deepinfra.com/v1/openai' } })],
|
|
39
39
|
['openai', fixed('openai', ['api-key'], { codex: 'OPENAI_API_KEY', opencode: 'OPENAI_API_KEY' }, {}, { codex: 'OPENAI_BASE_URL', opencode: 'OPENAI_BASE_URL' })],
|
|
40
40
|
['xai', fixed('xai', ['api-key'], { grok: 'XAI_API_KEY', claude: 'ANTHROPIC_AUTH_TOKEN' })],
|
|
41
|
-
['google', fixed('google', ['api-key'], {
|
|
41
|
+
['google', fixed('google', ['api-key'], { antigravity: 'ANTIGRAVITY_API_KEY' })],
|
|
42
42
|
['opencode', fixed('opencode', ['api-key'], { opencode: 'OPENCODE_API_KEY' })],
|
|
43
43
|
['proxy', fixed('proxy', ['api-key', 'bearer-token'], { claude: 'ANTHROPIC_AUTH_TOKEN', codex: 'OPENAI_API_KEY' }, {}, { claude: 'ANTHROPIC_BASE_URL', codex: 'OPENAI_BASE_URL' })],
|
|
44
44
|
['truefoundry', fixed('truefoundry', ['api-key', 'bearer-token'], { claude: 'ANTHROPIC_AUTH_TOKEN' }, {}, { claude: 'ANTHROPIC_BASE_URL' })],
|
|
@@ -22,7 +22,7 @@ interface ResolvedCredentialAccount {
|
|
|
22
22
|
export interface NativeAccount extends NativeAccountRecord {
|
|
23
23
|
kind: 'native';
|
|
24
24
|
}
|
|
25
|
-
export { recordSlot, readSlots } from './accounts/slots.js';
|
|
25
|
+
export { recordSlot, readSlots, dropSlots } from './accounts/slots.js';
|
|
26
26
|
export { registeredNativeAccountForEmail, parseNativeIdentityKey } from './native-accounts.js';
|
|
27
27
|
export type UnifiedAccount = (CredentialAccount & {
|
|
28
28
|
kind: 'provider';
|
|
@@ -27,7 +27,9 @@ import { isAgentId } from './types.js';
|
|
|
27
27
|
import { bundleExistsSync, deleteBundleSync, deleteKeychainTokenSync, getKeychainTokenSync, hasKeychainTokenSync, listBundlesSync, readAndResolveBundleEnvSync, readBundleSync, renameBundleSync, writeBundleWithItemsSync, } from './secrets-client.js';
|
|
28
28
|
import { getAccountProvider } from './account-provider-registry.js';
|
|
29
29
|
import { accountSecretItem, buildAccountBundle, parseAccountBundle, secretVarFor } from './account-schema.js';
|
|
30
|
-
|
|
30
|
+
import { readClaudeHomeConfig } from './agent-spec/agents.js';
|
|
31
|
+
import { getVersionHomePath, listInstalledVersions } from './installations/store.js';
|
|
32
|
+
export { recordSlot, readSlots, dropSlots } from './accounts/slots.js';
|
|
31
33
|
export { registeredNativeAccountForEmail, parseNativeIdentityKey } from './native-accounts.js';
|
|
32
34
|
const NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
33
35
|
const NATIVE_LABEL = /^[a-zA-Z0-9][a-zA-Z0-9@._+-]*$/;
|
|
@@ -663,6 +665,35 @@ export function resolveCredentialAccount(name, host, expectedProvider, base = ge
|
|
|
663
665
|
env: { ...connectionEnv, [envVar]: secret },
|
|
664
666
|
};
|
|
665
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Sync fallback: scan installed version homes to discover a native identity
|
|
670
|
+
* that matches the given email, for accounts that predate the registration
|
|
671
|
+
* system (empty `accounts.native`). Returns a synthetic SpawnAccount or null.
|
|
672
|
+
*/
|
|
673
|
+
function discoverUnregisteredNativeAccount(email, agent) {
|
|
674
|
+
if (agent !== 'claude')
|
|
675
|
+
return null;
|
|
676
|
+
const needle = email.toLowerCase();
|
|
677
|
+
for (const label of listInstalledVersions(agent)) {
|
|
678
|
+
const home = getVersionHomePath(agent, label);
|
|
679
|
+
if (!fs.existsSync(home))
|
|
680
|
+
continue;
|
|
681
|
+
const config = readClaudeHomeConfig(home);
|
|
682
|
+
if (!config?.identity?.email)
|
|
683
|
+
continue;
|
|
684
|
+
if (config.identity.email.toLowerCase() === needle) {
|
|
685
|
+
return {
|
|
686
|
+
kind: 'native',
|
|
687
|
+
id: email,
|
|
688
|
+
name: email,
|
|
689
|
+
agent,
|
|
690
|
+
identityKey: config.identity.accountKey ?? email.toLowerCase(),
|
|
691
|
+
scope: 'version',
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
666
697
|
/**
|
|
667
698
|
* Resolve the account a run should launch under, following the binding order
|
|
668
699
|
* (explicit → exact `agent@version` → device-scoped `agent` → per-harness
|
|
@@ -689,7 +720,10 @@ export function resolveSpawnAccount(explicit, agent, version, meta, opts = {}) {
|
|
|
689
720
|
// Scope the lookup to the harness being launched: a bare identity selector
|
|
690
721
|
// (`claude#muqsitnawaz@gmail.com`) matches every harness that identity is signed
|
|
691
722
|
// into, and only this one can authenticate the spawn.
|
|
692
|
-
|
|
723
|
+
let unified = findUnifiedAccount(selection.id, meta, undefined, agent);
|
|
724
|
+
if (!unified && selection.source === 'explicit' && selection.id.includes('@')) {
|
|
725
|
+
unified = discoverUnregisteredNativeAccount(selection.id, agent);
|
|
726
|
+
}
|
|
693
727
|
if (!unified) {
|
|
694
728
|
// A stale per-harness default is a preference, not a hard requirement: the
|
|
695
729
|
// machine stays runnable by falling back to balanced rotation. Bindings and
|