@phnx-labs/agents-cli 1.22.8 → 1.22.10
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 +64 -4
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/exec.js +41 -9
- package/dist/commands/feed.js +38 -1
- package/dist/commands/harness.d.ts +40 -1
- package/dist/commands/harness.js +316 -76
- package/dist/commands/profiles.d.ts +42 -0
- package/dist/commands/profiles.js +91 -3
- package/dist/commands/secrets.js +38 -39
- 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/ssh.js +30 -8
- package/dist/commands/teams.js +12 -1
- package/dist/commands/workflows.js +1 -1
- package/dist/index.js +9 -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/devices/resolve-target.d.ts +4 -3
- package/dist/lib/devices/resolve-target.js +4 -3
- package/dist/lib/hosts/passthrough.js +18 -1
- package/dist/lib/hosts/registry.d.ts +4 -0
- package/dist/lib/hosts/registry.js +16 -0
- package/dist/lib/hosts/remote-cmd.js +1 -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/plugins.d.ts +14 -5
- package/dist/lib/plugins.js +29 -5
- package/dist/lib/redact.js +4 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources/workflows.d.ts +1 -1
- package/dist/lib/resources/workflows.js +22 -20
- 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/secrets/bundles.d.ts +9 -0
- package/dist/lib/secrets/bundles.js +9 -23
- package/dist/lib/secrets/headless.d.ts +3 -15
- package/dist/lib/secrets/headless.js +5 -33
- package/dist/lib/secrets/index.d.ts +8 -0
- package/dist/lib/secrets/index.js +16 -36
- 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/share/config.js +5 -7
- package/dist/lib/startup/command-registry.js +15 -1
- package/dist/lib/types.d.ts +6 -0
- 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/dist/lib/workflows.d.ts +19 -4
- package/dist/lib/workflows.js +81 -7
- package/package.json +1 -1
|
@@ -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 —
|
|
@@ -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) {
|
|
@@ -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';
|
|
@@ -396,7 +397,7 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
396
397
|
const deviceFlag = flagValue(allArgs, 'device');
|
|
397
398
|
const hostsFlag = flagValue(allArgs, 'hosts');
|
|
398
399
|
const devicesFlag = flagValue(allArgs, 'devices');
|
|
399
|
-
|
|
400
|
+
let hostName = hostFlag ?? deviceFlag;
|
|
400
401
|
const fleetAll = isFleetAllSentinel(hostFlag, deviceFlag, hostsFlag, devicesFlag);
|
|
401
402
|
// Proceed when any routing flag is present, including the plural fleet flags
|
|
402
403
|
// that may carry the `all` sentinel.
|
|
@@ -460,6 +461,22 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
460
461
|
// flags and bare flags were already handled.
|
|
461
462
|
if (!hostName)
|
|
462
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
|
+
}
|
|
463
480
|
// Running against your own machine is just a local run — skip the SSH round-trip.
|
|
464
481
|
// Match EVERY identity the box answers to (short id, loopback, tailscale
|
|
465
482
|
// dnsName), not just machineId() — a `--host <self-dnsName>` used to slip past a
|
|
@@ -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
|
}
|
|
@@ -113,6 +113,7 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
113
113
|
lease: 'local-only',
|
|
114
114
|
box: 'local-only',
|
|
115
115
|
keepBox: 'local-only',
|
|
116
|
+
fresh: 'local-only', // skips the warm-pool reuse for --lease; the lease path is always local
|
|
116
117
|
reuse: 'local-only', // reuse-picker choice for --lease; the lease path is always local
|
|
117
118
|
bare: 'local-only', // skips the local setup-copy push; lease-only concern
|
|
118
119
|
tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observe-umbrella aliases (Phase 3 surface consolidation).
|
|
3
|
+
*
|
|
4
|
+
* Thin name → real command expansion. No store merge — feed / sessions / events
|
|
5
|
+
* remain the stores; these are doors that point at the right reader.
|
|
6
|
+
*
|
|
7
|
+
* inbox → feed (needs-you default)
|
|
8
|
+
* timeline → feed --filter updates (agent progress stream)
|
|
9
|
+
* roster → sessions --active (live agent roster)
|
|
10
|
+
*
|
|
11
|
+
* `audit` is NOT an alias here — `agents audit` is already the tamper-evident
|
|
12
|
+
* run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
|
|
13
|
+
*/
|
|
14
|
+
export type ObserveAlias = 'inbox' | 'timeline' | 'roster';
|
|
15
|
+
export declare const OBSERVE_ALIASES: readonly ObserveAlias[];
|
|
16
|
+
export interface ObserveExpandResult {
|
|
17
|
+
/** argv for the real command (no program name): e.g. ['feed', '--filter', 'updates'] */
|
|
18
|
+
argv: string[];
|
|
19
|
+
/** One-line note for stderr (optional); empty when silent. */
|
|
20
|
+
note: string;
|
|
21
|
+
}
|
|
22
|
+
/** True when `rest` already carries a `--filter` / `--filter=…` flag. */
|
|
23
|
+
export declare function hasFilterFlag(rest: readonly string[]): boolean;
|
|
24
|
+
/** True when `rest` already carries `--active`. */
|
|
25
|
+
export declare function hasActiveFlag(rest: readonly string[]): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Expand an observe alias + remaining user args into the real command argv.
|
|
28
|
+
* Returns null when `alias` is not an observe alias.
|
|
29
|
+
*/
|
|
30
|
+
export declare function expandObserveAlias(alias: string, rest?: readonly string[]): ObserveExpandResult | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observe-umbrella aliases (Phase 3 surface consolidation).
|
|
3
|
+
*
|
|
4
|
+
* Thin name → real command expansion. No store merge — feed / sessions / events
|
|
5
|
+
* remain the stores; these are doors that point at the right reader.
|
|
6
|
+
*
|
|
7
|
+
* inbox → feed (needs-you default)
|
|
8
|
+
* timeline → feed --filter updates (agent progress stream)
|
|
9
|
+
* roster → sessions --active (live agent roster)
|
|
10
|
+
*
|
|
11
|
+
* `audit` is NOT an alias here — `agents audit` is already the tamper-evident
|
|
12
|
+
* run-dispatch log. Ops trail = `agents events` (optionally `--audit`).
|
|
13
|
+
*/
|
|
14
|
+
export const OBSERVE_ALIASES = ['inbox', 'timeline', 'roster'];
|
|
15
|
+
/** True when `rest` already carries a `--filter` / `--filter=…` flag. */
|
|
16
|
+
export function hasFilterFlag(rest) {
|
|
17
|
+
return rest.some((a) => a === '--filter' || a.startsWith('--filter='));
|
|
18
|
+
}
|
|
19
|
+
/** True when `rest` already carries `--active`. */
|
|
20
|
+
export function hasActiveFlag(rest) {
|
|
21
|
+
return rest.some((a) => a === '--active');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Expand an observe alias + remaining user args into the real command argv.
|
|
25
|
+
* Returns null when `alias` is not an observe alias.
|
|
26
|
+
*/
|
|
27
|
+
export function expandObserveAlias(alias, rest = []) {
|
|
28
|
+
const tail = [...rest];
|
|
29
|
+
switch (alias) {
|
|
30
|
+
case 'inbox':
|
|
31
|
+
return {
|
|
32
|
+
argv: ['feed', ...tail],
|
|
33
|
+
note: 'agents inbox → agents feed (needs-you inbox)',
|
|
34
|
+
};
|
|
35
|
+
case 'timeline': {
|
|
36
|
+
const argv = hasFilterFlag(tail)
|
|
37
|
+
? ['feed', ...tail]
|
|
38
|
+
: ['feed', '--filter', 'updates', ...tail];
|
|
39
|
+
return {
|
|
40
|
+
argv,
|
|
41
|
+
note: 'agents timeline → agents feed --filter updates',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
case 'roster': {
|
|
45
|
+
const argv = hasActiveFlag(tail)
|
|
46
|
+
? ['sessions', ...tail]
|
|
47
|
+
: ['sessions', '--active', ...tail];
|
|
48
|
+
return {
|
|
49
|
+
argv,
|
|
50
|
+
note: 'agents roster → agents sessions --active',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Plugin discovery, validation, and syncing.
|
|
3
3
|
*
|
|
4
4
|
* Plugins are bundles in ~/.agents/plugins/ that package skills, hooks,
|
|
5
|
-
* commands, agents, bin scripts, MCP servers, and settings under a
|
|
6
|
-
* manifest (plugin.json). They are user-authored resources, sitting
|
|
7
|
-
* skills/, commands/, hooks/, etc. — git-tracked as source of truth.
|
|
8
|
-
* module discovers plugins, validates their manifests, and syncs their
|
|
9
|
-
* contents into agent version homes.
|
|
5
|
+
* commands, agents, workflows, bin scripts, MCP servers, and settings under a
|
|
6
|
+
* single manifest (plugin.json). They are user-authored resources, sitting
|
|
7
|
+
* alongside skills/, commands/, hooks/, etc. — git-tracked as source of truth.
|
|
8
|
+
* This module discovers plugins, validates their manifests, and syncs their
|
|
9
|
+
* contents into agent version homes. Workflows under a plugin’s workflows/
|
|
10
|
+
* are resolved at run time by resolveWorkflowRef (Phase 5 packaging).
|
|
10
11
|
*/
|
|
11
12
|
import type { AgentId, DiscoveredPlugin, PluginManifest, MarketplaceSpec } from './types.js';
|
|
12
13
|
export interface PluginCapabilities {
|
|
@@ -95,6 +96,14 @@ export declare function discoverPluginHooks(pluginRoot: string): string[];
|
|
|
95
96
|
export declare function discoverPluginCommands(pluginRoot: string): string[];
|
|
96
97
|
/** Discover agent definition .md files inside a plugin's agents/ directory. */
|
|
97
98
|
export declare function discoverPluginAgentDefs(pluginRoot: string): string[];
|
|
99
|
+
/**
|
|
100
|
+
* Discover workflow directories inside a plugin's `workflows/` folder.
|
|
101
|
+
* A valid workflow is a directory containing WORKFLOW.md (same contract as
|
|
102
|
+
* project/user/system workflows). Phase 5: plugins package workflows as
|
|
103
|
+
* entrypoints so `agents run <name>` can resolve them without a separate
|
|
104
|
+
* install into ~/.agents/workflows/.
|
|
105
|
+
*/
|
|
106
|
+
export declare function discoverPluginWorkflows(pluginRoot: string): string[];
|
|
98
107
|
/** Discover executable files in a plugin's bin/ directory. */
|
|
99
108
|
export declare function discoverPluginBin(pluginRoot: string): string[];
|
|
100
109
|
/** Discover MCP server names from .mcp.json at the plugin root. */
|
package/dist/lib/plugins.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Plugin discovery, validation, and syncing.
|
|
3
3
|
*
|
|
4
4
|
* Plugins are bundles in ~/.agents/plugins/ that package skills, hooks,
|
|
5
|
-
* commands, agents, bin scripts, MCP servers, and settings under a
|
|
6
|
-
* manifest (plugin.json). They are user-authored resources, sitting
|
|
7
|
-
* skills/, commands/, hooks/, etc. — git-tracked as source of truth.
|
|
8
|
-
* module discovers plugins, validates their manifests, and syncs their
|
|
9
|
-
* contents into agent version homes.
|
|
5
|
+
* commands, agents, workflows, bin scripts, MCP servers, and settings under a
|
|
6
|
+
* single manifest (plugin.json). They are user-authored resources, sitting
|
|
7
|
+
* alongside skills/, commands/, hooks/, etc. — git-tracked as source of truth.
|
|
8
|
+
* This module discovers plugins, validates their manifests, and syncs their
|
|
9
|
+
* contents into agent version homes. Workflows under a plugin’s workflows/
|
|
10
|
+
* are resolved at run time by resolveWorkflowRef (Phase 5 packaging).
|
|
10
11
|
*/
|
|
11
12
|
import * as fs from 'fs';
|
|
12
13
|
import * as path from 'path';
|
|
@@ -107,6 +108,7 @@ export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user
|
|
|
107
108
|
scripts: discoverPluginScripts(pluginRoot),
|
|
108
109
|
commands: discoverPluginCommands(pluginRoot),
|
|
109
110
|
agentDefs: discoverPluginAgentDefs(pluginRoot),
|
|
111
|
+
workflows: discoverPluginWorkflows(pluginRoot),
|
|
110
112
|
memory: discoverPluginMemory(pluginRoot),
|
|
111
113
|
bin: discoverPluginBin(pluginRoot),
|
|
112
114
|
mcpServers: discoverPluginMcpServers(pluginRoot),
|
|
@@ -131,6 +133,7 @@ export function pluginResourceGroups(plugin) {
|
|
|
131
133
|
{ label: 'skills', items: plugin.skills.map((s) => `/${plugin.name}:${s}`) },
|
|
132
134
|
{ label: 'commands', items: plugin.commands.map((c) => `/${plugin.name}:${c}`) },
|
|
133
135
|
{ label: 'subagents', items: plugin.agentDefs },
|
|
136
|
+
{ label: 'workflows', items: plugin.workflows },
|
|
134
137
|
{ label: 'hooks', items: plugin.hooks },
|
|
135
138
|
{ label: 'memory', items: plugin.memory },
|
|
136
139
|
{ label: 'mcp', items: plugin.mcpServers },
|
|
@@ -334,6 +337,27 @@ export function discoverPluginAgentDefs(pluginRoot) {
|
|
|
334
337
|
.filter(f => f.endsWith('.md') && !f.startsWith('.'))
|
|
335
338
|
.map(f => f.slice(0, -3));
|
|
336
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Discover workflow directories inside a plugin's `workflows/` folder.
|
|
342
|
+
* A valid workflow is a directory containing WORKFLOW.md (same contract as
|
|
343
|
+
* project/user/system workflows). Phase 5: plugins package workflows as
|
|
344
|
+
* entrypoints so `agents run <name>` can resolve them without a separate
|
|
345
|
+
* install into ~/.agents/workflows/.
|
|
346
|
+
*/
|
|
347
|
+
export function discoverPluginWorkflows(pluginRoot) {
|
|
348
|
+
const workflowsDir = path.join(pluginRoot, 'workflows');
|
|
349
|
+
if (!fs.existsSync(workflowsDir))
|
|
350
|
+
return [];
|
|
351
|
+
try {
|
|
352
|
+
return fs.readdirSync(workflowsDir, { withFileTypes: true })
|
|
353
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') &&
|
|
354
|
+
fs.existsSync(path.join(workflowsDir, e.name, 'WORKFLOW.md')))
|
|
355
|
+
.map((e) => e.name);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
337
361
|
/** Discover executable files in a plugin's bin/ directory. */
|
|
338
362
|
export function discoverPluginBin(pluginRoot) {
|
|
339
363
|
const binDir = path.join(pluginRoot, 'bin');
|
package/dist/lib/redact.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Shared redaction helpers for text that may be exported or logged.
|
|
3
3
|
*/
|
|
4
4
|
const SECRET_PATTERNS = [
|
|
5
|
+
// Local home paths identify operators and disclose internal filesystem layout.
|
|
6
|
+
// Keep the useful path suffix while masking the machine-specific home prefix.
|
|
7
|
+
[/(^|[\s,"'`(=:])\/(?:home|Users)\/[^/\s,"'`]+/g, '$1[HOME]'],
|
|
8
|
+
[/(^|[\s,"'`(=])[A-Z]:\\Users\\[^\\\s,"'`]+/gi, '$1[HOME]'],
|
|
5
9
|
[/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED_AWS_KEY]'],
|
|
6
10
|
// GitHub: classic PATs (ghp_), OAuth (gho_), app/refresh/server tokens
|
|
7
11
|
// (ghs_/ghr_), and fine-grained PATs (github_pat_). All share the 36-char
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - Override on name conflict: Higher layer wins (project > user > system)
|
|
7
7
|
*/
|
|
8
8
|
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi';
|
|
9
|
-
|
|
9
|
+
/** Resource origin. Precedence (highest first): project > user > plugin > system. */
|
|
10
|
+
export type Layer = 'system' | 'user' | 'project' | 'plugin';
|
|
10
11
|
export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
|
|
11
12
|
/** A resolved resource with its origin layer. */
|
|
12
13
|
export interface ResolvedItem<T> {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
|
|
5
5
|
* They optionally contain subagents/, skills/, and plugins/ subdirectories.
|
|
6
|
-
* Resolution order: project > user > system.
|
|
6
|
+
* Resolution order (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentId, ResolvedItem, ResourceHandler } from './types.js';
|
|
9
9
|
export interface WorkflowItem {
|