@phnx-labs/agents-cli 1.22.117 → 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 +235 -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.js +53 -1
- 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.js +9 -9
- 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/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/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 +5 -10
- 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
|
@@ -24,7 +24,7 @@ import { resolveClaudeSetupToken } from '../claude-account-token.js';
|
|
|
24
24
|
import { formatBackoffRemaining, noteUsageRateLimited, usageRateLimitedUntil, } from '../usage-backoff.js';
|
|
25
25
|
import { getCacheDir } from '../state.js';
|
|
26
26
|
import { mapBounded } from '../concurrency.js';
|
|
27
|
-
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from '../fs-atomic.js';
|
|
27
|
+
import { atomicWriteFileSync, ensureLockTarget, withFileLock, withFileLockAsync } from '../fs-atomic.js';
|
|
28
28
|
import { withRefreshLease } from '../refresh-coordinator.js';
|
|
29
29
|
import { padToWidth } from '../session/width.js';
|
|
30
30
|
const execFileAsync = promisify(execFile);
|
|
@@ -723,6 +723,19 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
723
723
|
}
|
|
724
724
|
return parts.join(' ');
|
|
725
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* The snapshot's windows that are still LIVE — reset time not yet passed. A
|
|
728
|
+
* window past its `resetsAt` has rolled over: its `usedPercent` is a reading of
|
|
729
|
+
* the PREVIOUS period, not a live throttle (PHNX-4116). Both the throttle
|
|
730
|
+
* verdict ({@link deriveUsageStatusFromSnapshot}) and the displayed
|
|
731
|
+
* `usedPercent` (`devices/harness-inventory.ts`) MUST select from this SAME set,
|
|
732
|
+
* or a maxed-then-reset account reads `available` yet shows a stale 99% next to
|
|
733
|
+
* its "ready" verdict (#3705). Callers still narrow further (dropping
|
|
734
|
+
* `sonnet_week`, the model sub-limit); this is only the reset-rollover cut.
|
|
735
|
+
*/
|
|
736
|
+
export function liveUsageWindows(snapshot, now = Date.now()) {
|
|
737
|
+
return snapshot.windows.filter((window) => !(window.resetsAt && window.resetsAt.getTime() <= now));
|
|
738
|
+
}
|
|
726
739
|
/**
|
|
727
740
|
* Derive an account's real throttle state from its live usage windows — the
|
|
728
741
|
* single signal both the `agents view` badge and run-rotation eligibility share
|
|
@@ -742,7 +755,7 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
742
755
|
* The model-specific `sonnet_week` sub-limit is excluded: hitting it throttles
|
|
743
756
|
* one model, not the account, so it shouldn't flip the whole row to throttled.
|
|
744
757
|
*/
|
|
745
|
-
export function deriveUsageStatusFromSnapshot(snapshot) {
|
|
758
|
+
export function deriveUsageStatusFromSnapshot(snapshot, now = Date.now()) {
|
|
746
759
|
if (!snapshot)
|
|
747
760
|
return null;
|
|
748
761
|
if (snapshot.unavailable) {
|
|
@@ -750,14 +763,17 @@ export function deriveUsageStatusFromSnapshot(snapshot) {
|
|
|
750
763
|
// clears it. session_limit blocks only until its reset time.
|
|
751
764
|
if (snapshot.unavailable.reason === 'out_of_credits')
|
|
752
765
|
return 'rate_limited';
|
|
753
|
-
if (snapshot.unavailable.resetsAt && snapshot.unavailable.resetsAt.getTime() >
|
|
766
|
+
if (snapshot.unavailable.resetsAt && snapshot.unavailable.resetsAt.getTime() > now) {
|
|
754
767
|
return 'rate_limited';
|
|
755
768
|
}
|
|
756
769
|
}
|
|
757
770
|
if (snapshot.windows.length === 0)
|
|
758
771
|
return null;
|
|
759
|
-
const
|
|
760
|
-
|
|
772
|
+
const live = liveUsageWindows(snapshot, now);
|
|
773
|
+
if (live.length === 0)
|
|
774
|
+
return 'available';
|
|
775
|
+
const blocking = live.filter((window) => window.key !== 'sonnet_week');
|
|
776
|
+
const windows = blocking.length > 0 ? blocking : live;
|
|
761
777
|
const maxUsed = Math.max(...windows.map((window) => window.usedPercent));
|
|
762
778
|
return maxUsed >= 100 ? 'rate_limited' : 'available';
|
|
763
779
|
}
|
|
@@ -1897,7 +1913,9 @@ function parseCapturedAtMs(capturedAt) {
|
|
|
1897
1913
|
* a fresher row another peer (or, on a headed receiver, the local status-line)
|
|
1898
1914
|
* already wrote. An incoming row with no `capturedAt` cannot prove it is newer, so
|
|
1899
1915
|
* it never displaces an existing timestamped row. Returns the count updated.
|
|
1900
|
-
* Locked + atomic like every other cache writer
|
|
1916
|
+
* Locked + atomic like every other cache writer, but through the ASYNC lock: it
|
|
1917
|
+
* runs on the daemon's usage-sync tick once per peer reply (PHNX-4116), where
|
|
1918
|
+
* the sync lock's `sleepSync` would freeze every other service under contention.
|
|
1901
1919
|
*
|
|
1902
1920
|
* Deliberately NOT role-gated on the receiver. "Consume only on worker/unmarked"
|
|
1903
1921
|
* is a SENDER-side optimization (don't waste a push on a headed peer that reads
|
|
@@ -1907,14 +1925,14 @@ function parseCapturedAtMs(capturedAt) {
|
|
|
1907
1925
|
* runs now shows a usage bar), so gating here on the receiver's own — laggier —
|
|
1908
1926
|
* view of its role would only reject legitimate data.
|
|
1909
1927
|
*/
|
|
1910
|
-
export function ingestPeerClaudeUsageRows(rows, cachePath = getClaudeUsageCachePath()) {
|
|
1928
|
+
export async function ingestPeerClaudeUsageRows(rows, cachePath = getClaudeUsageCachePath()) {
|
|
1911
1929
|
const incoming = Object.entries(rows).filter(([, row]) => row && Array.isArray(row.windows) && row.windows.length > 0);
|
|
1912
1930
|
if (incoming.length === 0)
|
|
1913
1931
|
return 0;
|
|
1914
1932
|
let merged = 0;
|
|
1915
1933
|
try {
|
|
1916
1934
|
ensureLockTarget(cachePath, '{}');
|
|
1917
|
-
|
|
1935
|
+
await withFileLockAsync(cachePath, () => {
|
|
1918
1936
|
const cache = readClaudeUsageCacheFile(cachePath);
|
|
1919
1937
|
for (const [key, row] of incoming) {
|
|
1920
1938
|
const prior = cache[key];
|
|
@@ -2,6 +2,14 @@ import type { AgentId, DeviceAccountSlot, Meta } from '../types.js';
|
|
|
2
2
|
export type { DeviceAccountSlot };
|
|
3
3
|
export declare function slotDir(harness: AgentId, accountId: string): string;
|
|
4
4
|
export declare function readSlots(meta: Pick<Meta, 'deviceAccounts'>): Record<string, DeviceAccountSlot>;
|
|
5
|
+
/**
|
|
6
|
+
* Remove slot records from THIS box's device doc (`deviceAccounts.slots`).
|
|
7
|
+
* Symmetric with {@link recordSlot}. The slot DIRECTORY is not touched — a
|
|
8
|
+
* caller that also wants the on-disk home gone removes it separately; a stale
|
|
9
|
+
* worker slot keeps its dir because `.claude/projects` holds transcripts
|
|
10
|
+
* (PHNX-4116). A no-op for an empty list or an id with no record.
|
|
11
|
+
*/
|
|
12
|
+
export declare function dropSlots(accountIds: readonly string[]): void;
|
|
5
13
|
export declare function recordSlot(accountId: string, slot: DeviceAccountSlot): void;
|
|
6
14
|
export interface SlotProjection {
|
|
7
15
|
accountId: string;
|
|
@@ -35,6 +35,23 @@ export function slotDir(harness, accountId) {
|
|
|
35
35
|
export function readSlots(meta) {
|
|
36
36
|
return { ...(meta.deviceAccounts?.slots ?? {}) };
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Remove slot records from THIS box's device doc (`deviceAccounts.slots`).
|
|
40
|
+
* Symmetric with {@link recordSlot}. The slot DIRECTORY is not touched — a
|
|
41
|
+
* caller that also wants the on-disk home gone removes it separately; a stale
|
|
42
|
+
* worker slot keeps its dir because `.claude/projects` holds transcripts
|
|
43
|
+
* (PHNX-4116). A no-op for an empty list or an id with no record.
|
|
44
|
+
*/
|
|
45
|
+
export function dropSlots(accountIds) {
|
|
46
|
+
if (accountIds.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
updateMeta((current) => {
|
|
49
|
+
const slots = { ...current.deviceAccounts?.slots };
|
|
50
|
+
for (const id of accountIds)
|
|
51
|
+
delete slots[id];
|
|
52
|
+
return { ...current, deviceAccounts: { ...current.deviceAccounts, slots } };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
38
55
|
export function recordSlot(accountId, slot) {
|
|
39
56
|
if (slot.accountId !== accountId) {
|
|
40
57
|
throw new Error(`recordSlot accountId mismatch: key '${accountId}' vs slot.accountId '${slot.accountId}'.`);
|
package/dist/lib/add-dir.js
CHANGED
|
@@ -89,9 +89,9 @@ export declare const ALL_AGENT_IDS: AgentId[];
|
|
|
89
89
|
* import (runner.ts already imports from routines.ts).
|
|
90
90
|
*
|
|
91
91
|
* This is a curated subset of AGENT_COMMANDS, not "every harness that can run
|
|
92
|
-
* headlessly". Expanding it is a product change
|
|
93
|
-
*
|
|
94
|
-
*
|
|
92
|
+
* headlessly". Expanding it is a product change. Argv itself is baked from
|
|
93
|
+
* AGENT_COMMANDS in daemon/runner.ts bakeRoutineArgv — do not reintroduce a
|
|
94
|
+
* second token table.
|
|
95
95
|
*/
|
|
96
96
|
export declare const ROUTINE_AGENT_IDS: readonly string[];
|
|
97
97
|
/** Agents that can receive managed installs, imports, and resource sync writes. */
|
|
@@ -222,7 +222,7 @@ export declare function formatClaudeOrgLabel(orgType: string | null | undefined)
|
|
|
222
222
|
*/
|
|
223
223
|
export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
|
|
224
224
|
/** Agents whose local credential formats expose enough state for account selection. */
|
|
225
|
-
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "
|
|
225
|
+
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "cursor", "grok", "antigravity", "kimi", "droid", "opencode", "muse"];
|
|
226
226
|
/** Whether agents-cli can determine this agent's per-version sign-in state. */
|
|
227
227
|
export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
228
228
|
/**
|
|
@@ -573,9 +573,8 @@ export declare function stripJsonComments(content: string): string;
|
|
|
573
573
|
*
|
|
574
574
|
* All three MCP path resolvers read `MCP_TARGETS`, so the file the writer
|
|
575
575
|
* targets, the file the parser reads, and the file the staleness detector
|
|
576
|
-
* watches cannot drift apart. An agent with no MCP target
|
|
577
|
-
*
|
|
578
|
-
* parse.
|
|
576
|
+
* watches cannot drift apart. An agent with no MCP target keeps the historical
|
|
577
|
+
* settings.json default so old configs still parse.
|
|
579
578
|
*/
|
|
580
579
|
export declare function getUserMcpConfigPath(agentId: AgentId): string;
|
|
581
580
|
/**
|
|
@@ -268,6 +268,7 @@ export const AGENTS = {
|
|
|
268
268
|
format: 'markdown',
|
|
269
269
|
variableSyntax: '$ARGUMENTS',
|
|
270
270
|
supportsHooks: true,
|
|
271
|
+
nativePluginSkills: true,
|
|
271
272
|
// Claude Code grew a native `claude --cloud "<prompt>"` (Anthropic-managed
|
|
272
273
|
// infra, claude.ai/code; requires claude.ai subscription auth). Routing
|
|
273
274
|
// still goes to Rush Cloud deliberately — it keeps cloud tasks in one
|
|
@@ -305,46 +306,6 @@ export const AGENTS = {
|
|
|
305
306
|
// ~/.codex/agents/*.toml (name, description, developer_instructions).
|
|
306
307
|
capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: { since: '0.138.0' }, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: { since: '0.117.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'auto', 'skip'], interactiveRepl: true },
|
|
307
308
|
},
|
|
308
|
-
gemini: {
|
|
309
|
-
id: 'gemini',
|
|
310
|
-
name: 'Gemini',
|
|
311
|
-
sessionDir: ['.gemini', 'tmp'],
|
|
312
|
-
sessionFileExt: '.json',
|
|
313
|
-
versionStdoutMatch: 'semver',
|
|
314
|
-
unmanagedBinary: 'path',
|
|
315
|
-
mcpRegister: 'cli',
|
|
316
|
-
mcpAddHttp: 'transport',
|
|
317
|
-
mcpAddStdio: 'simple',
|
|
318
|
-
mcpConfigWrite: 'json-mcpServers',
|
|
319
|
-
color: 'blue',
|
|
320
|
-
cliCommand: 'gemini',
|
|
321
|
-
npmPackage: '@google/gemini-cli',
|
|
322
|
-
configDir: path.join(HOME, '.gemini'),
|
|
323
|
-
commandsDir: path.join(HOME, '.gemini', 'commands'),
|
|
324
|
-
commandsSubdir: 'commands',
|
|
325
|
-
skillsDir: path.join(HOME, '.gemini', 'skills'),
|
|
326
|
-
hooksDir: 'hooks',
|
|
327
|
-
instructionsFile: 'GEMINI.md',
|
|
328
|
-
format: 'toml',
|
|
329
|
-
variableSyntax: '{{args}}',
|
|
330
|
-
supportsHooks: true,
|
|
331
|
-
nativeAgentsSkillsDir: true,
|
|
332
|
-
// Google retired the Gemini CLI (announced at Google I/O 2026, May 19); the `gemini`
|
|
333
|
-
// command stopped serving free/Pro/Ultra requests on June 18, 2026. Antigravity CLI
|
|
334
|
-
// (`agy`) is the official successor. Hard deprecation keeps legacy Gemini
|
|
335
|
-
// sessions/config parseable while blocking install/import/sync.
|
|
336
|
-
deprecated: {
|
|
337
|
-
by: 'Google',
|
|
338
|
-
date: 'June 18, 2026',
|
|
339
|
-
reason: 'The Gemini CLI was retired for free, Pro, and Ultra tiers and no longer serves requests (announced at Google I/O 2026 on May 19).',
|
|
340
|
-
replacement: 'antigravity',
|
|
341
|
-
url: 'https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/',
|
|
342
|
-
hard: true,
|
|
343
|
-
},
|
|
344
|
-
// gemini hooks: shipped in v0.26.0 (Jan 2026); older binaries silently ignore the `hooks` key.
|
|
345
|
-
// extensions: gemini-extension.json bundles shipped in v0.8.0; custom subagents in v0.36.0.
|
|
346
|
-
capabilities: { hooks: { since: '0.26.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: { since: '0.8.0' }, subagents: { since: '0.36.0' }, rules: { file: 'GEMINI.md' }, workflows: false, memory: false, modes: ['plan', 'edit', 'skip'], rulesImports: true, interactiveRepl: false },
|
|
347
|
-
},
|
|
348
309
|
cursor: {
|
|
349
310
|
id: 'cursor',
|
|
350
311
|
name: 'Cursor',
|
|
@@ -1019,16 +980,13 @@ export const ALL_AGENT_IDS = Object.keys(AGENTS);
|
|
|
1019
980
|
* import (runner.ts already imports from routines.ts).
|
|
1020
981
|
*
|
|
1021
982
|
* This is a curated subset of AGENT_COMMANDS, not "every harness that can run
|
|
1022
|
-
* headlessly". Expanding it is a product change
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
983
|
+
* headlessly". Expanding it is a product change. Argv itself is baked from
|
|
984
|
+
* AGENT_COMMANDS in daemon/runner.ts bakeRoutineArgv — do not reintroduce a
|
|
985
|
+
* second token table.
|
|
1025
986
|
*/
|
|
1026
987
|
export const ROUTINE_AGENT_IDS = Object.freeze([
|
|
1027
988
|
'claude',
|
|
1028
989
|
'codex',
|
|
1029
|
-
// gemini is hard-deprecated (Antigravity replaced it) — no routine target. A
|
|
1030
|
-
// legacy gemini routine now fails validateJob loud instead of firing a
|
|
1031
|
-
// retired backend; the id survives only for reading old sessions/config.
|
|
1032
990
|
'cursor',
|
|
1033
991
|
'kimi',
|
|
1034
992
|
'droid',
|
|
@@ -1338,7 +1296,6 @@ export function accountOrgBadge(info) {
|
|
|
1338
1296
|
export const ACCOUNT_INSPECTION_AGENT_IDS = [
|
|
1339
1297
|
'claude',
|
|
1340
1298
|
'codex',
|
|
1341
|
-
'gemini',
|
|
1342
1299
|
'cursor',
|
|
1343
1300
|
'grok',
|
|
1344
1301
|
'antigravity',
|
|
@@ -1429,7 +1386,6 @@ function resolveAccountCredentialPath(base, ...segments) {
|
|
|
1429
1386
|
const CREDENTIAL_FILE_SEGMENTS = {
|
|
1430
1387
|
claude: [['.claude', '.claude.json'], ['.claude.json']],
|
|
1431
1388
|
codex: [['.codex', 'auth.json']],
|
|
1432
|
-
gemini: [['.gemini', 'google_accounts.json']],
|
|
1433
1389
|
grok: [['.grok', 'auth.json']],
|
|
1434
1390
|
kimi: [['.kimi-code', 'credentials', 'kimi-code.json']],
|
|
1435
1391
|
droid: [['.factory', 'auth.v2.file']],
|
|
@@ -2028,7 +1984,6 @@ export async function getAccountInfo(agentId, home) {
|
|
|
2028
1984
|
const configFiles = {
|
|
2029
1985
|
claude: path.join(base, '.claude.json'),
|
|
2030
1986
|
codex: path.join(base, '.codex', 'auth.json'),
|
|
2031
|
-
gemini: path.join(base, '.gemini', 'google_accounts.json'),
|
|
2032
1987
|
// OpenCode keeps every session in ONE sqlite file rather than a directory of
|
|
2033
1988
|
// per-session transcripts, so the session-file walk resolveLastActive runs
|
|
2034
1989
|
// for other agents finds nothing. Its mtime fallback is the right read here:
|
|
@@ -2151,11 +2106,6 @@ export async function getAccountInfo(agentId, home) {
|
|
|
2151
2106
|
signedIn: !!email,
|
|
2152
2107
|
};
|
|
2153
2108
|
}
|
|
2154
|
-
case 'gemini': {
|
|
2155
|
-
const data = JSON.parse(await fs.promises.readFile(path.join(base, '.gemini', 'google_accounts.json'), 'utf-8'));
|
|
2156
|
-
const email = data.active || null;
|
|
2157
|
-
return { ...empty, email, signedIn: !!email, lastActive };
|
|
2158
|
-
}
|
|
2159
2109
|
case 'cursor': {
|
|
2160
2110
|
// Cursor CLI keeps account metadata in ~/.cursor/cli-config.json
|
|
2161
2111
|
// (authInfo: { email, userId, authId }) and its OAuth tokens SEPARATELY
|
|
@@ -2902,9 +2852,8 @@ function parseMcpFromOpenCodeConfig(configPath) {
|
|
|
2902
2852
|
*
|
|
2903
2853
|
* All three MCP path resolvers read `MCP_TARGETS`, so the file the writer
|
|
2904
2854
|
* targets, the file the parser reads, and the file the staleness detector
|
|
2905
|
-
* watches cannot drift apart. An agent with no MCP target
|
|
2906
|
-
*
|
|
2907
|
-
* parse.
|
|
2855
|
+
* watches cannot drift apart. An agent with no MCP target keeps the historical
|
|
2856
|
+
* settings.json default so old configs still parse.
|
|
2908
2857
|
*/
|
|
2909
2858
|
export function getUserMcpConfigPath(agentId) {
|
|
2910
2859
|
return getMcpConfigPathForHome(agentId, HOME);
|
|
@@ -3036,9 +2985,6 @@ const AGENT_NAME_ALIASES = {
|
|
|
3036
2985
|
codex: 'codex',
|
|
3037
2986
|
'openai-codex': 'codex',
|
|
3038
2987
|
cx: 'codex',
|
|
3039
|
-
gemini: 'gemini',
|
|
3040
|
-
'gemini-cli': 'gemini',
|
|
3041
|
-
gx: 'gemini',
|
|
3042
2988
|
cursor: 'cursor',
|
|
3043
2989
|
'cursor-agent': 'cursor',
|
|
3044
2990
|
cr: 'cursor',
|
|
@@ -19,10 +19,21 @@ import { type ProviderProbe } from './accounting/usage.js';
|
|
|
19
19
|
* endpoint cannot prove live for a known non-revocation
|
|
20
20
|
* reason (Claude setup-token usage-scope gap — RUSH-2392),
|
|
21
21
|
* OR a throttled probe with no fresh previous verdict (PHNX-4051).
|
|
22
|
+
* - `no_evidence` — a credential is present on disk but THIS box has no evidence,
|
|
23
|
+
* either way, that it can authenticate: no probe was possible
|
|
24
|
+
* (a worker's setup-token box does not probe the usage endpoint)
|
|
25
|
+
* and no run outcome has been recorded here yet. Distinct from
|
|
26
|
+
* `unverified` (which is "signed in, no probe endpoint exists for
|
|
27
|
+
* this harness") — `no_evidence` is "we did not look", the exact
|
|
28
|
+
* state PHNX-4116 stops rendering as a verdict word. A worker
|
|
29
|
+
* never PUBLISHES this row (it is dropped in
|
|
30
|
+
* {@link probeLocalFleetAuth}); it surfaces only as an in-memory
|
|
31
|
+
* account-catalog verdict whose display is a FACT (token presence
|
|
32
|
+
* + recorded run outcomes), never the word "no_evidence".
|
|
22
33
|
* - `unconfigured`— no usable credential on disk.
|
|
23
34
|
* - `error` — network/other failure; verdict indeterminate (keep the last known one).
|
|
24
35
|
*/
|
|
25
|
-
export type AuthVerdict = 'live' | 'revoked' | 'expired' | 'rate_limited' | 'unverified' | 'unconfigured' | 'error';
|
|
36
|
+
export type AuthVerdict = 'live' | 'revoked' | 'expired' | 'rate_limited' | 'unverified' | 'no_evidence' | 'unconfigured' | 'error';
|
|
26
37
|
export interface AuthHealth {
|
|
27
38
|
verdict: AuthVerdict;
|
|
28
39
|
/** epoch ms of the probe. */
|
|
@@ -33,6 +44,15 @@ export interface AuthHealth {
|
|
|
33
44
|
account?: string;
|
|
34
45
|
/** Stable registered account id. Display labels are never used as identity. */
|
|
35
46
|
accountId?: string;
|
|
47
|
+
/**
|
|
48
|
+
* How this row was observed (PHNX-4116). `probe` (default when absent) is a
|
|
49
|
+
* network/usage-endpoint probe the daemon ran; `run` is a REAL agent run
|
|
50
|
+
* outcome recorded on exit — a successful inference (`live`) or an auth failure
|
|
51
|
+
* (`revoked`/`expired`/`rate_limited`). The auth FACT `agents view` renders
|
|
52
|
+
* (`last used ok 12m ago` vs `last auth failure 401 …`) reads this so a token's
|
|
53
|
+
* usability is stated as evidence, not inferred from a stale usage file.
|
|
54
|
+
*/
|
|
55
|
+
source?: 'probe' | 'run';
|
|
36
56
|
}
|
|
37
57
|
/** Maximum age of an auth verdict used for automatic routing decisions. */
|
|
38
58
|
export declare const AUTH_PROBE_MAX_AGE_MS: number;
|
|
@@ -284,4 +304,52 @@ export declare function probeLocalFleetAuth(opts?: {
|
|
|
284
304
|
* merges as before.
|
|
285
305
|
*/
|
|
286
306
|
export declare function writeFleetAuthRows(host: string, rows: AuthProbeRow[], installed?: ReadonlySet<string>): void;
|
|
307
|
+
/**
|
|
308
|
+
* The installed-version label a run outcome falls back to when the account has no
|
|
309
|
+
* slot on this box: the explicit `version`, else the label derived from `home` (a
|
|
310
|
+
* version home is `<versionDir>/home`, so the label is its parent dir's basename).
|
|
311
|
+
* Null when neither resolves. Pure (no fs) so it is unit-tested directly.
|
|
312
|
+
*/
|
|
313
|
+
export declare function runOutcomeVersionKey(opts: {
|
|
314
|
+
version?: string | null;
|
|
315
|
+
home?: string | null;
|
|
316
|
+
}): string | null;
|
|
317
|
+
/** A real agent-run outcome — the evidence behind `last used ok` / `last auth failure`. */
|
|
318
|
+
export type RunAuthOutcome = {
|
|
319
|
+
ok: true;
|
|
320
|
+
} | {
|
|
321
|
+
ok: false;
|
|
322
|
+
verdict: 'revoked' | 'expired' | 'rate_limited' | 'error';
|
|
323
|
+
detail?: string;
|
|
324
|
+
resetsAt?: number | null;
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Record a REAL run's auth outcome into the auth-health cache with
|
|
328
|
+
* `source: 'run'`. Best-effort and never throws: an unattributable run (no
|
|
329
|
+
* accountId/version/home) writes nothing, and the underlying write already
|
|
330
|
+
* swallows IO errors. Keyed exactly as the catalog reads, so the recorded fact
|
|
331
|
+
* surfaces on the same account row.
|
|
332
|
+
*/
|
|
333
|
+
export declare function recordRunAuthOutcome(opts: {
|
|
334
|
+
agent: AgentId | string;
|
|
335
|
+
accountId?: string | null;
|
|
336
|
+
version?: string | null;
|
|
337
|
+
home?: string | null;
|
|
338
|
+
account?: string;
|
|
339
|
+
outcome: RunAuthOutcome;
|
|
340
|
+
host?: string;
|
|
341
|
+
now?: number;
|
|
342
|
+
}): void;
|
|
343
|
+
/** `Sep 20 14:02` — the fact timestamp for an auth failure. */
|
|
344
|
+
export declare function factTimestamp(ms: number): string;
|
|
345
|
+
/**
|
|
346
|
+
* The auth FACT `agents view` / `agents accounts` render per account per box
|
|
347
|
+
* (PHNX-4116): what actually happened here, with its time — never a word that
|
|
348
|
+
* means "we did not look". A `live` row (a recorded run OR a real probe) reads
|
|
349
|
+
* `last used ok <age>`; a server rejection reads `last auth failure <detail>
|
|
350
|
+
* <time>`; a throttle reads `rate-limited <detail> (<time>)`; anything with no
|
|
351
|
+
* usable evidence — including `no_evidence`/`unverified` — reads `not used on
|
|
352
|
+
* this box yet`.
|
|
353
|
+
*/
|
|
354
|
+
export declare function formatAuthFact(health: AuthHealth | null | undefined, now?: number): string;
|
|
287
355
|
export {};
|
package/dist/lib/auth-health.js
CHANGED
|
@@ -27,6 +27,7 @@ import { getVersionHomePath, listInstalledVersions } from './installations/versi
|
|
|
27
27
|
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
28
28
|
import { selfConfiguredDeviceRole } from './device-config.js';
|
|
29
29
|
import { mayIssueUsageEndpointProbe, trySpendUsageApiCall } from './usage-refresh.js';
|
|
30
|
+
import { machineId } from './machine-id.js';
|
|
30
31
|
/** Maximum age of an auth verdict used for automatic routing decisions. */
|
|
31
32
|
export const AUTH_PROBE_MAX_AGE_MS = 20 * 60_000;
|
|
32
33
|
/** Agents with a live network probe wired up today. The rest are best-effort. */
|
|
@@ -82,6 +83,7 @@ const VERDICT_GLYPHS = {
|
|
|
82
83
|
expired: '○', // ○
|
|
83
84
|
rate_limited: '◐', // ◐
|
|
84
85
|
unverified: '◐', // ◐
|
|
86
|
+
no_evidence: '◌', // ◌ — credential present, no evidence either way
|
|
85
87
|
unconfigured: '·', // ·
|
|
86
88
|
error: '·', // ·
|
|
87
89
|
};
|
|
@@ -97,6 +99,7 @@ export function verdictLabel(verdict) {
|
|
|
97
99
|
case 'expired': return 'expired';
|
|
98
100
|
case 'rate_limited': return 'limited';
|
|
99
101
|
case 'unverified': return 'unverified';
|
|
102
|
+
case 'no_evidence': return 'no evidence';
|
|
100
103
|
case 'unconfigured': return '—';
|
|
101
104
|
case 'error': return '?';
|
|
102
105
|
}
|
|
@@ -109,7 +112,7 @@ export function summarizeVerdicts(verdicts) {
|
|
|
109
112
|
for (const v of verdicts) {
|
|
110
113
|
if (v === 'live')
|
|
111
114
|
live++;
|
|
112
|
-
else if (v === 'unverified')
|
|
115
|
+
else if (v === 'unverified' || v === 'no_evidence')
|
|
113
116
|
present++;
|
|
114
117
|
else if (v === 'revoked')
|
|
115
118
|
bad++;
|
|
@@ -132,6 +135,7 @@ export function verdictColor(verdict) {
|
|
|
132
135
|
case 'live': return 'green';
|
|
133
136
|
case 'revoked': return 'red';
|
|
134
137
|
case 'unverified': return 'gray';
|
|
138
|
+
case 'no_evidence': return 'gray';
|
|
135
139
|
case 'unconfigured': return 'dim';
|
|
136
140
|
default: return 'yellow'; // expired / rate_limited / error — soft, self-healing/indeterminate
|
|
137
141
|
}
|
|
@@ -188,6 +192,9 @@ export function summarizeHostAuth(cache, host) {
|
|
|
188
192
|
case 'unverified':
|
|
189
193
|
present++;
|
|
190
194
|
break; // signed in, no probe — benign
|
|
195
|
+
case 'no_evidence':
|
|
196
|
+
present++;
|
|
197
|
+
break; // credential present, no evidence — benign
|
|
191
198
|
case 'revoked':
|
|
192
199
|
revoked++;
|
|
193
200
|
break; // server said no — re-login
|
|
@@ -437,6 +444,14 @@ function verdictFromFreshUsage(usageKey, signedIn, now) {
|
|
|
437
444
|
const capturedAt = snapshot?.capturedAt?.getTime();
|
|
438
445
|
if (!capturedAt || now - capturedAt >= FRESH_USAGE_VERDICT_MAX_AGE_MS)
|
|
439
446
|
return null;
|
|
447
|
+
// A `sync`-sourced snapshot arrived from ANOTHER box's poller over the fleet
|
|
448
|
+
// store — it proves the shared setup-token works SOMEWHERE, not that THIS box
|
|
449
|
+
// can authenticate (PHNX-4116). Deriving `live` from it is exactly the false
|
|
450
|
+
// "we did not look here, but a synced file is fresh, so call it live" the
|
|
451
|
+
// ticket removes. Only a reading THIS box captured itself (statusline/poll)
|
|
452
|
+
// is admissible live evidence.
|
|
453
|
+
if (snapshot?.freshness?.source === 'sync')
|
|
454
|
+
return null;
|
|
440
455
|
const ageMin = Math.max(1, Math.round((now - capturedAt) / 60_000));
|
|
441
456
|
return { verdict: 'live', checkedAt: now, detail: `token proven live by a usage fetch ${ageMin}m ago` };
|
|
442
457
|
}
|
|
@@ -461,15 +476,21 @@ export async function probeAuthHealth(agent, home, opts) {
|
|
|
461
476
|
role: selfConfiguredDeviceRole(),
|
|
462
477
|
forceLive: opts?.forceLive,
|
|
463
478
|
})) {
|
|
479
|
+
// A worker's setup-token cannot read the usage endpoint (RUSH-2392), so this
|
|
480
|
+
// box has no probe evidence for the account. This is NOT `unverified` (which
|
|
481
|
+
// conflated it with codex/grok's permanent no-probe-endpoint state): it is
|
|
482
|
+
// "we did not look here". A worker never publishes it — probeLocalFleetAuth
|
|
483
|
+
// drops `no_evidence` like `unconfigured` — so the account's facts come from
|
|
484
|
+
// token presence + recorded run outcomes instead (PHNX-4116).
|
|
464
485
|
return {
|
|
465
|
-
verdict: '
|
|
486
|
+
verdict: 'no_evidence',
|
|
466
487
|
checkedAt,
|
|
467
488
|
detail: 'setup-token box does not probe the usage endpoint',
|
|
468
489
|
};
|
|
469
490
|
}
|
|
470
491
|
if (opts?.forceLive !== true && usageScope && !trySpendUsageApiCall(usageScope, agent, checkedAt)) {
|
|
471
492
|
return {
|
|
472
|
-
verdict: '
|
|
493
|
+
verdict: 'no_evidence',
|
|
473
494
|
checkedAt,
|
|
474
495
|
detail: 'usage-endpoint budget already spent this hour',
|
|
475
496
|
};
|
|
@@ -573,7 +594,12 @@ export async function probeLocalFleetAuth(opts) {
|
|
|
573
594
|
const health = await probeAuthHealth(rep.agent, rep.home, { cliVersion: opts?.cliVersion, info: rep.info, forceLive: opts?.forceLive, signal: opts?.signal });
|
|
574
595
|
health.account = authAccountLabel(rep.info);
|
|
575
596
|
health.accountId = rep.accountId;
|
|
576
|
-
|
|
597
|
+
health.source = 'probe';
|
|
598
|
+
// `unconfigured` (no credential) and `no_evidence` (a worker that cannot probe)
|
|
599
|
+
// both write NO row: the first has nothing to say, the second says only "we
|
|
600
|
+
// did not look", which is not a fleet-publishable verdict — the account's
|
|
601
|
+
// facts come from token presence + recorded run outcomes instead (PHNX-4116).
|
|
602
|
+
if (health.verdict === 'unconfigured' || health.verdict === 'no_evidence') {
|
|
577
603
|
perGroup.push([]);
|
|
578
604
|
}
|
|
579
605
|
else {
|
|
@@ -618,3 +644,106 @@ export function writeFleetAuthRows(host, rows, installed) {
|
|
|
618
644
|
: undefined;
|
|
619
645
|
writeAuthHealthEntries(entries, drop);
|
|
620
646
|
}
|
|
647
|
+
// ---------------------------------------------------------------------------
|
|
648
|
+
// Run-outcome recording + auth-fact rendering (PHNX-4116)
|
|
649
|
+
//
|
|
650
|
+
// A worker never probes, so the honest evidence that its token authenticates is
|
|
651
|
+
// a REAL run: a successful inference (`live`) or an auth failure. These rows are
|
|
652
|
+
// written with `source: 'run'` and read back as a FACT — `last used ok 12m ago`
|
|
653
|
+
// / `last auth failure 401 Sep 20 14:02` / `not used on this box yet` — instead
|
|
654
|
+
// of the misleading verdict word the ticket removes.
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
/**
|
|
657
|
+
* The installed-version label a run outcome falls back to when the account has no
|
|
658
|
+
* slot on this box: the explicit `version`, else the label derived from `home` (a
|
|
659
|
+
* version home is `<versionDir>/home`, so the label is its parent dir's basename).
|
|
660
|
+
* Null when neither resolves. Pure (no fs) so it is unit-tested directly.
|
|
661
|
+
*/
|
|
662
|
+
export function runOutcomeVersionKey(opts) {
|
|
663
|
+
if (opts.version)
|
|
664
|
+
return opts.version;
|
|
665
|
+
if (opts.home) {
|
|
666
|
+
const label = path.basename(path.dirname(opts.home));
|
|
667
|
+
if (label && label !== '.' && label !== path.sep)
|
|
668
|
+
return label;
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Record a REAL run's auth outcome into the auth-health cache with
|
|
674
|
+
* `source: 'run'`. Best-effort and never throws: an unattributable run (no
|
|
675
|
+
* accountId/version/home) writes nothing, and the underlying write already
|
|
676
|
+
* swallows IO errors. Keyed exactly as the catalog reads, so the recorded fact
|
|
677
|
+
* surfaces on the same account row.
|
|
678
|
+
*/
|
|
679
|
+
export function recordRunAuthOutcome(opts) {
|
|
680
|
+
// Key exactly as the account catalog READS (slot key first, then the version
|
|
681
|
+
// label) so the fact lands on the row: the slot key ONLY when a slot dir
|
|
682
|
+
// actually exists for this account on this box (workers), else the installed
|
|
683
|
+
// version label (headed native logins have no slot). A last-resort slot key
|
|
684
|
+
// keeps an attributable-by-id run recorded even with no version hint.
|
|
685
|
+
const slotKey = opts.accountId && readSlots(readMeta())[opts.accountId]
|
|
686
|
+
? slotAuthVersionKey(opts.accountId)
|
|
687
|
+
: null;
|
|
688
|
+
const versionKey = slotKey
|
|
689
|
+
?? runOutcomeVersionKey(opts)
|
|
690
|
+
?? (opts.accountId ? slotAuthVersionKey(opts.accountId) : null);
|
|
691
|
+
if (!versionKey)
|
|
692
|
+
return;
|
|
693
|
+
const checkedAt = opts.now ?? Date.now();
|
|
694
|
+
const host = opts.host ?? machineId();
|
|
695
|
+
let detail;
|
|
696
|
+
if (opts.outcome.ok) {
|
|
697
|
+
detail = 'run ok';
|
|
698
|
+
}
|
|
699
|
+
else if (opts.outcome.verdict === 'rate_limited' && opts.outcome.resetsAt) {
|
|
700
|
+
detail = `until ${factClock(opts.outcome.resetsAt)}`;
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
detail = opts.outcome.detail;
|
|
704
|
+
}
|
|
705
|
+
const health = {
|
|
706
|
+
verdict: opts.outcome.ok ? 'live' : opts.outcome.verdict,
|
|
707
|
+
checkedAt,
|
|
708
|
+
source: 'run',
|
|
709
|
+
...(detail ? { detail } : {}),
|
|
710
|
+
...(opts.account ? { account: opts.account } : {}),
|
|
711
|
+
...(opts.accountId ? { accountId: opts.accountId } : {}),
|
|
712
|
+
};
|
|
713
|
+
writeAuthHealthEntries({ [authCacheKey(host, opts.agent, versionKey)]: health });
|
|
714
|
+
}
|
|
715
|
+
const FACT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
716
|
+
/** `HH:MM` local clock — used for a throttle reset time. */
|
|
717
|
+
function factClock(ms) {
|
|
718
|
+
const d = new Date(ms);
|
|
719
|
+
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
720
|
+
}
|
|
721
|
+
/** `Sep 20 14:02` — the fact timestamp for an auth failure. */
|
|
722
|
+
export function factTimestamp(ms) {
|
|
723
|
+
const d = new Date(ms);
|
|
724
|
+
return `${FACT_MONTHS[d.getMonth()]} ${d.getDate()} ${factClock(ms)}`;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* The auth FACT `agents view` / `agents accounts` render per account per box
|
|
728
|
+
* (PHNX-4116): what actually happened here, with its time — never a word that
|
|
729
|
+
* means "we did not look". A `live` row (a recorded run OR a real probe) reads
|
|
730
|
+
* `last used ok <age>`; a server rejection reads `last auth failure <detail>
|
|
731
|
+
* <time>`; a throttle reads `rate-limited <detail> (<time>)`; anything with no
|
|
732
|
+
* usable evidence — including `no_evidence`/`unverified` — reads `not used on
|
|
733
|
+
* this box yet`.
|
|
734
|
+
*/
|
|
735
|
+
export function formatAuthFact(health, now = Date.now()) {
|
|
736
|
+
if (!health)
|
|
737
|
+
return 'not used on this box yet';
|
|
738
|
+
switch (health.verdict) {
|
|
739
|
+
case 'live':
|
|
740
|
+
return `last used ok ${formatCheckedAge(health.checkedAt, now)}`;
|
|
741
|
+
case 'revoked':
|
|
742
|
+
case 'expired':
|
|
743
|
+
return `last auth failure${health.detail ? ` ${health.detail}` : ''} ${factTimestamp(health.checkedAt)}`;
|
|
744
|
+
case 'rate_limited':
|
|
745
|
+
return `rate-limited${health.detail ? ` ${health.detail}` : ''} (${factTimestamp(health.checkedAt)})`;
|
|
746
|
+
default:
|
|
747
|
+
return 'not used on this box yet';
|
|
748
|
+
}
|
|
749
|
+
}
|