@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
|
@@ -6,7 +6,7 @@ import { getBinaryPath, invalidateInstalledVersionsCache, invalidateLiveVersionC
|
|
|
6
6
|
import { assertValidRelease, selectUpdateStrategy, } from './strategies.js';
|
|
7
7
|
import { listInstallations, readInstallation, recordRelease, writeInstallation } from './store.js';
|
|
8
8
|
import { effectiveUpdatePolicy, isAutoUpdateEnabledForAgent } from './update-policy.js';
|
|
9
|
-
import {
|
|
9
|
+
import { describeInstallationActivity, formatInUseDeferral } from './active-check.js';
|
|
10
10
|
import { installationLockTarget, INSTALLATION_LOCK_OPTIONS } from './installation-lock.js';
|
|
11
11
|
/**
|
|
12
12
|
* Move one frozen installation to a new vendor release, preserving its identity.
|
|
@@ -88,18 +88,21 @@ async function runUpdateInstallation(agent, requestedInstallation, options) {
|
|
|
88
88
|
return { installation, strategy: strategy.id, fromRelease: installation.releaseVersion, toRelease: target,
|
|
89
89
|
unchanged: true, deferred: 'Update cancelled or automatic update policy changed.', alsoUpdated: [] };
|
|
90
90
|
}
|
|
91
|
-
if (strategy.transactional
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
if (strategy.transactional) {
|
|
92
|
+
const activity = await describeInstallationActivity(installation);
|
|
93
|
+
if (activity.active) {
|
|
94
|
+
const name = `${AGENTS[agent].name}@${installation.label}`;
|
|
95
|
+
options.onProgress?.(`${name} looks active right now; not staging release ${target}.`);
|
|
96
|
+
return {
|
|
97
|
+
installation,
|
|
98
|
+
strategy: strategy.id,
|
|
99
|
+
fromRelease: installation.releaseVersion,
|
|
100
|
+
toRelease: target,
|
|
101
|
+
unchanged: true,
|
|
102
|
+
deferred: formatInUseDeferral(name, activity),
|
|
103
|
+
alsoUpdated: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
103
106
|
}
|
|
104
107
|
let staged = null;
|
|
105
108
|
try {
|
|
@@ -179,7 +182,8 @@ async function runUpdateInstallation(agent, requestedInstallation, options) {
|
|
|
179
182
|
// identical pre-stage check above for why this cannot be opt-in: a launch
|
|
180
183
|
// that starts AFTER that first check but before this swap is exactly the
|
|
181
184
|
// window this closes.
|
|
182
|
-
|
|
185
|
+
const lateActivity = strategy.transactional ? await describeInstallationActivity(installation) : null;
|
|
186
|
+
if (options.shouldCancel?.() || lateActivity?.active) {
|
|
183
187
|
options.onProgress?.(`${AGENTS[agent].name}@${installation.label} looks active now (a process or launch lease appeared while `
|
|
184
188
|
+ `${staged.release} was staging); not committing it.`);
|
|
185
189
|
return {
|
|
@@ -188,7 +192,9 @@ async function runUpdateInstallation(agent, requestedInstallation, options) {
|
|
|
188
192
|
fromRelease: installation.releaseVersion,
|
|
189
193
|
toRelease: staged.release,
|
|
190
194
|
unchanged: true,
|
|
191
|
-
deferred:
|
|
195
|
+
deferred: lateActivity?.active
|
|
196
|
+
? formatInUseDeferral(`${AGENTS[agent].name}@${installation.label}`, lateActivity)
|
|
197
|
+
: 'Update cancelled while the release was being prepared.',
|
|
192
198
|
alsoUpdated: [],
|
|
193
199
|
};
|
|
194
200
|
}
|
|
@@ -28,7 +28,7 @@ import { INSTALLATION_RECORD_FILE } from './types.js';
|
|
|
28
28
|
import { composeWin32CommandLine } from '../platform/index.js';
|
|
29
29
|
import { listInstalledSubagents } from '../subagents.js';
|
|
30
30
|
import { listInstalledWorkflows } from '../workflows.js';
|
|
31
|
-
import { parseHookManifest, registerHooksToSettings, selectHookManifest, pruneVersionHomeHookEntriesFromSettings, installSessionTrackerHookSync, installSessionTrackerHook } from '../hooks/install.js';
|
|
31
|
+
import { parseHookManifest, registerHooksToSettings, selectHookManifest, pruneVersionHomeHookEntriesFromSettings, installSessionTrackerHookSync, installSessionTrackerHook, repairManagedHookRuntimeArtifacts } from '../hooks/install.js';
|
|
32
32
|
import { supports, explainSkip } from '../capabilities.js';
|
|
33
33
|
import { discoverPlugins, syncPluginToVersion, pluginSupportsAgent, cleanOrphanedPluginSkills, marketplaceSpecForName } from '../plugins/plugins.js';
|
|
34
34
|
import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from '../staleness/index.js';
|
|
@@ -1630,6 +1630,17 @@ export function removeVersion(agent, version) {
|
|
|
1630
1630
|
pruneVersionHomeHookEntriesFromSettings(settingsPath, agent, version);
|
|
1631
1631
|
}
|
|
1632
1632
|
}
|
|
1633
|
+
// The global hook shims embed one SOURCE path. When it pointed into this
|
|
1634
|
+
// version home every shimmed guard would exit 127 (allow) until a self-heal
|
|
1635
|
+
// pass ran, and that pass needs a live daemon. Re-point them now. Best
|
|
1636
|
+
// effort: the removal itself already succeeded, and a repair failure on an
|
|
1637
|
+
// unrelated harness's shim must not abort a bulk removal.
|
|
1638
|
+
try {
|
|
1639
|
+
repairManagedHookRuntimeArtifacts();
|
|
1640
|
+
}
|
|
1641
|
+
catch (err) {
|
|
1642
|
+
console.warn(`hook shim repair after removing ${agent}@${version} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1643
|
+
}
|
|
1633
1644
|
emit('version.remove', { agent, version });
|
|
1634
1645
|
return true;
|
|
1635
1646
|
}
|
package/dist/lib/manifest.js
CHANGED
package/dist/lib/models.js
CHANGED
|
@@ -113,8 +113,6 @@ export function locateModelSource(agent, version) {
|
|
|
113
113
|
}
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
|
-
// gemini is hard-deprecated (no launch path left to validate a model
|
|
117
|
-
// against), so its bundle is deliberately not parsed for a catalog here.
|
|
118
116
|
if (agent === 'opencode') {
|
|
119
117
|
// The `opencode` shim under node_modules/.bin dispatches to a platform-
|
|
120
118
|
// specific native binary. We don't parse the 100MB binary; we let the CLI
|
package/dist/lib/open-url.js
CHANGED
|
@@ -101,68 +101,42 @@ export async function resolveViewer(opts = {}) {
|
|
|
101
101
|
if (opts.osBrowser)
|
|
102
102
|
return 'os';
|
|
103
103
|
const { getConfigValue } = await import('./device-config.js');
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
104
|
+
// A configured viewer, else the profile agents drive (`browser.profile`) — the
|
|
105
|
+
// whole point is that a machine with a configured browser stops leaking pages
|
|
106
|
+
// to the OS handler. Both keys are shared with the standalone `browser` CLI,
|
|
107
|
+
// whose `show` verb applies exactly this precedence itself; agents-cli resolves
|
|
108
|
+
// the NAME only to decide viewer-vs-OS, then delegates suitability (Arc/Firefox/
|
|
109
|
+
// launchable-here) to the engine (PHNX-4101).
|
|
110
|
+
const name = opts.profile
|
|
111
|
+
?? (getConfigValue('browser.viewer').value || undefined)
|
|
112
|
+
?? (getConfigValue('browser.profile').value || undefined);
|
|
113
|
+
if (!name || name === 'os')
|
|
110
114
|
return 'os';
|
|
111
|
-
|
|
112
|
-
return 'os';
|
|
113
|
-
let resolved;
|
|
114
|
-
try {
|
|
115
|
-
resolved = await resolveProfileRef(name);
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
console.error(`[viewer] ${name}: ${err instanceof Error ? err.message : String(err)} — using the OS browser.`);
|
|
119
|
-
return 'os';
|
|
120
|
-
}
|
|
121
|
-
if (!resolved) {
|
|
122
|
-
console.error(`[viewer] "${name}" does not resolve to a profile — using the OS browser.`);
|
|
123
|
-
return 'os';
|
|
124
|
-
}
|
|
125
|
-
const profile = await getProfile(resolved);
|
|
126
|
-
if (!profile) {
|
|
127
|
-
console.error(`[viewer] profile "${resolved}" is not configured — using the OS browser.`);
|
|
128
|
-
return 'os';
|
|
129
|
-
}
|
|
130
|
-
if (profile.browser === 'arc') {
|
|
131
|
-
// agents browser can drive an attached Arc's existing tabs, but showing the
|
|
132
|
-
// user a page needs its OWN fresh tab, and Arc crashes on tab creation over
|
|
133
|
-
// CDP (Target.createTarget). So Arc is a fine automation profile but never a
|
|
134
|
-
// viewer — fall back to the OS browser for anything a human is meant to read.
|
|
135
|
-
console.error(`[viewer] "${resolved}" is Arc, which cannot open a new viewer tab — using the OS browser.`);
|
|
136
|
-
return 'os';
|
|
137
|
-
}
|
|
138
|
-
if (profile.browser === 'firefox') {
|
|
139
|
-
// agents drives Firefox headless over BiDi for its own read-back; a page a
|
|
140
|
-
// human is meant to read goes to the visible OS browser, not the headless one.
|
|
141
|
-
console.error(`[viewer] "${resolved}" is a headless-automation Firefox profile — using the OS browser.`);
|
|
142
|
-
return 'os';
|
|
143
|
-
}
|
|
144
|
-
if (!isProfileLaunchableHere(profile)) {
|
|
145
|
-
console.error(`[viewer] "${resolved}" cannot launch on this machine — using the OS browser.`);
|
|
146
|
-
return 'os';
|
|
147
|
-
}
|
|
148
|
-
return { profile: resolved };
|
|
115
|
+
return { profile: name };
|
|
149
116
|
}
|
|
150
117
|
/** Show a URL to the human at this machine. Never throws. */
|
|
151
118
|
export async function showUrl(url, opts = {}) {
|
|
152
119
|
const viewer = await resolveViewer(opts);
|
|
153
120
|
if (viewer === 'os')
|
|
154
121
|
return osOpen(url, opts.spawnOpen);
|
|
122
|
+
// Delegate to the standalone `browser show`, which opens the URL in the viewer
|
|
123
|
+
// profile (or `browser.viewer`/`browser.profile`) and owns its own service
|
|
124
|
+
// lifecycle and viewer-suitability fallbacks. When the engine is not installed,
|
|
125
|
+
// or the call fails, degrade to the OS handler with one stderr line.
|
|
155
126
|
try {
|
|
156
|
-
const {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
127
|
+
const { browserInstalled, runBrowser } = await import('./browser-client.js');
|
|
128
|
+
if (!browserInstalled()) {
|
|
129
|
+
console.error(`[viewer] the standalone browser CLI is not installed — using the OS browser.`);
|
|
130
|
+
return osOpen(url, opts.spawnOpen);
|
|
131
|
+
}
|
|
132
|
+
const { buildBrowserContext } = await import('./browser/context.js');
|
|
133
|
+
const { exitCode } = await runBrowser({
|
|
134
|
+
argv: ['show', url, '--profile', viewer.profile],
|
|
135
|
+
context: await buildBrowserContext(),
|
|
136
|
+
});
|
|
137
|
+
if (exitCode === 0)
|
|
138
|
+
return { via: 'profile', profile: viewer.profile };
|
|
139
|
+
console.error(`[viewer] ${viewer.profile}: browser show exited ${exitCode} — using the OS browser.`);
|
|
166
140
|
}
|
|
167
141
|
catch (err) {
|
|
168
142
|
console.error(`[viewer] ${viewer.profile}: ${err instanceof Error ? err.message : String(err)} — using the OS browser.`);
|
package/dist/lib/placement.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ interface RunPlacementFlags {
|
|
|
45
45
|
cloud?: boolean;
|
|
46
46
|
/** --provider: refines the cloud placement; not a placement on its own. */
|
|
47
47
|
provider?: string;
|
|
48
|
+
/** --local: this machine, said explicitly — the pin a bare interactive run needs to stay off `--device auto`. */
|
|
49
|
+
local?: boolean;
|
|
48
50
|
}
|
|
49
51
|
export declare class PlacementError extends Error {
|
|
50
52
|
constructor(message: string);
|
package/dist/lib/placement.js
CHANGED
|
@@ -94,9 +94,12 @@ export function placementFromRunFlags(flags) {
|
|
|
94
94
|
const hasLease = flags.lease !== undefined && flags.lease !== false;
|
|
95
95
|
const hasBox = !!flags.box;
|
|
96
96
|
const hasCloud = flags.cloud === true;
|
|
97
|
+
const hasLocal = flags.local === true;
|
|
97
98
|
const placementFlags = [];
|
|
98
99
|
if (where)
|
|
99
100
|
placementFlags.push('--where');
|
|
101
|
+
if (hasLocal)
|
|
102
|
+
placementFlags.push('--local');
|
|
100
103
|
if (hostT)
|
|
101
104
|
placementFlags.push('--device');
|
|
102
105
|
if (hasLease)
|
|
@@ -111,6 +114,8 @@ export function placementFromRunFlags(flags) {
|
|
|
111
114
|
}
|
|
112
115
|
if (where)
|
|
113
116
|
return parseWhereSpec(where, '--where');
|
|
117
|
+
if (hasLocal)
|
|
118
|
+
return { kind: 'local', source: '--local' };
|
|
114
119
|
if (hasCloud)
|
|
115
120
|
return { kind: 'cloud', target: flags.provider, source: '--cloud' };
|
|
116
121
|
if (hasBox)
|
package/dist/lib/profiles.js
CHANGED
|
@@ -352,7 +352,6 @@ const MODEL_ENV_KEY_BY_HOST = {
|
|
|
352
352
|
claude: 'ANTHROPIC_MODEL',
|
|
353
353
|
opencode: 'OPENCODE_MODEL',
|
|
354
354
|
grok: 'GROK_MODEL',
|
|
355
|
-
gemini: 'GEMINI_MODEL',
|
|
356
355
|
codex: 'OPENAI_MODEL',
|
|
357
356
|
};
|
|
358
357
|
/** Base-URL env var per host, for the OpenAI/Anthropic-compatible hosts where it applies. */
|
|
@@ -372,7 +371,6 @@ export function baseUrlEnvKeyForHost(host) {
|
|
|
372
371
|
const AUTH_ENV_KEY_BY_HOST = {
|
|
373
372
|
claude: 'ANTHROPIC_AUTH_TOKEN',
|
|
374
373
|
codex: 'OPENAI_API_KEY',
|
|
375
|
-
gemini: 'GEMINI_API_KEY',
|
|
376
374
|
grok: 'XAI_API_KEY',
|
|
377
375
|
opencode: 'OPENCODE_API_KEY',
|
|
378
376
|
};
|
package/dist/lib/refresh.js
CHANGED
|
@@ -32,7 +32,7 @@ function migratePromptcutsToRoot(agentsDir, quiet = false) {
|
|
|
32
32
|
const rootPath = path.join(agentsDir, 'promptcuts.yaml');
|
|
33
33
|
if (fs.existsSync(rootPath))
|
|
34
34
|
return;
|
|
35
|
-
const agentDirs = ['claude', 'codex', 'cursor', '
|
|
35
|
+
const agentDirs = ['claude', 'codex', 'cursor', 'opencode'];
|
|
36
36
|
for (const dir of agentDirs) {
|
|
37
37
|
const legacyPath = path.join(agentsDir, dir, 'promptcuts.yaml');
|
|
38
38
|
if (fs.existsSync(legacyPath)) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { JobConfig } from './scheduling/routines.js';
|
|
13
13
|
import { type RoutineReadinessResult, type RoutineReadiness } from './routine-context.js';
|
|
14
|
+
import { type AuthVerdict } from './auth-health.js';
|
|
14
15
|
/**
|
|
15
16
|
* Fire-time auth preflight: read the daemon-warmed auth-health cache for the
|
|
16
17
|
* exact (agent, version) a routine has resolved to run, and return an
|
|
@@ -48,6 +49,40 @@ export declare function parseRemoteProjectSnapshot(stdout: string): {
|
|
|
48
49
|
export declare function buildRemoteWorkspaceProbe(cwd: string, windows: boolean): string;
|
|
49
50
|
/** A successful probe must contain the exact requested postcondition, not merely exit zero. */
|
|
50
51
|
export declare function probeOutputHasSentinel(stdout: string, sentinel: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* ONE decision the local ({@link evaluateActivationReadinessLive}) and host
|
|
54
|
+
* ({@link evaluateHostActivationReadiness}) readiness paths share, so both reach
|
|
55
|
+
* the SAME answer for the same box (PHNX-4116). `row` is that box's probe verdict
|
|
56
|
+
* for the routine's agent — ABSENT when the box could not probe (a worker's
|
|
57
|
+
* setup-token box, or a headed box that spent its hourly usage-endpoint budget,
|
|
58
|
+
* both drop `no_evidence` in `probeLocalFleetAuth`). `launchable` is whether the
|
|
59
|
+
* box holds a launchable signed-in credential, from the SAME
|
|
60
|
+
* `collectRunCandidates`/`isLaunchableSignedIn` the run router uses — so a
|
|
61
|
+
* token-backed routine on a box that cannot probe stays activatable, a box with
|
|
62
|
+
* no credential at all fails `unconfigured`, and a server rejection (`revoked` —
|
|
63
|
+
* the only present-but-not-accepted verdict these force-probed paths produce)
|
|
64
|
+
* blocks regardless of a token on disk.
|
|
65
|
+
*/
|
|
66
|
+
export declare function decideRoutineAuthReadiness(row: {
|
|
67
|
+
verdict: AuthVerdict;
|
|
68
|
+
} | undefined, launchable: boolean): {
|
|
69
|
+
ok: boolean;
|
|
70
|
+
reason?: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Decide a host-placed routine's auth readiness from the REMOTE box's `devices
|
|
74
|
+
* ping --local --json` payload (PHNX-4116). Pure over the payload TEXT so it is
|
|
75
|
+
* unit-tested without SSH: it finds this agent's probe row (ABSENT when the box
|
|
76
|
+
* dropped its `no_evidence` row) and reads the box's per-agent launchability the
|
|
77
|
+
* ping now carries, then runs {@link decideRoutineAuthReadiness} — so a host box
|
|
78
|
+
* that could not probe but holds a token is ready, exactly as the local path
|
|
79
|
+
* decides for the same box. Returns null when the payload cannot be parsed; the
|
|
80
|
+
* caller treats that as a probe failure, never as "no credential".
|
|
81
|
+
*/
|
|
82
|
+
export declare function decideHostAuthFromPing(stdout: string, agent: string): {
|
|
83
|
+
ok: boolean;
|
|
84
|
+
reason?: string;
|
|
85
|
+
} | null;
|
|
51
86
|
/**
|
|
52
87
|
* Interactive setup/repair readiness. Unlike the scheduler's deterministic
|
|
53
88
|
* structural gate, this completes a real local auth request and reads Codex's
|
|
@@ -162,6 +162,62 @@ function codexWorkspaceTrusted(version, cwd) {
|
|
|
162
162
|
return false;
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Verdicts a routine's activation auth check accepts as usable. `no_evidence` is
|
|
167
|
+
* here for robustness (a payload that still carries one), but a box that cannot
|
|
168
|
+
* probe DROPS it in {@link probeLocalFleetAuth}, so the real path for those boxes
|
|
169
|
+
* is the absent-row + launchability fallback in {@link decideRoutineAuthReadiness}
|
|
170
|
+
* (PHNX-4116).
|
|
171
|
+
*/
|
|
172
|
+
const ROUTINE_AUTH_ACCEPTED = new Set([
|
|
173
|
+
'live', 'rate_limited', 'unverified', 'no_evidence',
|
|
174
|
+
]);
|
|
175
|
+
/**
|
|
176
|
+
* ONE decision the local ({@link evaluateActivationReadinessLive}) and host
|
|
177
|
+
* ({@link evaluateHostActivationReadiness}) readiness paths share, so both reach
|
|
178
|
+
* the SAME answer for the same box (PHNX-4116). `row` is that box's probe verdict
|
|
179
|
+
* for the routine's agent — ABSENT when the box could not probe (a worker's
|
|
180
|
+
* setup-token box, or a headed box that spent its hourly usage-endpoint budget,
|
|
181
|
+
* both drop `no_evidence` in `probeLocalFleetAuth`). `launchable` is whether the
|
|
182
|
+
* box holds a launchable signed-in credential, from the SAME
|
|
183
|
+
* `collectRunCandidates`/`isLaunchableSignedIn` the run router uses — so a
|
|
184
|
+
* token-backed routine on a box that cannot probe stays activatable, a box with
|
|
185
|
+
* no credential at all fails `unconfigured`, and a server rejection (`revoked` —
|
|
186
|
+
* the only present-but-not-accepted verdict these force-probed paths produce)
|
|
187
|
+
* blocks regardless of a token on disk.
|
|
188
|
+
*/
|
|
189
|
+
export function decideRoutineAuthReadiness(row, launchable) {
|
|
190
|
+
if (row && ROUTINE_AUTH_ACCEPTED.has(row.verdict))
|
|
191
|
+
return { ok: true };
|
|
192
|
+
if (!row)
|
|
193
|
+
return launchable ? { ok: true } : { ok: false, reason: 'unconfigured' };
|
|
194
|
+
return { ok: false, reason: row.verdict };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Decide a host-placed routine's auth readiness from the REMOTE box's `devices
|
|
198
|
+
* ping --local --json` payload (PHNX-4116). Pure over the payload TEXT so it is
|
|
199
|
+
* unit-tested without SSH: it finds this agent's probe row (ABSENT when the box
|
|
200
|
+
* dropped its `no_evidence` row) and reads the box's per-agent launchability the
|
|
201
|
+
* ping now carries, then runs {@link decideRoutineAuthReadiness} — so a host box
|
|
202
|
+
* that could not probe but holds a token is ready, exactly as the local path
|
|
203
|
+
* decides for the same box. Returns null when the payload cannot be parsed; the
|
|
204
|
+
* caller treats that as a probe failure, never as "no credential".
|
|
205
|
+
*/
|
|
206
|
+
export function decideHostAuthFromPing(stdout, agent) {
|
|
207
|
+
let payload;
|
|
208
|
+
try {
|
|
209
|
+
payload = JSON.parse(stdout);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
if (!payload || typeof payload !== 'object')
|
|
215
|
+
return null;
|
|
216
|
+
const { rows, launchable } = payload;
|
|
217
|
+
const verdict = rows?.find((row) => row.agent === agent)?.health?.verdict;
|
|
218
|
+
const row = verdict ? { verdict } : undefined;
|
|
219
|
+
return decideRoutineAuthReadiness(row, launchable?.includes(agent) ?? false);
|
|
220
|
+
}
|
|
165
221
|
/**
|
|
166
222
|
* Interactive setup/repair readiness. Unlike the scheduler's deterministic
|
|
167
223
|
* structural gate, this completes a real local auth request and reads Codex's
|
|
@@ -180,13 +236,14 @@ export async function evaluateActivationReadinessLive(config) {
|
|
|
180
236
|
if (!version)
|
|
181
237
|
return structural;
|
|
182
238
|
const context = resolveJobExecutionContext(config, { mode: 'local' });
|
|
183
|
-
|
|
239
|
+
// The probe row (absent when this box could not probe — a worker's setup-token
|
|
240
|
+
// box drops `no_evidence`) plus the run-router launchability, run through the
|
|
241
|
+
// ONE decision the host path also uses so both agree for the same box.
|
|
184
242
|
const rows = await probeLocalFleetAuth({ agents: [config.agent] });
|
|
185
243
|
const row = rows.find((candidate) => candidate.version === version);
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
: { ok: false, reason: row?.health.verdict ?? 'unconfigured' };
|
|
244
|
+
const { collectRunCandidates } = await import('./accounting/rotate.js');
|
|
245
|
+
const launchable = (await collectRunCandidates(config.agent)).some((candidate) => candidate.signedIn);
|
|
246
|
+
const authVerdict = decideRoutineAuthReadiness(row?.health, launchable);
|
|
190
247
|
return evaluateRoutineReadiness(context, {
|
|
191
248
|
agentInstalled: () => true,
|
|
192
249
|
...(config.agent === 'codex' && context.absoluteCwd
|
|
@@ -259,19 +316,20 @@ export async function evaluateHostActivationReadiness(config) {
|
|
|
259
316
|
? `powershell -NoProfile -EncodedCommand ${encodePowershell(`${POWERSHELL_PROGRESS_SILENCE}; & agents ${pingArgs.map(powershellQuote).join(' ')}`)}`
|
|
260
317
|
: `agents ${pingArgs.map(shellQuote).join(' ')}`;
|
|
261
318
|
const probe = sshExec(target, command, { timeoutMs: 30_000, extraSshArgs: identity });
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
319
|
+
// A ping that could not run or parse is a probe FAILURE, never mistaken for
|
|
320
|
+
// "no credential": block with the failure so the operator sees it. A parsed
|
|
321
|
+
// payload runs the shared decision — an ABSENT row (the remote dropped its
|
|
322
|
+
// `no_evidence`) plus the launchability the ping now carries is ready,
|
|
323
|
+
// exactly as the local path decides for the same box (PHNX-4116).
|
|
324
|
+
const decision = probe.code === 0 ? decideHostAuthFromPing(probe.stdout, config.agent) : null;
|
|
325
|
+
if (!decision) {
|
|
326
|
+
return evaluateRoutineReadiness(unprobed, {
|
|
327
|
+
authOk: () => ({ ok: false, reason: `devices ping --local failed on ${host.name}` }),
|
|
328
|
+
}, { agent: config.agent });
|
|
271
329
|
}
|
|
272
|
-
if (!
|
|
330
|
+
if (!decision.ok) {
|
|
273
331
|
return evaluateRoutineReadiness(unprobed, {
|
|
274
|
-
authOk: () =>
|
|
332
|
+
authOk: () => decision,
|
|
275
333
|
}, { agent: config.agent });
|
|
276
334
|
}
|
|
277
335
|
}
|
|
@@ -303,6 +303,12 @@ export function compileRulesForProject(cwd, opts = {}) {
|
|
|
303
303
|
const fname = agent.instructionsFile;
|
|
304
304
|
if (seen.has(fname))
|
|
305
305
|
continue;
|
|
306
|
+
// Hard-deprecated agents (e.g. Gemini, retired by Google) never get an
|
|
307
|
+
// instruction-file symlink — the harness is gone, so GEMINI.md would only
|
|
308
|
+
// litter the tree. Mirrors the deprecated?.hard skip used everywhere else
|
|
309
|
+
// (capabilities.ts, MANAGED_AGENT_IDS, modes.ts).
|
|
310
|
+
if (agent.deprecated?.hard)
|
|
311
|
+
continue;
|
|
306
312
|
// Skip agents whose instructions live at a nested path (e.g. OpenClaw's
|
|
307
313
|
// workspace/AGENTS.md) — those are managed by their own setup paths.
|
|
308
314
|
if (fname.includes('/') || fname.includes('\\'))
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -137,8 +137,6 @@ export function prepareJobHome(config, version) {
|
|
|
137
137
|
const overlayHome = getJobHomePath(config.name);
|
|
138
138
|
cleanJobHome(config.name);
|
|
139
139
|
fs.mkdirSync(overlayHome, { recursive: true });
|
|
140
|
-
// gemini is hard-deprecated: runner.ts rejects a gemini routine before it
|
|
141
|
-
// ever reaches prepareJobHome, so there is deliberately no branch here.
|
|
142
140
|
if (config.agent === 'claude') {
|
|
143
141
|
generateClaudeConfig(overlayHome, config);
|
|
144
142
|
}
|
|
@@ -2,7 +2,7 @@ import { type SecretsContext } from './secrets-client.js';
|
|
|
2
2
|
import type { PushBundleResult } from './secrets-types.js';
|
|
3
3
|
import type { AgentId, Meta, NativeAccountRecord } from './types.js';
|
|
4
4
|
import { type DeviceProfile } from './devices/registry.js';
|
|
5
|
-
import { type SharedAuthStatus } from './fleet-shared-state.js';
|
|
5
|
+
import { type FleetSharedDeviceState, type SharedAuthStatus } from './fleet-shared-state.js';
|
|
6
6
|
import { selfConfiguredDeviceRole } from './device-config.js';
|
|
7
7
|
import { AUTH_STORE_ALIAS, AUTH_BUNDLE_BACKEND, RESERVED_BUNDLE_NAMES, isReservedBundleName, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, RESERVED_STORES, reservedStoreName, isReservedStoreName, assertStorableCredentialKind, inspectReservedAuthBundle, type StorableCredentialKind } from './reserved-stores.js';
|
|
8
8
|
export { AUTH_STORE_ALIAS, AUTH_BUNDLE_BACKEND, RESERVED_BUNDLE_NAMES, isReservedBundleName, ReservedBundleWrongBackendError, assertReservedAuthBackend, isReservedBundleBackendError, RESERVED_STORES, reservedStoreName, isReservedStoreName, assertStorableCredentialKind, inspectReservedAuthBundle, type StorableCredentialKind, };
|
|
@@ -107,6 +107,25 @@ export declare function publishReservedAuthVerdict(options?: PublishAuthVerdictO
|
|
|
107
107
|
* only peers whose shared verdict says the bundle is missing.
|
|
108
108
|
*/
|
|
109
109
|
export declare function syncReservedAuthBundle(deps?: AuthSyncDeps): Promise<AuthSyncResult>;
|
|
110
|
+
/**
|
|
111
|
+
* Skip reason for a peer that has never sent a daemon-state reply (no
|
|
112
|
+
* `devices/<peer>/daemon-state.json`). auth-sync logs these at INFO — a
|
|
113
|
+
* brand-new or never-dialed worker legitimately has no reply on an early tick,
|
|
114
|
+
* so it is informational, not a warning. Stable so the service classifies the
|
|
115
|
+
* skip without matching a substring.
|
|
116
|
+
*/
|
|
117
|
+
export declare const SKIP_REASON_NO_PEER_REPLY = "no daemon-state reply from this peer yet";
|
|
118
|
+
/**
|
|
119
|
+
* Skip reason for a peer whose reply exists but carries no `accounts.rows` field
|
|
120
|
+
* at all (an older CLI that does not publish per-account verdicts, or a partial
|
|
121
|
+
* state written before the account-state daemon ran). Without those rows there is
|
|
122
|
+
* no first-hand knowledge of what the peer holds, so the plan FAILS CLOSED and
|
|
123
|
+
* skips rather than pushing every key blindly. auth-sync logs it at INFO — it is
|
|
124
|
+
* transient during a rolling upgrade. An EMPTY `accounts.rows` array (a peer with
|
|
125
|
+
* no registered account) is NOT this case: it is a legitimate "holds nothing",
|
|
126
|
+
* and its keys are (correctly) pushed to provision it.
|
|
127
|
+
*/
|
|
128
|
+
export declare const SKIP_REASON_NO_ACCOUNT_ROWS = "daemon-state reply carries no account rows (fail closed)";
|
|
110
129
|
/** One account's durable worker credential, resolved to (bundle, key). */
|
|
111
130
|
export interface ReservedSyncAccount {
|
|
112
131
|
accountId: string;
|
|
@@ -116,8 +135,11 @@ export interface ReservedSyncAccount {
|
|
|
116
135
|
/** Storage key `<ENV>_<accountId>` (or the legacy email-keyed claude key). */
|
|
117
136
|
key: string;
|
|
118
137
|
/**
|
|
119
|
-
* `workerCredential.mintedAt`
|
|
120
|
-
*
|
|
138
|
+
* The credential's rotation fingerprint: `workerCredential.mintedAt` for a T1
|
|
139
|
+
* row, `'legacy'` for a pre-T1 claude row. A re-mint (`accounts login`) bumps
|
|
140
|
+
* `mintedAt`, so this changes even though the OLD token still authenticates —
|
|
141
|
+
* which is why presence is gated on BOTH the peer's verdict AND a delivered-
|
|
142
|
+
* fingerprint match (see {@link peerPresentKeys}).
|
|
121
143
|
*/
|
|
122
144
|
fingerprint: string;
|
|
123
145
|
}
|
|
@@ -128,7 +150,24 @@ export interface ReservedSyncPeer {
|
|
|
128
150
|
headed: boolean;
|
|
129
151
|
reachable: boolean;
|
|
130
152
|
pinned: boolean;
|
|
131
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* Whether this peer has ever sent a daemon-state reply on this box
|
|
155
|
+
* (`devices/<peer>/daemon-state.json` exists). Without one there is no
|
|
156
|
+
* first-hand knowledge of what it holds, so the peer is skipped this tick.
|
|
157
|
+
*/
|
|
158
|
+
hasReply: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Whether the peer's reply actually carried an `accounts.rows` array (present,
|
|
161
|
+
* even if empty). A reply file with no such field (older CLI, partial state)
|
|
162
|
+
* has no first-hand inventory, so the plan skips it fail-closed rather than
|
|
163
|
+
* pushing blindly. See {@link SKIP_REASON_NO_ACCOUNT_ROWS}.
|
|
164
|
+
*/
|
|
165
|
+
hasAccountRows: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* bundle -> keys the peer is KNOWN to hold: its own reply verdict says present
|
|
168
|
+
* AND the fingerprint we last delivered to it matches the current one. Absent ⇒
|
|
169
|
+
* none known.
|
|
170
|
+
*/
|
|
132
171
|
presentKeys: Record<string, ReadonlySet<string>>;
|
|
133
172
|
}
|
|
134
173
|
type ReservedSyncPlanItem = {
|
|
@@ -144,7 +183,12 @@ type ReservedSyncPlanItem = {
|
|
|
144
183
|
/**
|
|
145
184
|
* Pure plan: for each worker peer, push each bundle it is missing at least one
|
|
146
185
|
* key of. Deterministic (peers and bundles sorted) so it pins exactly in tests.
|
|
147
|
-
* A headed peer is skipped BEFORE any key comparison -- it never receives a key
|
|
186
|
+
* A headed peer is skipped BEFORE any key comparison -- it never receives a key;
|
|
187
|
+
* a peer with no reply yet (`hasReply: false`) is skipped next -- there is no
|
|
188
|
+
* first-hand knowledge of what it holds, so nothing is pushed to it this tick;
|
|
189
|
+
* a peer whose reply carries no `accounts.rows` field (`hasAccountRows: false`)
|
|
190
|
+
* is skipped FAIL-CLOSED for the same reason (an older CLI's reply is not proof
|
|
191
|
+
* of an empty inventory), rather than pushing every key blindly.
|
|
148
192
|
*/
|
|
149
193
|
export declare function planReservedStoreSync(accounts: ReservedSyncAccount[], peers: ReservedSyncPeer[]): ReservedSyncPlanItem[];
|
|
150
194
|
/**
|
|
@@ -156,14 +200,41 @@ export declare function planReservedStoreSync(accounts: ReservedSyncAccount[], p
|
|
|
156
200
|
* `workerCredential`) is not a sync target.
|
|
157
201
|
*/
|
|
158
202
|
export declare function reservedSyncTargets(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): ReservedSyncAccount[];
|
|
203
|
+
/** The subset of a peer's per-account verdict row this planner reads. */
|
|
204
|
+
export interface PeerAccountVerdict {
|
|
205
|
+
accountId: string;
|
|
206
|
+
harness: string;
|
|
207
|
+
/** `missing` ⇒ no working credential on the peer; anything else ⇒ present. */
|
|
208
|
+
verdict: string;
|
|
209
|
+
}
|
|
210
|
+
/** Narrow a peer's opaque `accounts.rows` to the verdict fields the planner reads. */
|
|
211
|
+
export declare function readPeerAccountVerdicts(state: FleetSharedDeviceState | undefined): PeerAccountVerdict[];
|
|
212
|
+
/**
|
|
213
|
+
* True when the peer's reply carried an `accounts.rows` array at all (present,
|
|
214
|
+
* even if empty). An older CLI, or a partial state written before the account-
|
|
215
|
+
* state daemon ran, has no such field — and its absence must NOT read as "the
|
|
216
|
+
* peer holds nothing", or the plan would push every key blindly. An EMPTY array
|
|
217
|
+
* (a peer with no registered account) IS a valid first-hand "holds nothing".
|
|
218
|
+
*/
|
|
219
|
+
export declare function peerHasAccountRows(state: FleetSharedDeviceState | undefined): boolean;
|
|
159
220
|
/**
|
|
160
|
-
* The keys a peer is KNOWN to hold, per bundle
|
|
161
|
-
* (
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
221
|
+
* The reserved keys a peer is KNOWN to hold, per bundle. Two conditions must BOTH
|
|
222
|
+
* hold (PHNX-4116):
|
|
223
|
+
*
|
|
224
|
+
* 1. FIRST-HAND — the peer's own daemon-state reply reports a NON-`missing`
|
|
225
|
+
* verdict for the account, so it has a working credential for it (its slot
|
|
226
|
+
* materialized). A `missing` verdict, or no row for the account at all, means
|
|
227
|
+
* it does not — self-correcting: a key removed on the worker flips its next
|
|
228
|
+
* verdict to `missing` and the push resumes, which a publisher-side memo alone
|
|
229
|
+
* could never see.
|
|
230
|
+
* 2. ROTATION — the fingerprint we last delivered to this peer (`delivered`)
|
|
231
|
+
* matches the account's CURRENT fingerprint. A re-mint bumps
|
|
232
|
+
* `workerCredential.mintedAt` while the old token still authenticates, so the
|
|
233
|
+
* verdict stays non-`missing`; without this the rotated key would never
|
|
234
|
+
* propagate. A never-delivered key (no memo entry ⇒ `undefined`) never
|
|
235
|
+
* matches, so a newly-added account still pushes.
|
|
165
236
|
*/
|
|
166
|
-
export declare function peerPresentKeys(
|
|
237
|
+
export declare function peerPresentKeys(accounts: ReservedSyncAccount[], verdicts: PeerAccountVerdict[], delivered: (bundle: string, key: string) => string | undefined): Record<string, ReadonlySet<string>>;
|
|
167
238
|
interface ReservedStoreSyncResult {
|
|
168
239
|
publisher: string | null;
|
|
169
240
|
/** Legacy raw reserved items adopted into their bundle on this box before planning (local repair). */
|
|
@@ -189,6 +260,7 @@ interface ReservedStoreSyncDeps {
|
|
|
189
260
|
listDevices?: () => DeviceProfile[];
|
|
190
261
|
localName?: string;
|
|
191
262
|
userAgentsDir?: string;
|
|
263
|
+
/** Where the publisher-side delivery memo lives; defaults to `getCacheDir()`. */
|
|
192
264
|
cacheDir?: string;
|
|
193
265
|
readMetaFn?: () => Pick<Meta, 'accounts' | 'deviceAccounts'>;
|
|
194
266
|
isPinned?: (name: string) => boolean;
|
|
@@ -221,6 +293,8 @@ interface ReservedStoreSyncDeps {
|
|
|
221
293
|
export declare function syncReservedStores(deps?: ReservedStoreSyncDeps): Promise<ReservedStoreSyncResult>;
|
|
222
294
|
interface ReconcileWorkerSlotsResult {
|
|
223
295
|
provisioned: string[];
|
|
296
|
+
/** Stale slots removed from the device doc (accountId no longer registered). */
|
|
297
|
+
dropped: string[];
|
|
224
298
|
skipped: Array<{
|
|
225
299
|
accountId: string;
|
|
226
300
|
reason: string;
|
|
@@ -237,6 +311,10 @@ interface ReconcileWorkerSlotsDeps {
|
|
|
237
311
|
provision?: (account: NativeAccountRecord) => void;
|
|
238
312
|
/** True when an existing durable slot already carries everything provisioning seeds. */
|
|
239
313
|
slotSeeded?: (harness: AgentId, slotDir: string) => boolean;
|
|
314
|
+
/** Remove stale slot records from the device doc. Default: {@link dropSlots}. */
|
|
315
|
+
dropSlots?: (accountIds: readonly string[]) => void;
|
|
316
|
+
/** Emit an operational log line. Default: the daemon log (fire-and-forget). */
|
|
317
|
+
log?: (level: 'INFO' | 'WARN', message: string) => void;
|
|
240
318
|
}
|
|
241
319
|
/**
|
|
242
320
|
* Worker-side: after a durable key lands, materialize a slot for each portable
|