@phnx-labs/agents-cli 1.20.34 → 1.20.36
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 +113 -0
- package/LICENSE +185 -21
- package/README.md +13 -5
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.js +81 -0
- package/dist/commands/cost.js +17 -12
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +54 -97
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +283 -12
- package/dist/commands/hosts.js +37 -29
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +62 -16
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +196 -91
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +221 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +143 -1
- package/dist/commands/sessions.js +628 -80
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +82 -32
- package/dist/lib/agents.js +30 -9
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +77 -16
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +37 -12
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +183 -23
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +92 -0
- package/dist/lib/daemon.js +222 -18
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +10 -3
- package/dist/lib/events.js +101 -24
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +100 -12
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +135 -10
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +45 -13
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +74 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +51 -7
- package/dist/lib/hosts/ready.js +98 -25
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +22 -0
- package/dist/lib/hosts/tasks.js +29 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +26 -0
- package/dist/lib/picker.js +127 -0
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +61 -10
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +61 -0
- package/dist/lib/session/active.js +196 -17
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +204 -27
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +286 -2
- package/dist/lib/session/pid-registry.d.ts +22 -0
- package/dist/lib/session/pid-registry.js +104 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +153 -0
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +18 -8
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +1 -7
- package/dist/lib/session/sync/config.js +4 -11
- package/dist/lib/session/types.d.ts +18 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +44 -6
- package/dist/lib/shims.js +143 -57
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +47 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +62 -63
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +8 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +19 -0
- package/dist/lib/terminal/backends/index.js +31 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +17 -0
- package/dist/lib/terminal/index.js +8 -0
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +30 -3
- package/dist/lib/versions.js +129 -18
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/commands/secrets.js
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Option } from 'commander';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
+
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
10
11
|
import * as fs from 'fs';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
12
|
+
import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
|
|
13
|
+
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
14
|
+
import { ensureDaemonStarted } from '../lib/daemon.js';
|
|
13
15
|
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, resolveSshTarget, } from '../lib/secrets/remote.js';
|
|
14
|
-
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
16
|
+
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
15
17
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
16
18
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
17
19
|
import { DEFAULT_TTL_MS, agentLoad, agentLock, agentStatus, ensureAgentRunning, installSecretsAgentService, runAgentLoadFromStdin, runSecretsAgent, secretsAgentServiceInstalled, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
|
|
@@ -139,6 +141,31 @@ function readStdinSync() {
|
|
|
139
141
|
// SSH target validation is defined canonically in src/lib/ssh-exec.ts and
|
|
140
142
|
// re-exported here for back-compat with existing importers of these symbols.
|
|
141
143
|
export { SSH_TARGET_RE, assertValidSshTarget };
|
|
144
|
+
/**
|
|
145
|
+
* Build the child environment for `agents secrets exec`. Strips
|
|
146
|
+
* loader/interpreter hijack vars (matching agent spawns in exec.ts) and never
|
|
147
|
+
* forwards AGENTS_SECRETS_PASSPHRASE — the master decryption key must not reach
|
|
148
|
+
* the executed command.
|
|
149
|
+
*/
|
|
150
|
+
export function buildSecretsExecEnv(parentEnv, secretEnv) {
|
|
151
|
+
const env = { ...sanitizeProcessEnv(parentEnv), ...secretEnv };
|
|
152
|
+
delete env.AGENTS_SECRETS_PASSPHRASE;
|
|
153
|
+
return env;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve the CLI version for the MCP server's `serverInfo.version`. package.json
|
|
157
|
+
* sits at the repo root — two levels up from both `src/commands/` (bun/tsx dev)
|
|
158
|
+
* and `dist/commands/` (built). Cosmetic only, so any failure falls back cleanly.
|
|
159
|
+
*/
|
|
160
|
+
function getCliVersion() {
|
|
161
|
+
try {
|
|
162
|
+
const pkgPath = new URL('../../package.json', import.meta.url);
|
|
163
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version ?? '0.0.0';
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return '0.0.0';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
142
169
|
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
143
170
|
function shellQuote(s) {
|
|
144
171
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -264,14 +291,19 @@ function compactRemaining(expiresAt) {
|
|
|
264
291
|
/** The POLICY column for `secrets list`: the prompt policy, plus a "Nh left"
|
|
265
292
|
* hint when a `daily` bundle is currently held by the secrets-agent. `held`
|
|
266
293
|
* maps bundle name → expiry epoch-ms (from agentStatus()). */
|
|
267
|
-
function renderPolicyCol(b, held) {
|
|
294
|
+
export function renderPolicyCol(b, held) {
|
|
295
|
+
// `never` is loud on purpose — it's the only tier with no user-presence gate.
|
|
296
|
+
if (bundlePolicy(b) === 'never')
|
|
297
|
+
return chalk.red.bold('never · NO ACL');
|
|
268
298
|
if (bundlePolicy(b) === 'always')
|
|
269
299
|
return chalk.yellow('always ask');
|
|
270
300
|
const exp = held?.get(b.name);
|
|
271
301
|
return exp ? chalk.green(`daily · ${compactRemaining(exp)} left`) : chalk.gray('daily');
|
|
272
302
|
}
|
|
303
|
+
/** Below this width the fixed date columns no longer fit; `list` uses cards. */
|
|
304
|
+
const SECRETS_WIDE = 96;
|
|
273
305
|
/** Format a single bundle as a table row for the `secrets list` output. */
|
|
274
|
-
function renderBundleRow(b, held) {
|
|
306
|
+
function renderBundleRow(b, held, cols = terminalWidth()) {
|
|
275
307
|
const entries = describeBundle(b);
|
|
276
308
|
const keys = entries.length;
|
|
277
309
|
const expiringCount = countExpiringSoon(b.meta);
|
|
@@ -296,10 +328,26 @@ function renderBundleRow(b, held) {
|
|
|
296
328
|
`${padVisible(used, 7)}`;
|
|
297
329
|
// Mark file-backed bundles so `list` distinguishes them from keychain ones.
|
|
298
330
|
const tag = b.backend === 'file' ? chalk.magenta('[file] ') : '';
|
|
299
|
-
|
|
331
|
+
// Cap the free-form description to whatever space is left on the line so a long
|
|
332
|
+
// description can't push the row to 200+ chars and wrap into a smear.
|
|
333
|
+
const budget = cols - stringWidth(head) - 1 - stringWidth(tag);
|
|
334
|
+
const desc = b.description && budget > 3
|
|
335
|
+
? chalk.gray(truncateToWidth(safePrint(b.description), budget))
|
|
336
|
+
: '';
|
|
300
337
|
const trailer = `${tag}${desc}`.trimEnd();
|
|
301
338
|
return trailer ? `${head} ${trailer}` : head.trimEnd();
|
|
302
339
|
}
|
|
340
|
+
/** Narrow-terminal card: name + compact meta on one line, description below. */
|
|
341
|
+
function renderBundleCard(b, held, cols) {
|
|
342
|
+
const keys = describeBundle(b).length;
|
|
343
|
+
const used = b.last_used ? relativeAge(b.last_used) : (b.created_at ? 'never' : '?');
|
|
344
|
+
const tag = b.backend === 'file' ? chalk.magenta(' [file]') : '';
|
|
345
|
+
const meta = chalk.gray(`${keys} key${keys === 1 ? '' : 's'} · `) + renderPolicyCol(b, held) + chalk.gray(` · used ${used}`);
|
|
346
|
+
const line1 = `${chalk.cyan(b.name)} ${meta}${tag}`;
|
|
347
|
+
if (!b.description)
|
|
348
|
+
return line1;
|
|
349
|
+
return `${line1}\n ${chalk.gray(truncateToWidth(safePrint(b.description), cols - 4))}`;
|
|
350
|
+
}
|
|
303
351
|
/** Colorize a variable source kind (literal, keychain, env, file, exec). */
|
|
304
352
|
function kindLabel(kind) {
|
|
305
353
|
switch (kind) {
|
|
@@ -487,7 +535,7 @@ export function registerSecretsCommands(program) {
|
|
|
487
535
|
{ title: 'Agent commands', names: ['start', 'stop', 'unlock', 'lock', 'status', 'policy'] },
|
|
488
536
|
{ title: 'Raw item commands', names: ['get', 'set'] },
|
|
489
537
|
{ title: 'Sync commands', names: ['push', 'pull', 'remote-list'] },
|
|
490
|
-
{ title: 'Utilities', names: ['exec', 'generate', 'migrate-acl'] },
|
|
538
|
+
{ title: 'Utilities', names: ['exec', 'mcp', 'generate', 'migrate-acl'] },
|
|
491
539
|
]);
|
|
492
540
|
cmd
|
|
493
541
|
.command('list')
|
|
@@ -519,9 +567,17 @@ export function registerSecretsCommands(program) {
|
|
|
519
567
|
/* broker not running — render policy without the countdown */
|
|
520
568
|
}
|
|
521
569
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
console.log(
|
|
570
|
+
const cols = terminalWidth();
|
|
571
|
+
if (cols >= SECRETS_WIDE) {
|
|
572
|
+
console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'POLICY'.padEnd(18)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
|
|
573
|
+
for (const b of bundles) {
|
|
574
|
+
console.log(renderBundleRow(b, held, cols));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
for (const b of bundles) {
|
|
579
|
+
console.log(renderBundleCard(b, held, cols));
|
|
580
|
+
}
|
|
525
581
|
}
|
|
526
582
|
});
|
|
527
583
|
cmd
|
|
@@ -562,9 +618,14 @@ export function registerSecretsCommands(program) {
|
|
|
562
618
|
console.log(chalk.yellow('allow_exec: true'));
|
|
563
619
|
if (bundle.backend === 'file')
|
|
564
620
|
console.log(chalk.gray('backend: file (passphrase-encrypted; reads need AGENTS_SECRETS_PASSPHRASE, no Touch ID)'));
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
621
|
+
if (bundlePolicy(bundle) === 'never') {
|
|
622
|
+
console.log(chalk.red.bold('policy: never — NO biometry ACL; reads are silent (no Touch ID, no user-presence check). Automation-only.'));
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
console.log(bundlePolicy(bundle) === 'daily'
|
|
626
|
+
? chalk.gray('policy: daily (ask once, then held ~24h until screen-lock / sleep / logout)')
|
|
627
|
+
: chalk.gray('policy: always (asks for Touch ID every time — never auto-held)'));
|
|
628
|
+
}
|
|
568
629
|
if (bundle.created_at)
|
|
569
630
|
console.log(chalk.gray(`created_at: ${bundle.created_at} (${humanAge(bundle.created_at)})`));
|
|
570
631
|
if (bundle.updated_at)
|
|
@@ -686,8 +747,9 @@ export function registerSecretsCommands(program) {
|
|
|
686
747
|
.description('Create an empty bundle')
|
|
687
748
|
.option('--description <text>', 'Free-form description')
|
|
688
749
|
.option('--allow-exec', 'Allow exec: refs in this bundle (off by default)')
|
|
689
|
-
.option('--policy <policy>', 'prompt policy: daily (default, ask once a day)
|
|
750
|
+
.option('--policy <policy>', 'prompt policy: daily (default, ask once a day), always (ask every time), or never (silent, NO biometry ACL — needs --i-understand)')
|
|
690
751
|
.addOption(new Option('--tier <policy>', 'deprecated alias for --policy').hideHelp())
|
|
752
|
+
.option('--i-understand', 'Confirm creating a "never"-policy bundle (no biometry ACL) without an interactive prompt')
|
|
691
753
|
.option('--backend <backend>', 'storage backend: keychain (default) or file (passphrase-encrypted, headless-readable)', 'keychain')
|
|
692
754
|
.option('--force', 'Overwrite an existing bundle')
|
|
693
755
|
.action(async (name, opts) => {
|
|
@@ -703,6 +765,13 @@ export function registerSecretsCommands(program) {
|
|
|
703
765
|
console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
|
|
704
766
|
process.exit(1);
|
|
705
767
|
}
|
|
768
|
+
// The `never` tier is the least-safe option — gate it loudly. Throws in a
|
|
769
|
+
// headless shell without --i-understand; prompts otherwise.
|
|
770
|
+
const ack = assertNeverPolicyAcknowledged(policy, { iUnderstand: opts.iUnderstand, interactive: isInteractiveTerminal() });
|
|
771
|
+
if (ack === 'prompt' && !(await confirmNeverPolicyInteractive(resolvedName))) {
|
|
772
|
+
console.error(chalk.yellow('Aborted.'));
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
706
775
|
const bundle = {
|
|
707
776
|
name: resolvedName,
|
|
708
777
|
description: opts.description,
|
|
@@ -712,11 +781,16 @@ export function registerSecretsCommands(program) {
|
|
|
712
781
|
vars: {},
|
|
713
782
|
};
|
|
714
783
|
writeBundle(bundle);
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
784
|
+
const policyTag = bundlePolicy(bundle) === 'daily'
|
|
785
|
+
? 'policy: daily'
|
|
786
|
+
: bundlePolicy(bundle) === 'always'
|
|
787
|
+
? 'policy: always ask'
|
|
788
|
+
: 'policy: never (NO biometry ACL)';
|
|
789
|
+
const tags = [policyTag, backend === 'file' ? 'backend: file' : null].filter(Boolean);
|
|
719
790
|
console.log(chalk.green(`Bundle '${resolvedName}' created (${tags.join(', ')}).`));
|
|
791
|
+
if (bundlePolicy(bundle) === 'never') {
|
|
792
|
+
console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only; rotate anything sensitive out of it.'));
|
|
793
|
+
}
|
|
720
794
|
if (backend === 'file') {
|
|
721
795
|
console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted under AGENTS_SECRETS_PASSPHRASE (no Touch ID).'));
|
|
722
796
|
}
|
|
@@ -850,7 +924,7 @@ export function registerSecretsCommands(program) {
|
|
|
850
924
|
secretValue = await promptForSecret(`Enter value for ${resolvedBundleName}.${resolvedKey}`);
|
|
851
925
|
}
|
|
852
926
|
const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
|
|
853
|
-
bundleItemStore(bundle.backend).set(item, secretValue);
|
|
927
|
+
bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' }).set(item, secretValue);
|
|
854
928
|
bundle.vars[resolvedKey] = keychainRef(resolvedKey);
|
|
855
929
|
applyMeta();
|
|
856
930
|
writeBundle(bundle);
|
|
@@ -1096,7 +1170,7 @@ Examples:
|
|
|
1096
1170
|
vars: {},
|
|
1097
1171
|
};
|
|
1098
1172
|
}
|
|
1099
|
-
const store = bundleItemStore(bundle.backend);
|
|
1173
|
+
const store = bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' });
|
|
1100
1174
|
let added = 0;
|
|
1101
1175
|
let skipped = 0;
|
|
1102
1176
|
if (opts.from1password) {
|
|
@@ -1218,20 +1292,18 @@ Examples:
|
|
|
1218
1292
|
const remoteCmd = `bash -lc ${shellQuote(remoteAgents)}`;
|
|
1219
1293
|
let failures = 0;
|
|
1220
1294
|
for (const host of hosts) {
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
});
|
|
1226
|
-
if (res.error) {
|
|
1295
|
+
// Routed through the shared ssh engine: full hardened options
|
|
1296
|
+
// (BatchMode, ConnectTimeout, keepalive) + control-socket reuse.
|
|
1297
|
+
const res = sshExec(host, remoteCmd, { input });
|
|
1298
|
+
if (res.code === null) {
|
|
1227
1299
|
failures++;
|
|
1228
|
-
console.error(chalk.red(`${host}: ${res.
|
|
1300
|
+
console.error(chalk.red(`${host}: ${res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed')}`));
|
|
1229
1301
|
continue;
|
|
1230
1302
|
}
|
|
1231
|
-
if (res.
|
|
1303
|
+
if (res.code !== 0) {
|
|
1232
1304
|
failures++;
|
|
1233
1305
|
const msg = (res.stderr || res.stdout || '').trim();
|
|
1234
|
-
console.error(chalk.red(`${host}: remote import failed (exit ${res.
|
|
1306
|
+
console.error(chalk.red(`${host}: remote import failed (exit ${res.code})${msg ? `: ${msg}` : ''}`));
|
|
1235
1307
|
continue;
|
|
1236
1308
|
}
|
|
1237
1309
|
const remoteMsg = (res.stdout || '').trim().split('\n').map((l) => l.trim()).filter(Boolean).pop();
|
|
@@ -1308,6 +1380,8 @@ Examples:
|
|
|
1308
1380
|
.command('exec <bundle> [command...]')
|
|
1309
1381
|
.description('Run a command with the bundle\'s secrets injected into the environment (use --host to resolve the bundle from a remote machine, ephemerally)')
|
|
1310
1382
|
.option('--host <target>', 'Resolve <bundle> on a remote host over SSH and inject it (ephemeral — never stored on this machine)')
|
|
1383
|
+
.option('--keys <keys>', 'Inject only this comma-separated subset of keys (e.g. KEY1,KEY2). Missing keys are an error.')
|
|
1384
|
+
.option('--allow-expired', 'Inject keys even if their expiry date has passed (overrides the pre-run expiry abort).')
|
|
1311
1385
|
.allowUnknownOption()
|
|
1312
1386
|
.action(async (bundleName, commandParts, execOpts) => {
|
|
1313
1387
|
try {
|
|
@@ -1316,28 +1390,42 @@ Examples:
|
|
|
1316
1390
|
process.exit(1);
|
|
1317
1391
|
}
|
|
1318
1392
|
const [cmd, ...args] = commandParts;
|
|
1393
|
+
const keysSubset = execOpts.keys
|
|
1394
|
+
? execOpts.keys.split(',').map((k) => k.trim()).filter(Boolean)
|
|
1395
|
+
: undefined;
|
|
1319
1396
|
let secretEnv;
|
|
1320
1397
|
if (execOpts.host) {
|
|
1398
|
+
// Least-privilege flags do not yet cross the SSH resolver —
|
|
1399
|
+
// silently applying them would inject the full remote env or an
|
|
1400
|
+
// expired key. Fail loud so the user can drop the flag or run
|
|
1401
|
+
// locally instead.
|
|
1402
|
+
const { assertRemoteBundleFlagsUnsupported } = await import('../lib/secrets/bundles.js');
|
|
1403
|
+
assertRemoteBundleFlagsUnsupported(bundleName, execOpts.host, { keys: keysSubset, allowExpired: execOpts.allowExpired }, { keysFlag: '--keys', allowExpiredFlag: '--allow-expired' });
|
|
1321
1404
|
secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName);
|
|
1322
1405
|
}
|
|
1323
1406
|
else {
|
|
1324
1407
|
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
1325
|
-
secretEnv = readAndResolveBundleEnv(bundleName, {
|
|
1408
|
+
secretEnv = readAndResolveBundleEnv(bundleName, {
|
|
1409
|
+
caller: `command ${cmd}`,
|
|
1410
|
+
keys: keysSubset,
|
|
1411
|
+
allowExpired: execOpts.allowExpired,
|
|
1412
|
+
}).env;
|
|
1326
1413
|
}
|
|
1327
1414
|
const { spawn } = await import('child_process');
|
|
1328
1415
|
// On Windows, spawn without a shell ENOENTs for `.cmd`/`.bat` launchers
|
|
1329
1416
|
// (npm, yarn, most JS CLIs) and shell built-ins, so we set shell:true.
|
|
1330
1417
|
// With shell:true Node hands cmd.exe a single command line with NO quoting
|
|
1331
|
-
// of its own
|
|
1332
|
-
//
|
|
1418
|
+
// of its own. Compose that line ourselves (composeWin32CommandLine quotes
|
|
1419
|
+
// every token) and pass an EMPTY args array so Node never concatenates the
|
|
1420
|
+
// user-supplied args unescaped (DEP0190 + injection). See that helper for
|
|
1333
1421
|
// the cmd.exe %VAR%/!VAR! expansion caveat.
|
|
1334
1422
|
const useShell = process.platform === 'win32';
|
|
1335
|
-
const spawnCmd = useShell ?
|
|
1336
|
-
const spawnArgs = useShell ?
|
|
1423
|
+
const spawnCmd = useShell ? composeWin32CommandLine(cmd, args) : cmd;
|
|
1424
|
+
const spawnArgs = useShell ? [] : args;
|
|
1337
1425
|
const proc = spawn(spawnCmd, spawnArgs, {
|
|
1338
1426
|
stdio: 'inherit',
|
|
1339
1427
|
shell: useShell,
|
|
1340
|
-
env:
|
|
1428
|
+
env: buildSecretsExecEnv(process.env, secretEnv),
|
|
1341
1429
|
});
|
|
1342
1430
|
proc.on('close', (code) => process.exit(code ?? 0));
|
|
1343
1431
|
proc.on('error', (err) => {
|
|
@@ -1352,6 +1440,17 @@ Examples:
|
|
|
1352
1440
|
process.exit(1);
|
|
1353
1441
|
}
|
|
1354
1442
|
});
|
|
1443
|
+
cmd
|
|
1444
|
+
.command('mcp')
|
|
1445
|
+
.description('Run a stdio MCP server exposing get_secret(bundle, key) — hand credentials to an MCP-speaking agent by name at call time, never through the child process environment')
|
|
1446
|
+
.action(async () => {
|
|
1447
|
+
// JIT credential delivery (#333): unlike `secrets exec`, which bakes every
|
|
1448
|
+
// resolved value into the child's env, this speaks MCP over stdio so a
|
|
1449
|
+
// framework requests one secret at a time and the raw value never enters
|
|
1450
|
+
// process.env. stdout is the JSON-RPC channel — nothing else may print there.
|
|
1451
|
+
const { runSecretsMcpServer } = await import('../lib/secrets/mcp.js');
|
|
1452
|
+
await runSecretsMcpServer({ version: getCliVersion() });
|
|
1453
|
+
});
|
|
1355
1454
|
cmd
|
|
1356
1455
|
.command('generate [length]')
|
|
1357
1456
|
.description('Generate a random password')
|
|
@@ -1454,6 +1553,12 @@ Examples:
|
|
|
1454
1553
|
console.error(chalk.red('Could not start the secrets-agent.'));
|
|
1455
1554
|
process.exit(1);
|
|
1456
1555
|
}
|
|
1556
|
+
// #415: the daemon should be always-on for any background need, not only
|
|
1557
|
+
// after `routines add`. A user who only ever unlocks secrets still gets
|
|
1558
|
+
// the daemon installed + running here. `ensureAgentRunning` above only
|
|
1559
|
+
// brings up the standalone secrets broker, not the daemon. Idempotent
|
|
1560
|
+
// (single-instance start lock, #414) and best-effort — never blocks unlock.
|
|
1561
|
+
ensureDaemonStarted();
|
|
1457
1562
|
let loaded = 0;
|
|
1458
1563
|
for (const name of targets) {
|
|
1459
1564
|
try {
|
|
@@ -1523,8 +1628,9 @@ Examples:
|
|
|
1523
1628
|
cmd
|
|
1524
1629
|
.command('policy <bundle> [policy]')
|
|
1525
1630
|
.alias('tier')
|
|
1526
|
-
.description("Show or set a bundle's prompt policy: daily (default, ask once a day)
|
|
1527
|
-
.
|
|
1631
|
+
.description("Show or set a bundle's prompt policy: daily (default, ask once a day), always (ask every time), or never (silent, NO biometry ACL).")
|
|
1632
|
+
.option('--i-understand', 'Confirm switching to the "never" policy (no biometry ACL) without an interactive prompt')
|
|
1633
|
+
.action(async (bundleName, policyArg, opts) => {
|
|
1528
1634
|
try {
|
|
1529
1635
|
const bundle = readBundle(bundleName);
|
|
1530
1636
|
if (policyArg === undefined) {
|
|
@@ -1532,15 +1638,24 @@ Examples:
|
|
|
1532
1638
|
return;
|
|
1533
1639
|
}
|
|
1534
1640
|
const next = parsePolicyOpt(policyArg);
|
|
1641
|
+
// Switching to `never` drops the biometry ACL — gate it exactly like create.
|
|
1642
|
+
const ack = assertNeverPolicyAcknowledged(next, { iUnderstand: opts.iUnderstand, interactive: isInteractiveTerminal() });
|
|
1643
|
+
if (ack === 'prompt' && !(await confirmNeverPolicyInteractive(bundle.name))) {
|
|
1644
|
+
console.error(chalk.yellow('Aborted.'));
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1535
1647
|
bundle.policy = next;
|
|
1536
1648
|
writeBundle(bundle);
|
|
1537
1649
|
console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
|
|
1538
1650
|
if (next === 'daily') {
|
|
1539
1651
|
console.log(chalk.gray('Held by the secrets-agent for ~24h after one unlock (auto-cache is on by default; disable with `secrets.agent.auto: false` in agents.yaml).'));
|
|
1540
1652
|
}
|
|
1541
|
-
else {
|
|
1653
|
+
else if (next === 'always') {
|
|
1542
1654
|
console.log(chalk.gray('Asks for Touch ID every time — never auto-held.'));
|
|
1543
1655
|
}
|
|
1656
|
+
else {
|
|
1657
|
+
console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only.'));
|
|
1658
|
+
}
|
|
1544
1659
|
}
|
|
1545
1660
|
catch (err) {
|
|
1546
1661
|
console.error(chalk.red(err.message));
|
|
@@ -1589,23 +1704,52 @@ Examples:
|
|
|
1589
1704
|
registerSecretsSyncCommands(cmd);
|
|
1590
1705
|
registerSecretsMigrateAclCommand(cmd);
|
|
1591
1706
|
}
|
|
1592
|
-
/** Validate a prompt-policy value,
|
|
1593
|
-
* Accepts the legacy `biometry
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1597
|
-
function parsePolicyOpt(raw) {
|
|
1707
|
+
/** Validate a prompt-policy value, throwing a clear message on a bad one (the
|
|
1708
|
+
* caller's try/catch renders it and exits). Accepts the legacy `biometry` /
|
|
1709
|
+
* `session` / `none` tokens as aliases for `always` / `daily` / `never` so older
|
|
1710
|
+
* flags and scripts keep working. `never`/`none` is the no-biometry-ACL tier —
|
|
1711
|
+
* accepted here, then gated behind a loud confirmation in the command layer. */
|
|
1712
|
+
export function parsePolicyOpt(raw) {
|
|
1598
1713
|
const v = (raw ?? 'always').toLowerCase();
|
|
1599
1714
|
if (v === 'always' || v === 'biometry')
|
|
1600
1715
|
return 'always';
|
|
1601
1716
|
if (v === 'daily' || v === 'session')
|
|
1602
1717
|
return 'daily';
|
|
1603
|
-
if (v === 'never' || v === 'none')
|
|
1604
|
-
|
|
1605
|
-
|
|
1718
|
+
if (v === 'never' || v === 'none')
|
|
1719
|
+
return 'never';
|
|
1720
|
+
throw new Error(`Invalid policy '${raw}'. Use 'always', 'daily', or 'never'.`);
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Gate a create/switch to the `never` prompt-policy behind an explicit,
|
|
1724
|
+
* deliberate acknowledgement — it is the least-safe tier (no user-presence
|
|
1725
|
+
* check on any read). Returns:
|
|
1726
|
+
* - `'ok'` — not `never`, or already acknowledged via `--i-understand`.
|
|
1727
|
+
* - `'prompt'` — interactive shell: caller must run a loud confirm prompt.
|
|
1728
|
+
* Throws in a non-interactive shell when the flag is absent, so a headless
|
|
1729
|
+
* `create --policy never` can't silently downgrade a bundle's protection.
|
|
1730
|
+
*/
|
|
1731
|
+
export function assertNeverPolicyAcknowledged(policy, opts) {
|
|
1732
|
+
if (policy !== 'never')
|
|
1733
|
+
return 'ok';
|
|
1734
|
+
if (opts.iUnderstand)
|
|
1735
|
+
return 'ok';
|
|
1736
|
+
if (!opts.interactive) {
|
|
1737
|
+
throw new Error("Refusing to set the 'never' prompt-policy without confirmation. This tier stores " +
|
|
1738
|
+
'the bundle WITHOUT the biometry access control: every read is silent, with no Touch ID ' +
|
|
1739
|
+
'and no user-presence check — any code running as you can read it. Re-run with ' +
|
|
1740
|
+
'--i-understand to confirm you want an unprotected, automation-only bundle.');
|
|
1606
1741
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1742
|
+
return 'prompt';
|
|
1743
|
+
}
|
|
1744
|
+
/** Loud confirm prompt shown before creating/switching to `never` in a TTY.
|
|
1745
|
+
* Returns true to proceed. Kept separate from assertNeverPolicyAcknowledged so
|
|
1746
|
+
* the decision logic stays pure and unit-testable. */
|
|
1747
|
+
async function confirmNeverPolicyInteractive(bundleName) {
|
|
1748
|
+
console.error(chalk.red.bold('WARNING: policy "never" stores this bundle with NO biometry ACL.'));
|
|
1749
|
+
console.error(chalk.red(`Reads of '${bundleName}' will be fully silent — no Touch ID, no user-presence check.`));
|
|
1750
|
+
console.error(chalk.yellow('Use it only for low-sensitivity, automation-only credentials.'));
|
|
1751
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
1752
|
+
return confirm({ message: `Store '${bundleName}' WITHOUT biometry protection?`, default: false });
|
|
1609
1753
|
}
|
|
1610
1754
|
/** Validate a --backend value, exiting with a clear message on a bad one. */
|
|
1611
1755
|
function parseBackendOpt(raw) {
|
|
@@ -1629,48 +1773,9 @@ function humanRemaining(expiresAt) {
|
|
|
1629
1773
|
const days = Math.round(hours / 24);
|
|
1630
1774
|
return `locks in ${days} day${days === 1 ? '' : 's'}`;
|
|
1631
1775
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
* NO quoting of its own, so an unquoted arg with a space is split into several
|
|
1636
|
-
* args, and a cmd metacharacter (`&|<>()^`) would be interpreted by the shell.
|
|
1637
|
-
* We wrap any arg with whitespace, a quote, or a metacharacter in double quotes
|
|
1638
|
-
* and escape embedded quotes / trailing backslashes per the CommandLineToArgvW
|
|
1639
|
-
* rules, so the *child's* argv parse reconstructs the original argument.
|
|
1640
|
-
*
|
|
1641
|
-
* CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
|
|
1642
|
-
* BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
|
|
1643
|
-
* "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
|
|
1644
|
-
* `agents secrets exec` runs a caller-supplied command against a bundle the
|
|
1645
|
-
* caller owns, so caller-controlled `%`/`!` is not a privilege boundary. If that
|
|
1646
|
-
* ever changes (exec'ing an untrusted command line), route through a shell that
|
|
1647
|
-
* disables expansion rather than relying on this quoter. An empty arg becomes
|
|
1648
|
-
* `""`. Exported for tests. No-ops on non-Windows (the caller only invokes it
|
|
1649
|
-
* under `process.platform === 'win32'`).
|
|
1650
|
-
*/
|
|
1651
|
-
export function quoteWin32ExecArg(arg) {
|
|
1652
|
-
if (arg.length > 0 && !/[\s"&|<>()^]/.test(arg))
|
|
1653
|
-
return arg;
|
|
1654
|
-
let result = '"';
|
|
1655
|
-
let backslashes = 0;
|
|
1656
|
-
for (const ch of arg) {
|
|
1657
|
-
if (ch === '\\') {
|
|
1658
|
-
backslashes += 1;
|
|
1659
|
-
continue;
|
|
1660
|
-
}
|
|
1661
|
-
if (ch === '"') {
|
|
1662
|
-
// Double the run of backslashes, then escape this quote.
|
|
1663
|
-
result += '\\'.repeat(backslashes * 2 + 1) + '"';
|
|
1664
|
-
backslashes = 0;
|
|
1665
|
-
continue;
|
|
1666
|
-
}
|
|
1667
|
-
result += '\\'.repeat(backslashes) + ch;
|
|
1668
|
-
backslashes = 0;
|
|
1669
|
-
}
|
|
1670
|
-
// Trailing backslashes precede the closing quote → must be doubled.
|
|
1671
|
-
result += '\\'.repeat(backslashes * 2) + '"';
|
|
1672
|
-
return result;
|
|
1673
|
-
}
|
|
1776
|
+
// `quoteWin32ExecArg` now lives in lib/platform/exec.ts (shared with the agent
|
|
1777
|
+
// run/shim spawn paths); re-exported here for the colocated secrets tests.
|
|
1778
|
+
export { quoteWin32ExecArg };
|
|
1674
1779
|
/**
|
|
1675
1780
|
* Copy text to the system clipboard, cross-platform.
|
|
1676
1781
|
* macOS: `pbcopy`. Windows: `clip`. Linux: tries `wl-copy` (Wayland), then
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents serve` — read-only local web companion.
|
|
3
|
+
*
|
|
4
|
+
* Boots a localhost-only (127.0.0.1) HTTP + SSE server that renders a live
|
|
5
|
+
* dashboard of team status + per-worktree diffs, scheduled routines, and cloud
|
|
6
|
+
* tasks. Everything is a viewer over data other commands already own — there
|
|
7
|
+
* are no mutation endpoints. See src/lib/serve/.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from 'commander';
|
|
10
|
+
export declare function registerServeCommand(program: Command): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { startServeServer, DEFAULT_SERVE_PORT, SERVE_HOST } from '../lib/serve/server.js';
|
|
3
|
+
export function registerServeCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('serve')
|
|
6
|
+
.description('Read-only local web companion: team diffs, routines, and cloud status (binds 127.0.0.1 only).')
|
|
7
|
+
.option('-p, --port <n>', `Port to bind on ${SERVE_HOST}`, String(DEFAULT_SERVE_PORT))
|
|
8
|
+
.option('--interval <ms>', 'SSE refresh cadence in milliseconds', '3000')
|
|
9
|
+
.action(async (opts) => {
|
|
10
|
+
const port = parseInt(opts.port ?? '', 10) || DEFAULT_SERVE_PORT;
|
|
11
|
+
const intervalMs = parseInt(opts.interval ?? '', 10) || 3000;
|
|
12
|
+
try {
|
|
13
|
+
const { server, port: bound } = await startServeServer(port, {
|
|
14
|
+
cwd: process.cwd(),
|
|
15
|
+
intervalMs,
|
|
16
|
+
});
|
|
17
|
+
const url = `http://${SERVE_HOST}:${bound}`;
|
|
18
|
+
console.log(`${chalk.green('agents serve')} ${chalk.dim('→')} ${chalk.cyan(url)}`);
|
|
19
|
+
console.log(chalk.dim('read-only · localhost only · Ctrl-C to stop'));
|
|
20
|
+
const shutdown = () => {
|
|
21
|
+
server.close(() => process.exit(0));
|
|
22
|
+
};
|
|
23
|
+
process.on('SIGINT', shutdown);
|
|
24
|
+
process.on('SIGTERM', shutdown);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const msg = String(err?.message ?? err);
|
|
28
|
+
if (msg.includes('EADDRINUSE')) {
|
|
29
|
+
console.error(chalk.red(`Port ${port} is already in use. Pass --port <n> to pick another.`));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.error(chalk.red(`Could not start serve: ${msg}`));
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents sessions inject <sessionId> <text>` — deliver text into the terminal a
|
|
3
|
+
* running session lives in. The CLI face of the Terminal Engine's Gap 2 primitive
|
|
4
|
+
* (`injectIntoTerminal`, src/lib/terminal/inject.ts), so a native watchdog
|
|
5
|
+
* (RUSH-1415) can shell out to nudge a stalled agent with "continue".
|
|
6
|
+
*
|
|
7
|
+
* Resolution: find the active session by id, map its `provenance.reply` rail
|
|
8
|
+
* (provenance.ts:47) to an engine `InjectTarget` and inject. Today only tmux rails
|
|
9
|
+
* are externally addressable; `--pane`/`--pty` target a backend directly when the
|
|
10
|
+
* handle is already known (skipping the session lookup).
|
|
11
|
+
*/
|
|
12
|
+
import type { Command } from 'commander';
|
|
13
|
+
/** Attach the `inject` subcommand to an existing `sessions` command. */
|
|
14
|
+
export declare function registerSessionsInjectCommand(sessionsCmd: Command): void;
|