@phnx-labs/agents-cli 1.20.63 → 1.20.65
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 +56 -3
- package/README.md +46 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- 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/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +36 -0
- package/dist/lib/agents.js +80 -20
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- 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/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- 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 +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -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/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -11,6 +11,7 @@ import { RushCloudProvider } from './rush.js';
|
|
|
11
11
|
import { CodexCloudProvider } from './codex.js';
|
|
12
12
|
import { FactoryCloudProvider } from './factory.js';
|
|
13
13
|
import { AntigravityCloudProvider } from './antigravity.js';
|
|
14
|
+
import { HostCloudProvider } from './host.js';
|
|
14
15
|
import { getUserAgentsDir } from '../state.js';
|
|
15
16
|
import { AGENTS } from '../agents.js';
|
|
16
17
|
const META_FILE = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
@@ -43,6 +44,9 @@ function initProviders() {
|
|
|
43
44
|
providers.set('codex', new CodexCloudProvider(config.providers?.codex));
|
|
44
45
|
providers.set('factory', new FactoryCloudProvider(config.providers?.factory));
|
|
45
46
|
providers.set('antigravity', new AntigravityCloudProvider(config.providers?.antigravity));
|
|
47
|
+
// Your own machines (agents hosts / agents devices) over SSH. No agent
|
|
48
|
+
// auto-routes here — it's always an explicit --provider host / --host choice.
|
|
49
|
+
providers.set('host', new HostCloudProvider());
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* The cloud provider an agent dispatches to by default. Reads the canonical
|
|
@@ -9,16 +9,18 @@ import type { JobTrigger } from '../routines.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Identifier for a supported cloud dispatch backend.
|
|
11
11
|
*
|
|
12
|
-
* Each id is one agent's *own* cloud:
|
|
12
|
+
* Each id is one agent's *own* cloud — plus your own machines:
|
|
13
13
|
* - `rush` — Rush Cloud (runs Claude against a GitHub repo → PR)
|
|
14
14
|
* - `codex` — OpenAI Codex Cloud (`codex cloud exec`)
|
|
15
15
|
* - `factory` — Factory Droid Computers (`droid computer ssh` + remote `droid exec`)
|
|
16
16
|
* - `antigravity` — Google Gemini Managed Agents (Interactions API)
|
|
17
|
+
* - `host` — a machine you own (`agents hosts` / `agents devices`), over SSH
|
|
17
18
|
*
|
|
18
19
|
* Agents route to their native cloud automatically (see `cloudProvider` in the
|
|
19
|
-
* agent registry); `--provider` overrides.
|
|
20
|
+
* agent registry); `--provider` overrides. Nothing auto-routes to `host` — it
|
|
21
|
+
* is always an explicit `--provider host` (or `--host <name>`) choice.
|
|
20
22
|
*/
|
|
21
|
-
export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity';
|
|
23
|
+
export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity' | 'host';
|
|
22
24
|
/**
|
|
23
25
|
* Lifecycle state of a cloud-dispatched task.
|
|
24
26
|
*
|
|
@@ -219,7 +221,7 @@ export interface CloudTarget {
|
|
|
219
221
|
kind: TargetKind;
|
|
220
222
|
}
|
|
221
223
|
/** Which dispatch option a provider's pre-provisioned target maps to. */
|
|
222
|
-
export type TargetKind = 'env' | 'computer';
|
|
224
|
+
export type TargetKind = 'env' | 'computer' | 'host';
|
|
223
225
|
/**
|
|
224
226
|
* Thrown by a provider's `dispatch()` when it needs a pre-provisioned target
|
|
225
227
|
* (Codex env / Factory computer) and none was supplied. The CLI catches this
|
|
@@ -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
|
+
}
|
package/dist/lib/computer-rpc.js
CHANGED
|
@@ -341,7 +341,9 @@ class TcpClient extends BaseClient {
|
|
|
341
341
|
this.failPending('helper_exited', 'tcp connection closed before reply');
|
|
342
342
|
});
|
|
343
343
|
// Kick off the auth handshake synchronously so its frame (id 1) is the
|
|
344
|
-
// first thing written.
|
|
344
|
+
// first thing written. The Windows daemon now REQUIRES a token (it refuses to
|
|
345
|
+
// start without one); a null token here means no auth frame is sent, the
|
|
346
|
+
// daemon drops the connection, and the caller is told to re-run `setup`.
|
|
345
347
|
this.authReady = token ? this.authenticate(token) : Promise.resolve();
|
|
346
348
|
}
|
|
347
349
|
async authenticate(token) {
|
|
@@ -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, isHeadlessSecretsContext, 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,99 @@ 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
|
+
// --lease is headless by contract and crabboxEnv is called several times
|
|
109
|
+
// per run (list/wait/spawn/stop) — resolve broker-only so a keychain bundle
|
|
110
|
+
// can't pop repeated unwatched Touch ID sheets mid-lease.
|
|
111
|
+
agentOnly: isHeadlessSecretsContext(),
|
|
112
|
+
});
|
|
34
113
|
return { ...process.env, ...env };
|
|
35
114
|
}
|
|
36
115
|
catch (e) {
|
|
37
|
-
throw new Error(`Could not load secrets bundle "${
|
|
38
|
-
`Fix the bundle (agents secrets view ${
|
|
116
|
+
throw new Error(`Could not load secrets bundle "${resolved.name}" for crabbox: ${e.message}. ` +
|
|
117
|
+
`Fix the bundle (agents secrets view ${resolved.name}) or unset lease.secretsBundle to use crabbox's own login.`);
|
|
39
118
|
}
|
|
40
119
|
}
|
|
41
120
|
function normalizeBox(raw) {
|
|
@@ -46,6 +125,12 @@ function normalizeBox(raw) {
|
|
|
46
125
|
const status = String(raw.status ?? '');
|
|
47
126
|
const state = String(labels.state ?? '');
|
|
48
127
|
const publicNet = (raw.public_net ?? {});
|
|
128
|
+
const num = (v) => {
|
|
129
|
+
if (v === undefined || v === '')
|
|
130
|
+
return null;
|
|
131
|
+
const n = Number(v);
|
|
132
|
+
return Number.isFinite(n) ? n : null;
|
|
133
|
+
};
|
|
49
134
|
return {
|
|
50
135
|
name: String(raw.name ?? ''),
|
|
51
136
|
status,
|
|
@@ -56,6 +141,11 @@ function normalizeBox(raw) {
|
|
|
56
141
|
profile: labels.profile,
|
|
57
142
|
class: labels.class,
|
|
58
143
|
ready: status === 'running' && state === 'ready',
|
|
144
|
+
keep: labels.keep === 'true',
|
|
145
|
+
createdAt: num(labels.created_at),
|
|
146
|
+
expiresAt: num(labels.expires_at),
|
|
147
|
+
lastTouchedAt: num(labels.last_touched_at),
|
|
148
|
+
idleTimeoutSecs: num(labels.idle_timeout_secs ?? labels.idle_timeout),
|
|
59
149
|
};
|
|
60
150
|
}
|
|
61
151
|
/** All crabbox machines the broker knows about. */
|
|
@@ -113,6 +203,24 @@ export async function crabboxWarmup(opts = {}) {
|
|
|
113
203
|
});
|
|
114
204
|
if (r.status !== 0) {
|
|
115
205
|
const detail = (r.stderr || r.stdout || '').trim();
|
|
206
|
+
// A provider `server_limit` / `resource_limit_exceeded` 403 means the account's
|
|
207
|
+
// box quota is full. Turn the raw 403 into an actionable message that names the
|
|
208
|
+
// reap-safe orphans + the one-command fix, instead of a generic failure.
|
|
209
|
+
if (/server_limit|resource_limit_exceeded/i.test(detail)) {
|
|
210
|
+
let hint = ' Stop unused boxes (`crabbox list`) or raise your provider server limit.';
|
|
211
|
+
try {
|
|
212
|
+
const orphans = reapSafeOrphans(crabboxList(opts), Math.floor(Date.now() / 1000));
|
|
213
|
+
if (orphans.length) {
|
|
214
|
+
hint =
|
|
215
|
+
` ${orphans.length} expired, idle box(es) are holding the quota — free them with ` +
|
|
216
|
+
`\`agents lease gc\` (or \`crabbox stop ${orphans[0].slug}\`).`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
/* best-effort hint; fall back to the generic guidance above */
|
|
221
|
+
}
|
|
222
|
+
throw new Error(`crabbox warmup failed: provider server limit reached.${hint}`);
|
|
223
|
+
}
|
|
116
224
|
throw new Error(`crabbox warmup failed: ${detail || 'unknown error'}. ` +
|
|
117
225
|
`Check provider access with \`crabbox doctor\`; a missing cloud token often means \`crabbox login\` or a lease.secretsBundle is needed.`);
|
|
118
226
|
}
|
|
@@ -229,3 +337,48 @@ export function crabboxStop(slug, opts = {}) {
|
|
|
229
337
|
return false;
|
|
230
338
|
}
|
|
231
339
|
}
|
|
340
|
+
/** Never reap a box touched within this many seconds, regardless of idle-timeout. */
|
|
341
|
+
export const REAP_MIN_IDLE_SECS = 3600;
|
|
342
|
+
/**
|
|
343
|
+
* Whether a box is a genuine orphan that is safe to reap.
|
|
344
|
+
*
|
|
345
|
+
* Reap-safe ONLY when BOTH hold: the lease has already expired (`expiresAt` in the
|
|
346
|
+
* past) AND the box has not been touched for a safety window of
|
|
347
|
+
* `max(2 × idleTimeout, 1h)`. The freshness guard is what makes this safe against
|
|
348
|
+
* a TOCTOU race: a box a concurrent run just reused (`cbx_acquire_box`) has a
|
|
349
|
+
* recent `lastTouchedAt` and is never eligible. Reaping by `profile`/`ready` alone
|
|
350
|
+
* — as `crabbox cleanup` cannot (it skips `keep=true`, which every real orphan is)
|
|
351
|
+
* — would kill in-use boxes. Boxes with unknown age (`expiresAt`/`lastTouchedAt`
|
|
352
|
+
* null) are never reaped. `nowSecs` is injected so tests don't wall-clock.
|
|
353
|
+
*/
|
|
354
|
+
export function isReapSafe(box, nowSecs) {
|
|
355
|
+
if (box.expiresAt === null || box.lastTouchedAt === null)
|
|
356
|
+
return false;
|
|
357
|
+
if (box.expiresAt > nowSecs)
|
|
358
|
+
return false;
|
|
359
|
+
const window = Math.max((box.idleTimeoutSecs ?? 0) * 2, REAP_MIN_IDLE_SECS);
|
|
360
|
+
return nowSecs - box.lastTouchedAt >= window;
|
|
361
|
+
}
|
|
362
|
+
/** The reap-safe orphans among `boxes`, most-stale (oldest touch) first. */
|
|
363
|
+
export function reapSafeOrphans(boxes, nowSecs) {
|
|
364
|
+
return boxes
|
|
365
|
+
.filter((b) => isReapSafe(b, nowSecs))
|
|
366
|
+
.sort((a, b) => (a.lastTouchedAt ?? 0) - (b.lastTouchedAt ?? 0));
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* List reap-safe orphans and (unless `dryRun`) stop them. Returns the candidates
|
|
370
|
+
* considered and the slugs actually stopped. Best-effort per box — a stop failure
|
|
371
|
+
* is skipped, never thrown. Backs `agents lease gc` and the 403 auto-reap opt-in.
|
|
372
|
+
*/
|
|
373
|
+
export function reapOrphans(opts = {}) {
|
|
374
|
+
const nowSecs = opts.nowSecs ?? Math.floor(Date.now() / 1000);
|
|
375
|
+
const candidates = reapSafeOrphans(crabboxList(opts), nowSecs);
|
|
376
|
+
if (opts.dryRun)
|
|
377
|
+
return { candidates, reaped: [] };
|
|
378
|
+
const reaped = [];
|
|
379
|
+
for (const b of candidates) {
|
|
380
|
+
if (crabboxStop(b.slug, opts))
|
|
381
|
+
reaped.push(b.slug);
|
|
382
|
+
}
|
|
383
|
+
return { candidates, reaped };
|
|
384
|
+
}
|
|
@@ -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
|
|
@@ -79,6 +79,30 @@ export async function pickRuntimes(detected, prompt) {
|
|
|
79
79
|
const { checkbox } = await import('@inquirer/prompts');
|
|
80
80
|
return checkbox({ message: 'Provision which runtime(s) on the leased box?', choices });
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The lease runtime to provision for a headless run of `agentName`.
|
|
84
|
+
*
|
|
85
|
+
* When the agent is itself a lease-capable runtime (claude/codex/gemini/grok)
|
|
86
|
+
* that IS the runtime to install. Otherwise fall back to the single signed-in
|
|
87
|
+
* lease runtime (preferring claude), or null when none is signed in. This is the
|
|
88
|
+
* non-interactive replacement for the runtime checkbox picker: `--lease` requires
|
|
89
|
+
* a prompt, so it is headless by contract and must never block on a TTY.
|
|
90
|
+
*
|
|
91
|
+
* Profile-dispatch agents (kimi/deepseek) and custom workflow agents that run
|
|
92
|
+
* under a non-obvious runtime are resolved separately — see RUSH-1725.
|
|
93
|
+
*/
|
|
94
|
+
export function inferLeaseRuntime(agentName, detected) {
|
|
95
|
+
const signedIn = detected.filter((d) => d.signedIn && d.credPath);
|
|
96
|
+
// The agent names a lease runtime directly: require that runtime to be signed
|
|
97
|
+
// in — never silently substitute a different one for an explicit `run <runtime>`
|
|
98
|
+
// (that would lease a billable box only to boot it "Not logged in"). Not signed
|
|
99
|
+
// in → null, so the caller exits with "sign into it locally first".
|
|
100
|
+
if (LEASE_RUNTIMES.some((c) => c.id === agentName)) {
|
|
101
|
+
return signedIn.find((d) => d.id === agentName)?.id ?? null;
|
|
102
|
+
}
|
|
103
|
+
// Custom/workflow agent: fall back to the signed-in runtime (preferring claude).
|
|
104
|
+
return signedIn.find((d) => d.id === 'claude')?.id ?? signedIn[0]?.id ?? null;
|
|
105
|
+
}
|
|
82
106
|
// A long random sentinel makes an accidental (or malicious) collision with a
|
|
83
107
|
// token's contents effectively impossible, so the quoted heredoc can never be
|
|
84
108
|
// closed early by the credential body.
|
|
@@ -134,8 +158,17 @@ export async function resolveClaudeCredentialsBlob(opts) {
|
|
|
134
158
|
if (process.platform === 'darwin') {
|
|
135
159
|
// 1) Bare service — the default native (non-managed) install.
|
|
136
160
|
const bare = tryRead(service(undefined));
|
|
137
|
-
if (bare)
|
|
138
|
-
|
|
161
|
+
if (bare) {
|
|
162
|
+
if (!opts?.preferEmail)
|
|
163
|
+
return bare;
|
|
164
|
+
// preferEmail is set — verify the bare service belongs to the right account
|
|
165
|
+
// before handing it back; on mismatch fall through to managed installs.
|
|
166
|
+
const realHome = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
167
|
+
const bareEmail = await accountEmail(realHome).catch(() => null);
|
|
168
|
+
if (bareEmail === opts.preferEmail)
|
|
169
|
+
return bare;
|
|
170
|
+
// email mismatch — fall through
|
|
171
|
+
}
|
|
139
172
|
// 2) Managed installs — hash-suffixed service keyed to each version home.
|
|
140
173
|
// Prefer the version whose account email matches the copied config.
|
|
141
174
|
let homes;
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -81,10 +81,26 @@ export declare function readDaemonClaudeOAuthToken(opts?: {
|
|
|
81
81
|
* embed long-lived credentials.
|
|
82
82
|
*/
|
|
83
83
|
export declare function writeOwnerOnlyServiceManifest(filePath: string, content: string): void;
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Generate a macOS launchd plist for auto-starting the daemon.
|
|
86
|
+
*
|
|
87
|
+
* The plist never embeds the Claude OAuth token (RUSH-1759): a persisted service
|
|
88
|
+
* manifest is a plaintext credential on disk even at 0600. The daemon instead
|
|
89
|
+
* obtains the token at startup from the `claude` secrets bundle
|
|
90
|
+
* (readDaemonClaudeOAuthToken, injected in runDaemon), so it stays in the
|
|
91
|
+
* Keychain-backed secure store and never touches the unit file.
|
|
92
|
+
*/
|
|
93
|
+
export declare function generateLaunchdPlist(agentsBin?: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Generate a Linux systemd user unit for auto-starting the daemon.
|
|
96
|
+
*
|
|
97
|
+
* The unit never embeds the Claude OAuth token (RUSH-1759): a persisted service
|
|
98
|
+
* manifest is a plaintext credential on disk even at 0600. The daemon instead
|
|
99
|
+
* obtains the token at startup from the `claude` secrets bundle
|
|
100
|
+
* (readDaemonClaudeOAuthToken, injected in runDaemon), so it stays in the secure
|
|
101
|
+
* store and never touches the unit file.
|
|
102
|
+
*/
|
|
103
|
+
export declare function generateSystemdUnit(agentsBin?: string): string;
|
|
88
104
|
export declare function getAgentsBinPath(argv1?: string | undefined, execPath?: string): string;
|
|
89
105
|
/** Start the daemon via launchd, systemd, or as a detached process. */
|
|
90
106
|
export declare function startDaemon(agentsBin?: string): {
|