@phnx-labs/agents-cli 1.22.22 → 1.22.24
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 +402 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +20 -7
- package/dist/commands/exec.js +100 -34
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +108 -5
- package/dist/commands/focus.js +455 -32
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +39 -1
- package/dist/commands/go.js +112 -7
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/resume.js +10 -3
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +79 -47
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +23 -3
- package/dist/commands/sessions-resume.js +73 -19
- package/dist/commands/sessions.d.ts +91 -5
- package/dist/commands/sessions.js +359 -125
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.js +13 -2
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +175 -62
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +100 -40
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +44 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +50 -36
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.js +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +148 -53
- package/dist/lib/secrets/reaper.d.ts +97 -0
- package/dist/lib/secrets/reaper.js +219 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/active.d.ts +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +32 -1
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/parse.d.ts +10 -0
- package/dist/lib/session/parse.js +98 -0
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/session/remote-list.d.ts +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +62 -8
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/usage.d.ts +13 -0
- package/dist/lib/usage.js +215 -0
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
|
@@ -10,9 +10,10 @@ import chalk from 'chalk';
|
|
|
10
10
|
import { listRemoteBundles, MIN_PASSPHRASE_LEN, pullBundle, pushBundle, } from '../lib/secrets/sync.js';
|
|
11
11
|
import { bundleExists, listBundles } from '../lib/secrets/bundles.js';
|
|
12
12
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
13
|
+
import { missingSyncPassphraseMessage, resolveSyncPassphraseFromEnv, warnEnvPassphraseReadableOnce, } from '../lib/secrets/sync-passphrase.js';
|
|
13
14
|
async function promptPassphrase(message, confirm = false) {
|
|
14
15
|
if (!isInteractiveTerminal()) {
|
|
15
|
-
throw new Error(
|
|
16
|
+
throw new Error(missingSyncPassphraseMessage());
|
|
16
17
|
}
|
|
17
18
|
const { password } = await import('@inquirer/prompts');
|
|
18
19
|
const first = await password({ message, mask: true });
|
|
@@ -25,21 +26,18 @@ async function promptPassphrase(message, confirm = false) {
|
|
|
25
26
|
throw new Error('Passphrases do not match.');
|
|
26
27
|
return first;
|
|
27
28
|
}
|
|
28
|
-
// Print the env-var-source warning at most once per process so a `--all` push
|
|
29
|
-
// over many bundles doesn't flood stderr with the same notice.
|
|
30
|
-
let envPassphraseWarned = false;
|
|
31
29
|
function passphraseFromEnvOrPrompt(confirm) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// Env resolution (current name, then the deprecated master-key name) lives in
|
|
31
|
+
// lib/secrets/sync-passphrase.ts so this command and `agents sync` share one
|
|
32
|
+
// chokepoint — otherwise each call site warns separately and the "once per
|
|
33
|
+
// process" promise is a lie.
|
|
34
|
+
const { value } = resolveSyncPassphraseFromEnv();
|
|
35
|
+
if (value) {
|
|
36
|
+
if (value.length < MIN_PASSPHRASE_LEN) {
|
|
35
37
|
return Promise.reject(new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`));
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
process.stderr.write(chalk.yellow('warn: using AGENTS_SECRETS_PASSPHRASE. Env vars are readable by other same-user processes ' +
|
|
40
|
-
'(/proc, ps, crash dumps, CI logs) — rotate the passphrase after CI use.\n'));
|
|
41
|
-
}
|
|
42
|
-
return Promise.resolve(fromEnv);
|
|
39
|
+
warnEnvPassphraseReadableOnce();
|
|
40
|
+
return Promise.resolve(value);
|
|
43
41
|
}
|
|
44
42
|
return promptPassphrase('Sync passphrase', confirm);
|
|
45
43
|
}
|
|
@@ -48,9 +48,11 @@ export declare function parseImportSource(opts: {
|
|
|
48
48
|
* defaults). Shared with the command action so the wiring is unit-testable
|
|
49
49
|
* without a live SSH session.
|
|
50
50
|
*/
|
|
51
|
+
export declare function resolveUnlockTtlMs(ttl: string | undefined, until: string | undefined, now?: number): number;
|
|
51
52
|
export declare function buildRemoteUnlockArgs(names: string[], opts: {
|
|
52
53
|
all?: boolean;
|
|
53
54
|
ttl?: string;
|
|
55
|
+
until?: string;
|
|
54
56
|
durable?: boolean;
|
|
55
57
|
}): string[];
|
|
56
58
|
/**
|
package/dist/commands/secrets.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as path from 'path';
|
|
|
15
15
|
import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
|
|
16
16
|
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
17
17
|
import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
18
|
-
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, } from '../lib/secrets/remote.js';
|
|
18
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, buildRemoteFileImportCommand, } from '../lib/secrets/remote.js';
|
|
19
19
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
20
20
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
21
21
|
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
@@ -31,6 +31,7 @@ import { readMeta } from '../lib/state.js';
|
|
|
31
31
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
32
32
|
import { emit, query } from '../lib/events.js';
|
|
33
33
|
import { emitSecretAudit } from '../lib/secrets/audit.js';
|
|
34
|
+
import { SYNC_PASSPHRASE_ENV, resolveSyncPassphraseFromEnv } from '../lib/secrets/sync-passphrase.js';
|
|
34
35
|
import { getBundleUsage, getAllBundleUsage, getUsageHistory, SECRET_USAGE_EVENTS, } from '../lib/secrets/usage-db.js';
|
|
35
36
|
import { frequentlyPromptedBundles } from '../lib/secrets/unlock-hints.js';
|
|
36
37
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
@@ -266,11 +267,31 @@ function maybePrintSyncedHint(name, stillPresent) {
|
|
|
266
267
|
* defaults). Shared with the command action so the wiring is unit-testable
|
|
267
268
|
* without a live SSH session.
|
|
268
269
|
*/
|
|
270
|
+
export function resolveUnlockTtlMs(ttl, until, now = Date.now()) {
|
|
271
|
+
if (ttl && until)
|
|
272
|
+
throw new Error('--ttl and --until are mutually exclusive.');
|
|
273
|
+
if (until) {
|
|
274
|
+
const expiresAt = Date.parse(until);
|
|
275
|
+
if (!Number.isFinite(expiresAt))
|
|
276
|
+
throw new Error("Invalid --until '" + until + "'. Use an ISO date or timestamp.");
|
|
277
|
+
if (expiresAt <= now)
|
|
278
|
+
throw new Error("Invalid --until '" + until + "': date must be in the future.");
|
|
279
|
+
return expiresAt - now;
|
|
280
|
+
}
|
|
281
|
+
if (ttl) {
|
|
282
|
+
const secs = parseDuration(ttl);
|
|
283
|
+
if (!secs)
|
|
284
|
+
throw new Error("Invalid --ttl '" + ttl + "'. Use e.g. 30m, 2h, 8h, 3d.");
|
|
285
|
+
return secs * 1000;
|
|
286
|
+
}
|
|
287
|
+
return secretsHoldMs();
|
|
288
|
+
}
|
|
269
289
|
export function buildRemoteUnlockArgs(names, opts) {
|
|
270
290
|
return [
|
|
271
291
|
'unlock',
|
|
272
292
|
...(opts.all ? ['--all'] : names),
|
|
273
293
|
...(opts.ttl ? ['--ttl', opts.ttl] : []),
|
|
294
|
+
...(opts.until ? ['--until', opts.until] : []),
|
|
274
295
|
// Forward --durable so a remote unlock honors it too; without this the remote
|
|
275
296
|
// silently falls back to its own secrets.agent.durable default (off).
|
|
276
297
|
...(opts.durable ? ['--durable'] : []),
|
|
@@ -333,10 +354,6 @@ function getCliVersion() {
|
|
|
333
354
|
return '0.0.0';
|
|
334
355
|
}
|
|
335
356
|
}
|
|
336
|
-
/** POSIX single-quote a string for safe interpolation into a remote shell command. */
|
|
337
|
-
function shellQuote(s) {
|
|
338
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
339
|
-
}
|
|
340
357
|
/**
|
|
341
358
|
* Serialize a resolved env map to `.env` lines that round-trip losslessly through
|
|
342
359
|
* `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
|
|
@@ -1332,7 +1349,7 @@ export function registerSecretsCommands(program) {
|
|
|
1332
1349
|
if (bundle.allow_exec)
|
|
1333
1350
|
console.log(chalk.yellow('allow_exec: true'));
|
|
1334
1351
|
if (bundle.backend === 'file')
|
|
1335
|
-
console.log(chalk.gray('backend: file (
|
|
1352
|
+
console.log(chalk.gray('backend: file (encrypted at rest; headless reads via a machine-local key, or AGENTS_SECRETS_PASSPHRASE if set — no Touch ID)'));
|
|
1336
1353
|
if (bundle.backend === 'vault')
|
|
1337
1354
|
console.log(chalk.gray('storage: synced (age-encrypted ~/.agents/vault.age; needs agents login)'));
|
|
1338
1355
|
if (bundlePolicy(bundle) === 'never') {
|
|
@@ -1588,7 +1605,7 @@ export function registerSecretsCommands(program) {
|
|
|
1588
1605
|
console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only; rotate anything sensitive out of it.'));
|
|
1589
1606
|
}
|
|
1590
1607
|
if (backend === 'file') {
|
|
1591
|
-
console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted under AGENTS_SECRETS_PASSPHRASE
|
|
1608
|
+
console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted at rest under a machine-local key (or AGENTS_SECRETS_PASSPHRASE if set); headless reads, no Touch ID.'));
|
|
1592
1609
|
}
|
|
1593
1610
|
if (backend === 'vault') {
|
|
1594
1611
|
console.log(chalk.gray('Synced: items are encrypted in ~/.agents/vault.age. Copy that file with your sync tool of choice.'));
|
|
@@ -1961,7 +1978,7 @@ Examples:
|
|
|
1961
1978
|
.option('--synced', 'When creating the bundle, store it in the age-encrypted synced secrets file')
|
|
1962
1979
|
.option('--force', 'Overwrite an existing key in the bundle')
|
|
1963
1980
|
.option('--purge', 'With --from icloud: delete the iCloud copies after a successful import (iCloud propagates the deletion to your other devices)')
|
|
1964
|
-
.option('--from-file <path>',
|
|
1981
|
+
.option('--from-file <path>', `Import from an AES-256-GCM encrypted offline bundle file (needs ${SYNC_PASSPHRASE_ENV}; symmetric counterpart of export --to-file)`)
|
|
1965
1982
|
.option('--from-ssh', 'Pull the bundle from a fleet peer over SSH and import it locally (requires --host)')
|
|
1966
1983
|
.option('--host <peer>', 'SSH peer to pull from when using --from-ssh (host alias or user@host)')
|
|
1967
1984
|
.action(async (bundleName, opts) => {
|
|
@@ -1977,9 +1994,10 @@ Examples:
|
|
|
1977
1994
|
throw new Error('--from-file, --from-ssh, and --from/--from-1password are mutually exclusive; pick one import source.');
|
|
1978
1995
|
}
|
|
1979
1996
|
if (opts.fromFile) {
|
|
1980
|
-
|
|
1997
|
+
// Transport, not the local store's master key — see lib/secrets/sync-passphrase.ts.
|
|
1998
|
+
const passphrase = resolveSyncPassphraseFromEnv().value ?? '';
|
|
1981
1999
|
if (!passphrase) {
|
|
1982
|
-
throw new Error(
|
|
2000
|
+
throw new Error(`--from-file needs ${SYNC_PASSPHRASE_ENV} set to decrypt the bundle file.`);
|
|
1983
2001
|
}
|
|
1984
2002
|
const env = importBundleFromFile(opts.fromFile, passphrase);
|
|
1985
2003
|
const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
|
|
@@ -2064,10 +2082,10 @@ Examples:
|
|
|
2064
2082
|
.option('--vault <name>', '1Password vault name (used with --to-1password)')
|
|
2065
2083
|
.option('--host <target...>', 'Push the bundle over SSH to this target (host alias or user@host); repeatable for multiple machines')
|
|
2066
2084
|
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
2067
|
-
.option('--remote-backend <backend>', 'Backend for the bundle on the remote (with --host): keychain (default) or file
|
|
2085
|
+
.option('--remote-backend <backend>', 'Backend for the bundle on the remote (with --host): keychain (default) or file. file is headless-readable via the remote\'s machine-local key; it forwards AGENTS_SECRETS_PASSPHRASE over stdin only if set (opt-in).', 'keychain')
|
|
2068
2086
|
.option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --host)')
|
|
2069
2087
|
.option('--format <shell|json>', 'Output for --plaintext export: shell (default) or json (lossless, machine-readable; used by remote resolve)', 'shell')
|
|
2070
|
-
.option('--to-file <path>',
|
|
2088
|
+
.option('--to-file <path>', `Write the bundle as an AES-256-GCM encrypted offline file (needs ${SYNC_PASSPHRASE_ENV}; symmetric counterpart of import --from-file)`)
|
|
2071
2089
|
.action(async (bundleName, opts) => {
|
|
2072
2090
|
try {
|
|
2073
2091
|
// `--device` is an alias for `--host` (fleet vocabulary parity with
|
|
@@ -2078,9 +2096,10 @@ Examples:
|
|
|
2078
2096
|
const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
|
|
2079
2097
|
const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
|
|
2080
2098
|
if (opts.toFile) {
|
|
2081
|
-
|
|
2099
|
+
// Transport, not the local store's master key — see lib/secrets/sync-passphrase.ts.
|
|
2100
|
+
const passphrase = resolveSyncPassphraseFromEnv().value ?? '';
|
|
2082
2101
|
if (!passphrase) {
|
|
2083
|
-
throw new Error(
|
|
2102
|
+
throw new Error(`--to-file needs ${SYNC_PASSPHRASE_ENV} set to encrypt the bundle. ` +
|
|
2084
2103
|
'Set it for this command, then supply the same value when importing.');
|
|
2085
2104
|
}
|
|
2086
2105
|
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: 'export --to-file', keyMode: 'storage', agentOnly: true });
|
|
@@ -2097,20 +2116,16 @@ Examples:
|
|
|
2097
2116
|
for (const h of hosts)
|
|
2098
2117
|
assertValidSshTarget(h);
|
|
2099
2118
|
const remoteBackend = parseBackendOpt(opts.remoteBackend);
|
|
2100
|
-
// For a file-backed remote bundle
|
|
2101
|
-
//
|
|
2102
|
-
//
|
|
2103
|
-
//
|
|
2104
|
-
//
|
|
2105
|
-
//
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
throw new Error('--remote-backend file needs AGENTS_SECRETS_PASSPHRASE set locally to encrypt the ' +
|
|
2111
|
-
'bundle at rest on the remote. Set it for this command, then unlock it the same way per run.');
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2119
|
+
// For a file-backed remote bundle a passphrase is OPTIONAL. The file
|
|
2120
|
+
// store is passphrase-free by default: with AGENTS_SECRETS_PASSPHRASE
|
|
2121
|
+
// unset the remote `import --backend file` auto-provisions the remote's
|
|
2122
|
+
// own machine-local key (0600 under ~/.agents/.secrets-key/), so reads
|
|
2123
|
+
// are HEADLESS. We forward the LOCAL AGENTS_SECRETS_PASSPHRASE only when
|
|
2124
|
+
// the operator opts in by setting it (e.g. to key the bundle off-disk
|
|
2125
|
+
// under a shared secret) — shipped as the FIRST stdin line so it never
|
|
2126
|
+
// lands in argv / `ps` / the remote shell history. Forcing a shared
|
|
2127
|
+
// passphrase would defeat headless reads, so we no longer require one.
|
|
2128
|
+
const remotePassphrase = remoteBackend === 'file' ? (process.env.AGENTS_SECRETS_PASSPHRASE ?? '') : '';
|
|
2114
2129
|
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `ssh export`, keyMode: 'storage', agentOnly: true });
|
|
2115
2130
|
const dotenv = bundleEnvToDotenv(env);
|
|
2116
2131
|
const keyCount = Object.keys(env).length;
|
|
@@ -2126,19 +2141,22 @@ Examples:
|
|
|
2126
2141
|
for (const host of hosts) {
|
|
2127
2142
|
let res;
|
|
2128
2143
|
if (remoteBackend === 'file') {
|
|
2129
|
-
// File backend
|
|
2130
|
-
//
|
|
2131
|
-
//
|
|
2132
|
-
//
|
|
2133
|
-
//
|
|
2144
|
+
// File backend: headless-readable via the remote's machine-local key
|
|
2145
|
+
// when no passphrase is set; otherwise forwards AGENTS_SECRETS_PASSPHRASE
|
|
2146
|
+
// as the FIRST stdin line (consumed by `read`, so it never lands in
|
|
2147
|
+
// argv / `ps` / remote history), then the .env. Both build a POSIX
|
|
2148
|
+
// `bash -lc` command — refuse a Windows target cleanly rather than
|
|
2149
|
+
// emit broken PowerShell.
|
|
2134
2150
|
if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2135
2151
|
failures++;
|
|
2136
2152
|
console.error(chalk.red(`${host}: file backend export to a Windows target is not yet supported.`));
|
|
2137
2153
|
continue;
|
|
2138
2154
|
}
|
|
2139
|
-
const
|
|
2140
|
-
|
|
2141
|
-
|
|
2155
|
+
const { remoteCmd, input } = buildRemoteFileImportCommand(resolvedBundleName, dotenv, {
|
|
2156
|
+
passphrase: remotePassphrase,
|
|
2157
|
+
force: opts.force,
|
|
2158
|
+
});
|
|
2159
|
+
res = sshExec(host, remoteCmd, { input });
|
|
2142
2160
|
}
|
|
2143
2161
|
else if (remoteShellFor(resolveRemoteOsSync(host.split('@').pop() ?? host)) === 'powershell') {
|
|
2144
2162
|
// Keychain on a Windows target: the `agents.ps1` shim doesn't
|
|
@@ -2488,11 +2506,16 @@ Examples:
|
|
|
2488
2506
|
.command('unlock [names...]')
|
|
2489
2507
|
.description('Hold a bundle in the secrets-agent after one Touch ID, so concurrent runs read it without re-prompting (macOS). With --host, unlock FILE-backed bundle(s) on a remote (the passphrase prompt surfaces over the SSH TTY); keychain/biometry bundles are GUI-only and can\'t be remote-unlocked.')
|
|
2490
2508
|
.option('--ttl <duration>', 'How long to hold it (e.g. 30m, 8h, 3d). Default 7d.')
|
|
2509
|
+
.option('--until <date>', 'Hold until this absolute date or timestamp (for example 2026-08-06T12:00:00Z). Mutually exclusive with --ttl.')
|
|
2491
2510
|
.option('--durable', 'Keep the unlock across sleep + reboot too (default: survives upgrade/restart but re-locks on sleep). Set secrets.agent.durable in agents.yaml to make this the default.')
|
|
2492
2511
|
.option('--for <agent>', 'Narrow the unlock to ONE harness type (for example claude, codex, or kimi). Default: the grant is global — every harness and a plain shell can read it, so one Touch ID covers them all.')
|
|
2493
2512
|
.option('--all', 'Unlock every configured bundle')
|
|
2494
2513
|
.option('--host <target>', 'Unlock the bundle(s) on this remote machine over SSH instead of locally (file-backed bundles only — the remote\'s passphrase prompt surfaces on your terminal over a -tt session). Single-valued (NOT variadic) so it never swallows the bundle name: `unlock <name> --host <machine>`.')
|
|
2495
2514
|
.action(async (names, opts) => {
|
|
2515
|
+
if (opts.ttl && opts.until) {
|
|
2516
|
+
console.error(chalk.red('--ttl and --until are mutually exclusive.'));
|
|
2517
|
+
process.exit(1);
|
|
2518
|
+
}
|
|
2496
2519
|
// Single-valued (not variadic): a variadic --host greedily consumes the
|
|
2497
2520
|
// positional bundle name (`unlock --host mac wztest` -> host=[mac,wztest],
|
|
2498
2521
|
// names=[]). Unlock targets one remote at a time anyway.
|
|
@@ -2529,6 +2552,15 @@ Examples:
|
|
|
2529
2552
|
process.exit(1);
|
|
2530
2553
|
return;
|
|
2531
2554
|
}
|
|
2555
|
+
if (opts.until) {
|
|
2556
|
+
try {
|
|
2557
|
+
resolveUnlockTtlMs(undefined, opts.until);
|
|
2558
|
+
}
|
|
2559
|
+
catch (err) {
|
|
2560
|
+
console.error(chalk.red(err.message));
|
|
2561
|
+
process.exit(1);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2532
2564
|
if (process.platform !== 'darwin') {
|
|
2533
2565
|
// No broker + no biometry prompt off darwin: secrets already resolve
|
|
2534
2566
|
// durably from the OS store (libsecret / Credential Manager) on every
|
|
@@ -2542,15 +2574,15 @@ Examples:
|
|
|
2542
2574
|
console.error(chalk.red('Specify one or more bundle names, or --all.'));
|
|
2543
2575
|
process.exit(1);
|
|
2544
2576
|
}
|
|
2545
|
-
let ttlMs
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
ttlMs = secs * 1000;
|
|
2577
|
+
let ttlMs;
|
|
2578
|
+
try {
|
|
2579
|
+
ttlMs = resolveUnlockTtlMs(opts.ttl, opts.until);
|
|
2580
|
+
}
|
|
2581
|
+
catch (err) {
|
|
2582
|
+
console.error(chalk.red(err.message));
|
|
2583
|
+
process.exit(1);
|
|
2553
2584
|
}
|
|
2585
|
+
const expiresAt = Date.now() + ttlMs;
|
|
2554
2586
|
if (!(await ensureAgentRunning())) {
|
|
2555
2587
|
console.error(chalk.red('Could not start the secrets broker.'));
|
|
2556
2588
|
process.exit(1);
|
|
@@ -2576,7 +2608,7 @@ Examples:
|
|
|
2576
2608
|
noAgent: true,
|
|
2577
2609
|
caller: 'unlock secrets',
|
|
2578
2610
|
agent: harness,
|
|
2579
|
-
duration: humanRemaining(
|
|
2611
|
+
duration: humanRemaining(expiresAt),
|
|
2580
2612
|
keyMode: 'storage',
|
|
2581
2613
|
});
|
|
2582
2614
|
// Migrations are authorized only by this explicit unlock. The bundle
|
|
@@ -2592,7 +2624,7 @@ Examples:
|
|
|
2592
2624
|
saveSession(name, {
|
|
2593
2625
|
bundle,
|
|
2594
2626
|
env,
|
|
2595
|
-
expiresAt
|
|
2627
|
+
expiresAt,
|
|
2596
2628
|
sleepPersist: durable,
|
|
2597
2629
|
harness,
|
|
2598
2630
|
});
|
|
@@ -2611,7 +2643,7 @@ Examples:
|
|
|
2611
2643
|
agent: harness,
|
|
2612
2644
|
ttlMs,
|
|
2613
2645
|
});
|
|
2614
|
-
console.log(`${chalk.green('unlocked')} ${chalk.cyan(name)} ${chalk.gray(`(${Object.keys(env).length} keys, ${humanRemaining(
|
|
2646
|
+
console.log(`${chalk.green('unlocked')} ${chalk.cyan(name)} ${chalk.gray(`(${Object.keys(env).length} keys, ${humanRemaining(expiresAt)})`)}`);
|
|
2615
2647
|
}
|
|
2616
2648
|
else {
|
|
2617
2649
|
console.error(chalk.red(`Failed to load '${name}' into the agent.`));
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type SessionMeta } from '../lib/session/types.js';
|
|
13
13
|
import type { ActiveSession } from '../lib/session/active.js';
|
|
14
|
+
import { type LiveStatusFilter } from './sessions.js';
|
|
14
15
|
/**
|
|
15
16
|
* The single canonical filter state. Every field has a flag equivalent, so the
|
|
16
17
|
* same view is reachable interactively (hotkeys) or from the command line (flags).
|
|
@@ -32,6 +33,23 @@ export interface BrowserFilter {
|
|
|
32
33
|
projectScope: 'repo' | 'all';
|
|
33
34
|
/** time window (undefined = all time) — the `W` key / `--since`. */
|
|
34
35
|
window?: string;
|
|
36
|
+
/** individual live states; an OR-union and, like the CLI flags, running-only. */
|
|
37
|
+
statuses: LiveStatusFilter[];
|
|
38
|
+
/** named project scope, independent of the repo/all hotkey. */
|
|
39
|
+
project?: string;
|
|
40
|
+
/** upper time bound from --until. */
|
|
41
|
+
until?: string;
|
|
42
|
+
/** retain only routine-origin sessions. */
|
|
43
|
+
routine: boolean;
|
|
44
|
+
/** indexed resource filters shared by the flag and focus surfaces. */
|
|
45
|
+
skill?: string;
|
|
46
|
+
plugin?: string;
|
|
47
|
+
/** discovery cap. */
|
|
48
|
+
limit: number;
|
|
49
|
+
/** include unmanaged native-home transcripts. */
|
|
50
|
+
unmanaged: boolean;
|
|
51
|
+
/** candidate order shared with --sort. */
|
|
52
|
+
sort: 'timestamp' | 'cost' | 'duration';
|
|
35
53
|
}
|
|
36
54
|
/**
|
|
37
55
|
* Complete a seed into the filter the picker actually runs on.
|
|
@@ -97,6 +115,9 @@ export declare function bareBrowserSeed(opts: {
|
|
|
97
115
|
inTeam?: string;
|
|
98
116
|
favorites?: boolean;
|
|
99
117
|
}): Partial<BrowserFilter>;
|
|
118
|
+
/** Build the static peer query. Keeping it pure pins the per-device alias
|
|
119
|
+
* contract: latest/oldest travel unresolved and are resolved by each peer. */
|
|
120
|
+
export declare function remotePoolArgs(f: BrowserFilter, fixedFilters: boolean): string[];
|
|
100
121
|
/**
|
|
101
122
|
* A live session's stable row key: its session id when the agent reported one,
|
|
102
123
|
* else a per-machine handle so an id-less live session (a just-booted harness, a
|
|
@@ -135,6 +156,20 @@ export declare function mergeLiveIntoPool(rows: SessionMeta[], live: Map<string,
|
|
|
135
156
|
* listing that has no live rows to explain it.
|
|
136
157
|
*/
|
|
137
158
|
export declare function shouldShowHostColumn(f: BrowserFilter, live: Map<string, ActiveSession> | null, rows: SessionMeta[]): boolean;
|
|
159
|
+
/** Apply the cheap in-memory filters (agent / device / project / running / favorites). */
|
|
160
|
+
export declare function applyFilters(rows: SessionMeta[], live: Map<string, ActiveSession>, f: BrowserFilter, self: string, favorites: Set<string>): SessionMeta[];
|
|
161
|
+
/** One-shot form of the browser's canonical candidate pipeline. Focus uses this
|
|
162
|
+
* instead of maintaining a second discovery/filter implementation. */
|
|
163
|
+
export declare function collectSessionCandidates(initial: Partial<BrowserFilter>, opts?: {
|
|
164
|
+
local?: boolean;
|
|
165
|
+
hosts?: string[];
|
|
166
|
+
includeLive?: boolean;
|
|
167
|
+
}): Promise<{
|
|
168
|
+
sessions: SessionMeta[];
|
|
169
|
+
liveById: Map<string, ActiveSession>;
|
|
170
|
+
self: string;
|
|
171
|
+
unreachable: string[];
|
|
172
|
+
}>;
|
|
138
173
|
/**
|
|
139
174
|
* Launch the interactive session browser. `initial` seeds the filter (e.g.
|
|
140
175
|
* `{ running: true }` for `--active`). Resolves after the user resumes a session
|