@phnx-labs/agents-cli 1.22.83 → 1.22.85
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 +96 -0
- package/README.md +80 -53
- package/dist/bootstrap.d.ts +7 -6
- package/dist/bootstrap.js +20 -27
- package/dist/cli/command-registry.js +5 -1
- package/dist/commands/accounts.d.ts +6 -13
- package/dist/commands/accounts.js +330 -143
- package/dist/commands/apply.js +1 -1
- package/dist/commands/auth-mint.d.ts +7 -3
- package/dist/commands/auth-mint.js +21 -9
- package/dist/commands/auth.js +2 -3
- package/dist/commands/browser.js +10 -10
- package/dist/commands/daemon.d.ts +7 -3
- package/dist/commands/daemon.js +40 -25
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/exec.js +87 -39
- package/dist/commands/fleet-capture.js +21 -2
- package/dist/commands/harness-wizard.js +2 -2
- package/dist/commands/lease.js +7 -12
- package/dist/commands/profiles.d.ts +2 -2
- package/dist/commands/profiles.js +20 -13
- package/dist/commands/repo.js +3 -3
- package/dist/commands/run-account-picker.d.ts +3 -3
- package/dist/commands/run-account-picker.js +29 -30
- package/dist/commands/secrets-passthrough.d.ts +20 -0
- package/dist/commands/secrets-passthrough.js +43 -0
- package/dist/commands/sessions-trace.d.ts +11 -0
- package/dist/commands/sessions-trace.js +71 -0
- package/dist/commands/setup-accounts.d.ts +3 -2
- package/dist/commands/setup-accounts.js +4 -4
- package/dist/commands/setup-secrets.d.ts +16 -21
- package/dist/commands/setup-secrets.js +43 -224
- package/dist/commands/ssh.js +2 -2
- package/dist/commands/sync.js +1 -1
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.js +43 -40
- package/dist/commands/versions.d.ts +11 -11
- package/dist/commands/versions.js +131 -83
- package/dist/commands/view.d.ts +1 -9
- package/dist/commands/view.js +41 -55
- package/dist/commands/webhook.js +5 -5
- package/dist/commands/workflows.js +2 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +13 -42
- package/dist/lib/account-capabilities.d.ts +1 -1
- package/dist/lib/account-capabilities.js +1 -1
- package/dist/lib/account-catalog.d.ts +121 -6
- package/dist/lib/account-catalog.js +421 -7
- package/dist/lib/account-registry.d.ts +8 -30
- package/dist/lib/account-registry.js +36 -79
- package/dist/lib/account-schema.d.ts +1 -1
- package/dist/lib/account-schema.js +1 -1
- package/dist/lib/accounting/account-pool-collect.js +16 -4
- package/dist/lib/accounting/rotate.d.ts +20 -7
- package/dist/lib/accounting/rotate.js +89 -13
- package/dist/lib/accounting/usage.js +11 -16
- package/dist/lib/accounts/add.d.ts +138 -0
- package/dist/lib/accounts/add.js +651 -0
- package/dist/lib/accounts/migrate.d.ts +117 -0
- package/dist/lib/accounts/migrate.js +536 -0
- package/dist/lib/accounts/slots.d.ts +13 -0
- package/dist/lib/accounts/slots.js +100 -0
- package/dist/lib/agent-spec/agents.d.ts +16 -0
- package/dist/lib/agent-spec/agents.js +32 -4
- package/dist/lib/app-bundle-install.js +5 -4
- package/dist/lib/auth-health.d.ts +3 -0
- package/dist/lib/auth-health.js +11 -0
- package/dist/lib/auth-mint.d.ts +52 -16
- package/dist/lib/auth-mint.js +117 -39
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +5 -5
- package/dist/lib/byok-usage.js +3 -3
- package/dist/lib/claude-account-token.d.ts +47 -3
- package/dist/lib/claude-account-token.js +172 -50
- package/dist/lib/cloud/antigravity.js +4 -4
- package/dist/lib/cloud/cursor.js +4 -4
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +6 -6
- package/dist/lib/crabbox/runtimes.d.ts +3 -3
- package/dist/lib/crabbox/runtimes.js +5 -5
- package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
- package/dist/lib/daemon/account-state-daemon-service.js +199 -3
- package/dist/lib/daemon/auth-sync-service.js +28 -1
- package/dist/lib/daemon/daemon.d.ts +6 -16
- package/dist/lib/daemon/daemon.js +52 -97
- package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
- package/dist/lib/daemon/daemon.test-fixture.js +22 -40
- package/dist/lib/daemon/harness-update-service.d.ts +1 -1
- package/dist/lib/daemon/harness-update-service.js +1 -1
- package/dist/lib/daemon/runner.d.ts +15 -3
- package/dist/lib/daemon/runner.js +49 -24
- package/dist/lib/daemon-health.d.ts +1 -2
- package/dist/lib/daemon-health.js +7 -6
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +1 -11
- package/dist/lib/daemon-ticks.js +23 -2
- package/dist/lib/daemon-webhooks.d.ts +8 -7
- package/dist/lib/daemon-webhooks.js +10 -9
- package/dist/lib/device-config.js +1 -1
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/harness-inventory.d.ts +39 -0
- package/dist/lib/devices/harness-inventory.js +126 -4
- package/dist/lib/doctor-diff.js +2 -1
- package/dist/lib/exec-account-home.d.ts +38 -0
- package/dist/lib/exec-account-home.js +164 -0
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +101 -31
- package/dist/lib/feed/activity-stream.d.ts +60 -0
- package/dist/lib/feed/activity-stream.js +271 -0
- package/dist/lib/feed/activity.d.ts +7 -0
- package/dist/lib/feed/activity.js +10 -4
- package/dist/lib/feed/watch.d.ts +9 -3
- package/dist/lib/feed/watch.js +72 -36
- package/dist/lib/fleet/apply.d.ts +2 -2
- package/dist/lib/fleet/apply.js +4 -4
- package/dist/lib/fleet/auth-sync.js +4 -3
- package/dist/lib/fleet-shared-repo-sync.js +6 -9
- package/dist/lib/fleet-shared-state.d.ts +6 -0
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapter.js +10 -6
- package/dist/lib/harness/adapters/grok.js +8 -3
- package/dist/lib/harness/adapters/muse.js +1 -1
- package/dist/lib/harness/adapters/opencode.js +12 -3
- package/dist/lib/harness-auth-capabilities.d.ts +61 -0
- package/dist/lib/harness-auth-capabilities.js +52 -0
- package/dist/lib/helper-versions.d.ts +6 -4
- package/dist/lib/helper-versions.js +5 -4
- package/dist/lib/hosts/credential-transport.d.ts +10 -0
- package/dist/lib/hosts/credential-transport.js +37 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +13 -6
- package/dist/lib/identity/client.d.ts +3 -3
- package/dist/lib/identity/client.js +3 -3
- package/dist/lib/installations/index.d.ts +1 -1
- package/dist/lib/installations/index.js +1 -1
- package/dist/lib/installations/migrate.js +9 -0
- package/dist/lib/installations/resolve.js +1 -1
- package/dist/lib/installations/shims.d.ts +15 -0
- package/dist/lib/installations/shims.js +69 -0
- package/dist/lib/installations/store.d.ts +43 -0
- package/dist/lib/installations/store.js +75 -7
- package/dist/lib/installations/versions.js +1 -1
- package/dist/lib/menubar/install-menubar.d.ts +3 -3
- package/dist/lib/menubar/install-menubar.js +3 -3
- package/dist/lib/native-accounts.d.ts +35 -0
- package/dist/lib/native-accounts.js +41 -0
- package/dist/lib/net-close.d.ts +11 -0
- package/dist/lib/net-close.js +25 -0
- package/dist/lib/openclaw-keychain.js +27 -1
- package/dist/lib/profiles.js +22 -9
- package/dist/lib/project-resources.js +1 -1
- package/dist/lib/reserved-stores.d.ts +53 -0
- package/dist/lib/reserved-stores.js +120 -0
- package/dist/lib/secrets-client.d.ts +191 -0
- package/dist/lib/secrets-client.js +710 -0
- package/dist/lib/secrets-policy.d.ts +222 -0
- package/dist/lib/secrets-policy.js +483 -0
- package/dist/lib/secrets-types.d.ts +188 -0
- package/dist/lib/secrets-types.js +24 -0
- package/dist/lib/service-manifest.js +3 -4
- package/dist/lib/session/active.d.ts +38 -4
- package/dist/lib/session/active.js +36 -4
- package/dist/lib/session/bash-command.js +10 -0
- package/dist/lib/session/db.d.ts +65 -2
- package/dist/lib/session/db.js +178 -18
- package/dist/lib/session/mirror.d.ts +5 -0
- package/dist/lib/session/mirror.js +166 -0
- package/dist/lib/session/parse.d.ts +49 -0
- package/dist/lib/session/parse.js +324 -30
- package/dist/lib/session/prompt.d.ts +93 -2
- package/dist/lib/session/prompt.js +266 -15
- package/dist/lib/session/remote/peer-stream.d.ts +47 -0
- package/dist/lib/session/remote/peer-stream.js +142 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +41 -42
- package/dist/lib/session/session-cache.d.ts +9 -0
- package/dist/lib/session/session-cache.js +40 -2
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/timeline-pass.d.ts +129 -0
- package/dist/lib/session/timeline-pass.js +323 -0
- package/dist/lib/session/timeline.d.ts +182 -0
- package/dist/lib/session/timeline.js +636 -0
- package/dist/lib/session/types.d.ts +155 -1
- package/dist/lib/sha256-asset.d.ts +11 -16
- package/dist/lib/sha256-asset.js +11 -16
- package/dist/lib/share/config.js +14 -5
- package/dist/lib/signin-badge.d.ts +26 -0
- package/dist/lib/signin-badge.js +42 -0
- package/dist/lib/staleness/detectors/workflows.js +3 -105
- package/dist/lib/staleness/writers/workflows.js +2 -1
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +18 -7
- package/dist/lib/summarizer/pass.d.ts +2 -0
- package/dist/lib/summarizer/pass.js +14 -1
- package/dist/lib/summarizer/summarize.d.ts +7 -0
- package/dist/lib/summarizer/summarize.js +3 -0
- package/dist/lib/sync-umbrella.js +2 -2
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +74 -33
- package/dist/lib/view-types.d.ts +6 -3
- package/dist/lib/workflows-registry.d.ts +71 -0
- package/dist/lib/workflows-registry.js +280 -0
- package/dist/lib/workflows.d.ts +21 -21
- package/dist/lib/workflows.js +12 -327
- package/package.json +2 -3
- package/scripts/postinstall.js +4 -29
- package/dist/commands/secrets-import.d.ts +0 -18
- package/dist/commands/secrets-import.js +0 -74
- package/dist/commands/secrets-migrate.d.ts +0 -25
- package/dist/commands/secrets-migrate.js +0 -334
- package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
- package/dist/commands/secrets-rotate-passphrase.js +0 -96
- package/dist/commands/secrets-sync.d.ts +0 -11
- package/dist/commands/secrets-sync.js +0 -153
- package/dist/commands/secrets-vault.d.ts +0 -10
- package/dist/commands/secrets-vault.js +0 -130
- package/dist/commands/secrets.d.ts +0 -212
- package/dist/commands/secrets.js +0 -3030
- package/dist/lib/accounts/connect.d.ts +0 -170
- package/dist/lib/accounts/connect.js +0 -383
- package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
- package/dist/lib/daemon/keychain-reap-service.js +0 -32
- package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
- package/dist/lib/daemon/secrets-broker-service.js +0 -51
- package/dist/lib/secrets/agent.d.ts +0 -358
- package/dist/lib/secrets/agent.js +0 -1291
- package/dist/lib/secrets/audit.d.ts +0 -46
- package/dist/lib/secrets/audit.js +0 -101
- package/dist/lib/secrets/bundles.d.ts +0 -290
- package/dist/lib/secrets/bundles.js +0 -1547
- package/dist/lib/secrets/download-keychain.d.ts +0 -47
- package/dist/lib/secrets/download-keychain.js +0 -70
- package/dist/lib/secrets/drivers/rush.d.ts +0 -14
- package/dist/lib/secrets/drivers/rush.js +0 -90
- package/dist/lib/secrets/fallback.d.ts +0 -48
- package/dist/lib/secrets/fallback.js +0 -48
- package/dist/lib/secrets/filestore.d.ts +0 -222
- package/dist/lib/secrets/filestore.js +0 -1099
- package/dist/lib/secrets/headless.d.ts +0 -43
- package/dist/lib/secrets/headless.js +0 -56
- package/dist/lib/secrets/icloud-import.d.ts +0 -79
- package/dist/lib/secrets/icloud-import.js +0 -206
- package/dist/lib/secrets/index.d.ts +0 -451
- package/dist/lib/secrets/index.js +0 -1568
- package/dist/lib/secrets/install-helper.d.ts +0 -72
- package/dist/lib/secrets/install-helper.js +0 -245
- package/dist/lib/secrets/lease.d.ts +0 -25
- package/dist/lib/secrets/lease.js +0 -44
- package/dist/lib/secrets/linux.d.ts +0 -77
- package/dist/lib/secrets/linux.js +0 -393
- package/dist/lib/secrets/list-filter.d.ts +0 -109
- package/dist/lib/secrets/list-filter.js +0 -261
- package/dist/lib/secrets/mcp.d.ts +0 -93
- package/dist/lib/secrets/mcp.js +0 -211
- package/dist/lib/secrets/profiles.d.ts +0 -10
- package/dist/lib/secrets/profiles.js +0 -13
- package/dist/lib/secrets/push.d.ts +0 -133
- package/dist/lib/secrets/push.js +0 -273
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
- package/dist/lib/secrets/rc-hygiene.js +0 -143
- package/dist/lib/secrets/read-backoff.d.ts +0 -27
- package/dist/lib/secrets/read-backoff.js +0 -64
- package/dist/lib/secrets/reaper.d.ts +0 -90
- package/dist/lib/secrets/reaper.js +0 -243
- package/dist/lib/secrets/remote.d.ts +0 -152
- package/dist/lib/secrets/remote.js +0 -344
- package/dist/lib/secrets/reserved-sync.d.ts +0 -66
- package/dist/lib/secrets/reserved-sync.js +0 -147
- package/dist/lib/secrets/scope.d.ts +0 -26
- package/dist/lib/secrets/scope.js +0 -29
- package/dist/lib/secrets/session-store.d.ts +0 -107
- package/dist/lib/secrets/session-store.js +0 -342
- package/dist/lib/secrets/sync-backend.d.ts +0 -48
- package/dist/lib/secrets/sync-backend.js +0 -13
- package/dist/lib/secrets/sync-commands.d.ts +0 -21
- package/dist/lib/secrets/sync-commands.js +0 -21
- package/dist/lib/secrets/sync.d.ts +0 -48
- package/dist/lib/secrets/sync.js +0 -237
- package/dist/lib/secrets/unlock-hints.d.ts +0 -27
- package/dist/lib/secrets/unlock-hints.js +0 -36
- package/dist/lib/secrets/usage-db.d.ts +0 -46
- package/dist/lib/secrets/usage-db.js +0 -96
- package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
- package/dist/lib/secrets/vault-age-helper.js +0 -34
- package/dist/lib/secrets/vault.d.ts +0 -49
- package/dist/lib/secrets/vault.js +0 -399
- package/dist/lib/secrets/windows.d.ts +0 -81
- package/dist/lib/secrets/windows.js +0 -556
- package/scripts/install-helper.js +0 -97
- /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
- /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agents secrets migrate-acl` — refresh existing keychain items so they pick
|
|
3
|
-
* up the new SecAccess ACL written by the signed Agents CLI.app helper.
|
|
4
|
-
*
|
|
5
|
-
* Items created before 1.19.2 (or by `security add-generic-password` directly)
|
|
6
|
-
* may carry the legacy "this-app-only" ACL that prompts the user for a
|
|
7
|
-
* password on every read. Re-writing them through the helper bakes in the
|
|
8
|
-
* empty trusted-app ACL that suppresses the prompt and lets the helper read
|
|
9
|
-
* them under LocalAuthentication instead.
|
|
10
|
-
*
|
|
11
|
-
* Sequence per item:
|
|
12
|
-
* 1. Read the current value (no auth prompt path — uses the unauthenticated
|
|
13
|
-
* `security` CLI for non-sync items, helper `get` for sync items).
|
|
14
|
-
* 2. Append (item, value, sync) to an encrypted backup before any writes.
|
|
15
|
-
* 3. Delete + rewrite via the helper so macOS hands us a fresh ACL on the
|
|
16
|
-
* new item.
|
|
17
|
-
* 4. Read back via the helper to verify the value round-trips.
|
|
18
|
-
*
|
|
19
|
-
* `--dry-run` (default) reports the planned actions. `--commit` performs the
|
|
20
|
-
* writes and produces the backup.
|
|
21
|
-
*/
|
|
22
|
-
import chalk from 'chalk';
|
|
23
|
-
import * as crypto from 'crypto';
|
|
24
|
-
import * as fs from 'fs';
|
|
25
|
-
import * as path from 'path';
|
|
26
|
-
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, listOrphanedKeychainItems, migrateOrphanedKeychainItems, rekeyServiceNames, rekeyStatus, setKeychainToken, withRawKeychainServiceNames, } from '../lib/secrets/index.js';
|
|
27
|
-
import { getBackupsDir } from '../lib/state.js';
|
|
28
|
-
import { encryptBlob, MIN_PASSPHRASE_LEN } from '../lib/secrets/sync.js';
|
|
29
|
-
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
30
|
-
const ITEM_PREFIX = 'agents-cli.';
|
|
31
|
-
function enumerateItems() {
|
|
32
|
-
const seen = new Map();
|
|
33
|
-
// Raw scope: operate on the literal enumerated names (see #316).
|
|
34
|
-
withRawKeychainServiceNames(() => {
|
|
35
|
-
for (const item of listKeychainItems(ITEM_PREFIX)) {
|
|
36
|
-
// hasKeychainToken with sync=false probes the non-synced keychain; the
|
|
37
|
-
// helper's list returns both. We don't try to distinguish — re-write with
|
|
38
|
-
// sync=false by default and only flip to sync=true if the value is only
|
|
39
|
-
// readable via the synced-only probe.
|
|
40
|
-
const localExists = hasKeychainToken(item);
|
|
41
|
-
seen.set(item, { item, sync: !localExists });
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
return [...seen.values()];
|
|
45
|
-
}
|
|
46
|
-
function writeEncryptedBackup(records, passphrase) {
|
|
47
|
-
const dir = getBackupsDir();
|
|
48
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
-
const iso = new Date().toISOString().replace(/[:.]/g, '-');
|
|
50
|
-
const file = path.join(dir, `keychain-pre-migrate-${iso}.json.enc`);
|
|
51
|
-
const envelope = encryptBlob(JSON.stringify({ v: 1, records }), passphrase);
|
|
52
|
-
fs.writeFileSync(file, JSON.stringify(envelope), { mode: 0o600 });
|
|
53
|
-
return file;
|
|
54
|
-
}
|
|
55
|
-
async function promptPassphrase() {
|
|
56
|
-
if (!isInteractiveTerminal()) {
|
|
57
|
-
throw new Error('A backup passphrase is required. Run from a TTY or set AGENTS_BACKUP_PASSPHRASE.');
|
|
58
|
-
}
|
|
59
|
-
const { password } = await import('@inquirer/prompts');
|
|
60
|
-
const first = await password({ message: 'Backup passphrase (used to encrypt the pre-migration snapshot)', mask: true });
|
|
61
|
-
if (first.length < MIN_PASSPHRASE_LEN)
|
|
62
|
-
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
63
|
-
const second = await password({ message: 'Confirm passphrase', mask: true });
|
|
64
|
-
if (first !== second)
|
|
65
|
-
throw new Error('Passphrases do not match.');
|
|
66
|
-
return first;
|
|
67
|
-
}
|
|
68
|
-
function migrateOne(record) {
|
|
69
|
-
// Raw scope: `item` is a literal service name enumerated from the helper —
|
|
70
|
-
// possibly a pre-re-key cleartext leftover — and this rewrite must target
|
|
71
|
-
// exactly that item, never its hashed transform (see #316).
|
|
72
|
-
return withRawKeychainServiceNames(() => migrateOneRaw(record));
|
|
73
|
-
}
|
|
74
|
-
function migrateOneRaw(record) {
|
|
75
|
-
const { item, value } = record;
|
|
76
|
-
// Delete + re-add to force macOS to bind a fresh ACL on the new item.
|
|
77
|
-
// SecItemUpdate preserves the existing ACL, so an in-place rewrite would
|
|
78
|
-
// not fix the legacy "enter password" prompt.
|
|
79
|
-
try {
|
|
80
|
-
deleteKeychainToken(item);
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
return { item, status: 'write-failed', detail: `delete: ${err.message}` };
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
setKeychainToken(item, value);
|
|
87
|
-
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
// Try to restore the old value so we don't lose data on a write failure.
|
|
90
|
-
// The backup is the durable safety net; this is best-effort UX.
|
|
91
|
-
try {
|
|
92
|
-
setKeychainToken(item, value);
|
|
93
|
-
}
|
|
94
|
-
catch { /* swallow */ }
|
|
95
|
-
return { item, status: 'write-failed', detail: `set: ${err.message}` };
|
|
96
|
-
}
|
|
97
|
-
let readBack;
|
|
98
|
-
try {
|
|
99
|
-
readBack = getKeychainToken(item);
|
|
100
|
-
}
|
|
101
|
-
catch (err) {
|
|
102
|
-
return { item, status: 'verify-failed', detail: `read-back: ${err.message}` };
|
|
103
|
-
}
|
|
104
|
-
if (readBack !== value) {
|
|
105
|
-
return { item, status: 'verify-failed', detail: 'value mismatch after rewrite' };
|
|
106
|
-
}
|
|
107
|
-
return { item, status: 'ok' };
|
|
108
|
-
}
|
|
109
|
-
/** Register the keychain-migration commands (`migrate-acl`, `rekey`) on the
|
|
110
|
-
* parent secrets Command. */
|
|
111
|
-
export function registerSecretsMigrateAclCommand(secrets) {
|
|
112
|
-
registerSecretsRekeyCommand(secrets);
|
|
113
|
-
secrets
|
|
114
|
-
.command('migrate-acl')
|
|
115
|
-
.description('Refresh legacy keychain ACLs and re-home items stranded in a stale access group. Dry-run by default.')
|
|
116
|
-
.option('--commit', 'Perform writes (default is dry-run reporting only)')
|
|
117
|
-
.option('--prefix <p>', `Restrict to items beginning with PREFIX (default ${ITEM_PREFIX})`, ITEM_PREFIX)
|
|
118
|
-
.option('--all', 'Rewrite EVERY matching item, not just legacy stragglers (slower; a Touch ID prompt per item)')
|
|
119
|
-
.option('--passphrase-env <var>', 'Read the backup passphrase from this env var instead of prompting')
|
|
120
|
-
.action(async (opts) => {
|
|
121
|
-
try {
|
|
122
|
-
if (process.platform !== 'darwin') {
|
|
123
|
-
throw new Error('migrate-acl is macOS-only. Linux items already use the keyring-native ACL model.');
|
|
124
|
-
}
|
|
125
|
-
const prefix = opts.prefix ?? ITEM_PREFIX;
|
|
126
|
-
if (!prefix.startsWith(ITEM_PREFIX)) {
|
|
127
|
-
throw new Error(`--prefix must start with '${ITEM_PREFIX}' to avoid touching unrelated Keychain items (got '${prefix}').`);
|
|
128
|
-
}
|
|
129
|
-
// Two independent classes of work under one command:
|
|
130
|
-
// (a) Legacy ACL stragglers — items still in the file-based keychain
|
|
131
|
-
// with a pre-migration trusted-app ACL. Rewritten value-by-value.
|
|
132
|
-
// (b) Orphaned access groups — data-protection items filed under a
|
|
133
|
-
// pre-#279 non-concrete group, invisible to the pinned queries.
|
|
134
|
-
// Re-homed by the helper behind a single Touch ID.
|
|
135
|
-
// Default: only legacy stragglers for (a); `--all` forces a full rewrite.
|
|
136
|
-
// Raw scope: migrate-acl operates on the literal names the helper
|
|
137
|
-
// enumerates (which may include pre-re-key cleartext leftovers) — the
|
|
138
|
-
// #316 name-hashing transform must not rewrite them.
|
|
139
|
-
const names = opts.all
|
|
140
|
-
? withRawKeychainServiceNames(() => listKeychainItems(prefix))
|
|
141
|
-
: listLegacyKeychainItems(prefix);
|
|
142
|
-
const items = names.map((item) => {
|
|
143
|
-
const localExists = withRawKeychainServiceNames(() => hasKeychainToken(item));
|
|
144
|
-
return { item, sync: !localExists };
|
|
145
|
-
});
|
|
146
|
-
const orphans = listOrphanedKeychainItems(prefix);
|
|
147
|
-
if (items.length === 0 && orphans.length === 0) {
|
|
148
|
-
console.log(opts.all
|
|
149
|
-
? chalk.gray(`No keychain items with prefix '${prefix}'.`)
|
|
150
|
-
: chalk.green(`Nothing to migrate — all items under '${prefix}' use the modern ACL and access group.`));
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const label = opts.all ? 'item' : 'legacy item';
|
|
154
|
-
if (items.length > 0) {
|
|
155
|
-
console.log(chalk.bold(`Found ${items.length} ${label}(s) under '${prefix}' with an outdated ACL.`));
|
|
156
|
-
}
|
|
157
|
-
if (orphans.length > 0) {
|
|
158
|
-
console.log(chalk.bold(`Found ${orphans.length} orphaned item(s) under '${prefix}' in a stale access group.`));
|
|
159
|
-
}
|
|
160
|
-
if (!opts.commit) {
|
|
161
|
-
for (const { item, sync } of items) {
|
|
162
|
-
console.log(` ${chalk.cyan(item)} ${chalk.gray(sync ? '(synced)' : '(local)')}`);
|
|
163
|
-
}
|
|
164
|
-
for (const item of orphans) {
|
|
165
|
-
console.log(` ${chalk.cyan(item)} ${chalk.yellow('(orphaned access group)')}`);
|
|
166
|
-
}
|
|
167
|
-
console.log();
|
|
168
|
-
console.log(chalk.gray('Dry-run — pass --commit to perform the migration.'));
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
// ---- Commit phase ----
|
|
172
|
-
let okCount = 0;
|
|
173
|
-
let failCount = 0;
|
|
174
|
-
// (a) Legacy ACL rewrite. Snapshot every value first (one batched read
|
|
175
|
-
// behind a single helper process), encrypt, then delete + re-add. A total
|
|
176
|
-
// read failure here counts the legacy items as failed but does NOT abort —
|
|
177
|
-
// the orphan sweep (b) is independent and must still run.
|
|
178
|
-
if (items.length > 0) {
|
|
179
|
-
const fetched = withRawKeychainServiceNames(() => getKeychainTokens(items.map((i) => i.item)));
|
|
180
|
-
const records = [];
|
|
181
|
-
for (const { item, sync } of items) {
|
|
182
|
-
const value = fetched.get(item);
|
|
183
|
-
if (value === undefined) {
|
|
184
|
-
console.error(chalk.red(`Skipping '${item}': read failed or item absent.`));
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
records.push({ item, sync, value });
|
|
188
|
-
}
|
|
189
|
-
if (records.length === 0) {
|
|
190
|
-
console.error(chalk.red('No legacy items could be read — skipping legacy rewrite.'));
|
|
191
|
-
failCount += items.length;
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
const passphrase = opts.passphraseEnv
|
|
195
|
-
? (() => {
|
|
196
|
-
const v = process.env[opts.passphraseEnv];
|
|
197
|
-
if (!v)
|
|
198
|
-
throw new Error(`Env var '${opts.passphraseEnv}' not set.`);
|
|
199
|
-
if (v.length < MIN_PASSPHRASE_LEN)
|
|
200
|
-
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
201
|
-
return v;
|
|
202
|
-
})()
|
|
203
|
-
: await promptPassphrase();
|
|
204
|
-
const backupPath = writeEncryptedBackup(records, passphrase);
|
|
205
|
-
// Quick fingerprint so the user can sanity-check recovery without decrypting.
|
|
206
|
-
const fingerprint = crypto
|
|
207
|
-
.createHash('sha256')
|
|
208
|
-
.update(records.length + '\n' + records.map((r) => r.item).sort().join('\n'))
|
|
209
|
-
.digest('hex')
|
|
210
|
-
.slice(0, 12);
|
|
211
|
-
console.log(chalk.green(`Encrypted backup written to ${backupPath} (sha256-12: ${fingerprint}).`));
|
|
212
|
-
const results = [];
|
|
213
|
-
for (const record of records) {
|
|
214
|
-
const r = migrateOne(record);
|
|
215
|
-
results.push(r);
|
|
216
|
-
if (r.status === 'ok') {
|
|
217
|
-
console.log(` ${chalk.green('ok')} ${record.item}`);
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
console.log(` ${chalk.red(r.status)} ${record.item} ${chalk.gray(r.detail ?? '')}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
const ok = results.filter((r) => r.status === 'ok').length;
|
|
224
|
-
okCount += ok;
|
|
225
|
-
failCount += results.length - ok;
|
|
226
|
-
if (results.length - ok > 0) {
|
|
227
|
-
console.error(chalk.gray(`Restore from ${backupPath} using the backup passphrase if needed.`));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
// (b) Orphan re-home — one helper call, one Touch ID for the batch. The
|
|
232
|
-
// helper adds the pinned copy before deleting the orphan, so no pre-write
|
|
233
|
-
// backup is needed (a failed add leaves the orphan intact and readable).
|
|
234
|
-
if (orphans.length > 0) {
|
|
235
|
-
console.log(chalk.bold(`Re-homing ${orphans.length} orphaned item(s) into the current access group (one Touch ID)…`));
|
|
236
|
-
const results = migrateOrphanedKeychainItems(prefix);
|
|
237
|
-
const healed = new Set();
|
|
238
|
-
for (const r of results) {
|
|
239
|
-
healed.add(r.item);
|
|
240
|
-
if (r.status === 'ok') {
|
|
241
|
-
okCount += 1;
|
|
242
|
-
console.log(` ${chalk.green('ok')} ${r.item}`);
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
failCount += 1;
|
|
246
|
-
console.log(` ${chalk.red(r.status)} ${r.item} ${chalk.gray(r.detail ?? '')}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
// Any orphan we listed but the helper couldn't reach (e.g. under a
|
|
250
|
-
// different signing team) — surface it, never drop it silently.
|
|
251
|
-
for (const item of orphans) {
|
|
252
|
-
if (!healed.has(item)) {
|
|
253
|
-
failCount += 1;
|
|
254
|
-
console.log(` ${chalk.red('fail')} ${item} ${chalk.gray('unreachable (different signing team?)')}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
const total = okCount + failCount;
|
|
259
|
-
console.log();
|
|
260
|
-
if (failCount === 0) {
|
|
261
|
-
console.log(chalk.green(`Migrated ${okCount}/${total} item(s).`));
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
console.error(chalk.yellow(`Migrated ${okCount}/${total} item(s); ${failCount} failed.`));
|
|
265
|
-
process.exit(1);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
269
|
-
if (isPromptCancelled(err))
|
|
270
|
-
return;
|
|
271
|
-
console.error(chalk.red(err.message));
|
|
272
|
-
process.exit(1);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* `agents secrets rekey` — the one-time #316 migration: replace enumerable
|
|
278
|
-
* cleartext keychain service names (`agents-cli.secrets.<bundle>.<KEY>`, …)
|
|
279
|
-
* with opaque HMAC-hashed names. Normally runs automatically on the first
|
|
280
|
-
* interactive keychain use after upgrade; this command exists for headless
|
|
281
|
-
* machines, retrying after a cancelled Touch ID, and prefix-restricted test
|
|
282
|
-
* runs. Idempotent — safe to re-run any time.
|
|
283
|
-
*/
|
|
284
|
-
function registerSecretsRekeyCommand(secrets) {
|
|
285
|
-
secrets
|
|
286
|
-
.command('rekey')
|
|
287
|
-
.description('Replace enumerable keychain service names with opaque HMAC-hashed names (one-time; idempotent). macOS only.')
|
|
288
|
-
.option('--status', 'Report re-key state without changing anything')
|
|
289
|
-
.option('--prefix <p...>', 'Restrict to cleartext services starting with PREFIX (partial run: moves matching items but does NOT activate hashed naming)')
|
|
290
|
-
.action((opts) => {
|
|
291
|
-
try {
|
|
292
|
-
if (process.platform !== 'darwin') {
|
|
293
|
-
throw new Error('secrets rekey is macOS-only. Linux/Windows service names are not enumerable via the keychain helper.');
|
|
294
|
-
}
|
|
295
|
-
if (opts.status) {
|
|
296
|
-
const st = rekeyStatus();
|
|
297
|
-
console.log(st.migrated
|
|
298
|
-
? chalk.green('Hashed service names: ACTIVE')
|
|
299
|
-
: chalk.yellow('Hashed service names: not yet active'));
|
|
300
|
-
console.log(` HMAC key present: ${st.hasKey ? 'yes' : 'no'}`);
|
|
301
|
-
if (!st.enumerationOk) {
|
|
302
|
-
console.log(chalk.red(' Keychain enumeration unavailable (locked keybag?) — counts below are unreliable.'));
|
|
303
|
-
}
|
|
304
|
-
console.log(` Cleartext-named items remaining: ${st.cleartext.length}`);
|
|
305
|
-
if (st.pendingDeletes > 0) {
|
|
306
|
-
console.log(chalk.yellow(` Pending deletes from an interrupted run: ${st.pendingDeletes} (finished automatically on next use)`));
|
|
307
|
-
}
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
for (const p of opts.prefix ?? []) {
|
|
311
|
-
if (!p.startsWith(ITEM_PREFIX)) {
|
|
312
|
-
throw new Error(`--prefix must start with '${ITEM_PREFIX}' to avoid touching unrelated Keychain items (got '${p}').`);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
const report = rekeyServiceNames({
|
|
316
|
-
prefixes: opts.prefix,
|
|
317
|
-
announce: true,
|
|
318
|
-
log: (line) => console.log(line),
|
|
319
|
-
});
|
|
320
|
-
if (report.failed.length > 0)
|
|
321
|
-
process.exit(1);
|
|
322
|
-
if (report.nothingToDo && report.activated) {
|
|
323
|
-
console.log(chalk.green('Nothing to re-key — hashed service names are active.'));
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
catch (err) {
|
|
327
|
-
const msg = err.message;
|
|
328
|
-
console.error(chalk.red(msg));
|
|
329
|
-
// Touch ID cancel exits 4 (matching the helper's contract); re-running
|
|
330
|
-
// resumes where it left off.
|
|
331
|
-
process.exit(/touch id cancelled/i.test(msg) ? 4 : 1);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agents secrets rotate-passphrase` — rotate the encrypted file store's
|
|
3
|
-
* machine-local master passphrase (RUSH-1975).
|
|
4
|
-
*
|
|
5
|
-
* Re-encrypts every `<item>.enc` under a freshly generated key and rewrites the
|
|
6
|
-
* 0600 key file in place, atomically: the new store is staged, verified
|
|
7
|
-
* (round-trip + count), fsync'd, then swapped by directory rename, so a crash
|
|
8
|
-
* leaves the old store intact and readable. Headless-safe and Linux-first — the
|
|
9
|
-
* remediation path for a compromised passphrase (RUSH-1968), where the two
|
|
10
|
-
* supported alternatives are unacceptable (a hand-rolled non-atomic script, or
|
|
11
|
-
* export-to-plaintext which is the exposure being fixed).
|
|
12
|
-
*
|
|
13
|
-
* Dry-run by default, matching `import-keyring`; `--commit` performs the swap.
|
|
14
|
-
*/
|
|
15
|
-
import type { Command } from 'commander';
|
|
16
|
-
/** Register `agents secrets rotate-passphrase` on the parent secrets Command. */
|
|
17
|
-
export declare function registerSecretsRotatePassphraseCommand(secrets: Command): void;
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agents secrets rotate-passphrase` — rotate the encrypted file store's
|
|
3
|
-
* machine-local master passphrase (RUSH-1975).
|
|
4
|
-
*
|
|
5
|
-
* Re-encrypts every `<item>.enc` under a freshly generated key and rewrites the
|
|
6
|
-
* 0600 key file in place, atomically: the new store is staged, verified
|
|
7
|
-
* (round-trip + count), fsync'd, then swapped by directory rename, so a crash
|
|
8
|
-
* leaves the old store intact and readable. Headless-safe and Linux-first — the
|
|
9
|
-
* remediation path for a compromised passphrase (RUSH-1968), where the two
|
|
10
|
-
* supported alternatives are unacceptable (a hand-rolled non-atomic script, or
|
|
11
|
-
* export-to-plaintext which is the exposure being fixed).
|
|
12
|
-
*
|
|
13
|
-
* Dry-run by default, matching `import-keyring`; `--commit` performs the swap.
|
|
14
|
-
*/
|
|
15
|
-
import chalk from 'chalk';
|
|
16
|
-
import { machinePassphraseExists, rotatePassphrase } from '../lib/secrets/filestore.js';
|
|
17
|
-
import { agentStatus } from '../lib/secrets/agent.js';
|
|
18
|
-
/** Register `agents secrets rotate-passphrase` on the parent secrets Command. */
|
|
19
|
-
export function registerSecretsRotatePassphraseCommand(secrets) {
|
|
20
|
-
secrets
|
|
21
|
-
.command('rotate-passphrase')
|
|
22
|
-
.description('Re-key the encrypted file store under a new machine-local passphrase (atomic, headless-safe). Dry-run by default.')
|
|
23
|
-
.option('--commit', 'Perform the rotation (default is dry-run reporting only)')
|
|
24
|
-
.option('--dry-run', 'Report bundle count and round-trip result without re-keying (the default). Still heals an interrupted rotation — that is the one thing it writes.')
|
|
25
|
-
.option('--force', 'Override the safety refusals (held broker unlocks, or a passphrase exported in the environment)')
|
|
26
|
-
.addHelpText('after', `
|
|
27
|
-
Rotates the auto-provisioned file-store key at ~/.agents/.secrets-key/passphrase:
|
|
28
|
-
decrypts every item under the current key, re-encrypts under a new one, verifies
|
|
29
|
-
every item round-trips, then swaps the store and key file atomically. A crash
|
|
30
|
-
before the swap leaves the old store readable with the old key; a crash inside the
|
|
31
|
-
swap self-heals on the next rotate-passphrase run (not on an ordinary get). No
|
|
32
|
-
plaintext secret value or passphrase is ever written to disk, argv, or a log.
|
|
33
|
-
|
|
34
|
-
Examples:
|
|
35
|
-
# Report what would rotate (no writes)
|
|
36
|
-
agents secrets rotate-passphrase
|
|
37
|
-
|
|
38
|
-
# Perform the rotation
|
|
39
|
-
agents secrets rotate-passphrase --commit`)
|
|
40
|
-
.action(async (opts) => {
|
|
41
|
-
try {
|
|
42
|
-
if (opts.commit && opts.dryRun) {
|
|
43
|
-
throw new Error('--commit and --dry-run are mutually exclusive.');
|
|
44
|
-
}
|
|
45
|
-
const dryRun = !opts.commit;
|
|
46
|
-
if (!machinePassphraseExists()) {
|
|
47
|
-
throw new Error('No machine-local passphrase is provisioned on this box, so there is nothing to rotate. ' +
|
|
48
|
-
'This command re-keys the file store\'s auto-provisioned key at ~/.agents/.secrets-key/passphrase.');
|
|
49
|
-
}
|
|
50
|
-
// Guard 1: refuse while the secrets-agent holds live unlocks (macOS), so a
|
|
51
|
-
// concurrent read cannot land against a half-swapped store. --force overrides.
|
|
52
|
-
if (!dryRun && !opts.force) {
|
|
53
|
-
const held = await agentStatus();
|
|
54
|
-
if (held.length > 0) {
|
|
55
|
-
throw new Error(`The secrets-agent is holding ${held.length} unlocked bundle(s) (${held.map((e) => e.name).join(', ')}). ` +
|
|
56
|
-
'Lock them first (`agents secrets lock --all`) so no concurrent read races the rotation, or pass --force.');
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// Guard 2: a passphrase exported in the environment shadows the on-disk key
|
|
60
|
-
// file with a now-stale value after the rotation, breaking every read. This
|
|
61
|
-
// is the exact RUSH-1968 footgun — refuse the commit and point at the fix.
|
|
62
|
-
if (!dryRun && !opts.force && (process.env.AGENTS_SECRETS_PASSPHRASE ?? '').length > 0) {
|
|
63
|
-
throw new Error('AGENTS_SECRETS_PASSPHRASE is set in this environment; it would shadow the rotated key file ' +
|
|
64
|
-
'with a stale value and break every read. Unset it (this is the RUSH-1968 fix) before rotating, or pass --force.');
|
|
65
|
-
}
|
|
66
|
-
const report = rotatePassphrase({ dryRun });
|
|
67
|
-
console.log(`${chalk.bold(String(report.bundleCount))} item(s) decrypt under the current key and ` +
|
|
68
|
-
`${report.roundTripOk ? chalk.green('round-trip cleanly') : chalk.red('failed to round-trip')} under a new key.`);
|
|
69
|
-
if (report.skipped.length > 0) {
|
|
70
|
-
console.log(chalk.gray(`skipped ${report.skipped.length} orphan file(s) (not re-keyed):`));
|
|
71
|
-
for (const s of report.skipped)
|
|
72
|
-
console.log(chalk.gray(` - ${s}`));
|
|
73
|
-
}
|
|
74
|
-
if (report.dryRun) {
|
|
75
|
-
if (report.recoveredInterruptedRotation) {
|
|
76
|
-
// Be precise: this run DID write. Recovery is deliberately not gated on
|
|
77
|
-
// --commit, because healing is how a crashed store becomes readable
|
|
78
|
-
// again without re-keying it.
|
|
79
|
-
console.log(chalk.yellow('Recovered an interrupted rotation: the store was healed back to a single ' +
|
|
80
|
-
'readable state. That is the only thing this dry run wrote — no re-keying happened.'));
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
console.log(chalk.gray('Dry-run: nothing written.'));
|
|
84
|
-
}
|
|
85
|
-
console.log(chalk.gray(`Pass --commit to re-encrypt the store and swap the key file (${report.keyFilePath}).`));
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
console.log(chalk.green(`Rotated: re-encrypted ${report.bundleCount} item(s) and rewrote ${report.keyFilePath} (mode 0600).`));
|
|
89
|
-
console.log(chalk.gray('The previous key and ciphertext were removed. Any process still holding the old passphrase in its environment must be restarted.'));
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
console.error(chalk.red(err.message));
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agents secrets push|pull` subcommands.
|
|
3
|
-
*
|
|
4
|
-
* Replaces iCloud Keychain as the cross-device sync mechanism with explicit
|
|
5
|
-
* encrypted-at-rest sync against api.prix.dev. Plaintext never leaves the
|
|
6
|
-
* machine — bundle contents are sealed with AES-256-GCM under a user-supplied
|
|
7
|
-
* passphrase before upload.
|
|
8
|
-
*/
|
|
9
|
-
import type { Command } from 'commander';
|
|
10
|
-
/** Register `agents secrets push|pull|remote-list` on the parent secrets Command. */
|
|
11
|
-
export declare function registerSecretsSyncCommands(secrets: Command): void;
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agents secrets push|pull` subcommands.
|
|
3
|
-
*
|
|
4
|
-
* Replaces iCloud Keychain as the cross-device sync mechanism with explicit
|
|
5
|
-
* encrypted-at-rest sync against api.prix.dev. Plaintext never leaves the
|
|
6
|
-
* machine — bundle contents are sealed with AES-256-GCM under a user-supplied
|
|
7
|
-
* passphrase before upload.
|
|
8
|
-
*/
|
|
9
|
-
import chalk from 'chalk';
|
|
10
|
-
import { listRemoteBundles, MIN_PASSPHRASE_LEN, pullBundle, pushBundle, } from '../lib/secrets/sync.js';
|
|
11
|
-
import { bundleExists, listBundles } from '../lib/secrets/bundles.js';
|
|
12
|
-
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
13
|
-
import { missingSyncPassphraseMessage, resolveSyncPassphraseFromEnv, warnEnvPassphraseReadableOnce, } from '../lib/secrets/sync-passphrase.js';
|
|
14
|
-
async function promptPassphrase(message, confirm = false) {
|
|
15
|
-
if (!isInteractiveTerminal()) {
|
|
16
|
-
throw new Error(missingSyncPassphraseMessage());
|
|
17
|
-
}
|
|
18
|
-
const { password } = await import('@inquirer/prompts');
|
|
19
|
-
const first = await password({ message, mask: true });
|
|
20
|
-
if (first.length < MIN_PASSPHRASE_LEN)
|
|
21
|
-
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
22
|
-
if (!confirm)
|
|
23
|
-
return first;
|
|
24
|
-
const second = await password({ message: 'Confirm passphrase', mask: true });
|
|
25
|
-
if (first !== second)
|
|
26
|
-
throw new Error('Passphrases do not match.');
|
|
27
|
-
return first;
|
|
28
|
-
}
|
|
29
|
-
function passphraseFromEnvOrPrompt(confirm) {
|
|
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) {
|
|
37
|
-
return Promise.reject(new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`));
|
|
38
|
-
}
|
|
39
|
-
warnEnvPassphraseReadableOnce();
|
|
40
|
-
return Promise.resolve(value);
|
|
41
|
-
}
|
|
42
|
-
return promptPassphrase('Sync passphrase', confirm);
|
|
43
|
-
}
|
|
44
|
-
/** Strip C0/C1 control bytes from server-supplied strings before terminal print. */
|
|
45
|
-
function safePrint(s) {
|
|
46
|
-
return s.replace(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, '');
|
|
47
|
-
}
|
|
48
|
-
/** Register `agents secrets push|pull|remote-list` on the parent secrets Command. */
|
|
49
|
-
export function registerSecretsSyncCommands(secrets) {
|
|
50
|
-
secrets
|
|
51
|
-
.command('push [name]')
|
|
52
|
-
.description('Encrypt a local bundle and upload it to api.prix.dev (replaces iCloud Keychain sync).')
|
|
53
|
-
.option('--all', 'Push every local bundle (each prompts independently if no passphrase env var is set)')
|
|
54
|
-
.action(async (name, opts) => {
|
|
55
|
-
try {
|
|
56
|
-
if (opts.all) {
|
|
57
|
-
const bundles = listBundles();
|
|
58
|
-
if (bundles.length === 0) {
|
|
59
|
-
console.log(chalk.gray('No local bundles to push.'));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const passphrase = await passphraseFromEnvOrPrompt(true);
|
|
63
|
-
for (const b of bundles) {
|
|
64
|
-
try {
|
|
65
|
-
const { updated_at } = await pushBundle(b.name, { passphrase });
|
|
66
|
-
console.log(chalk.green(`Pushed '${b.name}' (updated_at=${updated_at}).`));
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
console.error(chalk.red(`Failed to push '${b.name}': ${err.message}`));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (!name) {
|
|
75
|
-
throw new Error('Bundle name required. Try: agents secrets push <name> | agents secrets push --all');
|
|
76
|
-
}
|
|
77
|
-
if (!bundleExists(name)) {
|
|
78
|
-
throw new Error(`Bundle '${name}' not found locally.`);
|
|
79
|
-
}
|
|
80
|
-
const passphrase = await passphraseFromEnvOrPrompt(true);
|
|
81
|
-
const { updated_at } = await pushBundle(name, { passphrase });
|
|
82
|
-
console.log(chalk.green(`Pushed '${name}' to api.prix.dev (updated_at=${updated_at}).`));
|
|
83
|
-
console.log(chalk.gray('Remember the passphrase — it is required to pull this bundle on another machine.'));
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
if (isPromptCancelled(err))
|
|
87
|
-
return;
|
|
88
|
-
console.error(chalk.red(err.message));
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
secrets
|
|
93
|
-
.command('pull [name]')
|
|
94
|
-
.description('Decrypt a remote bundle from api.prix.dev and restore it into the local keychain.')
|
|
95
|
-
.option('--all', 'Pull every bundle visible on the remote')
|
|
96
|
-
.option('--force', 'Overwrite a local bundle with the same name')
|
|
97
|
-
.action(async (name, opts) => {
|
|
98
|
-
try {
|
|
99
|
-
if (opts.all) {
|
|
100
|
-
const remote = await listRemoteBundles();
|
|
101
|
-
if (remote.length === 0) {
|
|
102
|
-
console.log(chalk.gray('No remote bundles found.'));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const passphrase = await passphraseFromEnvOrPrompt(false);
|
|
106
|
-
for (const r of remote) {
|
|
107
|
-
try {
|
|
108
|
-
await pullBundle(r.name, { passphrase, force: opts.force });
|
|
109
|
-
console.log(chalk.green(`Pulled '${r.name}'.`));
|
|
110
|
-
}
|
|
111
|
-
catch (err) {
|
|
112
|
-
console.error(chalk.red(`Failed to pull '${r.name}': ${err.message}`));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (!name) {
|
|
118
|
-
throw new Error('Bundle name required. Try: agents secrets pull <name> | agents secrets pull --all');
|
|
119
|
-
}
|
|
120
|
-
const passphrase = await passphraseFromEnvOrPrompt(false);
|
|
121
|
-
const bundle = await pullBundle(name, { passphrase, force: opts.force });
|
|
122
|
-
const keyCount = Object.keys(bundle.vars).length;
|
|
123
|
-
console.log(chalk.green(`Pulled '${name}' (${keyCount} key${keyCount === 1 ? '' : 's'}) into local keychain.`));
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
if (isPromptCancelled(err))
|
|
127
|
-
return;
|
|
128
|
-
console.error(chalk.red(err.message));
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
secrets
|
|
133
|
-
.command('remote-list')
|
|
134
|
-
.alias('remote-ls')
|
|
135
|
-
.description('List bundles currently stored on api.prix.dev for this account.')
|
|
136
|
-
.action(async () => {
|
|
137
|
-
try {
|
|
138
|
-
const remote = await listRemoteBundles();
|
|
139
|
-
if (remote.length === 0) {
|
|
140
|
-
console.log(chalk.gray('No remote bundles found.'));
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
console.log(chalk.bold(`${'NAME'.padEnd(24)} UPDATED_AT`));
|
|
144
|
-
for (const r of remote) {
|
|
145
|
-
console.log(`${chalk.cyan(safePrint(r.name).padEnd(24))} ${chalk.gray(safePrint(r.updated_at))}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
console.error(chalk.red(err.message));
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agents secrets vault` — unlock/lock the age-encrypted synced-secrets file
|
|
3
|
-
* at ~/.agents/vault.age (distinct from `agents secrets unlock|lock`, which
|
|
4
|
-
* holds keychain bundles in the secrets-agent).
|
|
5
|
-
*
|
|
6
|
-
* Formerly top-level `agents login` / `agents logout`.
|
|
7
|
-
*/
|
|
8
|
-
import type { Command } from 'commander';
|
|
9
|
-
/** Register `agents secrets vault unlock|lock` under the secrets parent. */
|
|
10
|
-
export declare function registerSecretsVaultCommands(secretsCmd: Command): void;
|