@phnx-labs/agents-cli 1.22.56 → 1.22.58
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 +70 -0
- package/README.md +4 -4
- package/dist/bootstrap.js +11 -2
- package/dist/cli/command-registry.d.ts +0 -1
- package/dist/cli/command-registry.js +0 -3
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/exec.js +1 -1
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/hooks.js +4 -4
- package/dist/commands/insights.d.ts +7 -5
- package/dist/commands/insights.js +16 -9
- package/dist/commands/monitors.js +11 -0
- package/dist/commands/perf.d.ts +16 -7
- package/dist/commands/perf.js +29 -20
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/rules.js +1 -1
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +24 -14
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/trash.d.ts +2 -2
- package/dist/commands/trash.js +2 -6
- package/dist/commands/versions.d.ts +2 -2
- package/dist/commands/versions.js +1 -10
- package/dist/commands/view.d.ts +2 -2
- package/dist/commands/view.js +7 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +14 -0
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-ingest.d.ts +1 -0
- package/dist/lib/accounting/usage-ingest.js +75 -0
- package/dist/lib/accounting/usage-sync.d.ts +97 -0
- package/dist/lib/accounting/usage-sync.js +203 -0
- package/dist/lib/accounting/usage.d.ts +48 -2
- package/dist/lib/accounting/usage.js +79 -2
- package/dist/lib/agent-spec/agents.js +1 -1
- package/dist/lib/analytics/mix-commands.d.ts +8 -7
- package/dist/lib/analytics/mix-commands.js +50 -73
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/daemon/daemon.js +5 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +95 -53
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -0
- package/dist/lib/daemon/usage-sync-service.js +42 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +17 -6
- package/dist/lib/device-config.js +25 -11
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/pool.d.ts +4 -3
- package/dist/lib/devices/pool.js +13 -5
- package/dist/lib/doctor-diff.js +77 -7
- package/dist/lib/exec.d.ts +6 -41
- package/dist/lib/exec.js +6 -41
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +13 -1
- package/dist/lib/git.js +36 -7
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapters/claude.js +3 -2
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/hosts/remote-cmd.d.ts +9 -0
- package/dist/lib/hosts/remote-cmd.js +22 -0
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +26 -133
- package/dist/lib/installations/versions.js +41 -204
- package/dist/lib/perf/db.d.ts +1 -1
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +23 -0
- package/dist/lib/self-update.js +50 -0
- package/dist/lib/session/active.d.ts +16 -32
- package/dist/lib/session/active.js +10 -68
- package/dist/lib/session/db.d.ts +24 -36
- package/dist/lib/session/db.js +143 -44
- package/dist/lib/session/discover.d.ts +6 -58
- package/dist/lib/session/discover.js +5 -43
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/parse.d.ts +1 -19
- package/dist/lib/session/parse.js +2 -15
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +8 -2
- package/dist/lib/startup/command-registry.js +12 -4
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +15 -0
- package/dist/lib/traces/sync.js +104 -19
- package/dist/lib/traces/worker-template.js +154 -1
- package/package.json +1 -1
|
@@ -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, assertSandboxForwardsHostGhAuth } from '../sandbox.js';
|
|
24
|
+
import { prepareJobHome, buildSpawnEnv, getJobHomePath, assertSandboxForwardsHostGhAuth, linkVersionAuth } 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';
|
|
@@ -37,9 +37,7 @@ import { walkForFiles } from '../fs-walk.js';
|
|
|
37
37
|
import { getBinaryPath, isVersionInstalled, resolveVersion, getVersionHomePath } from '../installations/versions.js';
|
|
38
38
|
import { resolveClaudeSetupToken } from '../claude-account-token.js';
|
|
39
39
|
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from '../accounting/rotate.js';
|
|
40
|
-
import {
|
|
41
|
-
import { machineId } from '../machine-id.js';
|
|
42
|
-
import { selfConfiguredDeviceRole } from '../device-config.js';
|
|
40
|
+
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../device-config.js';
|
|
43
41
|
import { isSelfUpdatingAgent, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
|
|
44
42
|
import { isCustomHarnessName, readProfile } from '../profiles.js';
|
|
45
43
|
export class RoutineAlreadyRunningError extends Error {
|
|
@@ -860,6 +858,27 @@ function readCommandExitCode(runDir) {
|
|
|
860
858
|
return null;
|
|
861
859
|
}
|
|
862
860
|
}
|
|
861
|
+
/** Claude's own local auth check; unlike account metadata it cannot mistake identity for a usable login. */
|
|
862
|
+
export function claudeVersionIsAuthenticated(version) {
|
|
863
|
+
if (!isVersionInstalled('claude', version))
|
|
864
|
+
return true; // synthetic/unit plans are validated at spawn
|
|
865
|
+
const binary = getBinaryPath('claude', version);
|
|
866
|
+
if (!binary)
|
|
867
|
+
return false;
|
|
868
|
+
const home = getVersionHomePath('claude', version);
|
|
869
|
+
try {
|
|
870
|
+
const raw = execFileSync(binary, ['auth', 'status', '--json'], {
|
|
871
|
+
encoding: 'utf8',
|
|
872
|
+
timeout: 5_000,
|
|
873
|
+
env: { ...process.env, HOME: process.env.AGENTS_REAL_HOME || os.homedir(), CLAUDE_CONFIG_DIR: path.join(home, '.claude') },
|
|
874
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
875
|
+
});
|
|
876
|
+
return JSON.parse(raw).loggedIn === true;
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
863
882
|
/**
|
|
864
883
|
* Resolve the version/account chain for a routine the same way `agents run`
|
|
865
884
|
* does: honor an explicit `version:` pin; otherwise use the configured run
|
|
@@ -890,7 +909,7 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
|
|
|
890
909
|
: false;
|
|
891
910
|
const selectedCredential = config.account
|
|
892
911
|
? (explicitCredential ? config.account : undefined)
|
|
893
|
-
: resolveAccountSelection(undefined, agent, meta);
|
|
912
|
+
: resolveAccountSelection(undefined, agent, meta)?.id;
|
|
894
913
|
if (selectedCredential) {
|
|
895
914
|
// A default/binding may resolve to a native account — never route that
|
|
896
915
|
// through the provider credential path (it has no bundle to resolve).
|
|
@@ -980,6 +999,24 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
|
|
|
980
999
|
if (!version) {
|
|
981
1000
|
version = resolveVersion(agent, cwd) ?? undefined;
|
|
982
1001
|
}
|
|
1002
|
+
if (agent === 'claude' && version) {
|
|
1003
|
+
const checkClaudeAuth = deps.claudeVersionIsAuthenticated ?? claudeVersionIsAuthenticated;
|
|
1004
|
+
const authenticatedHealthy = rotation?.healthy.filter((candidate) => checkClaudeAuth(candidate.version));
|
|
1005
|
+
const authenticated = authenticatedHealthy?.find((candidate) => candidate.version === version) ?? authenticatedHealthy?.[0];
|
|
1006
|
+
if (rotation && (!authenticated || authenticatedHealthy.length === 0)) {
|
|
1007
|
+
throw new Error(`Routine '${config.name}' found no authenticated Claude account; run \`claude /login\` for an installed version or pin a provider profile.`);
|
|
1008
|
+
}
|
|
1009
|
+
if (!rotation && !checkClaudeAuth(version)) {
|
|
1010
|
+
throw new Error(`Routine '${config.name}' found no authenticated Claude account at claude@${version}; run \`claude /login\` for that version or remove the pin.`);
|
|
1011
|
+
}
|
|
1012
|
+
if (rotation && authenticated) {
|
|
1013
|
+
const rejected = rotation.healthy.filter((candidate) => !authenticatedHealthy.includes(candidate));
|
|
1014
|
+
if (rejected.length > 0 || rotation.picked !== authenticated) {
|
|
1015
|
+
rotation = { ...rotation, picked: authenticated, healthy: authenticatedHealthy, excluded: [...rotation.excluded, ...rejected] };
|
|
1016
|
+
}
|
|
1017
|
+
version = authenticated.version;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
983
1020
|
if (!version) {
|
|
984
1021
|
process.stderr.write(`[agents] routine ${config.name}: no version of ${agent} configured — ` +
|
|
985
1022
|
`run: agents add ${agent}@<version> && agents use ${agent} <version>\n`);
|
|
@@ -1121,13 +1158,14 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone, overlayH
|
|
|
1121
1158
|
const setupToken = agent === 'claude' && version
|
|
1122
1159
|
? resolveClaudeSetupToken(getVersionHomePath('claude', version))
|
|
1123
1160
|
: null;
|
|
1124
|
-
// A
|
|
1125
|
-
// login for EVERY run, routines
|
|
1126
|
-
// credential (RUSH-2395, mirrors
|
|
1127
|
-
// On a
|
|
1128
|
-
// setup-token by value so a leaked ambient
|
|
1129
|
-
// token the user set deliberately is a
|
|
1130
|
-
|
|
1161
|
+
// A headed device (personal or desktop — the user's own interactive box or a
|
|
1162
|
+
// headed always-on box) uses the per-version login for EVERY run, routines
|
|
1163
|
+
// included — the setup-token is a worker-only credential (RUSH-2395, mirrors
|
|
1164
|
+
// the claude adapter's headed-device branch). On a headed box, only strip an
|
|
1165
|
+
// inherited copy of the account's OWN setup-token by value so a leaked ambient
|
|
1166
|
+
// value can't override the login; a token the user set deliberately is a
|
|
1167
|
+
// different string and survives.
|
|
1168
|
+
if (isHeadedDeviceRole(selfConfiguredDeviceRole())) {
|
|
1131
1169
|
if (setupToken && out.CLAUDE_CODE_OAUTH_TOKEN === setupToken)
|
|
1132
1170
|
delete out.CLAUDE_CODE_OAUTH_TOKEN;
|
|
1133
1171
|
}
|
|
@@ -1142,6 +1180,10 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone, overlayH
|
|
|
1142
1180
|
// to the overlay so an ambient value cannot bypass the routine sandbox.
|
|
1143
1181
|
out.XDG_CONFIG_HOME = path.join(overlayHome, '.config');
|
|
1144
1182
|
}
|
|
1183
|
+
if (overlayHome && agent === 'claude')
|
|
1184
|
+
out.HOME = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1185
|
+
if (overlayHome && agent === 'codex')
|
|
1186
|
+
out.CODEX_HOME = path.join(overlayHome, '.codex');
|
|
1145
1187
|
if (timezone)
|
|
1146
1188
|
out.TZ = timezone;
|
|
1147
1189
|
return out;
|
|
@@ -1303,7 +1345,7 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1303
1345
|
// sentinel, version homes) from HOME, which the overlay would hide. Exec
|
|
1304
1346
|
// still isolates the run in the host version home it swaps HOME into.
|
|
1305
1347
|
const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
|
|
1306
|
-
const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
|
|
1348
|
+
const overlayHome = useSandbox ? prepareJobHome(config, primaryVersion) : undefined;
|
|
1307
1349
|
const runId = attempt.runId;
|
|
1308
1350
|
const runDir = getRunDir(config.name, runId);
|
|
1309
1351
|
fs.mkdirSync(runDir, { recursive: true });
|
|
@@ -1328,7 +1370,7 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1328
1370
|
if (!dispatchesViaAgentsRun(config)) {
|
|
1329
1371
|
const { findUnifiedAccount, resolveAccountSelection, resolveCredentialAccount } = await import('../account-registry.js');
|
|
1330
1372
|
const meta = readMeta();
|
|
1331
|
-
const selectedAccount = resolveAccountSelection(config.account, effectiveAgent, meta);
|
|
1373
|
+
const selectedAccount = resolveAccountSelection(config.account, effectiveAgent, meta)?.id;
|
|
1332
1374
|
if (selectedAccount) {
|
|
1333
1375
|
// Only a provider account injects env; a native account (explicit or via a
|
|
1334
1376
|
// device-scoped binding) is read from the harness home and forwards nothing.
|
|
@@ -1359,24 +1401,24 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1359
1401
|
// Baseline the child's transcript dirs BEFORE spawning so archiveRoutineTranscripts
|
|
1360
1402
|
// copies only THIS run's transcript out of a shared per-version home (RUSH-2271).
|
|
1361
1403
|
snapshotRoutineTranscriptBase(meta, runDir, overlayHome);
|
|
1362
|
-
// Auth preflight: if the
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1404
|
+
// Auth preflight: if the resolved account is provably signed out (revoked or
|
|
1405
|
+
// unconfigured), record a terminal `blocked`/`agent_auth_failed` run with the
|
|
1406
|
+
// re-login repair instead of spawning a doomed run that 401s and burns a
|
|
1407
|
+
// session (PHNX-3415). `blocked` (readiness rejection, no body ran) is kept
|
|
1408
|
+
// distinct from `failed` (a body ran and errored) per RT-7. Cache-only + fails
|
|
1409
|
+
// OPEN — see fireTimeAuthReadiness.
|
|
1368
1410
|
const preflightVersion = launch.chain[0]?.version;
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1411
|
+
// Dynamic import breaks the routine-readiness -> scheduling/routines -> runner cycle.
|
|
1412
|
+
const { fireTimeAuthReadiness } = await import('../routine-readiness.js');
|
|
1413
|
+
const authBlocker = preflightVersion ? fireTimeAuthReadiness(effectiveAgent, preflightVersion) : null;
|
|
1414
|
+
if (authBlocker) {
|
|
1415
|
+
process.stderr.write(`[agents] routine ${config.name}: ${effectiveAgent}@${preflightVersion} ${authBlocker.code} — skipping run (${authBlocker.repair})\n`);
|
|
1416
|
+
meta.readiness = authBlocker;
|
|
1417
|
+
finalizeRunMeta(meta, 'blocked', null, { errorMessage: `blocked: ${authBlocker.code} — ${authBlocker.message}` });
|
|
1418
|
+
writeRunMeta(meta);
|
|
1419
|
+
timer.end({ status: 'blocked', runId, error: authBlocker.code });
|
|
1420
|
+
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
1421
|
+
return { meta, reportPath: null };
|
|
1380
1422
|
}
|
|
1381
1423
|
const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
|
|
1382
1424
|
// Loop path: delegate to runLoop (same driver as `agents run --loop` / workflow loop:).
|
|
@@ -1443,6 +1485,8 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1443
1485
|
meta.version = attemptVersion;
|
|
1444
1486
|
snapshotRoutineTranscriptBase(meta, runDir, overlayHome);
|
|
1445
1487
|
const viaAgentsRun = dispatchesViaAgentsRun(config);
|
|
1488
|
+
if (overlayHome)
|
|
1489
|
+
linkVersionAuth(overlayHome, attemptAgent, attemptVersion);
|
|
1446
1490
|
const cmd = viaAgentsRun
|
|
1447
1491
|
? baseCmd
|
|
1448
1492
|
: pinJobBinary(baseCmd, attemptAgent, attemptVersion);
|
|
@@ -1836,7 +1880,7 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
|
|
|
1836
1880
|
// sentinel, version homes) from HOME, which the overlay would hide. Exec
|
|
1837
1881
|
// still isolates the run in the host version home it swaps HOME into.
|
|
1838
1882
|
const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
|
|
1839
|
-
const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
|
|
1883
|
+
const overlayHome = useSandbox ? prepareJobHome(config, version) : undefined;
|
|
1840
1884
|
const runId = attempt.runId;
|
|
1841
1885
|
const runDir = getRunDir(config.name, runId);
|
|
1842
1886
|
fs.mkdirSync(runDir, { recursive: true });
|
|
@@ -1849,7 +1893,7 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
|
|
|
1849
1893
|
const { findAccount, resolveAccountSelection, resolveCredentialAccount } = await import('../account-registry.js');
|
|
1850
1894
|
const selectedAccount = config.account && !findAccount(config.account)
|
|
1851
1895
|
? undefined
|
|
1852
|
-
: resolveAccountSelection(config.account, config.agent, readMeta());
|
|
1896
|
+
: resolveAccountSelection(config.account, config.agent, readMeta())?.id;
|
|
1853
1897
|
if (selectedAccount)
|
|
1854
1898
|
Object.assign(baseEnv, resolveCredentialAccount(selectedAccount, config.agent).env);
|
|
1855
1899
|
}
|
|
@@ -1890,28 +1934,26 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
|
|
|
1890
1934
|
// Baseline the child's transcript dirs BEFORE spawning so archiveRoutineTranscripts
|
|
1891
1935
|
// copies only THIS run's transcript out of a shared per-version home (RUSH-2271).
|
|
1892
1936
|
snapshotRoutineTranscriptBase(meta, runDir, overlayHome);
|
|
1893
|
-
// Auth preflight (mirrors executeJob):
|
|
1894
|
-
//
|
|
1895
|
-
//
|
|
1896
|
-
//
|
|
1897
|
-
// doomed run + poisoned report. Cache-only; fails OPEN on any non-dead/missing
|
|
1898
|
-
// verdict, and agents with no live probe (codex/gemini/grok) are never blocked.
|
|
1937
|
+
// Auth preflight (mirrors executeJob): a provably signed-out resolved account
|
|
1938
|
+
// records a terminal `blocked`/`agent_auth_failed` run with the re-login repair
|
|
1939
|
+
// instead of spawning a doomed run that 401s and burns a session (PHNX-3415).
|
|
1940
|
+
// Cache-only + fails OPEN — see fireTimeAuthReadiness.
|
|
1899
1941
|
const preflightVersion = launch.chain[0]?.version;
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
}
|
|
1908
|
-
catch { /* already closed */ }
|
|
1909
|
-
finalizeRunMeta(meta, 'failed', 1, { errorMessage: reason });
|
|
1910
|
-
writeRunMeta(meta);
|
|
1911
|
-
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
1912
|
-
timer.end({ status: 'failed', exitCode: 1, runId, error: reason });
|
|
1913
|
-
return meta;
|
|
1942
|
+
// Dynamic import breaks the routine-readiness -> scheduling/routines -> runner cycle.
|
|
1943
|
+
const { fireTimeAuthReadiness } = await import('../routine-readiness.js');
|
|
1944
|
+
const authBlocker = preflightVersion ? fireTimeAuthReadiness(effectiveAgent, preflightVersion) : null;
|
|
1945
|
+
if (authBlocker) {
|
|
1946
|
+
process.stderr.write(`[agents] routine ${config.name}: ${effectiveAgent}@${preflightVersion} ${authBlocker.code} — skipping run (${authBlocker.repair})\n`);
|
|
1947
|
+
try {
|
|
1948
|
+
fs.closeSync(stdoutFd);
|
|
1914
1949
|
}
|
|
1950
|
+
catch { /* already closed */ }
|
|
1951
|
+
meta.readiness = authBlocker;
|
|
1952
|
+
finalizeRunMeta(meta, 'blocked', null, { errorMessage: `blocked: ${authBlocker.code} — ${authBlocker.message}` });
|
|
1953
|
+
writeRunMeta(meta);
|
|
1954
|
+
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
1955
|
+
timer.end({ status: 'blocked', runId, error: authBlocker.code });
|
|
1956
|
+
return meta;
|
|
1915
1957
|
}
|
|
1916
1958
|
const child = spawn(cmd[0], cmd.slice(1), {
|
|
1917
1959
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet usage-snapshot sync as a `PeriodicService` (PHNX-3392 usage-sync).
|
|
3
|
+
*
|
|
4
|
+
* On a headed box (personal/desktop) each tick pushes the local identity-keyed
|
|
5
|
+
* Claude usage rows to worker peers that cannot read usage themselves. On a
|
|
6
|
+
* worker whose cache is empty or stale, the same tick pulls those rows from the
|
|
7
|
+
* primary headed device. Both drivers gate on the self role, so this service is
|
|
8
|
+
* safe to register everywhere. Single-executor per destination: each daemon only
|
|
9
|
+
* writes the DESTINATION's own cache, and the merge is newest-wins + idempotent.
|
|
10
|
+
*/
|
|
11
|
+
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
12
|
+
import type { DaemonServiceId } from '../daemon-services.js';
|
|
13
|
+
export declare class UsageSyncService extends BasePeriodicService {
|
|
14
|
+
readonly id: DaemonServiceId;
|
|
15
|
+
readonly intervalMs: number;
|
|
16
|
+
readonly deadlineMs: number;
|
|
17
|
+
readonly startupDelayMs = 90000;
|
|
18
|
+
protected onStart(_ctx: DaemonContext): Promise<void>;
|
|
19
|
+
protected onStop(): Promise<void>;
|
|
20
|
+
protected onTick(ctx: DaemonContext): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet usage-snapshot sync as a `PeriodicService` (PHNX-3392 usage-sync).
|
|
3
|
+
*
|
|
4
|
+
* On a headed box (personal/desktop) each tick pushes the local identity-keyed
|
|
5
|
+
* Claude usage rows to worker peers that cannot read usage themselves. On a
|
|
6
|
+
* worker whose cache is empty or stale, the same tick pulls those rows from the
|
|
7
|
+
* primary headed device. Both drivers gate on the self role, so this service is
|
|
8
|
+
* safe to register everywhere. Single-executor per destination: each daemon only
|
|
9
|
+
* writes the DESTINATION's own cache, and the merge is newest-wins + idempotent.
|
|
10
|
+
*/
|
|
11
|
+
import { BasePeriodicService } from './service.js';
|
|
12
|
+
const USAGE_SYNC_TICK_MS = 15 * 60_000;
|
|
13
|
+
const USAGE_SYNC_DEADLINE_MS = 2 * 60_000;
|
|
14
|
+
const USAGE_SYNC_KICKOFF_MS = 90_000;
|
|
15
|
+
export class UsageSyncService extends BasePeriodicService {
|
|
16
|
+
id = 'usage-sync';
|
|
17
|
+
intervalMs = USAGE_SYNC_TICK_MS;
|
|
18
|
+
deadlineMs = USAGE_SYNC_DEADLINE_MS;
|
|
19
|
+
startupDelayMs = USAGE_SYNC_KICKOFF_MS;
|
|
20
|
+
async onStart(_ctx) {
|
|
21
|
+
// No connections to open — each tick re-reads the local cache + registry.
|
|
22
|
+
}
|
|
23
|
+
async onStop() {
|
|
24
|
+
// Nothing to release — the supervisor's timer teardown is the only cleanup.
|
|
25
|
+
}
|
|
26
|
+
async onTick(ctx) {
|
|
27
|
+
const { pullUsageFromPrimary, syncFleetUsageSnapshots } = await import('../accounting/usage-sync.js');
|
|
28
|
+
const pushResult = syncFleetUsageSnapshots();
|
|
29
|
+
if (pushResult.pushed.length > 0) {
|
|
30
|
+
ctx.log('INFO', `usage-sync: pushed usage to ${pushResult.pushed.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
for (const err of pushResult.errors) {
|
|
33
|
+
ctx.log('WARN', `usage-sync: ${err.device}: ${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
const pullResult = pullUsageFromPrimary();
|
|
36
|
+
if (pullResult.pulledFrom) {
|
|
37
|
+
ctx.log('INFO', `usage-sync: pulled usage from ${pullResult.pulledFrom}; merged ${pullResult.merged} row(s)`);
|
|
38
|
+
}
|
|
39
|
+
if (pullResult.error)
|
|
40
|
+
ctx.log('WARN', `usage-sync: pull: ${pullResult.error}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* enabled without pulling in the whole daemon lifecycle.
|
|
8
8
|
*/
|
|
9
9
|
/** Every service the daemon can host. IDs are kebab-case and stable. */
|
|
10
|
-
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'keychain-reap' | 'account-state' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state';
|
|
10
|
+
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'keychain-reap' | 'account-state' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state';
|
|
11
11
|
/** Human-readable metadata for each service. */
|
|
12
12
|
export interface DaemonServiceDef {
|
|
13
13
|
id: DaemonServiceId;
|
|
@@ -97,6 +97,11 @@ export const DAEMON_SERVICES = [
|
|
|
97
97
|
title: 'Auth bundle sync',
|
|
98
98
|
description: 'Pushes the reserved file-backed auth bundle (setup-tokens) to pinned fleet devices that do not yet have it.',
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
id: 'usage-sync',
|
|
102
|
+
title: 'Usage snapshot sync',
|
|
103
|
+
description: 'Bidirectional per-account usage sync: a headed personal/desktop box pushes its snapshot to worker peers, and a worker with a stale cache pulls from the primary — so workers that cannot read the endpoint themselves still route by real capacity.',
|
|
104
|
+
},
|
|
100
105
|
];
|
|
101
106
|
/** Stable order of service IDs. */
|
|
102
107
|
export const DAEMON_SERVICE_IDS = DAEMON_SERVICES.map((s) => s.id);
|
|
@@ -87,7 +87,7 @@ export interface ConfigTarget {
|
|
|
87
87
|
fleet?: boolean;
|
|
88
88
|
}
|
|
89
89
|
/** Roles a device can be marked with — see the `role` key below. */
|
|
90
|
-
declare const DEVICE_ROLES: readonly ["worker", "personal"];
|
|
90
|
+
declare const DEVICE_ROLES: readonly ["worker", "personal", "desktop"];
|
|
91
91
|
/** Which devices automatic placement may pick — see the `auto.pool` key below. */
|
|
92
92
|
declare const AUTO_POOL_MODES: readonly ["workers", "all"];
|
|
93
93
|
export declare const CONFIG_KEYS: readonly ConfigKeySpec[];
|
|
@@ -150,13 +150,24 @@ export declare function configuredDeviceRole(name: string): ConfiguredDeviceRole
|
|
|
150
150
|
* it was never marked. Keyed off {@link machineId} (overridable via
|
|
151
151
|
* AGENTS_SYNC_MACHINE_ID), so it matches the device's own config-folder key.
|
|
152
152
|
*
|
|
153
|
-
* The auth strategy reads this: a
|
|
154
|
-
*
|
|
155
|
-
* run, interactive or headless; only a `worker`
|
|
156
|
-
* (RUSH-2395). `undefined` is treated as
|
|
157
|
-
* that gate — an unmarked box has no login to
|
|
153
|
+
* The auth strategy reads this: a headed device (`personal` or `desktop`, see
|
|
154
|
+
* {@link isHeadedDeviceRole}) holds a real per-version login and MUST
|
|
155
|
+
* authenticate from it for EVERY run, interactive or headless; only a `worker`
|
|
156
|
+
* uses the file-based setup-token (RUSH-2395). `undefined` is treated as
|
|
157
|
+
* non-headed (worker-equivalent) by that gate — an unmarked box has no login to
|
|
158
|
+
* defer to.
|
|
158
159
|
*/
|
|
159
160
|
export declare function selfConfiguredDeviceRole(): ConfiguredDeviceRole | undefined;
|
|
161
|
+
/**
|
|
162
|
+
* A "headed" device — one with an interactive desktop login, so it authenticates
|
|
163
|
+
* from its own per-version Claude login (which carries the `user:profile` scope
|
|
164
|
+
* the usage endpoint needs) rather than the headless file setup-token. Both
|
|
165
|
+
* `personal` (the box you sit at) and `desktop` (a headed always-on box like a
|
|
166
|
+
* Mac mini) qualify; a `worker` and an unmarked box do NOT. This is the single
|
|
167
|
+
* predicate the auth-bucket sites read, so `personal` and `desktop` never drift
|
|
168
|
+
* apart (claude adapter, routine spawn env, `agents view` usage login).
|
|
169
|
+
*/
|
|
170
|
+
export declare function isHeadedDeviceRole(role: ConfiguredDeviceRole | undefined): boolean;
|
|
160
171
|
/** Mark a device's role fleet-wide; `undefined` clears the mark. */
|
|
161
172
|
export declare function setConfiguredDeviceRole(name: string, role: ConfiguredDeviceRole | undefined): void;
|
|
162
173
|
/**
|
|
@@ -41,7 +41,7 @@ import { migrateDeviceConfigStores } from './devices/config-migration.js';
|
|
|
41
41
|
const DEVICE_PLATFORMS = ['windows', 'linux', 'macos', 'unknown'];
|
|
42
42
|
const SSH_AUTH_METHODS = ['key', 'password'];
|
|
43
43
|
/** Roles a device can be marked with — see the `role` key below. */
|
|
44
|
-
const DEVICE_ROLES = ['worker', 'personal'];
|
|
44
|
+
const DEVICE_ROLES = ['worker', 'personal', 'desktop'];
|
|
45
45
|
/** Which devices automatic placement may pick — see the `auto.pool` key below. */
|
|
46
46
|
const AUTO_POOL_MODES = ['workers', 'all'];
|
|
47
47
|
export const CONFIG_KEYS = [
|
|
@@ -67,8 +67,8 @@ export const CONFIG_KEYS = [
|
|
|
67
67
|
scope: 'user',
|
|
68
68
|
type: 'string',
|
|
69
69
|
description: "Which devices automatic placement (`--device auto`) may pick: 'workers' (default — only devices marked role=worker, " +
|
|
70
|
-
"once at least one is marked) or 'all' (every online device, ignoring worker marks). A device marked personal
|
|
71
|
-
'never picked automatically under either mode.',
|
|
70
|
+
"once at least one is marked) or 'all' (every online device, ignoring worker marks). A device marked personal or " +
|
|
71
|
+
'desktop is never picked automatically under either mode.',
|
|
72
72
|
defaultValue: 'workers',
|
|
73
73
|
validate: (v) => AUTO_POOL_MODES.includes(v)
|
|
74
74
|
? null
|
|
@@ -279,9 +279,10 @@ export const CONFIG_KEYS = [
|
|
|
279
279
|
scope: 'device',
|
|
280
280
|
visibility: 'shared',
|
|
281
281
|
type: 'string',
|
|
282
|
-
description: "What this device is for, fleet-wide: 'worker' (a box agents run on)
|
|
283
|
-
|
|
284
|
-
'
|
|
282
|
+
description: "What this device is for, fleet-wide: 'worker' (a box agents run on), 'personal' (a machine you sit at — your " +
|
|
283
|
+
"interactive seat), or 'desktop' (a headed always-on box like a Mac mini — the release/credential home). Personal " +
|
|
284
|
+
'and desktop are never picked automatically. Marking ANY device worker turns automatic placement into an allowlist: ' +
|
|
285
|
+
'`--device auto` then picks only from the marked workers.',
|
|
285
286
|
validate: (v) => DEVICE_ROLES.includes(v)
|
|
286
287
|
? null
|
|
287
288
|
: `role must be one of ${DEVICE_ROLES.join(' | ')}.`,
|
|
@@ -630,15 +631,28 @@ export function configuredDeviceRole(name) {
|
|
|
630
631
|
* it was never marked. Keyed off {@link machineId} (overridable via
|
|
631
632
|
* AGENTS_SYNC_MACHINE_ID), so it matches the device's own config-folder key.
|
|
632
633
|
*
|
|
633
|
-
* The auth strategy reads this: a
|
|
634
|
-
*
|
|
635
|
-
* run, interactive or headless; only a `worker`
|
|
636
|
-
* (RUSH-2395). `undefined` is treated as
|
|
637
|
-
* that gate — an unmarked box has no login to
|
|
634
|
+
* The auth strategy reads this: a headed device (`personal` or `desktop`, see
|
|
635
|
+
* {@link isHeadedDeviceRole}) holds a real per-version login and MUST
|
|
636
|
+
* authenticate from it for EVERY run, interactive or headless; only a `worker`
|
|
637
|
+
* uses the file-based setup-token (RUSH-2395). `undefined` is treated as
|
|
638
|
+
* non-headed (worker-equivalent) by that gate — an unmarked box has no login to
|
|
639
|
+
* defer to.
|
|
638
640
|
*/
|
|
639
641
|
export function selfConfiguredDeviceRole() {
|
|
640
642
|
return configuredDeviceRole(machineId());
|
|
641
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* A "headed" device — one with an interactive desktop login, so it authenticates
|
|
646
|
+
* from its own per-version Claude login (which carries the `user:profile` scope
|
|
647
|
+
* the usage endpoint needs) rather than the headless file setup-token. Both
|
|
648
|
+
* `personal` (the box you sit at) and `desktop` (a headed always-on box like a
|
|
649
|
+
* Mac mini) qualify; a `worker` and an unmarked box do NOT. This is the single
|
|
650
|
+
* predicate the auth-bucket sites read, so `personal` and `desktop` never drift
|
|
651
|
+
* apart (claude adapter, routine spawn env, `agents view` usage login).
|
|
652
|
+
*/
|
|
653
|
+
export function isHeadedDeviceRole(role) {
|
|
654
|
+
return role === 'personal' || role === 'desktop';
|
|
655
|
+
}
|
|
642
656
|
/** Mark a device's role fleet-wide; `undefined` clears the mark. */
|
|
643
657
|
export function setConfiguredDeviceRole(name, role) {
|
|
644
658
|
assertValidDeviceName(name);
|
|
@@ -47,16 +47,25 @@ export declare function wrapRemoteCommand(device: DeviceProfile, cmd: string[]):
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function isAgentsBrowserDrive(cmd: string[]): boolean;
|
|
49
49
|
/**
|
|
50
|
-
* Prefix a remote command
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* Prefix a remote command with the provenance the far-side browser needs: the
|
|
51
|
+
* `AGENTS_FLEET_REMOTE=1` consent marker AND the caller's resolved actor
|
|
52
|
+
* (`AGENTS_ACTOR*`/`GIT_*`). Without the actor, a task created over
|
|
53
|
+
* `agents ssh <host> 'agents browser …'` is stamped `UNRESOLVED@<host>` because
|
|
54
|
+
* the peer re-resolves identity from its own (empty) env — the same RUSH-2028
|
|
55
|
+
* gap the `--device` dispatch already closes via `withActorEnv`, applied here to
|
|
56
|
+
* the `agents ssh` browser-drive seam so ownership is honest across BOTH paths.
|
|
55
57
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
+
* `wrapRemoteCommand` joins the argv with spaces (POSIX) or base64-encodes it for
|
|
59
|
+
* PowerShell, so a shell-appropriate prelude rides through both: `env VAR=v …` on
|
|
60
|
+
* POSIX, `$env:VAR='v'; …` on PowerShell. The marker stays the FIRST token so the
|
|
61
|
+
* consent gate and the idempotency guard below both still key on it.
|
|
62
|
+
*
|
|
63
|
+
* `provenanceEnv` is injectable so tests can pin a deterministic actor; it
|
|
64
|
+
* defaults to the live resolved actor. Already-marked argv (the `--device`
|
|
65
|
+
* fan-out stamps the marker before {@link buildSshInvocation}) is left unchanged
|
|
66
|
+
* so nothing is doubled.
|
|
58
67
|
*/
|
|
59
|
-
export declare function markFleetRemote(cmd: string[], device: Pick<DeviceProfile, 'shell'>): string[];
|
|
68
|
+
export declare function markFleetRemote(cmd: string[], device: Pick<DeviceProfile, 'shell'>, provenanceEnv?: Record<string, string>): string[];
|
|
60
69
|
/**
|
|
61
70
|
* Build the remote command that starts an INTERACTIVE LOGIN shell inside a
|
|
62
71
|
* mirrored project directory, falling back to the remote home when that
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import { assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
19
|
+
import { resolveActor, actorEnv } from '../actor.js';
|
|
19
20
|
import { getCliLaunch } from '../cli-entry.js';
|
|
20
21
|
import { encodePwshBase64 } from '../pwsh.js';
|
|
21
22
|
import { homeRemainder, remoteCdPrefix } from '../project-root.js';
|
|
@@ -104,24 +105,40 @@ export function isAgentsBrowserDrive(cmd) {
|
|
|
104
105
|
return (bin === 'agents' || bin === 'ag') && tokens[1] === 'browser';
|
|
105
106
|
}
|
|
106
107
|
/**
|
|
107
|
-
* Prefix a remote command
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
108
|
+
* Prefix a remote command with the provenance the far-side browser needs: the
|
|
109
|
+
* `AGENTS_FLEET_REMOTE=1` consent marker AND the caller's resolved actor
|
|
110
|
+
* (`AGENTS_ACTOR*`/`GIT_*`). Without the actor, a task created over
|
|
111
|
+
* `agents ssh <host> 'agents browser …'` is stamped `UNRESOLVED@<host>` because
|
|
112
|
+
* the peer re-resolves identity from its own (empty) env — the same RUSH-2028
|
|
113
|
+
* gap the `--device` dispatch already closes via `withActorEnv`, applied here to
|
|
114
|
+
* the `agents ssh` browser-drive seam so ownership is honest across BOTH paths.
|
|
112
115
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
116
|
+
* `wrapRemoteCommand` joins the argv with spaces (POSIX) or base64-encodes it for
|
|
117
|
+
* PowerShell, so a shell-appropriate prelude rides through both: `env VAR=v …` on
|
|
118
|
+
* POSIX, `$env:VAR='v'; …` on PowerShell. The marker stays the FIRST token so the
|
|
119
|
+
* consent gate and the idempotency guard below both still key on it.
|
|
120
|
+
*
|
|
121
|
+
* `provenanceEnv` is injectable so tests can pin a deterministic actor; it
|
|
122
|
+
* defaults to the live resolved actor. Already-marked argv (the `--device`
|
|
123
|
+
* fan-out stamps the marker before {@link buildSshInvocation}) is left unchanged
|
|
124
|
+
* so nothing is doubled.
|
|
115
125
|
*/
|
|
116
|
-
export function markFleetRemote(cmd, device) {
|
|
126
|
+
export function markFleetRemote(cmd, device, provenanceEnv = actorEnv(resolveActor())) {
|
|
117
127
|
if (device.shell === 'powershell') {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
// Exact-match guard, symmetric with the POSIX branch below: the marker token
|
|
129
|
+
// is always this literal, so `startsWith` would only loosen it for no gain.
|
|
130
|
+
if (cmd[0] === `$env:AGENTS_FLEET_REMOTE='1';`)
|
|
131
|
+
return cmd;
|
|
132
|
+
const prelude = [
|
|
133
|
+
`$env:AGENTS_FLEET_REMOTE='1';`,
|
|
134
|
+
...Object.entries(provenanceEnv).map(([k, v]) => `$env:${k}='${v.replace(/'/g, "''")}';`),
|
|
135
|
+
];
|
|
136
|
+
return [...prelude, ...cmd];
|
|
121
137
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
if (cmd[0] === 'env' && cmd[1] === 'AGENTS_FLEET_REMOTE=1')
|
|
139
|
+
return cmd;
|
|
140
|
+
const actorTokens = Object.entries(provenanceEnv).map(([k, v]) => shellQuote(`${k}=${v}`));
|
|
141
|
+
return ['env', 'AGENTS_FLEET_REMOTE=1', ...actorTokens, ...cmd];
|
|
125
142
|
}
|
|
126
143
|
/**
|
|
127
144
|
* Build the remote command that starts an INTERACTIVE LOGIN shell inside a
|
|
@@ -15,10 +15,11 @@
|
|
|
15
15
|
* |---|---|
|
|
16
16
|
* | no device marked | every online device (unchanged behavior) |
|
|
17
17
|
* | some marked `worker` | ONLY those workers |
|
|
18
|
-
* | marked `personal` | never, under either state |
|
|
18
|
+
* | marked `personal` / `desktop` | never, under either state |
|
|
19
19
|
*
|
|
20
|
-
* `auto.pool all` turns the allowlist off; `personal`
|
|
21
|
-
*
|
|
20
|
+
* `auto.pool all` turns the allowlist off; `personal` and `desktop` stay
|
|
21
|
+
* excluded, because a box the user sits at (personal) or a headed always-on
|
|
22
|
+
* release/credential box (desktop) is marked precisely so agents stay off it.
|
|
22
23
|
*/
|
|
23
24
|
import { type AutoPoolMode, type ConfiguredDeviceRole } from '../device-config.js';
|
|
24
25
|
export interface AutoPoolOptions {
|
package/dist/lib/devices/pool.js
CHANGED
|
@@ -15,15 +15,23 @@
|
|
|
15
15
|
* |---|---|
|
|
16
16
|
* | no device marked | every online device (unchanged behavior) |
|
|
17
17
|
* | some marked `worker` | ONLY those workers |
|
|
18
|
-
* | marked `personal` | never, under either state |
|
|
18
|
+
* | marked `personal` / `desktop` | never, under either state |
|
|
19
19
|
*
|
|
20
|
-
* `auto.pool all` turns the allowlist off; `personal`
|
|
21
|
-
*
|
|
20
|
+
* `auto.pool all` turns the allowlist off; `personal` and `desktop` stay
|
|
21
|
+
* excluded, because a box the user sits at (personal) or a headed always-on
|
|
22
|
+
* release/credential box (desktop) is marked precisely so agents stay off it.
|
|
22
23
|
*/
|
|
23
24
|
import { autoPoolMode, listConfiguredDeviceRoles } from '../device-config.js';
|
|
24
25
|
import { normalizeHost } from '../machine-id.js';
|
|
25
|
-
/**
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Roles that automatic placement never picks, whatever the pool mode.
|
|
28
|
+
*
|
|
29
|
+
* `personal` (a box you sit at) and `desktop` (a headed always-on box — the
|
|
30
|
+
* release/credential home, e.g. a Mac mini) are both off-limits to `--device
|
|
31
|
+
* auto`: neither is headless fan-out capacity, and landing agent work on them
|
|
32
|
+
* is the outcome the mark exists to prevent. Only `worker` is auto-eligible.
|
|
33
|
+
*/
|
|
34
|
+
const NEVER_AUTO = new Set(['personal', 'desktop']);
|
|
27
35
|
/**
|
|
28
36
|
* Narrow a candidate host list to the devices automatic placement may pick.
|
|
29
37
|
*
|