@phnx-labs/agents-cli 1.20.35 → 1.20.37
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- 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.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- 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 +156 -75
- 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.js +20 -8
- package/dist/commands/sessions-sync.d.ts +3 -0
- package/dist/commands/sessions-sync.js +44 -4
- package/dist/commands/sessions.d.ts +78 -1
- package/dist/commands/sessions.js +506 -66
- 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/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- 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/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- 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 +112 -8
- 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 +57 -0
- package/dist/lib/daemon.js +196 -18
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/resolve-target.d.ts +24 -0
- package/dist/lib/devices/resolve-target.js +80 -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 +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- 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 +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- 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 +14 -0
- package/dist/lib/hosts/tasks.js +15 -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/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- 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/plugins.js +16 -6
- 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 +73 -0
- package/dist/lib/session/active.js +199 -22
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +41 -5
- package/dist/lib/session/discover.d.ts +39 -0
- package/dist/lib/session/discover.js +127 -29
- package/dist/lib/session/ghostty-tabs.d.ts +33 -0
- package/dist/lib/session/ghostty-tabs.js +126 -0
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/relative-time.js +6 -2
- package/dist/lib/session/remote-active.js +19 -17
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +205 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +20 -4
- package/dist/lib/session/sync/config.d.ts +14 -15
- package/dist/lib/session/sync/config.js +60 -20
- package/dist/lib/session/types.d.ts +23 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -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 +21 -13
- 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/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/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- 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/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/tmux/session.d.ts +10 -0
- package/dist/lib/tmux/session.js +31 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- 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/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -257,6 +257,8 @@ function parsePolicy(raw) {
|
|
|
257
257
|
return 'daily';
|
|
258
258
|
if (raw === 'always' || raw === 'biometry')
|
|
259
259
|
return 'always';
|
|
260
|
+
if (raw === 'never' || raw === 'none')
|
|
261
|
+
return 'never';
|
|
260
262
|
return undefined;
|
|
261
263
|
}
|
|
262
264
|
/** The default prompt policy applied to bundles without an explicit per-bundle
|
|
@@ -314,9 +316,14 @@ export function writeBundle(bundle) {
|
|
|
314
316
|
backend: backend === 'file' ? 'file' : undefined,
|
|
315
317
|
// Wire format: persist the policy under the legacy `tier` token so older CLI
|
|
316
318
|
// versions on other synced machines keep reading it — `daily`⇒`session`,
|
|
317
|
-
// explicit `always`⇒`biometry`. An absent policy omits the
|
|
318
|
-
// and resolves to the configured default (`daily`) on read.
|
|
319
|
-
|
|
319
|
+
// explicit `always`⇒`biometry`, `never`⇒`none`. An absent policy omits the
|
|
320
|
+
// token entirely and resolves to the configured default (`daily`) on read.
|
|
321
|
+
// An older CLI that doesn't know `none` reads it as undefined and falls back
|
|
322
|
+
// to its own default — safe, since it also lacks the no-ACL write path.
|
|
323
|
+
tier: bundle.policy === 'daily' ? 'session'
|
|
324
|
+
: bundle.policy === 'always' ? 'biometry'
|
|
325
|
+
: bundle.policy === 'never' ? 'none'
|
|
326
|
+
: undefined,
|
|
320
327
|
created_at: bundle.created_at,
|
|
321
328
|
updated_at: bundle.updated_at,
|
|
322
329
|
last_used: bundle.last_used,
|
|
@@ -324,7 +331,11 @@ export function writeBundle(bundle) {
|
|
|
324
331
|
meta,
|
|
325
332
|
};
|
|
326
333
|
const json = JSON.stringify(payload);
|
|
327
|
-
|
|
334
|
+
// A `never` bundle's metadata is stored without the biometry ACL too, so
|
|
335
|
+
// `view` and the metadata half of a read resolve silently — the whole point
|
|
336
|
+
// of the tier. On an un-updated pinned helper this write fails loudly (the
|
|
337
|
+
// no-ACL command is missing) rather than silently landing an ACL'd item.
|
|
338
|
+
itemStore(backend).set(bundleMetaItem(bundle.name), json, { noAcl: bundle.policy === 'never' });
|
|
328
339
|
emit('secrets.set', { bundle: bundle.name });
|
|
329
340
|
}
|
|
330
341
|
export function deleteBundle(name) {
|
|
@@ -504,6 +515,87 @@ function stampLastUsed(bundle) {
|
|
|
504
515
|
// Swallow — telemetry must never block secret resolution.
|
|
505
516
|
}
|
|
506
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Abort if any of the selected keys has an `expires` date in the past.
|
|
520
|
+
* Bundle-level expiry is not a concept today (expiry is per-key via `meta`),
|
|
521
|
+
* so we iterate only the per-key meta entries.
|
|
522
|
+
*/
|
|
523
|
+
function assertNotExpired(bundle, selectedKeys, allowExpired) {
|
|
524
|
+
if (allowExpired)
|
|
525
|
+
return;
|
|
526
|
+
if (!bundle.meta)
|
|
527
|
+
return;
|
|
528
|
+
const now = Date.now();
|
|
529
|
+
for (const key of selectedKeys) {
|
|
530
|
+
const meta = bundle.meta[key];
|
|
531
|
+
if (!meta?.expires)
|
|
532
|
+
continue;
|
|
533
|
+
// expires is 'YYYY-MM-DD'; treat as end-of-day UTC.
|
|
534
|
+
const expiry = new Date(meta.expires + 'T23:59:59Z').getTime();
|
|
535
|
+
if (expiry < now) {
|
|
536
|
+
throw new Error(`Bundle '${bundle.name}' key '${key}' expired on ${meta.expires}. ` +
|
|
537
|
+
`Rotate it with: agents secrets rotate ${bundle.name} ${key}` +
|
|
538
|
+
` (or pass --allow-expired to skip this check).`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Resolve the requested key subset against a bundle's `vars` map. Throws a
|
|
544
|
+
* fail-loud error listing available keys if any requested key is absent. When
|
|
545
|
+
* `requested` is undefined or empty, every key in the bundle is selected.
|
|
546
|
+
*/
|
|
547
|
+
function selectRequestedKeys(bundle, requested) {
|
|
548
|
+
const req = requested?.length ? requested : undefined;
|
|
549
|
+
if (req) {
|
|
550
|
+
const missing = req.filter((k) => !(k in bundle.vars));
|
|
551
|
+
if (missing.length > 0) {
|
|
552
|
+
const available = Object.keys(bundle.vars).join(', ') || '(none)';
|
|
553
|
+
throw new Error(`Bundle '${bundle.name}' does not contain key(s): ${missing.join(', ')}. Available: ${available}.`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return new Set(req ?? Object.keys(bundle.vars));
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Apply the --keys subset + expiry gate to an already-resolved snapshot from
|
|
560
|
+
* the secrets-agent fast-path. The agent stores the FULL bundle env, so a
|
|
561
|
+
* naive fast-path return would silently defeat --keys and inject expired
|
|
562
|
+
* values. Mirrors the slow-path pre-checks in `resolveBundleEnv` /
|
|
563
|
+
* `readAndResolveBundleEnv` and returns a new env whose keys match the subset.
|
|
564
|
+
*
|
|
565
|
+
* Exported for tests; production callers reach it via the fast-path branch in
|
|
566
|
+
* `readAndResolveBundleEnv`.
|
|
567
|
+
*/
|
|
568
|
+
export function filterAgentHitBySubsetAndExpiry(hit, opts) {
|
|
569
|
+
const selectedKeys = selectRequestedKeys(hit.bundle, opts.keys);
|
|
570
|
+
assertNotExpired(hit.bundle, [...selectedKeys], opts.allowExpired ?? false);
|
|
571
|
+
// When no subset was requested, return the cached env untouched — same
|
|
572
|
+
// reference the agent handed back, so no per-call allocation on the hot path.
|
|
573
|
+
if (!opts.keys?.length)
|
|
574
|
+
return hit;
|
|
575
|
+
const env = {};
|
|
576
|
+
for (const key of selectedKeys) {
|
|
577
|
+
if (key in hit.env)
|
|
578
|
+
env[key] = hit.env[key];
|
|
579
|
+
}
|
|
580
|
+
return { bundle: hit.bundle, env };
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Guard for remote-bundle callers (`bundle@host` / `--host`) — the SSH
|
|
584
|
+
* resolver in `remoteResolveEnv` does not thread --keys or --allow-expired
|
|
585
|
+
* yet. Silently applying them would inject the full remote env or an expired
|
|
586
|
+
* value, defeating the least-privilege intent, so we fail loud.
|
|
587
|
+
*
|
|
588
|
+
* Exported so `agents run --secrets bundle@host` and `agents secrets exec
|
|
589
|
+
* --host` share the exact same error text; the tests exercise this helper
|
|
590
|
+
* directly instead of driving the whole CLI.
|
|
591
|
+
*/
|
|
592
|
+
export function assertRemoteBundleFlagsUnsupported(bundleName, host, opts, flagLabels) {
|
|
593
|
+
const hasKeys = Array.isArray(opts.keys) && opts.keys.length > 0;
|
|
594
|
+
if (!hasKeys && !opts.allowExpired)
|
|
595
|
+
return;
|
|
596
|
+
throw new Error(`Bundle '${bundleName}@${host}': ${flagLabels.keysFlag} and ${flagLabels.allowExpiredFlag} are not supported for remote (bundle@host) bundles yet. ` +
|
|
597
|
+
`Drop the flag or resolve the bundle locally.`);
|
|
598
|
+
}
|
|
507
599
|
// Walk the bundle and produce a flat env map. Every keychain: ref is gathered
|
|
508
600
|
// into a single batch read so macOS shows ONE Touch ID prompt for the whole
|
|
509
601
|
// bundle — including the metadata fetch that already happened in readBundle
|
|
@@ -513,9 +605,14 @@ function stampLastUsed(bundle) {
|
|
|
513
605
|
// resolved inline and never reach the keychain.
|
|
514
606
|
export function resolveBundleEnv(bundle, _opts = {}) {
|
|
515
607
|
stampLastUsed(bundle);
|
|
608
|
+
// Key-subset validation and expiry pre-check.
|
|
609
|
+
const selectedKeys = selectRequestedKeys(bundle, _opts.keys);
|
|
610
|
+
assertNotExpired(bundle, [...selectedKeys], _opts.allowExpired ?? false);
|
|
516
611
|
const parsedByKey = new Map();
|
|
517
612
|
const keychainItemsToFetch = [];
|
|
518
613
|
for (const [key, raw] of Object.entries(bundle.vars)) {
|
|
614
|
+
if (!selectedKeys.has(key))
|
|
615
|
+
continue;
|
|
519
616
|
const parsed = parseBundleValue(raw);
|
|
520
617
|
parsedByKey.set(key, parsed);
|
|
521
618
|
if ('ref' in parsed && parsed.ref.provider === 'keychain') {
|
|
@@ -528,6 +625,8 @@ export function resolveBundleEnv(bundle, _opts = {}) {
|
|
|
528
625
|
: new Map();
|
|
529
626
|
const env = {};
|
|
530
627
|
for (const [key, raw] of Object.entries(bundle.vars)) {
|
|
628
|
+
if (!selectedKeys.has(key))
|
|
629
|
+
continue;
|
|
531
630
|
const parsed = parsedByKey.get(key);
|
|
532
631
|
if ('literal' in parsed) {
|
|
533
632
|
env[key] = parsed.literal;
|
|
@@ -582,15 +681,20 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
582
681
|
if (backend === 'keychain' && !opts.noAgent && process.env.AGENTS_SECRETS_NO_AGENT !== '1') {
|
|
583
682
|
const hit = agentGetSync(name);
|
|
584
683
|
if (hit) {
|
|
585
|
-
|
|
684
|
+
// The agent stores the FULL bundle env. Apply the same subset filter and
|
|
685
|
+
// expiry gate as the slow path — without this, `--secrets-keys X` would
|
|
686
|
+
// silently inject every key and an expired key would flow through after
|
|
687
|
+
// the first cache-populating run.
|
|
688
|
+
const filtered = filterAgentHitBySubsetAndExpiry(hit, opts);
|
|
689
|
+
stampLastUsed(filtered.bundle);
|
|
586
690
|
emit('secrets.get', {
|
|
587
691
|
bundle: name,
|
|
588
692
|
caller: opts.caller,
|
|
589
693
|
status: 'success',
|
|
590
694
|
source: 'agent',
|
|
591
|
-
keyCount: Object.keys(
|
|
695
|
+
keyCount: Object.keys(filtered.env).length,
|
|
592
696
|
});
|
|
593
|
-
return
|
|
697
|
+
return filtered;
|
|
594
698
|
}
|
|
595
699
|
}
|
|
596
700
|
if (backend === 'file')
|
|
@@ -651,11 +755,16 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
651
755
|
for (const key of Object.keys(bundle.vars)) {
|
|
652
756
|
validateEnvKey(key);
|
|
653
757
|
}
|
|
758
|
+
// Key-subset validation and expiry pre-check (mirrors resolveBundleEnv logic).
|
|
759
|
+
const selectedKeys = selectRequestedKeys(bundle, opts.keys);
|
|
760
|
+
assertNotExpired(bundle, [...selectedKeys], opts.allowExpired ?? false);
|
|
654
761
|
stampLastUsed(bundle);
|
|
655
762
|
const parsedByKey = new Map();
|
|
656
763
|
const keychainKeys = [];
|
|
657
764
|
const kindCounts = {};
|
|
658
765
|
for (const [key, raw] of Object.entries(bundle.vars)) {
|
|
766
|
+
if (!selectedKeys.has(key))
|
|
767
|
+
continue;
|
|
659
768
|
const p = parseBundleValue(raw);
|
|
660
769
|
parsedByKey.set(key, p);
|
|
661
770
|
const kind = 'literal' in p ? 'literal' : p.ref.provider;
|
|
@@ -664,7 +773,7 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
664
773
|
keychainKeys.push(key);
|
|
665
774
|
}
|
|
666
775
|
}
|
|
667
|
-
const keys =
|
|
776
|
+
const keys = [...selectedKeys].sort();
|
|
668
777
|
keychainKeys.sort();
|
|
669
778
|
const emitReadAudit = (status, err) => {
|
|
670
779
|
emit('secrets.get', {
|
|
@@ -681,6 +790,8 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
681
790
|
try {
|
|
682
791
|
const env = {};
|
|
683
792
|
for (const [key] of Object.entries(bundle.vars)) {
|
|
793
|
+
if (!selectedKeys.has(key))
|
|
794
|
+
continue;
|
|
684
795
|
const p = parsedByKey.get(key);
|
|
685
796
|
if ('literal' in p) {
|
|
686
797
|
env[key] = p.literal;
|
|
@@ -749,7 +860,7 @@ export function rotateBundleSecret(bundle, key, opts) {
|
|
|
749
860
|
}
|
|
750
861
|
const shortId = raw.slice('keychain:'.length);
|
|
751
862
|
const item = secretsKeychainItem(bundle.name, shortId);
|
|
752
|
-
itemStore(bundle.backend ?? 'keychain').set(item, opts.newValue);
|
|
863
|
+
itemStore(bundle.backend ?? 'keychain').set(item, opts.newValue, { noAcl: bundlePolicy(bundle) === 'never' });
|
|
753
864
|
if (opts.clearMeta) {
|
|
754
865
|
if (bundle.meta)
|
|
755
866
|
delete bundle.meta[key];
|
|
@@ -817,7 +928,7 @@ export function renameBundle(oldName, newName, opts = {}) {
|
|
|
817
928
|
const shortId = raw.slice('keychain:'.length);
|
|
818
929
|
const newItem = secretsKeychainItem(newName, shortId);
|
|
819
930
|
const value = store.get(oldItem);
|
|
820
|
-
store.set(newItem, value);
|
|
931
|
+
store.set(newItem, value, { noAcl: bundlePolicy(source) === 'never' });
|
|
821
932
|
}
|
|
822
933
|
// writeBundle preserves source.created_at, refreshes updated_at, and keeps
|
|
823
934
|
// the source backend (spread carries source.backend).
|
|
@@ -837,8 +948,16 @@ export function renameBundle(oldName, newName, opts = {}) {
|
|
|
837
948
|
* `import` / `remove` / `delete`. Pass the bundle's resolved backend
|
|
838
949
|
* (`bundle.backend ?? 'keychain'`).
|
|
839
950
|
*/
|
|
840
|
-
export function bundleItemStore(backend) {
|
|
841
|
-
|
|
951
|
+
export function bundleItemStore(backend, opts) {
|
|
952
|
+
const store = itemStore(backend ?? 'keychain');
|
|
953
|
+
// `never`-policy bundles write their per-key values without the biometry ACL
|
|
954
|
+
// (same rationale as the metadata write in writeBundle). Wrap `set` so every
|
|
955
|
+
// value the add/import paths write inherits the no-ACL flag; reads, deletes,
|
|
956
|
+
// and existence checks are ACL-independent and pass through untouched.
|
|
957
|
+
if (opts?.noAcl) {
|
|
958
|
+
return { ...store, set: (item, value) => store.set(item, value, { noAcl: true }) };
|
|
959
|
+
}
|
|
960
|
+
return store;
|
|
842
961
|
}
|
|
843
962
|
// Iterate all keychain-backed keys in a bundle for cleanup on rm/unset.
|
|
844
963
|
export function keychainItemsForBundle(bundle) {
|
|
@@ -74,9 +74,12 @@ export declare const fileStore: {
|
|
|
74
74
|
/** File-only KeychainBackend (exported for tests; the Linux backend uses these
|
|
75
75
|
* ops with auto-provision allowed). */
|
|
76
76
|
export declare const fileBackend: KeychainBackend;
|
|
77
|
+
/** Resolved passphrase directory (exported for integration tests). */
|
|
78
|
+
export declare function resolvePassphraseDir(): string;
|
|
77
79
|
/** Test-only: reset module state (file dir + cached passphrase). */
|
|
78
80
|
export declare function _resetFileStoreForTest(opts?: {
|
|
79
81
|
fileDir?: string | null;
|
|
82
|
+
passphraseDir?: string | null;
|
|
80
83
|
passphrase?: string | null;
|
|
81
84
|
}): void;
|
|
82
85
|
export {};
|
|
@@ -28,6 +28,7 @@ import * as path from 'path';
|
|
|
28
28
|
import { encodePwshBase64 } from '../pwsh.js';
|
|
29
29
|
// ---------- file store location ----------
|
|
30
30
|
let fileDirOverride = null;
|
|
31
|
+
let passphraseDirOverride = null;
|
|
31
32
|
let cachedPassphrase = null;
|
|
32
33
|
let warnedAutoPassphrase = false;
|
|
33
34
|
export function fileDir() {
|
|
@@ -106,29 +107,40 @@ function readPassphraseFromTty() {
|
|
|
106
107
|
fs.closeSync(fd);
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
|
-
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
110
|
+
/**
|
|
111
|
+
* Directory for the auto-provisioned machine-local passphrase. Kept outside
|
|
112
|
+
* `fileDir()` so a scan of the encrypted store never co-locates key + ciphertext.
|
|
113
|
+
*/
|
|
114
|
+
function passphraseDir() {
|
|
115
|
+
return passphraseDirOverride ?? path.join(os.homedir(), '.agents', '.secrets-key');
|
|
116
|
+
}
|
|
117
|
+
function ensurePassphraseDir() {
|
|
118
|
+
fs.mkdirSync(passphraseDir(), { recursive: true, mode: 0o700 });
|
|
119
|
+
}
|
|
120
|
+
/** Path of the auto-provisioned machine-local passphrase (not an `.enc` item). */
|
|
112
121
|
function passphraseFilePath() {
|
|
122
|
+
return path.join(passphraseDir(), 'passphrase');
|
|
123
|
+
}
|
|
124
|
+
/** Legacy co-located path — read-only for machines provisioned before #479. */
|
|
125
|
+
function legacyPassphraseFilePath() {
|
|
113
126
|
return path.join(fileDir(), '.passphrase');
|
|
114
127
|
}
|
|
115
128
|
/** True if a machine-local passphrase has already been provisioned. */
|
|
116
129
|
export function machinePassphraseExists() {
|
|
117
|
-
|
|
118
|
-
return fs.readFileSync(passphraseFilePath(), 'utf8').trim().length > 0;
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
130
|
+
return readMachinePassphrase() !== null;
|
|
123
131
|
}
|
|
124
132
|
function readMachinePassphrase() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
for (const fp of [passphraseFilePath(), legacyPassphraseFilePath()]) {
|
|
134
|
+
try {
|
|
135
|
+
const p = fs.readFileSync(fp, 'utf8').trim();
|
|
136
|
+
if (p.length > 0)
|
|
137
|
+
return p;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// try next location
|
|
141
|
+
}
|
|
131
142
|
}
|
|
143
|
+
return null;
|
|
132
144
|
}
|
|
133
145
|
/**
|
|
134
146
|
* Provision (or read back) a stable machine-local passphrase for the encrypted
|
|
@@ -146,7 +158,7 @@ function provisionMachinePassphrase() {
|
|
|
146
158
|
const existing = readMachinePassphrase();
|
|
147
159
|
if (existing)
|
|
148
160
|
return existing;
|
|
149
|
-
|
|
161
|
+
ensurePassphraseDir();
|
|
150
162
|
const generated = randomBytes(32).toString('base64');
|
|
151
163
|
const fp = passphraseFilePath();
|
|
152
164
|
try {
|
|
@@ -317,9 +329,23 @@ export const fileBackend = {
|
|
|
317
329
|
delete: fileDelete,
|
|
318
330
|
list: fileList,
|
|
319
331
|
};
|
|
332
|
+
/** Resolved passphrase directory (exported for integration tests). */
|
|
333
|
+
export function resolvePassphraseDir() {
|
|
334
|
+
return passphraseDir();
|
|
335
|
+
}
|
|
320
336
|
/** Test-only: reset module state (file dir + cached passphrase). */
|
|
321
337
|
export function _resetFileStoreForTest(opts = {}) {
|
|
322
338
|
fileDirOverride = opts.fileDir ?? null;
|
|
339
|
+
if (opts.passphraseDir !== undefined) {
|
|
340
|
+
passphraseDirOverride = opts.passphraseDir;
|
|
341
|
+
}
|
|
342
|
+
else if (opts.fileDir) {
|
|
343
|
+
// Hermetic sibling when only the store dir is overridden (linux.test.ts).
|
|
344
|
+
passphraseDirOverride = path.resolve(opts.fileDir, '..', `${path.basename(opts.fileDir)}-key`);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
passphraseDirOverride = null;
|
|
348
|
+
}
|
|
323
349
|
cachedPassphrase = opts.passphrase ?? null;
|
|
324
350
|
warnedAutoPassphrase = false;
|
|
325
351
|
}
|
|
@@ -103,8 +103,16 @@ export declare function getKeychainToken(item: string): string;
|
|
|
103
103
|
* lookups — no biometric prompt path on those platforms.
|
|
104
104
|
*/
|
|
105
105
|
export declare function getKeychainTokens(items: string[]): Map<string, string>;
|
|
106
|
-
/** Store or update a secret value in the keychain/keyring. Device-local;
|
|
107
|
-
|
|
106
|
+
/** Store or update a secret value in the keychain/keyring. Device-local;
|
|
107
|
+
* biometry-gated on macOS. `opts.noAcl` (the `never` prompt-policy) writes our
|
|
108
|
+
* item WITHOUT the biometry access control so later reads are fully silent — it
|
|
109
|
+
* routes through the signed helper's `set-no-acl` path. A pinned helper that
|
|
110
|
+
* predates that path rejects the unknown command (exit 2) and this throws,
|
|
111
|
+
* rather than silently falling back to an ACL'd `set` (which would behave like
|
|
112
|
+
* `always`). Ignored by the Linux/Windows/test backends, which have no ACL. */
|
|
113
|
+
export declare function setKeychainToken(item: string, value: string, opts?: {
|
|
114
|
+
noAcl?: boolean;
|
|
115
|
+
}): void;
|
|
108
116
|
/** Delete a keychain/keyring item. Returns true if it existed. Never prompts for biometry. */
|
|
109
117
|
export declare function deleteKeychainToken(item: string): boolean;
|
|
110
118
|
/**
|
|
@@ -140,6 +148,39 @@ export declare function listLegacyKeychainItems(prefix: string): string[];
|
|
|
140
148
|
* concept, so the call is a no-op there.
|
|
141
149
|
*/
|
|
142
150
|
export declare function migrateKeychainItem(item: string): boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Enumerate data-protection items whose service starts with `prefix` that live
|
|
153
|
+
* under a NON-concrete access group — pre-#279 "orphans" filed under the implicit
|
|
154
|
+
* default group (the literal `2HTP252L87.*`) that the pinned-group queries can't
|
|
155
|
+
* see. Attributes only: never decrypts, never prompts. macOS only — Linux/Windows
|
|
156
|
+
* and the test backend have no access-group concept, so this returns [].
|
|
157
|
+
*/
|
|
158
|
+
export declare function listOrphanedKeychainItems(prefix: string): string[];
|
|
159
|
+
/** Outcome of re-homing one orphaned keychain item. */
|
|
160
|
+
export interface OrphanMigrationResult {
|
|
161
|
+
item: string;
|
|
162
|
+
status: 'ok' | 'warn' | 'fail';
|
|
163
|
+
detail?: string;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Parse the `migrate-orphans` helper summary (one record per line):
|
|
167
|
+
* OK <service> re-homed
|
|
168
|
+
* WARN <service> <detail> pinned copy written but orphan not removed
|
|
169
|
+
* FAIL <service> <detail> could not re-home (orphan left intact)
|
|
170
|
+
* Unknown lines are ignored. Exported for unit testing without a keychain.
|
|
171
|
+
*/
|
|
172
|
+
export declare function parseOrphanMigrationOutput(stdout: string): OrphanMigrationResult[];
|
|
173
|
+
/**
|
|
174
|
+
* Re-home every pre-#279 orphaned data-protection item under `prefix` into the
|
|
175
|
+
* concrete access group, behind a SINGLE Touch ID prompt for the whole batch.
|
|
176
|
+
* The helper reads each orphan by its exact persistent ref, adds the pinned copy
|
|
177
|
+
* (add-before-delete: a failed add leaves the orphan intact), then deletes the
|
|
178
|
+
* orphan by ref. Returns one result per item. macOS only — no-op elsewhere.
|
|
179
|
+
*
|
|
180
|
+
* Throws on Touch ID cancellation (exit 4) so callers can distinguish "user
|
|
181
|
+
* aborted" from "nothing to do" (empty array).
|
|
182
|
+
*/
|
|
183
|
+
export declare function migrateOrphanedKeychainItems(prefix: string): OrphanMigrationResult[];
|
|
143
184
|
/** Options controlling how secret refs are resolved. */
|
|
144
185
|
export interface ResolveOptions {
|
|
145
186
|
/** Translate a short keychain ID to a fully namespaced item name. */
|
|
@@ -264,8 +264,14 @@ export function getKeychainTokens(items) {
|
|
|
264
264
|
}
|
|
265
265
|
return result;
|
|
266
266
|
}
|
|
267
|
-
/** Store or update a secret value in the keychain/keyring. Device-local;
|
|
268
|
-
|
|
267
|
+
/** Store or update a secret value in the keychain/keyring. Device-local;
|
|
268
|
+
* biometry-gated on macOS. `opts.noAcl` (the `never` prompt-policy) writes our
|
|
269
|
+
* item WITHOUT the biometry access control so later reads are fully silent — it
|
|
270
|
+
* routes through the signed helper's `set-no-acl` path. A pinned helper that
|
|
271
|
+
* predates that path rejects the unknown command (exit 2) and this throws,
|
|
272
|
+
* rather than silently falling back to an ACL'd `set` (which would behave like
|
|
273
|
+
* `always`). Ignored by the Linux/Windows/test backends, which have no ACL. */
|
|
274
|
+
export function setKeychainToken(item, value, opts) {
|
|
269
275
|
if (backend) {
|
|
270
276
|
backend.set(item, value);
|
|
271
277
|
return;
|
|
@@ -303,12 +309,22 @@ export function setKeychainToken(item, value) {
|
|
|
303
309
|
return;
|
|
304
310
|
}
|
|
305
311
|
const bin = getKeychainHelperPath();
|
|
306
|
-
|
|
312
|
+
// `never` policy → no-ACL write. The `set-no-acl` subcommand exists only in a
|
|
313
|
+
// re-notarized helper; an older pinned helper dies with "Unknown command:
|
|
314
|
+
// set-no-acl" (exit 2), surfaced below — never a silent ACL'd downgrade.
|
|
315
|
+
const helperCmd = opts?.noAcl ? 'set-no-acl' : 'set';
|
|
316
|
+
const result = spawnSync(bin, [helperCmd, item, os.userInfo().username], {
|
|
307
317
|
input: value,
|
|
308
318
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
309
319
|
});
|
|
310
320
|
if (result.status !== 0) {
|
|
311
321
|
const msg = result.stderr?.toString().trim();
|
|
322
|
+
if (opts?.noAcl && /unknown command/i.test(msg ?? '')) {
|
|
323
|
+
throw new Error(`The 'never' prompt-policy needs a Keychain helper with the no-ACL write path, ` +
|
|
324
|
+
`but the installed helper does not support it. Rebuild + re-notarize the signed ` +
|
|
325
|
+
`helper (scripts/build-keychain-helper.sh) and re-pin its sha, then retry. ` +
|
|
326
|
+
`(helper said: ${msg})`);
|
|
327
|
+
}
|
|
312
328
|
throw new Error(msg || `Failed to write keychain item '${item}'.`);
|
|
313
329
|
}
|
|
314
330
|
}
|
|
@@ -419,6 +435,89 @@ export function migrateKeychainItem(item) {
|
|
|
419
435
|
const msg = result.stderr?.toString().trim();
|
|
420
436
|
throw new Error(msg || `Failed to migrate keychain item '${item}'.`);
|
|
421
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Enumerate data-protection items whose service starts with `prefix` that live
|
|
440
|
+
* under a NON-concrete access group — pre-#279 "orphans" filed under the implicit
|
|
441
|
+
* default group (the literal `2HTP252L87.*`) that the pinned-group queries can't
|
|
442
|
+
* see. Attributes only: never decrypts, never prompts. macOS only — Linux/Windows
|
|
443
|
+
* and the test backend have no access-group concept, so this returns [].
|
|
444
|
+
*/
|
|
445
|
+
export function listOrphanedKeychainItems(prefix) {
|
|
446
|
+
if (backend)
|
|
447
|
+
return [];
|
|
448
|
+
assertSupportedPlatform();
|
|
449
|
+
if (isLinux() || isWindows())
|
|
450
|
+
return [];
|
|
451
|
+
const bin = getKeychainHelperPath();
|
|
452
|
+
const result = spawnSync(bin, ['list-orphans', prefix, os.userInfo().username], {
|
|
453
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
454
|
+
});
|
|
455
|
+
if (result.status !== 0) {
|
|
456
|
+
const msg = result.stderr?.toString().trim();
|
|
457
|
+
throw new Error(msg || `Failed to enumerate orphaned keychain items with prefix '${prefix}'.`);
|
|
458
|
+
}
|
|
459
|
+
const out = result.stdout?.toString() || '';
|
|
460
|
+
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Parse the `migrate-orphans` helper summary (one record per line):
|
|
464
|
+
* OK <service> re-homed
|
|
465
|
+
* WARN <service> <detail> pinned copy written but orphan not removed
|
|
466
|
+
* FAIL <service> <detail> could not re-home (orphan left intact)
|
|
467
|
+
* Unknown lines are ignored. Exported for unit testing without a keychain.
|
|
468
|
+
*/
|
|
469
|
+
export function parseOrphanMigrationOutput(stdout) {
|
|
470
|
+
const results = [];
|
|
471
|
+
for (const line of stdout.split('\n')) {
|
|
472
|
+
const trimmed = line.trim();
|
|
473
|
+
if (!trimmed)
|
|
474
|
+
continue;
|
|
475
|
+
const sep = trimmed.indexOf(' ');
|
|
476
|
+
const tag = sep === -1 ? trimmed : trimmed.slice(0, sep);
|
|
477
|
+
const rest = sep === -1 ? '' : trimmed.slice(sep + 1);
|
|
478
|
+
if (tag === 'OK') {
|
|
479
|
+
// OK carries only the service name (no trailing detail).
|
|
480
|
+
results.push({ item: rest, status: 'ok' });
|
|
481
|
+
}
|
|
482
|
+
else if (tag === 'WARN' || tag === 'FAIL') {
|
|
483
|
+
// WARN/FAIL are 'TAG <service> <detail>'. Service names are space-free
|
|
484
|
+
// (validateBundleName / validateEnvKey), so the first token IS the exact
|
|
485
|
+
// service — this stays consistent with listOrphanedKeychainItems for the
|
|
486
|
+
// healed-set reconciliation in migrate-acl.
|
|
487
|
+
const item = rest.split(' ')[0] ?? rest;
|
|
488
|
+
results.push({ item, status: tag === 'WARN' ? 'warn' : 'fail', detail: rest });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return results;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Re-home every pre-#279 orphaned data-protection item under `prefix` into the
|
|
495
|
+
* concrete access group, behind a SINGLE Touch ID prompt for the whole batch.
|
|
496
|
+
* The helper reads each orphan by its exact persistent ref, adds the pinned copy
|
|
497
|
+
* (add-before-delete: a failed add leaves the orphan intact), then deletes the
|
|
498
|
+
* orphan by ref. Returns one result per item. macOS only — no-op elsewhere.
|
|
499
|
+
*
|
|
500
|
+
* Throws on Touch ID cancellation (exit 4) so callers can distinguish "user
|
|
501
|
+
* aborted" from "nothing to do" (empty array).
|
|
502
|
+
*/
|
|
503
|
+
export function migrateOrphanedKeychainItems(prefix) {
|
|
504
|
+
if (backend)
|
|
505
|
+
return [];
|
|
506
|
+
assertSupportedPlatform();
|
|
507
|
+
if (isLinux() || isWindows())
|
|
508
|
+
return [];
|
|
509
|
+
const bin = getKeychainHelperPath();
|
|
510
|
+
const result = spawnSync(bin, ['migrate-orphans', prefix, os.userInfo().username], {
|
|
511
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
512
|
+
});
|
|
513
|
+
if (result.status === 4)
|
|
514
|
+
throw new Error('Touch ID cancelled during orphan migration.');
|
|
515
|
+
if (result.status !== 0) {
|
|
516
|
+
const msg = result.stderr?.toString().trim();
|
|
517
|
+
throw new Error(msg || `Failed to migrate orphaned keychain items with prefix '${prefix}'.`);
|
|
518
|
+
}
|
|
519
|
+
return parseOrphanMigrationOutput(result.stdout?.toString() || '');
|
|
520
|
+
}
|
|
422
521
|
function expandHome(p) {
|
|
423
522
|
if (p.startsWith('~/') || p === '~') {
|
|
424
523
|
return path.join(os.homedir(), p.slice(1));
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-knowledge / JIT secret delivery over MCP (issue #333).
|
|
3
|
+
*
|
|
4
|
+
* A stdio MCP server exposing a single `get_secret(bundle, key)` tool. Unlike
|
|
5
|
+
* `agents secrets exec`, which spawns the child process with EVERY resolved
|
|
6
|
+
* value baked into `process.env` (any subprocess can `printenv` the lot — see
|
|
7
|
+
* buildSecretsExecEnv in src/commands/secrets.ts), this server hands an
|
|
8
|
+
* MCP-speaking agent framework ONE credential at a time, by name, at call
|
|
9
|
+
* time. The raw value is returned only inside the tool result: it never enters
|
|
10
|
+
* the child's environment, is never logged, and never appears in `tools/list`
|
|
11
|
+
* metadata (names only).
|
|
12
|
+
*
|
|
13
|
+
* Resolution reuses the canonical bundle read path
|
|
14
|
+
* (readAndResolveBundleEnv -> getKeychainToken); no keychain access is
|
|
15
|
+
* re-implemented here. On Linux the libsecret backend (or its encrypted-file
|
|
16
|
+
* fallback for locked/headless collections) applies transparently through that
|
|
17
|
+
* abstraction, so the server stays backend-agnostic and works unchanged on
|
|
18
|
+
* macOS.
|
|
19
|
+
*
|
|
20
|
+
* Transport: the MCP stdio transport is newline-delimited JSON-RPC 2.0 (one
|
|
21
|
+
* message per line, UTF-8, no embedded newlines). That framing is trivial, so
|
|
22
|
+
* we implement it directly rather than pull in the @modelcontextprotocol/sdk
|
|
23
|
+
* server dependency — keeping the tree dependency-free and the handler pure and
|
|
24
|
+
* unit-testable in-process.
|
|
25
|
+
*/
|
|
26
|
+
/** MCP protocol revision this server negotiates. */
|
|
27
|
+
export declare const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
28
|
+
/** The single tool this server exposes. */
|
|
29
|
+
export declare const GET_SECRET_TOOL = "get_secret";
|
|
30
|
+
/** serverInfo.name reported at `initialize`. */
|
|
31
|
+
export declare const MCP_SERVER_NAME = "agents-secrets";
|
|
32
|
+
export interface JsonRpcRequest {
|
|
33
|
+
jsonrpc?: string;
|
|
34
|
+
id?: string | number | null;
|
|
35
|
+
method?: string;
|
|
36
|
+
params?: unknown;
|
|
37
|
+
}
|
|
38
|
+
interface JsonRpcSuccess {
|
|
39
|
+
jsonrpc: '2.0';
|
|
40
|
+
id: string | number | null;
|
|
41
|
+
result: unknown;
|
|
42
|
+
}
|
|
43
|
+
interface JsonRpcErrorResponse {
|
|
44
|
+
jsonrpc: '2.0';
|
|
45
|
+
id: string | number | null;
|
|
46
|
+
error: {
|
|
47
|
+
code: number;
|
|
48
|
+
message: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export type JsonRpcResponse = JsonRpcSuccess | JsonRpcErrorResponse;
|
|
52
|
+
/** Per-bundle metadata for `tools/list` — names only, NEVER values. */
|
|
53
|
+
export interface BundleKeyMetadata {
|
|
54
|
+
bundle: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
keys: string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Enumerate every bundle and its key names WITHOUT resolving or reading any
|
|
60
|
+
* value. Mirrors how `agents secrets list` surfaces bundle metadata: names are
|
|
61
|
+
* safe to expose, values are not. `listBundles()` reads only the bundle
|
|
62
|
+
* metadata items (var definitions), never the secret items behind them.
|
|
63
|
+
*/
|
|
64
|
+
export declare function listSecretMetadata(): BundleKeyMetadata[];
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a single secret value by bundle + key through the canonical read
|
|
67
|
+
* path. Throws a clear error when the bundle or the key is absent.
|
|
68
|
+
*
|
|
69
|
+
* The bundle metadata is read first (readBundle) so a missing key is reported
|
|
70
|
+
* without triggering resolution — and, on macOS, without prompting Touch ID —
|
|
71
|
+
* for the other keys in the bundle.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveSecret(bundle: string, key: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Handle one parsed JSON-RPC request. Returns the response object, or `null`
|
|
76
|
+
* for notifications (which take no reply). Pure — no I/O, no logging — so tests
|
|
77
|
+
* drive it directly.
|
|
78
|
+
*/
|
|
79
|
+
export declare function handleMcpRequest(req: JsonRpcRequest, ctx?: {
|
|
80
|
+
version?: string;
|
|
81
|
+
}): JsonRpcResponse | null;
|
|
82
|
+
/**
|
|
83
|
+
* Run the stdio MCP server: read newline-delimited JSON-RPC requests from
|
|
84
|
+
* `input`, write responses to `output`. Resolves when the input stream ends.
|
|
85
|
+
* Streams are injectable so tests can drive a full request/response loop
|
|
86
|
+
* without spawning a process.
|
|
87
|
+
*/
|
|
88
|
+
export declare function runSecretsMcpServer(opts?: {
|
|
89
|
+
input?: NodeJS.ReadableStream;
|
|
90
|
+
output?: NodeJS.WritableStream;
|
|
91
|
+
version?: string;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
export {};
|