@phnx-labs/agents-cli 1.22.116 → 1.22.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +247 -0
- package/dist/bootstrap.js +4 -11
- package/dist/commands/browser.d.ts +52 -70
- package/dist/commands/browser.js +192 -3562
- package/dist/commands/daemon.d.ts +39 -2
- package/dist/commands/daemon.js +72 -73
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +104 -12
- package/dist/commands/repo.js +13 -49
- package/dist/commands/route.js +5 -5
- package/dist/commands/routines.js +39 -12
- package/dist/commands/sessions-render.d.ts +1 -1
- package/dist/commands/sessions-render.js +0 -1
- package/dist/commands/sessions.test-fixture.d.ts +0 -1
- package/dist/commands/sessions.test-fixture.js +0 -37
- package/dist/commands/setup-browser.d.ts +17 -12
- package/dist/commands/setup-browser.js +52 -87
- package/dist/commands/setup-preferences.d.ts +0 -17
- package/dist/commands/setup-preferences.js +18 -49
- package/dist/commands/setup-term.d.ts +4 -3
- package/dist/commands/setup-term.js +5 -4
- package/dist/commands/setup.js +11 -11
- package/dist/commands/ssh.js +10 -1
- package/dist/commands/sync.js +6 -44
- package/dist/commands/teams.js +0 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +34 -6
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +47 -2
- package/dist/index.js +107 -26
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-catalog.d.ts +38 -0
- package/dist/lib/account-catalog.js +110 -26
- package/dist/lib/account-provider-registry.js +1 -1
- package/dist/lib/account-registry.d.ts +1 -1
- package/dist/lib/account-registry.js +36 -2
- package/dist/lib/accounting/rotate.js +22 -8
- package/dist/lib/accounting/usage-ingest.d.ts +6 -1
- package/dist/lib/accounting/usage-ingest.js +115 -45
- package/dist/lib/accounting/usage-sync.d.ts +134 -31
- package/dist/lib/accounting/usage-sync.js +227 -66
- package/dist/lib/accounting/usage.d.ts +16 -3
- package/dist/lib/accounting/usage.js +26 -8
- package/dist/lib/accounts/slots.d.ts +8 -0
- package/dist/lib/accounts/slots.js +17 -0
- package/dist/lib/add-dir.js +0 -1
- package/dist/lib/agent-cli-commands.js +0 -1
- package/dist/lib/agent-spec/agents.d.ts +6 -7
- package/dist/lib/agent-spec/agents.js +6 -60
- package/dist/lib/auth-health.d.ts +69 -1
- package/dist/lib/auth-health.js +133 -4
- package/dist/lib/browser/context.d.ts +82 -0
- package/dist/lib/browser/context.js +88 -0
- package/dist/lib/browser/paths.d.ts +25 -0
- package/dist/lib/browser/paths.js +51 -0
- package/dist/lib/browser/record.d.ts +38 -0
- package/dist/lib/browser/record.js +57 -0
- package/dist/lib/browser/sessions-list.js +1 -2
- package/dist/lib/browser-client.d.ts +158 -0
- package/dist/lib/browser-client.js +234 -0
- package/dist/lib/channels/owner-sink.d.ts +1 -18
- package/dist/lib/channels/owner-sink.js +29 -82
- package/dist/lib/channels/providers/rush.d.ts +7 -7
- package/dist/lib/channels/providers/rush.js +127 -57
- package/dist/lib/claude-statusline.d.ts +1 -1
- package/dist/lib/claude-statusline.js +15 -3
- package/dist/lib/commands.js +1 -1
- package/dist/lib/crabbox/lease.js +1 -1
- package/dist/lib/crabbox/runtimes.js +1 -3
- package/dist/lib/daemon/account-state-daemon-service.js +1 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
- package/dist/lib/daemon/auth-sync-service.js +37 -43
- package/dist/lib/daemon/daemon.d.ts +25 -14
- package/dist/lib/daemon/daemon.js +92 -154
- package/dist/lib/daemon/runner.d.ts +2 -2
- package/dist/lib/daemon/runner.js +32 -9
- package/dist/lib/daemon/self-update-service.d.ts +35 -34
- package/dist/lib/daemon/self-update-service.js +40 -36
- package/dist/lib/daemon/service.d.ts +12 -8
- package/dist/lib/daemon/supervisor.d.ts +65 -54
- package/dist/lib/daemon/supervisor.js +136 -171
- package/dist/lib/daemon/testdata/process-view-start.js +5 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
- package/dist/lib/daemon/usage-sync-service.js +46 -78
- package/dist/lib/daemon-health.d.ts +22 -5
- package/dist/lib/daemon-health.js +47 -3
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +0 -10
- package/dist/lib/daemon-ticks.js +4 -2
- package/dist/lib/devices/doctor-findings.js +8 -11
- package/dist/lib/devices/harness-inventory.js +14 -5
- package/dist/lib/exec-account-home.js +5 -0
- package/dist/lib/exec.js +0 -11
- package/dist/lib/feed/events.js +0 -1
- package/dist/lib/feed/tool-activity.js +5 -2
- package/dist/lib/fleet-shared-state.d.ts +45 -0
- package/dist/lib/fleet-shared-state.js +78 -9
- package/dist/lib/fs-atomic.js +25 -0
- package/dist/lib/harness-auth-capabilities.js +0 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hooks/cache.d.ts +7 -0
- package/dist/lib/hooks/cache.js +27 -2
- package/dist/lib/hooks/install.js +7 -1
- package/dist/lib/hosts/ready.d.ts +38 -8
- package/dist/lib/hosts/ready.js +62 -14
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/installations/active-check.d.ts +27 -0
- package/dist/lib/installations/active-check.js +61 -0
- package/dist/lib/installations/migrate.js +6 -3
- package/dist/lib/installations/shims.js +9 -7
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +12 -1
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/models.js +0 -2
- package/dist/lib/open-url.js +28 -54
- package/dist/lib/placement.d.ts +2 -0
- package/dist/lib/placement.js +5 -0
- package/dist/lib/profiles.js +0 -2
- package/dist/lib/refresh.js +1 -1
- package/dist/lib/routine-readiness.d.ts +35 -0
- package/dist/lib/routine-readiness.js +74 -16
- package/dist/lib/rules/compile.js +6 -0
- package/dist/lib/sandbox.js +0 -2
- package/dist/lib/secrets-policy.d.ts +89 -11
- package/dist/lib/secrets-policy.js +175 -28
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +65 -18
- package/dist/lib/session/discover.js +1 -283
- package/dist/lib/session/mirror.d.ts +4 -3
- package/dist/lib/session/mirror.js +4 -3
- package/dist/lib/session/parse.d.ts +0 -2
- package/dist/lib/session/parse.js +1 -138
- package/dist/lib/session/throughput.d.ts +1 -3
- package/dist/lib/session/throughput.js +0 -20
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +2 -2
- package/dist/lib/sessions-client.d.ts +60 -1
- package/dist/lib/sessions-client.js +227 -4
- package/dist/lib/setup-tool-status.js +8 -12
- package/dist/lib/signin-badge.d.ts +3 -2
- package/dist/lib/signin-badge.js +6 -3
- package/dist/lib/smart-launch.js +4 -1
- package/dist/lib/staleness/detectors/commands.d.ts +1 -1
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
- package/dist/lib/staleness/detectors/rules.d.ts +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +1 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.d.ts +1 -1
- package/dist/lib/staleness/writers/hooks.d.ts +1 -1
- package/dist/lib/staleness/writers/mcp.d.ts +1 -1
- package/dist/lib/staleness/writers/permissions.d.ts +1 -1
- package/dist/lib/staleness/writers/plugins.d.ts +1 -1
- package/dist/lib/staleness/writers/rules.d.ts +1 -1
- package/dist/lib/staleness/writers/skills.d.ts +1 -1
- package/dist/lib/staleness/writers/sources.js +5 -0
- package/dist/lib/staleness/writers/subagents.d.ts +1 -1
- package/dist/lib/staleness/writers/workflows.d.ts +1 -1
- package/dist/lib/state.d.ts +6 -4
- package/dist/lib/state.js +7 -17
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +1 -136
- package/dist/lib/teams/placement-probe.js +50 -15
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/types.d.ts +10 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/view-types.d.ts +32 -0
- package/dist/lib/watchdog/read.js +0 -1
- package/dist/lib/watchdog/rotate.js +1 -2
- package/dist/lib/watchdog/runner.js +2 -2
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdogTail.js +0 -31
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -8
- package/dist/browser.d.ts +0 -2
- package/dist/browser.js +0 -17
- package/dist/commands/browser-picker.d.ts +0 -18
- package/dist/commands/browser-picker.js +0 -97
- package/dist/lib/browser/arc-discovery.d.ts +0 -52
- package/dist/lib/browser/arc-discovery.js +0 -197
- package/dist/lib/browser/arc-dom.d.ts +0 -14
- package/dist/lib/browser/arc-dom.js +0 -121
- package/dist/lib/browser/caller-identity.d.ts +0 -40
- package/dist/lib/browser/caller-identity.js +0 -175
- package/dist/lib/browser/cdp.d.ts +0 -51
- package/dist/lib/browser/cdp.js +0 -254
- package/dist/lib/browser/chrome.d.ts +0 -141
- package/dist/lib/browser/chrome.js +0 -704
- package/dist/lib/browser/chromium-discovery.d.ts +0 -28
- package/dist/lib/browser/chromium-discovery.js +0 -96
- package/dist/lib/browser/devices.d.ts +0 -34
- package/dist/lib/browser/devices.js +0 -61
- package/dist/lib/browser/domain-skills.d.ts +0 -71
- package/dist/lib/browser/domain-skills.js +0 -195
- package/dist/lib/browser/drivers/arc.d.ts +0 -40
- package/dist/lib/browser/drivers/arc.js +0 -276
- package/dist/lib/browser/drivers/firefox.d.ts +0 -99
- package/dist/lib/browser/drivers/firefox.js +0 -377
- package/dist/lib/browser/drivers/local.d.ts +0 -55
- package/dist/lib/browser/drivers/local.js +0 -266
- package/dist/lib/browser/drivers/ssh.d.ts +0 -120
- package/dist/lib/browser/drivers/ssh.js +0 -467
- package/dist/lib/browser/editor.d.ts +0 -3
- package/dist/lib/browser/editor.js +0 -50
- package/dist/lib/browser/ffmpeg.d.ts +0 -12
- package/dist/lib/browser/ffmpeg.js +0 -184
- package/dist/lib/browser/firefox-discovery.d.ts +0 -69
- package/dist/lib/browser/firefox-discovery.js +0 -162
- package/dist/lib/browser/har.d.ts +0 -85
- package/dist/lib/browser/har.js +0 -77
- package/dist/lib/browser/hygiene.d.ts +0 -97
- package/dist/lib/browser/hygiene.js +0 -153
- package/dist/lib/browser/index.d.ts +0 -5
- package/dist/lib/browser/index.js +0 -5
- package/dist/lib/browser/input.d.ts +0 -7
- package/dist/lib/browser/input.js +0 -92
- package/dist/lib/browser/ipc.d.ts +0 -204
- package/dist/lib/browser/ipc.js +0 -1453
- package/dist/lib/browser/login-detection.d.ts +0 -87
- package/dist/lib/browser/login-detection.js +0 -274
- package/dist/lib/browser/profiles.d.ts +0 -422
- package/dist/lib/browser/profiles.js +0 -1157
- package/dist/lib/browser/refs.d.ts +0 -89
- package/dist/lib/browser/refs.js +0 -191
- package/dist/lib/browser/registry.d.ts +0 -70
- package/dist/lib/browser/registry.js +0 -252
- package/dist/lib/browser/remote-control.d.ts +0 -59
- package/dist/lib/browser/remote-control.js +0 -85
- package/dist/lib/browser/resolve-target.d.ts +0 -66
- package/dist/lib/browser/resolve-target.js +0 -264
- package/dist/lib/browser/runtime-state.d.ts +0 -291
- package/dist/lib/browser/runtime-state.js +0 -584
- package/dist/lib/browser/secret-ref.d.ts +0 -10
- package/dist/lib/browser/secret-ref.js +0 -14
- package/dist/lib/browser/service.d.ts +0 -812
- package/dist/lib/browser/service.js +0 -4693
- package/dist/lib/browser/stream.d.ts +0 -17
- package/dist/lib/browser/stream.js +0 -72
- package/dist/lib/browser/task-index.d.ts +0 -78
- package/dist/lib/browser/task-index.js +0 -199
- package/dist/lib/browser/types.d.ts +0 -647
- package/dist/lib/browser/types.js +0 -61
- package/dist/lib/browser/upload.d.ts +0 -29
- package/dist/lib/browser/upload.js +0 -298
- package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
- package/dist/lib/daemon/browser-ipc-service.js +0 -46
- package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
- package/dist/lib/daemon/browser-task-reap-service.js +0 -26
- package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
- package/dist/lib/fleet-shared-repo-sync.js +0 -490
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Non-secret daemon state exchanged
|
|
2
|
+
* Non-secret daemon state exchanged over SSH between fleet devices (PHNX-4116).
|
|
3
3
|
*
|
|
4
|
-
* Each device owns exactly one file under `~/.agents/devices/<device
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Each device owns exactly one file under `~/.agents/devices/<device>/`
|
|
5
|
+
* (`daemon-state.json`, UNTRACKED in the user repo). A device writes its OWN
|
|
6
|
+
* file locally; a PEER's file on this box is written only by the usage-sync
|
|
7
|
+
* exchange — a headed daemon dials each peer with `agents __usage-ingest
|
|
8
|
+
* --reply`, sending its own envelope on stdin and writing the peer's reply
|
|
9
|
+
* envelope here stamped with `receivedAt`. The envelope used to ride the user
|
|
10
|
+
* repo as a tracked file, which turned the shared store into 18k `chore(devices)`
|
|
11
|
+
* commits and wedged every clone behind a `git fetch` that timed out; git now
|
|
12
|
+
* carries only human-authored resources. OAuth/setup-token values never belong
|
|
13
|
+
* here: auth publishes only a readiness verdict; the existing encrypted SSH
|
|
14
|
+
* bundle push remains the one path that may carry secret material.
|
|
9
15
|
*/
|
|
10
16
|
import * as fs from 'node:fs';
|
|
11
17
|
import * as fsp from 'node:fs/promises';
|
|
@@ -23,11 +29,19 @@ export function fleetSharedStatePath(device, userAgentsDir = getUserAgentsDir())
|
|
|
23
29
|
function isRecord(value) {
|
|
24
30
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
25
31
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Validate an already-parsed envelope. `owner` pins the `device` field when the
|
|
34
|
+
* caller knows whose envelope this must be (a file under `devices/<owner>/`);
|
|
35
|
+
* an envelope arriving over the exchange names its own owner, so the caller
|
|
36
|
+
* passes none and checks `device` against the dialed peer afterwards.
|
|
37
|
+
*/
|
|
38
|
+
export function parseFleetSharedDeviceStateEnvelope(parsed, owner) {
|
|
39
|
+
if (!isRecord(parsed) || parsed.version !== FLEET_SHARED_STATE_VERSION || typeof parsed.device !== 'string' || !parsed.device) {
|
|
29
40
|
throw new Error('unrecognized shared-state envelope');
|
|
30
41
|
}
|
|
42
|
+
if (owner !== undefined && parsed.device !== owner)
|
|
43
|
+
throw new Error('unrecognized shared-state envelope');
|
|
44
|
+
assertValidDeviceName(parsed.device);
|
|
31
45
|
const usage = parsed.usage;
|
|
32
46
|
if (usage !== undefined && (!isRecord(usage) || !isRecord(usage.rows))) {
|
|
33
47
|
throw new Error('unrecognized usage snapshot');
|
|
@@ -41,8 +55,18 @@ function parseFleetSharedDeviceState(raw, owner) {
|
|
|
41
55
|
if (sessions !== undefined && (!isRecord(sessions) || !Array.isArray(sessions.rows))) {
|
|
42
56
|
throw new Error('unrecognized session mirror');
|
|
43
57
|
}
|
|
58
|
+
const accounts = parsed.accounts;
|
|
59
|
+
if (accounts !== undefined && (!isRecord(accounts) || !Array.isArray(accounts.rows))) {
|
|
60
|
+
throw new Error('unrecognized account verdict rows');
|
|
61
|
+
}
|
|
62
|
+
if (parsed.receivedAt !== undefined && (typeof parsed.receivedAt !== 'number' || !Number.isFinite(parsed.receivedAt))) {
|
|
63
|
+
throw new Error('unrecognized receivedAt');
|
|
64
|
+
}
|
|
44
65
|
return parsed;
|
|
45
66
|
}
|
|
67
|
+
function parseFleetSharedDeviceState(raw, owner) {
|
|
68
|
+
return parseFleetSharedDeviceStateEnvelope(JSON.parse(raw), owner);
|
|
69
|
+
}
|
|
46
70
|
/**
|
|
47
71
|
* Merge one daemon-owned field into this device's shared file under a real
|
|
48
72
|
* inter-process lock. Stable serialization avoids dirtying the user repo when
|
|
@@ -63,6 +87,8 @@ function mergeFleetState(currentRaw, device, patch) {
|
|
|
63
87
|
...(patch.usage !== undefined ? { usage: patch.usage } : {}),
|
|
64
88
|
...(patch.auth !== undefined ? { auth: patch.auth } : {}),
|
|
65
89
|
...(patch.sessions !== undefined ? { sessions: patch.sessions } : {}),
|
|
90
|
+
...(patch.accounts !== undefined ? { accounts: patch.accounts } : {}),
|
|
91
|
+
...(patch.receivedAt !== undefined ? { receivedAt: patch.receivedAt } : {}),
|
|
66
92
|
version: FLEET_SHARED_STATE_VERSION,
|
|
67
93
|
device,
|
|
68
94
|
};
|
|
@@ -138,3 +164,46 @@ export function readFleetSharedDeviceStates(userAgentsDir = getUserAgentsDir())
|
|
|
138
164
|
}
|
|
139
165
|
return { states, errors };
|
|
140
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* This device's own envelope as it stands on disk — what the usage-sync exchange
|
|
169
|
+
* sends to peers and what `__usage-ingest --reply` prints back. A device that has
|
|
170
|
+
* never published anything yields the bare `{version, device}` envelope, so a
|
|
171
|
+
* peer still learns the device exists and answered.
|
|
172
|
+
*/
|
|
173
|
+
export function readOwnFleetSharedDeviceState(device, userAgentsDir = getUserAgentsDir()) {
|
|
174
|
+
const file = fleetSharedStatePath(device, userAgentsDir);
|
|
175
|
+
let raw = '';
|
|
176
|
+
try {
|
|
177
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
178
|
+
}
|
|
179
|
+
catch { /* never published → bare envelope */ }
|
|
180
|
+
if (!raw.trim())
|
|
181
|
+
return { version: FLEET_SHARED_STATE_VERSION, device };
|
|
182
|
+
const state = parseFleetSharedDeviceState(raw, device);
|
|
183
|
+
// `receivedAt` is a receiver-side stamp; the owner never advertises one.
|
|
184
|
+
const { receivedAt: _receivedAt, ...own } = state;
|
|
185
|
+
return own;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Store a PEER's envelope, received over the SSH exchange, in that peer's file on
|
|
189
|
+
* this box, stamped with `receivedAt`. Merges field-by-field onto whatever the
|
|
190
|
+
* file already holds, so a partial envelope (a placement probe sends usage only)
|
|
191
|
+
* refreshes that field without erasing the peer's last auth verdict, session
|
|
192
|
+
* digests, or account rows. The five readers of `devices/<peer>/daemon-state.json`
|
|
193
|
+
* (usage merge, poller claims, auth verdicts, session mirror, account catalog)
|
|
194
|
+
* are unchanged by the transport swap. Async because it runs on the daemon's
|
|
195
|
+
* usage-sync tick once per peer reply: the sync lock would `sleepSync` the event
|
|
196
|
+
* loop for up to 30 s under contention (see {@link updateFleetSharedDeviceStateAsync}).
|
|
197
|
+
*/
|
|
198
|
+
export async function storePeerFleetSharedDeviceState(state, userAgentsDir = getUserAgentsDir(), receivedAt = Date.now()) {
|
|
199
|
+
const patch = { receivedAt };
|
|
200
|
+
if (state.usage !== undefined)
|
|
201
|
+
patch.usage = state.usage;
|
|
202
|
+
if (state.auth !== undefined)
|
|
203
|
+
patch.auth = state.auth;
|
|
204
|
+
if (state.sessions !== undefined)
|
|
205
|
+
patch.sessions = state.sessions;
|
|
206
|
+
if (state.accounts !== undefined)
|
|
207
|
+
patch.accounts = state.accounts;
|
|
208
|
+
return updateFleetSharedDeviceStateAsync(state.device, patch, userAgentsDir);
|
|
209
|
+
}
|
package/dist/lib/fs-atomic.js
CHANGED
|
@@ -106,6 +106,21 @@ export function withFileLock(filePath, fn, opts = {}) {
|
|
|
106
106
|
}
|
|
107
107
|
catch (err) {
|
|
108
108
|
lastError = err;
|
|
109
|
+
// proper-lockfile breaks stale locks with rmdir, which fails with ENOTDIR
|
|
110
|
+
// when a prior crash left a regular file instead of the expected directory.
|
|
111
|
+
// Remove the stale file so the next attempt can succeed. Match
|
|
112
|
+
// proper-lockfile's own path resolution: realpath when the option is on
|
|
113
|
+
// (the default), raw path when it is off.
|
|
114
|
+
if (err instanceof Error && err.code === 'ENOTDIR') {
|
|
115
|
+
try {
|
|
116
|
+
const base = (opts.realpath ?? true) ? fs.realpathSync(filePath) : filePath;
|
|
117
|
+
const lockPath = `${base}.lock`;
|
|
118
|
+
const st = fs.statSync(lockPath);
|
|
119
|
+
if (st.isFile())
|
|
120
|
+
fs.unlinkSync(lockPath);
|
|
121
|
+
}
|
|
122
|
+
catch { /* best effort */ }
|
|
123
|
+
}
|
|
109
124
|
if (Date.now() >= deadline)
|
|
110
125
|
break;
|
|
111
126
|
const backoff = Math.min(LOCK_RETRY_MIN_MS * (attempt + 1), LOCK_RETRY_MAX_MS);
|
|
@@ -175,6 +190,16 @@ export async function withFileLockAsync(filePath, fn, opts = {}) {
|
|
|
175
190
|
}
|
|
176
191
|
catch (err) {
|
|
177
192
|
lastError = err;
|
|
193
|
+
if (err instanceof Error && err.code === 'ENOTDIR') {
|
|
194
|
+
try {
|
|
195
|
+
const base = (opts.realpath ?? true) ? fs.realpathSync(filePath) : filePath;
|
|
196
|
+
const lockPath = `${base}.lock`;
|
|
197
|
+
const st = fs.statSync(lockPath);
|
|
198
|
+
if (st.isFile())
|
|
199
|
+
fs.unlinkSync(lockPath);
|
|
200
|
+
}
|
|
201
|
+
catch { /* best effort */ }
|
|
202
|
+
}
|
|
178
203
|
if (Date.now() >= deadline)
|
|
179
204
|
break;
|
|
180
205
|
const backoff = Math.min(LOCK_RETRY_MIN_MS * (attempt + 1), LOCK_RETRY_MAX_MS);
|
|
@@ -17,7 +17,6 @@ export const HARNESS_AUTH = {
|
|
|
17
17
|
kimi: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: 'KIMI_CODE_HOME' },
|
|
18
18
|
antigravity: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
19
19
|
droid: { login: null, status: null, identity: 'opaque', worker: 'api-key:FACTORY_API_KEY', slotEnv: null },
|
|
20
|
-
gemini: { login: null, status: null, identity: 'email', worker: 'none', slotEnv: null },
|
|
21
20
|
copilot: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: 'COPILOT_HOME' },
|
|
22
21
|
openclaw: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
23
22
|
amp: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* it would claim a distribution responsibility agents-cli no longer has.
|
|
42
42
|
*/
|
|
43
43
|
export const HELPER_RELEASES = {
|
|
44
|
-
menubar: { tagPrefix: 'menubar', floor: '1.
|
|
44
|
+
menubar: { tagPrefix: 'menubar', floor: '1.6.1' },
|
|
45
45
|
};
|
|
46
46
|
/** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
|
|
47
47
|
export function helperTag(helper, version) {
|
|
@@ -45,6 +45,13 @@ export declare function generateHookShim(args: {
|
|
|
45
45
|
cache?: HookCacheConfig | null;
|
|
46
46
|
matches?: HookMatches;
|
|
47
47
|
paths?: HookShimPaths;
|
|
48
|
+
/**
|
|
49
|
+
* Deny the tool call (exit 2) when the source script is missing instead of
|
|
50
|
+
* failing with 127, which every harness reads as "allow". Set for
|
|
51
|
+
* PreToolUse hooks: a guard whose script vanished (version pruned, sync
|
|
52
|
+
* rewriting the hooks dir) must not silently wave tool calls through.
|
|
53
|
+
*/
|
|
54
|
+
failClosed?: boolean;
|
|
48
55
|
}): string;
|
|
49
56
|
/**
|
|
50
57
|
* Remove a hook's shim. Called by the registrar's garbage collection when a
|
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -119,7 +119,7 @@ export function generateHookShim(args) {
|
|
|
119
119
|
const logsDir = args.paths?.logsDir ?? getLogsDir();
|
|
120
120
|
const perfDir = args.paths?.perfDir ?? getPerfDir();
|
|
121
121
|
const shimPath = resolveContainedHookShimPath(shimsDir, args.name);
|
|
122
|
-
const content = renderShim(args.name, args.scriptPath, args.cache ?? null, args.matches, { cacheDir, logsDir, perfDir });
|
|
122
|
+
const content = renderShim(args.name, args.scriptPath, args.cache ?? null, args.matches, { cacheDir, logsDir, perfDir }, args.failClosed === true);
|
|
123
123
|
fs.mkdirSync(shimsDir, { recursive: true });
|
|
124
124
|
let existing = null;
|
|
125
125
|
if (fs.existsSync(shimPath)) {
|
|
@@ -363,7 +363,7 @@ exit "$EXIT"`;
|
|
|
363
363
|
* a gate-only pass-through: it forwards stdin to the script and its stdout back,
|
|
364
364
|
* with no cache read/write.
|
|
365
365
|
*/
|
|
366
|
-
function renderShim(name, scriptPath, cache, matches, paths) {
|
|
366
|
+
function renderShim(name, scriptPath, cache, matches, paths, failClosed = false) {
|
|
367
367
|
const ttl = cache ? (typeof cache.ttl === 'number' ? cache.ttl : (parseDuration(cache.ttl) ?? 0)) : 0;
|
|
368
368
|
const key = cache?.key ?? 'global';
|
|
369
369
|
const prefetch = cache?.prefetch ?? 'none';
|
|
@@ -394,6 +394,7 @@ TTL=${ttl}
|
|
|
394
394
|
PREFETCH=${q(prefetch)}
|
|
395
395
|
KEY_MODE=${q(key)}
|
|
396
396
|
MATCHES_JSON=${q(matchesJson)}
|
|
397
|
+
FAIL_CLOSED=${failClosed ? 1 : 0}
|
|
397
398
|
|
|
398
399
|
mkdir -p "$CACHE_DIR" "$LOGS_DIR" "$PERF_DIR"
|
|
399
400
|
|
|
@@ -458,6 +459,30 @@ if [ -n "$MATCHES_JSON" ]; then
|
|
|
458
459
|
exit 0
|
|
459
460
|
fi
|
|
460
461
|
fi
|
|
462
|
+
# A source that is not on disk cannot be executed: bash exits 127, and the
|
|
463
|
+
# harnesses that share the exit-2 deny contract (Claude Code, Codex, Droid)
|
|
464
|
+
# read anything else as "allow" -- a guard silently disabled. Deny instead and
|
|
465
|
+
# name the repair, but only for a PreToolUse firing that the matches: gate
|
|
466
|
+
# above let through: one shim serves every event a hook declares, so a
|
|
467
|
+
# SessionStart or Stop leg of the same hook must not wedge a session, and a
|
|
468
|
+
# fire the predicates would have skipped must not turn into a denial. Other
|
|
469
|
+
# shims keep their existing path (a cached hook still serves its cache).
|
|
470
|
+
if [ "$FAIL_CLOSED" = 1 ] && [ ! -f "$SOURCE" ]; then
|
|
471
|
+
_EVT="$(printf '%s' "$STDIN_PAYLOAD" | "$PY" -c 'import json, sys
|
|
472
|
+
try:
|
|
473
|
+
d = json.load(sys.stdin)
|
|
474
|
+
except Exception:
|
|
475
|
+
d = {}
|
|
476
|
+
print(d.get("hook_event_name") or d.get("hookEventName") or "")' 2>/dev/null || true)"
|
|
477
|
+
if [ -z "$_EVT" ] || [ "$_EVT" = PreToolUse ]; then
|
|
478
|
+
_TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
479
|
+
printf '{"ts":"%s","event":"hook.fire","hook":"%s","ms":0,"cache":"missing-source","exit":2}\\n' \\
|
|
480
|
+
"$_TS" "$HOOK_NAME" >>"$LOGS_DIR/events-$(date -u +%Y-%m-%d).jsonl" 2>/dev/null || true
|
|
481
|
+
printf '%s: hook source is missing (%s); refusing the tool call unchecked (fail-closed). Run: agents hooks sync\\n' \\
|
|
482
|
+
"$HOOK_NAME" "$SOURCE" >&2
|
|
483
|
+
exit 2
|
|
484
|
+
fi
|
|
485
|
+
fi
|
|
461
486
|
${cache ? CACHE_TAIL : PASSTHROUGH_TAIL}
|
|
462
487
|
`;
|
|
463
488
|
}
|
|
@@ -363,7 +363,13 @@ function resolveHookCommand(name, hookDef, resolveScript) {
|
|
|
363
363
|
// script when predicates don't hold) and, when `cache:` is set, layers the
|
|
364
364
|
// cache/timing machinery on top; with neither, it is a pure pass-through
|
|
365
365
|
// timing wrapper.
|
|
366
|
-
return toPortableCommand(generateHookShim({
|
|
366
|
+
return toPortableCommand(generateHookShim({
|
|
367
|
+
name,
|
|
368
|
+
scriptPath,
|
|
369
|
+
cache,
|
|
370
|
+
matches,
|
|
371
|
+
failClosed: hookDef.events.includes('PreToolUse'),
|
|
372
|
+
}));
|
|
367
373
|
}
|
|
368
374
|
/**
|
|
369
375
|
* Extensions that are NEVER hooks — docs, configuration, plain data. A file
|
|
@@ -57,8 +57,19 @@ export interface ReadyProbe {
|
|
|
57
57
|
* `$LASTEXITCODE` (no `printf`/`||`); `parseReadyProbe` keys off the sentinel
|
|
58
58
|
* substring, so the missing leading newline vs the POSIX `printf '\n…'` form is
|
|
59
59
|
* absorbed by its `.trim()`. Pure/exported so both branches are unit-testable.
|
|
60
|
+
*
|
|
61
|
+
* With `ingestUsage`, the probe first runs `agents __usage-ingest` on the
|
|
62
|
+
* dispatcher's daemon-state envelope, which the caller pipes to stdin
|
|
63
|
+
* (PHNX-4116): the chosen worker then holds the dispatcher's current usage
|
|
64
|
+
* numbers at dispatch instead of waiting for the next 15-minute usage-sync
|
|
65
|
+
* tick. The ingest writes nothing to stdout (only `--reply` does), so the
|
|
66
|
+
* version/sentinel/listing shape `parseReadyProbe` reads is unchanged. On
|
|
67
|
+
* Windows the `agents.ps1` shim drops ssh-piped stdin, so PowerShell reads it
|
|
68
|
+
* into a temp file and hands the verb `--from <path>`.
|
|
60
69
|
*/
|
|
61
|
-
export declare function buildReadyProbeCommand(os?: string
|
|
70
|
+
export declare function buildReadyProbeCommand(os?: string, opts?: {
|
|
71
|
+
ingestUsage?: boolean;
|
|
72
|
+
}): string;
|
|
62
73
|
export declare function readyProbe(target: string, os?: string, extraSshArgs?: string[]): ReadyProbe;
|
|
63
74
|
/** Pure parser for `readyProbe` output (unit-tested without ssh). */
|
|
64
75
|
export declare function parseReadyProbe(stdout: string): ReadyProbe;
|
|
@@ -92,6 +103,12 @@ interface ViewAgentAccountEligibility {
|
|
|
92
103
|
signedIn: boolean | undefined;
|
|
93
104
|
/** At least one account can launch immediately or enter the harness login flow. */
|
|
94
105
|
pickerEligible: boolean | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* The remote box's own aggregate exclusion reason when it is not signed in
|
|
108
|
+
* (`runReady.reason` — e.g. `all signed_out`), for the operator-facing
|
|
109
|
+
* placement error. Undefined when ready, absent, or on an older remote CLI.
|
|
110
|
+
*/
|
|
111
|
+
reason?: string;
|
|
95
112
|
}
|
|
96
113
|
/**
|
|
97
114
|
* Read the two account gates automatic placement needs from `agents view
|
|
@@ -99,13 +116,26 @@ interface ViewAgentAccountEligibility {
|
|
|
99
116
|
* the login flow, but it must not route to a device whose every signed-in
|
|
100
117
|
* account is throttled.
|
|
101
118
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
119
|
+
* ONE readiness gate, computed on the box that runs (PHNX-4116). The remote box
|
|
120
|
+
* publishes `runReady` — the router's own `collectRunCandidates` →
|
|
121
|
+
* `readinessFromCandidate` verdict over its native slots AND version homes — so
|
|
122
|
+
* this reads that answer rather than re-deriving freshness here. The
|
|
123
|
+
* dispatching box re-deriving from the per-version `versions[]` list was the bug:
|
|
124
|
+
* that list enumerates version homes, misses the account slots a run picks from,
|
|
125
|
+
* and applied a 40-minute usage-freshness refusal a synced-only worker could
|
|
126
|
+
* never satisfy, turning a usage-sync lag into "no ready device" while
|
|
127
|
+
* `--device <name>` on the same box launched fine.
|
|
128
|
+
*
|
|
129
|
+
* An older remote CLI omits `runReady`; the one-release fallback derives the
|
|
130
|
+
* verdict from the per-version list the same way the pre-PHNX-4116 dispatcher
|
|
131
|
+
* did: the strict `launchable` signal (`isLaunchableSignedIn`, PHNX-3466) for
|
|
132
|
+
* sign-in, MINUS a FRESH throttle (`rate_limited`/`out_of_credits` captured
|
|
133
|
+
* within {@link USAGE_STALE_REFUSAL_MAX_AGE_MS}) or a FRESH dead auth verdict
|
|
134
|
+
* (checked within {@link AUTH_PROBE_MAX_AGE_MS}, {@link isDeadVerdict}). A stale
|
|
135
|
+
* reading is UNVERIFIED, not disqualifying (PHNX-4116/#3700), so a usage-sync
|
|
136
|
+
* lag never bars an old-CLI worker `--device <name>` would launch fine — but a
|
|
137
|
+
* throttled-but-launchable worker no longer reads `signedIn: true` and slips
|
|
138
|
+
* into `--device auto`'s pick during a rolling upgrade.
|
|
109
139
|
*/
|
|
110
140
|
export declare function viewAgentAccountEligibility(view: string, agent: string, now?: number): ViewAgentAccountEligibility;
|
|
111
141
|
export declare function viewAgentSignedIn(view: string, agent: string): boolean | undefined;
|
package/dist/lib/hosts/ready.js
CHANGED
|
@@ -102,15 +102,31 @@ const READY_MARKER = '@@AGENTS_READY@@';
|
|
|
102
102
|
* `$LASTEXITCODE` (no `printf`/`||`); `parseReadyProbe` keys off the sentinel
|
|
103
103
|
* substring, so the missing leading newline vs the POSIX `printf '\n…'` form is
|
|
104
104
|
* absorbed by its `.trim()`. Pure/exported so both branches are unit-testable.
|
|
105
|
+
*
|
|
106
|
+
* With `ingestUsage`, the probe first runs `agents __usage-ingest` on the
|
|
107
|
+
* dispatcher's daemon-state envelope, which the caller pipes to stdin
|
|
108
|
+
* (PHNX-4116): the chosen worker then holds the dispatcher's current usage
|
|
109
|
+
* numbers at dispatch instead of waiting for the next 15-minute usage-sync
|
|
110
|
+
* tick. The ingest writes nothing to stdout (only `--reply` does), so the
|
|
111
|
+
* version/sentinel/listing shape `parseReadyProbe` reads is unchanged. On
|
|
112
|
+
* Windows the `agents.ps1` shim drops ssh-piped stdin, so PowerShell reads it
|
|
113
|
+
* into a temp file and hands the verb `--from <path>`.
|
|
105
114
|
*/
|
|
106
|
-
export function buildReadyProbeCommand(os) {
|
|
115
|
+
export function buildReadyProbeCommand(os, opts = {}) {
|
|
107
116
|
if (remoteShellFor(os) === 'powershell') {
|
|
108
|
-
const
|
|
117
|
+
const ingest = opts.ingestUsage
|
|
118
|
+
? '$in = [Console]::In.ReadToEnd(); $tmp = $null; ' +
|
|
119
|
+
'try { $tmp = [System.IO.Path]::GetTempFileName(); [System.IO.File]::WriteAllText($tmp, $in); ' +
|
|
120
|
+
'agents __usage-ingest --from $tmp 2>$null } ' +
|
|
121
|
+
'finally { if ($tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } }; '
|
|
122
|
+
: '';
|
|
123
|
+
const script = `${POWERSHELL_PROGRESS_SILENCE}; ${ingest}` +
|
|
109
124
|
`agents --version 2>$null; Write-Output "${READY_MARKER}"; ` +
|
|
110
125
|
`agents view --json 2>$null; if ($LASTEXITCODE -ne 0) { agents list 2>$null }`;
|
|
111
126
|
return `powershell -NoProfile -EncodedCommand ${encodePowershell(script)}`;
|
|
112
127
|
}
|
|
113
|
-
const script =
|
|
128
|
+
const script = `${opts.ingestUsage ? 'agents __usage-ingest 2>/dev/null; ' : ''}` +
|
|
129
|
+
`agents --version 2>/dev/null; printf '\\n${READY_MARKER}\\n'; ` +
|
|
114
130
|
`agents view --json 2>/dev/null || agents list 2>/dev/null`;
|
|
115
131
|
return `bash -lc ${shellQuote(script)}`;
|
|
116
132
|
}
|
|
@@ -207,13 +223,26 @@ export function missingPinnedVersionMessage(hostName, agent, version, installed)
|
|
|
207
223
|
* the login flow, but it must not route to a device whose every signed-in
|
|
208
224
|
* account is throttled.
|
|
209
225
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
226
|
+
* ONE readiness gate, computed on the box that runs (PHNX-4116). The remote box
|
|
227
|
+
* publishes `runReady` — the router's own `collectRunCandidates` →
|
|
228
|
+
* `readinessFromCandidate` verdict over its native slots AND version homes — so
|
|
229
|
+
* this reads that answer rather than re-deriving freshness here. The
|
|
230
|
+
* dispatching box re-deriving from the per-version `versions[]` list was the bug:
|
|
231
|
+
* that list enumerates version homes, misses the account slots a run picks from,
|
|
232
|
+
* and applied a 40-minute usage-freshness refusal a synced-only worker could
|
|
233
|
+
* never satisfy, turning a usage-sync lag into "no ready device" while
|
|
234
|
+
* `--device <name>` on the same box launched fine.
|
|
235
|
+
*
|
|
236
|
+
* An older remote CLI omits `runReady`; the one-release fallback derives the
|
|
237
|
+
* verdict from the per-version list the same way the pre-PHNX-4116 dispatcher
|
|
238
|
+
* did: the strict `launchable` signal (`isLaunchableSignedIn`, PHNX-3466) for
|
|
239
|
+
* sign-in, MINUS a FRESH throttle (`rate_limited`/`out_of_credits` captured
|
|
240
|
+
* within {@link USAGE_STALE_REFUSAL_MAX_AGE_MS}) or a FRESH dead auth verdict
|
|
241
|
+
* (checked within {@link AUTH_PROBE_MAX_AGE_MS}, {@link isDeadVerdict}). A stale
|
|
242
|
+
* reading is UNVERIFIED, not disqualifying (PHNX-4116/#3700), so a usage-sync
|
|
243
|
+
* lag never bars an old-CLI worker `--device <name>` would launch fine — but a
|
|
244
|
+
* throttled-but-launchable worker no longer reads `signedIn: true` and slips
|
|
245
|
+
* into `--device auto`'s pick during a rolling upgrade.
|
|
217
246
|
*/
|
|
218
247
|
export function viewAgentAccountEligibility(view, agent, now = Date.now()) {
|
|
219
248
|
try {
|
|
@@ -221,11 +250,25 @@ export function viewAgentAccountEligibility(view, agent, now = Date.now()) {
|
|
|
221
250
|
const row = rows.find((candidate) => candidate.agent?.toLowerCase() === agent.toLowerCase());
|
|
222
251
|
if (!row)
|
|
223
252
|
return { signedIn: undefined, pickerEligible: undefined };
|
|
253
|
+
const runReady = row.runReady;
|
|
254
|
+
if (runReady && typeof runReady.ready === 'boolean') {
|
|
255
|
+
// A signed-out / revoked account is picker-eligible even when not ready —
|
|
256
|
+
// launching it IS the login flow. A throttled account is not.
|
|
257
|
+
const accounts = runReady.accounts ?? [];
|
|
258
|
+
const pickerEligible = runReady.ready
|
|
259
|
+
|| accounts.some((a) => a.reason === 'signed_out' || a.reason === 'revoked');
|
|
260
|
+
return {
|
|
261
|
+
signedIn: runReady.ready,
|
|
262
|
+
pickerEligible,
|
|
263
|
+
reason: runReady.ready ? undefined : runReady.reason,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// One-release fallback for an older remote CLI without `runReady`.
|
|
224
267
|
const verdicts = (row.versions ?? []).flatMap((version) => {
|
|
225
268
|
if (typeof version.signedIn !== 'boolean')
|
|
226
269
|
return [];
|
|
227
270
|
// Prefer the strict per-version launch signal; fall back to the display
|
|
228
|
-
// `signedIn` for
|
|
271
|
+
// `signedIn` for a CLI old enough to omit `launchable` too.
|
|
229
272
|
const launchable = typeof version.launchable === 'boolean' ? version.launchable : version.signedIn;
|
|
230
273
|
const usageCapturedAt = version.usageCapturedAt ? Date.parse(version.usageCapturedAt) : Number.NaN;
|
|
231
274
|
const usageFresh = Number.isFinite(usageCapturedAt)
|
|
@@ -240,9 +283,14 @@ export function viewAgentAccountEligibility(view, agent, now = Date.now()) {
|
|
|
240
283
|
&& version.authVerdict !== undefined
|
|
241
284
|
&& authFresh
|
|
242
285
|
&& isDeadVerdict(version.authVerdict);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
286
|
+
// A stale usage/auth reading makes the account UNVERIFIED, not unusable:
|
|
287
|
+
// the number is ignored (`throttled`/`authBlocked` only trust a fresh one)
|
|
288
|
+
// and the remote `agents run` weights an unverified account at the floor.
|
|
289
|
+
// Refusing the whole device on a stale reading turned a fleet-wide
|
|
290
|
+
// usage-sync lag into "no healthy device can run claude" while
|
|
291
|
+
// `--device <name>` on the same box launched fine (PHNX-4116/#3700). Only a
|
|
292
|
+
// FRESH throttle or a FRESH dead auth verdict disqualifies.
|
|
293
|
+
const ready = launchable && !authBlocked && !throttled;
|
|
246
294
|
return [{ ready, pickerEligible: ready || !launchable || authBlocked }];
|
|
247
295
|
});
|
|
248
296
|
if (verdicts.length === 0)
|
|
@@ -109,6 +109,7 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
109
109
|
disableTmux: 'local-only',
|
|
110
110
|
device: 'local-only',
|
|
111
111
|
where: 'local-only', // expands into host/lease before dispatch; never re-forwarded
|
|
112
|
+
local: 'local-only', // this machine by definition — a local run never reaches the SSH boundary
|
|
112
113
|
on: 'local-only',
|
|
113
114
|
computer: 'local-only',
|
|
114
115
|
any: 'local-only',
|
|
@@ -25,6 +25,14 @@ import type { Installation } from './types.js';
|
|
|
25
25
|
*/
|
|
26
26
|
export interface ProcessSnapshot {
|
|
27
27
|
listCommandLines(): Promise<string[]>;
|
|
28
|
+
/** Optional richer listing (pid, elapsed, tty) for naming a blocking process. */
|
|
29
|
+
listProcessRows?(): Promise<ProcessRow[]>;
|
|
30
|
+
}
|
|
31
|
+
export interface ProcessRow {
|
|
32
|
+
pid?: number;
|
|
33
|
+
elapsed?: string;
|
|
34
|
+
tty?: string;
|
|
35
|
+
args: string;
|
|
28
36
|
}
|
|
29
37
|
export declare const realProcessSnapshot: ProcessSnapshot;
|
|
30
38
|
/**
|
|
@@ -46,3 +54,22 @@ export declare function installationLooksActive(installation: Pick<Installation,
|
|
|
46
54
|
* update to a harness this check simply failed to observe.
|
|
47
55
|
*/
|
|
48
56
|
export declare function isInstallationLikelyActive(installation: Pick<Installation, 'agent' | 'label'>, snapshot?: ProcessSnapshot): Promise<boolean>;
|
|
57
|
+
export interface InstallationActivity {
|
|
58
|
+
active: boolean;
|
|
59
|
+
/** A launch lease is held (a launch is starting and may not be in the process table yet). */
|
|
60
|
+
lease: boolean;
|
|
61
|
+
/** Live processes naming this installation's directory, one line each. */
|
|
62
|
+
processes: string[];
|
|
63
|
+
/** The process scan failed; `active` is the fail-closed default. */
|
|
64
|
+
scanError?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Same verdict as {@link isInstallationLikelyActive}, plus WHAT is holding the
|
|
68
|
+
* installation, so a refused update can name the session to finish instead of
|
|
69
|
+
* "in use; retry later" (PHNX-4116 follow-up: an operator on yosemite-s0 had a
|
|
70
|
+
* one-hour-old resumed session on another tty and no way to see it from the
|
|
71
|
+
* refusal).
|
|
72
|
+
*/
|
|
73
|
+
export declare function describeInstallationActivity(installation: Pick<Installation, 'agent' | 'label'>, snapshot?: ProcessSnapshot): Promise<InstallationActivity>;
|
|
74
|
+
/** The one-line refusal an update prints for a busy installation. */
|
|
75
|
+
export declare function formatInUseDeferral(name: string, activity: InstallationActivity): string;
|
|
@@ -22,6 +22,16 @@ import { promisify } from 'util';
|
|
|
22
22
|
import { getVersionDir } from './store.js';
|
|
23
23
|
import { hasLiveLaunchLease } from './shims.js';
|
|
24
24
|
const execFileAsync = promisify(execFile);
|
|
25
|
+
async function listProcessRowsPosix() {
|
|
26
|
+
const { stdout } = await execFileAsync('ps', ['-Ao', 'pid=,etime=,tty=,args='], {
|
|
27
|
+
timeout: 5_000,
|
|
28
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
29
|
+
});
|
|
30
|
+
return stdout.split('\n').flatMap((line) => {
|
|
31
|
+
const m = /^\s*(\d+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
32
|
+
return m ? [{ pid: Number(m[1]), elapsed: m[2], tty: m[3], args: m[4] }] : [];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
25
35
|
async function listCommandLinesPosix() {
|
|
26
36
|
try {
|
|
27
37
|
const { stdout } = await execFileAsync('ps', ['-Ao', 'args'], {
|
|
@@ -49,6 +59,9 @@ async function listCommandLinesWindows() {
|
|
|
49
59
|
}
|
|
50
60
|
export const realProcessSnapshot = {
|
|
51
61
|
listCommandLines: () => (process.platform === 'win32' ? listCommandLinesWindows() : listCommandLinesPosix()),
|
|
62
|
+
listProcessRows: async () => (process.platform === 'win32'
|
|
63
|
+
? (await listCommandLinesWindows()).map((args) => ({ args }))
|
|
64
|
+
: listProcessRowsPosix()),
|
|
52
65
|
};
|
|
53
66
|
/**
|
|
54
67
|
* Does any live process reference this installation's own directory? Matches
|
|
@@ -82,3 +95,51 @@ export async function isInstallationLikelyActive(installation, snapshot = realPr
|
|
|
82
95
|
return true;
|
|
83
96
|
}
|
|
84
97
|
}
|
|
98
|
+
/** `ps` prints `?` (Linux), `??` (macOS) or `-` for a process with no controlling terminal. */
|
|
99
|
+
const NO_TTY_MARKERS = new Set(['?', '??', '-']);
|
|
100
|
+
function shortenArgs(args, versionDir) {
|
|
101
|
+
// Strip the long install path so the line reads as the command the user ran.
|
|
102
|
+
const trimmed = args.replace(versionDir, '…').replace(/\/node_modules\/\.bin\//, '/');
|
|
103
|
+
return trimmed.length > 110 ? `${trimmed.slice(0, 107)}...` : trimmed;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Same verdict as {@link isInstallationLikelyActive}, plus WHAT is holding the
|
|
107
|
+
* installation, so a refused update can name the session to finish instead of
|
|
108
|
+
* "in use; retry later" (PHNX-4116 follow-up: an operator on yosemite-s0 had a
|
|
109
|
+
* one-hour-old resumed session on another tty and no way to see it from the
|
|
110
|
+
* refusal).
|
|
111
|
+
*/
|
|
112
|
+
export async function describeInstallationActivity(installation, snapshot = realProcessSnapshot) {
|
|
113
|
+
const lease = hasLiveLaunchLease(installation.agent, installation.label);
|
|
114
|
+
const versionDir = getVersionDir(installation.agent, installation.label);
|
|
115
|
+
try {
|
|
116
|
+
const rows = snapshot.listProcessRows
|
|
117
|
+
? await snapshot.listProcessRows()
|
|
118
|
+
: (await snapshot.listCommandLines()).map((args) => ({ args }));
|
|
119
|
+
const processes = rows
|
|
120
|
+
.filter((row) => row.args.includes(versionDir))
|
|
121
|
+
.map((row) => {
|
|
122
|
+
const meta = [row.pid !== undefined ? `pid ${row.pid}` : null, row.elapsed ? `up ${row.elapsed}` : null, row.tty && !NO_TTY_MARKERS.has(row.tty) ? row.tty : null]
|
|
123
|
+
.filter((part) => part !== null)
|
|
124
|
+
.join(', ');
|
|
125
|
+
return meta ? `${meta}: ${shortenArgs(row.args, versionDir)}` : shortenArgs(row.args, versionDir);
|
|
126
|
+
});
|
|
127
|
+
return { active: lease || processes.length > 0, lease, processes };
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
return { active: true, lease, processes: [], scanError: err instanceof Error ? err.message : String(err) };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** The one-line refusal an update prints for a busy installation. */
|
|
134
|
+
export function formatInUseDeferral(name, activity) {
|
|
135
|
+
if (activity.scanError)
|
|
136
|
+
return `${name}: could not confirm nothing is running (${activity.scanError}); not updating.`;
|
|
137
|
+
if (activity.processes.length > 0) {
|
|
138
|
+
const list = activity.processes.length === 1
|
|
139
|
+
? activity.processes[0]
|
|
140
|
+
: activity.processes.map((p) => `\n ${p}`).join('');
|
|
141
|
+
return `${name} is in use by ${activity.processes.length === 1 ? 'a running process' : `${activity.processes.length} running processes`} — ${list}. `
|
|
142
|
+
+ 'Finish that session (agents sessions stop <id> for an agents session), then retry.';
|
|
143
|
+
}
|
|
144
|
+
return `${name} has a launch in flight; retry once it has started or exited.`;
|
|
145
|
+
}
|
|
@@ -17,10 +17,13 @@ import { enabledRoutineNames, replaceEnabledRoutines } from '../routine-activati
|
|
|
17
17
|
import { evaluateActivationReadiness } from '../routine-readiness.js';
|
|
18
18
|
import { migrateDeviceConfigStores } from '../devices/config-migration.js';
|
|
19
19
|
import { detrackViaGitExclude } from '../project-resources.js';
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
import { LEGACY_DEFAULT_BROWSER_PROFILE_NAME } from '../browser/profiles.js';
|
|
20
|
+
// Migrations operate on raw YAML so they never take the meta lock or prime the
|
|
21
|
+
// meta cache mid-migration.
|
|
23
22
|
import { META_HEADER as DEVICE_META_HEADER } from '../state.js';
|
|
23
|
+
/** The pre-device-scope central browser profile name this one-shot migration
|
|
24
|
+
* folds into the device doc. Inlined (the in-repo browser engine that defined it
|
|
25
|
+
* was removed with PHNX-4101); the legacy on-disk name is unchanged. */
|
|
26
|
+
const LEGACY_DEFAULT_BROWSER_PROFILE_NAME = 'default';
|
|
24
27
|
import { COMPILED_HEADER_PROJECT } from '../rules/compile.js';
|
|
25
28
|
const HOME = process.env.HOME ?? os.homedir();
|
|
26
29
|
const USER_DIR = path.join(HOME, '.agents');
|
|
@@ -2189,15 +2189,17 @@ export function listShimFileNames() {
|
|
|
2189
2189
|
}
|
|
2190
2190
|
/**
|
|
2191
2191
|
* Legacy command shims that are wrong even when their baked install is alive.
|
|
2192
|
-
* `secrets` (PHNX-3989), `sessions` (PHNX-4012)
|
|
2193
|
-
* shim `exec`s `agents <name>`, which is a passthrough
|
|
2194
|
-
* so the shim re-enters itself (the 1.22.85 fork
|
|
2192
|
+
* `secrets` (PHNX-3989), `sessions` (PHNX-4012), `computer` (PHNX-4075) and
|
|
2193
|
+
* `browser` (PHNX-4101): the shim `exec`s `agents <name>`, which is a passthrough
|
|
2194
|
+
* to the STANDALONE binary — so the shim re-enters itself (the 1.22.85 fork
|
|
2195
|
+
* bomb) and, sitting first on PATH, shadows the real `@phnx-labs/<name>-cli`
|
|
2196
|
+
* binary a user types directly. Pruned unconditionally.
|
|
2195
2197
|
*
|
|
2196
|
-
* `computer`
|
|
2197
|
-
* `computer` bin of its own: a machine that installed an
|
|
2198
|
-
* shim on PATH under the name the standalone engine now owns.
|
|
2198
|
+
* `computer` and `browser` are the sharpest, because agents-cli USED to publish
|
|
2199
|
+
* (`computer`) or shim (`browser`) a bin of its own: a machine that installed an
|
|
2200
|
+
* older CLI has a real shim on PATH under the name the standalone engine now owns.
|
|
2199
2201
|
*/
|
|
2200
|
-
const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets', 'sessions', 'computer', 'pty']);
|
|
2202
|
+
const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets', 'sessions', 'computer', 'pty', 'browser']);
|
|
2201
2203
|
/**
|
|
2202
2204
|
* Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
|
|
2203
2205
|
* alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
|