@phnx-labs/agents-cli 1.22.84 → 1.22.85
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 +57 -0
- package/README.md +80 -53
- package/dist/bootstrap.d.ts +7 -6
- package/dist/bootstrap.js +20 -27
- package/dist/cli/command-registry.js +5 -1
- package/dist/commands/accounts.d.ts +6 -13
- package/dist/commands/accounts.js +330 -143
- package/dist/commands/apply.js +1 -1
- package/dist/commands/auth-mint.d.ts +7 -3
- package/dist/commands/auth-mint.js +21 -9
- package/dist/commands/auth.js +2 -3
- package/dist/commands/browser.js +10 -10
- package/dist/commands/daemon.d.ts +7 -3
- package/dist/commands/daemon.js +40 -25
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/exec.js +87 -39
- package/dist/commands/fleet-capture.js +21 -2
- package/dist/commands/harness-wizard.js +2 -2
- package/dist/commands/lease.js +7 -12
- package/dist/commands/profiles.d.ts +2 -2
- package/dist/commands/profiles.js +20 -13
- package/dist/commands/repo.js +3 -3
- package/dist/commands/run-account-picker.d.ts +3 -3
- package/dist/commands/run-account-picker.js +29 -30
- package/dist/commands/secrets-passthrough.d.ts +20 -0
- package/dist/commands/secrets-passthrough.js +43 -0
- package/dist/commands/setup-accounts.d.ts +3 -2
- package/dist/commands/setup-accounts.js +4 -4
- package/dist/commands/setup-secrets.d.ts +16 -21
- package/dist/commands/setup-secrets.js +43 -224
- package/dist/commands/ssh.js +2 -2
- package/dist/commands/sync.js +1 -1
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.js +43 -40
- package/dist/commands/versions.d.ts +11 -11
- package/dist/commands/versions.js +131 -83
- package/dist/commands/view.d.ts +1 -9
- package/dist/commands/view.js +41 -55
- package/dist/commands/webhook.js +5 -5
- package/dist/commands/workflows.js +2 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +13 -42
- package/dist/lib/account-capabilities.d.ts +1 -1
- package/dist/lib/account-capabilities.js +1 -1
- package/dist/lib/account-catalog.d.ts +121 -6
- package/dist/lib/account-catalog.js +421 -7
- package/dist/lib/account-registry.d.ts +8 -30
- package/dist/lib/account-registry.js +36 -79
- package/dist/lib/account-schema.d.ts +1 -1
- package/dist/lib/account-schema.js +1 -1
- package/dist/lib/accounting/account-pool-collect.js +16 -4
- package/dist/lib/accounting/rotate.d.ts +20 -7
- package/dist/lib/accounting/rotate.js +89 -13
- package/dist/lib/accounting/usage.js +11 -16
- package/dist/lib/accounts/add.d.ts +138 -0
- package/dist/lib/accounts/add.js +651 -0
- package/dist/lib/accounts/migrate.d.ts +117 -0
- package/dist/lib/accounts/migrate.js +536 -0
- package/dist/lib/accounts/slots.d.ts +13 -0
- package/dist/lib/accounts/slots.js +100 -0
- package/dist/lib/agent-spec/agents.d.ts +16 -0
- package/dist/lib/agent-spec/agents.js +32 -4
- package/dist/lib/app-bundle-install.js +5 -4
- package/dist/lib/auth-health.d.ts +3 -0
- package/dist/lib/auth-health.js +11 -0
- package/dist/lib/auth-mint.d.ts +52 -16
- package/dist/lib/auth-mint.js +117 -39
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +5 -5
- package/dist/lib/byok-usage.js +3 -3
- package/dist/lib/claude-account-token.d.ts +47 -3
- package/dist/lib/claude-account-token.js +172 -50
- package/dist/lib/cloud/antigravity.js +4 -4
- package/dist/lib/cloud/cursor.js +4 -4
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +6 -6
- package/dist/lib/crabbox/runtimes.d.ts +3 -3
- package/dist/lib/crabbox/runtimes.js +5 -5
- package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
- package/dist/lib/daemon/account-state-daemon-service.js +199 -3
- package/dist/lib/daemon/auth-sync-service.js +28 -1
- package/dist/lib/daemon/daemon.d.ts +6 -16
- package/dist/lib/daemon/daemon.js +52 -97
- package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
- package/dist/lib/daemon/daemon.test-fixture.js +22 -40
- package/dist/lib/daemon/harness-update-service.d.ts +1 -1
- package/dist/lib/daemon/harness-update-service.js +1 -1
- package/dist/lib/daemon/runner.d.ts +15 -3
- package/dist/lib/daemon/runner.js +49 -24
- package/dist/lib/daemon-health.d.ts +1 -2
- package/dist/lib/daemon-health.js +7 -6
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +1 -11
- package/dist/lib/daemon-webhooks.d.ts +8 -7
- package/dist/lib/daemon-webhooks.js +10 -9
- package/dist/lib/device-config.js +1 -1
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/harness-inventory.d.ts +39 -0
- package/dist/lib/devices/harness-inventory.js +126 -4
- package/dist/lib/doctor-diff.js +2 -1
- package/dist/lib/exec-account-home.d.ts +38 -0
- package/dist/lib/exec-account-home.js +164 -0
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +101 -31
- package/dist/lib/fleet/apply.d.ts +2 -2
- package/dist/lib/fleet/apply.js +4 -4
- package/dist/lib/fleet/auth-sync.js +4 -3
- package/dist/lib/fleet-shared-repo-sync.js +6 -9
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapter.js +10 -6
- package/dist/lib/harness/adapters/grok.js +8 -3
- package/dist/lib/harness/adapters/muse.js +1 -1
- package/dist/lib/harness/adapters/opencode.js +12 -3
- package/dist/lib/harness-auth-capabilities.d.ts +61 -0
- package/dist/lib/harness-auth-capabilities.js +52 -0
- package/dist/lib/helper-versions.d.ts +6 -4
- package/dist/lib/helper-versions.js +5 -4
- package/dist/lib/hosts/credential-transport.d.ts +10 -0
- package/dist/lib/hosts/credential-transport.js +37 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +13 -6
- package/dist/lib/identity/client.d.ts +3 -3
- package/dist/lib/identity/client.js +3 -3
- package/dist/lib/installations/index.d.ts +1 -1
- package/dist/lib/installations/index.js +1 -1
- package/dist/lib/installations/migrate.js +9 -0
- package/dist/lib/installations/resolve.js +1 -1
- package/dist/lib/installations/shims.d.ts +15 -0
- package/dist/lib/installations/shims.js +69 -0
- package/dist/lib/installations/store.d.ts +43 -0
- package/dist/lib/installations/store.js +75 -7
- package/dist/lib/installations/versions.js +1 -1
- package/dist/lib/menubar/install-menubar.d.ts +3 -3
- package/dist/lib/menubar/install-menubar.js +3 -3
- package/dist/lib/native-accounts.d.ts +35 -0
- package/dist/lib/native-accounts.js +41 -0
- package/dist/lib/net-close.d.ts +11 -0
- package/dist/lib/net-close.js +25 -0
- package/dist/lib/openclaw-keychain.js +27 -1
- package/dist/lib/profiles.js +22 -9
- package/dist/lib/project-resources.js +1 -1
- package/dist/lib/reserved-stores.d.ts +53 -0
- package/dist/lib/reserved-stores.js +120 -0
- package/dist/lib/secrets-client.d.ts +191 -0
- package/dist/lib/secrets-client.js +710 -0
- package/dist/lib/secrets-policy.d.ts +222 -0
- package/dist/lib/secrets-policy.js +483 -0
- package/dist/lib/secrets-types.d.ts +188 -0
- package/dist/lib/secrets-types.js +24 -0
- package/dist/lib/service-manifest.js +3 -4
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +47 -14
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/sha256-asset.d.ts +11 -16
- package/dist/lib/sha256-asset.js +11 -16
- package/dist/lib/share/config.js +14 -5
- package/dist/lib/signin-badge.d.ts +26 -0
- package/dist/lib/signin-badge.js +42 -0
- package/dist/lib/staleness/detectors/workflows.js +3 -105
- package/dist/lib/staleness/writers/workflows.js +2 -1
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +18 -7
- package/dist/lib/sync-umbrella.js +2 -2
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +74 -33
- package/dist/lib/view-types.d.ts +6 -3
- package/dist/lib/workflows-registry.d.ts +71 -0
- package/dist/lib/workflows-registry.js +280 -0
- package/dist/lib/workflows.d.ts +21 -21
- package/dist/lib/workflows.js +12 -327
- package/package.json +2 -3
- package/scripts/postinstall.js +4 -29
- package/dist/commands/secrets-import.d.ts +0 -18
- package/dist/commands/secrets-import.js +0 -74
- package/dist/commands/secrets-migrate.d.ts +0 -25
- package/dist/commands/secrets-migrate.js +0 -334
- package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
- package/dist/commands/secrets-rotate-passphrase.js +0 -96
- package/dist/commands/secrets-sync.d.ts +0 -11
- package/dist/commands/secrets-sync.js +0 -153
- package/dist/commands/secrets-vault.d.ts +0 -10
- package/dist/commands/secrets-vault.js +0 -130
- package/dist/commands/secrets.d.ts +0 -212
- package/dist/commands/secrets.js +0 -3030
- package/dist/lib/accounts/connect.d.ts +0 -176
- package/dist/lib/accounts/connect.js +0 -453
- package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
- package/dist/lib/daemon/keychain-reap-service.js +0 -32
- package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
- package/dist/lib/daemon/secrets-broker-service.js +0 -51
- package/dist/lib/secrets/agent.d.ts +0 -358
- package/dist/lib/secrets/agent.js +0 -1291
- package/dist/lib/secrets/audit.d.ts +0 -46
- package/dist/lib/secrets/audit.js +0 -101
- package/dist/lib/secrets/bundles.d.ts +0 -290
- package/dist/lib/secrets/bundles.js +0 -1547
- package/dist/lib/secrets/download-keychain.d.ts +0 -47
- package/dist/lib/secrets/download-keychain.js +0 -70
- package/dist/lib/secrets/drivers/rush.d.ts +0 -14
- package/dist/lib/secrets/drivers/rush.js +0 -90
- package/dist/lib/secrets/fallback.d.ts +0 -48
- package/dist/lib/secrets/fallback.js +0 -48
- package/dist/lib/secrets/filestore.d.ts +0 -222
- package/dist/lib/secrets/filestore.js +0 -1099
- package/dist/lib/secrets/headless.d.ts +0 -43
- package/dist/lib/secrets/headless.js +0 -56
- package/dist/lib/secrets/icloud-import.d.ts +0 -79
- package/dist/lib/secrets/icloud-import.js +0 -206
- package/dist/lib/secrets/index.d.ts +0 -451
- package/dist/lib/secrets/index.js +0 -1568
- package/dist/lib/secrets/install-helper.d.ts +0 -72
- package/dist/lib/secrets/install-helper.js +0 -245
- package/dist/lib/secrets/lease.d.ts +0 -25
- package/dist/lib/secrets/lease.js +0 -44
- package/dist/lib/secrets/linux.d.ts +0 -77
- package/dist/lib/secrets/linux.js +0 -393
- package/dist/lib/secrets/list-filter.d.ts +0 -109
- package/dist/lib/secrets/list-filter.js +0 -261
- package/dist/lib/secrets/mcp.d.ts +0 -93
- package/dist/lib/secrets/mcp.js +0 -211
- package/dist/lib/secrets/profiles.d.ts +0 -10
- package/dist/lib/secrets/profiles.js +0 -13
- package/dist/lib/secrets/push.d.ts +0 -133
- package/dist/lib/secrets/push.js +0 -273
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
- package/dist/lib/secrets/rc-hygiene.js +0 -143
- package/dist/lib/secrets/read-backoff.d.ts +0 -27
- package/dist/lib/secrets/read-backoff.js +0 -64
- package/dist/lib/secrets/reaper.d.ts +0 -90
- package/dist/lib/secrets/reaper.js +0 -243
- package/dist/lib/secrets/remote.d.ts +0 -152
- package/dist/lib/secrets/remote.js +0 -344
- package/dist/lib/secrets/reserved-sync.d.ts +0 -66
- package/dist/lib/secrets/reserved-sync.js +0 -147
- package/dist/lib/secrets/scope.d.ts +0 -26
- package/dist/lib/secrets/scope.js +0 -29
- package/dist/lib/secrets/session-store.d.ts +0 -107
- package/dist/lib/secrets/session-store.js +0 -342
- package/dist/lib/secrets/sync-backend.d.ts +0 -48
- package/dist/lib/secrets/sync-backend.js +0 -13
- package/dist/lib/secrets/sync-commands.d.ts +0 -21
- package/dist/lib/secrets/sync-commands.js +0 -21
- package/dist/lib/secrets/sync.d.ts +0 -48
- package/dist/lib/secrets/sync.js +0 -237
- package/dist/lib/secrets/unlock-hints.d.ts +0 -27
- package/dist/lib/secrets/unlock-hints.js +0 -36
- package/dist/lib/secrets/usage-db.d.ts +0 -46
- package/dist/lib/secrets/usage-db.js +0 -96
- package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
- package/dist/lib/secrets/vault-age-helper.js +0 -34
- package/dist/lib/secrets/vault.d.ts +0 -49
- package/dist/lib/secrets/vault.js +0 -399
- package/dist/lib/secrets/windows.d.ts +0 -81
- package/dist/lib/secrets/windows.js +0 -556
- package/scripts/install-helper.js +0 -97
- /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
- /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
|
@@ -15,10 +15,14 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import { ALL_AGENT_IDS, accountDisplayLabel, getAccountInfo, } from '../agents.js';
|
|
18
|
-
import { deriveUsageStatusFromSnapshot, getUsageInfoByIdentity, getUsageLookupKey, usageErrorForDisplay, } from '../accounting/usage.js';
|
|
18
|
+
import { deriveUsageStatusFromSnapshot, classifyUsageErrorKind, getUsageInfoByIdentity, getUsageLookupKey, usageErrorForDisplay, } from '../accounting/usage.js';
|
|
19
19
|
import { getVersionHomePath, listInstalledVersions } from '../installations/store.js';
|
|
20
20
|
import { readMeta } from '../state.js';
|
|
21
21
|
import { findNativeAccountByIdentity } from '../account-registry.js';
|
|
22
|
+
import { authCacheKey, readAuthHealthCache } from '../auth-health.js';
|
|
23
|
+
import { machineId } from '../machine-id.js';
|
|
24
|
+
import { fixFor } from '../signin-badge.js';
|
|
25
|
+
import { harnessWorkerIsPerDevice } from '../harness-auth-capabilities.js';
|
|
22
26
|
/**
|
|
23
27
|
* Roll one usage snapshot into a {@link QuotaSummary}. Mirrors the blocking-window
|
|
24
28
|
* selection of {@link deriveUsageStatusFromSnapshot} (the model-specific
|
|
@@ -79,6 +83,51 @@ export function summarizeQuota(snapshot, unavailableReason = null, accountStatus
|
|
|
79
83
|
unavailableReason: null,
|
|
80
84
|
};
|
|
81
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Scope/permission failures on a worker mean the usage endpoint cannot be read.
|
|
88
|
+
* They are not evidence of exhausted credits, even when stale account metadata
|
|
89
|
+
* previously carried that coarse status.
|
|
90
|
+
*/
|
|
91
|
+
export function summarizeObservedQuota(snapshot, error, accountStatus = null) {
|
|
92
|
+
const displayError = usageErrorForDisplay(error);
|
|
93
|
+
const unverified = classifyUsageErrorKind(error) === 'headless-scope'
|
|
94
|
+
|| /scope|permission/i.test(displayError ?? '');
|
|
95
|
+
return {
|
|
96
|
+
quota: unverified
|
|
97
|
+
? summarizeQuota(null, displayError, null)
|
|
98
|
+
: summarizeQuota(snapshot, displayError, accountStatus),
|
|
99
|
+
unverified,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A scope/permission failure on the usage endpoint is not a credit claim.
|
|
104
|
+
* Strip utilization and, only when auth still looks healthy, surface
|
|
105
|
+
* `unverified` instead of `live`/`rate_limited`. A real auth failure
|
|
106
|
+
* (expired/revoked/missing) stays the auth failure.
|
|
107
|
+
*/
|
|
108
|
+
export function applyUsageHonesty(verdict, usage) {
|
|
109
|
+
if (!usage)
|
|
110
|
+
return { verdict, usage };
|
|
111
|
+
const reason = usage.unavailableReason;
|
|
112
|
+
const scopeUnknown = !!reason && (classifyUsageErrorKind(reason) === 'headless-scope'
|
|
113
|
+
|| /scope|permission|headless/i.test(reason));
|
|
114
|
+
if (scopeUnknown) {
|
|
115
|
+
const stripped = {
|
|
116
|
+
...usage,
|
|
117
|
+
status: null,
|
|
118
|
+
verdict: 'unavailable',
|
|
119
|
+
usedPercent: null,
|
|
120
|
+
};
|
|
121
|
+
if (verdict === 'live' || verdict === 'rate_limited') {
|
|
122
|
+
return { verdict: 'unverified', usage: stripped };
|
|
123
|
+
}
|
|
124
|
+
return { verdict, usage: stripped };
|
|
125
|
+
}
|
|
126
|
+
if (usage.status === 'rate_limited' && (verdict === 'live' || verdict === 'unverified')) {
|
|
127
|
+
return { verdict: 'rate_limited', usage };
|
|
128
|
+
}
|
|
129
|
+
return { verdict, usage };
|
|
130
|
+
}
|
|
82
131
|
/**
|
|
83
132
|
* "Ready" = signed in AND not rate-limited. A missing quota snapshot does NOT
|
|
84
133
|
* block readiness: the account is signed in and usable, we just have no live
|
|
@@ -119,6 +168,8 @@ export async function collectLocalHarnessInventory(opts) {
|
|
|
119
168
|
? (await getUsageInfoByIdentity(usageInputs, opts?.refresh ? { forceRefresh: true } : undefined)).usageByKey
|
|
120
169
|
: new Map();
|
|
121
170
|
const meta = readMeta();
|
|
171
|
+
const authCache = readAuthHealthCache();
|
|
172
|
+
const host = machineId();
|
|
122
173
|
return pending.map(({ agent, version, info }) => {
|
|
123
174
|
const key = getUsageLookupKey(info);
|
|
124
175
|
const usage = key ? usageByKey.get(key) : undefined;
|
|
@@ -126,13 +177,34 @@ export async function collectLocalHarnessInventory(opts) {
|
|
|
126
177
|
// Normalize the internal 'stale' not-collected sentinel out before it reaches
|
|
127
178
|
// the JSON `quota.unavailableReason` — same leak PHNX-3348 fixed for
|
|
128
179
|
// `agents view --json`, here for `agents devices harnesses/accounts --json`.
|
|
129
|
-
const
|
|
180
|
+
const observed = summarizeObservedQuota(snapshot, usage?.error, info?.usageStatus ?? null);
|
|
181
|
+
const quota = observed.quota;
|
|
130
182
|
const signedIn = !!info?.signedIn;
|
|
131
|
-
const { ready, reason } = computeReady(signedIn, quota);
|
|
132
183
|
const display = info ? accountDisplayLabel(info) || null : null;
|
|
133
184
|
const saved = findNativeAccountByIdentity(meta, agent, info);
|
|
134
185
|
const account = saved ? `${saved.name} · ${display || saved.identityLabel || saved.identityKey}` : display;
|
|
135
|
-
|
|
186
|
+
const cachedVerdict = authCache[authCacheKey(host, agent, version)]?.verdict;
|
|
187
|
+
const fromAuth = cachedVerdict === 'unconfigured' || !signedIn
|
|
188
|
+
? 'missing'
|
|
189
|
+
: cachedVerdict === 'error' || !cachedVerdict
|
|
190
|
+
? 'unverified'
|
|
191
|
+
: cachedVerdict;
|
|
192
|
+
const honest = applyUsageHonesty(fromAuth, quota);
|
|
193
|
+
const verdict = honest.verdict;
|
|
194
|
+
const honestQuota = honest.usage ?? quota;
|
|
195
|
+
const { ready, reason } = computeReady(signedIn, honestQuota);
|
|
196
|
+
const provisioning = harnessWorkerIsPerDevice(agent) ? 'per-device' : 'portable';
|
|
197
|
+
return {
|
|
198
|
+
agent,
|
|
199
|
+
version,
|
|
200
|
+
account,
|
|
201
|
+
signedIn,
|
|
202
|
+
quota: honestQuota,
|
|
203
|
+
ready,
|
|
204
|
+
reason,
|
|
205
|
+
verdict,
|
|
206
|
+
fix: fixFor({ agent, verdict, name: saved?.name, version, provisioning }),
|
|
207
|
+
};
|
|
136
208
|
});
|
|
137
209
|
}
|
|
138
210
|
/**
|
|
@@ -280,3 +352,53 @@ export function renderAccountsMatrix(results) {
|
|
|
280
352
|
lines.push(chalk.gray(' one row per account · agents = harnesses signed into it · ready = signed in and not rate-limited'));
|
|
281
353
|
return lines;
|
|
282
354
|
}
|
|
355
|
+
function fleetVerdictLabel(verdict) {
|
|
356
|
+
if (verdict === 'rate_limited')
|
|
357
|
+
return 'LIMITED';
|
|
358
|
+
return verdict.toUpperCase();
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Account × device matrix used by `agents accounts list --fleet`.
|
|
362
|
+
* Rows are logical accounts, not installations. Columns come from the
|
|
363
|
+
* daemon-state observations already joined onto each account row.
|
|
364
|
+
*/
|
|
365
|
+
export function renderAccountFleetMatrix(rows, opts = {}) {
|
|
366
|
+
const devices = [...new Set(rows.flatMap((row) => row.devices.map((device) => device.device)))].sort();
|
|
367
|
+
const accountLabels = rows.map((row) => `${row.harness}#${row.name ?? 'unnamed'} ${row.identityLabel}`);
|
|
368
|
+
const accountW = colWidth(accountLabels, 16);
|
|
369
|
+
const deviceWidths = new Map(devices.map((device) => [
|
|
370
|
+
device,
|
|
371
|
+
colWidth([
|
|
372
|
+
device,
|
|
373
|
+
...rows.map((row) => fleetVerdictLabel(row.devices.find((item) => item.device === device)?.verdict ?? 'missing')),
|
|
374
|
+
], 7),
|
|
375
|
+
]));
|
|
376
|
+
const lines = [chalk.bold('Fleet accounts')];
|
|
377
|
+
if (rows.length === 0) {
|
|
378
|
+
lines.push(chalk.gray(' no accounts'));
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
lines.push(` ${'ACCOUNT'.padEnd(accountW)} `
|
|
382
|
+
+ devices.map((device) => device.padEnd(deviceWidths.get(device))).join(' '));
|
|
383
|
+
for (const [index, row] of rows.entries()) {
|
|
384
|
+
const label = accountLabels[index].padEnd(accountW);
|
|
385
|
+
const cells = devices.map((device) => {
|
|
386
|
+
const verdict = row.devices.find((item) => item.device === device)?.verdict ?? 'missing';
|
|
387
|
+
const plain = fleetVerdictLabel(verdict).padEnd(deviceWidths.get(device));
|
|
388
|
+
if (verdict === 'live')
|
|
389
|
+
return chalk.green(plain);
|
|
390
|
+
if (verdict === 'revoked' || verdict === 'expired' || verdict === 'missing')
|
|
391
|
+
return chalk.red(plain);
|
|
392
|
+
if (verdict === 'rate_limited')
|
|
393
|
+
return chalk.yellow(plain);
|
|
394
|
+
return chalk.gray(plain);
|
|
395
|
+
});
|
|
396
|
+
lines.push(` ${chalk.cyan(label)} ${cells.join(' ')}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const uncovered = (opts.uncoveredDevices ?? []).filter((device) => !devices.includes(device));
|
|
400
|
+
if (uncovered.length > 0) {
|
|
401
|
+
lines.push(chalk.gray(` ${uncovered.join(', ')}: daemon-state carries no account verdicts (older release)`));
|
|
402
|
+
}
|
|
403
|
+
return lines;
|
|
404
|
+
}
|
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -45,7 +45,8 @@ import { dirsContentMatch, filesContentMatch } from './resource-content-diff.js'
|
|
|
45
45
|
import { subagentContentMatches } from './subagents-registry.js';
|
|
46
46
|
import { getMcpServersByName, mcpServerMatches } from './mcp.js';
|
|
47
47
|
import { permissionsGroupMatches, PERMISSIONS_REPRESENTABLE } from './permissions.js';
|
|
48
|
-
import {
|
|
48
|
+
import { resolveWorkflowRef } from './workflows.js';
|
|
49
|
+
import { workflowContentMatches } from './workflows-registry.js';
|
|
49
50
|
import { listMemoryFacts, memoryTargetDir } from './memory.js';
|
|
50
51
|
import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, compareVersions, } from './installations/versions.js';
|
|
51
52
|
import { discoverPlugins, marketplaceSpecForName } from './plugins/plugins.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isSymlinkAdoptedHarness } from './installations/shims.js';
|
|
2
|
+
import type { AgentId, DeviceAccountSlot, Meta } from './types.js';
|
|
3
|
+
export { isAccountSlotDir } from './agents.js';
|
|
4
|
+
export { isSymlinkAdoptedHarness };
|
|
5
|
+
export type SpawnHomeSource = 'slot' | 'legacy-home' | 'provisioned';
|
|
6
|
+
export interface NativeSpawnHome {
|
|
7
|
+
execHome: string;
|
|
8
|
+
source: SpawnHomeSource;
|
|
9
|
+
slot?: DeviceAccountSlot;
|
|
10
|
+
/** Installation label when `source` is `legacy-home`. */
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function missingSlotHint(agent: AgentId, name: string): string;
|
|
14
|
+
export declare function symlinkAdoptedAccountError(agent: AgentId, name: string, defaultName: string | undefined): string;
|
|
15
|
+
export declare function adoptedSymlinkMismatchError(agent: AgentId, name: string, execHome: string): string;
|
|
16
|
+
/** The adopted `~/.<config>` symlink's resolved target, or null when missing / not a symlink. */
|
|
17
|
+
export declare function adoptedConfigTarget(agent: AgentId): string | null;
|
|
18
|
+
export declare function adoptedConfigPointsAtHome(agent: AgentId, home: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* For a symlink-adopted harness, point `~/.<config>` at this account's slot
|
|
21
|
+
* or throw. Does not write the harness default — the caller records that only
|
|
22
|
+
* after this succeeds.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ensureAdoptedDefaultRepoint(agent: AgentId, account: {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
agent: AgentId;
|
|
28
|
+
}, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the spawn HOME for a native account on THIS device.
|
|
31
|
+
* Existing slot, then a provisionable worker slot, then a leftover `homes`
|
|
32
|
+
* label, then identity match across installed homes, then fail loud.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveNativeSpawnHome(agent: AgentId, account: {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
agent: AgentId;
|
|
38
|
+
}, meta?: Pick<Meta, 'accounts' | 'deviceAccounts'>): Promise<NativeSpawnHome>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn-time native-account HOME resolution (PHNX-3940 T5).
|
|
3
|
+
*
|
|
4
|
+
* Order: existing slot → provisionable worker slot → recorded leftover `homes`
|
|
5
|
+
* label → identity match across installed version homes → fail loud with the
|
|
6
|
+
* T4 hint. Never a wrong home that looks like success. Identity matching is
|
|
7
|
+
* the pre-T5 fallback (`resolveAccountVersion` over each installed home's own
|
|
8
|
+
* credential file) so a native account whose backfill never wrote a `homes`
|
|
9
|
+
* label still spawns from the home that actually holds its login.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as os from 'node:os';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { listNativeAccounts, nativeAccountHome } from './account-registry.js';
|
|
15
|
+
import { readSlots } from './accounts/slots.js';
|
|
16
|
+
import { workerProvisioningHint } from './accounts/add.js';
|
|
17
|
+
import { AGENTS, credentialPresence, getAccountInfo } from './agents.js';
|
|
18
|
+
import { AUTH_BUNDLE, claudeAccountTokenKey, provisionWorkerSlot, readReservedCredential } from './claude-account-token.js';
|
|
19
|
+
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from './device-config.js';
|
|
20
|
+
import { harnessWorkerKinds } from './harness-auth-capabilities.js';
|
|
21
|
+
import { getAgentConfigPath, isSymlinkAdoptedHarness, repointAdoptedConfigToHome } from './installations/shims.js';
|
|
22
|
+
import { getVersionHomePath, listInstalledVersions } from './installations/versions.js';
|
|
23
|
+
export { isAccountSlotDir } from './agents.js';
|
|
24
|
+
export { isSymlinkAdoptedHarness };
|
|
25
|
+
export function missingSlotHint(agent, name) {
|
|
26
|
+
const role = selfConfiguredDeviceRole();
|
|
27
|
+
if (!isHeadedDeviceRole(role)) {
|
|
28
|
+
return `${agent}#${name} has no slot on this device. `
|
|
29
|
+
+ `Add the account on your personal device with \`agents accounts add ${agent} ${name}\`; `
|
|
30
|
+
+ `workers are provisioned from the durable credential automatically `
|
|
31
|
+
+ `(${agent}: ${workerProvisioningHint(agent)}).`;
|
|
32
|
+
}
|
|
33
|
+
return `${agent}#${name} has no slot on this device. Sign in with: agents accounts login ${agent}#${name}`;
|
|
34
|
+
}
|
|
35
|
+
export function symlinkAdoptedAccountError(agent, name, defaultName) {
|
|
36
|
+
if (!defaultName) {
|
|
37
|
+
return `${agent} holds one active account per device (adopted ~/.<config> symlink). `
|
|
38
|
+
+ `Set the default first: agents accounts default ${agent} ${name}`;
|
|
39
|
+
}
|
|
40
|
+
return `${agent} holds one active account per device (adopted ~/.<config> symlink). `
|
|
41
|
+
+ `'${name}' is not the default ('${defaultName}'). `
|
|
42
|
+
+ `Switch with: agents accounts default ${agent} ${name}`;
|
|
43
|
+
}
|
|
44
|
+
export function adoptedSymlinkMismatchError(agent, name, execHome) {
|
|
45
|
+
return `${agent} default is '${name}' but the adopted ~/.<config> does not point at that account's home (${execHome}). `
|
|
46
|
+
+ `Switch with: agents accounts default ${agent} ${name}`;
|
|
47
|
+
}
|
|
48
|
+
/** The adopted `~/.<config>` symlink's resolved target, or null when missing / not a symlink. */
|
|
49
|
+
export function adoptedConfigTarget(agent) {
|
|
50
|
+
const configPath = getAgentConfigPath(agent);
|
|
51
|
+
try {
|
|
52
|
+
const stat = fs.lstatSync(configPath);
|
|
53
|
+
if (!stat.isSymbolicLink())
|
|
54
|
+
return null;
|
|
55
|
+
return path.resolve(path.dirname(configPath), fs.readlinkSync(configPath));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function adoptedConfigPointsAtHome(agent, home) {
|
|
62
|
+
if (!isSymlinkAdoptedHarness(agent))
|
|
63
|
+
return true;
|
|
64
|
+
const current = adoptedConfigTarget(agent);
|
|
65
|
+
if (!current)
|
|
66
|
+
return false;
|
|
67
|
+
const configDirName = path.relative(os.homedir(), AGENTS[agent].configDir);
|
|
68
|
+
return current === path.resolve(home, configDirName);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* For a symlink-adopted harness, point `~/.<config>` at this account's slot
|
|
72
|
+
* or throw. Does not write the harness default — the caller records that only
|
|
73
|
+
* after this succeeds.
|
|
74
|
+
*/
|
|
75
|
+
export function ensureAdoptedDefaultRepoint(agent, account, meta) {
|
|
76
|
+
if (!isSymlinkAdoptedHarness(agent))
|
|
77
|
+
return;
|
|
78
|
+
const slot = readSlots(meta)[account.id];
|
|
79
|
+
if (!slot || !fs.existsSync(slot.slotDir)) {
|
|
80
|
+
throw new Error(`${agent}#${account.name} has no slot on this device, so the adopted ~/.<config> cannot be pointed at it. `
|
|
81
|
+
+ missingSlotHint(agent, account.name));
|
|
82
|
+
}
|
|
83
|
+
const result = repointAdoptedConfigToHome(agent, slot.slotDir);
|
|
84
|
+
if (!result.success) {
|
|
85
|
+
throw new Error(result.error ?? `Failed to point ${agent} at account '${account.name}'.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function nativeRow(accountId, meta) {
|
|
89
|
+
return listNativeAccounts(meta).find((row) => row.id === accountId);
|
|
90
|
+
}
|
|
91
|
+
function isProvisionableWorker(account) {
|
|
92
|
+
if (isHeadedDeviceRole(selfConfiguredDeviceRole()))
|
|
93
|
+
return false;
|
|
94
|
+
const durable = harnessWorkerKinds(account.agent).some((kind) => kind === 'setup-token' || kind.startsWith('api-key'));
|
|
95
|
+
if (!durable)
|
|
96
|
+
return false;
|
|
97
|
+
if (account.workerCredential) {
|
|
98
|
+
return readReservedCredential(account.workerCredential.bundle, account.workerCredential.key) != null;
|
|
99
|
+
}
|
|
100
|
+
// Pre-T1 Claude rows key the legacy `auth` bundle by email.
|
|
101
|
+
if (account.agent === 'claude' && account.identityLabel) {
|
|
102
|
+
return readReservedCredential(AUTH_BUNDLE, claudeAccountTokenKey(account.identityLabel)) != null;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
async function matchLegacyIdentityHome(agent, account, meta) {
|
|
107
|
+
const row = nativeRow(account.id, meta);
|
|
108
|
+
const needles = [row?.identityKey, row?.identityLabel]
|
|
109
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
110
|
+
.map((value) => value.trim().toLowerCase());
|
|
111
|
+
if (needles.length === 0)
|
|
112
|
+
return null;
|
|
113
|
+
const needleSet = new Set(needles);
|
|
114
|
+
const preferred = nativeAccountHome(account.id, meta);
|
|
115
|
+
const versions = listInstalledVersions(agent);
|
|
116
|
+
const ordered = preferred && versions.includes(preferred)
|
|
117
|
+
? [preferred, ...versions.filter((version) => version !== preferred)]
|
|
118
|
+
: versions;
|
|
119
|
+
for (const label of ordered) {
|
|
120
|
+
const home = getVersionHomePath(agent, label);
|
|
121
|
+
if (!fs.existsSync(home))
|
|
122
|
+
continue;
|
|
123
|
+
const info = await getAccountInfo(agent, home);
|
|
124
|
+
const presence = credentialPresence(agent, home);
|
|
125
|
+
if (!info.signedIn)
|
|
126
|
+
continue;
|
|
127
|
+
if (presence.knownLocation && !presence.perVersion)
|
|
128
|
+
continue;
|
|
129
|
+
const keys = [info.accountKey, info.email]
|
|
130
|
+
.filter((value) => typeof value === 'string' && value.length > 0)
|
|
131
|
+
.map((value) => value.toLowerCase());
|
|
132
|
+
if (keys.some((key) => needleSet.has(key)))
|
|
133
|
+
return { home, label };
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolve the spawn HOME for a native account on THIS device.
|
|
139
|
+
* Existing slot, then a provisionable worker slot, then a leftover `homes`
|
|
140
|
+
* label, then identity match across installed homes, then fail loud.
|
|
141
|
+
*/
|
|
142
|
+
export async function resolveNativeSpawnHome(agent, account, meta = { accounts: undefined, deviceAccounts: undefined }) {
|
|
143
|
+
const slot = readSlots(meta)[account.id];
|
|
144
|
+
if (slot && fs.existsSync(slot.slotDir)) {
|
|
145
|
+
return { execHome: slot.slotDir, source: 'slot', slot };
|
|
146
|
+
}
|
|
147
|
+
const row = nativeRow(account.id, meta);
|
|
148
|
+
if (row && isProvisionableWorker(row)) {
|
|
149
|
+
const provisioned = provisionWorkerSlot(row);
|
|
150
|
+
return { execHome: provisioned.slotDir, source: 'provisioned', slot: provisioned };
|
|
151
|
+
}
|
|
152
|
+
const label = nativeAccountHome(account.id, meta);
|
|
153
|
+
if (label) {
|
|
154
|
+
const legacyHome = getVersionHomePath(agent, label);
|
|
155
|
+
if (fs.existsSync(legacyHome)) {
|
|
156
|
+
return { execHome: legacyHome, source: 'legacy-home', label };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const matched = await matchLegacyIdentityHome(agent, account, meta);
|
|
160
|
+
if (matched) {
|
|
161
|
+
return { execHome: matched.home, source: 'legacy-home', label: matched.label };
|
|
162
|
+
}
|
|
163
|
+
throw new Error(missingSlotHint(agent, account.name));
|
|
164
|
+
}
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -133,6 +133,12 @@ export interface ExecOptions {
|
|
|
133
133
|
version?: string;
|
|
134
134
|
/** Version home whose native auth/config is overlaid onto this run's binary. */
|
|
135
135
|
configVersion?: string;
|
|
136
|
+
/**
|
|
137
|
+
* HOME-shaped slot dir whose native auth/config is overlaid onto this run's
|
|
138
|
+
* binary (PHNX-3940 T5). Wins over {@link configVersion}. The binary still
|
|
139
|
+
* comes from {@link version} (the managed install unless `@<label>` pins one).
|
|
140
|
+
*/
|
|
141
|
+
execHome?: string;
|
|
136
142
|
/** Omit to launch the CLI interactively -- no prompt, no --print, stdio fully inherited. */
|
|
137
143
|
prompt?: string;
|
|
138
144
|
/** Force interactive mode even when a prompt is provided. Wins over `headless`. */
|
|
@@ -656,6 +662,30 @@ export type ClaudeRefusalAction = {
|
|
|
656
662
|
action: 'none';
|
|
657
663
|
};
|
|
658
664
|
export declare function classifyClaudeRunRefusal(output: string, exitCode: number): ClaudeRefusalAction;
|
|
665
|
+
/**
|
|
666
|
+
* Parse Codex's usage-limit refusal reset. Codex prints
|
|
667
|
+
* `ERROR: You've hit your usage limit. … try again at Sep 12th, 2026 8:32 AM.`
|
|
668
|
+
* (or `…try again at 8:32 AM.` for the 5-hour window). The reset is what makes
|
|
669
|
+
* this a session-style limit that auto-clears on its clock rather than a
|
|
670
|
+
* clock-less billing exhaustion. Ordinal suffixes (`12th`) are stripped so
|
|
671
|
+
* `Date.parse` accepts the date; a time-only reset resolves against today/tomorrow.
|
|
672
|
+
* Returns null when there is no future reset to parse (caller then leaves the
|
|
673
|
+
* marker untouched rather than persisting a sticky one it cannot expire).
|
|
674
|
+
*/
|
|
675
|
+
export declare function parseCodexUsageLimitReset(text: string, nowMs?: number): Date | null;
|
|
676
|
+
/**
|
|
677
|
+
* Classify what a Codex run's output + exit code means for the account's
|
|
678
|
+
* persisted refusal marker — the Codex sibling of {@link classifyClaudeRunRefusal}
|
|
679
|
+
* (the `ClaudeRefusalAction` shape is harness-generic; the usage cache it drives
|
|
680
|
+
* is identity-keyed and shared across harnesses). Codex's usage limit is
|
|
681
|
+
* time-windowed and carries a reset ("try again at <date>"), so it is noted as a
|
|
682
|
+
* clock-bearing session limit that auto-clears — NOT a clock-less out_of_credits,
|
|
683
|
+
* which would stay sticky until a successful run the excluded account can never
|
|
684
|
+
* get. A genuine credit/quota exhaustion IS clock-less. A clean run clears any
|
|
685
|
+
* stale marker; a limit with no parseable reset is left untouched (the run still
|
|
686
|
+
* cascades via detectRateLimit) rather than persisting an unexpirable marker.
|
|
687
|
+
*/
|
|
688
|
+
export declare function classifyCodexRunRefusal(output: string, exitCode: number, nowMs?: number): ClaudeRefusalAction;
|
|
659
689
|
/**
|
|
660
690
|
* Patterns that indicate an authentication failure — the agent is logged out,
|
|
661
691
|
* its token was revoked, or the session expired. These are the user-visible
|
package/dist/lib/exec.js
CHANGED
|
@@ -15,7 +15,7 @@ import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled,
|
|
|
15
15
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
16
16
|
import { isTierToken, resolveTier } from './model-tiers.js';
|
|
17
17
|
import { emit, createTimer, redactPrompt, redactArgs } from './feed/events.js';
|
|
18
|
-
import { sanitizeProcessEnv } from './secrets
|
|
18
|
+
import { sanitizeProcessEnv } from './secrets-client.js';
|
|
19
19
|
import { resolveActor, actorEnv } from './actor.js';
|
|
20
20
|
import { expandLocalHome } from './project-root.js';
|
|
21
21
|
import { getShimsDir, getHistoryDir, getRuntimeStateDir } from './state.js';
|
|
@@ -360,8 +360,9 @@ export function buildExecEnv(options) {
|
|
|
360
360
|
if (configAdapter.applyExecConfigEnv) {
|
|
361
361
|
// Resolve version/versionHome here (this module already imports
|
|
362
362
|
// installations/versions); the adapters must not import it themselves or they
|
|
363
|
-
// close a versions -> shims -> harness -> adapter import cycle.
|
|
364
|
-
|
|
363
|
+
// close a versions -> shims -> harness -> adapter import cycle. execHome is
|
|
364
|
+
// the account slot (PHNX-3940 T5) and wins over a legacy configVersion label.
|
|
365
|
+
const { version, versionHome } = resolveExecConfigHome(options);
|
|
365
366
|
configAdapter.applyExecConfigEnv(result, {
|
|
366
367
|
agent: options.agent,
|
|
367
368
|
version,
|
|
@@ -373,16 +374,6 @@ export function buildExecEnv(options) {
|
|
|
373
374
|
}
|
|
374
375
|
else {
|
|
375
376
|
stripForeignConfigDir(result);
|
|
376
|
-
// Grok's adapter owns the same GROK_HOME export in generated shims, but a
|
|
377
|
-
// labeled account deliberately runs one version's binary with another
|
|
378
|
-
// version's config. Apply that overlay here because the shim's binary-slot
|
|
379
|
-
// export cannot express a distinct account slot.
|
|
380
|
-
if (options.agent === 'grok' && options.configVersion) {
|
|
381
|
-
const { versionHome } = resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion);
|
|
382
|
-
if (!versionHome)
|
|
383
|
-
throw new Error(`Cannot resolve Grok account config home for ${options.configVersion}.`);
|
|
384
|
-
result.GROK_HOME = path.join(versionHome, '.grok');
|
|
385
|
-
}
|
|
386
377
|
}
|
|
387
378
|
// Point the agent at its own mailbox so the PreToolUse `mailbox-inject` hook
|
|
388
379
|
// knows which box to drain and inject mid-run. Keyed by the session id — the
|
|
@@ -458,8 +449,16 @@ export function ensureVendorHomeDir(agent, versionHome) {
|
|
|
458
449
|
fs.mkdirSync(vendorHome, { recursive: true });
|
|
459
450
|
return vendorHome;
|
|
460
451
|
}
|
|
452
|
+
function resolveExecConfigHome(options) {
|
|
453
|
+
if (options.execHome) {
|
|
454
|
+
const resolved = options.version
|
|
455
|
+
?? resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.version).version;
|
|
456
|
+
return { version: resolved ?? null, versionHome: options.execHome };
|
|
457
|
+
}
|
|
458
|
+
return resolveConfigVersion(options.agent, options.cwd || process.cwd(), options.configVersion ?? options.version);
|
|
459
|
+
}
|
|
461
460
|
function ensureVendorHomeForSpawn(options) {
|
|
462
|
-
const { versionHome } =
|
|
461
|
+
const { versionHome } = resolveExecConfigHome(options);
|
|
463
462
|
if (versionHome)
|
|
464
463
|
ensureVendorHomeDir(options.agent, versionHome);
|
|
465
464
|
}
|
|
@@ -2158,6 +2157,73 @@ export function classifyClaudeRunRefusal(output, exitCode) {
|
|
|
2158
2157
|
return { action: 'clear' };
|
|
2159
2158
|
return { action: 'none' };
|
|
2160
2159
|
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Parse Codex's usage-limit refusal reset. Codex prints
|
|
2162
|
+
* `ERROR: You've hit your usage limit. … try again at Sep 12th, 2026 8:32 AM.`
|
|
2163
|
+
* (or `…try again at 8:32 AM.` for the 5-hour window). The reset is what makes
|
|
2164
|
+
* this a session-style limit that auto-clears on its clock rather than a
|
|
2165
|
+
* clock-less billing exhaustion. Ordinal suffixes (`12th`) are stripped so
|
|
2166
|
+
* `Date.parse` accepts the date; a time-only reset resolves against today/tomorrow.
|
|
2167
|
+
* Returns null when there is no future reset to parse (caller then leaves the
|
|
2168
|
+
* marker untouched rather than persisting a sticky one it cannot expire).
|
|
2169
|
+
*/
|
|
2170
|
+
export function parseCodexUsageLimitReset(text, nowMs = Date.now()) {
|
|
2171
|
+
// Gate on the CLI's own refusal phrasing, NOT a bare "usage limit" substring:
|
|
2172
|
+
// this runs against a 16KB stdout tail of `codex exec`, which streams the whole
|
|
2173
|
+
// agent transcript, so a session that merely *discusses* usage-limit code would
|
|
2174
|
+
// otherwise false-positive and wrongly mark a HEALTHY account excluded. Mirrors
|
|
2175
|
+
// the narrow Claude convention (`hit your session limit`).
|
|
2176
|
+
if (!/hit your usage limit/i.test(text))
|
|
2177
|
+
return null;
|
|
2178
|
+
const match = /try again (?:at|on)\s+([^.\n]+)/i.exec(text);
|
|
2179
|
+
if (!match)
|
|
2180
|
+
return null;
|
|
2181
|
+
const segment = match[1].trim().replace(/(\d{1,2})(st|nd|rd|th)\b/gi, '$1');
|
|
2182
|
+
const absolute = Date.parse(segment);
|
|
2183
|
+
if (!Number.isNaN(absolute)) {
|
|
2184
|
+
// A full date parsed — honor it only while still in the future; a past reset
|
|
2185
|
+
// means the window already recovered, so there is nothing to note. Do NOT
|
|
2186
|
+
// fall through to the clock path, which would understate the real date as
|
|
2187
|
+
// today/tomorrow.
|
|
2188
|
+
return absolute > nowMs ? new Date(absolute) : null;
|
|
2189
|
+
}
|
|
2190
|
+
// No date parsed — the 5-hour "try again at 8:32 AM" (time-only) form. Resolve
|
|
2191
|
+
// it against today, rolling to tomorrow when the clock has already passed.
|
|
2192
|
+
const clock = /(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i.exec(segment);
|
|
2193
|
+
if (!clock)
|
|
2194
|
+
return null;
|
|
2195
|
+
let hour = Number(clock[1]) % 12;
|
|
2196
|
+
if (clock[3].toLowerCase() === 'pm')
|
|
2197
|
+
hour += 12;
|
|
2198
|
+
const minute = clock[2] ? Number(clock[2]) : 0;
|
|
2199
|
+
const result = new Date(nowMs);
|
|
2200
|
+
result.setHours(hour, minute, 0, 0);
|
|
2201
|
+
if (result.getTime() <= nowMs)
|
|
2202
|
+
result.setDate(result.getDate() + 1);
|
|
2203
|
+
return result;
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Classify what a Codex run's output + exit code means for the account's
|
|
2207
|
+
* persisted refusal marker — the Codex sibling of {@link classifyClaudeRunRefusal}
|
|
2208
|
+
* (the `ClaudeRefusalAction` shape is harness-generic; the usage cache it drives
|
|
2209
|
+
* is identity-keyed and shared across harnesses). Codex's usage limit is
|
|
2210
|
+
* time-windowed and carries a reset ("try again at <date>"), so it is noted as a
|
|
2211
|
+
* clock-bearing session limit that auto-clears — NOT a clock-less out_of_credits,
|
|
2212
|
+
* which would stay sticky until a successful run the excluded account can never
|
|
2213
|
+
* get. A genuine credit/quota exhaustion IS clock-less. A clean run clears any
|
|
2214
|
+
* stale marker; a limit with no parseable reset is left untouched (the run still
|
|
2215
|
+
* cascades via detectRateLimit) rather than persisting an unexpirable marker.
|
|
2216
|
+
*/
|
|
2217
|
+
export function classifyCodexRunRefusal(output, exitCode, nowMs = Date.now()) {
|
|
2218
|
+
const reset = parseCodexUsageLimitReset(output, nowMs);
|
|
2219
|
+
if (reset)
|
|
2220
|
+
return { action: 'note_session', resetsAt: reset };
|
|
2221
|
+
if (detectOutOfCredits(output))
|
|
2222
|
+
return { action: 'note_out_of_credits' };
|
|
2223
|
+
if (exitCode === 0)
|
|
2224
|
+
return { action: 'clear' };
|
|
2225
|
+
return { action: 'none' };
|
|
2226
|
+
}
|
|
2161
2227
|
/**
|
|
2162
2228
|
* Patterns that indicate an authentication failure — the agent is logged out,
|
|
2163
2229
|
* its token was revoked, or the session expired. These are the user-visible
|
|
@@ -2380,24 +2446,28 @@ export async function runWithFallback(options) {
|
|
|
2380
2446
|
}
|
|
2381
2447
|
const output = `${result.stderr}\n${result.stdout}`;
|
|
2382
2448
|
// Persist a per-account refusal marker so rotation stops re-picking a
|
|
2383
|
-
// known-dead account: a session
|
|
2449
|
+
// known-dead account: a session/usage limit recovers on its clock, a billing
|
|
2384
2450
|
// exhaustion (tokens/credits) recovers only on a later successful run, and a
|
|
2385
|
-
// clean run clears any stale marker.
|
|
2386
|
-
//
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2451
|
+
// clean run clears any stale marker. Claude and Codex both surface these as
|
|
2452
|
+
// run-ending text, and the usage cache is identity-keyed (shared across
|
|
2453
|
+
// harnesses), so one path notes either. Decisions extracted + unit-tested in
|
|
2454
|
+
// classify{Claude,Codex}RunRefusal.
|
|
2455
|
+
const refusal = agent === 'claude'
|
|
2456
|
+
? classifyClaudeRunRefusal(output, result.exitCode ?? 1)
|
|
2457
|
+
: agent === 'codex'
|
|
2458
|
+
? classifyCodexRunRefusal(output, result.exitCode ?? 1)
|
|
2459
|
+
: null;
|
|
2460
|
+
const sessionLimitReset = refusal?.action === 'note_session' ? refusal.resetsAt : null;
|
|
2461
|
+
if (refusal && version && refusal.action !== 'none') {
|
|
2462
|
+
const account = await getAccountInfo(agent, getVersionHomePath(agent, version));
|
|
2463
|
+
const usageKey = getUsageLookupKey(account);
|
|
2464
|
+
if (usageKey) {
|
|
2465
|
+
if (refusal.action === 'note_session')
|
|
2466
|
+
noteClaudeSessionLimit(usageKey, refusal.resetsAt);
|
|
2467
|
+
else if (refusal.action === 'note_out_of_credits')
|
|
2468
|
+
noteClaudeOutOfCredits(usageKey);
|
|
2469
|
+
else if (refusal.action === 'clear')
|
|
2470
|
+
clearClaudeAccountRefusal(usageKey);
|
|
2401
2471
|
}
|
|
2402
2472
|
}
|
|
2403
2473
|
if (result.exitCode === 0 && !sessionLimitReset)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
|
|
9
9
|
*/
|
|
10
10
|
import type { DeviceProfile } from '../devices/registry.js';
|
|
11
|
-
import {
|
|
11
|
+
import type { RemoteBackend } from '../secrets-types.js';
|
|
12
12
|
import type { DeviceDesired, DeviceProbe, DeviceDiff, FleetAction, FleetPlan, AuthFilePayload } from './types.js';
|
|
13
13
|
/** Strip a version suffix from an agent spec: `claude@latest` -> `claude`. */
|
|
14
14
|
export declare function agentIdOf(spec: string): string;
|
|
@@ -149,7 +149,7 @@ export interface ExecContext {
|
|
|
149
149
|
dryRun?: boolean;
|
|
150
150
|
}
|
|
151
151
|
/** Execute one device's planned actions in order. Real SSH — no mocks. */
|
|
152
|
-
export declare function reconcileDevice(row: DeviceDiff, device: DeviceProfile, ctx: ExecContext): DeviceApplyResult
|
|
152
|
+
export declare function reconcileDevice(row: DeviceDiff, device: DeviceProfile, ctx: ExecContext): Promise<DeviceApplyResult>;
|
|
153
153
|
/** Run a pool of async tasks with a concurrency cap, preserving input order. */
|
|
154
154
|
export declare function pool<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
155
155
|
/** Reconcile every device row in parallel (capped). */
|
package/dist/lib/fleet/apply.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
|
|
9
9
|
*/
|
|
10
10
|
import * as os from 'os';
|
|
11
|
-
import { pushBundleToHost } from '../secrets
|
|
12
|
-
import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle, isReservedBundleName } from '../secrets
|
|
11
|
+
import { pushBundleToHost } from '../secrets-client.js';
|
|
12
|
+
import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle, isReservedBundleName } from '../secrets-policy.js';
|
|
13
13
|
import { deviceIdentityArgs, sshTargetFor } from '../devices/connect.js';
|
|
14
14
|
import { readyProbe, bootstrapAgentsCli } from '../hosts/ready.js';
|
|
15
15
|
import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
|
|
@@ -371,7 +371,7 @@ export function parseRemoteBundles(stdout) {
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
/** Execute one device's planned actions in order. Real SSH — no mocks. */
|
|
374
|
-
export function reconcileDevice(row, device, ctx) {
|
|
374
|
+
export async function reconcileDevice(row, device, ctx) {
|
|
375
375
|
if (!row.probe.reachable) {
|
|
376
376
|
return { device: row.device, ok: false, steps: [], note: row.probe.note ?? 'unreachable' };
|
|
377
377
|
}
|
|
@@ -438,7 +438,7 @@ export function reconcileDevice(row, device, ctx) {
|
|
|
438
438
|
}
|
|
439
439
|
const backend = isReservedBundleName(bundle) || device.platform === 'linux' ? 'file' : 'keychain';
|
|
440
440
|
try {
|
|
441
|
-
const out = pushBundleToHost(bundle, target, {
|
|
441
|
+
const out = await pushBundleToHost(bundle, target, {
|
|
442
442
|
remoteBackend: backend,
|
|
443
443
|
operation: `fleet apply ${row.device}`,
|
|
444
444
|
// No passphrase, ever, from this path. On the file backend the remote
|