@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
|
@@ -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
|
|
@@ -212,9 +212,18 @@ export function hasStaleUsage(candidate, nowMs = Date.now()) {
|
|
|
212
212
|
const capturedAt = snapshot?.capturedAt;
|
|
213
213
|
if (!capturedAt || !snapshot?.windows.length)
|
|
214
214
|
return false;
|
|
215
|
+
// A row that arrived via sync (D1, PHNX-4116) cannot be refreshed on THIS box —
|
|
216
|
+
// the account's poller lives on the headed device that published it, not here.
|
|
217
|
+
// Its age is shown, never used to refuse: a blind synced pool takes a
|
|
218
|
+
// floor-weight pick (capacityWeight → UNVERIFIED_WEIGHT) and
|
|
219
|
+
// rotationFailoverChain handles a real 429. Only a locally-captured reading a
|
|
220
|
+
// broken poller left hours old is genuinely "stale" for a refusal. See the
|
|
221
|
+
// GWT-E5d amendment.
|
|
222
|
+
if (snapshot.freshness?.source === 'sync')
|
|
223
|
+
return false;
|
|
215
224
|
return nowMs - capturedAt.getTime() > USAGE_STALE_REFUSAL_MAX_AGE_MS;
|
|
216
225
|
}
|
|
217
|
-
function hasUsageAvailable(candidate) {
|
|
226
|
+
function hasUsageAvailable(candidate, now = Date.now()) {
|
|
218
227
|
const snapshot = candidate.usageSnapshot;
|
|
219
228
|
if (snapshot) {
|
|
220
229
|
// Eligibility mirrors the `agents view` throttle badge exactly
|
|
@@ -225,7 +234,7 @@ function hasUsageAvailable(candidate) {
|
|
|
225
234
|
// stayed "eligible" and the router kept launching into it while `ag view`
|
|
226
235
|
// showed it rate-limited. Capacity *weighting* still ranks eligible accounts
|
|
227
236
|
// by weekly headroom; this gate only decides can-it-run-right-now.
|
|
228
|
-
const status = deriveUsageStatusFromSnapshot(snapshot);
|
|
237
|
+
const status = deriveUsageStatusFromSnapshot(snapshot, now);
|
|
229
238
|
if (status !== null)
|
|
230
239
|
return status !== 'rate_limited';
|
|
231
240
|
}
|
|
@@ -279,11 +288,11 @@ export function readinessFromCandidate(candidate, now = Date.now(), model) {
|
|
|
279
288
|
return { ready: false, reason: 'model_limited', email: candidate.email };
|
|
280
289
|
}
|
|
281
290
|
}
|
|
282
|
-
if (hasUsageAvailable(candidate)) {
|
|
291
|
+
if (hasUsageAvailable(candidate, now)) {
|
|
283
292
|
return { ready: true };
|
|
284
293
|
}
|
|
285
294
|
const snap = candidate.usageSnapshot;
|
|
286
|
-
const snapRateLimited = !!snap && snap.windows.length > 0 && deriveUsageStatusFromSnapshot(snap) === 'rate_limited';
|
|
295
|
+
const snapRateLimited = !!snap && snap.windows.length > 0 && deriveUsageStatusFromSnapshot(snap, now) === 'rate_limited';
|
|
287
296
|
const reason = !snapRateLimited && candidate.usageStatus === 'out_of_credits' ? 'out_of_credits' : 'rate_limited';
|
|
288
297
|
return { ready: false, reason, email: candidate.email };
|
|
289
298
|
}
|
|
@@ -573,11 +582,11 @@ export function pickAvailableCandidate(candidates, preferredVersion, nowMs = Dat
|
|
|
573
582
|
export function classifyHarnessCandidates(byHarness, nowMs = Date.now()) {
|
|
574
583
|
const summaries = [];
|
|
575
584
|
for (const [agent, candidates] of byHarness) {
|
|
576
|
-
const eligible = candidates.filter((c) => isRotationEligible(c));
|
|
585
|
+
const eligible = candidates.filter((c) => isRotationEligible(c, nowMs));
|
|
577
586
|
if (eligible.length === 0) {
|
|
578
587
|
const counts = new Map();
|
|
579
588
|
for (const c of candidates) {
|
|
580
|
-
const readiness = readinessFromCandidate(c);
|
|
589
|
+
const readiness = readinessFromCandidate(c, nowMs);
|
|
581
590
|
const reason = readiness.ready ? 'ineligible' : readiness.reason;
|
|
582
591
|
counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
583
592
|
}
|
|
@@ -668,7 +677,7 @@ export function formatNoHealthyAccountError(agent, strategy, excluded, nowMs = D
|
|
|
668
677
|
const excludedStr = excluded.length === 0
|
|
669
678
|
? 'no installed versions'
|
|
670
679
|
: excluded.map((c) => {
|
|
671
|
-
const readiness = readinessFromCandidate(c);
|
|
680
|
+
const readiness = readinessFromCandidate(c, nowMs);
|
|
672
681
|
const reason = readiness.ready ? 'ineligible' : readiness.reason;
|
|
673
682
|
return `${c.version} (${reason})`;
|
|
674
683
|
}).join(', ');
|
|
@@ -700,7 +709,12 @@ export function formatNoVerifiedUsageError(agent, strategy, candidates, nowMs =
|
|
|
700
709
|
? 'no signed-in accounts'
|
|
701
710
|
: candidates.map((c) => {
|
|
702
711
|
const age = snapshotAgeMinutes(c, nowMs);
|
|
703
|
-
|
|
712
|
+
// A synced row's age never drives a refusal (see hasStaleUsage), so name
|
|
713
|
+
// it as synced rather than letting the operator read it as the culprit.
|
|
714
|
+
const synced = c.usageSnapshot?.freshness?.source === 'sync';
|
|
715
|
+
const staleness = age === null
|
|
716
|
+
? 'no usage snapshot'
|
|
717
|
+
: `usage ${age}m old${synced ? ', synced' : ''}`;
|
|
704
718
|
return `${c.version} (${staleness})`;
|
|
705
719
|
}).join(', ');
|
|
706
720
|
const maxAgeMin = Math.round(USAGE_STALE_REFUSAL_MAX_AGE_MS / 60_000);
|
|
@@ -1 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/** Stdin grew past {@link REMOTE_STDOUT_MAX_BYTES}; the payload was refused unread. */
|
|
2
|
+
export declare class UsageIngestInputTooLargeError extends Error {
|
|
3
|
+
readonly limitBytes: number;
|
|
4
|
+
constructor(limitBytes: number);
|
|
5
|
+
}
|
|
6
|
+
export declare function runUsageIngest(argv?: string[]): Promise<number>;
|
|
@@ -1,35 +1,80 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `agents __usage-ingest` receiver
|
|
2
|
+
* The `agents __usage-ingest` receiver — the worker side of the usage exchange
|
|
3
|
+
* (PHNX-3392 usage-sync, PHNX-4116 transport over SSH).
|
|
3
4
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* A headed peer pipes its daemon-state envelope ({@link FleetStateExchangePayload},
|
|
6
|
+
* v2) to our stdin. We store it as that peer's `devices/<peer>/daemon-state.json`
|
|
7
|
+
* (stamped `receivedAt`) and merge its usage rows into the local cache
|
|
8
|
+
* newest-wins ({@link applyPeerFleetState}). With `--reply` we then refresh our
|
|
9
|
+
* own fields (session digests, reserved-auth verdict, and usage if this box is
|
|
10
|
+
* headed) and print our own envelope to stdout after a marker line, so the
|
|
11
|
+
* headed box holds this device's state without a second round-trip. Without
|
|
12
|
+
* `--reply` nothing is written to stdout: the ready probe runs this verb ahead
|
|
13
|
+
* of `agents --version`/`agents view --json` in one shell and parses that
|
|
14
|
+
* stdout, so a silent ingest is what keeps the probe parseable.
|
|
9
15
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `ingestPeerClaudeUsageRows` like every other cache writer.
|
|
16
|
+
* The legacy v1 envelope (`{v:1, rows}`) from an older headed peer is still
|
|
17
|
+
* merged. Hidden internal verb — intercepted in index.ts before bootstrap, so
|
|
18
|
+
* it never triggers an update check or a detached sync.
|
|
14
19
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* Exit codes: 0 = applied (or nothing to apply — an empty payload is not an
|
|
21
|
+
* error), 2 = malformed or oversized input. It fails loud on a bad envelope
|
|
22
|
+
* rather than silently accepting a wrong shape, but a busy cache lock degrades
|
|
23
|
+
* to best-effort inside `ingestPeerClaudeUsageRows` like every other cache
|
|
24
|
+
* writer. Stdin is bounded at {@link REMOTE_STDOUT_MAX_BYTES} — the same ceiling
|
|
25
|
+
* the dialer puts on a peer's reply — so a runaway pusher cannot grow this
|
|
26
|
+
* every-15-min receiver's heap; an overflow stops the read, exits 2, and writes
|
|
27
|
+
* nothing.
|
|
28
|
+
*
|
|
29
|
+
* The payload arrives on stdin, EXCEPT on a Windows receiver: the `agents.ps1`
|
|
30
|
+
* shim does not forward ssh-piped stdin to the node process, so the pusher
|
|
31
|
+
* writes the payload to a temp file and passes `--from <path>`
|
|
32
|
+
* (`buildWindowsStdinAgentsCommand`).
|
|
21
33
|
*/
|
|
22
34
|
import * as fs from 'fs';
|
|
35
|
+
import { REMOTE_STDOUT_MAX_BYTES, RemoteUtf8Accumulator } from '../ssh-exec.js';
|
|
23
36
|
import { ingestPeerClaudeUsageRows } from './usage.js';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
import { applyPeerFleetState, buildFleetStatePayload, formatFleetStateReply, parseFleetStateExchangeInput, publishOwnFleetState, } from './usage-sync.js';
|
|
38
|
+
/** Stdin grew past {@link REMOTE_STDOUT_MAX_BYTES}; the payload was refused unread. */
|
|
39
|
+
export class UsageIngestInputTooLargeError extends Error {
|
|
40
|
+
limitBytes;
|
|
41
|
+
constructor(limitBytes) {
|
|
42
|
+
super(`stdin payload exceeds ${limitBytes} bytes (${limitBytes / (1024 * 1024)} MiB); refusing it unread`);
|
|
43
|
+
this.limitBytes = limitBytes;
|
|
44
|
+
this.name = 'UsageIngestInputTooLargeError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read stdin to EOF, bounded at {@link REMOTE_STDOUT_MAX_BYTES}. On overflow the
|
|
49
|
+
* stream is destroyed so the pusher gets EPIPE instead of a full drain, and the
|
|
50
|
+
* promise rejects with {@link UsageIngestInputTooLargeError} — the caller exits
|
|
51
|
+
* 2 without parsing or writing anything.
|
|
52
|
+
*/
|
|
53
|
+
function readStdin(limitBytes = REMOTE_STDOUT_MAX_BYTES) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const acc = new RemoteUtf8Accumulator();
|
|
56
|
+
let bytes = 0;
|
|
57
|
+
let settled = false;
|
|
28
58
|
process.stdin.on('data', (chunk) => {
|
|
29
|
-
|
|
59
|
+
if (settled)
|
|
60
|
+
return;
|
|
61
|
+
bytes += chunk.byteLength;
|
|
62
|
+
if (bytes > limitBytes) {
|
|
63
|
+
settled = true;
|
|
64
|
+
process.stdin.destroy();
|
|
65
|
+
reject(new UsageIngestInputTooLargeError(limitBytes));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
acc.write(chunk);
|
|
30
69
|
});
|
|
31
|
-
process.stdin.on('end', () =>
|
|
32
|
-
|
|
70
|
+
process.stdin.on('end', () => { if (!settled) {
|
|
71
|
+
settled = true;
|
|
72
|
+
resolve(acc.end());
|
|
73
|
+
} });
|
|
74
|
+
process.stdin.on('error', () => { if (!settled) {
|
|
75
|
+
settled = true;
|
|
76
|
+
resolve(acc.end());
|
|
77
|
+
} });
|
|
33
78
|
});
|
|
34
79
|
}
|
|
35
80
|
/** `--from <path>` reads the payload from a file instead of stdin (Windows path). */
|
|
@@ -37,8 +82,9 @@ function fromFileArg(argv) {
|
|
|
37
82
|
const i = argv.indexOf('--from');
|
|
38
83
|
return i !== -1 && argv[i + 1] ? argv[i + 1] : null;
|
|
39
84
|
}
|
|
40
|
-
export async function runUsageIngest() {
|
|
41
|
-
const
|
|
85
|
+
export async function runUsageIngest(argv = process.argv.slice(3)) {
|
|
86
|
+
const reply = argv.includes('--reply');
|
|
87
|
+
const fromPath = fromFileArg(argv);
|
|
42
88
|
let source;
|
|
43
89
|
if (fromPath) {
|
|
44
90
|
try {
|
|
@@ -50,28 +96,52 @@ export async function runUsageIngest() {
|
|
|
50
96
|
}
|
|
51
97
|
}
|
|
52
98
|
else {
|
|
53
|
-
|
|
99
|
+
try {
|
|
100
|
+
source = await readStdin();
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
if (!(err instanceof UsageIngestInputTooLargeError))
|
|
104
|
+
throw err;
|
|
105
|
+
process.stderr.write(`[agents] __usage-ingest: ${err.name}: ${err.message}\n`);
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
54
108
|
}
|
|
55
109
|
const raw = source.trim();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
110
|
+
const errors = [];
|
|
111
|
+
if (raw) {
|
|
112
|
+
let payload;
|
|
113
|
+
try {
|
|
114
|
+
payload = parseFleetStateExchangeInput(raw);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
process.stderr.write(`[agents] __usage-ingest: ${err.message}\n`);
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
if (payload.v === 1) {
|
|
121
|
+
await ingestPeerClaudeUsageRows(payload.rows);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
try {
|
|
125
|
+
await applyPeerFleetState(payload.state);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
// A well-formed envelope we refuse (it names this device) is the
|
|
129
|
+
// sender's mistake, not malformed input: say so on stderr and in the
|
|
130
|
+
// reply, and still answer so the sender learns what this box holds.
|
|
131
|
+
const message = err.message;
|
|
132
|
+
process.stderr.write(`[agents] __usage-ingest: ${message}\n`);
|
|
133
|
+
errors.push(`ingest: ${message}`);
|
|
134
|
+
if (!reply)
|
|
135
|
+
return 2;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
74
138
|
}
|
|
75
|
-
|
|
139
|
+
if (!reply)
|
|
140
|
+
return 0;
|
|
141
|
+
const published = await publishOwnFleetState();
|
|
142
|
+
errors.push(...published.errors);
|
|
143
|
+
for (const message of published.errors)
|
|
144
|
+
process.stderr.write(`[agents] __usage-ingest: ${message}\n`);
|
|
145
|
+
process.stdout.write(formatFleetStateReply(buildFleetStatePayload({ errors })));
|
|
76
146
|
return 0;
|
|
77
147
|
}
|