@phnx-labs/agents-cli 1.22.7 → 1.22.9
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 +97 -0
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +61 -0
- package/dist/commands/exec.js +89 -21
- package/dist/commands/feed.js +38 -1
- package/dist/commands/harness.d.ts +40 -2
- package/dist/commands/harness.js +316 -20
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/profiles.d.ts +42 -0
- package/dist/commands/profiles.js +91 -3
- package/dist/commands/routines.js +2 -2
- package/dist/commands/run-account-picker.js +2 -0
- package/dist/commands/sessions-picker.js +3 -3
- package/dist/commands/sessions-render.d.ts +12 -0
- package/dist/commands/sessions-render.js +124 -0
- package/dist/commands/sessions.js +28 -1
- package/dist/commands/snapshot.d.ts +11 -0
- package/dist/commands/snapshot.js +107 -0
- package/dist/commands/ssh.js +26 -3
- package/dist/commands/teams.js +14 -2
- package/dist/commands/view.d.ts +7 -0
- package/dist/commands/view.js +1 -1
- package/dist/index.js +11 -1
- package/dist/lib/browser/ipc.js +2 -0
- package/dist/lib/browser/remote-control.d.ts +35 -0
- package/dist/lib/browser/remote-control.js +48 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +19 -1
- package/dist/lib/browser/types.d.ts +14 -2
- package/dist/lib/crabbox/cli.d.ts +35 -0
- package/dist/lib/crabbox/cli.js +46 -0
- package/dist/lib/crabbox/config.d.ts +21 -0
- package/dist/lib/crabbox/config.js +43 -0
- package/dist/lib/crabbox/lease.d.ts +15 -5
- package/dist/lib/crabbox/lease.js +57 -17
- package/dist/lib/daemon.js +12 -11
- package/dist/lib/device-config.js +8 -0
- package/dist/lib/devices/resolve-target.d.ts +4 -3
- package/dist/lib/devices/resolve-target.js +4 -3
- package/dist/lib/hosts/passthrough.d.ts +10 -1
- package/dist/lib/hosts/passthrough.js +41 -3
- package/dist/lib/hosts/registry.d.ts +4 -0
- package/dist/lib/hosts/registry.js +16 -0
- package/dist/lib/hosts/remote-cmd.js +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/observe-aliases.d.ts +30 -0
- package/dist/lib/observe-aliases.js +56 -0
- package/dist/lib/placement.d.ts +82 -0
- package/dist/lib/placement.js +188 -0
- package/dist/lib/profiles.d.ts +31 -9
- package/dist/lib/profiles.js +83 -13
- package/dist/lib/redact.js +4 -0
- package/dist/lib/rotate.d.ts +19 -4
- package/dist/lib/rotate.js +24 -1
- package/dist/lib/routines.d.ts +2 -0
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.js +90 -7
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/parse.d.ts +5 -1
- package/dist/lib/session/parse.js +23 -13
- package/dist/lib/session/prompt.js +5 -0
- package/dist/lib/session/render.d.ts +3 -0
- package/dist/lib/session/render.js +33 -10
- package/dist/lib/snapshot.d.ts +103 -0
- package/dist/lib/snapshot.js +99 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/usage-refresh.d.ts +19 -11
- package/dist/lib/usage-refresh.js +75 -43
- package/dist/lib/usage.d.ts +12 -0
- package/dist/lib/usage.js +37 -6
- package/package.json +1 -1
|
@@ -79,10 +79,20 @@ export interface Task {
|
|
|
79
79
|
createdAt: number;
|
|
80
80
|
pid: number;
|
|
81
81
|
/**
|
|
82
|
-
* Resolved actor id (`resolveActor().id`) stamped at task start —
|
|
83
|
-
* this browser task
|
|
82
|
+
* Resolved actor id (`resolveActor().id`) stamped at task start — WHO launched
|
|
83
|
+
* this browser task (a person/agent identity, stable across runs). Forwarded
|
|
84
|
+
* from the caller over IPC — the daemon is shared, so it cannot resolve the
|
|
85
|
+
* caller's actor itself. Optional: tasks persisted before RUSH-2020 carry none.
|
|
84
86
|
*/
|
|
85
87
|
owner?: string;
|
|
88
|
+
/**
|
|
89
|
+
* The caller's per-run launch id (`$AGENT_LAUNCH_ID`, minted by exec.ts for
|
|
90
|
+
* every harness) stamped at task start — WHICH run created this task. Distinct
|
|
91
|
+
* from `owner`: two runs by the same actor get different launchIds, which is
|
|
92
|
+
* the scope the current-task default and `status --mine` filter on. Forwarded
|
|
93
|
+
* from the caller over IPC. Optional: tasks from before this shipped carry none.
|
|
94
|
+
*/
|
|
95
|
+
launchId?: string;
|
|
86
96
|
/**
|
|
87
97
|
* Per-tab snapshot of the last ref listing captured for that tab
|
|
88
98
|
* (shortId -> {descriptors, opts}). Persisted to tasks.json so a later
|
|
@@ -184,6 +194,8 @@ export interface IPCRequest {
|
|
|
184
194
|
until?: string;
|
|
185
195
|
appLevel?: string;
|
|
186
196
|
skipDomainSkill?: boolean;
|
|
197
|
+
actor?: string;
|
|
198
|
+
launchId?: string;
|
|
187
199
|
}
|
|
188
200
|
/** Subset of IPCResponse describing a recording start result. */
|
|
189
201
|
export interface RecordStartFields {
|
|
@@ -114,6 +114,41 @@ export declare function crabboxEnv(opts: CrabboxOptions): NodeJS.ProcessEnv;
|
|
|
114
114
|
export declare function crabboxList(opts?: CrabboxOptions): CrabboxBox[];
|
|
115
115
|
/** Find one box by slug, or null. */
|
|
116
116
|
export declare function crabboxFind(slug: string, opts?: CrabboxOptions): CrabboxBox | null;
|
|
117
|
+
/**
|
|
118
|
+
* Whether `crabbox status` reports the box SSH-ready (`ready=true`). A box whose
|
|
119
|
+
* cloud-init bootstrap failed still LISTS as `running` but never becomes ready —
|
|
120
|
+
* selecting it burns the full SSH wait before hard-failing. `crabbox status`
|
|
121
|
+
* flips ready=true only once sshd answers, so warm-pool reuse gates on it
|
|
122
|
+
* (mirrors scripts/sandbox.sh's `box_ready`).
|
|
123
|
+
*/
|
|
124
|
+
export declare function crabboxStatusReady(slug: string, opts?: CrabboxOptions): boolean;
|
|
125
|
+
export interface PoolMatchOptions {
|
|
126
|
+
/**
|
|
127
|
+
* Profile label this run would warm a box with (from the repo's
|
|
128
|
+
* `.crabbox.yaml`; see config.ts). Both sides normalize an unset profile to
|
|
129
|
+
* DEFAULT_CRABBOX_PROFILE, so profile-less runs match unlabeled boxes.
|
|
130
|
+
*/
|
|
131
|
+
profile?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Network mode of the run (default 'public'). A tailnet-joined box is never
|
|
134
|
+
* handed to a public run, nor a public box to a tailnet run — reachability and
|
|
135
|
+
* exposure differ, so the pool is partitioned by it.
|
|
136
|
+
*/
|
|
137
|
+
netMode?: 'public' | 'tailscale';
|
|
138
|
+
/** Injectable clock (unix seconds) for the expiry check. */
|
|
139
|
+
nowSecs?: number;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Warm boxes in the profile pool this run could reuse: `running`, same profile
|
|
143
|
+
* label, same network mode, lease unexpired — most-recently-touched first.
|
|
144
|
+
*
|
|
145
|
+
* Readiness is NOT required here (deliberately mirrors sandbox.sh's
|
|
146
|
+
* `running_slugs_for_profile`, which filters on `status` only): the list `state`
|
|
147
|
+
* label can lag, so the caller gates each candidate on `crabboxStatusReady`
|
|
148
|
+
* before committing. A not-ready box is skipped, never stopped — a concurrent
|
|
149
|
+
* run may be mid-boot on it, and crabbox's idle timeout reaps genuine duds.
|
|
150
|
+
*/
|
|
151
|
+
export declare function poolReusableBoxes(boxes: CrabboxBox[], opts?: PoolMatchOptions): CrabboxBox[];
|
|
117
152
|
export interface WarmupOptions extends CrabboxOptions {
|
|
118
153
|
class?: string;
|
|
119
154
|
profile?: string;
|
package/dist/lib/crabbox/cli.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { spawn, spawnSync } from 'child_process';
|
|
15
15
|
import { readAndResolveBundleEnv, listBundles, bundleExists } from '../secrets/bundles.js';
|
|
16
16
|
import { readMeta, writeMeta } from '../state.js';
|
|
17
|
+
import { DEFAULT_CRABBOX_PROFILE } from './config.js';
|
|
17
18
|
/** Locate the crabbox binary, or throw an actionable error. */
|
|
18
19
|
export function findCrabbox() {
|
|
19
20
|
const r = spawnSync('crabbox', ['--help'], { encoding: 'utf-8' });
|
|
@@ -301,6 +302,51 @@ export function crabboxList(opts = {}) {
|
|
|
301
302
|
export function crabboxFind(slug, opts = {}) {
|
|
302
303
|
return crabboxList(opts).find((b) => b.slug === slug) ?? null;
|
|
303
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Whether `crabbox status` reports the box SSH-ready (`ready=true`). A box whose
|
|
307
|
+
* cloud-init bootstrap failed still LISTS as `running` but never becomes ready —
|
|
308
|
+
* selecting it burns the full SSH wait before hard-failing. `crabbox status`
|
|
309
|
+
* flips ready=true only once sshd answers, so warm-pool reuse gates on it
|
|
310
|
+
* (mirrors scripts/sandbox.sh's `box_ready`).
|
|
311
|
+
*/
|
|
312
|
+
export function crabboxStatusReady(slug, opts = {}) {
|
|
313
|
+
findCrabbox();
|
|
314
|
+
const r = spawnSync('crabbox', ['status', '--id', slug], {
|
|
315
|
+
encoding: 'utf-8',
|
|
316
|
+
env: crabboxEnv(opts),
|
|
317
|
+
timeout: opts.timeoutMs ?? 15000,
|
|
318
|
+
});
|
|
319
|
+
if (r.status !== 0 || !r.stdout)
|
|
320
|
+
return false;
|
|
321
|
+
return /(^|\s)ready=true(\s|$)/m.test(r.stdout);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Warm boxes in the profile pool this run could reuse: `running`, same profile
|
|
325
|
+
* label, same network mode, lease unexpired — most-recently-touched first.
|
|
326
|
+
*
|
|
327
|
+
* Readiness is NOT required here (deliberately mirrors sandbox.sh's
|
|
328
|
+
* `running_slugs_for_profile`, which filters on `status` only): the list `state`
|
|
329
|
+
* label can lag, so the caller gates each candidate on `crabboxStatusReady`
|
|
330
|
+
* before committing. A not-ready box is skipped, never stopped — a concurrent
|
|
331
|
+
* run may be mid-boot on it, and crabbox's idle timeout reaps genuine duds.
|
|
332
|
+
*/
|
|
333
|
+
export function poolReusableBoxes(boxes, opts = {}) {
|
|
334
|
+
const profile = opts.profile ?? DEFAULT_CRABBOX_PROFILE;
|
|
335
|
+
const netMode = opts.netMode ?? 'public';
|
|
336
|
+
const nowSecs = opts.nowSecs ?? Math.floor(Date.now() / 1000);
|
|
337
|
+
return boxes
|
|
338
|
+
.filter((b) => {
|
|
339
|
+
if (b.status !== 'running')
|
|
340
|
+
return false;
|
|
341
|
+
if ((b.profile ?? DEFAULT_CRABBOX_PROFILE) !== profile)
|
|
342
|
+
return false;
|
|
343
|
+
const boxNet = b.tailscaleIPv4 || b.tailscaleFQDN ? 'tailscale' : 'public';
|
|
344
|
+
if (boxNet !== netMode)
|
|
345
|
+
return false;
|
|
346
|
+
return b.expiresAt === null || b.expiresAt > nowSecs;
|
|
347
|
+
})
|
|
348
|
+
.sort((a, b) => (b.lastTouchedAt ?? 0) - (a.lastTouchedAt ?? 0));
|
|
349
|
+
}
|
|
304
350
|
/**
|
|
305
351
|
* Lease a box and block until it is ready. Returns the leased box.
|
|
306
352
|
*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repo-local crabbox config (`.crabbox.yaml` at the repo root).
|
|
3
|
+
*
|
|
4
|
+
* crabbox itself reads this file when warming a box (the `profile:` key becomes
|
|
5
|
+
* the box's `profile` label). `agents run --lease` reads it too so the warm-pool
|
|
6
|
+
* reuse check matches on the SAME profile the warmup would have used — a reused
|
|
7
|
+
* box is then interchangeable with a fresh one (scripts/sandbox.sh's
|
|
8
|
+
* `pick_ready_box` resolves the pool the same way).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The profile label a box warm pool shares. Matches sandbox.sh's
|
|
12
|
+
* `PROFILE="${PROFILE:-default}"`: a run with no configured profile and a box
|
|
13
|
+
* with no `profile` label both normalize here, so they still match each other.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_CRABBOX_PROFILE = "default";
|
|
16
|
+
/**
|
|
17
|
+
* The `profile:` declared by `<repoRoot>/.crabbox.yaml`, or undefined when the
|
|
18
|
+
* file is missing, unreadable, or has no profile key (crabbox then applies its
|
|
19
|
+
* own default, which the pool matcher treats as DEFAULT_CRABBOX_PROFILE).
|
|
20
|
+
*/
|
|
21
|
+
export declare function readCrabboxRepoProfile(repoRoot: string): string | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repo-local crabbox config (`.crabbox.yaml` at the repo root).
|
|
3
|
+
*
|
|
4
|
+
* crabbox itself reads this file when warming a box (the `profile:` key becomes
|
|
5
|
+
* the box's `profile` label). `agents run --lease` reads it too so the warm-pool
|
|
6
|
+
* reuse check matches on the SAME profile the warmup would have used — a reused
|
|
7
|
+
* box is then interchangeable with a fresh one (scripts/sandbox.sh's
|
|
8
|
+
* `pick_ready_box` resolves the pool the same way).
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import * as yaml from 'yaml';
|
|
13
|
+
/**
|
|
14
|
+
* The profile label a box warm pool shares. Matches sandbox.sh's
|
|
15
|
+
* `PROFILE="${PROFILE:-default}"`: a run with no configured profile and a box
|
|
16
|
+
* with no `profile` label both normalize here, so they still match each other.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_CRABBOX_PROFILE = 'default';
|
|
19
|
+
/**
|
|
20
|
+
* The `profile:` declared by `<repoRoot>/.crabbox.yaml`, or undefined when the
|
|
21
|
+
* file is missing, unreadable, or has no profile key (crabbox then applies its
|
|
22
|
+
* own default, which the pool matcher treats as DEFAULT_CRABBOX_PROFILE).
|
|
23
|
+
*/
|
|
24
|
+
export function readCrabboxRepoProfile(repoRoot) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = fs.readFileSync(path.join(repoRoot, '.crabbox.yaml'), 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined; // no repo crabbox config — crabbox's own default applies
|
|
31
|
+
}
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = yaml.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
if (!parsed || typeof parsed !== 'object')
|
|
40
|
+
return undefined;
|
|
41
|
+
const profile = parsed.profile;
|
|
42
|
+
return typeof profile === 'string' && profile.length > 0 ? profile : undefined;
|
|
43
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `agents run --lease` orchestrator.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Acquire a box → provision the picked runtime(s) + their credentials → run the
|
|
5
|
+
* agent on the box (via `crabbox run`, which owns the SSH) → tear the box down.
|
|
6
|
+
* Acquisition is reuse-first against the warm profile pool (a ready box carrying
|
|
7
|
+
* the run's profile + netMode labels is reused and kept; `--fresh` opts out and
|
|
8
|
+
* always leases a new, torn-down box). The whole box-side sequence rides a
|
|
9
|
+
* single `--script-stdin` body so the token contents never touch argv.
|
|
8
10
|
*
|
|
9
11
|
* ── Command-layer contract (RUSH-1920/1921/1924) ─────────────────────────────
|
|
10
12
|
* Exports the commands layer (exec.ts / lease.ts / ssh.ts) consumes:
|
|
@@ -12,7 +14,9 @@
|
|
|
12
14
|
* --bare) gates the `copy-setup` progress sentinel; `opts.netMode`
|
|
13
15
|
* ('public' | 'tailscale', default 'public') adds the `joined-tailnet` step.
|
|
14
16
|
* • `LeaseRunOptions.copySetup` / `LeaseRunOptions.netMode` — forwarded by
|
|
15
|
-
* `leaseAndRun` (netMode → `crabboxWarmup`).
|
|
17
|
+
* `leaseAndRun` (netMode → `crabboxWarmup`). `LeaseRunOptions.fresh`
|
|
18
|
+
* (`--fresh`) skips the warm profile-pool reuse check and always leases a
|
|
19
|
+
* new box, torn down after the run.
|
|
16
20
|
* • `crabboxWarmup(opts.netMode)` — 'tailscale' leases onto the tailnet
|
|
17
21
|
* (`--network tailscale -tailscale-tags tag:crabbox`); auth key rides the
|
|
18
22
|
* child env as `CRABBOX_TAILSCALE_AUTH_KEY` (crabboxEnv, cli.ts).
|
|
@@ -80,6 +84,12 @@ export interface LeaseRunOptions {
|
|
|
80
84
|
keep?: boolean;
|
|
81
85
|
/** Existing warm crabbox slug to reuse instead of provisioning a new lease. */
|
|
82
86
|
reuseBox?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Force a brand-new box: skip the warm profile-pool reuse check and tear the
|
|
89
|
+
* box down after the run (the pre-pool `--lease` behavior). `--fresh` at the
|
|
90
|
+
* command layer.
|
|
91
|
+
*/
|
|
92
|
+
fresh?: boolean;
|
|
83
93
|
/**
|
|
84
94
|
* Raw wrapped Claude OAuth payload (from `resolveClaudeCredentialsBlob`), written
|
|
85
95
|
* to `~/.claude/.credentials.json` on the box. The command layer resolves it
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `agents run --lease` orchestrator.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Acquire a box → provision the picked runtime(s) + their credentials → run the
|
|
5
|
+
* agent on the box (via `crabbox run`, which owns the SSH) → tear the box down.
|
|
6
|
+
* Acquisition is reuse-first against the warm profile pool (a ready box carrying
|
|
7
|
+
* the run's profile + netMode labels is reused and kept; `--fresh` opts out and
|
|
8
|
+
* always leases a new, torn-down box). The whole box-side sequence rides a
|
|
9
|
+
* single `--script-stdin` body so the token contents never touch argv.
|
|
8
10
|
*
|
|
9
11
|
* ── Command-layer contract (RUSH-1920/1921/1924) ─────────────────────────────
|
|
10
12
|
* Exports the commands layer (exec.ts / lease.ts / ssh.ts) consumes:
|
|
@@ -12,7 +14,9 @@
|
|
|
12
14
|
* --bare) gates the `copy-setup` progress sentinel; `opts.netMode`
|
|
13
15
|
* ('public' | 'tailscale', default 'public') adds the `joined-tailnet` step.
|
|
14
16
|
* • `LeaseRunOptions.copySetup` / `LeaseRunOptions.netMode` — forwarded by
|
|
15
|
-
* `leaseAndRun` (netMode → `crabboxWarmup`).
|
|
17
|
+
* `leaseAndRun` (netMode → `crabboxWarmup`). `LeaseRunOptions.fresh`
|
|
18
|
+
* (`--fresh`) skips the warm profile-pool reuse check and always leases a
|
|
19
|
+
* new box, torn down after the run.
|
|
16
20
|
* • `crabboxWarmup(opts.netMode)` — 'tailscale' leases onto the tailnet
|
|
17
21
|
* (`--network tailscale -tailscale-tags tag:crabbox`); auth key rides the
|
|
18
22
|
* child env as `CRABBOX_TAILSCALE_AUTH_KEY` (crabboxEnv, cli.ts).
|
|
@@ -24,7 +28,7 @@
|
|
|
24
28
|
* secretsBundle?, userAgentsDir?, onData? }): Promise<CopySetupResult>` — the
|
|
25
29
|
* push-from-local the command layer runs before the box run.
|
|
26
30
|
*/
|
|
27
|
-
import { crabboxFind, crabboxWarmup, crabboxWaitReady, crabboxRunScript, crabboxStop } from './cli.js';
|
|
31
|
+
import { crabboxFind, crabboxList, crabboxStatusReady, crabboxWarmup, crabboxWaitReady, crabboxRunScript, crabboxStop, poolReusableBoxes } from './cli.js';
|
|
28
32
|
import * as yaml from 'yaml';
|
|
29
33
|
import { buildCredentialScript, buildHomeFileWriteScript, CLAUDE_TOKEN_REMOTE } from './runtimes.js';
|
|
30
34
|
import { LEASE_AGENT_MARKER, leasePhaseSentinel } from './progress.js';
|
|
@@ -149,9 +153,32 @@ export function buildBootstrapScript(opts) {
|
|
|
149
153
|
.filter((l) => l.length > 0)
|
|
150
154
|
.join('\n');
|
|
151
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* The first warm box in this run's profile pool that is actually SSH-ready, or
|
|
158
|
+
* null. Mirrors scripts/sandbox.sh's `pick_ready_box`: list the running boxes
|
|
159
|
+
* for the run's profile + netMode, then gate each on `crabbox status`
|
|
160
|
+
* ready=true — a box whose bootstrap failed still lists as `running` and would
|
|
161
|
+
* burn the whole SSH wait before hard-failing. A skipped box is left alone
|
|
162
|
+
* (never stopped): a concurrent run may be mid-boot on it, and crabbox's idle
|
|
163
|
+
* timeout reaps genuine duds.
|
|
164
|
+
*/
|
|
165
|
+
function pickReadyPoolBox(opts) {
|
|
166
|
+
const candidates = poolReusableBoxes(crabboxList({ secretsBundle: opts.secretsBundle }), {
|
|
167
|
+
profile: opts.profile,
|
|
168
|
+
netMode: opts.netMode,
|
|
169
|
+
});
|
|
170
|
+
for (const b of candidates) {
|
|
171
|
+
if (crabboxStatusReady(b.slug, { secretsBundle: opts.secretsBundle }))
|
|
172
|
+
return b;
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
152
176
|
export async function leaseAndRun(opts) {
|
|
153
177
|
const startedAt = Date.now();
|
|
154
178
|
let box;
|
|
179
|
+
// A box this run did NOT provision — either the caller named it (`--box`) or
|
|
180
|
+
// it came out of the warm profile pool. Reused boxes are never torn down.
|
|
181
|
+
let reused = false;
|
|
155
182
|
if (opts.reuseBox) {
|
|
156
183
|
opts.onPhase?.({ kind: 'reuse', slug: opts.reuseBox });
|
|
157
184
|
const found = crabboxFind(opts.reuseBox, { secretsBundle: opts.secretsBundle });
|
|
@@ -160,17 +187,29 @@ export async function leaseAndRun(opts) {
|
|
|
160
187
|
box = found.ready
|
|
161
188
|
? found
|
|
162
189
|
: await crabboxWaitReady(opts.reuseBox, { secretsBundle: opts.secretsBundle });
|
|
190
|
+
reused = true;
|
|
163
191
|
}
|
|
164
192
|
else {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
// Reuse-first: before paying for a fresh lease, look for a warm box in this
|
|
194
|
+
// run's profile pool (same profile label the warmup would use, same netMode).
|
|
195
|
+
// `--fresh` opts out and always provisions.
|
|
196
|
+
const pooled = opts.fresh ? null : pickReadyPoolBox(opts);
|
|
197
|
+
if (pooled) {
|
|
198
|
+
opts.onPhase?.({ kind: 'reuse', slug: pooled.slug });
|
|
199
|
+
box = pooled;
|
|
200
|
+
reused = true;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
opts.onPhase?.({ kind: 'warmup', backend: opts.backend });
|
|
204
|
+
box = await crabboxWarmup({
|
|
205
|
+
class: opts.boxClass,
|
|
206
|
+
profile: opts.profile,
|
|
207
|
+
provider: opts.backend,
|
|
208
|
+
secretsBundle: opts.secretsBundle,
|
|
209
|
+
netMode: opts.netMode,
|
|
210
|
+
});
|
|
211
|
+
await crabboxWaitReady(box.slug, { secretsBundle: opts.secretsBundle });
|
|
212
|
+
}
|
|
174
213
|
}
|
|
175
214
|
opts.onPhase?.({ kind: 'ready', box, elapsedMs: Date.now() - startedAt });
|
|
176
215
|
// Setup-copy (F1, RUSH-1920): push the git-tracked ~/.agents config onto the
|
|
@@ -203,8 +242,9 @@ export async function leaseAndRun(opts) {
|
|
|
203
242
|
}
|
|
204
243
|
finally {
|
|
205
244
|
// Always attempt teardown (bounds credential lifetime to the run) unless the
|
|
206
|
-
// caller explicitly asked to keep the box or
|
|
207
|
-
|
|
245
|
+
// caller explicitly asked to keep the box or the box was reused (an explicit
|
|
246
|
+
// --box target or a warm pool box — both outlive this run).
|
|
247
|
+
if (!opts.keep && !reused) {
|
|
208
248
|
opts.onPhase?.({ kind: 'teardown' });
|
|
209
249
|
toreDown = crabboxStop(box.slug, { secretsBundle: opts.secretsBundle });
|
|
210
250
|
}
|
package/dist/lib/daemon.js
CHANGED
|
@@ -888,14 +888,14 @@ export async function runDaemon() {
|
|
|
888
888
|
};
|
|
889
889
|
const fleetCacheInterval = setInterval(() => { void runFleetCacheWarm(); }, 3 * 60_000);
|
|
890
890
|
const fleetCacheKickoff = setTimeout(() => { void runFleetCacheWarm(); }, 60_000);
|
|
891
|
-
//
|
|
891
|
+
// Usage refresh: keep the usage cache the `agents run` router reads
|
|
892
892
|
// (RUSH-2061, readOnly hot path) fresh, WITHOUT the hot path ever fetching.
|
|
893
|
-
// This host is the sole writer for its own local accounts. The tick wakes
|
|
894
|
-
//
|
|
895
|
-
//
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
893
|
+
// This host is the sole writer for its own local accounts. The tick wakes
|
|
894
|
+
// every 60s (USAGE_REFRESH_TICK_MS) to consider due accounts; each account
|
|
895
|
+
// is scheduled at a fixed 5-minute cadence (REFRESH_INTERVAL_MS), capped at
|
|
896
|
+
// ~12 provider calls/account/hour, skipped under 429 backoff, and fetched
|
|
897
|
+
// with fileOnly credentials so a background tick never pops macOS Touch ID.
|
|
898
|
+
// Overlap-guarded: a slow pass cannot stack concurrent refresh loops.
|
|
899
899
|
let refreshingUsage = false;
|
|
900
900
|
const runUsageRefreshTick = async () => {
|
|
901
901
|
if (refreshingUsage)
|
|
@@ -910,9 +910,9 @@ export async function runDaemon() {
|
|
|
910
910
|
writeUsageCache: writeClaudeUsageCache,
|
|
911
911
|
backoffUntil: usageRateLimitedUntil,
|
|
912
912
|
});
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
913
|
+
// Always log a compact summary so "is refresh working?" is greppable even
|
|
914
|
+
// when every account was not-due (proves the tick ran).
|
|
915
|
+
log('INFO', `usage refresh: ${r.refreshed} refreshed, ${r.failed} failed, ${r.skippedNotDue} not-due, ${r.skippedBackoff} backed-off, ${r.skippedCap} capped`);
|
|
916
916
|
}
|
|
917
917
|
catch (err) {
|
|
918
918
|
log('ERROR', `usage refresh failed: ${err.message}`);
|
|
@@ -921,7 +921,8 @@ export async function runDaemon() {
|
|
|
921
921
|
refreshingUsage = false;
|
|
922
922
|
}
|
|
923
923
|
};
|
|
924
|
-
|
|
924
|
+
// 60s wake matches USAGE_REFRESH_TICK_MS in usage-refresh.ts (keep in sync).
|
|
925
|
+
const usageRefreshInterval = setInterval(() => { void runUsageRefreshTick(); }, 60_000);
|
|
925
926
|
const usageRefreshKickoff = setTimeout(() => { void runUsageRefreshTick(); }, 30_000);
|
|
926
927
|
// RUSH-1817: the startup host decision above is one-shot. If a standalone
|
|
927
928
|
// broker answered agentPing() at daemon start, the daemon declined to host —
|
|
@@ -67,6 +67,14 @@ export const CONFIG_KEYS = [
|
|
|
67
67
|
type: 'bool',
|
|
68
68
|
description: 'Whether the routines scheduler (daemon) may fire on this device.',
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'browser.remote-control',
|
|
72
|
+
yamlKey: 'browserRemoteControl',
|
|
73
|
+
scope: 'device',
|
|
74
|
+
type: 'bool',
|
|
75
|
+
description: "Whether other fleet machines may drive THIS device's browser over `browser --host <this-device>`. " +
|
|
76
|
+
'Default off — a fleet-remote drive is refused until the owner runs `agents browser remote-control on`.',
|
|
77
|
+
},
|
|
70
78
|
{
|
|
71
79
|
name: 'notes',
|
|
72
80
|
yamlKey: 'notes',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { splitUserHost } from '../hosts/registry.js';
|
|
1
|
+
import { splitUserHost, type MatchHostOptions } from '../hosts/registry.js';
|
|
2
2
|
import { type DeviceProfile } from './registry.js';
|
|
3
3
|
export { splitUserHost };
|
|
4
4
|
/** A dialable peer: the ssh target, the machine id used to tag its rows, a
|
|
@@ -21,9 +21,10 @@ export interface ResolvedExplicitTargetSet {
|
|
|
21
21
|
* `user@host` / IP / FQDN literal yields a synthesized key-auth profile. A bare
|
|
22
22
|
* unregistered alias (no `@`/dot) — or an ssh_config-only alias, which `agents
|
|
23
23
|
* ssh` has never dialed — returns undefined so the caller reports "Unknown
|
|
24
|
-
* device" rather than dialing a literal.
|
|
24
|
+
* device" rather than dialing a literal. `token` may also be the `auto`
|
|
25
|
+
* affinity sentinel (RUSH-2185); `opts.resolveAuto` overrides the pick (tests).
|
|
25
26
|
*/
|
|
26
|
-
export declare function resolveDeviceTarget(token: string): Promise<DeviceProfile | undefined>;
|
|
27
|
+
export declare function resolveDeviceTarget(token: string, opts?: Pick<MatchHostOptions, 'resolveAuto'>): Promise<DeviceProfile | undefined>;
|
|
27
28
|
/**
|
|
28
29
|
* Resolve an explicit `--host`/`--device` list to dialable targets. A token that
|
|
29
30
|
* fails the injection guard or names nothing reachable is skipped with a stderr
|
|
@@ -71,10 +71,11 @@ async function toResolvedTarget(token) {
|
|
|
71
71
|
* `user@host` / IP / FQDN literal yields a synthesized key-auth profile. A bare
|
|
72
72
|
* unregistered alias (no `@`/dot) — or an ssh_config-only alias, which `agents
|
|
73
73
|
* ssh` has never dialed — returns undefined so the caller reports "Unknown
|
|
74
|
-
* device" rather than dialing a literal.
|
|
74
|
+
* device" rather than dialing a literal. `token` may also be the `auto`
|
|
75
|
+
* affinity sentinel (RUSH-2185); `opts.resolveAuto` overrides the pick (tests).
|
|
75
76
|
*/
|
|
76
|
-
export async function resolveDeviceTarget(token) {
|
|
77
|
-
const host = await matchHost(token, { allowBareLiteral: true });
|
|
77
|
+
export async function resolveDeviceTarget(token, opts = {}) {
|
|
78
|
+
const host = await matchHost(token, { allowBareLiteral: true, ...opts });
|
|
78
79
|
if (!host)
|
|
79
80
|
return undefined;
|
|
80
81
|
if (host.device) {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* table or, when `--host`/`--device` is present, exits with a clear
|
|
17
17
|
* "not supported" message — never commander's raw `unknown option`.
|
|
18
18
|
*/
|
|
19
|
-
import { type DeviceRegistry } from '../devices/registry.js';
|
|
19
|
+
import { type DeviceProfile, type DeviceRegistry } from '../devices/registry.js';
|
|
20
20
|
import { runLocalCommand, runOnDevice } from '../devices/fleet.js';
|
|
21
21
|
/** Per-command remote behaviour. Absence from this map = not host-routable here. */
|
|
22
22
|
interface RemoteSpec {
|
|
@@ -36,6 +36,15 @@ export interface FleetPassthroughOptions {
|
|
|
36
36
|
/** Override this machine's id (tests). Defaults to `machineId()`. */
|
|
37
37
|
self?: string;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Prefix a fan-out remote command so the far side sees AGENTS_FLEET_REMOTE=1 —
|
|
41
|
+
* the same marker the single-target dispatch sets via env. `wrapRemoteCommand`
|
|
42
|
+
* joins the argv with spaces (POSIX) or base64-encodes it for PowerShell, so a
|
|
43
|
+
* shell-appropriate leading token rides through both: `env VAR=1 …` on POSIX,
|
|
44
|
+
* `$env:VAR='1'; …` on PowerShell. Only remote (non-self) targets get it; the
|
|
45
|
+
* self target runs locally and must stay ungated.
|
|
46
|
+
*/
|
|
47
|
+
export declare function markFleetRemote(cmd: string[], device: DeviceProfile): string[];
|
|
39
48
|
/** Run `agents <command> …` across every registered device and render the roster. */
|
|
40
49
|
export declare function runFleetPassthrough(command: string, allArgs: string[], spec: RemoteSpec, opts?: FleetPassthroughOptions): Promise<boolean>;
|
|
41
50
|
/**
|
|
@@ -24,6 +24,7 @@ import { dispatchAgentsCommand, withActorEnv } from './dispatch.js';
|
|
|
24
24
|
import { stripRoutingFlags, buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, } from './remote-cmd.js';
|
|
25
25
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
26
26
|
import { machineId } from '../session/sync/config.js';
|
|
27
|
+
import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
|
|
27
28
|
import { loadDevices } from '../devices/registry.js';
|
|
28
29
|
import { isSelfHost } from '../devices/self-host.js';
|
|
29
30
|
import { fanOutDevices, planFleetTargets, runLocalCommand, runOnDevice, } from '../devices/fleet.js';
|
|
@@ -319,6 +320,19 @@ function renderFleetRoster(command, forwarded, results, self) {
|
|
|
319
320
|
console.log(chalk.gray(summaryParts.join(' · ')));
|
|
320
321
|
}
|
|
321
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Prefix a fan-out remote command so the far side sees AGENTS_FLEET_REMOTE=1 —
|
|
325
|
+
* the same marker the single-target dispatch sets via env. `wrapRemoteCommand`
|
|
326
|
+
* joins the argv with spaces (POSIX) or base64-encodes it for PowerShell, so a
|
|
327
|
+
* shell-appropriate leading token rides through both: `env VAR=1 …` on POSIX,
|
|
328
|
+
* `$env:VAR='1'; …` on PowerShell. Only remote (non-self) targets get it; the
|
|
329
|
+
* self target runs locally and must stay ungated.
|
|
330
|
+
*/
|
|
331
|
+
export function markFleetRemote(cmd, device) {
|
|
332
|
+
return device.shell === 'powershell'
|
|
333
|
+
? [`$env:AGENTS_FLEET_REMOTE='1';`, ...cmd]
|
|
334
|
+
: ['env', 'AGENTS_FLEET_REMOTE=1', ...cmd];
|
|
335
|
+
}
|
|
322
336
|
/** Run `agents <command> …` across every registered device and render the roster. */
|
|
323
337
|
export async function runFleetPassthrough(command, allArgs, spec, opts = {}) {
|
|
324
338
|
const self = opts.self ?? machineId();
|
|
@@ -335,7 +349,12 @@ export async function runFleetPassthrough(command, allArgs, spec, opts = {}) {
|
|
|
335
349
|
const results = await fanOutDevices(targets, async (target) => {
|
|
336
350
|
const cmd = ['agents', ...forwarded];
|
|
337
351
|
const isSelf = target.device.name.toLowerCase() === self.toLowerCase() || isSelfHost(target.device.name);
|
|
338
|
-
|
|
352
|
+
// Only `browser` consults the fleet-remote marker (its consent gate), and
|
|
353
|
+
// the fan-out has no separate env channel — the marker must ride the argv.
|
|
354
|
+
// So scope the env-prefix to a REMOTE browser drive: every other command's
|
|
355
|
+
// remote argv stays byte-identical, and the self target is never gated.
|
|
356
|
+
const remoteCmd = !isSelf && command === 'browser' ? markFleetRemote(cmd, target.device) : cmd;
|
|
357
|
+
const res = isSelf ? localRunner(cmd) : runner(target.device, remoteCmd);
|
|
339
358
|
if (res.code !== 0) {
|
|
340
359
|
const detail = (res.stderr || res.stdout || 'unreachable').trim().slice(0, 200);
|
|
341
360
|
throw new Error(detail || 'unreachable');
|
|
@@ -378,7 +397,7 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
378
397
|
const deviceFlag = flagValue(allArgs, 'device');
|
|
379
398
|
const hostsFlag = flagValue(allArgs, 'hosts');
|
|
380
399
|
const devicesFlag = flagValue(allArgs, 'devices');
|
|
381
|
-
|
|
400
|
+
let hostName = hostFlag ?? deviceFlag;
|
|
382
401
|
const fleetAll = isFleetAllSentinel(hostFlag, deviceFlag, hostsFlag, devicesFlag);
|
|
383
402
|
// Proceed when any routing flag is present, including the plural fleet flags
|
|
384
403
|
// that may carry the `all` sentinel.
|
|
@@ -442,6 +461,22 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
442
461
|
// flags and bare flags were already handled.
|
|
443
462
|
if (!hostName)
|
|
444
463
|
return false;
|
|
464
|
+
// `auto` is the same affinity sentinel `agents run --device auto` resolves
|
|
465
|
+
// (RUSH-2185) — pick the concrete target up front via resolveDeviceAffinity so
|
|
466
|
+
// the isSelfHost check right below (which compares a literal name, not "auto")
|
|
467
|
+
// still catches a local pick and runs the command locally rather than
|
|
468
|
+
// resolving to a real Host and self-SSHing (or, if this box isn't itself a
|
|
469
|
+
// registered device, dialing a literal, nonexistent host named "auto").
|
|
470
|
+
if (isDeviceAuto(hostName)) {
|
|
471
|
+
const plan = resolveDeviceAffinity({});
|
|
472
|
+
if (!plan.host) {
|
|
473
|
+
const stripped = stripRoutingFlags(allArgs, STRIP_SPECS);
|
|
474
|
+
process.argv = [process.argv[0], process.argv[1], ...stripped];
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
process.stderr.write(chalk.gray(`[agents] device=auto → ${plan.host}\n`));
|
|
478
|
+
hostName = plan.host;
|
|
479
|
+
}
|
|
445
480
|
// Running against your own machine is just a local run — skip the SSH round-trip.
|
|
446
481
|
// Match EVERY identity the box answers to (short id, loopback, tailscale
|
|
447
482
|
// dnsName), not just machineId() — a `--host <self-dnsName>` used to slip past a
|
|
@@ -500,7 +535,10 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
500
535
|
// UNDER the doctor PATH so that PATH still wins — without this the remote
|
|
501
536
|
// re-resolves the actor from THIS box's SSH_CONNECTION and mis-credits it
|
|
502
537
|
// (RUSH-2028). Flows to both POSIX (export) and Windows ($env:) dialects.
|
|
503
|
-
|
|
538
|
+
// AGENTS_FLEET_REMOTE marks this as a fleet-dispatched `--host` run so the far
|
|
539
|
+
// side can gate consent-sensitive actions — the browser consent gate
|
|
540
|
+
// (lib/browser/remote-control.ts) reads it to allow/deny a cross-machine drive.
|
|
541
|
+
const env = withActorEnv({ ...doctorPath, AGENTS_FLEET_REMOTE: '1' });
|
|
504
542
|
const remoteCmd = buildRemoteAgentsInvocation(forwarded, remoteCwd, remoteOs, env);
|
|
505
543
|
const code = sshStream(target, remoteCmd, { tty: interactive, multiplex: true });
|
|
506
544
|
if (code === 255) {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import type { Host, HostProvider, HostProviderId } from './types.js';
|
|
20
20
|
import { DeviceOffloadUnsupportedError } from './types.js';
|
|
21
21
|
import { type DeviceProfile } from '../devices/registry.js';
|
|
22
|
+
import { type DeviceAffinityPlan } from '../smart-launch.js';
|
|
22
23
|
export { DeviceOffloadUnsupportedError };
|
|
23
24
|
export declare function getProvider(id: HostProviderId): HostProvider;
|
|
24
25
|
export declare function getAllProviders(): HostProvider[];
|
|
@@ -52,6 +53,9 @@ export interface MatchHostOptions {
|
|
|
52
53
|
* "Unknown device" verdict reachable).
|
|
53
54
|
*/
|
|
54
55
|
allowBareLiteral?: boolean;
|
|
56
|
+
/** Override the affinity pick for the `auto` sentinel (tests). Defaults to
|
|
57
|
+
* `resolveDeviceAffinity({})` — the same engine `agents run --device auto` uses. */
|
|
58
|
+
resolveAuto?: () => DeviceAffinityPlan;
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
57
61
|
* The one place a `--host` / `--device` token becomes a resolved host. Reads the
|
|
@@ -25,6 +25,8 @@ import { readMeta } from '../state.js';
|
|
|
25
25
|
import { isSshConfigHost } from './ssh-config.js';
|
|
26
26
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
27
27
|
import { loadDevices, isControlDevice } from '../devices/registry.js';
|
|
28
|
+
import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
|
|
29
|
+
import { localMachineId } from '../session/origin-machine.js';
|
|
28
30
|
// Re-export so existing importers (tests, commands) keep their path; the class
|
|
29
31
|
// itself lives in types.ts so providers can throw it without a circular import.
|
|
30
32
|
export { DeviceOffloadUnsupportedError };
|
|
@@ -146,6 +148,20 @@ function literalHost(token, host, user) {
|
|
|
146
148
|
* into a dispatch verdict.
|
|
147
149
|
*/
|
|
148
150
|
export async function matchHost(name, opts = {}) {
|
|
151
|
+
// `auto` is the same affinity sentinel `agents run --device auto` resolves
|
|
152
|
+
// (isDeviceAuto / resolveDeviceAffinity in ../smart-launch.js) — shared here so
|
|
153
|
+
// every caller through this one core (ssh, teams, dispatch/passthrough) picks a
|
|
154
|
+
// device the SAME way `run` does instead of rejecting it as "Unknown device
|
|
155
|
+
// 'auto'" (RUSH-2185). A `null` plan.host means the affinity engine picked this
|
|
156
|
+
// very machine; resolve that as the local device/host entry (if any) rather than
|
|
157
|
+
// returning nothing — callers that already special-case "target is this
|
|
158
|
+
// machine" (teams add/create, the passthrough self-host check) then treat it as
|
|
159
|
+
// local exactly as they would if the user had typed the local name.
|
|
160
|
+
if (isDeviceAuto(name)) {
|
|
161
|
+
const plan = (opts.resolveAuto ?? (() => resolveDeviceAffinity({})))();
|
|
162
|
+
const picked = plan.host ?? normalizeHost(localMachineId());
|
|
163
|
+
return matchHost(picked, opts);
|
|
164
|
+
}
|
|
149
165
|
try {
|
|
150
166
|
assertValidSshTarget(name);
|
|
151
167
|
}
|
|
@@ -105,6 +105,7 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
105
105
|
disableTmux: 'local-only',
|
|
106
106
|
host: 'local-only',
|
|
107
107
|
device: 'local-only',
|
|
108
|
+
where: 'local-only', // expands into host/lease before dispatch; never re-forwarded
|
|
108
109
|
on: 'local-only',
|
|
109
110
|
computer: 'local-only',
|
|
110
111
|
any: 'local-only',
|
|
@@ -112,6 +113,7 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
112
113
|
lease: 'local-only',
|
|
113
114
|
box: 'local-only',
|
|
114
115
|
keepBox: 'local-only',
|
|
116
|
+
fresh: 'local-only', // skips the warm-pool reuse for --lease; the lease path is always local
|
|
115
117
|
reuse: 'local-only', // reuse-picker choice for --lease; the lease path is always local
|
|
116
118
|
bare: 'local-only', // skips the local setup-copy push; lease-only concern
|
|
117
119
|
tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
|
|
Binary file
|