@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
|
@@ -25,7 +25,7 @@ import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainTo
|
|
|
25
25
|
import { fileStore } from './filestore.js';
|
|
26
26
|
import { emit } from '../events.js';
|
|
27
27
|
import { readMeta } from '../state.js';
|
|
28
|
-
import { agentGetSync, agentAutoLoadSync, agentGetMetaSync, agentAutoLoadMetaSync, agentEvictSync, secretsAgentAutoEnabled,
|
|
28
|
+
import { agentGetSync, agentAutoLoadSync, agentGetMetaSync, agentAutoLoadMetaSync, agentEvictSync, secretsAgentAutoEnabled, secretsHoldMs } from './agent.js';
|
|
29
29
|
import { createHash } from 'node:crypto';
|
|
30
30
|
const keychainStore = {
|
|
31
31
|
has: hasKeychainToken,
|
|
@@ -492,8 +492,11 @@ export function listBundles() {
|
|
|
492
492
|
for (const bundle of keychainBundles)
|
|
493
493
|
out.push(bundle);
|
|
494
494
|
// Populate the broker for the rest of the hold window (fire-and-forget).
|
|
495
|
+
// Same configurable cap as the value read-path — otherwise `secrets list`
|
|
496
|
+
// would keep serving a stale metadata snapshot for 7d even when the user
|
|
497
|
+
// capped the hold at 24h via secrets.agent.holdMs.
|
|
495
498
|
if (useAgent && keychainBundles.length > 0) {
|
|
496
|
-
agentAutoLoadMetaSync(nameSetHash, keychainBundles,
|
|
499
|
+
agentAutoLoadMetaSync(nameSetHash, keychainBundles, secretsHoldMs());
|
|
497
500
|
}
|
|
498
501
|
}
|
|
499
502
|
}
|
|
@@ -705,6 +708,44 @@ export function resolveBundleEnv(bundle, _opts = {}) {
|
|
|
705
708
|
void _opts.caller;
|
|
706
709
|
return env;
|
|
707
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* True when the current process is a background / non-interactive context that
|
|
713
|
+
* must NEVER raise a Keychain biometry prompt on the interactive user's screen —
|
|
714
|
+
* a prompt nobody is watching. Two signals, either sufficient:
|
|
715
|
+
* - `AGENTS_RUNTIME` is `headless` or `teams` (set on the child env by
|
|
716
|
+
* `agents run --headless`, scheduled routines, and teammates — see
|
|
717
|
+
* exec.ts:resolveInteractive, runner.ts, teams/agents.ts).
|
|
718
|
+
* - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
|
|
719
|
+
* stdio is redirected to a log — e.g. a release script run in the
|
|
720
|
+
* background as `( ... ) >log 2>&1 </dev/null`).
|
|
721
|
+
* `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
|
|
722
|
+
* even in a non-TTY context. An interactive `eval "$(agents secrets export X)"`
|
|
723
|
+
* keeps its terminal stdin, so it is NOT classified headless and still prompts.
|
|
724
|
+
*
|
|
725
|
+
* Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
|
|
726
|
+
* broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
|
|
727
|
+
* the Windows credential store resolve without any prompt. So off-darwin this
|
|
728
|
+
* ALWAYS returns false: forcing broker-only there would break every headless
|
|
729
|
+
* Linux/Windows read (CI, `agents run --headless`, routines, the Linux-driven
|
|
730
|
+
* release flow) for no benefit — there is no prompt to suppress.
|
|
731
|
+
*
|
|
732
|
+
* A read in a macOS headless context resolves broker-only (agentOnly) and fails
|
|
733
|
+
* fast with an actionable error instead of hijacking Touch ID. This generalizes
|
|
734
|
+
* the per-caller pattern already used by the daemon (daemon.ts:readDaemonClaudeOAuthToken).
|
|
735
|
+
*/
|
|
736
|
+
export function isHeadlessSecretsContext(env = process.env, platform = process.platform) {
|
|
737
|
+
if (platform !== 'darwin')
|
|
738
|
+
return false; // no biometry prompt to suppress off-darwin
|
|
739
|
+
const override = env.AGENTS_SECRETS_NO_PROMPT;
|
|
740
|
+
if (override === '1')
|
|
741
|
+
return true;
|
|
742
|
+
if (override === '0')
|
|
743
|
+
return false;
|
|
744
|
+
const runtime = env.AGENTS_RUNTIME;
|
|
745
|
+
if (runtime === 'headless' || runtime === 'teams')
|
|
746
|
+
return true;
|
|
747
|
+
return !process.stdin.isTTY && !process.stdout.isTTY;
|
|
748
|
+
}
|
|
708
749
|
/**
|
|
709
750
|
* Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
|
|
710
751
|
*
|
|
@@ -747,8 +788,15 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
747
788
|
return filtered;
|
|
748
789
|
}
|
|
749
790
|
}
|
|
750
|
-
|
|
751
|
-
|
|
791
|
+
// Only keychain-backed bundles can pop a Touch ID prompt and are the only ones
|
|
792
|
+
// the broker ever holds. A file-backed bundle resolves via passphrase with no
|
|
793
|
+
// prompt, so agentOnly must never block it — the broker never holds file
|
|
794
|
+
// bundles, so the throw would fire unconditionally and break a legitimate read.
|
|
795
|
+
if (opts.agentOnly && backend === 'keychain') {
|
|
796
|
+
throw new Error(`Secrets bundle '${name}' is not unlocked in the secrets agent, and this is a ` +
|
|
797
|
+
`headless/background process that must not raise a Touch ID prompt on the ` +
|
|
798
|
+
`interactive user's screen. Run 'agents secrets unlock ${name}' in a terminal ` +
|
|
799
|
+
`first, or set AGENTS_SECRETS_NO_PROMPT=0 to force an interactive prompt.`);
|
|
752
800
|
}
|
|
753
801
|
if (backend === 'file')
|
|
754
802
|
assertFileBackendUsable(name);
|
|
@@ -881,15 +929,18 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
881
929
|
emitReadAudit('success');
|
|
882
930
|
// Auto-cache: this was a real keychain read (the agent fast-path returned
|
|
883
931
|
// earlier on a hit). If the bundle opts into the `daily` policy and the user
|
|
884
|
-
// enabled `secrets.agent.auto`, populate the broker
|
|
885
|
-
//
|
|
886
|
-
//
|
|
932
|
+
// enabled `secrets.agent.auto`, populate the broker so the next concurrent
|
|
933
|
+
// run reads silently. Skipped when noAgent (e.g. `unlock`, which loads the
|
|
934
|
+
// agent itself). When a broker is already up this warms it synchronously
|
|
935
|
+
// (bounded ~3s) so `daily` reliably sticks; only a cold-start broker uses the
|
|
936
|
+
// detached fire-and-forget path (see agentAutoLoadSync). The costly Touch ID
|
|
937
|
+
// prompt already happened, so the bounded wait is invisible.
|
|
887
938
|
if (backend === 'keychain' &&
|
|
888
939
|
!opts.noAgent &&
|
|
889
940
|
process.env.AGENTS_SECRETS_NO_AGENT !== '1' &&
|
|
890
941
|
bundlePolicy(bundle) === 'daily' &&
|
|
891
942
|
secretsAgentAutoEnabled()) {
|
|
892
|
-
agentAutoLoadSync(name, bundle, env,
|
|
943
|
+
agentAutoLoadSync(name, bundle, env, secretsHoldMs());
|
|
893
944
|
}
|
|
894
945
|
return { bundle, env };
|
|
895
946
|
}
|
|
@@ -22,6 +22,15 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { KeychainBackend } from './index.js';
|
|
24
24
|
export declare function fileDir(): string;
|
|
25
|
+
/**
|
|
26
|
+
* Turn off terminal echo on the controlling TTY, or throw — fail CLOSED. If echo
|
|
27
|
+
* cannot be disabled (`stty` missing, no controlling terminal) we must NOT fall
|
|
28
|
+
* through and read the passphrase anyway: that echoes the secret to the screen
|
|
29
|
+
* and into scrollback (RUSH-1764). Refuse and point the user at the environment
|
|
30
|
+
* variable instead. `run` performs the echo-disable and throws iff it fails.
|
|
31
|
+
* Exported so the fail-closed contract has direct test coverage.
|
|
32
|
+
*/
|
|
33
|
+
export declare function disableTtyEchoOrThrow(run: () => void): void;
|
|
25
34
|
/** True if a machine-local passphrase has already been provisioned. */
|
|
26
35
|
export declare function machinePassphraseExists(): boolean;
|
|
27
36
|
/**
|
|
@@ -65,6 +65,24 @@ finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) }
|
|
|
65
65
|
}
|
|
66
66
|
return (res.stdout?.toString() ?? '').replace(/\r?\n$/, '');
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Turn off terminal echo on the controlling TTY, or throw — fail CLOSED. If echo
|
|
70
|
+
* cannot be disabled (`stty` missing, no controlling terminal) we must NOT fall
|
|
71
|
+
* through and read the passphrase anyway: that echoes the secret to the screen
|
|
72
|
+
* and into scrollback (RUSH-1764). Refuse and point the user at the environment
|
|
73
|
+
* variable instead. `run` performs the echo-disable and throws iff it fails.
|
|
74
|
+
* Exported so the fail-closed contract has direct test coverage.
|
|
75
|
+
*/
|
|
76
|
+
export function disableTtyEchoOrThrow(run) {
|
|
77
|
+
try {
|
|
78
|
+
run();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new Error('Refusing to prompt for AGENTS_SECRETS_PASSPHRASE: terminal echo could not be ' +
|
|
82
|
+
'disabled (stty unavailable or no controlling TTY), so the passphrase would be ' +
|
|
83
|
+
'shown in cleartext. Set AGENTS_SECRETS_PASSPHRASE in the environment instead.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
68
86
|
function readPassphraseFromTty() {
|
|
69
87
|
if (process.platform === 'win32')
|
|
70
88
|
return readPassphraseFromTtyWindows();
|
|
@@ -72,14 +90,9 @@ function readPassphraseFromTty() {
|
|
|
72
90
|
let echoDisabled = false;
|
|
73
91
|
try {
|
|
74
92
|
fs.writeSync(fd, 'Enter AGENTS_SECRETS_PASSPHRASE: ');
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
// stty not available — fall through; passphrase will echo. Better
|
|
81
|
-
// than refusing to function.
|
|
82
|
-
}
|
|
93
|
+
// Fail closed: if echo can't be turned off, abort rather than echo the secret.
|
|
94
|
+
disableTtyEchoOrThrow(() => execSync('stty -echo < /dev/tty', { stdio: 'ignore' }));
|
|
95
|
+
echoDisabled = true;
|
|
83
96
|
let pass = '';
|
|
84
97
|
const buf = Buffer.alloc(1);
|
|
85
98
|
while (true) {
|
|
@@ -214,6 +214,13 @@ export declare function getKeychainTokens(items: string[]): Map<string, string>;
|
|
|
214
214
|
* predates that path rejects the unknown command (exit 2) and this throws,
|
|
215
215
|
* rather than silently falling back to an ACL'd `set` (which would behave like
|
|
216
216
|
* `always`). Ignored by the Linux/Windows/test backends, which have no ACL. */
|
|
217
|
+
/**
|
|
218
|
+
* argv for writing a bare (non-`agents-cli.`) keychain item via
|
|
219
|
+
* `/usr/bin/security add-generic-password`, deliberately WITHOUT the value: the
|
|
220
|
+
* secret travels over stdin (see setKeychainToken) so it never lands in argv or
|
|
221
|
+
* a `ps` snapshot. Exported so a test can assert the value is absent from argv.
|
|
222
|
+
*/
|
|
223
|
+
export declare function buildAddGenericPasswordArgs(account: string, item: string): string[];
|
|
217
224
|
export declare function setKeychainToken(item: string, value: string, opts?: {
|
|
218
225
|
noAcl?: boolean;
|
|
219
226
|
}): void;
|
|
@@ -844,6 +844,15 @@ function parseBatchRecords(out, record) {
|
|
|
844
844
|
* predates that path rejects the unknown command (exit 2) and this throws,
|
|
845
845
|
* rather than silently falling back to an ACL'd `set` (which would behave like
|
|
846
846
|
* `always`). Ignored by the Linux/Windows/test backends, which have no ACL. */
|
|
847
|
+
/**
|
|
848
|
+
* argv for writing a bare (non-`agents-cli.`) keychain item via
|
|
849
|
+
* `/usr/bin/security add-generic-password`, deliberately WITHOUT the value: the
|
|
850
|
+
* secret travels over stdin (see setKeychainToken) so it never lands in argv or
|
|
851
|
+
* a `ps` snapshot. Exported so a test can assert the value is absent from argv.
|
|
852
|
+
*/
|
|
853
|
+
export function buildAddGenericPasswordArgs(account, item) {
|
|
854
|
+
return ['add-generic-password', '-U', '-a', account, '-s', item, '-w'];
|
|
855
|
+
}
|
|
847
856
|
export function setKeychainToken(item, value, opts) {
|
|
848
857
|
// Validate the CLEARTEXT name (a hashed storage name is always clean), then
|
|
849
858
|
// resolve the storage name.
|
|
@@ -872,12 +881,23 @@ export function setKeychainToken(item, value, opts) {
|
|
|
872
881
|
// /usr/bin/security read can't satisfy without popping the legacy password
|
|
873
882
|
// sheet. -U upserts so repeated sets overwrite in place.
|
|
874
883
|
if (!isOurItem(item)) {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
884
|
+
// The secret VALUE must never appear in argv — a `ps` snapshot on a shared
|
|
885
|
+
// host would leak it (RUSH-1764). `security add-generic-password` has no
|
|
886
|
+
// stdin-password flag; instead a BARE `-w` as the LAST option makes it prompt
|
|
887
|
+
// ("password data for new item:" / "retype...") and read the secret from fd 0
|
|
888
|
+
// via readpassphrase(3) -- so the value travels over stdin, never on the
|
|
889
|
+
// command line. The prompt asks twice (enter + confirm), so we pipe the value
|
|
890
|
+
// TWICE; a single line fails the confirm and would store an empty secret. The
|
|
891
|
+
// item stays ACL-free (no biometry gate), so the no-prompt /usr/bin/security
|
|
892
|
+
// read path in getKeychainToken still works. Values are newline-free on darwin
|
|
893
|
+
// (assertValueStorable above), so each line carries the whole secret verbatim
|
|
894
|
+
// (no shell/quoting layer). `timeout` bounds the call so a context that cannot
|
|
895
|
+
// read the prompt fails loudly instead of hanging.
|
|
896
|
+
const sec = spawnSync('/usr/bin/security', buildAddGenericPasswordArgs(os.userInfo().username, item), {
|
|
897
|
+
input: `${value}\n${value}\n`,
|
|
898
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
899
|
+
timeout: 10_000,
|
|
900
|
+
});
|
|
881
901
|
if (sec.status !== 0) {
|
|
882
902
|
const msg = sec.stderr?.toString().trim();
|
|
883
903
|
throw new Error(msg || `Failed to write keychain item '${item}'.`);
|
package/dist/lib/secrets/mcp.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* unit-testable in-process.
|
|
25
25
|
*/
|
|
26
26
|
import * as readline from 'readline';
|
|
27
|
-
import { listBundles, readAndResolveBundleEnv, readBundle, validateBundleName, } from './bundles.js';
|
|
27
|
+
import { listBundles, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, validateBundleName, } from './bundles.js';
|
|
28
28
|
/** MCP protocol revision this server negotiates. */
|
|
29
29
|
export const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
30
30
|
/** The single tool this server exposes. */
|
|
@@ -68,7 +68,9 @@ export function resolveSecret(bundle, key) {
|
|
|
68
68
|
throw new Error(`Key '${key}' not found in bundle '${bundle}'.` +
|
|
69
69
|
(available.length ? ` Available keys: ${available.join(', ')}.` : ' Bundle has no keys.'));
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
// The MCP get_secret tool is typically served by a background/headless agent
|
|
72
|
+
// process; resolve broker-only there so it never raises an unwatched prompt.
|
|
73
|
+
const { env } = readAndResolveBundleEnv(bundle, { caller: 'secrets-mcp', agentOnly: isHeadlessSecretsContext() });
|
|
72
74
|
const value = env[key];
|
|
73
75
|
if (value === undefined) {
|
|
74
76
|
throw new Error(`Key '${key}' in bundle '${bundle}' could not be resolved.`);
|
|
@@ -16,6 +16,23 @@
|
|
|
16
16
|
* in argv / `ps` / remote shell history. Nothing is persisted locally.
|
|
17
17
|
*/
|
|
18
18
|
import { type SshExecResult } from '../ssh-exec.js';
|
|
19
|
+
/**
|
|
20
|
+
* Trust boundary for a remote-resolved env map. A peer's `secrets export` output
|
|
21
|
+
* is untrusted input: a compromised or misconfigured host could return keys that
|
|
22
|
+
* silently reshape THIS process's behavior once merged into the agent env
|
|
23
|
+
* (bundles.ts:251 `sanitizeProcessEnv` only strips loader vars from process.env,
|
|
24
|
+
* never the remote bundle). Block the dangerous-override classes here — at the
|
|
25
|
+
* source — so every consumer (`run --secrets b@host`, `secrets exec --host`) is
|
|
26
|
+
* protected, not just one call site:
|
|
27
|
+
* - LD_* / DYLD_* / NODE_OPTIONS and the other loader/interpreter injections
|
|
28
|
+
* (reuses the canonical bundles.ts predicate);
|
|
29
|
+
* - GIT_* — GIT_SSH_COMMAND et al. hijack every git subprocess;
|
|
30
|
+
* - *_PROXY — HTTP(S)_PROXY / ALL_PROXY reroute outbound traffic (MITM);
|
|
31
|
+
* - *_BASE_URL — ANTHROPIC_BASE_URL / OPENAI_BASE_URL redirect the model API.
|
|
32
|
+
* These keys are already rejected on the ADD side (validateEnvKey for loaders),
|
|
33
|
+
* so a legitimate bundle never carries them — only a hostile peer would.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isDangerousRemoteEnvKey(name: string): boolean;
|
|
19
36
|
/**
|
|
20
37
|
* Resolve a `--host` value to an ssh target string. Tries the `agents hosts`
|
|
21
38
|
* registry first (enrolled name → ssh-config alias / `user@host`); on a miss,
|
|
@@ -21,7 +21,36 @@ import { emit } from '../events.js';
|
|
|
21
21
|
import { sshTargetFor } from '../hosts/types.js';
|
|
22
22
|
import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
|
|
23
23
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
24
|
+
import { isLoaderOrInterpreterEnv } from './bundles.js';
|
|
24
25
|
const REMOTE_TIMEOUT_MS = 30_000;
|
|
26
|
+
/**
|
|
27
|
+
* Trust boundary for a remote-resolved env map. A peer's `secrets export` output
|
|
28
|
+
* is untrusted input: a compromised or misconfigured host could return keys that
|
|
29
|
+
* silently reshape THIS process's behavior once merged into the agent env
|
|
30
|
+
* (bundles.ts:251 `sanitizeProcessEnv` only strips loader vars from process.env,
|
|
31
|
+
* never the remote bundle). Block the dangerous-override classes here — at the
|
|
32
|
+
* source — so every consumer (`run --secrets b@host`, `secrets exec --host`) is
|
|
33
|
+
* protected, not just one call site:
|
|
34
|
+
* - LD_* / DYLD_* / NODE_OPTIONS and the other loader/interpreter injections
|
|
35
|
+
* (reuses the canonical bundles.ts predicate);
|
|
36
|
+
* - GIT_* — GIT_SSH_COMMAND et al. hijack every git subprocess;
|
|
37
|
+
* - *_PROXY — HTTP(S)_PROXY / ALL_PROXY reroute outbound traffic (MITM);
|
|
38
|
+
* - *_BASE_URL — ANTHROPIC_BASE_URL / OPENAI_BASE_URL redirect the model API.
|
|
39
|
+
* These keys are already rejected on the ADD side (validateEnvKey for loaders),
|
|
40
|
+
* so a legitimate bundle never carries them — only a hostile peer would.
|
|
41
|
+
*/
|
|
42
|
+
export function isDangerousRemoteEnvKey(name) {
|
|
43
|
+
const upper = name.toUpperCase();
|
|
44
|
+
if (isLoaderOrInterpreterEnv(upper))
|
|
45
|
+
return true;
|
|
46
|
+
if (upper.startsWith('GIT_'))
|
|
47
|
+
return true;
|
|
48
|
+
if (upper.endsWith('_PROXY'))
|
|
49
|
+
return true;
|
|
50
|
+
if (upper.endsWith('_BASE_URL'))
|
|
51
|
+
return true;
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
25
54
|
/** Remote OS for a host name or target string. Prefer the original host name
|
|
26
55
|
* because enrolled inline hosts resolve to `user@address`, while the OS
|
|
27
56
|
* registry is keyed by the host name. */
|
|
@@ -153,9 +182,20 @@ export async function remoteResolveEnv(target, bundle, opts = {}) {
|
|
|
153
182
|
throw new Error(`Unexpected payload resolving '${bundle}' on ${target}.`);
|
|
154
183
|
}
|
|
155
184
|
const env = {};
|
|
185
|
+
const blocked = [];
|
|
156
186
|
for (const [k, v] of Object.entries(parsed)) {
|
|
187
|
+
// Drop dangerous-override keys returned by the (untrusted) peer before they
|
|
188
|
+
// can reshape this process — see isDangerousRemoteEnvKey.
|
|
189
|
+
if (isDangerousRemoteEnvKey(k)) {
|
|
190
|
+
blocked.push(k);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
157
193
|
env[k] = typeof v === 'string' ? v : String(v);
|
|
158
194
|
}
|
|
195
|
+
if (blocked.length > 0) {
|
|
196
|
+
process.stderr.write(`[secrets] Dropped ${blocked.length} dangerous key(s) from '${bundle}'@${target} ` +
|
|
197
|
+
`(remote override blocked): ${blocked.join(', ')}\n`);
|
|
198
|
+
}
|
|
159
199
|
// The remote host audits its own `secrets export` read; this emit records the
|
|
160
200
|
// event on the INITIATING host too (values were pulled into this process and
|
|
161
201
|
// injected locally). Covers `secrets exec --host` and `run --secrets b@host`.
|
|
@@ -83,6 +83,26 @@ export declare function installPackageIntoPrefix(spec: string, prefix: string):
|
|
|
83
83
|
* package's postinstall hook.
|
|
84
84
|
*/
|
|
85
85
|
export declare function installPackageWithBun(spec: string): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Verify a downloaded tarball's bytes against a Subresource Integrity (SRI)
|
|
88
|
+
* string of the form `sha512-<base64>` — npm's `dist.integrity`. Recomputes the
|
|
89
|
+
* digest over the actual bytes with the named algorithm and compares it, in
|
|
90
|
+
* constant time, to the decoded expected digest.
|
|
91
|
+
*
|
|
92
|
+
* Fails closed: a malformed SRI, an algorithm weaker than sha512, or any digest
|
|
93
|
+
* mismatch throws. This is the gate that makes self-update refuse a tampered or
|
|
94
|
+
* corrupted tarball *before* it is ever handed to a package manager to install.
|
|
95
|
+
*/
|
|
96
|
+
export declare function verifyTarballIntegrity(tarball: Buffer, integrity: string): void;
|
|
97
|
+
/**
|
|
98
|
+
* Download the published tarball at `tarballUrl` and prove its bytes match
|
|
99
|
+
* `integrity` before returning a path to it on disk. The returned .tgz is safe
|
|
100
|
+
* to hand to `npm install`/`bun add` — it has been verified byte-for-byte
|
|
101
|
+
* against the registry attestation. Fails closed: a non-200, a download error,
|
|
102
|
+
* or a hash mismatch throws and no file path is returned, so the caller never
|
|
103
|
+
* installs an unverified artifact.
|
|
104
|
+
*/
|
|
105
|
+
export declare function downloadVerifiedTarball(tarballUrl: string, integrity: string, timeoutMs?: number): Promise<string>;
|
|
86
106
|
/** Read the version field of the package.json at `packageRoot`, fresh from disk. */
|
|
87
107
|
export declare function readInstalledVersion(packageRoot: string): string;
|
|
88
108
|
/**
|
package/dist/lib/self-update.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as os from 'os';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
+
import { createHash, timingSafeEqual } from 'crypto';
|
|
17
18
|
import { spawnSync } from 'child_process';
|
|
18
19
|
import { compareVersions } from './versions.js';
|
|
19
20
|
import { needsWindowsShell } from './platform/index.js';
|
|
@@ -164,7 +165,59 @@ export async function installPackageWithBun(spec) {
|
|
|
164
165
|
const { promisify } = await import('util');
|
|
165
166
|
const execFileAsync = promisify(execFile);
|
|
166
167
|
// On Windows `bun` resolves to `bun.exe`/`bun.cmd`; force shell for the .cmd case.
|
|
167
|
-
|
|
168
|
+
// --ignore-scripts: the tarball has already been integrity-verified, but its
|
|
169
|
+
// lifecycle scripts must not run at install time (the caller refreshes shims
|
|
170
|
+
// explicitly via refreshAliasShims()) — same fail-closed posture as the npm path.
|
|
171
|
+
await execFileAsync('bun', ['add', '-g', spec, '--ignore-scripts'], { shell: needsWindowsShell('bun') });
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Verify a downloaded tarball's bytes against a Subresource Integrity (SRI)
|
|
175
|
+
* string of the form `sha512-<base64>` — npm's `dist.integrity`. Recomputes the
|
|
176
|
+
* digest over the actual bytes with the named algorithm and compares it, in
|
|
177
|
+
* constant time, to the decoded expected digest.
|
|
178
|
+
*
|
|
179
|
+
* Fails closed: a malformed SRI, an algorithm weaker than sha512, or any digest
|
|
180
|
+
* mismatch throws. This is the gate that makes self-update refuse a tampered or
|
|
181
|
+
* corrupted tarball *before* it is ever handed to a package manager to install.
|
|
182
|
+
*/
|
|
183
|
+
export function verifyTarballIntegrity(tarball, integrity) {
|
|
184
|
+
const dash = integrity.indexOf('-');
|
|
185
|
+
if (dash <= 0) {
|
|
186
|
+
throw new Error(`malformed integrity string: ${JSON.stringify(integrity)}`);
|
|
187
|
+
}
|
|
188
|
+
const algorithm = integrity.slice(0, dash);
|
|
189
|
+
const expectedBase64 = integrity.slice(dash + 1);
|
|
190
|
+
// npm publishes sha512 SRI; refuse to verify against anything weaker rather
|
|
191
|
+
// than silently accepting a downgraded (e.g. sha1) attestation.
|
|
192
|
+
if (algorithm !== 'sha512') {
|
|
193
|
+
throw new Error(`unsupported integrity algorithm '${algorithm}' (expected sha512)`);
|
|
194
|
+
}
|
|
195
|
+
const expected = Buffer.from(expectedBase64, 'base64');
|
|
196
|
+
const actual = createHash('sha512').update(tarball).digest();
|
|
197
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
|
|
198
|
+
throw new Error(`integrity check failed: tarball hash sha512-${actual.toString('base64')} ` +
|
|
199
|
+
`does not match expected ${integrity}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Download the published tarball at `tarballUrl` and prove its bytes match
|
|
204
|
+
* `integrity` before returning a path to it on disk. The returned .tgz is safe
|
|
205
|
+
* to hand to `npm install`/`bun add` — it has been verified byte-for-byte
|
|
206
|
+
* against the registry attestation. Fails closed: a non-200, a download error,
|
|
207
|
+
* or a hash mismatch throws and no file path is returned, so the caller never
|
|
208
|
+
* installs an unverified artifact.
|
|
209
|
+
*/
|
|
210
|
+
export async function downloadVerifiedTarball(tarballUrl, integrity, timeoutMs = 60_000) {
|
|
211
|
+
const response = await fetch(tarballUrl, { signal: AbortSignal.timeout(timeoutMs) });
|
|
212
|
+
if (!response.ok) {
|
|
213
|
+
throw new Error(`could not download tarball from ${tarballUrl} (HTTP ${response.status})`);
|
|
214
|
+
}
|
|
215
|
+
const tarball = Buffer.from(await response.arrayBuffer());
|
|
216
|
+
verifyTarballIntegrity(tarball, integrity);
|
|
217
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-cli-upgrade-'));
|
|
218
|
+
const file = path.join(dir, path.basename(new URL(tarballUrl).pathname) || 'package.tgz');
|
|
219
|
+
fs.writeFileSync(file, tarball);
|
|
220
|
+
return file;
|
|
168
221
|
}
|
|
169
222
|
/** Read the version field of the package.json at `packageRoot`, fresh from disk. */
|
|
170
223
|
export function readInstalledVersion(packageRoot) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticated `agents serve --control` server — the "anchor" the iOS/iPadOS
|
|
3
|
+
* cockpit talks to (RUSH-1731).
|
|
4
|
+
*
|
|
5
|
+
* It is the read-only {@link handleServeGet} surface plus a small mutation API,
|
|
6
|
+
* gated by a bearer token ({@link verifyControlToken}) on EVERY request because,
|
|
7
|
+
* unlike plain `agents serve`, this variant may bind beyond loopback (a tailnet
|
|
8
|
+
* address) so the phone can reach it. It adds no new execution machinery: a run
|
|
9
|
+
* request re-invokes the same `agents run` the CLI uses (inheriting host
|
|
10
|
+
* offload, secrets, detached dispatch), and a message re-invokes `agents
|
|
11
|
+
* message`. The server generates the run's session id so the run is immediately
|
|
12
|
+
* addressable for streaming (Phase 2) and steering.
|
|
13
|
+
*
|
|
14
|
+
* Both side-effecting operations are behind DI seams ({@link ControlOptions})
|
|
15
|
+
* so tests drive them without spawning real agents — mirroring the `snapshot`
|
|
16
|
+
* seam on the read-only server.
|
|
17
|
+
*/
|
|
18
|
+
import type http from 'http';
|
|
19
|
+
import type { StdioOptions } from 'child_process';
|
|
20
|
+
import { type ServeOptions } from './server.js';
|
|
21
|
+
/** A request to start an agent run on this anchor (local or offloaded). */
|
|
22
|
+
export interface RunRequest {
|
|
23
|
+
/** Agent to run: 'claude' | 'codex' | 'gemini' | … */
|
|
24
|
+
agent: string;
|
|
25
|
+
/** Headless prompt. */
|
|
26
|
+
prompt: string;
|
|
27
|
+
/** Permission mode: plan | edit | auto | skip. Defaults to the CLI default. */
|
|
28
|
+
mode?: string;
|
|
29
|
+
/** Offload onto a registered device / host (the executor). Omit → run on the anchor. */
|
|
30
|
+
host?: string;
|
|
31
|
+
/** Working directory (on the host when `host` is set). */
|
|
32
|
+
cwd?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Identifiers by which the started run can later be streamed / messaged. */
|
|
35
|
+
export interface RunResult {
|
|
36
|
+
/** Addressable session id (a UUID for claude; otherwise equals `name`). */
|
|
37
|
+
sessionId: string;
|
|
38
|
+
/** Durable run name seeded into the session label. */
|
|
39
|
+
name: string;
|
|
40
|
+
}
|
|
41
|
+
/** Starts a run and returns its addressable ids. */
|
|
42
|
+
export type RunDispatcher = (req: RunRequest) => Promise<RunResult>;
|
|
43
|
+
/** Delivers a message to a running/parked agent by id. */
|
|
44
|
+
export type Messenger = (id: string, text: string, from?: string) => Promise<void>;
|
|
45
|
+
export interface ControlOptions extends ServeOptions {
|
|
46
|
+
/** Bearer verifier. Defaults to {@link verifyControlToken}. */
|
|
47
|
+
verifyToken?: (presented: string | undefined) => boolean;
|
|
48
|
+
/** Run starter. Defaults to spawning `agents run …` detached. */
|
|
49
|
+
runner?: RunDispatcher;
|
|
50
|
+
/** Message sender. Defaults to spawning `agents message …`. */
|
|
51
|
+
messenger?: Messenger;
|
|
52
|
+
/** Poll cadence (ms) for the session event stream. Defaults to 300. */
|
|
53
|
+
streamPollMs?: number;
|
|
54
|
+
/** Resolve a session id to its NDJSON capture file. Defaults to {@link streamLogPath}. */
|
|
55
|
+
streamLogPathFor?: (sessionId: string) => string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* SSE stream of a session's normalized NDJSON events, offset-tailed from its
|
|
59
|
+
* capture file. Resumes from `?offset=<bytes>` or the `Last-Event-ID` header
|
|
60
|
+
* (each event's `id:` is its exact byte offset, so resume neither loses nor
|
|
61
|
+
* duplicates). Closes when a terminal (`result`/`error`) event is seen or the
|
|
62
|
+
* client disconnects. A not-yet-created file simply yields nothing until the
|
|
63
|
+
* run starts writing — the phone can open the stream the instant it dispatches.
|
|
64
|
+
*/
|
|
65
|
+
export declare function startSessionStream(file: string, req: http.IncomingMessage, res: http.ServerResponse, pollMs: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Spawn a detached child and settle once it has either successfully spawned or
|
|
68
|
+
* failed to. Attaching an `'error'` listener is mandatory: a `ChildProcess` is
|
|
69
|
+
* an `EventEmitter`, so an `'error'` event (e.g. ENOENT from a stale/missing
|
|
70
|
+
* `agents` binary — a real condition `validateDaemonBinary` guards against)
|
|
71
|
+
* with no listener throws and would take down the whole anchor process, not
|
|
72
|
+
* just the one request. Mirrors the `once('spawn') / once('error')` pattern in
|
|
73
|
+
* `teams/agents.ts`. The `'error'` listener registered by `once` survives after
|
|
74
|
+
* a successful spawn, so a later error is absorbed rather than crashing.
|
|
75
|
+
*/
|
|
76
|
+
export declare function spawnDetached(command: string, args: string[], stdio?: StdioOptions): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Default run dispatcher: re-invoke `agents run` with a server-minted id so the
|
|
79
|
+
* run is addressable, detached so it outlives this request (and the anchor
|
|
80
|
+
* process). Local runs detach here; `--host` runs detach on the executor via
|
|
81
|
+
* the existing dispatch path.
|
|
82
|
+
*/
|
|
83
|
+
export declare const defaultRunner: RunDispatcher;
|
|
84
|
+
/** Default messenger: re-invoke `agents message <id> <text>` and await its exit. */
|
|
85
|
+
export declare const defaultMessenger: Messenger;
|
|
86
|
+
/**
|
|
87
|
+
* Create (but do not start) the authenticated control server. Caller invokes
|
|
88
|
+
* `.listen(port, bind)`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function createControlServer(opts?: ControlOptions): http.Server;
|
|
91
|
+
/** Start the control server on `port`, bound to `bind`. Resolves the bound port. */
|
|
92
|
+
export declare function startControlServer(port: number, bind: string, opts?: ControlOptions): Promise<{
|
|
93
|
+
server: http.Server;
|
|
94
|
+
port: number;
|
|
95
|
+
}>;
|