@phnx-labs/agents-cli 1.20.73 → 1.20.76
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 +989 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +31 -1
- package/dist/commands/exec.js +31 -3
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +87 -9
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/setup.js +6 -1
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +51 -11
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +99 -31
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +56 -0
- package/dist/lib/agents.js +133 -7
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +60 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +25 -0
- package/dist/lib/import.js +107 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/project-resources.js +20 -5
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +27 -1
- package/dist/lib/shims.js +88 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +15 -36
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +48 -2
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +22 -2
- package/dist/lib/versions.js +98 -10
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainTo
|
|
|
28
28
|
import { fileStore } from './filestore.js';
|
|
29
29
|
import { getVaultSession, vaultDeleteItem, vaultExists, vaultGetItems, vaultGetItem, vaultHasItem, vaultListItems, vaultSetItems, vaultSetItem, } from './vault.js';
|
|
30
30
|
import { emit } from '../events.js';
|
|
31
|
-
import { readMeta } from '../state.js';
|
|
31
|
+
import { readMeta, getHelpersDir } from '../state.js';
|
|
32
32
|
import { assertNameActiveInResourceProfile, filterNamesForActiveResourceProfile } from '../resource-profiles.js';
|
|
33
33
|
import { agentGetSync, agentAutoLoadSync, agentGetMetaSync, agentAutoLoadMetaSync, agentEvictSync, secretsAgentAutoEnabled, secretsHoldMs } from './agent.js';
|
|
34
34
|
import { loadSession, deleteSession } from './session-store.js';
|
|
@@ -243,6 +243,44 @@ export function bundleExists(name) {
|
|
|
243
243
|
validateBundleName(name);
|
|
244
244
|
return itemStore(bundleBackend(name)).has(bundleMetaItem(name));
|
|
245
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Thrown by `readBundle` for the one state `readBundleIfDecryptable` may treat
|
|
248
|
+
* as "present but permanently unreadable": file-store ciphertext on disk that
|
|
249
|
+
* will not decrypt with the passphrase in effect (lost/rotated key or a tampered
|
|
250
|
+
* store). It is deliberately narrow — a bundle that is merely *locked for this
|
|
251
|
+
* run* (headless macOS without `AGENTS_SECRETS_PASSPHRASE`, or a vault that is
|
|
252
|
+
* not logged in) is recoverable and must not be collapsed into this.
|
|
253
|
+
*/
|
|
254
|
+
export class BundleUndecryptableError extends Error {
|
|
255
|
+
constructor(message) {
|
|
256
|
+
super(message);
|
|
257
|
+
this.name = 'BundleUndecryptableError';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Read a bundle, or return null when its metadata is present but genuinely
|
|
262
|
+
* cannot be decrypted — a lost or rotated file-store passphrase, or a tampered
|
|
263
|
+
* file store (signalled by `BundleUndecryptableError`).
|
|
264
|
+
*
|
|
265
|
+
* Deleting such a bundle is the only way out of that state, and deletion needs
|
|
266
|
+
* no plaintext, so it must not be gated behind a successful decrypt. Every other
|
|
267
|
+
* failure rethrows: a genuinely missing bundle ("not found"), and — critically —
|
|
268
|
+
* a bundle that is only *temporarily locked* for this run (headless macOS with no
|
|
269
|
+
* `AGENTS_SECRETS_PASSPHRASE`, or a not-logged-in vault). Collapsing that
|
|
270
|
+
* recoverable "set the env / log in" state into "unreadable, safe to delete"
|
|
271
|
+
* would let `secrets delete <name> --yes` silently destroy a perfectly healthy
|
|
272
|
+
* bundle from a cron/launchd run that merely forgot to export the passphrase.
|
|
273
|
+
*/
|
|
274
|
+
export function readBundleIfDecryptable(name) {
|
|
275
|
+
try {
|
|
276
|
+
return readBundle(name);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
if (err instanceof BundleUndecryptableError)
|
|
280
|
+
return null;
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
246
284
|
export function readBundle(name) {
|
|
247
285
|
validateBundleName(name);
|
|
248
286
|
const backend = bundleBackend(name);
|
|
@@ -258,7 +296,7 @@ export function readBundle(name) {
|
|
|
258
296
|
// A file-backed bundle whose metadata is on disk but fails to decrypt is a
|
|
259
297
|
// wrong-passphrase error, not a missing bundle — surface that clearly.
|
|
260
298
|
if (backend === 'file' && fileStore.has(bundleMetaItem(name))) {
|
|
261
|
-
throw new
|
|
299
|
+
throw new BundleUndecryptableError(`Bundle '${name}': failed to decrypt — wrong AGENTS_SECRETS_PASSPHRASE or tampered file store. (${err.message})`);
|
|
262
300
|
}
|
|
263
301
|
if (vaultExists() && !getVaultSession().loggedIn) {
|
|
264
302
|
throw new Error(`Synced secrets are locked. Run: agents login`);
|
|
@@ -429,18 +467,40 @@ function finishBundleWrite(bundle, opts) {
|
|
|
429
467
|
}
|
|
430
468
|
export function writeBundle(bundle, opts = {}) {
|
|
431
469
|
const prepared = prepareBundleWrite(bundle);
|
|
432
|
-
//
|
|
433
|
-
// `
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
|
|
470
|
+
// Bundle metadata (name, description, policy, var names + refs, and any
|
|
471
|
+
// non-sensitive `--value` literals) is stored WITHOUT the biometry ACL at
|
|
472
|
+
// EVERY tier. It is non-sensitive by contract — the real secret values live in
|
|
473
|
+
// separate agents-cli.secrets.* items that keep the bundle's policy ACL — so a
|
|
474
|
+
// no-ACL metadata item is what lets `secrets list` and crabbox's `agents
|
|
475
|
+
// devices list` enumerate bundles with no Touch ID prompt (RUSH-1759). On an
|
|
476
|
+
// un-updated pinned helper this write fails loudly (the no-ACL command is
|
|
477
|
+
// missing) rather than silently landing an ACL'd item.
|
|
478
|
+
itemStore(prepared.backend).set(prepared.metadataItem, prepared.metadataJson, { noAcl: true });
|
|
437
479
|
finishBundleWrite(bundle, opts);
|
|
438
480
|
}
|
|
439
481
|
export function writeBundleWithItems(bundle, items, opts = {}) {
|
|
440
482
|
const prepared = prepareBundleWrite(bundle);
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
483
|
+
const store = itemStore(prepared.backend);
|
|
484
|
+
if (prepared.backend === 'keychain') {
|
|
485
|
+
// Only the keychain backend has a biometry ACL. Secret VALUE items carry the
|
|
486
|
+
// bundle's policy ACL (`never` ⇒ no-ACL); the metadata item is ALWAYS no-ACL
|
|
487
|
+
// (see writeBundle). The two must NOT ride one batch flag — a single noAcl
|
|
488
|
+
// over both would either strip biometry off the real secrets or re-ACL the
|
|
489
|
+
// metadata. Write the values first, then the metadata last: bundle discovery
|
|
490
|
+
// keys on the metadata item's presence, so metadata-last means a partial
|
|
491
|
+
// write reads as "no bundle yet", never as a bundle with missing values.
|
|
492
|
+
if (items.size > 0) {
|
|
493
|
+
store.setBatch(new Map(items), { noAcl: bundle.policy === 'never' });
|
|
494
|
+
}
|
|
495
|
+
store.set(prepared.metadataItem, prepared.metadataJson, { noAcl: true });
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
// file / vault: no ACL concept (noAcl is ignored), so one batched write is
|
|
499
|
+
// both correct and cheaper — e.g. a single age re-encrypt for the vault.
|
|
500
|
+
const batch = new Map(items);
|
|
501
|
+
batch.set(prepared.metadataItem, prepared.metadataJson);
|
|
502
|
+
store.setBatch(batch, { noAcl: bundle.policy === 'never' });
|
|
503
|
+
}
|
|
444
504
|
finishBundleWrite(bundle, opts);
|
|
445
505
|
}
|
|
446
506
|
export function deleteBundle(name) {
|
|
@@ -503,6 +563,79 @@ function parseBundleMeta(nameHint, json, backend) {
|
|
|
503
563
|
}
|
|
504
564
|
return bundle;
|
|
505
565
|
}
|
|
566
|
+
// Sentinel marking the one-time RUSH-1759 metadata-ACL heal as done. Lives under
|
|
567
|
+
// the regenerable helpers dir (same tree as the secrets-agent runtime state), so
|
|
568
|
+
// a cache wipe just re-runs the heal — harmless, since it is idempotent.
|
|
569
|
+
const METADATA_NOACL_SENTINEL = 'bundles-metadata-noacl-healed';
|
|
570
|
+
function metadataNoAclSentinelPath() {
|
|
571
|
+
return path.join(getHelpersDir(), 'secrets-agent', METADATA_NOACL_SENTINEL);
|
|
572
|
+
}
|
|
573
|
+
function bundleMetadataAclHealed() {
|
|
574
|
+
try {
|
|
575
|
+
return fs.existsSync(metadataNoAclSentinelPath());
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function markBundleMetadataAclHealed() {
|
|
582
|
+
try {
|
|
583
|
+
const file = metadataNoAclSentinelPath();
|
|
584
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
585
|
+
fs.writeFileSync(file, '', 'utf8');
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// Best effort — a missing sentinel just means the (idempotent) heal re-runs
|
|
589
|
+
// on the next broker-miss listing.
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Re-write already-read keychain bundle metadata items WITHOUT the biometry ACL.
|
|
594
|
+
* `metaJsonByName` maps bundle name → the exact metadata JSON listBundles just
|
|
595
|
+
* batch-read, so writing it back only flips the ACL (via the helper's
|
|
596
|
+
* delete-then-add `set-no-acl`) — contents and updated_at are preserved and no
|
|
597
|
+
* extra keychain read is issued. Exported for tests. Returns the count healed.
|
|
598
|
+
*/
|
|
599
|
+
export function healKeychainBundleMetadata(metaJsonByName) {
|
|
600
|
+
let healed = 0;
|
|
601
|
+
for (const [name, json] of metaJsonByName) {
|
|
602
|
+
try {
|
|
603
|
+
// Cleartext meta-item name → hashed by keychainStore.set (#316) to the same
|
|
604
|
+
// service the read enumerated, so this overwrites the existing item in
|
|
605
|
+
// place, no-ACL. A per-item failure (e.g. a pinned helper without
|
|
606
|
+
// set-no-acl) must not abort the rest.
|
|
607
|
+
keychainStore.set(bundleMetaItem(name), json, { noAcl: true });
|
|
608
|
+
healed++;
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
/* keep healing the remaining items */
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return healed;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* One-time driver around healKeychainBundleMetadata (RUSH-1759). macOS + real
|
|
618
|
+
* keychain only — libsecret/CredMan have no biometry ACL to shed, and a test
|
|
619
|
+
* backend has no real keychain — and gated by a sentinel so it runs at most
|
|
620
|
+
* once. Best-effort: a heal failure never breaks bundle listing.
|
|
621
|
+
*/
|
|
622
|
+
function healKeychainBundleMetadataAclOnce(metaJsonByName) {
|
|
623
|
+
if (metaJsonByName.size === 0)
|
|
624
|
+
return;
|
|
625
|
+
if (process.platform !== 'darwin')
|
|
626
|
+
return;
|
|
627
|
+
if (isKeychainBackendOverridden())
|
|
628
|
+
return;
|
|
629
|
+
if (bundleMetadataAclHealed())
|
|
630
|
+
return;
|
|
631
|
+
try {
|
|
632
|
+
if (healKeychainBundleMetadata(metaJsonByName) > 0)
|
|
633
|
+
markBundleMetadataAclHealed();
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
/* never let a heal failure break `secrets list` */
|
|
637
|
+
}
|
|
638
|
+
}
|
|
506
639
|
export function listBundles() {
|
|
507
640
|
const out = [];
|
|
508
641
|
// Keychain-backed bundles: batch all metadata reads behind ONE Touch ID
|
|
@@ -558,6 +691,7 @@ export function listBundles() {
|
|
|
558
691
|
else {
|
|
559
692
|
const fetched = getKeychainTokens(keychainServices);
|
|
560
693
|
const keychainBundles = [];
|
|
694
|
+
const metaJsonByName = new Map();
|
|
561
695
|
for (const service of keychainServices) {
|
|
562
696
|
const json = fetched.get(service);
|
|
563
697
|
if (json === undefined)
|
|
@@ -566,8 +700,10 @@ export function listBundles() {
|
|
|
566
700
|
? service.slice(BUNDLE_META_PREFIX.length)
|
|
567
701
|
: undefined;
|
|
568
702
|
const bundle = parseBundleMeta(nameHint, json, 'keychain');
|
|
569
|
-
if (bundle)
|
|
703
|
+
if (bundle) {
|
|
570
704
|
keychainBundles.push(bundle);
|
|
705
|
+
metaJsonByName.set(bundle.name, json);
|
|
706
|
+
}
|
|
571
707
|
}
|
|
572
708
|
for (const bundle of keychainBundles)
|
|
573
709
|
out.push(bundle);
|
|
@@ -578,6 +714,13 @@ export function listBundles() {
|
|
|
578
714
|
if (useAgent && keychainBundles.length > 0) {
|
|
579
715
|
agentAutoLoadMetaSync(nameSetHash, keychainBundles, secretsHoldMs());
|
|
580
716
|
}
|
|
717
|
+
// One-time RUSH-1759 heal: bundles written before the metadata-no-ACL
|
|
718
|
+
// change carry an ACL'd metadata item, so this fresh read (a broker miss)
|
|
719
|
+
// popped Touch ID just to enumerate them. Re-home each metadata item
|
|
720
|
+
// no-ACL now — reusing the JSON we just read, so the heal adds no extra
|
|
721
|
+
// prompt — and every later enumeration is silent. Runs at most once (a
|
|
722
|
+
// sentinel under the regenerable helpers dir).
|
|
723
|
+
healKeychainBundleMetadataAclOnce(metaJsonByName);
|
|
581
724
|
}
|
|
582
725
|
}
|
|
583
726
|
}
|
|
@@ -977,15 +1120,32 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
977
1120
|
return filtered;
|
|
978
1121
|
}
|
|
979
1122
|
}
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
//
|
|
983
|
-
//
|
|
1123
|
+
// The headless guard exists only to keep a Touch ID sheet off the interactive
|
|
1124
|
+
// user's screen — so it must block ONLY reads that can actually raise one.
|
|
1125
|
+
// A file-backed bundle resolves via passphrase with no prompt (handled below,
|
|
1126
|
+
// never held by the broker). A `never`/no-ACL keychain bundle is written
|
|
1127
|
+
// WITHOUT the biometry access control at every tier (see writeBundle), so its
|
|
1128
|
+
// reads are fully silent (no Touch ID, no broker) — exactly like a file
|
|
1129
|
+
// bundle, and exactly the "automation-only" tier a headless daemon reads
|
|
1130
|
+
// (daemon.ts:readDaemonClaudeOAuthToken). Learning the policy here is itself
|
|
1131
|
+
// prompt-less: metadata items are written no-ACL at every tier, so readBundle
|
|
1132
|
+
// reads them silently. An unreadable metadata read is treated as prompt-risky
|
|
1133
|
+
// (throw) — the safe default.
|
|
984
1134
|
if (opts.agentOnly && backend === 'keychain') {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1135
|
+
let noAclBundle = false;
|
|
1136
|
+
try {
|
|
1137
|
+
noAclBundle = bundlePolicy(readBundle(name)) === 'never';
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
noAclBundle = false;
|
|
1141
|
+
}
|
|
1142
|
+
if (!noAclBundle) {
|
|
1143
|
+
throw new Error(`Secrets bundle '${name}' is not unlocked in the secrets agent, and this is a ` +
|
|
1144
|
+
`headless/background process that must not raise a Touch ID prompt on the ` +
|
|
1145
|
+
`interactive user's screen. Run 'agents secrets unlock ${name}' in a terminal ` +
|
|
1146
|
+
`first, or set AGENTS_SECRETS_NO_PROMPT=0 to force an interactive prompt.`);
|
|
1147
|
+
}
|
|
1148
|
+
// never/no-ACL ⇒ fall through to the prompt-less keychain read below.
|
|
989
1149
|
}
|
|
990
1150
|
if (backend === 'file')
|
|
991
1151
|
assertFileBackendUsable(name);
|
|
@@ -187,7 +187,21 @@ export declare function rekeyStatus(): {
|
|
|
187
187
|
* count is then meaningless. Only probeable once the key record exists. */
|
|
188
188
|
enumerationOk: boolean;
|
|
189
189
|
};
|
|
190
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Check if a keychain/keyring item exists. Never prompts for biometry.
|
|
192
|
+
*
|
|
193
|
+
* Throws when the item cannot be reached — on macOS, when the signed helper is
|
|
194
|
+
* unavailable. That is deliberate: this primitive gates destructive writes as
|
|
195
|
+
* well as reads. Through `bundleExists()` it guards the `--force` overwrite
|
|
196
|
+
* checks in `agents secrets create` (`../../commands/secrets.ts`), the
|
|
197
|
+
* bundle-rename purge (`./bundles.ts`), the pull-rollback bookkeeping
|
|
198
|
+
* (`./sync.ts`), and the reuse-never-overwrite rule for `R2_SYNC_ENC_KEY`
|
|
199
|
+
* (`../session/sync/provision.ts`). A false "absent" silently disarms every one
|
|
200
|
+
* of them, so an unreachable keychain must fail loudly rather than answer "no".
|
|
201
|
+
*
|
|
202
|
+
* Tests needing this path without a helper install a backend via
|
|
203
|
+
* `setKeychainBackendForTest()`, which short-circuits on the next line.
|
|
204
|
+
*/
|
|
191
205
|
export declare function hasKeychainToken(item: string): boolean;
|
|
192
206
|
/**
|
|
193
207
|
* Retrieve a secret value from the keychain/keyring. Throws if not found.
|
|
@@ -481,8 +481,12 @@ export function computeRekeyPlan(services, values, key) {
|
|
|
481
481
|
try {
|
|
482
482
|
const parsed = JSON.parse(value);
|
|
483
483
|
if (parsed && typeof parsed === 'object') {
|
|
484
|
-
|
|
485
|
-
|
|
484
|
+
// Bundle metadata is non-sensitive by contract and stored no-ACL at
|
|
485
|
+
// EVERY tier (matches writeBundle in bundles.ts), so `secrets list` /
|
|
486
|
+
// crabbox's `agents devices list` enumerate bundles with no Touch ID
|
|
487
|
+
// (RUSH-1759). Re-home metadata no-ACL regardless of the bundle's policy;
|
|
488
|
+
// the real secret values (second loop) still carry their per-bundle ACL.
|
|
489
|
+
noAcl = true;
|
|
486
490
|
payload = JSON.stringify({ ...parsed, name });
|
|
487
491
|
}
|
|
488
492
|
}
|
|
@@ -669,7 +673,21 @@ export function rekeyStatus() {
|
|
|
669
673
|
enumerationOk,
|
|
670
674
|
};
|
|
671
675
|
}
|
|
672
|
-
/**
|
|
676
|
+
/**
|
|
677
|
+
* Check if a keychain/keyring item exists. Never prompts for biometry.
|
|
678
|
+
*
|
|
679
|
+
* Throws when the item cannot be reached — on macOS, when the signed helper is
|
|
680
|
+
* unavailable. That is deliberate: this primitive gates destructive writes as
|
|
681
|
+
* well as reads. Through `bundleExists()` it guards the `--force` overwrite
|
|
682
|
+
* checks in `agents secrets create` (`../../commands/secrets.ts`), the
|
|
683
|
+
* bundle-rename purge (`./bundles.ts`), the pull-rollback bookkeeping
|
|
684
|
+
* (`./sync.ts`), and the reuse-never-overwrite rule for `R2_SYNC_ENC_KEY`
|
|
685
|
+
* (`../session/sync/provision.ts`). A false "absent" silently disarms every one
|
|
686
|
+
* of them, so an unreachable keychain must fail loudly rather than answer "no".
|
|
687
|
+
*
|
|
688
|
+
* Tests needing this path without a helper install a backend via
|
|
689
|
+
* `setKeychainBackendForTest()`, which short-circuits on the next line.
|
|
690
|
+
*/
|
|
673
691
|
export function hasKeychainToken(item) {
|
|
674
692
|
item = prepareServiceName(item);
|
|
675
693
|
if (backend)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect credentials exported from shell rc files.
|
|
3
|
+
*
|
|
4
|
+
* A secret exported from `~/.zshenv`, `~/.zshrc`, `~/.bashrc`, `~/.profile`, …
|
|
5
|
+
* is inherited by every process the login shell spawns and is readable from
|
|
6
|
+
* `/proc/<pid>/environ` by any same-user process — the master passphrase leak
|
|
7
|
+
* behind RUSH-1968. `.zshenv` in particular is sourced by *every* zsh
|
|
8
|
+
* invocation, including non-interactive `ssh host 'cmd'`, so the value lands in
|
|
9
|
+
* the environment of essentially everything the box runs.
|
|
10
|
+
*
|
|
11
|
+
* The keychain-backed store (`agents secrets`) is the sanctioned home for
|
|
12
|
+
* credentials; the file-store master passphrase belongs in the off-env 0600 key
|
|
13
|
+
* file (`~/.agents/.secrets-key/passphrase`), never a shell rc export.
|
|
14
|
+
*
|
|
15
|
+
* This scanner powers a warn-level advisory in `agents doctor`. It reads only
|
|
16
|
+
* the exported variable *name* and its line number — never the value — so a
|
|
17
|
+
* finding can be printed, logged, or shipped without leaking the secret.
|
|
18
|
+
*/
|
|
19
|
+
/** Shell rc files sourced at login/startup, in the order a report lists them. */
|
|
20
|
+
export declare const RC_FILENAMES: readonly [".zshenv", ".zprofile", ".zshrc", ".zlogin", ".bash_profile", ".bash_login", ".bashrc", ".profile", ".kshrc"];
|
|
21
|
+
/**
|
|
22
|
+
* A single credential-shaped export found in an rc file. Carries the variable
|
|
23
|
+
* NAME and location only — the value is never captured.
|
|
24
|
+
*/
|
|
25
|
+
export interface RcSecretFinding {
|
|
26
|
+
/** Basename of the rc file, e.g. `.zshenv`. */
|
|
27
|
+
file: string;
|
|
28
|
+
/** 1-based line number of the export. */
|
|
29
|
+
line: number;
|
|
30
|
+
/** The exported variable name (e.g. `AGENTS_SECRETS_PASSPHRASE`). Never the value. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** The file-store master passphrase gets called out separately — it is the highest-severity case. */
|
|
33
|
+
isMasterPassphrase: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** True if a variable name looks like it holds a credential value. */
|
|
36
|
+
export declare function isCredentialName(name: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Scan one rc file's contents for credential-shaped exports. Pure — takes the
|
|
39
|
+
* text, returns findings (names + line numbers only, never values). A trailing
|
|
40
|
+
* `# comment` is stripped before matching so a commented-out export is ignored;
|
|
41
|
+
* the variable name always precedes any inline `#`, so stripping never loses it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function scanRcExports(basename: string, content: string): RcSecretFinding[];
|
|
44
|
+
/**
|
|
45
|
+
* Scan the current user's shell rc files. Reads each file that exists under
|
|
46
|
+
* `homeDir` (default `os.homedir()`) and aggregates the findings. Missing or
|
|
47
|
+
* unreadable files are skipped silently — an advisory, not a hard requirement.
|
|
48
|
+
*/
|
|
49
|
+
export declare function scanUserRcFiles(homeDir?: string): RcSecretFinding[];
|
|
50
|
+
/**
|
|
51
|
+
* Build the advisory lines for `agents doctor` from a set of findings. Returns
|
|
52
|
+
* `[]` when there is nothing to report. The first line is the headline; the
|
|
53
|
+
* rest are indented detail. Names only — never values.
|
|
54
|
+
*/
|
|
55
|
+
export declare function rcSecretWarningLines(findings: RcSecretFinding[]): string[];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect credentials exported from shell rc files.
|
|
3
|
+
*
|
|
4
|
+
* A secret exported from `~/.zshenv`, `~/.zshrc`, `~/.bashrc`, `~/.profile`, …
|
|
5
|
+
* is inherited by every process the login shell spawns and is readable from
|
|
6
|
+
* `/proc/<pid>/environ` by any same-user process — the master passphrase leak
|
|
7
|
+
* behind RUSH-1968. `.zshenv` in particular is sourced by *every* zsh
|
|
8
|
+
* invocation, including non-interactive `ssh host 'cmd'`, so the value lands in
|
|
9
|
+
* the environment of essentially everything the box runs.
|
|
10
|
+
*
|
|
11
|
+
* The keychain-backed store (`agents secrets`) is the sanctioned home for
|
|
12
|
+
* credentials; the file-store master passphrase belongs in the off-env 0600 key
|
|
13
|
+
* file (`~/.agents/.secrets-key/passphrase`), never a shell rc export.
|
|
14
|
+
*
|
|
15
|
+
* This scanner powers a warn-level advisory in `agents doctor`. It reads only
|
|
16
|
+
* the exported variable *name* and its line number — never the value — so a
|
|
17
|
+
* finding can be printed, logged, or shipped without leaking the secret.
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'fs';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
/** Shell rc files sourced at login/startup, in the order a report lists them. */
|
|
23
|
+
export const RC_FILENAMES = [
|
|
24
|
+
'.zshenv',
|
|
25
|
+
'.zprofile',
|
|
26
|
+
'.zshrc',
|
|
27
|
+
'.zlogin',
|
|
28
|
+
'.bash_profile',
|
|
29
|
+
'.bash_login',
|
|
30
|
+
'.bashrc',
|
|
31
|
+
'.profile',
|
|
32
|
+
'.kshrc',
|
|
33
|
+
];
|
|
34
|
+
/** The file-store master key. Its own resolution prefers an off-env 0600 file, so
|
|
35
|
+
* a shell-rc export is always wrong once the store exists (RUSH-1968). */
|
|
36
|
+
const MASTER_PASSPHRASE = 'AGENTS_SECRETS_PASSPHRASE';
|
|
37
|
+
/**
|
|
38
|
+
* Last `_`-delimited segment values that mark a variable as credential-shaped.
|
|
39
|
+
* Matched against the FINAL segment (segment equality, not substring) so
|
|
40
|
+
* `X_API_KEY` fires on `KEY` while `SOME_KEY_PATH` (ends `PATH`) and `MONKEY`
|
|
41
|
+
* (single segment, not equal to `KEY`) do not.
|
|
42
|
+
*/
|
|
43
|
+
const CREDENTIAL_SEGMENTS = new Set([
|
|
44
|
+
'PASSPHRASE',
|
|
45
|
+
'PASSWORD',
|
|
46
|
+
'PASSWD',
|
|
47
|
+
'SECRET',
|
|
48
|
+
'SECRETS',
|
|
49
|
+
'TOKEN',
|
|
50
|
+
'KEY',
|
|
51
|
+
'APIKEY',
|
|
52
|
+
'PAT',
|
|
53
|
+
'CREDENTIAL',
|
|
54
|
+
'CREDENTIALS',
|
|
55
|
+
]);
|
|
56
|
+
/**
|
|
57
|
+
* Final segments that look credential-shaped but name a file path, socket, or
|
|
58
|
+
* config knob rather than a secret value — excluded to cut false positives.
|
|
59
|
+
* Applied to the whole name's final segment before the credential check.
|
|
60
|
+
*/
|
|
61
|
+
const BENIGN_FINAL_SEGMENTS = new Set([
|
|
62
|
+
'PATH',
|
|
63
|
+
'FILE',
|
|
64
|
+
'DIR',
|
|
65
|
+
'SOCK',
|
|
66
|
+
'SOCKET',
|
|
67
|
+
'ID',
|
|
68
|
+
'NAME',
|
|
69
|
+
'PARALLELISM', // TOKENIZERS_PARALLELISM and friends
|
|
70
|
+
]);
|
|
71
|
+
/** True if a variable name looks like it holds a credential value. */
|
|
72
|
+
export function isCredentialName(name) {
|
|
73
|
+
if (name === MASTER_PASSPHRASE)
|
|
74
|
+
return true;
|
|
75
|
+
const segments = name.toUpperCase().split('_');
|
|
76
|
+
const last = segments[segments.length - 1];
|
|
77
|
+
if (BENIGN_FINAL_SEGMENTS.has(last))
|
|
78
|
+
return false;
|
|
79
|
+
return CREDENTIAL_SEGMENTS.has(last);
|
|
80
|
+
}
|
|
81
|
+
// Matches an env assignment that puts a value in the environment:
|
|
82
|
+
// export NAME=... (sh/bash/zsh)
|
|
83
|
+
// typeset -x NAME=... / declare -x NAME=... (bash/zsh export-attribute form)
|
|
84
|
+
const EXPORT_RE = /^\s*(?:export\s+|(?:typeset|declare)\s+-[A-Za-z]*x[A-Za-z]*\s+)([A-Za-z_][A-Za-z0-9_]*)\s*=/;
|
|
85
|
+
/**
|
|
86
|
+
* Scan one rc file's contents for credential-shaped exports. Pure — takes the
|
|
87
|
+
* text, returns findings (names + line numbers only, never values). A trailing
|
|
88
|
+
* `# comment` is stripped before matching so a commented-out export is ignored;
|
|
89
|
+
* the variable name always precedes any inline `#`, so stripping never loses it.
|
|
90
|
+
*/
|
|
91
|
+
export function scanRcExports(basename, content) {
|
|
92
|
+
const out = [];
|
|
93
|
+
const lines = content.split('\n');
|
|
94
|
+
for (let i = 0; i < lines.length; i++) {
|
|
95
|
+
const code = lines[i].replace(/#.*$/, '');
|
|
96
|
+
const m = EXPORT_RE.exec(code);
|
|
97
|
+
if (!m)
|
|
98
|
+
continue;
|
|
99
|
+
const name = m[1];
|
|
100
|
+
if (!isCredentialName(name))
|
|
101
|
+
continue;
|
|
102
|
+
out.push({
|
|
103
|
+
file: basename,
|
|
104
|
+
line: i + 1,
|
|
105
|
+
name,
|
|
106
|
+
isMasterPassphrase: name === MASTER_PASSPHRASE,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Scan the current user's shell rc files. Reads each file that exists under
|
|
113
|
+
* `homeDir` (default `os.homedir()`) and aggregates the findings. Missing or
|
|
114
|
+
* unreadable files are skipped silently — an advisory, not a hard requirement.
|
|
115
|
+
*/
|
|
116
|
+
export function scanUserRcFiles(homeDir = os.homedir()) {
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const name of RC_FILENAMES) {
|
|
119
|
+
const fp = path.join(homeDir, name);
|
|
120
|
+
let content;
|
|
121
|
+
try {
|
|
122
|
+
content = fs.readFileSync(fp, 'utf8');
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
continue; // absent or unreadable — nothing to report
|
|
126
|
+
}
|
|
127
|
+
out.push(...scanRcExports(name, content));
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Build the advisory lines for `agents doctor` from a set of findings. Returns
|
|
133
|
+
* `[]` when there is nothing to report. The first line is the headline; the
|
|
134
|
+
* rest are indented detail. Names only — never values.
|
|
135
|
+
*/
|
|
136
|
+
export function rcSecretWarningLines(findings) {
|
|
137
|
+
if (findings.length === 0)
|
|
138
|
+
return [];
|
|
139
|
+
const lines = [];
|
|
140
|
+
const master = findings.filter((f) => f.isMasterPassphrase);
|
|
141
|
+
const others = findings.filter((f) => !f.isMasterPassphrase);
|
|
142
|
+
const total = findings.length;
|
|
143
|
+
lines.push(`${total} credential-shaped export${total === 1 ? '' : 's'} found in shell rc files — ` +
|
|
144
|
+
`readable from /proc/<pid>/environ by any same-user process.`);
|
|
145
|
+
for (const f of master) {
|
|
146
|
+
lines.push(`${f.file}:${f.line} ${f.name} — the file-store master key. Move it off-env to ` +
|
|
147
|
+
`~/.agents/.secrets-key/passphrase (chmod 600) and delete the export.`);
|
|
148
|
+
}
|
|
149
|
+
for (const f of others) {
|
|
150
|
+
lines.push(`${f.file}:${f.line} ${f.name} — move to \`agents secrets\` and inject via \`agents secrets exec\`.`);
|
|
151
|
+
}
|
|
152
|
+
lines.push('Rule: no credentials in env vars or shell config. Use the keychain-backed store.');
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
@@ -34,11 +34,14 @@ import { type SshExecResult } from '../ssh-exec.js';
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function isDangerousRemoteEnvKey(name: string): boolean;
|
|
36
36
|
/**
|
|
37
|
-
* Resolve a `--host` value to an ssh target
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Resolve a `--host` value to an ssh target STRING for the remote-secrets path.
|
|
38
|
+
* Delegates to the single host/device resolver (`resolveHost`, RUSH-1967) so a
|
|
39
|
+
* name here dials the exact same box `run --host` does; on a miss, treats the
|
|
40
|
+
* value as a raw ssh target and validates it against injection. Named distinctly
|
|
41
|
+
* from `../devices/resolve-target.ts` (which returns richer shapes) so importing
|
|
42
|
+
* the wrong one can't silently change which machine you dial.
|
|
40
43
|
*/
|
|
41
|
-
export declare function
|
|
44
|
+
export declare function resolveHostSshTarget(nameOrAlias: string): Promise<string>;
|
|
42
45
|
/**
|
|
43
46
|
* Merge `--host <single>` and `--hosts <a,b,c>` into an ordered, de-duplicated
|
|
44
47
|
* list. Both flags compose; either alone works. Empty when neither is set.
|
|
@@ -59,11 +59,14 @@ function osForTarget(target, lookupName) {
|
|
|
59
59
|
return byName ?? resolveRemoteOsSync(target.split('@').pop() ?? target);
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
* Resolve a `--host` value to an ssh target
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* Resolve a `--host` value to an ssh target STRING for the remote-secrets path.
|
|
63
|
+
* Delegates to the single host/device resolver (`resolveHost`, RUSH-1967) so a
|
|
64
|
+
* name here dials the exact same box `run --host` does; on a miss, treats the
|
|
65
|
+
* value as a raw ssh target and validates it against injection. Named distinctly
|
|
66
|
+
* from `../devices/resolve-target.ts` (which returns richer shapes) so importing
|
|
67
|
+
* the wrong one can't silently change which machine you dial.
|
|
65
68
|
*/
|
|
66
|
-
export async function
|
|
69
|
+
export async function resolveHostSshTarget(nameOrAlias) {
|
|
67
70
|
const host = await resolveHost(nameOrAlias);
|
|
68
71
|
if (host)
|
|
69
72
|
return sshTargetFor(host);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The argv tokens for the synchronous secrets-broker clients: the tokens
|
|
3
|
+
* `agentGetSync`/`agentReachableSync`/`agentEvictSync` spawn, and the tokens
|
|
4
|
+
* `index.ts` dispatches on before commander.
|
|
5
|
+
*
|
|
6
|
+
* This is a LEAF module — it imports nothing, so the CLI entrypoint can bind
|
|
7
|
+
* these statically without dragging the secrets graph (and its `state.js` /
|
|
8
|
+
* `install-helper.js` / `session-store.js` imports) into every invocation. That
|
|
9
|
+
* is the whole reason the file exists: with the tokens in `agent.ts`, `index.ts`
|
|
10
|
+
* had to repeat them as string literals, and the two could silently drift —
|
|
11
|
+
* a drift with no visible failure mode, because the CLI answers "unknown
|
|
12
|
+
* command", the client reads the non-zero exit as "broker down", and every
|
|
13
|
+
* secret read falls back to a Touch ID prompt. Exactly the bug this subsystem
|
|
14
|
+
* was fixed for. Sharing one binding makes that divergence a type error rather
|
|
15
|
+
* than something a test has to notice.
|
|
16
|
+
*
|
|
17
|
+
* Same leaf discipline, and same motivation, as `lib/cli-entry.ts`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SYNC_GET_CMD = "__secrets-get";
|
|
20
|
+
export declare const SYNC_PING_CMD = "__secrets-ping";
|
|
21
|
+
export declare const SYNC_LOCK_CMD = "__secrets-lock";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The argv tokens for the synchronous secrets-broker clients: the tokens
|
|
3
|
+
* `agentGetSync`/`agentReachableSync`/`agentEvictSync` spawn, and the tokens
|
|
4
|
+
* `index.ts` dispatches on before commander.
|
|
5
|
+
*
|
|
6
|
+
* This is a LEAF module — it imports nothing, so the CLI entrypoint can bind
|
|
7
|
+
* these statically without dragging the secrets graph (and its `state.js` /
|
|
8
|
+
* `install-helper.js` / `session-store.js` imports) into every invocation. That
|
|
9
|
+
* is the whole reason the file exists: with the tokens in `agent.ts`, `index.ts`
|
|
10
|
+
* had to repeat them as string literals, and the two could silently drift —
|
|
11
|
+
* a drift with no visible failure mode, because the CLI answers "unknown
|
|
12
|
+
* command", the client reads the non-zero exit as "broker down", and every
|
|
13
|
+
* secret read falls back to a Touch ID prompt. Exactly the bug this subsystem
|
|
14
|
+
* was fixed for. Sharing one binding makes that divergence a type error rather
|
|
15
|
+
* than something a test has to notice.
|
|
16
|
+
*
|
|
17
|
+
* Same leaf discipline, and same motivation, as `lib/cli-entry.ts`.
|
|
18
|
+
*/
|
|
19
|
+
export const SYNC_GET_CMD = '__secrets-get';
|
|
20
|
+
export const SYNC_PING_CMD = '__secrets-ping';
|
|
21
|
+
export const SYNC_LOCK_CMD = '__secrets-lock';
|