@phnx-labs/agents-cli 1.20.63 → 1.20.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -3
- package/README.md +46 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +36 -0
- package/dist/lib/agents.js +80 -20
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear auto-close: close Linear issues whose linked GitHub PRs have merged.
|
|
3
|
+
*
|
|
4
|
+
* The pure decision function `shouldCloseIssue` is the only logic here —
|
|
5
|
+
* it is unit-tested and shared with the command routine's equivalent
|
|
6
|
+
* shell check. The rest (Linear GraphQL, gh invocations) lives in the
|
|
7
|
+
* routine YAML's `command:` shell script so the routine stays self-contained
|
|
8
|
+
* and independent of the CLI build.
|
|
9
|
+
*
|
|
10
|
+
* @see apps/cli/routines/linear-autoclose.yml
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Pure decision: returns true when the PR has been merged and its linked
|
|
14
|
+
* Linear issue should therefore be closed.
|
|
15
|
+
*
|
|
16
|
+
* A PR is considered merged only when both conditions hold:
|
|
17
|
+
* 1. `state === 'MERGED'` — gh marks CLOSED (rejected) separately from MERGED
|
|
18
|
+
* 2. `mergedAt !== null` — defensive guard; a merged PR always carries a timestamp
|
|
19
|
+
*/
|
|
20
|
+
export function shouldCloseIssue(pr) {
|
|
21
|
+
return pr.state === 'MERGED' && pr.mergedAt !== null;
|
|
22
|
+
}
|
package/dist/lib/mailbox.d.ts
CHANGED
|
@@ -69,3 +69,42 @@ export declare function drain(boxDir: string, boxId?: string, now?: Date): Mailb
|
|
|
69
69
|
export declare function peek(boxDir: string, boxId?: string, now?: Date): MailboxMessage[];
|
|
70
70
|
/** Delete pending (not-yet-claimed) inbox messages. Returns the count removed. */
|
|
71
71
|
export declare function clear(boxDir: string): number;
|
|
72
|
+
/** Which bucket a stored message currently sits in. */
|
|
73
|
+
export type MailboxState = 'inbox' | 'processing' | 'consumed';
|
|
74
|
+
/** A message read back from a box, tagged with the bucket it was found in. */
|
|
75
|
+
export interface StoredMessage extends MailboxMessage {
|
|
76
|
+
state: MailboxState;
|
|
77
|
+
}
|
|
78
|
+
/** A stored message enriched with the mailbox identity used by comms renderers. */
|
|
79
|
+
export interface CommsMsg {
|
|
80
|
+
from: string;
|
|
81
|
+
to: string;
|
|
82
|
+
toLabel: string;
|
|
83
|
+
ts: string;
|
|
84
|
+
text: string;
|
|
85
|
+
state: MailboxState;
|
|
86
|
+
box: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Enumerate the box ids under `root` (directory names that are valid mailbox
|
|
90
|
+
* ids). Read-only; does not create the root. Sorted for stable output.
|
|
91
|
+
*/
|
|
92
|
+
export declare function listBoxes(root?: string): string[];
|
|
93
|
+
/**
|
|
94
|
+
* Read every message in a box across all three buckets (inbox, processing,
|
|
95
|
+
* consumed) WITHOUT consuming, sweeping, or archiving anything. Unlike `peek`,
|
|
96
|
+
* this includes `consumed/` — the delivered history — so callers can surface a
|
|
97
|
+
* communication log. Each row is tagged with its bucket. Sorted FIFO by msgId
|
|
98
|
+
* (which is time-sortable), oldest first.
|
|
99
|
+
*/
|
|
100
|
+
export declare function readBox(boxDir: string): StoredMessage[];
|
|
101
|
+
/**
|
|
102
|
+
* Poll the complete spool and yield each message once when its box/msgId pair
|
|
103
|
+
* first appears. Existing messages establish the initial baseline unless
|
|
104
|
+
* `backfill` is requested; moving a message between buckets does not re-emit it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function watchMessages(root: string, opts: {
|
|
107
|
+
signal?: AbortSignal;
|
|
108
|
+
intervalMs?: number;
|
|
109
|
+
backfill?: boolean;
|
|
110
|
+
}): AsyncGenerator<CommsMsg>;
|
package/dist/lib/mailbox.js
CHANGED
|
@@ -267,3 +267,115 @@ export function clear(boxDir) {
|
|
|
267
267
|
}
|
|
268
268
|
return n;
|
|
269
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Enumerate the box ids under `root` (directory names that are valid mailbox
|
|
272
|
+
* ids). Read-only; does not create the root. Sorted for stable output.
|
|
273
|
+
*/
|
|
274
|
+
export function listBoxes(root = getMailboxRootDir()) {
|
|
275
|
+
let names;
|
|
276
|
+
try {
|
|
277
|
+
names = fs.readdirSync(root);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
return names.filter((n) => isValidMailboxId(n) && fs.statSync(path.join(root, n)).isDirectory()).sort();
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Read every message in a box across all three buckets (inbox, processing,
|
|
286
|
+
* consumed) WITHOUT consuming, sweeping, or archiving anything. Unlike `peek`,
|
|
287
|
+
* this includes `consumed/` — the delivered history — so callers can surface a
|
|
288
|
+
* communication log. Each row is tagged with its bucket. Sorted FIFO by msgId
|
|
289
|
+
* (which is time-sortable), oldest first.
|
|
290
|
+
*/
|
|
291
|
+
export function readBox(boxDir) {
|
|
292
|
+
const out = [];
|
|
293
|
+
const buckets = [
|
|
294
|
+
[inboxDir(boxDir), 'inbox'],
|
|
295
|
+
[processingDir(boxDir), 'processing'],
|
|
296
|
+
[consumedDir(boxDir), 'consumed'],
|
|
297
|
+
];
|
|
298
|
+
for (const [dir, state] of buckets) {
|
|
299
|
+
for (const name of jsonFiles(dir)) {
|
|
300
|
+
const msg = readMessage(path.join(dir, name));
|
|
301
|
+
if (msg)
|
|
302
|
+
out.push({ ...msg, state });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
out.sort((a, b) => (a.msgId < b.msgId ? -1 : a.msgId > b.msgId ? 1 : 0));
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Poll the complete spool and yield each message once when its box/msgId pair
|
|
310
|
+
* first appears. Existing messages establish the initial baseline unless
|
|
311
|
+
* `backfill` is requested; moving a message between buckets does not re-emit it.
|
|
312
|
+
*/
|
|
313
|
+
export async function* watchMessages(root, opts) {
|
|
314
|
+
const seen = new Set();
|
|
315
|
+
const requestedInterval = opts.intervalMs ?? 500;
|
|
316
|
+
const intervalMs = Number.isFinite(requestedInterval) ? Math.max(1, requestedInterval) : 500;
|
|
317
|
+
let firstPoll = true;
|
|
318
|
+
while (!opts.signal?.aborted) {
|
|
319
|
+
const fresh = [];
|
|
320
|
+
for (const box of listBoxes(root)) {
|
|
321
|
+
for (const stored of readBox(mailboxDir(box, root))) {
|
|
322
|
+
const key = `${box}\0${stored.msgId}`;
|
|
323
|
+
if (seen.has(key))
|
|
324
|
+
continue;
|
|
325
|
+
seen.add(key);
|
|
326
|
+
if (firstPoll && !opts.backfill)
|
|
327
|
+
continue;
|
|
328
|
+
fresh.push({
|
|
329
|
+
key,
|
|
330
|
+
message: {
|
|
331
|
+
from: stored.from || 'operator',
|
|
332
|
+
to: stored.to,
|
|
333
|
+
toLabel: box.slice(0, 8),
|
|
334
|
+
ts: stored.ts,
|
|
335
|
+
text: stored.text,
|
|
336
|
+
state: stored.state,
|
|
337
|
+
box,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
firstPoll = false;
|
|
343
|
+
fresh.sort((a, b) => compareWatched(a.message.ts, b.message.ts) ||
|
|
344
|
+
compareWatched(a.key, b.key));
|
|
345
|
+
for (const { message } of fresh) {
|
|
346
|
+
if (opts.signal?.aborted)
|
|
347
|
+
return;
|
|
348
|
+
yield message;
|
|
349
|
+
}
|
|
350
|
+
if (!await waitForMailboxPoll(intervalMs, opts.signal))
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function compareWatched(a, b) {
|
|
355
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
356
|
+
}
|
|
357
|
+
/** Wait for the next poll, resolving immediately when the watcher is aborted. */
|
|
358
|
+
function waitForMailboxPoll(intervalMs, signal) {
|
|
359
|
+
if (signal?.aborted)
|
|
360
|
+
return Promise.resolve(false);
|
|
361
|
+
return new Promise((resolve) => {
|
|
362
|
+
let timer;
|
|
363
|
+
let settled = false;
|
|
364
|
+
const finish = (keepWatching) => {
|
|
365
|
+
if (settled)
|
|
366
|
+
return;
|
|
367
|
+
settled = true;
|
|
368
|
+
if (timer)
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
signal?.removeEventListener('abort', onAbort);
|
|
371
|
+
resolve(keepWatching);
|
|
372
|
+
};
|
|
373
|
+
const onAbort = () => finish(false);
|
|
374
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
375
|
+
if (signal?.aborted) {
|
|
376
|
+
finish(false);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
timer = setTimeout(() => finish(true), intervalMs);
|
|
380
|
+
});
|
|
381
|
+
}
|
package/dist/lib/mcp.d.ts
CHANGED
|
@@ -44,10 +44,35 @@ export declare function parseMcpServerConfig(filePath: string): McpYamlConfig |
|
|
|
44
44
|
* command-line options or that contain characters unsafe for argv/identifier use.
|
|
45
45
|
*/
|
|
46
46
|
export declare function validateMcpServerName(name: string): void;
|
|
47
|
+
/** Path to the user-owned project-trust store (never inside a repo). */
|
|
48
|
+
export declare function getMcpTrustStorePath(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the project that owns `projectAgentsDir` has been explicitly trusted
|
|
51
|
+
* for MCP auto-apply. Untrusted by default (fail closed).
|
|
52
|
+
*/
|
|
53
|
+
export declare function isProjectMcpTrusted(projectAgentsDir: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Record explicit trust for the project containing `cwd` so its project-scoped
|
|
56
|
+
* MCP servers may be registered/spawned. Returns the trusted project root, or
|
|
57
|
+
* null when `cwd` is not inside a project (no `.agents/` to trust).
|
|
58
|
+
*/
|
|
59
|
+
export declare function trustProjectMcp(cwd?: string): string | null;
|
|
60
|
+
/** Revoke MCP trust for the project containing `cwd`. Returns true if it was trusted. */
|
|
61
|
+
export declare function untrustProjectMcp(cwd?: string): boolean;
|
|
47
62
|
/**
|
|
48
63
|
* List all MCP server configs from ~/.agents/mcp/.
|
|
64
|
+
*
|
|
65
|
+
* When `enforceProjectTrust` is set, project-scoped configs are included only
|
|
66
|
+
* for a project the user has explicitly trusted (see `isProjectMcpTrusted`) —
|
|
67
|
+
* this is the choke point that keeps an untrusted cloned repo's MCP servers out
|
|
68
|
+
* of the register/spawn path. It ALSO fixes name-collision shadowing: an
|
|
69
|
+
* untrusted project entry is dropped before dedup, so it can never mask a
|
|
70
|
+
* same-named user entry. Display callers omit the flag to surface project
|
|
71
|
+
* entries (command+args and all) regardless of trust.
|
|
49
72
|
*/
|
|
50
|
-
export declare function listMcpServerConfigs(cwd?: string
|
|
73
|
+
export declare function listMcpServerConfigs(cwd?: string, options?: {
|
|
74
|
+
enforceProjectTrust?: boolean;
|
|
75
|
+
}): InstalledMcpServer[];
|
|
51
76
|
/**
|
|
52
77
|
* Scan a repository for MCP server YAML configs.
|
|
53
78
|
* Looks under <repoPath>/mcp/*.yaml — same on-disk layout as ~/.agents/mcp/.
|
|
@@ -70,6 +95,7 @@ export declare function installMcpConfigCentrally(sourcePath: string): {
|
|
|
70
95
|
*/
|
|
71
96
|
export declare function getMcpServersByName(names?: string[], options?: {
|
|
72
97
|
cwd?: string;
|
|
98
|
+
enforceProjectTrust?: boolean;
|
|
73
99
|
}): InstalledMcpServer[];
|
|
74
100
|
/**
|
|
75
101
|
* Assemble the JSON payload Claude's `--mcp-config` flag expects from a set of
|
package/dist/lib/mcp.js
CHANGED
|
@@ -12,9 +12,9 @@ import * as path from 'path';
|
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
14
|
import * as os from 'os';
|
|
15
|
-
import { getMcpDir, getUserMcpDir, getProjectAgentsDir, getVersionsDir } from './state.js';
|
|
15
|
+
import { getMcpDir, getUserMcpDir, getProjectAgentsDir, getVersionsDir, getUserAgentsDir } from './state.js';
|
|
16
16
|
import { getBinaryPath, getVersionHomePath } from './versions.js';
|
|
17
|
-
import { IS_WINDOWS,
|
|
17
|
+
import { IS_WINDOWS, execFileShellSpec } from './platform/index.js';
|
|
18
18
|
import { AGENTS } from './agents.js';
|
|
19
19
|
import { isCapable } from './capabilities.js';
|
|
20
20
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
@@ -93,13 +93,116 @@ function isStringRecord(value) {
|
|
|
93
93
|
return false;
|
|
94
94
|
return Object.values(value).every((item) => typeof item === 'string');
|
|
95
95
|
}
|
|
96
|
+
// ─── Project MCP trust (RUSH-1776) ───────────────────────────────────────────
|
|
97
|
+
// Project-scoped MCP configs (<repo>/.agents/mcp/*.yaml) are UNTRUSTED by
|
|
98
|
+
// default. An MCP server is an arbitrary command spawned under the agent's
|
|
99
|
+
// authority, so merely cloning a hostile repo must never auto-register or run
|
|
100
|
+
// it. A project's MCP servers enter the register/spawn path only after the user
|
|
101
|
+
// explicitly trusts that project (`agents mcp trust`), recorded in a user-owned
|
|
102
|
+
// store OUTSIDE any repo so a cloned repo can't grant itself trust. User- and
|
|
103
|
+
// system-scoped MCPs (~/.agents/mcp/*) are always trusted.
|
|
104
|
+
/** Path to the user-owned project-trust store (never inside a repo). */
|
|
105
|
+
export function getMcpTrustStorePath() {
|
|
106
|
+
return path.join(getUserAgentsDir(), 'mcp-trust.yaml');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Key a project by its ROOT (parent of `.agents/`), resolved through symlinks
|
|
110
|
+
* so the key is stable no matter how the cwd was spelled.
|
|
111
|
+
*/
|
|
112
|
+
function normalizeProjectKey(projectAgentsDir) {
|
|
113
|
+
const root = path.dirname(projectAgentsDir);
|
|
114
|
+
try {
|
|
115
|
+
return fs.realpathSync(root);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return path.resolve(root);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function readTrustedProjects() {
|
|
122
|
+
const storePath = getMcpTrustStorePath();
|
|
123
|
+
if (!fs.existsSync(storePath))
|
|
124
|
+
return new Set();
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = yaml.parse(fs.readFileSync(storePath, 'utf-8'));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return new Set();
|
|
131
|
+
}
|
|
132
|
+
const list = parsed && typeof parsed === 'object' && Array.isArray(parsed.trustedProjects)
|
|
133
|
+
? parsed.trustedProjects
|
|
134
|
+
: [];
|
|
135
|
+
const out = new Set();
|
|
136
|
+
for (const entry of list) {
|
|
137
|
+
if (typeof entry !== 'string' || entry.length === 0)
|
|
138
|
+
continue;
|
|
139
|
+
try {
|
|
140
|
+
out.add(fs.realpathSync(entry));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
out.add(path.resolve(entry));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function writeTrustedProjects(trusted) {
|
|
149
|
+
const storePath = getMcpTrustStorePath();
|
|
150
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
151
|
+
fs.writeFileSync(storePath, yaml.stringify({ trustedProjects: Array.from(trusted).sort() }), 'utf-8');
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Whether the project that owns `projectAgentsDir` has been explicitly trusted
|
|
155
|
+
* for MCP auto-apply. Untrusted by default (fail closed).
|
|
156
|
+
*/
|
|
157
|
+
export function isProjectMcpTrusted(projectAgentsDir) {
|
|
158
|
+
return readTrustedProjects().has(normalizeProjectKey(projectAgentsDir));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Record explicit trust for the project containing `cwd` so its project-scoped
|
|
162
|
+
* MCP servers may be registered/spawned. Returns the trusted project root, or
|
|
163
|
+
* null when `cwd` is not inside a project (no `.agents/` to trust).
|
|
164
|
+
*/
|
|
165
|
+
export function trustProjectMcp(cwd = process.cwd()) {
|
|
166
|
+
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
167
|
+
if (!projectAgentsDir)
|
|
168
|
+
return null;
|
|
169
|
+
const key = normalizeProjectKey(projectAgentsDir);
|
|
170
|
+
const trusted = readTrustedProjects();
|
|
171
|
+
if (!trusted.has(key)) {
|
|
172
|
+
trusted.add(key);
|
|
173
|
+
writeTrustedProjects(trusted);
|
|
174
|
+
}
|
|
175
|
+
return key;
|
|
176
|
+
}
|
|
177
|
+
/** Revoke MCP trust for the project containing `cwd`. Returns true if it was trusted. */
|
|
178
|
+
export function untrustProjectMcp(cwd = process.cwd()) {
|
|
179
|
+
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
180
|
+
if (!projectAgentsDir)
|
|
181
|
+
return false;
|
|
182
|
+
const key = normalizeProjectKey(projectAgentsDir);
|
|
183
|
+
const trusted = readTrustedProjects();
|
|
184
|
+
if (!trusted.delete(key))
|
|
185
|
+
return false;
|
|
186
|
+
writeTrustedProjects(trusted);
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
96
189
|
/**
|
|
97
190
|
* List all MCP server configs from ~/.agents/mcp/.
|
|
191
|
+
*
|
|
192
|
+
* When `enforceProjectTrust` is set, project-scoped configs are included only
|
|
193
|
+
* for a project the user has explicitly trusted (see `isProjectMcpTrusted`) —
|
|
194
|
+
* this is the choke point that keeps an untrusted cloned repo's MCP servers out
|
|
195
|
+
* of the register/spawn path. It ALSO fixes name-collision shadowing: an
|
|
196
|
+
* untrusted project entry is dropped before dedup, so it can never mask a
|
|
197
|
+
* same-named user entry. Display callers omit the flag to surface project
|
|
198
|
+
* entries (command+args and all) regardless of trust.
|
|
98
199
|
*/
|
|
99
|
-
export function listMcpServerConfigs(cwd = process.cwd()) {
|
|
200
|
+
export function listMcpServerConfigs(cwd = process.cwd(), options = {}) {
|
|
100
201
|
const dirs = [];
|
|
101
202
|
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
102
|
-
|
|
203
|
+
const includeProject = projectAgentsDir !== null
|
|
204
|
+
&& (!options.enforceProjectTrust || isProjectMcpTrusted(projectAgentsDir));
|
|
205
|
+
if (projectAgentsDir && includeProject) {
|
|
103
206
|
dirs.push({ scope: 'project', dir: path.join(projectAgentsDir, 'mcp') });
|
|
104
207
|
}
|
|
105
208
|
// User dir first (wins on name collision), then system
|
|
@@ -175,7 +278,10 @@ export function installMcpConfigCentrally(sourcePath) {
|
|
|
175
278
|
* Otherwise returns all servers.
|
|
176
279
|
*/
|
|
177
280
|
export function getMcpServersByName(names, options = {}) {
|
|
178
|
-
|
|
281
|
+
// This feeds the register/spawn path (installMcpServers, workflow assembly),
|
|
282
|
+
// so untrusted project-scoped servers are excluded by default (fail closed).
|
|
283
|
+
const enforceProjectTrust = options.enforceProjectTrust ?? true;
|
|
284
|
+
const allServers = listMcpServerConfigs(options.cwd, { enforceProjectTrust });
|
|
179
285
|
if (!names || names.length === 0) {
|
|
180
286
|
return allServers;
|
|
181
287
|
}
|
|
@@ -228,20 +334,24 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
|
|
|
228
334
|
server.config.command,
|
|
229
335
|
...(server.config.args || [])
|
|
230
336
|
];
|
|
231
|
-
|
|
337
|
+
// RUSH-1752: user-controlled MCP command/args must not reach cmd.exe unquoted.
|
|
338
|
+
const spec = execFileShellSpec(binaryPath, args);
|
|
339
|
+
execFileSync(spec.command, spec.args, {
|
|
232
340
|
stdio: 'pipe',
|
|
233
341
|
timeout: 30000,
|
|
234
342
|
env: execEnv,
|
|
235
|
-
shell:
|
|
343
|
+
shell: spec.shell,
|
|
236
344
|
});
|
|
237
345
|
}
|
|
238
346
|
else {
|
|
239
347
|
// claude mcp add --scope user --transport http -- <name> <url>
|
|
240
|
-
|
|
348
|
+
const httpArgs = ['mcp', 'add', '--scope', 'user', '--transport', 'http', '--', server.name, server.config.url];
|
|
349
|
+
const spec = execFileShellSpec(binaryPath, httpArgs);
|
|
350
|
+
execFileSync(spec.command, spec.args, {
|
|
241
351
|
stdio: 'pipe',
|
|
242
352
|
timeout: 30000,
|
|
243
353
|
env: execEnv,
|
|
244
|
-
shell:
|
|
354
|
+
shell: spec.shell,
|
|
245
355
|
});
|
|
246
356
|
}
|
|
247
357
|
}
|
|
@@ -259,11 +369,13 @@ function installMcpViaCodex(binaryPath, server, versionHome) {
|
|
|
259
369
|
server.config.command,
|
|
260
370
|
...(server.config.args || [])
|
|
261
371
|
];
|
|
262
|
-
|
|
372
|
+
// RUSH-1752: user-controlled MCP command/args must not reach cmd.exe unquoted.
|
|
373
|
+
const spec = execFileShellSpec(binaryPath, args);
|
|
374
|
+
execFileSync(spec.command, spec.args, {
|
|
263
375
|
stdio: 'pipe',
|
|
264
376
|
timeout: 30000,
|
|
265
377
|
env: { ...process.env, HOME: versionHome },
|
|
266
|
-
shell:
|
|
378
|
+
shell: spec.shell,
|
|
267
379
|
});
|
|
268
380
|
}
|
|
269
381
|
// Note: Codex may not support HTTP MCPs
|
|
@@ -314,7 +426,9 @@ function registerMcpCommand(agentId, name, commandSpec, scope, transport, option
|
|
|
314
426
|
? ['mcp', 'add', '--transport', transport, '--scope', scope, '--', name, ...commandArgs]
|
|
315
427
|
: ['mcp', 'add', '--', name, ...commandArgs];
|
|
316
428
|
const env = options.home ? { ...process.env, HOME: options.home } : process.env;
|
|
317
|
-
|
|
429
|
+
// RUSH-1752: user-controlled MCP command/args must not reach cmd.exe unquoted.
|
|
430
|
+
const spec = execFileShellSpec(bin, args);
|
|
431
|
+
execFileSync(spec.command, spec.args, { stdio: 'pipe', timeout: 30000, env, shell: spec.shell });
|
|
318
432
|
return { success: true };
|
|
319
433
|
}
|
|
320
434
|
catch (err) {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor (event-triggered watcher) configuration, validation, and CRUD.
|
|
3
|
+
*
|
|
4
|
+
* A monitor is a routine whose trigger is a *watched source* instead of a
|
|
5
|
+
* *clock*. It watches a SOURCE, detects a CONDITION change, and fires an ACTION —
|
|
6
|
+
* reusing the routines daemon, dispatch engine, device model, and notify path.
|
|
7
|
+
*
|
|
8
|
+
* Monitors are YAML files in ~/.agents/monitors/. This module owns the on-disk
|
|
9
|
+
* shape (mirroring lib/routines.ts read/write helpers), hand-rolled validation
|
|
10
|
+
* (mirroring validateJob — no zod), and the device-owner eligibility gate.
|
|
11
|
+
*/
|
|
12
|
+
import type { AgentId } from '../types.js';
|
|
13
|
+
/** Source types a monitor can watch. */
|
|
14
|
+
export type MonitorSourceType = 'command' | 'poll' | 'poll-http' | 'webhook' | 'ws' | 'file' | 'device';
|
|
15
|
+
/** Webhook source filters — reuse the github/linear matcher shape from triggers/webhook.ts. */
|
|
16
|
+
export interface MonitorWebhookSource {
|
|
17
|
+
source: 'github' | 'linear';
|
|
18
|
+
event: string;
|
|
19
|
+
repo?: string;
|
|
20
|
+
branch?: string;
|
|
21
|
+
action?: string;
|
|
22
|
+
teamKey?: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What a monitor watches. Exactly one source-payload field is populated, keyed by
|
|
27
|
+
* `type`: `command`/`interval` for command/poll, `url`/`interval` for poll-http,
|
|
28
|
+
* `wsUrl` for ws, `path` for file, `device` for device, `webhook` for webhook.
|
|
29
|
+
*/
|
|
30
|
+
export interface MonitorSource {
|
|
31
|
+
type: MonitorSourceType;
|
|
32
|
+
/** Shell command whose stdout is the observation (command, poll). */
|
|
33
|
+
command?: string;
|
|
34
|
+
/** Re-evaluation interval (poll, poll-http; optional for command/file/device). e.g. `30s`, `15m`, `8h`. */
|
|
35
|
+
interval?: string;
|
|
36
|
+
/** URL to GET (poll-http). */
|
|
37
|
+
url?: string;
|
|
38
|
+
/** WebSocket URL; each frame is an observation (ws). */
|
|
39
|
+
wsUrl?: string;
|
|
40
|
+
/** File or directory to watch (file). */
|
|
41
|
+
path?: string;
|
|
42
|
+
/** A registered fleet device whose health/reachability is the observation (device). */
|
|
43
|
+
device?: string;
|
|
44
|
+
/** Webhook trigger filters (webhook). */
|
|
45
|
+
webhook?: MonitorWebhookSource;
|
|
46
|
+
}
|
|
47
|
+
/** How an observation becomes a fire. */
|
|
48
|
+
export type MonitorConditionMode = 'on-change' | 'match' | 'every';
|
|
49
|
+
export interface MonitorCondition {
|
|
50
|
+
mode: MonitorConditionMode;
|
|
51
|
+
/** Regex (required for `match` mode) — fire when the observation matches. */
|
|
52
|
+
match?: string;
|
|
53
|
+
/**
|
|
54
|
+
* What counts as "the same event" for de-duplication. When set, the dedupe
|
|
55
|
+
* signature is the first regex match of this expression against the
|
|
56
|
+
* observation (so re-observing the same token is silent); when omitted, the
|
|
57
|
+
* full observation is the signature.
|
|
58
|
+
*/
|
|
59
|
+
dedupeKey?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Action types a monitor can fire. */
|
|
62
|
+
export type MonitorActionType = 'run' | 'routine' | 'notify' | 'webhook-out';
|
|
63
|
+
/**
|
|
64
|
+
* What a monitor does on a fire. Shares the run-shaped fields (agent, prompt,
|
|
65
|
+
* mode, effort, timeout) conceptually with JobConfig (lib/routines.ts) so
|
|
66
|
+
* dispatch reuses the routines path (executeJobDetached). The fired event is
|
|
67
|
+
* injected into the prompt as `{event}`.
|
|
68
|
+
*/
|
|
69
|
+
export interface ActionConfig {
|
|
70
|
+
type: MonitorActionType;
|
|
71
|
+
/** run: which agent to spawn. */
|
|
72
|
+
agent?: AgentId;
|
|
73
|
+
/** run: the prompt; `{event}` is replaced with the fired event summary. */
|
|
74
|
+
prompt?: string;
|
|
75
|
+
/** run: execution mode (shared with routines). */
|
|
76
|
+
mode?: 'plan' | 'edit' | 'auto' | 'skip' | 'full';
|
|
77
|
+
/** run: reasoning effort (shared with routines). */
|
|
78
|
+
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
|
|
79
|
+
/** run: kill the action if it runs longer than this (e.g. `10m`). */
|
|
80
|
+
timeout?: string;
|
|
81
|
+
/** routine: name of an existing routine to fire. */
|
|
82
|
+
routine?: string;
|
|
83
|
+
/** notify: channel for the notification (default `telegram`). */
|
|
84
|
+
notifyChannel?: string;
|
|
85
|
+
/** webhook-out: URL to POST the event to. */
|
|
86
|
+
url?: string;
|
|
87
|
+
}
|
|
88
|
+
/** Full monitor configuration (persisted as YAML in ~/.agents/monitors/). */
|
|
89
|
+
export interface MonitorConfig {
|
|
90
|
+
name: string;
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
source: MonitorSource;
|
|
93
|
+
condition: MonitorCondition;
|
|
94
|
+
action: ActionConfig;
|
|
95
|
+
/**
|
|
96
|
+
* Pin-to-one OWNER device — the single machine whose daemon evaluates the
|
|
97
|
+
* source and fires. Exactly-once for v1 (no distributed lock). When set, only
|
|
98
|
+
* that machine is eligible; everywhere else the monitor is inert.
|
|
99
|
+
*/
|
|
100
|
+
device?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Fleet allowlist (advanced) — each listed device evaluates and fires
|
|
103
|
+
* independently, like routines' `devices`. Mutually exclusive with `device`.
|
|
104
|
+
*/
|
|
105
|
+
devices?: string[];
|
|
106
|
+
/** Execute the ACTION on this machine over SSH (placement), distinct from the owner that fires it. */
|
|
107
|
+
runOn?: string;
|
|
108
|
+
/** Firehose guard: auto-pause the monitor if it fires more than `max` times per `per`. */
|
|
109
|
+
rateLimit?: {
|
|
110
|
+
max: number;
|
|
111
|
+
per: string;
|
|
112
|
+
};
|
|
113
|
+
/** User-defined prompt variables (expanded like routines' variables). */
|
|
114
|
+
variables?: Record<string, string>;
|
|
115
|
+
/** Pin the agent version for `run` actions (omit to use the run strategy). */
|
|
116
|
+
version?: string;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A fired event. `summary` is injected into the action prompt as `{event}`;
|
|
120
|
+
* `payload` carries the structured observation for `webhook-out` and inspection.
|
|
121
|
+
*/
|
|
122
|
+
export interface MonitorEvent {
|
|
123
|
+
monitorName: string;
|
|
124
|
+
firedAt: string;
|
|
125
|
+
summary: string;
|
|
126
|
+
payload: Record<string, unknown>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Parse a human interval string (e.g. `30s`, `15m`, `8h`, `1d`, `1h30m`) into
|
|
130
|
+
* milliseconds. Unlike routines' parseTimeout, seconds are supported (polls tick
|
|
131
|
+
* in seconds). Returns null on empty/unparseable/zero input.
|
|
132
|
+
*/
|
|
133
|
+
export declare function parseInterval(interval: string): number | null;
|
|
134
|
+
/**
|
|
135
|
+
* True when the monitor may evaluate + fire on this machine. Owner semantics:
|
|
136
|
+
* `device` (single owner, exactly-once) → only that machine; else `devices`
|
|
137
|
+
* (allowlist) → any listed machine; else unrestricted. Both sides normalize so
|
|
138
|
+
* `Yosemite-S0` and `yosemite-s0.tailnet.ts.net` agree with `yosemite-s0`.
|
|
139
|
+
*/
|
|
140
|
+
export declare function monitorRunsOnThisDevice(config: Pick<MonitorConfig, 'device' | 'devices'>): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Validate a partial monitor config, returning a list of human-readable errors.
|
|
143
|
+
* Hand-rolled like validateJob (lib/routines.ts) — no zod. Rejects: no source,
|
|
144
|
+
* two sources, no action, two actions, match-mode without `match`, plus the
|
|
145
|
+
* per-type field/shape checks.
|
|
146
|
+
*/
|
|
147
|
+
export declare function validateMonitor(config: Partial<MonitorConfig>): string[];
|
|
148
|
+
/** List all monitor configs in ~/.agents/monitors/. */
|
|
149
|
+
export declare function listMonitors(): MonitorConfig[];
|
|
150
|
+
/** Read a single monitor config by name. Returns null if not found or corrupt. */
|
|
151
|
+
export declare function readMonitor(name: string): MonitorConfig | null;
|
|
152
|
+
/** Get the filesystem path of a monitor's YAML config, or null if not found. */
|
|
153
|
+
export declare function getMonitorPath(name: string): string | null;
|
|
154
|
+
/** Write a monitor config to disk atomically, omitting fields that match defaults. */
|
|
155
|
+
export declare function writeMonitor(config: MonitorConfig): void;
|
|
156
|
+
/** Delete a monitor config file by name. Returns true if the file existed. */
|
|
157
|
+
export declare function deleteMonitor(name: string): boolean;
|
|
158
|
+
/** Enable or disable a monitor by name. */
|
|
159
|
+
export declare function setMonitorEnabled(name: string, enabled: boolean): void;
|
|
160
|
+
/** Check whether a monitor with the given name exists on disk. */
|
|
161
|
+
export declare function monitorExists(name: string): boolean;
|