@phnx-labs/agents-cli 1.20.63 → 1.20.64
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 +17 -0
- package/README.md +9 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +55 -28
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +27 -8
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/routines.js +20 -14
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +279 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.js +98 -3
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/view.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +18 -0
- package/dist/lib/agents.js +27 -17
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +158 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +24 -0
- package/dist/lib/daemon.js +6 -1
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/routines.d.ts +21 -2
- package/dist/lib/routines.js +35 -12
- package/dist/lib/runner.js +255 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/session/bundle.d.ts +150 -0
- package/dist/lib/session/bundle.js +189 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/ssh-tunnel.js +13 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/types.d.ts +9 -0
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +162 -22
- package/package.json +1 -1
package/dist/commands/usage.js
CHANGED
|
@@ -3,8 +3,13 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, resolveAgentName, formatAgentError, } from '../lib/agents.js';
|
|
4
4
|
import { listInstalledVersions, getGlobalDefault, getVersionHomePath } from '../lib/versions.js';
|
|
5
5
|
import { formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Agents whose CLI surfaces usage data we can read today. Kept in sync with the
|
|
8
|
+
* live/last-seen sources `getUsageInfo` dispatches on in `../lib/usage.js`
|
|
9
|
+
* (claude, codex, kimi, droid) — an agent with a usage source but missing here
|
|
10
|
+
* would wrongly print "does not publish usage data" for a signed-in account.
|
|
11
|
+
*/
|
|
12
|
+
const USAGE_SUPPORTED = new Set(['claude', 'codex', 'kimi', 'droid']);
|
|
8
13
|
export function registerUsageCommand(program) {
|
|
9
14
|
addHostOption(program.command('usage [agent]'))
|
|
10
15
|
.description('Show rate-limit / quota usage per agent')
|
package/dist/commands/view.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
|
|
|
50
50
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
51
51
|
// first byte of output), the registry maps a command name to a thunk that
|
|
52
52
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
53
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
53
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
54
54
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
55
55
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
56
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -707,6 +707,7 @@ async function registerAllEagerCommands() {
|
|
|
707
707
|
await reg(loadWebhook);
|
|
708
708
|
await reg(loadFunnel);
|
|
709
709
|
await reg(loadFeed);
|
|
710
|
+
await reg(loadMailboxes);
|
|
710
711
|
await reg(loadSsh);
|
|
711
712
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
712
713
|
registerJobsCronAliasCommand(program, 'cron');
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -130,6 +130,22 @@ export interface AccountInfo {
|
|
|
130
130
|
}
|
|
131
131
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
132
132
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
|
133
|
+
/** Decrypted contents of Droid's auth.v2.file (subset we consume). */
|
|
134
|
+
export interface DroidAuthPayload {
|
|
135
|
+
access_token?: string;
|
|
136
|
+
active_organization_id?: string | null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Factory Droid stores its OAuth credential encrypted at ~/.factory/auth.v2.file
|
|
140
|
+
* (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
|
|
141
|
+
* in ~/.factory/auth.v2.key. On the keyfile-v2 source there is no OS-keychain /
|
|
142
|
+
* device binding — the key is on disk — so we can decrypt locally with no
|
|
143
|
+
* network call. Every failure (missing key file — e.g. a keyring-v2/legacy
|
|
144
|
+
* login with no on-disk key, a bad GCM tag, or malformed JSON) returns null.
|
|
145
|
+
* Never throws. Shared by account identity below and the Droid usage fetcher
|
|
146
|
+
* in usage.ts.
|
|
147
|
+
*/
|
|
148
|
+
export declare function decryptDroidAuthPayload(base: string): DroidAuthPayload | null;
|
|
133
149
|
/**
|
|
134
150
|
* Antigravity (`agy`) stores its OAuth token via the Go keyring library
|
|
135
151
|
* (zalando/go-keyring), which is platform-split:
|
|
@@ -173,6 +189,8 @@ export declare function resolveLastActive(agentId: AgentId, base: string, config
|
|
|
173
189
|
* Used during init to show approximate session count to user.
|
|
174
190
|
*/
|
|
175
191
|
export declare function countSessionFiles(agentId: AgentId): number;
|
|
192
|
+
/** Decode the payload section of a JWT token without verifying its signature. */
|
|
193
|
+
export declare function decodeJwtPayload(token: string): Record<string, any> | null;
|
|
176
194
|
/** Register an MCP server with an agent's CLI via `mcp add`. */
|
|
177
195
|
export declare function registerMcp(agentId: AgentId, name: string, command: string, scope?: 'user' | 'project', transport?: string, options?: {
|
|
178
196
|
home?: string;
|
package/dist/lib/agents.js
CHANGED
|
@@ -984,15 +984,12 @@ function resolveAccountCredentialPath(base, ...segments) {
|
|
|
984
984
|
* (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
|
|
985
985
|
* in ~/.factory/auth.v2.key. On the keyfile-v2 source there is no OS-keychain /
|
|
986
986
|
* device binding — the key is on disk — so we can decrypt locally with no
|
|
987
|
-
* network call.
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
* Every failure (missing key file — e.g. a keyring-v2/legacy login with no
|
|
992
|
-
* on-disk key, a bad GCM tag, malformed JSON, or no email claim) returns null so
|
|
993
|
-
* the caller falls back to the file-presence signed-in signal. Never throws.
|
|
987
|
+
* network call. Every failure (missing key file — e.g. a keyring-v2/legacy
|
|
988
|
+
* login with no on-disk key, a bad GCM tag, or malformed JSON) returns null.
|
|
989
|
+
* Never throws. Shared by account identity below and the Droid usage fetcher
|
|
990
|
+
* in usage.ts.
|
|
994
991
|
*/
|
|
995
|
-
function
|
|
992
|
+
export function decryptDroidAuthPayload(base) {
|
|
996
993
|
const filePath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.file');
|
|
997
994
|
const keyPath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.key');
|
|
998
995
|
if (!filePath || !keyPath)
|
|
@@ -1012,19 +1009,32 @@ function decryptDroidCredential(base) {
|
|
|
1012
1009
|
decipher.final(),
|
|
1013
1010
|
]).toString('utf-8');
|
|
1014
1011
|
const cred = JSON.parse(plaintext);
|
|
1015
|
-
|
|
1016
|
-
if (!claims)
|
|
1017
|
-
return null;
|
|
1018
|
-
return {
|
|
1019
|
-
email: typeof claims.email === 'string' ? claims.email : null,
|
|
1020
|
-
orgId: normalizeIdentityPart(claims.org_id ?? cred.active_organization_id),
|
|
1021
|
-
role: typeof claims.role === 'string' ? claims.role : null,
|
|
1022
|
-
};
|
|
1012
|
+
return cred && typeof cred === 'object' ? cred : null;
|
|
1023
1013
|
}
|
|
1024
1014
|
catch {
|
|
1025
1015
|
return null;
|
|
1026
1016
|
}
|
|
1027
1017
|
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Derive Droid account identity from the decrypted credential. The
|
|
1020
|
+
* `access_token` is a WorkOS JWT carrying an `email` claim (plus org_id /
|
|
1021
|
+
* role). We decode the claim WITHOUT verifying `exp`: the email is stable
|
|
1022
|
+
* identity for display, not an authorization decision, so an expired token
|
|
1023
|
+
* still yields the right address. Returns null when the credential can't be
|
|
1024
|
+
* decrypted or has no decodable claims, so the caller falls back to the
|
|
1025
|
+
* file-presence signed-in signal.
|
|
1026
|
+
*/
|
|
1027
|
+
function decryptDroidCredential(base) {
|
|
1028
|
+
const cred = decryptDroidAuthPayload(base);
|
|
1029
|
+
const claims = typeof cred?.access_token === 'string' ? decodeJwtPayload(cred.access_token) : null;
|
|
1030
|
+
if (!claims)
|
|
1031
|
+
return null;
|
|
1032
|
+
return {
|
|
1033
|
+
email: typeof claims.email === 'string' ? claims.email : null,
|
|
1034
|
+
orgId: normalizeIdentityPart(claims.org_id ?? cred?.active_organization_id),
|
|
1035
|
+
role: typeof claims.role === 'string' ? claims.role : null,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1028
1038
|
let cachedAgyKeychainSignedIn;
|
|
1029
1039
|
/**
|
|
1030
1040
|
* Antigravity (`agy`) stores its OAuth token via the Go keyring library
|
|
@@ -1564,7 +1574,7 @@ export function countSessionFiles(agentId) {
|
|
|
1564
1574
|
return count;
|
|
1565
1575
|
}
|
|
1566
1576
|
/** Decode the payload section of a JWT token without verifying its signature. */
|
|
1567
|
-
function decodeJwtPayload(token) {
|
|
1577
|
+
export function decodeJwtPayload(token) {
|
|
1568
1578
|
const payload = token.split('.')[1];
|
|
1569
1579
|
if (!payload)
|
|
1570
1580
|
return null;
|
|
@@ -9,7 +9,7 @@ import { writeProfileRuntime, clearProfileRuntime } from '../runtime-state.js';
|
|
|
9
9
|
// is the shared hardened baseline (BatchMode + ConnectTimeout + keepalive) —
|
|
10
10
|
// reuse it so the raw-ssh spawns below fail fast on an unreachable host instead
|
|
11
11
|
// of hanging on the default ~127s TCP timeout, rather than re-listing options.
|
|
12
|
-
import { shellQuote, SSH_OPTS } from '../../ssh-exec.js';
|
|
12
|
+
import { shellQuote, SSH_OPTS, assertValidSshTarget } from '../../ssh-exec.js';
|
|
13
13
|
export { shellQuote };
|
|
14
14
|
// The `ssh -L` tunnel spawn is shared with `agents computer --host`; it lives in
|
|
15
15
|
// the single ssh-tunnel helper. Calling it with no options preserves this
|
|
@@ -292,6 +292,12 @@ export function buildKillCmd(remoteOs, port) {
|
|
|
292
292
|
return `pids=$(lsof -ti ${shellQuote(`:${port}`)} 2>/dev/null); [ -z "$pids" ] || kill -9 $pids 2>/dev/null || true`;
|
|
293
293
|
}
|
|
294
294
|
export async function ensureRemoteBrowser(user, host, browserType, port, remoteOs, customBinary) {
|
|
295
|
+
// `user`/`host` derive from an ssh:// profile endpoint (git-tracked user
|
|
296
|
+
// config). Validate the composed target before it reaches the raw `ssh` spawn:
|
|
297
|
+
// an endpoint like `ssh://-Fattack@victim` would otherwise place `-Fattack` at
|
|
298
|
+
// the target position, where OpenSSH parses it as `-F <file>` and an
|
|
299
|
+
// attacker-supplied ssh config's ProxyCommand yields local code execution.
|
|
300
|
+
assertValidSshTarget(`${user}@${host}`);
|
|
295
301
|
const remoteCmd = buildLaunchCmd(remoteOs, browserType, port, customBinary);
|
|
296
302
|
return new Promise((resolve, reject) => {
|
|
297
303
|
// Capture stderr so a real launch failure (missing exe, auth denied, bad
|
|
@@ -362,7 +368,18 @@ export function killRemoteBrowser(user, host, remoteOs, port) {
|
|
|
362
368
|
return runSSHCommand(user, host, buildKillCmd(remoteOs, port));
|
|
363
369
|
}
|
|
364
370
|
function runSSHCommand(user, host, cmd) {
|
|
365
|
-
return new Promise((resolve) => {
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
// Reject an option-injecting target (e.g. a `-`-leading user from a crafted
|
|
373
|
+
// ssh:// profile) before it reaches the raw `ssh` spawn. Reject rather than
|
|
374
|
+
// throw synchronously so `killRemoteBrowser(...).catch()` stays best-effort.
|
|
375
|
+
// See ensureRemoteBrowser.
|
|
376
|
+
try {
|
|
377
|
+
assertValidSshTarget(`${user}@${host}`);
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
reject(err);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
366
383
|
// Reuse the shared hardened baseline so a hung TCP SYN to an unreachable
|
|
367
384
|
// host is bounded by ConnectTimeout (the local 3s kill below only bounds a
|
|
368
385
|
// connected-but-slow command, not the connect itself). Options precede the
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CommsMsg } from './mailbox.js';
|
|
2
|
+
export type { CommsMsg } from './mailbox.js';
|
|
3
|
+
export declare const GLYPH: {
|
|
4
|
+
readonly live: "●";
|
|
5
|
+
readonly idle: "○";
|
|
6
|
+
readonly ask: "▲";
|
|
7
|
+
readonly delivered: "✓";
|
|
8
|
+
readonly pending: "⏳";
|
|
9
|
+
readonly route: "→";
|
|
10
|
+
readonly stream: "─→";
|
|
11
|
+
readonly thread: "⇄";
|
|
12
|
+
};
|
|
13
|
+
export type Accent = 'cyan' | 'amber';
|
|
14
|
+
/** Render the shared one-line comms header, with `right` aligned to the terminal edge. */
|
|
15
|
+
export declare function masthead(o: {
|
|
16
|
+
title: string;
|
|
17
|
+
host: string;
|
|
18
|
+
accent: Accent;
|
|
19
|
+
right?: string;
|
|
20
|
+
stats?: string[];
|
|
21
|
+
}): string;
|
|
22
|
+
/** Normalize non-negative counts across the eight Unicode sparkline levels. */
|
|
23
|
+
export declare function sparkline(counts: number[]): string;
|
|
24
|
+
/** Flatten mailbox histories into one newest-first communication stream. */
|
|
25
|
+
export declare function aggregate(boxes: {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
messages: import('./mailbox.js').StoredMessage[];
|
|
29
|
+
}[]): CommsMsg[];
|
|
30
|
+
/** Count messages in rolling one-hour buckets, ordered oldest to newest. */
|
|
31
|
+
export declare function hourlyCounts(msgs: CommsMsg[], hours: number, now?: Date): number[];
|
|
32
|
+
/** Aggregate the human-readable sender-to-recipient routes, busiest first. */
|
|
33
|
+
export declare function graphEdges(msgs: CommsMsg[]): {
|
|
34
|
+
from: string;
|
|
35
|
+
to: string;
|
|
36
|
+
count: number;
|
|
37
|
+
}[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { stringWidth, terminalWidth } from './session/width.js';
|
|
3
|
+
export const GLYPH = {
|
|
4
|
+
live: '●',
|
|
5
|
+
idle: '○',
|
|
6
|
+
ask: '▲',
|
|
7
|
+
delivered: '✓',
|
|
8
|
+
pending: '⏳',
|
|
9
|
+
route: '→',
|
|
10
|
+
stream: '─→',
|
|
11
|
+
thread: '⇄',
|
|
12
|
+
};
|
|
13
|
+
/** Render the shared one-line comms header, with `right` aligned to the terminal edge. */
|
|
14
|
+
export function masthead(o) {
|
|
15
|
+
const accent = o.accent === 'cyan' ? chalk.cyan : chalk.yellow;
|
|
16
|
+
const title = `${accent('⌁')} ${chalk.bold(accent(o.title))}`;
|
|
17
|
+
const host = chalk.dim(` · ${o.host}`);
|
|
18
|
+
const stats = o.stats?.length ? ` ${o.stats.join(chalk.dim(' · '))}` : '';
|
|
19
|
+
const left = `${title}${host}${stats}`;
|
|
20
|
+
if (!o.right)
|
|
21
|
+
return left;
|
|
22
|
+
const right = chalk.dim(o.right);
|
|
23
|
+
const gap = Math.max(1, terminalWidth() - stringWidth(left) - stringWidth(right));
|
|
24
|
+
return `${left}${' '.repeat(gap)}${right}`;
|
|
25
|
+
}
|
|
26
|
+
const SPARK_LEVELS = '▁▂▃▄▅▆▇█';
|
|
27
|
+
/** Normalize non-negative counts across the eight Unicode sparkline levels. */
|
|
28
|
+
export function sparkline(counts) {
|
|
29
|
+
if (counts.length === 0)
|
|
30
|
+
return ' ';
|
|
31
|
+
const normalized = counts.map((count) => Number.isFinite(count) ? Math.max(0, count) : 0);
|
|
32
|
+
const max = Math.max(...normalized);
|
|
33
|
+
if (max === 0)
|
|
34
|
+
return ' '.repeat(counts.length);
|
|
35
|
+
return normalized
|
|
36
|
+
.map((count) => SPARK_LEVELS[Math.round((count / max) * (SPARK_LEVELS.length - 1))])
|
|
37
|
+
.join('');
|
|
38
|
+
}
|
|
39
|
+
/** Flatten mailbox histories into one newest-first communication stream. */
|
|
40
|
+
export function aggregate(boxes) {
|
|
41
|
+
return boxes
|
|
42
|
+
.flatMap((box) => {
|
|
43
|
+
const toLabel = box.label || box.id.slice(0, 8);
|
|
44
|
+
return box.messages.map((message) => ({
|
|
45
|
+
from: message.from || 'operator',
|
|
46
|
+
to: message.to,
|
|
47
|
+
toLabel,
|
|
48
|
+
ts: message.ts,
|
|
49
|
+
text: message.text,
|
|
50
|
+
state: message.state,
|
|
51
|
+
box: box.id,
|
|
52
|
+
}));
|
|
53
|
+
})
|
|
54
|
+
.sort((a, b) => compareText(b.ts, a.ts));
|
|
55
|
+
}
|
|
56
|
+
const HOUR_MS = 60 * 60 * 1_000;
|
|
57
|
+
/** Count messages in rolling one-hour buckets, ordered oldest to newest. */
|
|
58
|
+
export function hourlyCounts(msgs, hours, now = new Date()) {
|
|
59
|
+
const bucketCount = Number.isFinite(hours) ? Math.max(0, Math.floor(hours)) : 0;
|
|
60
|
+
const counts = Array.from({ length: bucketCount }, () => 0);
|
|
61
|
+
const nowMs = now.getTime();
|
|
62
|
+
if (bucketCount === 0 || !Number.isFinite(nowMs))
|
|
63
|
+
return counts;
|
|
64
|
+
for (const message of msgs) {
|
|
65
|
+
const timestamp = Date.parse(message.ts);
|
|
66
|
+
const age = nowMs - timestamp;
|
|
67
|
+
if (!Number.isFinite(timestamp) || age < 0 || age >= bucketCount * HOUR_MS)
|
|
68
|
+
continue;
|
|
69
|
+
const bucket = bucketCount - 1 - Math.floor(age / HOUR_MS);
|
|
70
|
+
counts[bucket]++;
|
|
71
|
+
}
|
|
72
|
+
return counts;
|
|
73
|
+
}
|
|
74
|
+
/** Aggregate the human-readable sender-to-recipient routes, busiest first. */
|
|
75
|
+
export function graphEdges(msgs) {
|
|
76
|
+
const edges = new Map();
|
|
77
|
+
for (const message of msgs) {
|
|
78
|
+
const key = JSON.stringify([message.from, message.toLabel]);
|
|
79
|
+
const existing = edges.get(key);
|
|
80
|
+
if (existing)
|
|
81
|
+
existing.count++;
|
|
82
|
+
else
|
|
83
|
+
edges.set(key, { from: message.from, to: message.toLabel, count: 1 });
|
|
84
|
+
}
|
|
85
|
+
return [...edges.values()].sort((a, b) => b.count - a.count || compareText(a.from, b.from) || compareText(a.to, b.to));
|
|
86
|
+
}
|
|
87
|
+
function compareText(a, b) {
|
|
88
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
89
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* needs a provider token (e.g. HCLOUD_TOKEN) in the environment. We inject it
|
|
12
12
|
* from a secrets bundle when one is configured (see `crabboxEnv`).
|
|
13
13
|
*/
|
|
14
|
+
import { type SecretsBundle } from '../secrets/bundles.js';
|
|
14
15
|
/** A crabbox machine as reported by `crabbox list --json`. */
|
|
15
16
|
export interface CrabboxBox {
|
|
16
17
|
/** Provider machine name, e.g. `crabbox-blue-hermit-1039689b`. */
|
|
@@ -29,6 +30,16 @@ export interface CrabboxBox {
|
|
|
29
30
|
class?: string;
|
|
30
31
|
/** True when running + bootstrap-complete. */
|
|
31
32
|
ready: boolean;
|
|
33
|
+
/** crabbox `keep` label — a kept box survives `crabbox cleanup` past its TTL. */
|
|
34
|
+
keep: boolean;
|
|
35
|
+
/** Unix seconds the box was created, or null when the label is absent. */
|
|
36
|
+
createdAt: number | null;
|
|
37
|
+
/** Unix seconds the lease expires, or null. */
|
|
38
|
+
expiresAt: number | null;
|
|
39
|
+
/** Unix seconds the box was last touched (reused / run against), or null. */
|
|
40
|
+
lastTouchedAt: number | null;
|
|
41
|
+
/** Idle-timeout window in seconds, or null. */
|
|
42
|
+
idleTimeoutSecs: number | null;
|
|
32
43
|
}
|
|
33
44
|
export interface CrabboxOptions {
|
|
34
45
|
/**
|
|
@@ -41,6 +52,38 @@ export interface CrabboxOptions {
|
|
|
41
52
|
}
|
|
42
53
|
/** Locate the crabbox binary, or throw an actionable error. */
|
|
43
54
|
export declare function findCrabbox(): string;
|
|
55
|
+
/**
|
|
56
|
+
* Env keys that mark a secrets bundle as usable for `--lease` — the provider
|
|
57
|
+
* tokens crabbox reads to reach a cloud API. Matching a bundle needs only its
|
|
58
|
+
* declared key NAMES; only the matched key's VALUE is ever injected (see
|
|
59
|
+
* `crabboxEnv`), so an auto-detected bundle can't leak its other secrets.
|
|
60
|
+
*/
|
|
61
|
+
export declare const LEASE_PROVIDER_TOKEN_KEYS: string[];
|
|
62
|
+
/** The first bundle that declares a provider token key, or undefined. Pure over `bundles`. */
|
|
63
|
+
export declare function pickLeaseBundleFromList(bundles: SecretsBundle[]): string | undefined;
|
|
64
|
+
/** A resolved lease bundle: its name, plus (auto-detect only) the exact keys to inject. */
|
|
65
|
+
export interface ResolvedLeaseBundle {
|
|
66
|
+
name: string;
|
|
67
|
+
/** When set (auto-detect), inject ONLY these keys — not the whole bundle. */
|
|
68
|
+
keys?: string[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The secrets bundle to feed crabbox, resolved in priority order:
|
|
72
|
+
* 1. `AGENTS_LEASE_SECRETS_BUNDLE` env var — explicit, no keychain
|
|
73
|
+
* 2. `lease.secretsBundle` config (set by `lease setup`) — explicit, no keychain
|
|
74
|
+
* 3. auto-detect: the first keychain bundle DECLARING a provider token key
|
|
75
|
+
*
|
|
76
|
+
* Tiers 1–2 are the frictionless steady state (env + config are plain, no keychain
|
|
77
|
+
* read). Tier 3 is a fallback that DOES read bundle metadata via `listBundles()`
|
|
78
|
+
* (one batched keychain unlock, ~7-day broker cache) — so it only runs when
|
|
79
|
+
* neither env nor config is set, and `crabboxEnv` memoizes the result for the
|
|
80
|
+
* process (it is called several times per lease, and we don't want a scan each
|
|
81
|
+
* time). Once `lease setup` persists the choice (tier 2), tier 3 never runs.
|
|
82
|
+
* Returns undefined when nothing matches — crabbox then falls back to `crabbox login`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveLeaseBundle(): ResolvedLeaseBundle | undefined;
|
|
85
|
+
/** Persist `lease.secretsBundle` in agents config so `--lease` needs no env var. */
|
|
86
|
+
export declare function setLeaseSecretsBundle(name: string): void;
|
|
44
87
|
/** Build the child env for crabbox, injecting a secrets bundle when configured. */
|
|
45
88
|
export declare function crabboxEnv(opts: CrabboxOptions): NodeJS.ProcessEnv;
|
|
46
89
|
/** All crabbox machines the broker knows about. */
|
|
@@ -96,3 +139,32 @@ export declare function crabboxRun(slug: string, remoteCmd: string, opts?: Crabb
|
|
|
96
139
|
export declare function crabboxRunScript(slug: string, script: string, opts?: CrabboxRunOptions): Promise<number | null>;
|
|
97
140
|
/** Release the lease / delete the box. Best-effort; never throws. */
|
|
98
141
|
export declare function crabboxStop(slug: string, opts?: CrabboxOptions): boolean;
|
|
142
|
+
/** Never reap a box touched within this many seconds, regardless of idle-timeout. */
|
|
143
|
+
export declare const REAP_MIN_IDLE_SECS = 3600;
|
|
144
|
+
/**
|
|
145
|
+
* Whether a box is a genuine orphan that is safe to reap.
|
|
146
|
+
*
|
|
147
|
+
* Reap-safe ONLY when BOTH hold: the lease has already expired (`expiresAt` in the
|
|
148
|
+
* past) AND the box has not been touched for a safety window of
|
|
149
|
+
* `max(2 × idleTimeout, 1h)`. The freshness guard is what makes this safe against
|
|
150
|
+
* a TOCTOU race: a box a concurrent run just reused (`cbx_acquire_box`) has a
|
|
151
|
+
* recent `lastTouchedAt` and is never eligible. Reaping by `profile`/`ready` alone
|
|
152
|
+
* — as `crabbox cleanup` cannot (it skips `keep=true`, which every real orphan is)
|
|
153
|
+
* — would kill in-use boxes. Boxes with unknown age (`expiresAt`/`lastTouchedAt`
|
|
154
|
+
* null) are never reaped. `nowSecs` is injected so tests don't wall-clock.
|
|
155
|
+
*/
|
|
156
|
+
export declare function isReapSafe(box: CrabboxBox, nowSecs: number): boolean;
|
|
157
|
+
/** The reap-safe orphans among `boxes`, most-stale (oldest touch) first. */
|
|
158
|
+
export declare function reapSafeOrphans(boxes: CrabboxBox[], nowSecs: number): CrabboxBox[];
|
|
159
|
+
/**
|
|
160
|
+
* List reap-safe orphans and (unless `dryRun`) stop them. Returns the candidates
|
|
161
|
+
* considered and the slugs actually stopped. Best-effort per box — a stop failure
|
|
162
|
+
* is skipped, never thrown. Backs `agents lease gc` and the 403 auto-reap opt-in.
|
|
163
|
+
*/
|
|
164
|
+
export declare function reapOrphans(opts?: CrabboxOptions & {
|
|
165
|
+
nowSecs?: number;
|
|
166
|
+
dryRun?: boolean;
|
|
167
|
+
}): {
|
|
168
|
+
candidates: CrabboxBox[];
|
|
169
|
+
reaped: string[];
|
|
170
|
+
};
|
package/dist/lib/crabbox/cli.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* from a secrets bundle when one is configured (see `crabboxEnv`).
|
|
13
13
|
*/
|
|
14
14
|
import { spawn, spawnSync } from 'child_process';
|
|
15
|
-
import { readAndResolveBundleEnv } from '../secrets/bundles.js';
|
|
15
|
+
import { readAndResolveBundleEnv, listBundles, bundleExists } from '../secrets/bundles.js';
|
|
16
|
+
import { readMeta, writeMeta } from '../state.js';
|
|
16
17
|
/** Locate the crabbox binary, or throw an actionable error. */
|
|
17
18
|
export function findCrabbox() {
|
|
18
19
|
const r = spawnSync('crabbox', ['--help'], { encoding: 'utf-8' });
|
|
@@ -21,21 +22,95 @@ export function findCrabbox() {
|
|
|
21
22
|
}
|
|
22
23
|
return 'crabbox';
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Env keys that mark a secrets bundle as usable for `--lease` — the provider
|
|
27
|
+
* tokens crabbox reads to reach a cloud API. Matching a bundle needs only its
|
|
28
|
+
* declared key NAMES; only the matched key's VALUE is ever injected (see
|
|
29
|
+
* `crabboxEnv`), so an auto-detected bundle can't leak its other secrets.
|
|
30
|
+
*/
|
|
31
|
+
export const LEASE_PROVIDER_TOKEN_KEYS = ['HCLOUD_TOKEN', 'AWS_ACCESS_KEY_ID', 'DIGITALOCEAN_TOKEN', 'DO_TOKEN'];
|
|
32
|
+
/** The first bundle that declares a provider token key, or undefined. Pure over `bundles`. */
|
|
33
|
+
export function pickLeaseBundleFromList(bundles) {
|
|
34
|
+
for (const b of bundles) {
|
|
35
|
+
if (Object.keys(b.vars ?? {}).some((k) => LEASE_PROVIDER_TOKEN_KEYS.includes(k)))
|
|
36
|
+
return b.name;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The secrets bundle to feed crabbox, resolved in priority order:
|
|
42
|
+
* 1. `AGENTS_LEASE_SECRETS_BUNDLE` env var — explicit, no keychain
|
|
43
|
+
* 2. `lease.secretsBundle` config (set by `lease setup`) — explicit, no keychain
|
|
44
|
+
* 3. auto-detect: the first keychain bundle DECLARING a provider token key
|
|
45
|
+
*
|
|
46
|
+
* Tiers 1–2 are the frictionless steady state (env + config are plain, no keychain
|
|
47
|
+
* read). Tier 3 is a fallback that DOES read bundle metadata via `listBundles()`
|
|
48
|
+
* (one batched keychain unlock, ~7-day broker cache) — so it only runs when
|
|
49
|
+
* neither env nor config is set, and `crabboxEnv` memoizes the result for the
|
|
50
|
+
* process (it is called several times per lease, and we don't want a scan each
|
|
51
|
+
* time). Once `lease setup` persists the choice (tier 2), tier 3 never runs.
|
|
52
|
+
* Returns undefined when nothing matches — crabbox then falls back to `crabbox login`.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveLeaseBundle() {
|
|
55
|
+
const env = process.env.AGENTS_LEASE_SECRETS_BUNDLE;
|
|
56
|
+
if (env)
|
|
57
|
+
return { name: env };
|
|
58
|
+
try {
|
|
59
|
+
const configured = readMeta().lease?.secretsBundle;
|
|
60
|
+
if (configured && bundleExists(configured))
|
|
61
|
+
return { name: configured };
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* config unreadable — fall through to auto-detect */
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const bundles = listBundles();
|
|
68
|
+
const name = pickLeaseBundleFromList(bundles);
|
|
69
|
+
if (name) {
|
|
70
|
+
const b = bundles.find((x) => x.name === name);
|
|
71
|
+
const keys = LEASE_PROVIDER_TOKEN_KEYS.filter((k) => !!b && k in (b.vars ?? {}));
|
|
72
|
+
return { name, keys };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* secrets unreadable — no auto-detect */
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/** Process-lifetime memo so the tier-3 `listBundles()` scan runs at most once. */
|
|
81
|
+
let leaseBundleMemo;
|
|
82
|
+
function resolveLeaseBundleMemo() {
|
|
83
|
+
if (!leaseBundleMemo)
|
|
84
|
+
leaseBundleMemo = { value: resolveLeaseBundle() };
|
|
85
|
+
return leaseBundleMemo.value;
|
|
86
|
+
}
|
|
87
|
+
/** Persist `lease.secretsBundle` in agents config so `--lease` needs no env var. */
|
|
88
|
+
export function setLeaseSecretsBundle(name) {
|
|
89
|
+
const meta = readMeta();
|
|
90
|
+
writeMeta({ ...meta, lease: { ...meta.lease, secretsBundle: name } });
|
|
91
|
+
leaseBundleMemo = undefined; // invalidate so the next resolve sees the new config
|
|
92
|
+
}
|
|
24
93
|
/** Build the child env for crabbox, injecting a secrets bundle when configured. */
|
|
25
94
|
export function crabboxEnv(opts) {
|
|
26
|
-
const
|
|
27
|
-
|
|
95
|
+
const resolved = opts.secretsBundle
|
|
96
|
+
? { name: opts.secretsBundle }
|
|
97
|
+
: resolveLeaseBundleMemo();
|
|
98
|
+
if (!resolved)
|
|
28
99
|
return process.env;
|
|
29
100
|
try {
|
|
30
|
-
//
|
|
31
|
-
// bundle
|
|
32
|
-
//
|
|
33
|
-
|
|
101
|
+
// Auto-detected bundle → inject ONLY the provider token key(s) (least
|
|
102
|
+
// privilege; an unrelated bundle can't leak its other secrets into crabbox).
|
|
103
|
+
// An explicitly-named bundle (env/config or `opts.secretsBundle`) injects
|
|
104
|
+
// whole — the user chose it. Same resolver `agents secrets exec` uses.
|
|
105
|
+
const { env } = readAndResolveBundleEnv(resolved.name, {
|
|
106
|
+
caller: 'agents run --lease (crabbox)',
|
|
107
|
+
keys: resolved.keys,
|
|
108
|
+
});
|
|
34
109
|
return { ...process.env, ...env };
|
|
35
110
|
}
|
|
36
111
|
catch (e) {
|
|
37
|
-
throw new Error(`Could not load secrets bundle "${
|
|
38
|
-
`Fix the bundle (agents secrets view ${
|
|
112
|
+
throw new Error(`Could not load secrets bundle "${resolved.name}" for crabbox: ${e.message}. ` +
|
|
113
|
+
`Fix the bundle (agents secrets view ${resolved.name}) or unset lease.secretsBundle to use crabbox's own login.`);
|
|
39
114
|
}
|
|
40
115
|
}
|
|
41
116
|
function normalizeBox(raw) {
|
|
@@ -46,6 +121,12 @@ function normalizeBox(raw) {
|
|
|
46
121
|
const status = String(raw.status ?? '');
|
|
47
122
|
const state = String(labels.state ?? '');
|
|
48
123
|
const publicNet = (raw.public_net ?? {});
|
|
124
|
+
const num = (v) => {
|
|
125
|
+
if (v === undefined || v === '')
|
|
126
|
+
return null;
|
|
127
|
+
const n = Number(v);
|
|
128
|
+
return Number.isFinite(n) ? n : null;
|
|
129
|
+
};
|
|
49
130
|
return {
|
|
50
131
|
name: String(raw.name ?? ''),
|
|
51
132
|
status,
|
|
@@ -56,6 +137,11 @@ function normalizeBox(raw) {
|
|
|
56
137
|
profile: labels.profile,
|
|
57
138
|
class: labels.class,
|
|
58
139
|
ready: status === 'running' && state === 'ready',
|
|
140
|
+
keep: labels.keep === 'true',
|
|
141
|
+
createdAt: num(labels.created_at),
|
|
142
|
+
expiresAt: num(labels.expires_at),
|
|
143
|
+
lastTouchedAt: num(labels.last_touched_at),
|
|
144
|
+
idleTimeoutSecs: num(labels.idle_timeout_secs ?? labels.idle_timeout),
|
|
59
145
|
};
|
|
60
146
|
}
|
|
61
147
|
/** All crabbox machines the broker knows about. */
|
|
@@ -113,6 +199,24 @@ export async function crabboxWarmup(opts = {}) {
|
|
|
113
199
|
});
|
|
114
200
|
if (r.status !== 0) {
|
|
115
201
|
const detail = (r.stderr || r.stdout || '').trim();
|
|
202
|
+
// A provider `server_limit` / `resource_limit_exceeded` 403 means the account's
|
|
203
|
+
// box quota is full. Turn the raw 403 into an actionable message that names the
|
|
204
|
+
// reap-safe orphans + the one-command fix, instead of a generic failure.
|
|
205
|
+
if (/server_limit|resource_limit_exceeded/i.test(detail)) {
|
|
206
|
+
let hint = ' Stop unused boxes (`crabbox list`) or raise your provider server limit.';
|
|
207
|
+
try {
|
|
208
|
+
const orphans = reapSafeOrphans(crabboxList(opts), Math.floor(Date.now() / 1000));
|
|
209
|
+
if (orphans.length) {
|
|
210
|
+
hint =
|
|
211
|
+
` ${orphans.length} expired, idle box(es) are holding the quota — free them with ` +
|
|
212
|
+
`\`agents lease gc\` (or \`crabbox stop ${orphans[0].slug}\`).`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
/* best-effort hint; fall back to the generic guidance above */
|
|
217
|
+
}
|
|
218
|
+
throw new Error(`crabbox warmup failed: provider server limit reached.${hint}`);
|
|
219
|
+
}
|
|
116
220
|
throw new Error(`crabbox warmup failed: ${detail || 'unknown error'}. ` +
|
|
117
221
|
`Check provider access with \`crabbox doctor\`; a missing cloud token often means \`crabbox login\` or a lease.secretsBundle is needed.`);
|
|
118
222
|
}
|
|
@@ -229,3 +333,48 @@ export function crabboxStop(slug, opts = {}) {
|
|
|
229
333
|
return false;
|
|
230
334
|
}
|
|
231
335
|
}
|
|
336
|
+
/** Never reap a box touched within this many seconds, regardless of idle-timeout. */
|
|
337
|
+
export const REAP_MIN_IDLE_SECS = 3600;
|
|
338
|
+
/**
|
|
339
|
+
* Whether a box is a genuine orphan that is safe to reap.
|
|
340
|
+
*
|
|
341
|
+
* Reap-safe ONLY when BOTH hold: the lease has already expired (`expiresAt` in the
|
|
342
|
+
* past) AND the box has not been touched for a safety window of
|
|
343
|
+
* `max(2 × idleTimeout, 1h)`. The freshness guard is what makes this safe against
|
|
344
|
+
* a TOCTOU race: a box a concurrent run just reused (`cbx_acquire_box`) has a
|
|
345
|
+
* recent `lastTouchedAt` and is never eligible. Reaping by `profile`/`ready` alone
|
|
346
|
+
* — as `crabbox cleanup` cannot (it skips `keep=true`, which every real orphan is)
|
|
347
|
+
* — would kill in-use boxes. Boxes with unknown age (`expiresAt`/`lastTouchedAt`
|
|
348
|
+
* null) are never reaped. `nowSecs` is injected so tests don't wall-clock.
|
|
349
|
+
*/
|
|
350
|
+
export function isReapSafe(box, nowSecs) {
|
|
351
|
+
if (box.expiresAt === null || box.lastTouchedAt === null)
|
|
352
|
+
return false;
|
|
353
|
+
if (box.expiresAt > nowSecs)
|
|
354
|
+
return false;
|
|
355
|
+
const window = Math.max((box.idleTimeoutSecs ?? 0) * 2, REAP_MIN_IDLE_SECS);
|
|
356
|
+
return nowSecs - box.lastTouchedAt >= window;
|
|
357
|
+
}
|
|
358
|
+
/** The reap-safe orphans among `boxes`, most-stale (oldest touch) first. */
|
|
359
|
+
export function reapSafeOrphans(boxes, nowSecs) {
|
|
360
|
+
return boxes
|
|
361
|
+
.filter((b) => isReapSafe(b, nowSecs))
|
|
362
|
+
.sort((a, b) => (a.lastTouchedAt ?? 0) - (b.lastTouchedAt ?? 0));
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* List reap-safe orphans and (unless `dryRun`) stop them. Returns the candidates
|
|
366
|
+
* considered and the slugs actually stopped. Best-effort per box — a stop failure
|
|
367
|
+
* is skipped, never thrown. Backs `agents lease gc` and the 403 auto-reap opt-in.
|
|
368
|
+
*/
|
|
369
|
+
export function reapOrphans(opts = {}) {
|
|
370
|
+
const nowSecs = opts.nowSecs ?? Math.floor(Date.now() / 1000);
|
|
371
|
+
const candidates = reapSafeOrphans(crabboxList(opts), nowSecs);
|
|
372
|
+
if (opts.dryRun)
|
|
373
|
+
return { candidates, reaped: [] };
|
|
374
|
+
const reaped = [];
|
|
375
|
+
for (const b of candidates) {
|
|
376
|
+
if (crabboxStop(b.slug, opts))
|
|
377
|
+
reaped.push(b.slug);
|
|
378
|
+
}
|
|
379
|
+
return { candidates, reaped };
|
|
380
|
+
}
|
|
@@ -47,6 +47,19 @@ export declare function pickRuntimes(detected: DetectedRuntime[], prompt?: (choi
|
|
|
47
47
|
checked: boolean;
|
|
48
48
|
disabled: boolean | string;
|
|
49
49
|
}[]) => Promise<AgentId[]>): Promise<AgentId[]>;
|
|
50
|
+
/**
|
|
51
|
+
* The lease runtime to provision for a headless run of `agentName`.
|
|
52
|
+
*
|
|
53
|
+
* When the agent is itself a lease-capable runtime (claude/codex/gemini/grok)
|
|
54
|
+
* that IS the runtime to install. Otherwise fall back to the single signed-in
|
|
55
|
+
* lease runtime (preferring claude), or null when none is signed in. This is the
|
|
56
|
+
* non-interactive replacement for the runtime checkbox picker: `--lease` requires
|
|
57
|
+
* a prompt, so it is headless by contract and must never block on a TTY.
|
|
58
|
+
*
|
|
59
|
+
* Profile-dispatch agents (kimi/deepseek) and custom workflow agents that run
|
|
60
|
+
* under a non-obvious runtime are resolved separately — see RUSH-1725.
|
|
61
|
+
*/
|
|
62
|
+
export declare function inferLeaseRuntime(agentName: string, detected: DetectedRuntime[]): AgentId | null;
|
|
50
63
|
/**
|
|
51
64
|
* Where Claude Code reads its OAuth token on the box. `.claude.json` (the file
|
|
52
65
|
* LEASE_RUNTIMES copies) is config/account-metadata ONLY — the actual token
|