@phnx-labs/agents-cli 1.22.42 → 1.22.44
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 +149 -0
- package/README.md +15 -13
- package/dist/bootstrap.js +33 -13
- package/dist/cli/command-registry.d.ts +0 -8
- package/dist/cli/command-registry.js +1 -25
- package/dist/commands/alias.d.ts +7 -4
- package/dist/commands/alias.js +33 -8
- package/dist/commands/apply.d.ts +7 -10
- package/dist/commands/apply.js +15 -35
- package/dist/commands/artifacts.js +4 -5
- package/dist/commands/audit.d.ts +6 -6
- package/dist/commands/audit.js +12 -12
- package/dist/commands/auth.js +14 -6
- package/dist/commands/beta.d.ts +7 -1
- package/dist/commands/beta.js +16 -9
- package/dist/commands/commands.js +1 -1
- package/dist/commands/computer.js +20 -6
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +4 -2
- package/dist/commands/doctor.js +21 -9
- package/dist/commands/events.d.ts +2 -2
- package/dist/commands/events.js +6 -5
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +11 -6
- package/dist/commands/factory.d.ts +1 -1
- package/dist/commands/feed.js +1 -30
- package/dist/commands/fleet-capture.d.ts +1 -1
- package/dist/commands/fleet-capture.js +4 -5
- package/dist/commands/harness.js +4 -52
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +3 -2
- package/dist/commands/menubar.js +58 -1
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/org.d.ts +5 -0
- package/dist/commands/org.js +83 -29
- package/dist/commands/output.js +2 -2
- package/dist/commands/profiles.js +2 -2
- package/dist/commands/prune.js +5 -4
- package/dist/commands/routines.js +43 -25
- package/dist/commands/routines.test-fixture.d.ts +86 -0
- package/dist/commands/routines.test-fixture.js +345 -0
- package/dist/commands/sessions-picker.js +124 -122
- package/dist/commands/sessions.test-fixture.d.ts +24 -0
- package/dist/commands/sessions.test-fixture.js +224 -0
- package/dist/commands/setup.js +9 -1
- package/dist/commands/share.d.ts +9 -10
- package/dist/commands/share.js +68 -50
- package/dist/commands/skills.js +1 -1
- package/dist/commands/snapshot.d.ts +1 -1
- package/dist/commands/snapshot.js +4 -4
- package/dist/commands/ssh.js +15 -62
- package/dist/commands/status.d.ts +4 -2
- package/dist/commands/status.js +10 -7
- package/dist/commands/subagents.js +1 -1
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +29 -2
- package/dist/commands/view.js +11 -2
- package/dist/commands/webhook.js +1 -1
- package/dist/lib/accounting/rotate.d.ts +5 -3
- package/dist/lib/accounting/rotate.js +36 -7
- package/dist/lib/accounting/usage.d.ts +18 -4
- package/dist/lib/accounting/usage.js +138 -27
- package/dist/lib/agent-spec/agents.d.ts +7 -7
- package/dist/lib/agent-spec/agents.js +15 -15
- package/dist/lib/analytics/mix-commands.d.ts +6 -28
- package/dist/lib/analytics/mix-commands.js +14 -40
- package/dist/lib/audit/log.d.ts +2 -2
- package/dist/lib/audit/log.js +2 -2
- package/dist/lib/beta.js +1 -1
- package/dist/lib/browser/domain-skills.d.ts +26 -6
- package/dist/lib/browser/domain-skills.js +81 -43
- package/dist/lib/cloud/rush.d.ts +0 -15
- package/dist/lib/cloud/rush.js +0 -35
- package/dist/lib/crabbox/runtimes.d.ts +6 -2
- package/dist/lib/crabbox/runtimes.js +21 -5
- package/dist/lib/daemon/daemon.js +97 -58
- package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
- package/dist/lib/daemon/daemon.test-fixture.js +71 -0
- package/dist/lib/daemon/runner.d.ts +18 -1
- package/dist/lib/daemon/runner.js +105 -11
- package/dist/lib/daemon-ticks.d.ts +38 -1
- package/dist/lib/daemon-ticks.js +52 -5
- package/dist/lib/device-config.js +1 -2
- package/dist/lib/devices/discovery-policy.d.ts +3 -3
- package/dist/lib/devices/discovery-policy.js +3 -3
- package/dist/lib/devices/doctor-findings.js +1 -1
- package/dist/lib/devices/fleet.d.ts +4 -8
- package/dist/lib/devices/fleet.js +3 -15
- package/dist/lib/devices/health-report.js +4 -4
- package/dist/lib/devices/pool.d.ts +0 -6
- package/dist/lib/devices/pool.js +0 -6
- package/dist/lib/devices/registry.d.ts +0 -15
- package/dist/lib/devices/registry.js +0 -9
- package/dist/lib/devices/stats-cache.d.ts +12 -0
- package/dist/lib/devices/stats-cache.js +24 -5
- package/dist/lib/drift-sync.d.ts +1 -1
- package/dist/lib/drift-sync.js +2 -2
- package/dist/lib/drift.d.ts +1 -1
- package/dist/lib/exec.js +13 -3
- package/dist/lib/fleet/capture.d.ts +1 -1
- package/dist/lib/fleet/manifest.js +1 -1
- package/dist/lib/fleet/remote-login.d.ts +2 -2
- package/dist/lib/fleet/remote-login.js +3 -4
- package/dist/lib/format.d.ts +9 -0
- package/dist/lib/format.js +16 -0
- package/dist/lib/github/pr-mergeable.d.ts +49 -0
- package/dist/lib/github/pr-mergeable.js +157 -0
- package/dist/lib/github/pr-verdict.d.ts +67 -0
- package/dist/lib/github/pr-verdict.js +72 -0
- package/dist/lib/harness/adapter.d.ts +6 -5
- package/dist/lib/hosts/dispatch.d.ts +2 -0
- package/dist/lib/hosts/dispatch.js +8 -1
- package/dist/lib/hosts/passthrough.d.ts +10 -0
- package/dist/lib/hosts/passthrough.js +28 -7
- package/dist/lib/hosts/providers/devices.js +1 -13
- package/dist/lib/hosts/ready.d.ts +12 -3
- package/dist/lib/hosts/ready.js +27 -12
- package/dist/lib/hosts/registry.d.ts +4 -5
- package/dist/lib/hosts/registry.js +3 -7
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/installations/shims.js +1 -1
- package/dist/lib/installations/versions.d.ts +40 -0
- package/dist/lib/installations/versions.js +78 -4
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
- package/dist/lib/menubar/install-menubar.d.ts +109 -0
- package/dist/lib/menubar/install-menubar.js +218 -12
- package/dist/lib/monitors/config.d.ts +2 -2
- package/dist/lib/monitors/config.js +3 -2
- package/dist/lib/origin-machine.d.ts +18 -0
- package/dist/lib/origin-machine.js +34 -0
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/perf/spool.js +1 -1
- package/dist/lib/prix-account.d.ts +2 -1
- package/dist/lib/prix-account.js +2 -1
- package/dist/lib/profiles.d.ts +7 -0
- package/dist/lib/profiles.js +26 -1
- package/dist/lib/remote-agents-json.js +1 -7
- package/dist/lib/routines-placement.d.ts +1 -1
- package/dist/lib/routines-placement.js +1 -1
- package/dist/lib/sandbox.d.ts +24 -0
- package/dist/lib/sandbox.js +101 -0
- package/dist/lib/scheduling/routines.d.ts +18 -9
- package/dist/lib/scheduling/routines.js +16 -8
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
- package/dist/lib/secrets/agent.js +9 -1
- package/dist/lib/secrets/index.js +1 -1
- package/dist/lib/secrets/list-filter.js +1 -1
- package/dist/lib/service-manifest.d.ts +20 -0
- package/dist/lib/service-manifest.js +34 -0
- package/dist/lib/session/active.d.ts +4 -16
- package/dist/lib/session/active.js +2 -6
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.d.ts +13 -98
- package/dist/lib/session/db.js +5 -12
- package/dist/lib/session/discover.d.ts +5 -81
- package/dist/lib/session/discover.js +5 -5
- package/dist/lib/session/fork.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -1
- package/dist/lib/session/origin-machine.d.ts +1 -18
- package/dist/lib/session/origin-machine.js +4 -34
- package/dist/lib/session/relative-time.d.ts +1 -44
- package/dist/lib/session/relative-time.js +4 -120
- package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote/remote-bundle.js +61 -0
- package/dist/lib/session/remote/remote-list.d.ts +148 -0
- package/dist/lib/session/remote/remote-list.js +607 -0
- package/dist/lib/session/remote/remote.d.ts +132 -0
- package/dist/lib/session/remote/remote.js +314 -0
- package/dist/lib/session/remote/watch.d.ts +101 -0
- package/dist/lib/session/remote/watch.js +241 -0
- package/dist/lib/session/remote-bundle.d.ts +1 -12
- package/dist/lib/session/remote-bundle.js +3 -61
- package/dist/lib/session/remote-list.d.ts +1 -148
- package/dist/lib/session/remote-list.js +3 -607
- package/dist/lib/session/remote.d.ts +1 -132
- package/dist/lib/session/remote.js +3 -314
- package/dist/lib/session/short-id.d.ts +1 -17
- package/dist/lib/session/short-id.js +4 -20
- package/dist/lib/session/watch.d.ts +1 -101
- package/dist/lib/session/watch.js +3 -242
- package/dist/lib/session/width.d.ts +1 -29
- package/dist/lib/session/width.js +4 -101
- package/dist/lib/share/config.d.ts +12 -2
- package/dist/lib/share/config.js +47 -11
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/publish.js +1 -1
- package/dist/lib/share/worker-template.js +2 -2
- package/dist/lib/smart-launch.d.ts +4 -4
- package/dist/lib/smart-launch.js +8 -18
- package/dist/lib/snapshot.d.ts +2 -2
- package/dist/lib/snapshot.js +1 -1
- package/dist/lib/staleness/index.d.ts +3 -1
- package/dist/lib/staleness/index.js +21 -1
- package/dist/lib/staleness/types.d.ts +11 -0
- package/dist/lib/staleness/writers/commands.js +13 -4
- package/dist/lib/staleness/writers/hooks.js +3 -1
- package/dist/lib/staleness/writers/mcp.d.ts +0 -7
- package/dist/lib/staleness/writers/mcp.js +16 -1
- package/dist/lib/staleness/writers/rules.js +1 -1
- package/dist/lib/staleness/writers/skills.js +3 -1
- package/dist/lib/staleness/writers/subagents.d.ts +0 -9
- package/dist/lib/staleness/writers/subagents.js +18 -1
- package/dist/lib/staleness/writers/types.d.ts +10 -0
- package/dist/lib/startup/command-registry.d.ts +10 -1
- package/dist/lib/startup/command-registry.js +26 -7
- package/dist/lib/startup/root-command.d.ts +18 -1
- package/dist/lib/startup/root-command.js +18 -1
- package/dist/lib/state.js +7 -0
- package/dist/lib/teams/placement-probe.js +28 -13
- package/dist/lib/teams/scheduler.d.ts +3 -1
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/text/relative-time.d.ts +44 -0
- package/dist/lib/text/relative-time.js +120 -0
- package/dist/lib/text/short-id.d.ts +17 -0
- package/dist/lib/text/short-id.js +20 -0
- package/dist/lib/text/width.d.ts +29 -0
- package/dist/lib/text/width.js +101 -0
- package/dist/lib/triggers/handlers.d.ts +7 -0
- package/dist/lib/triggers/handlers.js +8 -0
- package/dist/lib/triggers/webhook.d.ts +1 -0
- package/dist/lib/triggers/webhook.js +2 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/view-types.d.ts +7 -0
- package/package.json +2 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/serve.d.ts +0 -10
- package/dist/commands/serve.js +0 -68
- package/dist/commands/tickets.d.ts +0 -2
- package/dist/commands/tickets.js +0 -43
- package/dist/commands/trends.d.ts +0 -10
- package/dist/commands/trends.js +0 -12
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/observe-aliases.d.ts +0 -30
- package/dist/lib/observe-aliases.js +0 -38
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/serve/control.d.ts +0 -95
- package/dist/lib/serve/control.js +0 -260
- package/dist/lib/serve/data.d.ts +0 -81
- package/dist/lib/serve/data.js +0 -91
- package/dist/lib/serve/page.d.ts +0 -7
- package/dist/lib/serve/page.js +0 -140
- package/dist/lib/serve/server.d.ts +0 -80
- package/dist/lib/serve/server.js +0 -145
- package/dist/lib/serve/stream.d.ts +0 -43
- package/dist/lib/serve/stream.js +0 -116
- package/dist/lib/serve/token.d.ts +0 -35
- package/dist/lib/serve/token.js +0 -85
- package/dist/lib/tickets/list.d.ts +0 -53
- package/dist/lib/tickets/list.js +0 -153
|
@@ -11,7 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
13
13
|
import { getDaemonDir } from '../state.js';
|
|
14
|
-
import { isolatedHomeSuffix, namespacedServiceLabel, serviceManifestHomeEnv } from '../service-manifest.js';
|
|
14
|
+
import { isolatedHomeSuffix, namespacedServiceLabel, serviceManifestHomeEnv, serviceManagerRegistrationAllowed } from '../service-manifest.js';
|
|
15
15
|
import { isAlive, killTree, backgroundSpawnOptions, waitForExit } from '../platform/index.js';
|
|
16
16
|
import { listJobs as listAllJobs } from '../scheduling/routines.js';
|
|
17
17
|
import { syncAllProjectRoutines } from '../routines-project.js';
|
|
@@ -1763,6 +1763,9 @@ export { getAgentsBinPath };
|
|
|
1763
1763
|
* in fact up. Returns null when the service isn't running or the query fails.
|
|
1764
1764
|
*/
|
|
1765
1765
|
function readServiceManagerPid(platform = os.platform()) {
|
|
1766
|
+
const reg = serviceManagerRegistrationAllowed();
|
|
1767
|
+
if (!reg.allowed)
|
|
1768
|
+
return null;
|
|
1766
1769
|
try {
|
|
1767
1770
|
if (platform === 'linux') {
|
|
1768
1771
|
const out = execFileSync('systemctl', ['--user', 'show', '-p', 'MainPID', '--value', daemonSystemdUnitName()], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
@@ -1865,6 +1868,17 @@ export function ensureDaemonStarted() {
|
|
|
1865
1868
|
// callers that branch on this return (e.g. secrets/agent.ts).
|
|
1866
1869
|
if (isDaemonRunning())
|
|
1867
1870
|
return startDaemon();
|
|
1871
|
+
// RUSH-3021: never LAUNCH a daemon from a redirected (sandbox/test) HOME.
|
|
1872
|
+
// #2860 gated service-manager registration on this signal but left the
|
|
1873
|
+
// detached spawn itself ungated, so a test-spawned CLI could fork a daemon
|
|
1874
|
+
// into the test's temp HOME; the child outlives the test and races its
|
|
1875
|
+
// recursive teardown rm (ENOTEMPTY). Placed after the already-running branch
|
|
1876
|
+
// — reporting a live daemon stays allowed, same as the circuit breaker.
|
|
1877
|
+
// AGENTS_SERVICE_MANAGER_ALLOW_REDIRECTED_HOME=1 is the test seam for suites
|
|
1878
|
+
// that exercise daemon startup deliberately; `agents daemon start` remains
|
|
1879
|
+
// the operator override.
|
|
1880
|
+
if (!serviceManagerRegistrationAllowed().allowed)
|
|
1881
|
+
return null;
|
|
1868
1882
|
// RUSH-2418: the auto-start circuit breaker. A daemon that dies during
|
|
1869
1883
|
// startup would otherwise be relaunched by EVERY foreground command that
|
|
1870
1884
|
// wants one (secrets unlock, browser start, watchdog, ...) — an
|
|
@@ -1912,61 +1926,72 @@ function startDaemonLocked(agentsBin, releaseLock) {
|
|
|
1912
1926
|
releaseLock();
|
|
1913
1927
|
return startDetached({ agentsBin });
|
|
1914
1928
|
};
|
|
1929
|
+
const reg = serviceManagerRegistrationAllowed();
|
|
1915
1930
|
if (platform === 'darwin') {
|
|
1916
|
-
|
|
1917
|
-
const plistPath = getLaunchdPlistPath();
|
|
1918
|
-
const plistDir = path.dirname(plistPath);
|
|
1919
|
-
if (!fs.existsSync(plistDir)) {
|
|
1920
|
-
fs.mkdirSync(plistDir, { recursive: true });
|
|
1921
|
-
}
|
|
1922
|
-
// The plist carries no credential (RUSH-1759 — the daemon reads the OAuth
|
|
1923
|
-
// token itself at startup); still create owner-only atomically to match the
|
|
1924
|
-
// detached path and keep the log/PATH surface owner-private.
|
|
1925
|
-
writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist(agentsBin));
|
|
1931
|
+
if (reg.allowed) {
|
|
1926
1932
|
try {
|
|
1927
|
-
|
|
1933
|
+
const plistPath = getLaunchdPlistPath();
|
|
1934
|
+
const plistDir = path.dirname(plistPath);
|
|
1935
|
+
if (!fs.existsSync(plistDir)) {
|
|
1936
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
1937
|
+
}
|
|
1938
|
+
// The plist carries no credential (RUSH-1759 — the daemon reads the OAuth
|
|
1939
|
+
// token itself at startup); still create owner-only atomically to match the
|
|
1940
|
+
// detached path and keep the log/PATH surface owner-private.
|
|
1941
|
+
writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist(agentsBin));
|
|
1942
|
+
try {
|
|
1943
|
+
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
1944
|
+
}
|
|
1945
|
+
catch { /* not loaded, expected */ }
|
|
1946
|
+
// launchctl prints `Load failed:` and exits 0 when the label is in a
|
|
1947
|
+
// stuck state from a prior session — so a zero exit code isn't proof
|
|
1948
|
+
// of success. If no pid materializes within the window, give up on
|
|
1949
|
+
// launchd and fall through to a plain detached spawn.
|
|
1950
|
+
execFileSync('launchctl', ['load', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1951
|
+
// Launch issued — the child needs this lock to claim (RUSH-2417).
|
|
1952
|
+
releaseLock();
|
|
1953
|
+
const pid = waitForPid(3000) ?? readServiceManagerPid();
|
|
1954
|
+
if (pid)
|
|
1955
|
+
return { pid, method: 'launchd' };
|
|
1956
|
+
// launchctl claimed success but nothing ran. Fall through.
|
|
1957
|
+
}
|
|
1958
|
+
catch {
|
|
1959
|
+
// load threw — fall through to detached spawn
|
|
1928
1960
|
}
|
|
1929
|
-
catch { /* not loaded, expected */ }
|
|
1930
|
-
// launchctl prints `Load failed:` and exits 0 when the label is in a
|
|
1931
|
-
// stuck state from a prior session — so a zero exit code isn't proof
|
|
1932
|
-
// of success. If no pid materializes within the window, give up on
|
|
1933
|
-
// launchd and fall through to a plain detached spawn.
|
|
1934
|
-
execFileSync('launchctl', ['load', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1935
|
-
// Launch issued — the child needs this lock to claim (RUSH-2417).
|
|
1936
|
-
releaseLock();
|
|
1937
|
-
const pid = waitForPid(3000) ?? readServiceManagerPid();
|
|
1938
|
-
if (pid)
|
|
1939
|
-
return { pid, method: 'launchd' };
|
|
1940
|
-
// launchctl claimed success but nothing ran. Fall through.
|
|
1941
1961
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1962
|
+
else {
|
|
1963
|
+
process.stderr.write(`[agents] ${reg.reason}\n`);
|
|
1944
1964
|
}
|
|
1945
1965
|
return detachedFallback();
|
|
1946
1966
|
}
|
|
1947
1967
|
if (platform === 'linux') {
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
fs.
|
|
1968
|
+
if (reg.allowed) {
|
|
1969
|
+
try {
|
|
1970
|
+
const unitPath = getSystemdUnitPath();
|
|
1971
|
+
const unitDir = path.dirname(unitPath);
|
|
1972
|
+
if (!fs.existsSync(unitDir)) {
|
|
1973
|
+
fs.mkdirSync(unitDir, { recursive: true });
|
|
1974
|
+
}
|
|
1975
|
+
// Carries no credential (RUSH-1759 — the daemon reads the OAuth token
|
|
1976
|
+
// itself at startup); owner-only to keep the PATH/log surface private.
|
|
1977
|
+
writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit(agentsBin));
|
|
1978
|
+
execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
|
|
1979
|
+
execFileSync('systemctl', ['--user', 'enable', daemonSystemdUnitName()], { encoding: 'utf-8' });
|
|
1980
|
+
execFileSync('systemctl', ['--user', 'start', daemonSystemdUnitName()], { encoding: 'utf-8' });
|
|
1981
|
+
// Launch issued — the child needs this lock to claim (RUSH-2417).
|
|
1982
|
+
releaseLock();
|
|
1983
|
+
const pid = waitForPid(3000) ?? readServiceManagerPid();
|
|
1984
|
+
if (pid)
|
|
1985
|
+
return { pid, method: 'systemd' };
|
|
1986
|
+
// systemctl returned success but no PID surfaced — fall through to a
|
|
1987
|
+
// plain detached spawn rather than reporting a null PID.
|
|
1988
|
+
}
|
|
1989
|
+
catch {
|
|
1990
|
+
// start threw — fall through to detached spawn
|
|
1953
1991
|
}
|
|
1954
|
-
// Carries no credential (RUSH-1759 — the daemon reads the OAuth token
|
|
1955
|
-
// itself at startup); owner-only to keep the PATH/log surface private.
|
|
1956
|
-
writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit(agentsBin));
|
|
1957
|
-
execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
|
|
1958
|
-
execFileSync('systemctl', ['--user', 'enable', daemonSystemdUnitName()], { encoding: 'utf-8' });
|
|
1959
|
-
execFileSync('systemctl', ['--user', 'start', daemonSystemdUnitName()], { encoding: 'utf-8' });
|
|
1960
|
-
// Launch issued — the child needs this lock to claim (RUSH-2417).
|
|
1961
|
-
releaseLock();
|
|
1962
|
-
const pid = waitForPid(3000) ?? readServiceManagerPid();
|
|
1963
|
-
if (pid)
|
|
1964
|
-
return { pid, method: 'systemd' };
|
|
1965
|
-
// systemctl returned success but no PID surfaced — fall through to a
|
|
1966
|
-
// plain detached spawn rather than reporting a null PID.
|
|
1967
1992
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
1993
|
+
else {
|
|
1994
|
+
process.stderr.write(`[agents] ${reg.reason}\n`);
|
|
1970
1995
|
}
|
|
1971
1996
|
return detachedFallback();
|
|
1972
1997
|
}
|
|
@@ -2261,29 +2286,43 @@ export function stopDaemon() {
|
|
|
2261
2286
|
const released = [];
|
|
2262
2287
|
const surviving = [];
|
|
2263
2288
|
let escalated = false;
|
|
2289
|
+
const reg = serviceManagerRegistrationAllowed();
|
|
2264
2290
|
if (platform === 'darwin') {
|
|
2265
2291
|
const plistPath = getLaunchdPlistPath();
|
|
2266
2292
|
if (fs.existsSync(plistPath)) {
|
|
2293
|
+
if (reg.allowed) {
|
|
2294
|
+
try {
|
|
2295
|
+
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8' });
|
|
2296
|
+
}
|
|
2297
|
+
catch (err) {
|
|
2298
|
+
if (process.env.AGENTS_DEBUG) {
|
|
2299
|
+
console.error(`[debug] launchctl unload failed: ${err.message}`);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
else {
|
|
2304
|
+
process.stderr.write(`[agents] ${reg.reason}\n`);
|
|
2305
|
+
}
|
|
2267
2306
|
try {
|
|
2268
|
-
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8' });
|
|
2269
2307
|
fs.unlinkSync(plistPath);
|
|
2270
2308
|
}
|
|
2309
|
+
catch { /* plist already removed */ }
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
if (platform === 'linux') {
|
|
2313
|
+
if (reg.allowed) {
|
|
2314
|
+
try {
|
|
2315
|
+
execFileSync('systemctl', ['--user', 'stop', daemonSystemdUnitName()], { encoding: 'utf-8' });
|
|
2316
|
+
execFileSync('systemctl', ['--user', 'disable', daemonSystemdUnitName()], { encoding: 'utf-8' });
|
|
2317
|
+
}
|
|
2271
2318
|
catch (err) {
|
|
2272
2319
|
if (process.env.AGENTS_DEBUG) {
|
|
2273
|
-
console.error(`[debug]
|
|
2320
|
+
console.error(`[debug] systemctl stop failed: ${err.message}`);
|
|
2274
2321
|
}
|
|
2275
2322
|
}
|
|
2276
2323
|
}
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
try {
|
|
2280
|
-
execFileSync('systemctl', ['--user', 'stop', daemonSystemdUnitName()], { encoding: 'utf-8' });
|
|
2281
|
-
execFileSync('systemctl', ['--user', 'disable', daemonSystemdUnitName()], { encoding: 'utf-8' });
|
|
2282
|
-
}
|
|
2283
|
-
catch (err) {
|
|
2284
|
-
if (process.env.AGENTS_DEBUG) {
|
|
2285
|
-
console.error(`[debug] systemctl stop failed: ${err.message}`);
|
|
2286
|
-
}
|
|
2324
|
+
else {
|
|
2325
|
+
process.stderr.write(`[agents] ${reg.reason}\n`);
|
|
2287
2326
|
}
|
|
2288
2327
|
const unitPath = getSystemdUnitPath();
|
|
2289
2328
|
if (fs.existsSync(unitPath)) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type KeychainBackend } from '../secrets/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared fixture for the daemon.*.test.ts suite slices (RUSH-2819).
|
|
4
|
+
*
|
|
5
|
+
* daemon.test.ts was a single 2201-line file (88 tests, ~112s in CI) — the
|
|
6
|
+
* slowest file in the daemon test-ownership group, serializing an entire
|
|
7
|
+
* vitest fork while every other selected file finished. The suite is split
|
|
8
|
+
* into topical slices so per-file fork parallelism can spread the
|
|
9
|
+
* process-spawning / real-daemon integration tests across workers; the
|
|
10
|
+
* helpers shared by more than one slice live here.
|
|
11
|
+
*/
|
|
12
|
+
/** An in-memory KeychainBackend stand-in, so a test never touches the real OS keychain. */
|
|
13
|
+
export declare function makeMemoryBackend(): {
|
|
14
|
+
backend: KeychainBackend;
|
|
15
|
+
store: Map<string, string>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Registers the keychain-backend swap + AGENTS_SECRETS_NO_AGENT hermeticity
|
|
19
|
+
* beforeEach/afterEach every slice of the original daemon.test.ts ran under,
|
|
20
|
+
* verbatim. Call once at the top level of each slice file.
|
|
21
|
+
*/
|
|
22
|
+
export declare function installKeychainHermeticity(): void;
|
|
23
|
+
export declare const REPO_ROOT: string;
|
|
24
|
+
export declare const DIST_ENTRY: string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { setKeychainBackendForTest } from '../secrets/index.js';
|
|
5
|
+
import { deleteBundle } from '../secrets/bundles.js';
|
|
6
|
+
/**
|
|
7
|
+
* Shared fixture for the daemon.*.test.ts suite slices (RUSH-2819).
|
|
8
|
+
*
|
|
9
|
+
* daemon.test.ts was a single 2201-line file (88 tests, ~112s in CI) — the
|
|
10
|
+
* slowest file in the daemon test-ownership group, serializing an entire
|
|
11
|
+
* vitest fork while every other selected file finished. The suite is split
|
|
12
|
+
* into topical slices so per-file fork parallelism can spread the
|
|
13
|
+
* process-spawning / real-daemon integration tests across workers; the
|
|
14
|
+
* helpers shared by more than one slice live here.
|
|
15
|
+
*/
|
|
16
|
+
/** An in-memory KeychainBackend stand-in, so a test never touches the real OS keychain. */
|
|
17
|
+
export function makeMemoryBackend() {
|
|
18
|
+
const store = new Map();
|
|
19
|
+
const backend = {
|
|
20
|
+
has: (item) => store.has(item),
|
|
21
|
+
get: (item) => {
|
|
22
|
+
const v = store.get(item);
|
|
23
|
+
if (v === undefined)
|
|
24
|
+
throw new Error(`Keychain item '${item}' not found.`);
|
|
25
|
+
return v;
|
|
26
|
+
},
|
|
27
|
+
set: (item, value) => { store.set(item, value); },
|
|
28
|
+
delete: (item) => store.delete(item),
|
|
29
|
+
list: (prefix) => Array.from(store.keys()).filter((k) => k.startsWith(prefix)),
|
|
30
|
+
};
|
|
31
|
+
return { backend, store };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Registers the keychain-backend swap + AGENTS_SECRETS_NO_AGENT hermeticity
|
|
35
|
+
* beforeEach/afterEach every slice of the original daemon.test.ts ran under,
|
|
36
|
+
* verbatim. Call once at the top level of each slice file.
|
|
37
|
+
*/
|
|
38
|
+
export function installKeychainHermeticity() {
|
|
39
|
+
let restore = null;
|
|
40
|
+
let prevNoAgent;
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
const m = makeMemoryBackend();
|
|
43
|
+
restore = setKeychainBackendForTest(m.backend);
|
|
44
|
+
// Hermeticity: readAndResolveBundleEnv consults the running secrets-agent
|
|
45
|
+
// (bundles.ts agentGetSync fast-path) BEFORE the injected keychain backend.
|
|
46
|
+
// On a dev machine where the agent is live and the real `claude` bundle is
|
|
47
|
+
// unlocked, that returns the machine's real CLAUDE_CODE_OAUTH_TOKEN and this
|
|
48
|
+
// test reads a live credential instead of the seeded value (CI has no agent,
|
|
49
|
+
// so it only bites locally). Disable the agent so the read falls through to
|
|
50
|
+
// the in-memory backend above — hermetic regardless of host state.
|
|
51
|
+
prevNoAgent = process.env.AGENTS_SECRETS_NO_AGENT;
|
|
52
|
+
process.env.AGENTS_SECRETS_NO_AGENT = '1';
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
try {
|
|
56
|
+
deleteBundle('claude');
|
|
57
|
+
}
|
|
58
|
+
catch { /* not created */ }
|
|
59
|
+
setKeychainBackendForTest(restore);
|
|
60
|
+
if (prevNoAgent === undefined)
|
|
61
|
+
delete process.env.AGENTS_SECRETS_NO_AGENT;
|
|
62
|
+
else
|
|
63
|
+
process.env.AGENTS_SECRETS_NO_AGENT = prevNoAgent;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// The real compiled CLI entry, shared by every slice that drives an actual
|
|
67
|
+
// `__daemon-run` subprocess (lifecycle/registry/stop). Computed relative to
|
|
68
|
+
// this file's own location, which stays in src/lib/daemon/ alongside every
|
|
69
|
+
// slice, so the path math is unaffected by the split.
|
|
70
|
+
export const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
71
|
+
export const DIST_ENTRY = path.join(REPO_ROOT, 'dist', 'index.js');
|
|
@@ -74,6 +74,23 @@ interface RoutineAttempt {
|
|
|
74
74
|
* but the gate should have paused such a routine before it ever spawned.
|
|
75
75
|
*/
|
|
76
76
|
export declare function routineSpawnCwd(config: Pick<JobConfig, 'name' | 'project' | 'cwd' | 'agent' | 'workflow' | 'command'>): string;
|
|
77
|
+
/**
|
|
78
|
+
* Bake the daemon-job argv skeleton from AGENT_COMMANDS.
|
|
79
|
+
*
|
|
80
|
+
* The two tables used to be independent copies of the same launch decision.
|
|
81
|
+
* Argv now comes from AGENT_COMMANDS.base / promptFlag / jsonFlags / modeFlags.
|
|
82
|
+
* Two documented exceptions, not a second table:
|
|
83
|
+
* - kimi uses `--prompt` (long form). `-p` is the exec alias; combining
|
|
84
|
+
* either with --plan/--auto/--yolo aborts, so routineModeArgs emits no
|
|
85
|
+
* mode flag. The daemon has always spawned the long form.
|
|
86
|
+
* - claude places `--verbose` (from jsonFlags) before the prompt and bakes
|
|
87
|
+
* modeFlags.plan so claudeAdapter.routineModeArgs can splice
|
|
88
|
+
* plan → acceptEdits / auto / skip.
|
|
89
|
+
*
|
|
90
|
+
* Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (gemini, grok,
|
|
91
|
+
* and every other harness the daemon does not fire locally).
|
|
92
|
+
*/
|
|
93
|
+
export declare function bakeRoutineArgv(agent: string): string[] | undefined;
|
|
77
94
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
78
95
|
export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string, forwardAccount?: boolean): string[];
|
|
79
96
|
/**
|
|
@@ -161,7 +178,7 @@ export declare function buildHostDispatchOptions(config: JobConfig, ctx: {
|
|
|
161
178
|
runDir: string;
|
|
162
179
|
detached: boolean;
|
|
163
180
|
}): import('../hosts/run-target.js').HostPromptRun;
|
|
164
|
-
export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow' | 'resume'>): boolean;
|
|
181
|
+
export declare function dispatchesViaAgentsRun(config: Pick<JobConfig, 'workflow' | 'resume' | 'agent'>): boolean;
|
|
165
182
|
/**
|
|
166
183
|
* Merge sandbox/base env with the canonical per-version exec env
|
|
167
184
|
* (CLAUDE_CONFIG_DIR / CODEX_HOME / …) so routines share account isolation
|
|
@@ -21,7 +21,7 @@ import { getCliLaunch, getAgentsBinDir } from '../cli-entry.js';
|
|
|
21
21
|
import { resolveJobPrompt, parseTimeout, writeRunMeta, listRuns, getJobRunsDir, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, resolveJobExecutionContext, slotRunId, claimRunSlot, readRunMeta, resolveHostStrategy, } from '../scheduling/routines.js';
|
|
22
22
|
import { getRunsDir, getUserAgentsDir, readMeta, getDaemonDir } from '../state.js';
|
|
23
23
|
import { shortCodexHome } from '../codex-home.js';
|
|
24
|
-
import { prepareJobHome, buildSpawnEnv, getJobHomePath } from '../sandbox.js';
|
|
24
|
+
import { prepareJobHome, buildSpawnEnv, getJobHomePath, assertSandboxForwardsHostGhAuth } from '../sandbox.js';
|
|
25
25
|
import { resolveModel, buildReasoningFlags } from '../models.js';
|
|
26
26
|
import { createTimer, redactPrompt, emitRoutineEnd } from '../feed/events.js';
|
|
27
27
|
import { resolveHarnessAdapter } from '../harness/index.js';
|
|
@@ -39,7 +39,8 @@ import { resolveClaudeSetupToken } from '../claude-account-token.js';
|
|
|
39
39
|
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from '../accounting/rotate.js';
|
|
40
40
|
import { readAuthHealth, isDeadVerdict } from '../auth-health.js';
|
|
41
41
|
import { machineId } from '../machine-id.js';
|
|
42
|
-
import { isSelfUpdatingAgent,
|
|
42
|
+
import { isSelfUpdatingAgent, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
|
|
43
|
+
import { isCustomHarnessName, readProfile } from '../profiles.js';
|
|
43
44
|
export class RoutineAlreadyRunningError extends Error {
|
|
44
45
|
constructor(jobName, runId) {
|
|
45
46
|
super(`Routine '${jobName}' already has a running execution (${runId})`);
|
|
@@ -308,7 +309,7 @@ function allocateRoutineAttempt(config, trigger) {
|
|
|
308
309
|
// harness leaves the command table (gemini did), and the legacy routine must
|
|
309
310
|
// still land a visible 'blocked' record, never a generic buildJobCommand
|
|
310
311
|
// failure (RUSH-2202).
|
|
311
|
-
if (!config.workflow && config.agent && isAgentHardDeprecated(config.agent)) {
|
|
312
|
+
if (!config.workflow && config.agent && !isCustomHarnessName(config.agent) && isAgentHardDeprecated(config.agent)) {
|
|
312
313
|
const reason = hardDeprecationError(config.agent);
|
|
313
314
|
return {
|
|
314
315
|
proceed: false,
|
|
@@ -488,6 +489,48 @@ export function routineSpawnCwd(config) {
|
|
|
488
489
|
const ctx = resolveJobExecutionContext(config, { mode: 'local' });
|
|
489
490
|
return ctx.absoluteCwd ?? os.homedir();
|
|
490
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Bake the daemon-job argv skeleton from AGENT_COMMANDS.
|
|
494
|
+
*
|
|
495
|
+
* The two tables used to be independent copies of the same launch decision.
|
|
496
|
+
* Argv now comes from AGENT_COMMANDS.base / promptFlag / jsonFlags / modeFlags.
|
|
497
|
+
* Two documented exceptions, not a second table:
|
|
498
|
+
* - kimi uses `--prompt` (long form). `-p` is the exec alias; combining
|
|
499
|
+
* either with --plan/--auto/--yolo aborts, so routineModeArgs emits no
|
|
500
|
+
* mode flag. The daemon has always spawned the long form.
|
|
501
|
+
* - claude places `--verbose` (from jsonFlags) before the prompt and bakes
|
|
502
|
+
* modeFlags.plan so claudeAdapter.routineModeArgs can splice
|
|
503
|
+
* plan → acceptEdits / auto / skip.
|
|
504
|
+
*
|
|
505
|
+
* Returns undefined when `agent` is outside ROUTINE_AGENT_IDS (gemini, grok,
|
|
506
|
+
* and every other harness the daemon does not fire locally).
|
|
507
|
+
*/
|
|
508
|
+
export function bakeRoutineArgv(agent) {
|
|
509
|
+
if (!ROUTINE_AGENT_IDS.includes(agent))
|
|
510
|
+
return undefined;
|
|
511
|
+
const template = AGENT_COMMANDS[agent];
|
|
512
|
+
if (!template)
|
|
513
|
+
return undefined;
|
|
514
|
+
const json = template.jsonFlags ?? [];
|
|
515
|
+
const cmd = [...template.base];
|
|
516
|
+
if (agent === 'kimi') {
|
|
517
|
+
cmd.push('--prompt', '{prompt}', ...json);
|
|
518
|
+
return cmd;
|
|
519
|
+
}
|
|
520
|
+
if (agent === 'claude') {
|
|
521
|
+
const verbose = json.includes('--verbose') ? ['--verbose'] : [];
|
|
522
|
+
const jsonRest = json.filter((flag) => flag !== '--verbose');
|
|
523
|
+
cmd.push(template.promptFlag, ...verbose, '{prompt}', ...jsonRest, ...(template.modeFlags.plan ?? []));
|
|
524
|
+
return cmd;
|
|
525
|
+
}
|
|
526
|
+
if (template.promptFlag === 'positional') {
|
|
527
|
+
cmd.push('{prompt}', ...json);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
cmd.push(template.promptFlag, '{prompt}', ...json);
|
|
531
|
+
}
|
|
532
|
+
return cmd;
|
|
533
|
+
}
|
|
491
534
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
492
535
|
export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
|
|
493
536
|
// Workflow branch: delegate to `agents run <workflow>` which handles subagent
|
|
@@ -514,9 +557,25 @@ export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
|
|
|
514
557
|
cmd.push('--account', config.account);
|
|
515
558
|
return cmd;
|
|
516
559
|
}
|
|
517
|
-
|
|
560
|
+
// Custom-harness branch: delegate to `agents run <name>`, which owns profile
|
|
561
|
+
// resolution (host binary, model env, provider auth) — the same delegation
|
|
562
|
+
// the workflow and resume branches use. The profile pins its own version and
|
|
563
|
+
// auth, so no command template, binary pinning, or account-env injection
|
|
564
|
+
// applies here.
|
|
565
|
+
if (isCustomHarnessName(agent)) {
|
|
566
|
+
const cmd = ['agents', 'run', agent, resolvedPrompt, '--mode', config.mode];
|
|
567
|
+
if (config.account && forwardAccount)
|
|
568
|
+
cmd.push('--account', config.account);
|
|
569
|
+
return cmd;
|
|
570
|
+
}
|
|
571
|
+
const template = bakeRoutineArgv(agent);
|
|
518
572
|
if (!template) {
|
|
519
|
-
|
|
573
|
+
// A name outside both tables was either never valid or was a custom
|
|
574
|
+
// harness whose profile has since been deleted — validateJob accepted it
|
|
575
|
+
// against a profile that existed then. Name the repair, not just the miss.
|
|
576
|
+
throw new Error(`Unsupported agent for daemon jobs: ${agent}. ` +
|
|
577
|
+
`If '${agent}' was a custom harness, its profile no longer exists on this device — ` +
|
|
578
|
+
`recreate it (agents harness add ${agent} ...) or point the routine at another agent.`);
|
|
520
579
|
}
|
|
521
580
|
let cmd = template.map((part) => part.replace('{prompt}', resolvedPrompt));
|
|
522
581
|
// Canonicalize mode (accepts legacy `full` as alias for `skip`).
|
|
@@ -554,7 +613,8 @@ export function buildJobCommand(config, resolvedPrompt, forwardAccount = true) {
|
|
|
554
613
|
* Reasoning level (config.config.reasoning) maps to per-agent flags via models.ts.
|
|
555
614
|
*/
|
|
556
615
|
function appendModelAndReasoning(cmd, config) {
|
|
557
|
-
// Only called from buildJobCommand's agent path
|
|
616
|
+
// Only called from buildJobCommand's agent path AFTER the custom-harness
|
|
617
|
+
// branch returned — config.agent is a native id here.
|
|
558
618
|
const agent = config.agent;
|
|
559
619
|
const model = config.config?.model;
|
|
560
620
|
if (model) {
|
|
@@ -636,6 +696,8 @@ function transcriptBasePath(runDir) {
|
|
|
636
696
|
export function snapshotRoutineTranscriptBase(meta, runDir, overlayHome) {
|
|
637
697
|
if (!meta.agent)
|
|
638
698
|
return;
|
|
699
|
+
// A host/cloud custom-harness run records the harness name, which has no
|
|
700
|
+
// transcript spec — the null-specs guard below skips it, as before.
|
|
639
701
|
const specs = ROUTINE_TRANSCRIPT_SPECS[meta.agent];
|
|
640
702
|
if (!specs)
|
|
641
703
|
return;
|
|
@@ -674,6 +736,8 @@ function readTranscriptBase(runDir) {
|
|
|
674
736
|
export function archiveRoutineTranscripts(meta, runDir, overlayHome) {
|
|
675
737
|
if (!meta.agent)
|
|
676
738
|
return;
|
|
739
|
+
// A host/cloud custom-harness run records the harness name, which has no
|
|
740
|
+
// transcript spec — the null-specs guard below skips it, as before.
|
|
677
741
|
const specs = ROUTINE_TRANSCRIPT_SPECS[meta.agent];
|
|
678
742
|
if (!specs)
|
|
679
743
|
return;
|
|
@@ -808,6 +872,13 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
|
|
|
808
872
|
if (config.workflow) {
|
|
809
873
|
return { chain: [], rotation: null, pinned: false };
|
|
810
874
|
}
|
|
875
|
+
if (config.agent && isCustomHarnessName(config.agent)) {
|
|
876
|
+
// A custom harness pins its own version/auth in the profile, so there is
|
|
877
|
+
// no version/account chain to resolve here — `agents run <name>` owns it
|
|
878
|
+
// (matching exec's "strategy ignored: custom harness pins its own
|
|
879
|
+
// version/auth").
|
|
880
|
+
return { chain: [], rotation: null, pinned: false };
|
|
881
|
+
}
|
|
811
882
|
// resolveRoutineLaunch is only called for agent jobs (workflow returns above;
|
|
812
883
|
// command jobs branch out of execute*Job before reaching this).
|
|
813
884
|
const agent = config.agent;
|
|
@@ -1012,7 +1083,7 @@ export function buildHostDispatchOptions(config, ctx) {
|
|
|
1012
1083
|
};
|
|
1013
1084
|
}
|
|
1014
1085
|
export function dispatchesViaAgentsRun(config) {
|
|
1015
|
-
return Boolean(config.workflow || config.resume);
|
|
1086
|
+
return Boolean(config.workflow || config.resume || (config.agent && isCustomHarnessName(config.agent)));
|
|
1016
1087
|
}
|
|
1017
1088
|
/**
|
|
1018
1089
|
* Merge sandbox/base env with the canonical per-version exec env
|
|
@@ -1214,7 +1285,11 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1214
1285
|
// the agent's config dir, and the sandbox overlay home has only a freshly-generated
|
|
1215
1286
|
// config with no session store. So a resume job is never sandboxed, regardless of
|
|
1216
1287
|
// `config.sandbox` (see the resume branch in buildJobCommand).
|
|
1217
|
-
|
|
1288
|
+
// Resume needs the REAL home (session store); a custom harness needs it too —
|
|
1289
|
+
// the delegated `agents run <name>` resolves ~/.agents (profiles, setup
|
|
1290
|
+
// sentinel, version homes) from HOME, which the overlay would hide. Exec
|
|
1291
|
+
// still isolates the run in the host version home it swaps HOME into.
|
|
1292
|
+
const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
|
|
1218
1293
|
const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
|
|
1219
1294
|
const runId = attempt.runId;
|
|
1220
1295
|
const runDir = getRunDir(config.name, runId);
|
|
@@ -1225,7 +1300,11 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1225
1300
|
// Workflows run via `agents run <workflow>` which delegates to claude under the hood.
|
|
1226
1301
|
// Use 'claude' as the effective agent for report extraction and metadata when workflow is set.
|
|
1227
1302
|
// (command jobs branched out earlier, so config.agent is set on the non-workflow path.)
|
|
1228
|
-
const effectiveAgent = config.workflow
|
|
1303
|
+
const effectiveAgent = config.workflow
|
|
1304
|
+
? 'claude'
|
|
1305
|
+
: isCustomHarnessName(config.agent)
|
|
1306
|
+
? readProfile(config.agent).host.agent
|
|
1307
|
+
: config.agent;
|
|
1229
1308
|
if (!dispatchesViaAgentsRun(config)) {
|
|
1230
1309
|
const { findUnifiedAccount, resolveAccountSelection, resolveCredentialAccount } = await import('../account-registry.js');
|
|
1231
1310
|
const meta = readMeta();
|
|
@@ -1238,6 +1317,9 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1238
1317
|
Object.assign(baseEnv, resolveCredentialAccount(selectedAccount, effectiveAgent).env);
|
|
1239
1318
|
}
|
|
1240
1319
|
}
|
|
1320
|
+
// RUSH-2860: if this host holds gh auth, the sandbox child MUST see it.
|
|
1321
|
+
if (useSandbox)
|
|
1322
|
+
assertSandboxForwardsHostGhAuth(baseEnv);
|
|
1241
1323
|
const meta = {
|
|
1242
1324
|
jobName: config.name,
|
|
1243
1325
|
runId,
|
|
@@ -1728,7 +1810,11 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
|
|
|
1728
1810
|
// the agent's config dir, and the sandbox overlay home has only a freshly-generated
|
|
1729
1811
|
// config with no session store. So a resume job is never sandboxed, regardless of
|
|
1730
1812
|
// `config.sandbox` (see the resume branch in buildJobCommand).
|
|
1731
|
-
|
|
1813
|
+
// Resume needs the REAL home (session store); a custom harness needs it too —
|
|
1814
|
+
// the delegated `agents run <name>` resolves ~/.agents (profiles, setup
|
|
1815
|
+
// sentinel, version homes) from HOME, which the overlay would hide. Exec
|
|
1816
|
+
// still isolates the run in the host version home it swaps HOME into.
|
|
1817
|
+
const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
|
|
1732
1818
|
const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
|
|
1733
1819
|
const runId = attempt.runId;
|
|
1734
1820
|
const runDir = getRunDir(config.name, runId);
|
|
@@ -1755,7 +1841,15 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
|
|
|
1755
1841
|
})()
|
|
1756
1842
|
// Non-command path only: config.agent is always set here (command/workflow branch earlier).
|
|
1757
1843
|
: buildRoutineSpawnEnv(baseEnv, config.agent, version, config.timezone, overlayHome);
|
|
1758
|
-
|
|
1844
|
+
// RUSH-2860: if this host holds gh auth, the sandbox child MUST see it —
|
|
1845
|
+
// otherwise monitors runs records ok while every gh call inside fails.
|
|
1846
|
+
if (useSandbox)
|
|
1847
|
+
assertSandboxForwardsHostGhAuth(spawnEnv);
|
|
1848
|
+
const effectiveAgent = config.workflow
|
|
1849
|
+
? 'claude'
|
|
1850
|
+
: isCustomHarnessName(config.agent)
|
|
1851
|
+
? readProfile(config.agent).host.agent
|
|
1852
|
+
: config.agent;
|
|
1759
1853
|
const meta = {
|
|
1760
1854
|
jobName: config.name,
|
|
1761
1855
|
runId,
|
|
@@ -18,11 +18,48 @@ export declare function isFreshFleetAuthSnapshot(value: {
|
|
|
18
18
|
row: FleetStatusRow;
|
|
19
19
|
authRows: AuthProbeRow[];
|
|
20
20
|
}, minimumCapturedAt: number): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* How stale a cached auth verdict may get before the periodic tick re-probes.
|
|
23
|
+
*
|
|
24
|
+
* The auth verdict rides the same rate-limited `/api/oauth/usage` endpoint as
|
|
25
|
+
* the usage probe. Firing it every 3-minute tick on every fleet device drove one
|
|
26
|
+
* per-account request quota to a permanent 429, and the shared backoff then
|
|
27
|
+
* parked usage fleet-wide and froze the usage cache (RUSH-2998). Re-probing at
|
|
28
|
+
* most every 20 minutes cuts that endpoint traffic ~5x while still catching a
|
|
29
|
+
* revocation within one window — and every device keeps a REAL verdict (not a
|
|
30
|
+
* degraded "unverified"), so `agents devices ping --strict` and the run
|
|
31
|
+
* auth-preflight keep working on every host, unlike a publisher/subscriber split
|
|
32
|
+
* that would blind every non-primary box to revocation. Fleet status still
|
|
33
|
+
* publishes every tick — it does not ride that endpoint.
|
|
34
|
+
*/
|
|
35
|
+
export declare const AUTH_PROBE_MAX_AGE_MS: number;
|
|
36
|
+
/**
|
|
37
|
+
* True when every cached auth row was probed within {@link AUTH_PROBE_MAX_AGE_MS}
|
|
38
|
+
* — i.e. reusing them would not let a verdict get staler than one probe window.
|
|
39
|
+
* Empty cache is never fresh (nothing to reuse). Pure — unit-tested.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isCachedFleetAuthProbeFresh(authRows: AuthProbeRow[], now: number, maxAgeMs?: number): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the tick may reuse the cached auth verdict instead of re-probing the
|
|
44
|
+
* rate-limited endpoint. `force` (an on-demand `agents devices ping`) ALWAYS
|
|
45
|
+
* re-probes — the whole point of the command is a genuinely live verdict, and
|
|
46
|
+
* missing this `!force` is exactly how a `--strict` check silently passed a
|
|
47
|
+
* revoked account (the second `runFleetPing` call site, RUSH-2998). Pure —
|
|
48
|
+
* unit-tested so that inversion cannot regress unnoticed.
|
|
49
|
+
*/
|
|
50
|
+
export declare function shouldReuseCachedAuthProbe(force: boolean, cached: AuthProbeRow[], now: number, maxAgeMs?: number): boolean;
|
|
21
51
|
/**
|
|
22
52
|
* Fleet cache warm: publish THIS host's row for the caches `agents fleet
|
|
23
53
|
* status` / `agents devices list` read (PUBLISH-OWN / READ-UNION, RUSH-2061).
|
|
54
|
+
*
|
|
55
|
+
* `force` is set by on-demand callers (`agents devices ping`) that must return a
|
|
56
|
+
* genuinely live verdict; the periodic daemon tick leaves it unset so it reuses a
|
|
57
|
+
* recent verdict per {@link AUTH_PROBE_MAX_AGE_MS} instead of hammering the
|
|
58
|
+
* rate-limited endpoint every 3 minutes (RUSH-2998).
|
|
24
59
|
*/
|
|
25
|
-
export declare function refreshLocalFleetAuthState(
|
|
60
|
+
export declare function refreshLocalFleetAuthState(opts?: {
|
|
61
|
+
force?: boolean;
|
|
62
|
+
}): Promise<{
|
|
26
63
|
row: FleetStatusRow;
|
|
27
64
|
authRows: import('./auth-health.js').AuthProbeRow[];
|
|
28
65
|
}>;
|