@phnx-labs/agents-cli 1.22.45 → 1.22.47
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 +303 -0
- package/LICENSE +102 -182
- package/README.md +54 -17
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +229 -0
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +266 -15
- package/dist/commands/config.js +26 -4
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +55 -38
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/lease.js +3 -3
- package/dist/commands/routines.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +194 -16
- package/dist/commands/teams.js +34 -2
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +19 -32
- package/dist/commands/watchdog.js +9 -14
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +92 -6
- package/dist/lib/accounting/usage.js +329 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +20 -7
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +33 -7
- package/dist/lib/browser/profiles.d.ts +106 -1
- package/dist/lib/browser/profiles.js +300 -17
- package/dist/lib/browser/remote-control.d.ts +26 -2
- package/dist/lib/browser/remote-control.js +33 -3
- package/dist/lib/browser/runtime-state.d.ts +9 -0
- package/dist/lib/browser/runtime-state.js +24 -8
- package/dist/lib/browser/service.d.ts +37 -0
- package/dist/lib/browser/service.js +78 -20
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/cli-resources.js +17 -15
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +17 -7
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon-ticks.js +2 -2
- package/dist/lib/device-config.d.ts +25 -8
- package/dist/lib/device-config.js +90 -17
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/harness-inventory.js +20 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +6 -0
- package/dist/lib/devices/stats-cache.js +10 -2
- package/dist/lib/exec.d.ts +25 -3
- package/dist/lib/exec.js +75 -22
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/hooks/cache.js +8 -0
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/passthrough.js +16 -0
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/identity/client.d.ts +53 -0
- package/dist/lib/identity/client.js +106 -0
- package/dist/lib/identity/index.d.ts +115 -0
- package/dist/lib/identity/index.js +82 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +13 -0
- package/dist/lib/menubar/install-menubar.js +26 -2
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/probe.d.ts +8 -0
- package/dist/lib/probe.js +105 -0
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +69 -1
- package/dist/lib/session/db.js +4 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +12 -6
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +6 -1
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.js +5 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +15 -0
- package/dist/lib/session/trajectory-html.js +750 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +277 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +335 -0
- package/dist/lib/startup/command-registry.d.ts +5 -2
- package/dist/lib/startup/command-registry.js +8 -5
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +173 -27
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +17 -3
- package/dist/lib/usage-refresh.js +57 -11
- package/dist/lib/view-types.d.ts +2 -2
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/package.json +2 -2
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
|
@@ -36,7 +36,7 @@ import * as yaml from 'yaml';
|
|
|
36
36
|
import { META_HEADER, getUserAgentsDir, readMeta, updateMeta, withMetaLock } from './state.js';
|
|
37
37
|
import { atomicWriteFileSync } from './fs-atomic.js';
|
|
38
38
|
import { machineId } from './machine-id.js';
|
|
39
|
-
import { assertValidDeviceName } from './devices/registry.js';
|
|
39
|
+
import { assertValidDeviceName, assertRegistrableDeviceName } from './devices/registry.js';
|
|
40
40
|
import { migrateDeviceConfigStores } from './devices/config-migration.js';
|
|
41
41
|
const DEVICE_PLATFORMS = ['windows', 'linux', 'macos', 'unknown'];
|
|
42
42
|
const SSH_AUTH_METHODS = ['key', 'password'];
|
|
@@ -53,7 +53,7 @@ export const CONFIG_KEYS = [
|
|
|
53
53
|
description: 'Device that shows the user artifacts (browser opens, dashboards) — the "online macOS box" skills should use instead of guessing.',
|
|
54
54
|
validate: (v) => {
|
|
55
55
|
try {
|
|
56
|
-
|
|
56
|
+
assertRegistrableDeviceName(v);
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
catch (err) {
|
|
@@ -69,7 +69,7 @@ export const CONFIG_KEYS = [
|
|
|
69
69
|
description: 'Device whose usage snapshots are authoritative for fleet-wide usage reporting.',
|
|
70
70
|
validate: (v) => {
|
|
71
71
|
try {
|
|
72
|
-
|
|
72
|
+
assertRegistrableDeviceName(v);
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
75
|
catch (err) {
|
|
@@ -90,6 +90,18 @@ export const CONFIG_KEYS = [
|
|
|
90
90
|
? null
|
|
91
91
|
: `auto.pool must be one of ${AUTO_POOL_MODES.join(' | ')}.`,
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: 'browser.viewer',
|
|
95
|
+
yamlKey: 'browserViewer',
|
|
96
|
+
scope: 'device',
|
|
97
|
+
visibility: 'machine',
|
|
98
|
+
type: 'string',
|
|
99
|
+
description: "Which browser THIS machine shows YOU a page in (an .html artifact, `agents feedback`, a login " +
|
|
100
|
+
"dashboard): a profile name, or `os` for the OS default handler. Unset follows browser.profile. " +
|
|
101
|
+
"Distinct from browser.profile, which is the profile agents DRIVE — see RUSH-2709 for why " +
|
|
102
|
+
"collapsing the two was a mistake. Set it to `os` to keep the OS default handler.",
|
|
103
|
+
validate: (v) => typeof v === 'string' && v.length > 0 ? null : 'browser.viewer must be `os` or a profile name.',
|
|
104
|
+
},
|
|
93
105
|
{
|
|
94
106
|
name: 'browser.profile',
|
|
95
107
|
yamlKey: 'defaultBrowserProfile',
|
|
@@ -144,11 +156,10 @@ export const CONFIG_KEYS = [
|
|
|
144
156
|
scope: 'device',
|
|
145
157
|
visibility: 'machine',
|
|
146
158
|
type: 'bool',
|
|
147
|
-
defaultValue:
|
|
159
|
+
defaultValue: false,
|
|
148
160
|
description: 'Whether an interactive `agents run` on this device is wrapped in the shared-socket tmux session. ' +
|
|
149
|
-
'
|
|
150
|
-
'`agents
|
|
151
|
-
'form of `--no-tmux`, for a machine whose tmux is broken or unwanted.',
|
|
161
|
+
'Off, the default, spawns the agent directly. Turn it on to give every agent an addressable pane for ' +
|
|
162
|
+
'`agents message`, injection, and `agents focus` once the tmux mouse, clipboard, and scrollback behavior suits this device.',
|
|
152
163
|
},
|
|
153
164
|
{
|
|
154
165
|
name: 'browser.remote-control',
|
|
@@ -179,7 +190,26 @@ export const CONFIG_KEYS = [
|
|
|
179
190
|
scope: 'device',
|
|
180
191
|
visibility: 'shared',
|
|
181
192
|
type: 'string-list',
|
|
182
|
-
description: 'Free-form operator notes about this device (one entry per `agents devices config <name> notes <text>`).'
|
|
193
|
+
description: 'Free-form operator notes about this device (one entry per `agents devices config <name> notes <text>`). ' +
|
|
194
|
+
'Long-form scratch, never shown in device listings — for the one-line synced summary of what the box is for, use `description`.',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'description',
|
|
198
|
+
yamlKey: 'description',
|
|
199
|
+
scope: 'device',
|
|
200
|
+
visibility: 'shared',
|
|
201
|
+
type: 'string',
|
|
202
|
+
description: 'One line saying what this device is FOR ("gpu box — cuda 12.4"), synced fleet-wide. Kept to one short line ' +
|
|
203
|
+
'because the device-list renderer will show it (RUSH-3062, `surface` track). ' +
|
|
204
|
+
'Replaces the value on each set; for appended long-form scratch use `notes`.',
|
|
205
|
+
validate: (v) => {
|
|
206
|
+
const s = v;
|
|
207
|
+
if (s.includes('\n') || s.includes('\r'))
|
|
208
|
+
return 'description must be a single line.';
|
|
209
|
+
if (s.length > 80)
|
|
210
|
+
return `description must be at most 80 characters (got ${s.length}) — it is a one-line table cell.`;
|
|
211
|
+
return null;
|
|
212
|
+
},
|
|
183
213
|
},
|
|
184
214
|
{
|
|
185
215
|
name: 'ssh.user',
|
|
@@ -593,6 +623,20 @@ export function configuredDeviceRole(name) {
|
|
|
593
623
|
assertValidDeviceName(name);
|
|
594
624
|
return getConfigValue('role', { device: name }).value;
|
|
595
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* The role marked on THIS machine — the one running the CLI — or undefined when
|
|
628
|
+
* it was never marked. Keyed off {@link machineId} (overridable via
|
|
629
|
+
* AGENTS_SYNC_MACHINE_ID), so it matches the device's own config-folder key.
|
|
630
|
+
*
|
|
631
|
+
* The auth strategy reads this: a `personal` device (the user's own interactive
|
|
632
|
+
* box) holds a real per-version login and MUST authenticate from it for EVERY
|
|
633
|
+
* run, interactive or headless; only a `worker` uses the file-based setup-token
|
|
634
|
+
* (RUSH-2395). `undefined` is treated as non-personal (worker-equivalent) by
|
|
635
|
+
* that gate — an unmarked box has no login to defer to.
|
|
636
|
+
*/
|
|
637
|
+
export function selfConfiguredDeviceRole() {
|
|
638
|
+
return configuredDeviceRole(machineId());
|
|
639
|
+
}
|
|
596
640
|
/** Mark a device's role fleet-wide; `undefined` clears the mark. */
|
|
597
641
|
export function setConfiguredDeviceRole(name, role) {
|
|
598
642
|
assertValidDeviceName(name);
|
|
@@ -601,6 +645,42 @@ export function setConfiguredDeviceRole(name, role) {
|
|
|
601
645
|
else
|
|
602
646
|
setConfigValue('role', role, { device: name });
|
|
603
647
|
}
|
|
648
|
+
/**
|
|
649
|
+
* Devices whose OWN config pins one of the browser profile keys to `profile`.
|
|
650
|
+
*
|
|
651
|
+
* Used by `profiles rename` to warn rather than silently leave a peer pointing at
|
|
652
|
+
* a name that no longer exists. Deliberately read-only: rewriting another
|
|
653
|
+
* machine's device doc from here would be a cross-machine mutation nobody asked
|
|
654
|
+
* for, and the pin may well be correct there (a local profile of the same name).
|
|
655
|
+
*/
|
|
656
|
+
export function devicesPinningBrowserProfile(profile) {
|
|
657
|
+
ensureDeviceConfigMigrated();
|
|
658
|
+
// Reports WHICH key each device used, not just that it matched. A caller
|
|
659
|
+
// telling the user to fix `browser.profile` on a device that actually pinned
|
|
660
|
+
// `browser.viewer` leaves the real pin broken and sets a second key nobody
|
|
661
|
+
// asked for.
|
|
662
|
+
const hits = [];
|
|
663
|
+
const devicesRoot = path.join(getUserAgentsDir(), 'devices');
|
|
664
|
+
let names = [];
|
|
665
|
+
try {
|
|
666
|
+
names = fs
|
|
667
|
+
.readdirSync(devicesRoot, { withFileTypes: true })
|
|
668
|
+
.filter((e) => e.isDirectory())
|
|
669
|
+
.map((e) => e.name);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
return hits;
|
|
673
|
+
}
|
|
674
|
+
for (const name of names.sort()) {
|
|
675
|
+
const cfg = readDeviceDocConfig(name);
|
|
676
|
+
// A device can pin BOTH; each needs its own fix line.
|
|
677
|
+
if (cfg.defaultBrowserProfile === profile)
|
|
678
|
+
hits.push({ device: name, key: 'browser.profile' });
|
|
679
|
+
if (cfg.browserViewer === profile)
|
|
680
|
+
hits.push({ device: name, key: 'browser.viewer' });
|
|
681
|
+
}
|
|
682
|
+
return hits;
|
|
683
|
+
}
|
|
604
684
|
/**
|
|
605
685
|
* Every device with an effective role, keyed by device name. Layers like every
|
|
606
686
|
* other device-scope key: the fleet default (central fleet.defaults.config)
|
|
@@ -720,16 +800,9 @@ export function assertSchedulerEnabled() {
|
|
|
720
800
|
throw new Error(`The routines scheduler is disabled on this device (scheduler.enabled=false in ~/.agents/devices/${machineId()}/agents.yaml). ` +
|
|
721
801
|
`Re-enable with: agents devices config ${machineId()} scheduler.enabled on`);
|
|
722
802
|
}
|
|
723
|
-
/**
|
|
724
|
-
* True unless this machine's config turns off the managed tmux wrap for
|
|
725
|
-
* interactive `agents run` launches (`tmux.enabled=false`).
|
|
726
|
-
*
|
|
727
|
-
* Read as one of the guards in `shouldWrapInTmux` (lib/exec.ts) — the durable,
|
|
728
|
-
* per-machine form of `--no-tmux` / `AGENTS_NO_TMUX=1`, for a box whose tmux is
|
|
729
|
-
* broken or unwanted. Unset means today's behavior: wrap.
|
|
730
|
-
*/
|
|
803
|
+
/** True only when this machine explicitly enables the managed tmux wrap. */
|
|
731
804
|
export function isTmuxEnabled() {
|
|
732
|
-
return getConfigValue('tmux.enabled').value
|
|
805
|
+
return getConfigValue('tmux.enabled').value === true;
|
|
733
806
|
}
|
|
734
807
|
/** True unless this machine's config disables the daemon outright (top-level kill switch). */
|
|
735
808
|
export function isDaemonEnabled() {
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* `.history/devices/pins-<host>.json` (pins file wins on conflict — it
|
|
14
14
|
* is the destination); peers' pins are simply dropped from the tracked
|
|
15
15
|
* file (each peer owns/rewrites its own pins locally).
|
|
16
|
+
* (e) legacy `.history/devices/ignored.json` (per-machine, UNTRACKED — a
|
|
17
|
+
* dismissal never reached the rest of the fleet) folds into the central
|
|
18
|
+
* TRACKED `fleet.ignored` list (RUSH-3062). Entries keep the legacy
|
|
19
|
+
* file's `updatedAt` as `ignoredAt` and take THIS machine's id as
|
|
20
|
+
* `ignoredOn` — the legacy store recorded neither, so the folding box
|
|
21
|
+
* is the only attribution available. The legacy file is removed only
|
|
22
|
+
* AFTER the central write lands.
|
|
16
23
|
*
|
|
17
24
|
* What stays put: a device doc's existing `config:` (already the right home)
|
|
18
25
|
* and its `routines:` list (operator-owned, read cross-device by
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* `.history/devices/pins-<host>.json` (pins file wins on conflict — it
|
|
14
14
|
* is the destination); peers' pins are simply dropped from the tracked
|
|
15
15
|
* file (each peer owns/rewrites its own pins locally).
|
|
16
|
+
* (e) legacy `.history/devices/ignored.json` (per-machine, UNTRACKED — a
|
|
17
|
+
* dismissal never reached the rest of the fleet) folds into the central
|
|
18
|
+
* TRACKED `fleet.ignored` list (RUSH-3062). Entries keep the legacy
|
|
19
|
+
* file's `updatedAt` as `ignoredAt` and take THIS machine's id as
|
|
20
|
+
* `ignoredOn` — the legacy store recorded neither, so the folding box
|
|
21
|
+
* is the only attribution available. The legacy file is removed only
|
|
22
|
+
* AFTER the central write lands.
|
|
16
23
|
*
|
|
17
24
|
* What stays put: a device doc's existing `config:` (already the right home)
|
|
18
25
|
* and its `routines:` list (operator-owned, read cross-device by
|
|
@@ -33,9 +40,10 @@
|
|
|
33
40
|
import * as fs from 'fs';
|
|
34
41
|
import * as path from 'path';
|
|
35
42
|
import * as yaml from 'yaml';
|
|
36
|
-
import { META_HEADER, getDevicesAutoLaunchPath, getDevicePinsPath, getUserAgentsDir, readMeta, updateMeta, withMetaLock, } from '../state.js';
|
|
43
|
+
import { META_HEADER, getDevicesAutoLaunchPath, getDevicesIgnoredPath, getDevicePinsPath, getUserAgentsDir, readMeta, updateMeta, withMetaLock, } from '../state.js';
|
|
37
44
|
import { atomicWriteFileSync } from '../fs-atomic.js';
|
|
38
45
|
import { machineId } from '../machine-id.js';
|
|
46
|
+
import { withIgnoredAdded } from './registry.js';
|
|
39
47
|
/** Read every device doc. A doc that fails to parse is loudly skipped. */
|
|
40
48
|
function readDeviceDocs(devicesRoot) {
|
|
41
49
|
const docs = [];
|
|
@@ -124,6 +132,32 @@ function readAutoLaunchFlags(autoLaunchPath) {
|
|
|
124
132
|
function isConfigMap(v) {
|
|
125
133
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
126
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The legacy per-machine ignored.json. Absent => null. A parse failure is
|
|
137
|
+
* logged loudly and reported as `undefined` so the caller LEAVES the file in
|
|
138
|
+
* place for a later retry — never silently emptied, the same corruption
|
|
139
|
+
* contract the ignore-list itself keeps (registry.ts's loadIgnoredEntries).
|
|
140
|
+
*/
|
|
141
|
+
function readLegacyIgnoredFile(p) {
|
|
142
|
+
let raw;
|
|
143
|
+
try {
|
|
144
|
+
raw = fs.readFileSync(p, 'utf-8');
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null; // absent — nothing to fold
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(raw);
|
|
151
|
+
return {
|
|
152
|
+
names: Array.isArray(parsed.ignored) ? parsed.ignored.filter((n) => typeof n === 'string') : [],
|
|
153
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
console.error(`device config migration: could not parse ${p} (${err.message}); leaving it for a later retry`);
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
127
161
|
/**
|
|
128
162
|
* Fold every legacy device-config/pins location into the current layout. Safe
|
|
129
163
|
* to call on every boot / config access: cheap no-op once folded.
|
|
@@ -139,6 +173,9 @@ export function migrateDeviceConfigStores() {
|
|
|
139
173
|
const centralHasConfig = Object.values(centralDevices).some((ov) => ov?.config && Object.keys(ov.config).length > 0);
|
|
140
174
|
const autoLaunchFlags = readAutoLaunchFlags(autoLaunchPath);
|
|
141
175
|
const docs = readDeviceDocs(devicesRoot);
|
|
176
|
+
const legacyIgnoredPath = getDevicesIgnoredPath();
|
|
177
|
+
const legacyIgnoredPending = fs.existsSync(legacyIgnoredPath);
|
|
178
|
+
const legacyIgnored = legacyIgnoredPending ? readLegacyIgnoredFile(legacyIgnoredPath) : null;
|
|
142
179
|
const pinsPath = getDevicePinsPath();
|
|
143
180
|
let selfPins = {};
|
|
144
181
|
try {
|
|
@@ -172,7 +209,7 @@ export function migrateDeviceConfigStores() {
|
|
|
172
209
|
: undefined;
|
|
173
210
|
const hasDestinationWork = plans.length > 0 || newDocs.length > 0 || docAgents !== undefined || docIsolated !== undefined;
|
|
174
211
|
const autoLaunchPending = fs.existsSync(autoLaunchPath);
|
|
175
|
-
if (!centralHasConfig && !hasDestinationWork && !autoLaunchPending)
|
|
212
|
+
if (!centralHasConfig && !hasDestinationWork && !autoLaunchPending && !legacyIgnoredPending)
|
|
176
213
|
return;
|
|
177
214
|
// ── 2. Destination writes FIRST (crash-safe), under the meta lock so they
|
|
178
215
|
// serialize against writeMetaUnlocked's own read-merge-write of the doc.
|
|
@@ -221,7 +258,18 @@ export function migrateDeviceConfigStores() {
|
|
|
221
258
|
nextDevices[name] = rest;
|
|
222
259
|
}
|
|
223
260
|
const fleet = { ...m.fleet, devices: nextDevices };
|
|
224
|
-
|
|
261
|
+
// Drop the whole `fleet` block only when NOTHING else lives in it. Every
|
|
262
|
+
// resident must be named here: `ignored` holds the user's dismissals and
|
|
263
|
+
// `discovery` their discovery policy, and deleting the block would not
|
|
264
|
+
// just lose them locally — `agents repo push` would sync the deletion
|
|
265
|
+
// fleet-wide. Add any new fleet.* key to this guard.
|
|
266
|
+
const fleetIsEmpty = Object.keys(nextDevices).length === 0 &&
|
|
267
|
+
!fleet.defaults &&
|
|
268
|
+
!fleet.secrets &&
|
|
269
|
+
!fleet.routines &&
|
|
270
|
+
!fleet.discovery &&
|
|
271
|
+
!(fleet.ignored && fleet.ignored.length > 0);
|
|
272
|
+
if (fleetIsEmpty) {
|
|
225
273
|
const { fleet: _, ...rest } = m;
|
|
226
274
|
void _;
|
|
227
275
|
return rest;
|
|
@@ -238,6 +286,22 @@ export function migrateDeviceConfigStores() {
|
|
|
238
286
|
console.error(`device config migration: could not remove ${autoLaunchPath} (${err.message}); a later run retries`);
|
|
239
287
|
}
|
|
240
288
|
}
|
|
289
|
+
// ── 4. Legacy ignored.json → central fleet.ignored ───────────────────────
|
|
290
|
+
// Destination write (updateMeta: withMetaLock + atomic write) lands BEFORE
|
|
291
|
+
// the legacy file is removed, so a crash re-folds on the next run and
|
|
292
|
+
// withIgnoredAdded's union-by-name makes that a no-op. A file that failed to
|
|
293
|
+
// parse (legacyIgnored === undefined) is left in place for that retry.
|
|
294
|
+
if (legacyIgnored) {
|
|
295
|
+
if (legacyIgnored.names.length > 0) {
|
|
296
|
+
updateMeta((m) => withIgnoredAdded(m, legacyIgnored.names, legacyIgnored.updatedAt ?? new Date().toISOString()));
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
fs.rmSync(legacyIgnoredPath, { force: true });
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
console.error(`device config migration: could not remove ${legacyIgnoredPath} (${err.message}); a later run retries`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
241
305
|
}
|
|
242
306
|
/** Shallow-set equality for folded doc content (values compared deep via JSON). */
|
|
243
307
|
function sameDocContent(a, b) {
|
|
@@ -27,15 +27,32 @@ import { listNativeAccounts } from '../account-registry.js';
|
|
|
27
27
|
*/
|
|
28
28
|
export function summarizeQuota(snapshot, unavailableReason = null, accountStatus = null) {
|
|
29
29
|
if (!snapshot || snapshot.windows.length === 0) {
|
|
30
|
-
|
|
30
|
+
// An active refusal marker (persisted out_of_credits, or an unexpired
|
|
31
|
+
// session_limit) blocks even when there are no live utilization windows —
|
|
32
|
+
// which is the normal state hours/days after a run, once cached windows
|
|
33
|
+
// expire. Check it BEFORE trusting the coarse account status, which is
|
|
34
|
+
// hardcoded 'available' for a signed-in Claude; otherwise a tokens-exhausted
|
|
35
|
+
// account reads ready:true here (RUSH-3018 finding, `agents devices harnesses`).
|
|
36
|
+
const marker = snapshot?.unavailable;
|
|
37
|
+
let status = accountStatus;
|
|
38
|
+
let reason = unavailableReason;
|
|
39
|
+
if (marker?.reason === 'out_of_credits') {
|
|
40
|
+
status = 'out_of_credits';
|
|
41
|
+
reason = 'out of credits';
|
|
42
|
+
}
|
|
43
|
+
else if (marker?.reason === 'session_limit' &&
|
|
44
|
+
(!marker.resetsAt || marker.resetsAt.getTime() > Date.now())) {
|
|
45
|
+
status = 'rate_limited';
|
|
46
|
+
reason = 'session-limited';
|
|
47
|
+
}
|
|
31
48
|
return {
|
|
32
49
|
status,
|
|
33
50
|
verdict: status ?? 'unavailable',
|
|
34
51
|
usedPercent: null,
|
|
35
52
|
stale: false,
|
|
36
53
|
capturedAt: snapshot?.capturedAt?.toISOString() ?? null,
|
|
37
|
-
resetsAt: null,
|
|
38
|
-
unavailableReason: status ? null : (
|
|
54
|
+
resetsAt: marker?.resetsAt?.toISOString() ?? null,
|
|
55
|
+
unavailableReason: status ? null : (reason ?? 'usage unavailable'),
|
|
39
56
|
};
|
|
40
57
|
}
|
|
41
58
|
const blocking = snapshot.windows.filter((w) => w.key !== 'sonnet_week');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device resource probing for `agents devices list`.
|
|
3
3
|
*
|
|
4
|
-
* One SSH round-trip per device gathers load average, memory pressure, and core
|
|
4
|
+
* One SSH round-trip per device gathers load average, memory pressure, disk, and core
|
|
5
5
|
* count (mac + linux via a POSIX snippet, windows via a CIM one-liner), parsed
|
|
6
6
|
* into a {@link DeviceStats}. Probes run in parallel with a bounded timeout so
|
|
7
7
|
* the list stays responsive — a slow or hung box degrades to "no stats" instead
|
|
@@ -19,6 +19,19 @@ export declare const PROBE_TIMEOUT_MS = 2500;
|
|
|
19
19
|
* "Preparing modules for first use" cost on top of PowerShell startup, which
|
|
20
20
|
* routinely blows the 2.5s POSIX budget on a relayed connection. */
|
|
21
21
|
export declare const WIN_PROBE_TIMEOUT_MS = 6000;
|
|
22
|
+
/** One-shot remote snapshot: load, memory, core count, then root filesystem. */
|
|
23
|
+
export declare const PROBE_SNIPPET = "uptime; echo ---AGSTAT---; (vm_stat 2>/dev/null || cat /proc/meminfo 2>/dev/null); echo ---AGSTAT---; (nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null); echo ---AGSTAT---; df -Pk / 2>/dev/null | tail -1";
|
|
24
|
+
/** Windows equivalent, one labeled line via CIM. PowerShell 5.1-safe: no `||`
|
|
25
|
+
* chaining, plain string concatenation. `LoadPercentage` is $null on some
|
|
26
|
+
* hosts/VMs, which concatenates to an empty field — the parser treats that as
|
|
27
|
+
* "no load signal" and headroom falls back to memory pressure alone.
|
|
28
|
+
* wrapRemoteCommand base64-encodes this for powershell-shell devices, so the
|
|
29
|
+
* quoting survives ssh intact. */
|
|
30
|
+
export declare const WIN_PROBE_SNIPPET = "$os = Get-CimInstance Win32_OperatingSystem; $cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average; $disk = Get-CimInstance Win32_LogicalDisk -Filter \"DeviceID='C:'\"; Write-Output ('AGWINSTAT load=' + $cpu + ' freeKb=' + $os.FreePhysicalMemory + ' totalKb=' + $os.TotalVisibleMemorySize + ' ncpu=' + $env:NUMBER_OF_PROCESSORS + ' diskFreeKb=' + ($disk.FreeSpace / 1KB) + ' diskTotalKb=' + ($disk.Size / 1KB))";
|
|
31
|
+
export declare function localProbeInvocation(platform: NodeJS.Platform): {
|
|
32
|
+
file: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
};
|
|
22
35
|
export interface DeviceStats {
|
|
23
36
|
host: string;
|
|
24
37
|
reachable: boolean;
|
|
@@ -30,6 +43,11 @@ export interface DeviceStats {
|
|
|
30
43
|
memPercent?: number;
|
|
31
44
|
memTotalBytes?: number;
|
|
32
45
|
memFreeBytes?: number;
|
|
46
|
+
diskTotalBytes?: number;
|
|
47
|
+
diskFreeBytes?: number;
|
|
48
|
+
diskUsedPercent?: number;
|
|
49
|
+
/** When the static ncpu/RAM/disk totals were last observed. */
|
|
50
|
+
specsFetchedAt?: number;
|
|
33
51
|
fetchedAt: number;
|
|
34
52
|
}
|
|
35
53
|
/** Compact human byte size: 512M, 64G, 1.5T (binary units, ≤1 decimal). */
|
|
@@ -47,7 +65,14 @@ export declare function parseLinuxMemInfo(out: string): MemStats;
|
|
|
47
65
|
export declare function parseNcpu(out: string): {
|
|
48
66
|
ncpu?: number;
|
|
49
67
|
};
|
|
50
|
-
|
|
68
|
+
interface DiskStats {
|
|
69
|
+
diskTotalBytes?: number;
|
|
70
|
+
diskFreeBytes?: number;
|
|
71
|
+
diskUsedPercent?: number;
|
|
72
|
+
}
|
|
73
|
+
/** Parse the final data row from `df -k /`, deriving usage from block counts. */
|
|
74
|
+
export declare function parseDf(out: string): DiskStats;
|
|
75
|
+
/** Assemble a DeviceStats from the four snippet sections. */
|
|
51
76
|
export declare function parseProbeOutput(host: string, stdout: string, fetchedAt: number): DeviceStats;
|
|
52
77
|
/** Assemble a DeviceStats from the windows one-liner. A missing marker line
|
|
53
78
|
* (e.g. CIM unavailable) keeps `reachable: true` — ssh answered — with no
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Device resource probing for `agents devices list`.
|
|
3
3
|
*
|
|
4
|
-
* One SSH round-trip per device gathers load average, memory pressure, and core
|
|
4
|
+
* One SSH round-trip per device gathers load average, memory pressure, disk, and core
|
|
5
5
|
* count (mac + linux via a POSIX snippet, windows via a CIM one-liner), parsed
|
|
6
6
|
* into a {@link DeviceStats}. Probes run in parallel with a bounded timeout so
|
|
7
7
|
* the list stays responsive — a slow or hung box degrades to "no stats" instead
|
|
@@ -21,16 +21,20 @@ export const PROBE_TIMEOUT_MS = 2_500;
|
|
|
21
21
|
* routinely blows the 2.5s POSIX budget on a relayed connection. */
|
|
22
22
|
export const WIN_PROBE_TIMEOUT_MS = 6_000;
|
|
23
23
|
const SEP = '---AGSTAT---';
|
|
24
|
-
/** One-shot remote snapshot: load,
|
|
25
|
-
|
|
26
|
-
const PROBE_SNIPPET = `uptime; echo ${SEP}; (vm_stat 2>/dev/null || cat /proc/meminfo 2>/dev/null); echo ${SEP}; (nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null)`;
|
|
24
|
+
/** One-shot remote snapshot: load, memory, core count, then root filesystem. */
|
|
25
|
+
export const PROBE_SNIPPET = `uptime; echo ${SEP}; (vm_stat 2>/dev/null || cat /proc/meminfo 2>/dev/null); echo ${SEP}; (nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null); echo ${SEP}; df -Pk / 2>/dev/null | tail -1`;
|
|
27
26
|
/** Windows equivalent, one labeled line via CIM. PowerShell 5.1-safe: no `||`
|
|
28
27
|
* chaining, plain string concatenation. `LoadPercentage` is $null on some
|
|
29
28
|
* hosts/VMs, which concatenates to an empty field — the parser treats that as
|
|
30
29
|
* "no load signal" and headroom falls back to memory pressure alone.
|
|
31
30
|
* wrapRemoteCommand base64-encodes this for powershell-shell devices, so the
|
|
32
31
|
* quoting survives ssh intact. */
|
|
33
|
-
const WIN_PROBE_SNIPPET = `$os = Get-CimInstance Win32_OperatingSystem; $cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average; Write-Output ('AGWINSTAT load=' + $cpu + ' freeKb=' + $os.FreePhysicalMemory + ' totalKb=' + $os.TotalVisibleMemorySize + ' ncpu=' + $env:NUMBER_OF_PROCESSORS)`;
|
|
32
|
+
export const WIN_PROBE_SNIPPET = `$os = Get-CimInstance Win32_OperatingSystem; $cpu = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average; $disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"; Write-Output ('AGWINSTAT load=' + $cpu + ' freeKb=' + $os.FreePhysicalMemory + ' totalKb=' + $os.TotalVisibleMemorySize + ' ncpu=' + $env:NUMBER_OF_PROCESSORS + ' diskFreeKb=' + ($disk.FreeSpace / 1KB) + ' diskTotalKb=' + ($disk.Size / 1KB))`;
|
|
33
|
+
export function localProbeInvocation(platform) {
|
|
34
|
+
return platform === 'win32'
|
|
35
|
+
? { file: 'powershell', args: ['-NoProfile', '-Command', WIN_PROBE_SNIPPET] }
|
|
36
|
+
: { file: 'sh', args: ['-c', PROBE_SNIPPET] };
|
|
37
|
+
}
|
|
34
38
|
/** Compact human byte size: 512M, 64G, 1.5T (binary units, ≤1 decimal). */
|
|
35
39
|
export function fmtBytes(bytes) {
|
|
36
40
|
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0)
|
|
@@ -99,12 +103,28 @@ export function parseNcpu(out) {
|
|
|
99
103
|
const n = parseInt(out.trim().split(/\s+/)[0] ?? '', 10);
|
|
100
104
|
return Number.isFinite(n) && n > 0 ? { ncpu: n } : {};
|
|
101
105
|
}
|
|
102
|
-
/**
|
|
106
|
+
/** Parse the final data row from `df -k /`, deriving usage from block counts. */
|
|
107
|
+
export function parseDf(out) {
|
|
108
|
+
const columns = out.trim().split(/\s+/);
|
|
109
|
+
if (columns.length < 4)
|
|
110
|
+
return {};
|
|
111
|
+
const totalKb = Number(columns[1]);
|
|
112
|
+
const freeKb = Number(columns[3]);
|
|
113
|
+
if (!Number.isFinite(totalKb) || totalKb <= 0 || !Number.isFinite(freeKb) || freeKb < 0)
|
|
114
|
+
return {};
|
|
115
|
+
return {
|
|
116
|
+
diskTotalBytes: totalKb * 1024,
|
|
117
|
+
diskFreeBytes: freeKb * 1024,
|
|
118
|
+
diskUsedPercent: Math.max(0, Math.min(100, ((totalKb - freeKb) / totalKb) * 100)),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Assemble a DeviceStats from the four snippet sections. */
|
|
103
122
|
export function parseProbeOutput(host, stdout, fetchedAt) {
|
|
104
|
-
const [uptimePart = '', memPart = '', ncpuPart = ''] = stdout.split(SEP);
|
|
123
|
+
const [uptimePart = '', memPart = '', ncpuPart = '', diskPart = ''] = stdout.split(SEP);
|
|
105
124
|
const { loadAvg1 } = parseUptime(uptimePart);
|
|
106
125
|
const mem = memPart.includes('MemTotal') ? parseLinuxMemInfo(memPart) : parseVmStat(memPart);
|
|
107
126
|
const { ncpu } = parseNcpu(ncpuPart);
|
|
127
|
+
const disk = parseDf(diskPart);
|
|
108
128
|
const loadPercent = loadAvg1 !== undefined && ncpu ? (loadAvg1 / ncpu) * 100 : undefined;
|
|
109
129
|
return {
|
|
110
130
|
host,
|
|
@@ -115,6 +135,8 @@ export function parseProbeOutput(host, stdout, fetchedAt) {
|
|
|
115
135
|
memPercent: mem.memPercent,
|
|
116
136
|
memTotalBytes: mem.memTotalBytes,
|
|
117
137
|
memFreeBytes: mem.memFreeBytes,
|
|
138
|
+
...disk,
|
|
139
|
+
specsFetchedAt: fetchedAt,
|
|
118
140
|
fetchedAt,
|
|
119
141
|
};
|
|
120
142
|
}
|
|
@@ -122,13 +144,16 @@ export function parseProbeOutput(host, stdout, fetchedAt) {
|
|
|
122
144
|
* (e.g. CIM unavailable) keeps `reachable: true` — ssh answered — with no
|
|
123
145
|
* numbers, mirroring how garbage POSIX output degrades. */
|
|
124
146
|
export function parseWinProbeOutput(host, stdout, fetchedAt) {
|
|
125
|
-
const m = stdout.match(/AGWINSTAT load=([0-9.]*) freeKb=([0-9]+) totalKb=([0-9]+) ncpu=([0-9]+)
|
|
147
|
+
const m = stdout.match(/AGWINSTAT load=([0-9.]*) freeKb=([0-9]+) totalKb=([0-9]+) ncpu=([0-9]+)(?: diskFreeKb=([0-9.]+) diskTotalKb=([0-9.]+))?/);
|
|
126
148
|
if (!m)
|
|
127
149
|
return { host, reachable: true, fetchedAt };
|
|
128
150
|
const loadPercent = m[1] === '' ? undefined : parseFloat(m[1]);
|
|
129
151
|
const freeKb = parseInt(m[2], 10);
|
|
130
152
|
const totalKb = parseInt(m[3], 10);
|
|
131
153
|
const ncpu = parseInt(m[4], 10);
|
|
154
|
+
const diskFreeKb = m[5] === undefined ? undefined : Number(m[5]);
|
|
155
|
+
const diskTotalKb = m[6] === undefined ? undefined : Number(m[6]);
|
|
156
|
+
const hasDisk = diskTotalKb !== undefined && diskTotalKb > 0 && diskFreeKb !== undefined && diskFreeKb >= 0;
|
|
132
157
|
return {
|
|
133
158
|
host,
|
|
134
159
|
reachable: true,
|
|
@@ -137,6 +162,10 @@ export function parseWinProbeOutput(host, stdout, fetchedAt) {
|
|
|
137
162
|
memPercent: totalKb > 0 ? Math.max(0, Math.min(100, ((totalKb - freeKb) / totalKb) * 100)) : undefined,
|
|
138
163
|
memTotalBytes: totalKb > 0 ? totalKb * 1024 : undefined,
|
|
139
164
|
memFreeBytes: totalKb > 0 ? freeKb * 1024 : undefined,
|
|
165
|
+
diskTotalBytes: hasDisk ? diskTotalKb * 1024 : undefined,
|
|
166
|
+
diskFreeBytes: hasDisk ? diskFreeKb * 1024 : undefined,
|
|
167
|
+
diskUsedPercent: hasDisk ? Math.max(0, Math.min(100, ((diskTotalKb - diskFreeKb) / diskTotalKb) * 100)) : undefined,
|
|
168
|
+
specsFetchedAt: fetchedAt,
|
|
140
169
|
fetchedAt,
|
|
141
170
|
};
|
|
142
171
|
}
|
|
@@ -208,8 +237,9 @@ export function probeDeviceStats(device, opts = {}) {
|
|
|
208
237
|
export function probeLocalStats(host, opts = {}) {
|
|
209
238
|
const fetchedAt = opts.now ?? Date.now();
|
|
210
239
|
const isWin = process.platform === 'win32';
|
|
240
|
+
const invocation = localProbeInvocation(process.platform);
|
|
211
241
|
return new Promise((resolve) => {
|
|
212
|
-
execFile(
|
|
242
|
+
execFile(invocation.file, invocation.args, { encoding: 'utf-8', timeout: opts.timeoutMs ?? (isWin ? WIN_PROBE_TIMEOUT_MS : PROBE_TIMEOUT_MS) }, (err, stdout) => {
|
|
213
243
|
if (err || !stdout)
|
|
214
244
|
return resolve({ host, reachable: false, fetchedAt });
|
|
215
245
|
resolve(isWin ? parseWinProbeOutput(host, stdout, fetchedAt) : parseProbeOutput(host, stdout, fetchedAt));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The reserved `--device` value meaning the box the human is at. */
|
|
2
|
+
export declare const INTERACTIVE_DEVICE_SENTINEL = "interactive";
|
|
3
|
+
/** True when a host flag value is the interactive sentinel. */
|
|
4
|
+
export declare function isDeviceInteractive(value: string | undefined | null): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* The device pinned as `interactive.host`, or null when unset.
|
|
7
|
+
*
|
|
8
|
+
* Callers MUST refuse on null rather than falling back to the local machine.
|
|
9
|
+
* The whole point of the sentinel is reaching a screen someone is watching;
|
|
10
|
+
* silently running on a headless worker instead is the exact failure it exists
|
|
11
|
+
* to prevent, and it would fail invisibly.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveInteractiveDevice(): string | null;
|
|
14
|
+
/** The actionable error for an unset pin — shared so every call site says the same thing. */
|
|
15
|
+
export declare function interactiveUnsetError(): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `interactive` device sentinel: "wherever the human is sitting".
|
|
3
|
+
*
|
|
4
|
+
* `--device auto` means "pick a box by load". This means the opposite — one
|
|
5
|
+
* specific box, the one whose screen a person is actually looking at, pinned as
|
|
6
|
+
* `interactive.host`.
|
|
7
|
+
*
|
|
8
|
+
* It exists because a skill cannot teach a host name. Guidance that says
|
|
9
|
+
* "deliver it to zion" is wrong on every other fleet and stale the moment the
|
|
10
|
+
* pin changes, so agents were left to infer the target or to skip the step. A
|
|
11
|
+
* fixed token is something a SKILL.md can state literally and have be correct
|
|
12
|
+
* everywhere.
|
|
13
|
+
*
|
|
14
|
+
* Resolution happens at the dispatch site, before `isSelfHost`, exactly like
|
|
15
|
+
* `auto` — so a pin naming this machine runs locally instead of self-SSHing, and
|
|
16
|
+
* the forwarded argv has the routing flag stripped, which is what keeps this
|
|
17
|
+
* from recursing.
|
|
18
|
+
*/
|
|
19
|
+
import { getConfigValue } from '../device-config.js';
|
|
20
|
+
import { RESERVED_DEVICE_NAMES } from './registry.js';
|
|
21
|
+
/** The reserved `--device` value meaning the box the human is at. */
|
|
22
|
+
export const INTERACTIVE_DEVICE_SENTINEL = 'interactive';
|
|
23
|
+
/** True when a host flag value is the interactive sentinel. */
|
|
24
|
+
export function isDeviceInteractive(value) {
|
|
25
|
+
return typeof value === 'string' && value.trim().toLowerCase() === INTERACTIVE_DEVICE_SENTINEL;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The device pinned as `interactive.host`, or null when unset.
|
|
29
|
+
*
|
|
30
|
+
* Callers MUST refuse on null rather than falling back to the local machine.
|
|
31
|
+
* The whole point of the sentinel is reaching a screen someone is watching;
|
|
32
|
+
* silently running on a headless worker instead is the exact failure it exists
|
|
33
|
+
* to prevent, and it would fail invisibly.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveInteractiveDevice() {
|
|
36
|
+
const pinned = getConfigValue('interactive.host').value;
|
|
37
|
+
if (typeof pinned !== 'string' || !pinned.trim())
|
|
38
|
+
return null;
|
|
39
|
+
const host = pinned.trim();
|
|
40
|
+
// Defensive only. `interactive.host` rejects every reserved sentinel at WRITE
|
|
41
|
+
// time (assertValidDeviceName), so a pin of "interactive" or "auto" cannot be
|
|
42
|
+
// stored in the first place — which is the right layer, because refusing on
|
|
43
|
+
// read could only ever say "none is set" and send the user back to the command
|
|
44
|
+
// they just ran. This catches a config written by an older version.
|
|
45
|
+
if (RESERVED_DEVICE_NAMES.has(host.toLowerCase()))
|
|
46
|
+
return null;
|
|
47
|
+
return host;
|
|
48
|
+
}
|
|
49
|
+
/** The actionable error for an unset pin — shared so every call site says the same thing. */
|
|
50
|
+
export function interactiveUnsetError() {
|
|
51
|
+
return (`--device interactive needs an interactive host pinned, and none is set.\n` +
|
|
52
|
+
` Set it on the machine you sit at: agents config set interactive.host <device>`);
|
|
53
|
+
}
|