@phnx-labs/agents-cli 1.20.64 → 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 +39 -3
- package/README.md +37 -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 +94 -49
- package/dist/commands/feed.js +25 -11
- package/dist/commands/hosts.js +44 -6
- 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 +23 -2
- 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.js +8 -3
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +36 -6
- package/dist/commands/ssh.js +45 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +26 -0
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +34 -14
- package/dist/lib/agents.d.ts +18 -0
- package/dist/lib/agents.js +53 -3
- 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/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/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.js +5 -1
- package/dist/lib/crabbox/runtimes.js +11 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +62 -19
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -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/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/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 +16 -0
- package/dist/lib/routines.js +19 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +102 -9
- 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 +14 -0
- package/dist/lib/session/bundle.js +12 -1
- 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/config.js +8 -2
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +86 -7
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- 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 +26 -1
- package/dist/lib/usage.js +7 -5
- 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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native per-monitor state-diff store.
|
|
3
|
+
*
|
|
4
|
+
* This is the one genuinely new primitive monitors add over routines: a
|
|
5
|
+
* last-observed-*value* store. Routines persist per-*run* metadata but have no
|
|
6
|
+
* last-seen value, so hand-built watchers (the RUSH-1107 SSL watcher) re-invented
|
|
7
|
+
* state-diffing through a markdown memory file every time. This store kills that.
|
|
8
|
+
*
|
|
9
|
+
* Layout (sibling of the runs layout, atomic writes like writeRunMeta):
|
|
10
|
+
* ~/.agents/.history/monitors/<name>/state.json # last-seen hash/value + fire bookkeeping
|
|
11
|
+
* ~/.agents/.history/monitors/<name>/fires/<id>/… # fire history
|
|
12
|
+
*/
|
|
13
|
+
import type { MonitorEvent } from './config.js';
|
|
14
|
+
/** Persisted last-seen state for one monitor. */
|
|
15
|
+
export interface MonitorState {
|
|
16
|
+
monitorName: string;
|
|
17
|
+
/** Hash of the last-seen de-dupe signature (see hasChanged). */
|
|
18
|
+
lastHash: string;
|
|
19
|
+
/** The last-seen raw observation (truncated for storage). */
|
|
20
|
+
lastValue: string;
|
|
21
|
+
/** RFC3339 timestamp of the last observation. */
|
|
22
|
+
lastSeenAt: string;
|
|
23
|
+
/** RFC3339 timestamp of the last fire, when the monitor has ever fired. */
|
|
24
|
+
lastFiredAt?: string;
|
|
25
|
+
/** Epoch-ms timestamps of recent fires, for the rate-limit / firehose guard. */
|
|
26
|
+
fireTimes?: number[];
|
|
27
|
+
}
|
|
28
|
+
/** Per-monitor history root, with the (untrusted) name contained to one segment. */
|
|
29
|
+
export declare function getMonitorHistoryDir(name: string): string;
|
|
30
|
+
/** Directory holding a monitor's fire history. */
|
|
31
|
+
export declare function getMonitorFiresDir(name: string): string;
|
|
32
|
+
/** Read a monitor's last-seen state, or null if it has never been observed. */
|
|
33
|
+
export declare function readState(name: string): MonitorState | null;
|
|
34
|
+
/** Persist a monitor's state atomically (temp file + rename, like writeRunMeta). */
|
|
35
|
+
export declare function writeStateRaw(state: MonitorState): void;
|
|
36
|
+
/**
|
|
37
|
+
* Record a new observation as the monitor's last-seen state, preserving fire
|
|
38
|
+
* bookkeeping. Truncates the stored raw value so a firehose can't bloat disk.
|
|
39
|
+
*/
|
|
40
|
+
export declare function writeState(name: string, value: string, dedupeKey?: string, extra?: Partial<Pick<MonitorState, 'lastFiredAt' | 'fireTimes'>>): MonitorState;
|
|
41
|
+
/**
|
|
42
|
+
* The de-dupe signature for an observation. When `dedupeKey` is set, the
|
|
43
|
+
* signature is the first regex match of dedupeKey against the observation (so
|
|
44
|
+
* "the same event" is same matched token); otherwise it is the full observation.
|
|
45
|
+
* An unmatched dedupeKey falls back to the full observation.
|
|
46
|
+
*/
|
|
47
|
+
export declare function dedupeSignature(observation: string, dedupeKey?: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* True when `observation`'s de-dupe signature differs from the monitor's
|
|
50
|
+
* last-seen signature (or the monitor has never been observed). Pure read — the
|
|
51
|
+
* caller persists the new value via writeState only on a real fire.
|
|
52
|
+
*/
|
|
53
|
+
export declare function hasChanged(name: string, observation: string, dedupeKey?: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Append a fire timestamp and return the pruned window (fires within `windowMs`).
|
|
56
|
+
* The engine uses the returned length to decide whether the rate limit tripped.
|
|
57
|
+
*/
|
|
58
|
+
export declare function recordFireTime(name: string, now: number, windowMs: number): number[];
|
|
59
|
+
/** Write a fire record to fires/<id>/event.json and return the fire id. */
|
|
60
|
+
export declare function writeFireRecord(event: MonitorEvent, meta?: Record<string, unknown>): string;
|
|
61
|
+
/** A single fire history entry (as read back from disk). */
|
|
62
|
+
export interface FireRecord extends MonitorEvent {
|
|
63
|
+
runId?: string;
|
|
64
|
+
action?: string;
|
|
65
|
+
ok?: boolean;
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
/** List a monitor's fire history, chronologically ascending. */
|
|
69
|
+
export declare function listFires(name: string): FireRecord[];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native per-monitor state-diff store.
|
|
3
|
+
*
|
|
4
|
+
* This is the one genuinely new primitive monitors add over routines: a
|
|
5
|
+
* last-observed-*value* store. Routines persist per-*run* metadata but have no
|
|
6
|
+
* last-seen value, so hand-built watchers (the RUSH-1107 SSL watcher) re-invented
|
|
7
|
+
* state-diffing through a markdown memory file every time. This store kills that.
|
|
8
|
+
*
|
|
9
|
+
* Layout (sibling of the runs layout, atomic writes like writeRunMeta):
|
|
10
|
+
* ~/.agents/.history/monitors/<name>/state.json # last-seen hash/value + fire bookkeeping
|
|
11
|
+
* ~/.agents/.history/monitors/<name>/fires/<id>/… # fire history
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
import { getMonitorsHistoryDir, ensureAgentsDir } from '../state.js';
|
|
17
|
+
import { safeJoin } from '../paths.js';
|
|
18
|
+
const MAX_STORED_VALUE = 4096;
|
|
19
|
+
/** Per-monitor history root, with the (untrusted) name contained to one segment. */
|
|
20
|
+
export function getMonitorHistoryDir(name) {
|
|
21
|
+
return safeJoin(getMonitorsHistoryDir(), name);
|
|
22
|
+
}
|
|
23
|
+
function getStatePath(name) {
|
|
24
|
+
return path.join(getMonitorHistoryDir(name), 'state.json');
|
|
25
|
+
}
|
|
26
|
+
/** Directory holding a monitor's fire history. */
|
|
27
|
+
export function getMonitorFiresDir(name) {
|
|
28
|
+
return path.join(getMonitorHistoryDir(name), 'fires');
|
|
29
|
+
}
|
|
30
|
+
/** Read a monitor's last-seen state, or null if it has never been observed. */
|
|
31
|
+
export function readState(name) {
|
|
32
|
+
const statePath = getStatePath(name);
|
|
33
|
+
if (!fs.existsSync(statePath))
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Persist a monitor's state atomically (temp file + rename, like writeRunMeta). */
|
|
43
|
+
export function writeStateRaw(state) {
|
|
44
|
+
ensureAgentsDir();
|
|
45
|
+
const dir = getMonitorHistoryDir(state.monitorName);
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
const statePath = path.join(dir, 'state.json');
|
|
48
|
+
const tmp = `${statePath}.tmp-${process.pid}`;
|
|
49
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf-8');
|
|
50
|
+
fs.renameSync(tmp, statePath);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Record a new observation as the monitor's last-seen state, preserving fire
|
|
54
|
+
* bookkeeping. Truncates the stored raw value so a firehose can't bloat disk.
|
|
55
|
+
*/
|
|
56
|
+
export function writeState(name, value, dedupeKey, extra = {}) {
|
|
57
|
+
const prev = readState(name);
|
|
58
|
+
const state = {
|
|
59
|
+
monitorName: name,
|
|
60
|
+
lastHash: hashSignature(value, dedupeKey),
|
|
61
|
+
lastValue: value.length > MAX_STORED_VALUE ? value.slice(0, MAX_STORED_VALUE) : value,
|
|
62
|
+
lastSeenAt: new Date().toISOString(),
|
|
63
|
+
...(prev?.lastFiredAt ? { lastFiredAt: prev.lastFiredAt } : {}),
|
|
64
|
+
...(prev?.fireTimes ? { fireTimes: prev.fireTimes } : {}),
|
|
65
|
+
...extra,
|
|
66
|
+
};
|
|
67
|
+
writeStateRaw(state);
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The de-dupe signature for an observation. When `dedupeKey` is set, the
|
|
72
|
+
* signature is the first regex match of dedupeKey against the observation (so
|
|
73
|
+
* "the same event" is same matched token); otherwise it is the full observation.
|
|
74
|
+
* An unmatched dedupeKey falls back to the full observation.
|
|
75
|
+
*/
|
|
76
|
+
export function dedupeSignature(observation, dedupeKey) {
|
|
77
|
+
if (!dedupeKey)
|
|
78
|
+
return observation;
|
|
79
|
+
try {
|
|
80
|
+
const m = new RegExp(dedupeKey).exec(observation);
|
|
81
|
+
if (m)
|
|
82
|
+
return m[1] ?? m[0];
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* invalid regex — fall back to full observation */
|
|
86
|
+
}
|
|
87
|
+
return observation;
|
|
88
|
+
}
|
|
89
|
+
function hashSignature(observation, dedupeKey) {
|
|
90
|
+
return createHash('sha256').update(dedupeSignature(observation, dedupeKey)).digest('hex');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* True when `observation`'s de-dupe signature differs from the monitor's
|
|
94
|
+
* last-seen signature (or the monitor has never been observed). Pure read — the
|
|
95
|
+
* caller persists the new value via writeState only on a real fire.
|
|
96
|
+
*/
|
|
97
|
+
export function hasChanged(name, observation, dedupeKey) {
|
|
98
|
+
const prev = readState(name);
|
|
99
|
+
if (!prev)
|
|
100
|
+
return true;
|
|
101
|
+
return prev.lastHash !== hashSignature(observation, dedupeKey);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Append a fire timestamp and return the pruned window (fires within `windowMs`).
|
|
105
|
+
* The engine uses the returned length to decide whether the rate limit tripped.
|
|
106
|
+
*/
|
|
107
|
+
export function recordFireTime(name, now, windowMs) {
|
|
108
|
+
const prev = readState(name);
|
|
109
|
+
const times = [...(prev?.fireTimes ?? []), now].filter((t) => now - t <= windowMs);
|
|
110
|
+
return times;
|
|
111
|
+
}
|
|
112
|
+
/** Write a fire record to fires/<id>/event.json and return the fire id. */
|
|
113
|
+
export function writeFireRecord(event, meta = {}) {
|
|
114
|
+
ensureAgentsDir();
|
|
115
|
+
const fireId = event.firedAt.replace(/[:.]/g, '-');
|
|
116
|
+
const fireDir = safeJoin(getMonitorFiresDir(event.monitorName), fireId);
|
|
117
|
+
fs.mkdirSync(fireDir, { recursive: true });
|
|
118
|
+
fs.writeFileSync(path.join(fireDir, 'event.json'), JSON.stringify({ ...event, ...meta }, null, 2), 'utf-8');
|
|
119
|
+
return fireId;
|
|
120
|
+
}
|
|
121
|
+
/** List a monitor's fire history, chronologically ascending. */
|
|
122
|
+
export function listFires(name) {
|
|
123
|
+
const dir = getMonitorFiresDir(name);
|
|
124
|
+
if (!fs.existsSync(dir))
|
|
125
|
+
return [];
|
|
126
|
+
const ids = fs
|
|
127
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
128
|
+
.filter((e) => e.isDirectory())
|
|
129
|
+
.map((e) => e.name)
|
|
130
|
+
.sort();
|
|
131
|
+
const fires = [];
|
|
132
|
+
for (const id of ids) {
|
|
133
|
+
const eventPath = path.join(dir, id, 'event.json');
|
|
134
|
+
if (!fs.existsSync(eventPath))
|
|
135
|
+
continue;
|
|
136
|
+
try {
|
|
137
|
+
fires.push(JSON.parse(fs.readFileSync(eventPath, 'utf-8')));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* skip corrupt record */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return fires;
|
|
144
|
+
}
|
|
@@ -66,3 +66,19 @@ export declare function quoteWin32ExecArg(arg: string): string;
|
|
|
66
66
|
* line is byte-identical to the old unquoted join for the common case.
|
|
67
67
|
*/
|
|
68
68
|
export declare function composeWin32CommandLine(command: string, args: string[]): string;
|
|
69
|
+
/**
|
|
70
|
+
* DEP0190-safe execFile/spawn spec when the binary may need `shell: true` on Windows.
|
|
71
|
+
*
|
|
72
|
+
* When `needsWindowsShell(bin)` is true, compose ONE fully-quoted command line via
|
|
73
|
+
* `composeWin32CommandLine` and return an EMPTY args array so Node never concatenates
|
|
74
|
+
* user-controlled argv (MCP command/args, prompts, …) into the cmd.exe line unescaped.
|
|
75
|
+
* Off that path the original bin+args are returned unchanged.
|
|
76
|
+
*
|
|
77
|
+
* Call as: `const s = execFileShellSpec(bin, args); execFile(s.command, s.args, { shell: s.shell })`.
|
|
78
|
+
* Pure; optional `platform` override keeps the win32 branch unit-testable on any host.
|
|
79
|
+
*/
|
|
80
|
+
export declare function execFileShellSpec(bin: string, args: string[], platform?: NodeJS.Platform): {
|
|
81
|
+
command: string;
|
|
82
|
+
args: string[];
|
|
83
|
+
shell: boolean;
|
|
84
|
+
};
|
|
@@ -119,3 +119,20 @@ export function quoteWin32ExecArg(arg) {
|
|
|
119
119
|
export function composeWin32CommandLine(command, args) {
|
|
120
120
|
return [command, ...args].map(quoteWin32ExecArg).join(' ');
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* DEP0190-safe execFile/spawn spec when the binary may need `shell: true` on Windows.
|
|
124
|
+
*
|
|
125
|
+
* When `needsWindowsShell(bin)` is true, compose ONE fully-quoted command line via
|
|
126
|
+
* `composeWin32CommandLine` and return an EMPTY args array so Node never concatenates
|
|
127
|
+
* user-controlled argv (MCP command/args, prompts, …) into the cmd.exe line unescaped.
|
|
128
|
+
* Off that path the original bin+args are returned unchanged.
|
|
129
|
+
*
|
|
130
|
+
* Call as: `const s = execFileShellSpec(bin, args); execFile(s.command, s.args, { shell: s.shell })`.
|
|
131
|
+
* Pure; optional `platform` override keeps the win32 branch unit-testable on any host.
|
|
132
|
+
*/
|
|
133
|
+
export function execFileShellSpec(bin, args, platform = process.platform) {
|
|
134
|
+
if (!needsWindowsShell(bin, platform)) {
|
|
135
|
+
return { command: bin, args, shell: false };
|
|
136
|
+
}
|
|
137
|
+
return { command: composeWin32CommandLine(bin, args), args: [], shell: true };
|
|
138
|
+
}
|
package/dist/lib/plugins.js
CHANGED
|
@@ -134,12 +134,34 @@ export function pluginResourceGroups(plugin) {
|
|
|
134
134
|
out.push({ label: 'settings', items: ['settings.json'] });
|
|
135
135
|
return out;
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* True when a manifest field declares an inline execution surface — a non-empty
|
|
139
|
+
* path string, a non-empty array, or an object with at least one key. The
|
|
140
|
+
* official plugin format lets `hooks`/`mcpServers` live inline in the manifest
|
|
141
|
+
* (a path or an inline map) instead of as a `hooks/` dir or `.mcp.json` file, so
|
|
142
|
+
* filesystem-only detection would miss them and auto-enable a hostile plugin.
|
|
143
|
+
*/
|
|
144
|
+
function manifestDeclaresExecSurface(value) {
|
|
145
|
+
if (typeof value === 'string')
|
|
146
|
+
return value.trim().length > 0;
|
|
147
|
+
if (Array.isArray(value))
|
|
148
|
+
return value.length > 0;
|
|
149
|
+
if (value && typeof value === 'object')
|
|
150
|
+
return Object.keys(value).length > 0;
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
137
153
|
export function inspectPluginCapabilities(pluginRoot) {
|
|
138
154
|
const manifest = loadPluginManifest(pluginRoot);
|
|
139
155
|
const plugin = manifest ? buildDiscoveredPlugin(pluginRoot, manifest) : null;
|
|
140
156
|
return {
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
// Inline manifest `hooks`/`mcpServers` are execution surfaces too — a cloned
|
|
158
|
+
// repo's project plugin must not be auto-enabled just because it ships the
|
|
159
|
+
// exec config inline in plugin.json rather than as a hooks/ dir or .mcp.json.
|
|
160
|
+
hasHooks: (plugin?.hooks.length || 0) > 0 ||
|
|
161
|
+
pluginHasDirectoryEntries(pluginRoot, 'hooks') ||
|
|
162
|
+
manifestDeclaresExecSurface(manifest?.hooks),
|
|
163
|
+
hasMcp: fs.existsSync(path.join(pluginRoot, '.mcp.json')) ||
|
|
164
|
+
manifestDeclaresExecSurface(manifest?.mcpServers),
|
|
143
165
|
hasBin: (plugin?.bin.length || 0) > 0,
|
|
144
166
|
hasScripts: (plugin?.scripts.length || 0) > 0,
|
|
145
167
|
hasSettings: pluginHasNonPermissionSettings(pluginRoot),
|
|
@@ -499,6 +521,14 @@ export function syncPluginToVersion(plugin, agent, versionHome, options = {}) {
|
|
|
499
521
|
// OpenCode uses TS/JS modules under ~/.config/opencode/plugins/, not the
|
|
500
522
|
// Claude marketplace layout. Install those modules and return early.
|
|
501
523
|
if (agent === 'opencode') {
|
|
524
|
+
// Trust gate (RUSH-1756): OpenCode plugins are raw executable TS/JS modules,
|
|
525
|
+
// so they must clear the same consent check as every other exec surface
|
|
526
|
+
// before install — this branch used to return early, bypassing the gate the
|
|
527
|
+
// Gemini/Hermes/marketplace branches all apply.
|
|
528
|
+
const enablePlugin = options.allowExecSurfaces === true || !hasPluginExecSurfaces(inspectPluginCapabilities(plugin.root));
|
|
529
|
+
if (!enablePlugin) {
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
502
532
|
const ok = installOpenCodePlugin(plugin, versionHome);
|
|
503
533
|
result.success = ok;
|
|
504
534
|
if (ok)
|
|
@@ -982,6 +1012,72 @@ function writeGeminiExtensionManifest(plugin, destRoot) {
|
|
|
982
1012
|
}
|
|
983
1013
|
fs.writeFileSync(path.join(destRoot, GEMINI_EXTENSION_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
984
1014
|
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Security (RUSH-1755): `fs.cpSync(..., { recursive: true })` copies symlinks
|
|
1017
|
+
* verbatim (dereference defaults to false), so a malicious plugin can ship a
|
|
1018
|
+
* symlink whose target escapes the install root — e.g.
|
|
1019
|
+
* `.agents-cli-managed -> ~/.bashrc`. The managed-marker / manifest writes that
|
|
1020
|
+
* follow the copy would then write THROUGH the link, clobbering an
|
|
1021
|
+
* attacker-chosen path outside the install root.
|
|
1022
|
+
*
|
|
1023
|
+
* Walk destRoot after the recursive copy, lstat each entry, and remove any
|
|
1024
|
+
* symlink whose resolved target escapes BOTH destRoot and sourceRoot. Both roots
|
|
1025
|
+
* matter because Node's cpSync rewrites a *relative* internal symlink
|
|
1026
|
+
* (`./x`) into an *absolute* link back into the source tree, so a legitimate
|
|
1027
|
+
* internal symlink resolves under sourceRoot (not destRoot) after the copy.
|
|
1028
|
+
* Keeping targets within sourceRoot preserves those internal symlinks — matching
|
|
1029
|
+
* copyPluginToMarketplace's policy — while genuinely external escapes are
|
|
1030
|
+
* dropped, neutralizing the write-through.
|
|
1031
|
+
*/
|
|
1032
|
+
function stripEscapingSymlinks(destRoot, sourceRoot) {
|
|
1033
|
+
const realRoots = [destRoot, sourceRoot].map((r) => {
|
|
1034
|
+
try {
|
|
1035
|
+
return fs.realpathSync(r);
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
return r;
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
const within = (target) => realRoots.some((root) => target === root || target.startsWith(root + path.sep));
|
|
1042
|
+
const removed = [];
|
|
1043
|
+
const walk = (dir) => {
|
|
1044
|
+
let entries;
|
|
1045
|
+
try {
|
|
1046
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
for (const entry of entries) {
|
|
1052
|
+
const full = path.join(dir, entry.name);
|
|
1053
|
+
if (entry.isSymbolicLink()) {
|
|
1054
|
+
let escapes;
|
|
1055
|
+
try {
|
|
1056
|
+
escapes = !within(fs.realpathSync(full));
|
|
1057
|
+
}
|
|
1058
|
+
catch {
|
|
1059
|
+
// Dangling / unresolvable symlink — treat as escaping and drop it.
|
|
1060
|
+
escapes = true;
|
|
1061
|
+
}
|
|
1062
|
+
if (escapes) {
|
|
1063
|
+
try {
|
|
1064
|
+
fs.rmSync(full, { force: true });
|
|
1065
|
+
removed.push(path.relative(destRoot, full) || entry.name);
|
|
1066
|
+
}
|
|
1067
|
+
catch { /* best effort */ }
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
else if (entry.isDirectory()) {
|
|
1071
|
+
// Do not descend into symlinked dirs — isDirectory() is false for a
|
|
1072
|
+
// symlink even when it points at a directory, so this only recurses
|
|
1073
|
+
// into real subdirectories, keeping the walk inside destRoot.
|
|
1074
|
+
walk(full);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
walk(destRoot);
|
|
1079
|
+
return removed;
|
|
1080
|
+
}
|
|
985
1081
|
export function installGeminiPlugin(plugin, versionHome) {
|
|
986
1082
|
const destRoot = path.join(geminiExtensionsDir(versionHome), plugin.name);
|
|
987
1083
|
try {
|
|
@@ -989,6 +1085,7 @@ export function installGeminiPlugin(plugin, versionHome) {
|
|
|
989
1085
|
fs.rmSync(destRoot, { recursive: true, force: true });
|
|
990
1086
|
}
|
|
991
1087
|
fs.cpSync(plugin.root, destRoot, { recursive: true });
|
|
1088
|
+
stripEscapingSymlinks(destRoot, plugin.root);
|
|
992
1089
|
const userConfig = loadUserConfig(plugin.name);
|
|
993
1090
|
if (Object.keys(userConfig).length > 0) {
|
|
994
1091
|
expandUserConfigInDir(destRoot, userConfig);
|
|
@@ -1030,6 +1127,7 @@ export function installGoosePlugin(plugin, versionHome) {
|
|
|
1030
1127
|
fs.rmSync(destRoot, { recursive: true, force: true });
|
|
1031
1128
|
}
|
|
1032
1129
|
fs.cpSync(plugin.root, destRoot, { recursive: true });
|
|
1130
|
+
stripEscapingSymlinks(destRoot, plugin.root);
|
|
1033
1131
|
fs.writeFileSync(path.join(destRoot, '.agents-cli-managed'), `plugin=${plugin.name}\n`, 'utf-8');
|
|
1034
1132
|
return true;
|
|
1035
1133
|
}
|
|
@@ -1111,6 +1209,7 @@ export function installHermesPlugin(plugin, versionHome, enable) {
|
|
|
1111
1209
|
fs.rmSync(destRoot, { recursive: true, force: true });
|
|
1112
1210
|
}
|
|
1113
1211
|
fs.cpSync(plugin.root, destRoot, { recursive: true });
|
|
1212
|
+
stripEscapingSymlinks(destRoot, plugin.root);
|
|
1114
1213
|
const userConfig = loadUserConfig(plugin.name);
|
|
1115
1214
|
if (Object.keys(userConfig).length > 0) {
|
|
1116
1215
|
expandUserConfigInDir(destRoot, userConfig);
|
package/dist/lib/redact.d.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared redaction helpers for text that may be exported or logged.
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Scrub secrets from `text`. Two passes: format-based patterns (above), then a
|
|
6
|
+
* value-aware pass that masks any `knownValues` verbatim — a credential we
|
|
7
|
+
* already hold in hand leaks regardless of its format, so an exact-value match
|
|
8
|
+
* catches tokens the regexes don't recognize.
|
|
9
|
+
*/
|
|
10
|
+
export declare function redactSecrets(text: string, knownValues?: readonly string[]): string;
|
|
11
|
+
/**
|
|
12
|
+
* Secret values already present in the environment (e.g. an injected secrets
|
|
13
|
+
* bundle), selected by secret-shaped var NAME. These are the "known" values fed
|
|
14
|
+
* to {@link redactSecrets} so an exported transcript can't leak a live
|
|
15
|
+
* credential verbatim even when its format matches no pattern.
|
|
16
|
+
*/
|
|
17
|
+
export declare function knownSecretValuesFromEnv(env?: NodeJS.ProcessEnv): string[];
|
package/dist/lib/redact.js
CHANGED
|
@@ -3,17 +3,63 @@
|
|
|
3
3
|
*/
|
|
4
4
|
const SECRET_PATTERNS = [
|
|
5
5
|
[/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED_AWS_KEY]'],
|
|
6
|
+
// GitHub: classic PATs (ghp_), OAuth (gho_), app/refresh/server tokens
|
|
7
|
+
// (ghs_/ghr_), and fine-grained PATs (github_pat_). All share the 36-char
|
|
8
|
+
// classic body; fine-grained tokens are longer, so match greedily.
|
|
6
9
|
[/\bghp_[A-Za-z0-9]{36}\b/g, '[REDACTED_GITHUB_TOKEN]'],
|
|
10
|
+
[/\bgh[osru]_[A-Za-z0-9]{36}\b/g, '[REDACTED_GITHUB_TOKEN]'],
|
|
11
|
+
[/\bgithub_pat_[A-Za-z0-9_]{22,}\b/g, '[REDACTED_GITHUB_TOKEN]'],
|
|
12
|
+
// Anthropic keys (sk-ant-api03-…) before the generic sk- rule so the marker
|
|
13
|
+
// is specific; the generic rule would otherwise swallow it first.
|
|
14
|
+
[/\bsk-ant-api03-[A-Za-z0-9_-]{20,}\b/g, '[REDACTED_ANTHROPIC_KEY]'],
|
|
15
|
+
// Stripe live secret / restricted keys.
|
|
16
|
+
[/\b[rs]k_live_[A-Za-z0-9]{20,}\b/g, '[REDACTED_STRIPE_KEY]'],
|
|
7
17
|
[/\bsk-[A-Za-z0-9]{20,}\b/g, '[REDACTED_API_KEY]'],
|
|
18
|
+
// Slack bot/user/app-level tokens (xoxb-/xoxp-/xapp-…).
|
|
19
|
+
[/\bxox[bp]-[A-Za-z0-9-]{10,}\b/g, '[REDACTED_SLACK_TOKEN]'],
|
|
20
|
+
[/\bxapp-[A-Za-z0-9-]{10,}\b/g, '[REDACTED_SLACK_TOKEN]'],
|
|
8
21
|
[/\bnpm_[A-Za-z0-9]{36}\b/g, '[REDACTED_NPM_TOKEN]'],
|
|
9
22
|
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '[REDACTED_JWT]'],
|
|
10
23
|
[/Bearer\s+\S+/gi, 'Bearer [REDACTED]'],
|
|
11
24
|
[/\b([A-Z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD)[A-Z0-9_]*)=("[^"]*"|'[^']*'|\S+)/gi, '$1=[REDACTED]'],
|
|
12
25
|
];
|
|
13
|
-
|
|
26
|
+
/** Env vars whose NAME marks their VALUE as a credential worth masking literally. */
|
|
27
|
+
const SECRET_ENV_NAME = /(?:TOKEN|KEY|SECRET|PASSWORD)/i;
|
|
28
|
+
/** Don't literal-mask trivially short values — they collide with ordinary text. */
|
|
29
|
+
const MIN_KNOWN_VALUE_LEN = 6;
|
|
30
|
+
/**
|
|
31
|
+
* Scrub secrets from `text`. Two passes: format-based patterns (above), then a
|
|
32
|
+
* value-aware pass that masks any `knownValues` verbatim — a credential we
|
|
33
|
+
* already hold in hand leaks regardless of its format, so an exact-value match
|
|
34
|
+
* catches tokens the regexes don't recognize.
|
|
35
|
+
*/
|
|
36
|
+
export function redactSecrets(text, knownValues) {
|
|
14
37
|
let safe = text;
|
|
15
38
|
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
16
39
|
safe = safe.replace(pattern, replacement);
|
|
17
40
|
}
|
|
41
|
+
if (knownValues) {
|
|
42
|
+
for (const value of knownValues) {
|
|
43
|
+
if (value.length < MIN_KNOWN_VALUE_LEN)
|
|
44
|
+
continue;
|
|
45
|
+
safe = safe.split(value).join('[REDACTED]');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
18
48
|
return safe;
|
|
19
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Secret values already present in the environment (e.g. an injected secrets
|
|
52
|
+
* bundle), selected by secret-shaped var NAME. These are the "known" values fed
|
|
53
|
+
* to {@link redactSecrets} so an exported transcript can't leak a live
|
|
54
|
+
* credential verbatim even when its format matches no pattern.
|
|
55
|
+
*/
|
|
56
|
+
export function knownSecretValuesFromEnv(env = process.env) {
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const [name, value] of Object.entries(env)) {
|
|
59
|
+
if (!value || value.length < MIN_KNOWN_VALUE_LEN)
|
|
60
|
+
continue;
|
|
61
|
+
if (SECRET_ENV_NAME.test(name))
|
|
62
|
+
out.push(value);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
@@ -11,7 +11,7 @@ import chalk from 'chalk';
|
|
|
11
11
|
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote } from './ssh-exec.js';
|
|
12
12
|
import { sshTargetFor } from './devices/connect.js';
|
|
13
13
|
import { resolveExplicitTargets } from './devices/resolve-target.js';
|
|
14
|
-
import { loadDevices } from './devices/registry.js';
|
|
14
|
+
import { loadDevices, isControlDevice } from './devices/registry.js';
|
|
15
15
|
import { remoteShellFor, buildWindowsAgentsCommand } from './hosts/remote-cmd.js';
|
|
16
16
|
import { machineId, normalizeHost } from './machine-id.js';
|
|
17
17
|
const REMOTE_TIMEOUT_MS = 12_000;
|
|
@@ -66,6 +66,12 @@ export async function gatherRemoteAgentsJson(options) {
|
|
|
66
66
|
continue;
|
|
67
67
|
if (normalizeHost(device.name) === self)
|
|
68
68
|
continue;
|
|
69
|
+
// Control-only devices (a phone/tablet cockpit) drive the fleet but never
|
|
70
|
+
// run agents — never dial them, whatever their platform reads as. Keyed on
|
|
71
|
+
// role, not platform, so this holds even for a control device that carries
|
|
72
|
+
// a real OS value (mirrors the skip in session/remote-list.ts).
|
|
73
|
+
if (isControlDevice(device))
|
|
74
|
+
continue;
|
|
69
75
|
if (!['windows', 'linux', 'macos'].includes(device.platform))
|
|
70
76
|
continue;
|
|
71
77
|
try {
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -182,18 +182,21 @@ export declare function rotationFailoverChain(rotation: RotateResult | null, pic
|
|
|
182
182
|
* would break a previously-working `agents run … --loop` / `--acp`.
|
|
183
183
|
* - `resumeCheckpoint` runs take the loop path (same guard).
|
|
184
184
|
* - `interactive` / no-prompt runs can't be re-dispatched headlessly.
|
|
185
|
-
* - `explicitFallback` (a user `--fallback` chain OR a profile fallback already
|
|
186
|
-
* unshifted) defines its own recovery; don't layer rotation failover on top.
|
|
187
185
|
* - `hasRotation`/`hasVersion` gate on an actual pre-flight rotation having
|
|
188
186
|
* picked an account, so pinned and non-rotation runs are untouched.
|
|
189
187
|
*
|
|
188
|
+
* An explicit `--fallback` chain does NOT disarm rotation failover: the
|
|
189
|
+
* synthesized same-agent entries are unshifted AHEAD of the user's cross-agent
|
|
190
|
+
* entries, so a rate limit exhausts the other accounts of the same agent
|
|
191
|
+
* before cascading to a different CLI. Profile fallbacks never reach here —
|
|
192
|
+
* strategy resolution is skipped for profiles, so hasRotation is false.
|
|
193
|
+
*
|
|
190
194
|
* Pure so the arming matrix is unit-testable without invoking the run command.
|
|
191
195
|
*/
|
|
192
196
|
export interface FailoverArmingContext {
|
|
193
197
|
hasRotation: boolean;
|
|
194
198
|
hasVersion: boolean;
|
|
195
199
|
hasPrompt: boolean;
|
|
196
|
-
explicitFallback: boolean;
|
|
197
200
|
interactive: boolean;
|
|
198
201
|
acp: boolean;
|
|
199
202
|
loop: boolean;
|
package/dist/lib/rotate.js
CHANGED
package/dist/lib/routines.d.ts
CHANGED
|
@@ -91,6 +91,17 @@ export interface JobConfig {
|
|
|
91
91
|
* overdue; everywhere else it is inert and `run` refuses with a pointer.
|
|
92
92
|
*/
|
|
93
93
|
devices?: string[];
|
|
94
|
+
/**
|
|
95
|
+
* Execution placement — run the job body on this machine over SSH (a
|
|
96
|
+
* registered host, device, capability tag, or user@host) instead of locally.
|
|
97
|
+
* Distinct from `devices`: `devices` says which daemon may FIRE the job,
|
|
98
|
+
* `host` says where the dispatched run EXECUTES. CLI flag: `--run-on`
|
|
99
|
+
* (`--host` on routines commands already means "manage routines on that
|
|
100
|
+
* machine" via the remote passthrough).
|
|
101
|
+
*/
|
|
102
|
+
host?: string;
|
|
103
|
+
/** Working directory on the host for `host:`-placed runs. */
|
|
104
|
+
remoteCwd?: string;
|
|
94
105
|
variables?: Record<string, string>;
|
|
95
106
|
sandbox?: boolean;
|
|
96
107
|
allow?: JobAllowConfig;
|
|
@@ -123,6 +134,11 @@ export interface RunMeta {
|
|
|
123
134
|
startedAt: string;
|
|
124
135
|
completedAt: string | null;
|
|
125
136
|
exitCode: number | null;
|
|
137
|
+
/** Set for `host:`-placed runs — where the job body executes (no local pid). */
|
|
138
|
+
host?: string;
|
|
139
|
+
/** The host-task sidecar id backing a `host:` run; the daemon monitor
|
|
140
|
+
* finalizes the run by reconciling it against the remote `.exit`. */
|
|
141
|
+
hostTaskId?: string;
|
|
126
142
|
}
|
|
127
143
|
/**
|
|
128
144
|
* True when the job may execute on this machine: no `devices` allowlist (or
|
package/dist/lib/routines.js
CHANGED
|
@@ -363,6 +363,25 @@ export function validateJob(config) {
|
|
|
363
363
|
if (config.device !== undefined) {
|
|
364
364
|
errors.push('singular "device" key is no longer supported — replace with devices: [<name>] (an array)');
|
|
365
365
|
}
|
|
366
|
+
if (config.host !== undefined) {
|
|
367
|
+
if (typeof config.host !== 'string' || config.host.trim() === '') {
|
|
368
|
+
errors.push('host must be a non-empty machine name (a registered host, device, capability tag, or user@host)');
|
|
369
|
+
}
|
|
370
|
+
// v1: the workflow bundle and the loop driver (with its signal files) live
|
|
371
|
+
// on the firing machine — neither can cross SSH to the target yet.
|
|
372
|
+
if (config.workflow) {
|
|
373
|
+
errors.push("host: can't be combined with workflow: yet (the bundle lives on the firing machine) — run the workflow locally or convert it to a plain prompt");
|
|
374
|
+
}
|
|
375
|
+
if (config.loop) {
|
|
376
|
+
errors.push("host: can't be combined with loop: yet (the loop driver and its signal files live on the firing machine)");
|
|
377
|
+
}
|
|
378
|
+
if (config.command) {
|
|
379
|
+
errors.push("host: can't be combined with command: yet (a plain shell command has no agent to place remotely) — run it locally, or convert it to a prompt");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (config.remoteCwd !== undefined && config.host === undefined) {
|
|
383
|
+
errors.push('remoteCwd only applies to host:-placed routines — set host: too, or drop it');
|
|
384
|
+
}
|
|
366
385
|
if (config.devices !== undefined) {
|
|
367
386
|
if (!Array.isArray(config.devices)) {
|
|
368
387
|
errors.push('devices must be an array of device names (as shown by `agents devices`)');
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export declare function buildRoutineSpawnEnv(baseEnv: Record<string, string>, ag
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function executeJob(config: JobConfig, deps?: LoopDeps): Promise<RunResult>;
|
|
79
79
|
/** Spawn a job as a detached process and return immediately with run metadata. */
|
|
80
|
+
/** Spawn a job as a detached process and return immediately with run metadata. */
|
|
80
81
|
export declare function executeJobDetached(config: JobConfig): Promise<RunMeta>;
|
|
81
82
|
/** Extract the final assistant message from a stream-JSON log file as a markdown report. */
|
|
82
83
|
export declare function extractReport(stdoutPath: string, agentType: AgentId): string | null;
|