@phnx-labs/agents-cli 1.20.64 → 1.20.66
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 +51 -3
- package/README.md +156 -3
- 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.d.ts +48 -0
- package/dist/commands/exec.js +159 -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 +748 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/plugins.js +28 -7
- 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-browser.d.ts +82 -0
- package/dist/commands/sessions-browser.js +320 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +157 -10
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +150 -0
- package/dist/commands/ssh.js +54 -3
- 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 +35 -14
- package/dist/lib/agents.d.ts +46 -0
- package/dist/lib/agents.js +121 -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/connect.d.ts +18 -1
- package/dist/lib/devices/connect.js +10 -2
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/known-hosts.d.ts +62 -0
- package/dist/lib/devices/known-hosts.js +137 -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 +66 -20
- 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 +79 -4
- 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 +205 -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 +45 -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/picker.d.ts +53 -0
- package/dist/lib/picker.js +214 -1
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.d.ts +31 -1
- package/dist/lib/plugins.js +175 -15
- 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 +83 -10
- package/dist/lib/secrets/agent.js +237 -70
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/filestore.d.ts +9 -0
- package/dist/lib/secrets/filestore.js +21 -8
- package/dist/lib/secrets/index.d.ts +7 -0
- package/dist/lib/secrets/index.js +26 -6
- 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/share/capture.d.ts +29 -0
- package/dist/lib/share/capture.js +140 -0
- package/dist/lib/share/config.d.ts +35 -0
- package/dist/lib/share/config.js +100 -0
- package/dist/lib/share/og.d.ts +25 -0
- package/dist/lib/share/og.js +84 -0
- package/dist/lib/share/provision.d.ts +10 -0
- package/dist/lib/share/provision.js +91 -0
- package/dist/lib/share/publish.d.ts +57 -0
- package/dist/lib/share/publish.js +145 -0
- package/dist/lib/share/worker-template.d.ts +2 -0
- package/dist/lib/share/worker-template.js +82 -0
- package/dist/lib/shims.d.ts +13 -0
- package/dist/lib/shims.js +42 -2
- package/dist/lib/ssh-exec.d.ts +24 -0
- package/dist/lib/ssh-exec.js +15 -3
- 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 +3 -0
- package/dist/lib/startup/command-registry.js +6 -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 +36 -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
package/dist/commands/exec.js
CHANGED
|
@@ -12,6 +12,8 @@ import { parseLoopInterval } from '../lib/loop.js';
|
|
|
12
12
|
import { AGENTS } from '../lib/agents.js';
|
|
13
13
|
import { recordDispatchedRun } from '../lib/audit/log.js';
|
|
14
14
|
import { warnUnpushedWork, shouldWarnUnpushed } from '../lib/warn-unpushed.js';
|
|
15
|
+
import { isHostPinned, pinHostKey, managedKnownHostsPath } from '../lib/devices/known-hosts.js';
|
|
16
|
+
import { sshResolve } from '../lib/hosts/ssh-config.js';
|
|
15
17
|
import * as fs from 'fs';
|
|
16
18
|
import * as path from 'path';
|
|
17
19
|
import * as os from 'os';
|
|
@@ -27,6 +29,46 @@ function formatRotationBanner(result, verb = 'balanced') {
|
|
|
27
29
|
const ratio = `${healthy.length} of ${healthy.length + excluded.length} healthy`;
|
|
28
30
|
return `[agents] ${verb} picked ${label} (${ratio})`;
|
|
29
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Decide whether `--copy-creds` may ship credentials (and the Claude OAuth
|
|
34
|
+
* token) to `host` (RUSH-1767). Credentials ship only to a host whose SSH host
|
|
35
|
+
* key is pinned in the managed known_hosts store, so the offload never rides an
|
|
36
|
+
* accept-new (TOFU) connection a machine-in-the-middle could intercept.
|
|
37
|
+
*
|
|
38
|
+
* Resolution:
|
|
39
|
+
* - The checked target is the host's concrete `address`, else its ssh-config
|
|
40
|
+
* HostName (`ssh -G`), else its name — so an alias is pinned/verified against
|
|
41
|
+
* the SAME real host the strict dispatch later connects to.
|
|
42
|
+
* - An unpinned NON-device (a bare `~/.ssh/config` `Host` alias or literal,
|
|
43
|
+
* which `agents ssh <name>` can't reach — "Unknown device") is self-pinned in
|
|
44
|
+
* place via ssh-keyscan. A registered device is left unpinned here — it earns
|
|
45
|
+
* its pin through the normal `agents ssh <name>` accept-new connect, so the
|
|
46
|
+
* caller steers there instead.
|
|
47
|
+
*
|
|
48
|
+
* The network seams (`resolve`, `selfPin`) default to the real ssh -G /
|
|
49
|
+
* ssh-keyscan implementations and are injectable so the ship/refuse decision is
|
|
50
|
+
* unit-testable against real known_hosts fixtures with no network.
|
|
51
|
+
*/
|
|
52
|
+
export function decideCopyCredsGate(host, deps = {}) {
|
|
53
|
+
const file = deps.file ?? managedKnownHostsPath();
|
|
54
|
+
const resolve = deps.resolve ?? sshResolve;
|
|
55
|
+
const selfPin = deps.selfPin ?? ((target, port, f) => pinHostKey(target, { file: f, ...(port ? { port } : {}) }).pinned);
|
|
56
|
+
// For an ssh-config host `host.address` is unset; ssh -G gives the real
|
|
57
|
+
// HostName (and Port) the strict dispatch will verify against.
|
|
58
|
+
const cfg = host.address ? undefined : resolve(host.name);
|
|
59
|
+
const pinTarget = host.address ?? cfg?.hostname ?? host.name;
|
|
60
|
+
// Not pinned yet? A registered device earns its pin through the normal
|
|
61
|
+
// accept-new connect (`agents ssh <name>` / the fleet sweep), so leave it and
|
|
62
|
+
// let the caller steer there. A bare ssh-config alias / literal is NOT a
|
|
63
|
+
// registered device — that flow can never pin it — so pin it right here.
|
|
64
|
+
let selfPinned = false;
|
|
65
|
+
if (!isHostPinned(pinTarget, file) && host.provider !== 'devices') {
|
|
66
|
+
const cfgPort = cfg?.port;
|
|
67
|
+
const port = cfgPort && cfgPort !== '22' ? Number(cfgPort) : undefined;
|
|
68
|
+
selfPinned = selfPin(pinTarget, port, file);
|
|
69
|
+
}
|
|
70
|
+
return { allowed: isHostPinned(pinTarget, file), pinTarget, selfPinned };
|
|
71
|
+
}
|
|
30
72
|
/**
|
|
31
73
|
* Build the LoopConfig the driver consumes from CLI flags and/or a workflow's
|
|
32
74
|
* `loop:` frontmatter block (issue #332). Returns undefined when neither source
|
|
@@ -507,33 +549,45 @@ export function registerRunCommand(program) {
|
|
|
507
549
|
process.exit(1);
|
|
508
550
|
}
|
|
509
551
|
const hostName = hostGiven[0];
|
|
510
|
-
const {
|
|
511
|
-
const {
|
|
512
|
-
const {
|
|
552
|
+
const { resolveHostRunTarget, dispatchPromptToHost, HostResolutionError } = await import('../lib/hosts/run-target.js');
|
|
553
|
+
const { runInteractiveOnHost } = await import('../lib/hosts/dispatch.js');
|
|
554
|
+
const { registerInteractiveHostSession } = await import('../lib/hosts/session-index.js');
|
|
555
|
+
const { RUN_OPTION_REJECT_MESSAGES } = await import('../lib/hosts/remote-cmd.js');
|
|
513
556
|
const { normalizeRunStrategy, RUN_STRATEGIES } = await import('../lib/rotate.js');
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
if (!host) {
|
|
534
|
-
console.error(chalk.red(`Unknown host "${hostName}". List hosts: agents hosts list`));
|
|
557
|
+
// The forwarding contract (RUN_OPTION_FORWARDING): options that cannot
|
|
558
|
+
// cross the SSH boundary fail loud BEFORE dispatch — never a silent
|
|
559
|
+
// drop. Value-aware: only reject what was actually passed.
|
|
560
|
+
const hostRejects = [];
|
|
561
|
+
if (options.secrets.length > 0)
|
|
562
|
+
hostRejects.push(RUN_OPTION_REJECT_MESSAGES.secrets);
|
|
563
|
+
if (options.secretsKeys)
|
|
564
|
+
hostRejects.push(RUN_OPTION_REJECT_MESSAGES.secretsKeys);
|
|
565
|
+
if (options.allowExpired)
|
|
566
|
+
hostRejects.push(RUN_OPTION_REJECT_MESSAGES.allowExpired);
|
|
567
|
+
if (options.resumeCheckpoint)
|
|
568
|
+
hostRejects.push(RUN_OPTION_REJECT_MESSAGES.resumeCheckpoint);
|
|
569
|
+
if (options.resume === true)
|
|
570
|
+
hostRejects.push(RUN_OPTION_REJECT_MESSAGES.resumeBare);
|
|
571
|
+
if (hostRejects.length > 0) {
|
|
572
|
+
for (const msg of hostRejects)
|
|
573
|
+
console.error(chalk.red(msg));
|
|
535
574
|
process.exit(1);
|
|
536
575
|
}
|
|
576
|
+
// Shared resolution (name → capability tag → error). A password-auth
|
|
577
|
+
// device throws DeviceOffloadUnsupportedError inside the helper and
|
|
578
|
+
// propagates untouched — it's printed cleanly by the top-level catch in
|
|
579
|
+
// index.ts (covers every resolveHost caller).
|
|
580
|
+
let host;
|
|
581
|
+
try {
|
|
582
|
+
host = await resolveHostRunTarget(hostName, { any: options.any });
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
if (e instanceof HostResolutionError) {
|
|
586
|
+
console.error(chalk.red(e.message));
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
throw e;
|
|
590
|
+
}
|
|
537
591
|
try {
|
|
538
592
|
const [runAgent, rawRunVersion] = agentSpec.split('@');
|
|
539
593
|
// Forward the explicit @version pin verbatim. Resolving aliases like
|
|
@@ -569,6 +623,29 @@ export function registerRunCommand(program) {
|
|
|
569
623
|
// --lease, a host is persistent, so this is strictly opt-in per run.
|
|
570
624
|
let hostCopyCreds;
|
|
571
625
|
if (options.copyCreds) {
|
|
626
|
+
// Refuse to copy credentials (and the Claude OAuth token) to a host
|
|
627
|
+
// whose SSH host key isn't pinned in the managed known_hosts store:
|
|
628
|
+
// the offload transport would otherwise ride an accept-new (TOFU)
|
|
629
|
+
// connection, so a machine-in-the-middle on an unverified first
|
|
630
|
+
// connect could capture the tokens. The dispatch itself then verifies
|
|
631
|
+
// strictly against that pin (see lib/hosts/dispatch.ts) (RUSH-1767).
|
|
632
|
+
// The ship/refuse decision — pinTarget resolution, the non-device
|
|
633
|
+
// self-pin, and the final pinned check — lives in decideCopyCredsGate
|
|
634
|
+
// (unit-tested against real known_hosts fixtures).
|
|
635
|
+
const gate = decideCopyCredsGate(host);
|
|
636
|
+
if (gate.selfPinned) {
|
|
637
|
+
console.error(chalk.gray(`Pinned "${host.name}" (${gate.pinTarget}) into the managed known_hosts store.`));
|
|
638
|
+
}
|
|
639
|
+
if (!gate.allowed) {
|
|
640
|
+
console.error(chalk.red(`Refusing --copy-creds to "${host.name}": its SSH host key isn't pinned, so the ` +
|
|
641
|
+
`credentials could be exposed to a machine-in-the-middle on an unverified first connect.`));
|
|
642
|
+
console.error(chalk.gray(host.provider === 'devices'
|
|
643
|
+
? `Pin the key first by connecting once so agents records and verifies it: agents ssh ${host.name}\n` +
|
|
644
|
+
`Then re-run with --copy-creds.`
|
|
645
|
+
: `Couldn't reach "${gate.pinTarget}" to pin its key (ssh-keyscan returned nothing). ` +
|
|
646
|
+
`Make sure the host is reachable over SSH, then re-run with --copy-creds.`));
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
572
649
|
const { detectSignedInRuntimes, pickRuntimes, resolveClaudeCredentialsBlob } = await import('../lib/crabbox/runtimes.js');
|
|
573
650
|
const { confirm } = await import('@inquirer/prompts');
|
|
574
651
|
const detected = await detectSignedInRuntimes();
|
|
@@ -636,10 +713,12 @@ export function registerRunCommand(program) {
|
|
|
636
713
|
agent: runAgent,
|
|
637
714
|
version: resumeId ? undefined : runVersion,
|
|
638
715
|
strategy: resumeId ? undefined : runStrategy,
|
|
716
|
+
fallback: options.fallback,
|
|
639
717
|
prompt,
|
|
640
718
|
mode: options.mode,
|
|
641
719
|
model: options.model,
|
|
642
720
|
effort: options.effort,
|
|
721
|
+
env: options.env,
|
|
643
722
|
addDir: hostAddDirs,
|
|
644
723
|
json: options.json,
|
|
645
724
|
verbose: options.verbose,
|
|
@@ -663,32 +742,37 @@ export function registerRunCommand(program) {
|
|
|
663
742
|
console.error(chalk.red('A prompt is required for headless host runs: agents run <agent> "<task>" --host <name>'));
|
|
664
743
|
process.exit(1);
|
|
665
744
|
}
|
|
666
|
-
|
|
667
|
-
|
|
745
|
+
// Session-id mint, detached dispatch, and local session-index
|
|
746
|
+
// registration all live in the shared helper (lib/hosts/run-target.ts).
|
|
747
|
+
const { task, exitCode } = await dispatchPromptToHost(host, {
|
|
668
748
|
agent: runAgent,
|
|
669
749
|
version: resumeId ? undefined : runVersion,
|
|
670
750
|
strategy: resumeId ? undefined : runStrategy,
|
|
751
|
+
fallback: options.fallback,
|
|
671
752
|
prompt,
|
|
672
753
|
mode: options.mode,
|
|
673
754
|
model: options.model,
|
|
674
755
|
effort: options.effort,
|
|
756
|
+
env: options.env,
|
|
675
757
|
addDir: hostAddDirs,
|
|
758
|
+
timeout: options.timeout,
|
|
759
|
+
loop: options.loop,
|
|
760
|
+
maxIterations: options.maxIterations,
|
|
761
|
+
budget: options.budget,
|
|
762
|
+
until: options.until,
|
|
763
|
+
interval: options.interval,
|
|
676
764
|
json: options.json,
|
|
677
765
|
verbose: options.verbose,
|
|
678
|
-
timeout: options.timeout,
|
|
679
766
|
yes: options.yes,
|
|
680
767
|
acp: options.acp,
|
|
768
|
+
autoSecrets: options.autoSecrets,
|
|
681
769
|
remoteCwd: hostCwd,
|
|
682
|
-
sessionId: hostSessionId,
|
|
683
770
|
name: options.name,
|
|
684
771
|
resume: resumeId,
|
|
685
772
|
follow: options.follow !== false,
|
|
773
|
+
passthroughArgs,
|
|
686
774
|
copyCreds: hostCopyCreds,
|
|
687
775
|
});
|
|
688
|
-
// Register the dispatched run in the LOCAL session index so it shows
|
|
689
|
-
// up in `agents sessions` and resolves by id/name, even though its
|
|
690
|
-
// transcript lives on the host. No-op when no session id was captured.
|
|
691
|
-
registerHostSession(task, { cwd: process.cwd(), prompt });
|
|
692
776
|
if (options.follow === false) {
|
|
693
777
|
// The handle the caller uses to check on the run: the name if given,
|
|
694
778
|
// else the real host-task id (never the old literal `<id>`). Steer
|
|
@@ -803,7 +887,7 @@ export function registerRunCommand(program) {
|
|
|
803
887
|
await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
|
|
804
888
|
process.exit(resumeExit);
|
|
805
889
|
}
|
|
806
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
890
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported, isHeadlessSecretsContext }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
807
891
|
import('../lib/exec.js'),
|
|
808
892
|
import('../lib/agents.js'),
|
|
809
893
|
import('../lib/profiles.js'),
|
|
@@ -841,6 +925,11 @@ export function registerRunCommand(program) {
|
|
|
841
925
|
// produced item, and drives the teams supervisor to drain — no `teams`
|
|
842
926
|
// subcommands needed.
|
|
843
927
|
let workflowForEach;
|
|
928
|
+
// True once this run copies ≥1 dispatchable subagent into the shared agents
|
|
929
|
+
// dir. Used below to keep the `Task` tool in a `tools:`-restricted workflow —
|
|
930
|
+
// an orchestrator handed subagents but denied `Task` cannot reach them and
|
|
931
|
+
// degenerates to a no-op ("I'll wait for the completion notification").
|
|
932
|
+
let workflowHasSubagents = false;
|
|
844
933
|
const cwd = options.cwd ?? process.cwd();
|
|
845
934
|
if (isValidAgent(rawAgent)) {
|
|
846
935
|
agent = rawAgent;
|
|
@@ -922,6 +1011,8 @@ export function registerRunCommand(program) {
|
|
|
922
1011
|
workflowSubagentTargets.push(dest);
|
|
923
1012
|
copied++;
|
|
924
1013
|
}
|
|
1014
|
+
if (copied > 0)
|
|
1015
|
+
workflowHasSubagents = true;
|
|
925
1016
|
if (allowedAgents !== undefined) {
|
|
926
1017
|
// Surface any allowedAgents entry with no matching subagent file, and
|
|
927
1018
|
// report how many were filtered out, so the scope is auditable.
|
|
@@ -965,7 +1056,9 @@ export function registerRunCommand(program) {
|
|
|
965
1056
|
}
|
|
966
1057
|
// Auto-inject secrets bundles declared in the workflow's frontmatter `secrets:` field.
|
|
967
1058
|
// Union with any --secrets flags the user passed; dedupe. Skip when --no-auto-secrets is set.
|
|
968
|
-
|
|
1059
|
+
// (Commander stores the negated flag as `autoSecrets: false` — the old
|
|
1060
|
+
// `noAutoSecrets` read was never populated, making the flag a no-op.)
|
|
1061
|
+
if (options.autoSecrets !== false) {
|
|
969
1062
|
const declared = workflowFrontmatter?.secrets ?? [];
|
|
970
1063
|
if (declared.length > 0) {
|
|
971
1064
|
const existing = new Set(options.secrets);
|
|
@@ -1002,8 +1095,14 @@ export function registerRunCommand(program) {
|
|
|
1002
1095
|
}
|
|
1003
1096
|
else if (hasScoping) {
|
|
1004
1097
|
if (tools && tools.length > 0) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1098
|
+
// An orchestrator with dispatchable subagents MUST retain the `Task`
|
|
1099
|
+
// tool, or it can't reach the subagents this run just installed for it
|
|
1100
|
+
// — the run silently no-ops ("I'll wait for the completion notification").
|
|
1101
|
+
workflowToolsRestrict = ensureSubagentDispatchTool(tools, workflowHasSubagents);
|
|
1102
|
+
process.stderr.write(chalk.gray(`[workflow] restricting available tools to: ${workflowToolsRestrict.join(', ')} (Write/Bash/Edit unavailable unless listed)\n`));
|
|
1103
|
+
if (workflowToolsRestrict.length !== tools.length) {
|
|
1104
|
+
process.stderr.write(chalk.gray(`[workflow] kept Task tool: workflow ships subagents to dispatch\n`));
|
|
1105
|
+
}
|
|
1007
1106
|
}
|
|
1008
1107
|
if (mcpServerNames && mcpServerNames.length > 0) {
|
|
1009
1108
|
const servers = getMcpServersByName(mcpServerNames, { cwd });
|
|
@@ -1186,8 +1285,12 @@ export function registerRunCommand(program) {
|
|
|
1186
1285
|
process.exit(1);
|
|
1187
1286
|
}
|
|
1188
1287
|
const strategy = options.balanced ? 'balanced' : explicitStrategy ?? configuredStrategy;
|
|
1189
|
-
// Strategy only applies to bare agent invocations. Explicit @version
|
|
1190
|
-
// profiles
|
|
1288
|
+
// Strategy only applies to bare agent invocations. Explicit @version and
|
|
1289
|
+
// profiles already define their execution target. A --fallback chain does
|
|
1290
|
+
// NOT pin the primary: it only names where to cascade on a rate limit, so
|
|
1291
|
+
// the bare primary still resolves through the strategy — otherwise every
|
|
1292
|
+
// `agents run claude --fallback codex` run lands on the pinned default
|
|
1293
|
+
// account and account rotation silently stops (the gh-monitor heal bug).
|
|
1191
1294
|
if (strategy !== 'pinned' || options.balanced || explicitStrategy) {
|
|
1192
1295
|
if (version) {
|
|
1193
1296
|
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: version ${version} is pinned\n`));
|
|
@@ -1195,9 +1298,6 @@ export function registerRunCommand(program) {
|
|
|
1195
1298
|
else if (fromProfile) {
|
|
1196
1299
|
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: profile pins its own version/auth\n`));
|
|
1197
1300
|
}
|
|
1198
|
-
else if (options.fallback) {
|
|
1199
|
-
process.stderr.write(chalk.yellow(`[agents] strategy ${strategy} ignored: --fallback pins versions directly\n`));
|
|
1200
|
-
}
|
|
1201
1301
|
else {
|
|
1202
1302
|
try {
|
|
1203
1303
|
const resolved = await resolveRunVersion(agent, strategy, cwd);
|
|
@@ -1350,6 +1450,10 @@ export function registerRunCommand(program) {
|
|
|
1350
1450
|
caller: `agent ${agent}`,
|
|
1351
1451
|
keys: secretsKeysSubset,
|
|
1352
1452
|
allowExpired: options.allowExpired,
|
|
1453
|
+
// A headless/background run (routine, teammate, detached) must not
|
|
1454
|
+
// pop a Touch ID sheet nobody can answer — resolve broker-only and
|
|
1455
|
+
// fail fast with an actionable error. Interactive runs still prompt.
|
|
1456
|
+
agentOnly: isHeadlessSecretsContext(),
|
|
1353
1457
|
});
|
|
1354
1458
|
const entries = describeBundle(bundle);
|
|
1355
1459
|
const counts = {};
|
|
@@ -1470,17 +1574,23 @@ export function registerRunCommand(program) {
|
|
|
1470
1574
|
// path (continuing the session via /continue). Because this injects into
|
|
1471
1575
|
// the same `fallback` array `--fallback` uses, it must only arm for run
|
|
1472
1576
|
// shapes that accept a fallback chain — shouldArmRotationFailover excludes
|
|
1473
|
-
// acp/loop/resume-checkpoint (which reject a non-empty fallback below)
|
|
1474
|
-
// interactive/no-prompt runs
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1577
|
+
// acp/loop/resume-checkpoint (which reject a non-empty fallback below)
|
|
1578
|
+
// and interactive/no-prompt runs. Pinned/single-account runs stay
|
|
1579
|
+
// unchanged because rotationResult is null or rotationFailoverChain
|
|
1580
|
+
// returns []. version is set here because rotationResult is only
|
|
1581
|
+
// populated when resolveRunVersion picked one.
|
|
1582
|
+
//
|
|
1583
|
+
// Composes with an explicit --fallback chain: the same-agent accounts are
|
|
1584
|
+
// UNSHIFTED ahead of the user's cross-agent entries, so a rate limit
|
|
1585
|
+
// first tries the other accounts of the same agent (cheapest recovery —
|
|
1586
|
+
// same CLI, session continues) and only then cascades to codex/gemini/etc.
|
|
1587
|
+
// Profiles never compose: strategy is skipped for them, rotationResult
|
|
1588
|
+
// stays null. (fromProfile's model-swap unshift above is therefore never
|
|
1589
|
+
// displaced by this one.)
|
|
1479
1590
|
if (shouldArmRotationFailover({
|
|
1480
1591
|
hasRotation: !!rotationResult,
|
|
1481
1592
|
hasVersion: !!version,
|
|
1482
1593
|
hasPrompt: prompt !== undefined,
|
|
1483
|
-
explicitFallback: fallback.length > 0,
|
|
1484
1594
|
interactive: !!options.interactive,
|
|
1485
1595
|
acp: !!options.acp,
|
|
1486
1596
|
loop: !!options.loop,
|
|
@@ -1488,7 +1598,7 @@ export function registerRunCommand(program) {
|
|
|
1488
1598
|
})) {
|
|
1489
1599
|
const failover = rotationFailoverChain(rotationResult, version);
|
|
1490
1600
|
if (failover.length > 0) {
|
|
1491
|
-
fallback.
|
|
1601
|
+
fallback.unshift(...failover);
|
|
1492
1602
|
if (!options.quiet) {
|
|
1493
1603
|
const accounts = failover.map(f => `${f.agent}@${f.version}`).join(', ');
|
|
1494
1604
|
process.stderr.write(chalk.gray(`[agents] rate-limit failover armed: ${accounts}\n`));
|
package/dist/commands/feed.js
CHANGED
|
@@ -8,6 +8,7 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
|
8
8
|
import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
|
|
9
9
|
import { notifyUrgentBlock } from '../lib/notify.js';
|
|
10
10
|
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
11
|
+
import { isValidMailboxId } from '../lib/mailbox.js';
|
|
11
12
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
12
13
|
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
13
14
|
import { GLYPH, masthead } from '../lib/comms-render.js';
|
|
@@ -38,6 +39,11 @@ export function parseRemoteFeed(stdout, machine) {
|
|
|
38
39
|
const block = item;
|
|
39
40
|
if (!block.blockId || !block.sessionId || !block.mailboxId || !block.questions?.length)
|
|
40
41
|
continue;
|
|
42
|
+
// A crafted mailboxId (path separators, `.`/`..`) would throw inside
|
|
43
|
+
// mailboxDir() when policy runs against the block, aborting the whole
|
|
44
|
+
// dispatch loop — drop it here so a malicious peer can't smuggle one in.
|
|
45
|
+
if (!isValidMailboxId(block.mailboxId))
|
|
46
|
+
continue;
|
|
41
47
|
blocks.push({ ...block, host: machine });
|
|
42
48
|
}
|
|
43
49
|
return blocks;
|
|
@@ -240,20 +246,28 @@ export function registerFeedCommand(program) {
|
|
|
240
246
|
const policy = loadPolicy();
|
|
241
247
|
const now = new Date();
|
|
242
248
|
for (const b of blocks) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
recordNotified(b.blockId);
|
|
251
|
-
console.log(`${chalk.green('notified')} ${b.blockId}`);
|
|
249
|
+
// Wrap per-block policy so one malformed block (e.g. a crafted
|
|
250
|
+
// mailboxId that throws in mailboxDir) can't abort the whole loop and
|
|
251
|
+
// strand every remaining block's dispatch.
|
|
252
|
+
try {
|
|
253
|
+
const result = applyPolicyToBlock(b, policy, now);
|
|
254
|
+
if (result.action !== 'none') {
|
|
255
|
+
console.log(`${chalk.yellow('policy')} ${b.blockId}: ${result.action}`);
|
|
252
256
|
}
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
if (isPhoneUrgent(b, policy)) {
|
|
258
|
+
const notifyResult = await notifyUrgentBlock(b, { dryRun: opts.json });
|
|
259
|
+
if (notifyResult.ok && !notifyResult.skipped) {
|
|
260
|
+
recordNotified(b.blockId);
|
|
261
|
+
console.log(`${chalk.green('notified')} ${b.blockId}`);
|
|
262
|
+
}
|
|
263
|
+
else if (notifyResult.error) {
|
|
264
|
+
console.error(chalk.yellow(`Notification failed for ${b.blockId}: ${notifyResult.error}`));
|
|
265
|
+
}
|
|
255
266
|
}
|
|
256
267
|
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
console.error(chalk.yellow(`Skipped block ${b.blockId}: ${err.message}`));
|
|
270
|
+
}
|
|
257
271
|
}
|
|
258
272
|
}
|
|
259
273
|
for (const warning of setupWarnings) {
|
package/dist/commands/hosts.js
CHANGED
|
@@ -11,6 +11,7 @@ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/widt
|
|
|
11
11
|
import { checkbox, confirm } from '@inquirer/prompts';
|
|
12
12
|
import { assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
13
13
|
import { getProvider, listAllHosts, resolveHost } from '../lib/hosts/registry.js';
|
|
14
|
+
import { getDevice } from '../lib/devices/registry.js';
|
|
14
15
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
15
16
|
import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
|
|
16
17
|
import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
|
|
@@ -93,7 +94,7 @@ async function doAdd(name, target, opts) {
|
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
95
96
|
let spec;
|
|
96
|
-
let sshTarget;
|
|
97
|
+
let sshTarget = '';
|
|
97
98
|
if (target) {
|
|
98
99
|
assertValidSshTarget(target);
|
|
99
100
|
const { address, user } = parseTarget(target);
|
|
@@ -105,9 +106,36 @@ async function doAdd(name, target, opts) {
|
|
|
105
106
|
sshTarget = name;
|
|
106
107
|
}
|
|
107
108
|
else {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// A registered device enrolls with no target — connection details come from
|
|
110
|
+
// the device profile. The main reason to enroll one at all is the overlay
|
|
111
|
+
// metadata (capability tags); plain dispatch already works via the devices
|
|
112
|
+
// provider.
|
|
113
|
+
const device = await getDevice(name);
|
|
114
|
+
if (device && device.auth.method !== 'password') {
|
|
115
|
+
const address = device.address.dnsName ?? device.address.ip;
|
|
116
|
+
if (address) {
|
|
117
|
+
spec = {
|
|
118
|
+
name,
|
|
119
|
+
provider: 'local',
|
|
120
|
+
source: 'inline',
|
|
121
|
+
address,
|
|
122
|
+
user: device.user,
|
|
123
|
+
caps: opts.cap,
|
|
124
|
+
os: opts.os ?? (device.platform !== 'unknown' ? device.platform : undefined),
|
|
125
|
+
};
|
|
126
|
+
sshTarget = device.user ? `${device.user}@${address}` : address;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!spec) {
|
|
130
|
+
if (device?.auth.method === 'password') {
|
|
131
|
+
console.log(chalk.red(`Device "${name}" uses password auth — dispatch needs key auth. Switch it first: agents devices set ${name} --auth key`));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.log(chalk.red(`"${name}" is not in ~/.ssh/config or the devices registry. Pass a target: agents hosts add ${name} <user@host>`));
|
|
135
|
+
}
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
111
139
|
}
|
|
112
140
|
const probe = probeHost(sshTarget);
|
|
113
141
|
if (!spec.os && probe.os)
|
|
@@ -133,8 +161,12 @@ async function doList(json) {
|
|
|
133
161
|
// Cap TARGET at its column so a long user@host can't shove CAPS out of alignment.
|
|
134
162
|
const tgtW = stringWidth(tgt);
|
|
135
163
|
const tgtCol = tgtW > 28 ? truncateToWidth(tgt, 28) : tgt + ' '.repeat(28 - tgtW);
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
// Devices surface their registry as SOURCE (they're synced, not enrolled here).
|
|
165
|
+
const source = h.provider === 'devices' ? 'devices' : h.source;
|
|
166
|
+
const mark = h.dispatchable === false
|
|
167
|
+
? chalk.yellow(' ·password-auth (no dispatch)')
|
|
168
|
+
: h.enrolled ? '' : chalk.gray(' ·available');
|
|
169
|
+
console.log(h.name.padEnd(20) + source.padEnd(13) + tgtCol + (h.caps?.join(',') ?? '') + mark);
|
|
138
170
|
}
|
|
139
171
|
}
|
|
140
172
|
async function doCheck(name) {
|
|
@@ -163,6 +195,12 @@ async function doRemove(name) {
|
|
|
163
195
|
console.log(chalk.yellow(`"${name}" is not enrolled (nothing to remove).`));
|
|
164
196
|
return;
|
|
165
197
|
}
|
|
198
|
+
if (host.provider === 'devices') {
|
|
199
|
+
// The devices registry owns this entry — hosts remove would silently no-op.
|
|
200
|
+
console.log(chalk.yellow(`"${name}" comes from the devices registry. Remove it there: agents devices rm ${name}`));
|
|
201
|
+
process.exitCode = 1;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
166
204
|
await getProvider('local').remove(name);
|
|
167
205
|
console.log(chalk.green(`Removed ${name}`));
|
|
168
206
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -6,9 +6,9 @@ import { capableAgents, isCapable } from '../lib/capabilities.js';
|
|
|
6
6
|
import { emit } from '../lib/events.js';
|
|
7
7
|
import { AGENTS, getAllCliStates, resolveAgentName, formatAgentError, registerMcpToTargets, unregisterMcpFromTargets, listInstalledMcpsWithScope, parseMcpConfig, getMcpConfigPathForHome, agentLabel, } from '../lib/agents.js';
|
|
8
8
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
9
|
-
import { listMcpServerConfigs, discoverMcpConfigsFromRepo, installMcpConfigCentrally, } from '../lib/mcp.js';
|
|
9
|
+
import { listMcpServerConfigs, discoverMcpConfigsFromRepo, installMcpConfigCentrally, isProjectMcpTrusted, trustProjectMcp, untrustProjectMcp, getMcpTrustStorePath, } from '../lib/mcp.js';
|
|
10
10
|
import { cloneRepo } from '../lib/git.js';
|
|
11
|
-
import { getMcpDir } from '../lib/state.js';
|
|
11
|
+
import { getMcpDir, getProjectAgentsDir } from '../lib/state.js';
|
|
12
12
|
import { getEffectiveHome, getGlobalDefault, listInstalledVersions, getVersionHomePath, resolveInstalledAgentTargets, resolveConfiguredAgentTargets, syncResourcesToVersion, } from '../lib/versions.js';
|
|
13
13
|
import { getUserAgentsDir } from '../lib/state.js';
|
|
14
14
|
import { isPromptCancelled, isInteractiveTerminal, requireInteractiveSelection, promptRemovalTargets, parseCommaSeparatedList, ensureAgentVersionsInstalled, resolveAgentTargetsAutoInstalling, resolveInstalledAgentTargetsAutoInstalling, VersionNotInstalledError, resolveListFilterOrExit, } from './utils.js';
|
|
@@ -178,6 +178,43 @@ When to use:
|
|
|
178
178
|
filterVersion,
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
|
+
mcpCmd
|
|
182
|
+
.command('trust')
|
|
183
|
+
.description('Trust this project so its .agents/mcp/ servers may be registered and spawned')
|
|
184
|
+
.action(() => {
|
|
185
|
+
const projectAgentsDir = getProjectAgentsDir();
|
|
186
|
+
if (!projectAgentsDir) {
|
|
187
|
+
console.log(chalk.red('Not inside a project (no .agents/ directory found) — nothing to trust.'));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const servers = listMcpServerConfigs().filter((s) => s.scope === 'project');
|
|
191
|
+
if (servers.length > 0) {
|
|
192
|
+
console.log(chalk.bold('Project-scoped MCP servers that will be trusted to run:'));
|
|
193
|
+
for (const s of servers) {
|
|
194
|
+
console.log(' ' + chalk.cyan(s.name) + ' ' + chalk.white(formatCentralCommand(s)));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const trusted = trustProjectMcp();
|
|
198
|
+
console.log(chalk.green(`Trusted project MCP servers for: ${trusted}`));
|
|
199
|
+
console.log(chalk.gray(`Recorded in ${getMcpTrustStorePath()} — revoke with: agents mcp untrust`));
|
|
200
|
+
});
|
|
201
|
+
mcpCmd
|
|
202
|
+
.command('untrust')
|
|
203
|
+
.description('Revoke MCP trust for this project (its .agents/mcp/ servers stop auto-applying)')
|
|
204
|
+
.action(() => {
|
|
205
|
+
const projectAgentsDir = getProjectAgentsDir();
|
|
206
|
+
if (!projectAgentsDir) {
|
|
207
|
+
console.log(chalk.red('Not inside a project (no .agents/ directory found).'));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
const removed = untrustProjectMcp();
|
|
211
|
+
if (removed) {
|
|
212
|
+
console.log(chalk.green('Revoked MCP trust for this project.'));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.log(chalk.gray('This project was not trusted — nothing to revoke.'));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
181
218
|
mcpCmd
|
|
182
219
|
.command('add <name> [command_or_url...]')
|
|
183
220
|
.description('Add an MCP server to the manifest (run "agents mcp register" afterward to apply)')
|
|
@@ -797,9 +834,15 @@ function formatCentralCommand(server) {
|
|
|
797
834
|
function formatMcpDetail(name, source, centralConfig, manifestConfig, command, targets) {
|
|
798
835
|
const lines = [];
|
|
799
836
|
lines.push(chalk.bold.cyan(name));
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
|
|
837
|
+
const isUntrustedProject = centralConfig?.scope === 'project'
|
|
838
|
+
&& !isProjectMcpTrusted(getProjectAgentsDir() ?? '');
|
|
839
|
+
const tag = centralConfig?.scope === 'project'
|
|
840
|
+
? (isUntrustedProject
|
|
841
|
+
? chalk.yellow('declared in <repo>/.agents/mcp/ (untrusted project — will NOT auto-apply)')
|
|
842
|
+
: chalk.green('declared in <repo>/.agents/mcp/ (trusted project)')) :
|
|
843
|
+
source === 'central' ? chalk.green('declared in ~/.agents/mcp/') :
|
|
844
|
+
source === 'manifest' ? chalk.gray('declared in agents.yaml') :
|
|
845
|
+
chalk.yellow('unmanaged (not in central or manifest)');
|
|
803
846
|
lines.push(' ' + tag);
|
|
804
847
|
lines.push('');
|
|
805
848
|
if (centralConfig) {
|
|
@@ -814,6 +857,13 @@ function formatMcpDetail(name, source, centralConfig, manifestConfig, command, t
|
|
|
814
857
|
if (command) {
|
|
815
858
|
lines.push(` ${chalk.gray('command:')} ${chalk.white(command)}`);
|
|
816
859
|
}
|
|
860
|
+
// A project-scoped server is an arbitrary command from a (possibly cloned)
|
|
861
|
+
// repo. Show exactly what would run and how to opt in (RUSH-1776).
|
|
862
|
+
if (isUntrustedProject) {
|
|
863
|
+
lines.push('');
|
|
864
|
+
lines.push(' ' + chalk.yellow('This project is untrusted — its MCP servers are skipped on sync and workflow runs.'));
|
|
865
|
+
lines.push(' ' + chalk.gray('Review the command above, then opt in with: ') + chalk.white('agents mcp trust'));
|
|
866
|
+
}
|
|
817
867
|
lines.push('');
|
|
818
868
|
lines.push(chalk.bold(' Synced to'));
|
|
819
869
|
lines.push(buildTargetsSection(targets));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitors — durable event-triggered watchers.
|
|
3
|
+
*
|
|
4
|
+
* Registers the `agents monitors` command tree: create, list, view, dry-run test,
|
|
5
|
+
* edit, logs, fire history, pause/resume, (re)pin the owner device, and remove.
|
|
6
|
+
* Mirrors `agents routines` (commands/routines.ts) and shares the same daemon +
|
|
7
|
+
* dispatch engine underneath — a monitor is a routine whose trigger is a watched
|
|
8
|
+
* source instead of a clock.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
/** Register the `agents monitors` command tree. */
|
|
12
|
+
export declare function registerMonitorsCommands(program: Command): void;
|