@phnx-labs/agents-cli 1.20.51 → 1.20.53
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 +71 -0
- package/README.md +12 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.js +8 -1
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +82 -3
- package/dist/commands/factory.js +8 -0
- package/dist/commands/feed.d.ts +38 -0
- package/dist/commands/feed.js +274 -0
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/memory.d.ts +9 -0
- package/dist/commands/memory.js +164 -0
- package/dist/commands/message.d.ts +11 -6
- package/dist/commands/message.js +140 -5
- package/dist/commands/routines.js +12 -0
- package/dist/commands/secrets-migrate.d.ts +2 -1
- package/dist/commands/secrets-migrate.js +88 -13
- package/dist/commands/secrets.d.ts +22 -0
- package/dist/commands/secrets.js +176 -42
- package/dist/commands/sessions.js +10 -1
- package/dist/commands/teams.js +4 -0
- package/dist/commands/worktree.js +4 -2
- package/dist/index.js +21 -22
- package/dist/lib/agents.js +249 -17
- package/dist/lib/answer-router.d.ts +75 -0
- package/dist/lib/answer-router.js +149 -0
- package/dist/lib/ask-classifier.d.ts +71 -0
- package/dist/lib/ask-classifier.js +197 -0
- package/dist/lib/browser/login-detection.d.ts +94 -0
- package/dist/lib/browser/login-detection.js +274 -0
- package/dist/lib/browser/profiles.d.ts +17 -8
- package/dist/lib/browser/profiles.js +27 -8
- package/dist/lib/browser/secret-ref.d.ts +10 -0
- package/dist/lib/browser/secret-ref.js +14 -0
- package/dist/lib/browser/service.js +14 -12
- package/dist/lib/cloud/antigravity.d.ts +0 -2
- package/dist/lib/cloud/antigravity.js +2 -17
- package/dist/lib/cloud/codex.js +3 -18
- package/dist/lib/cloud/rush.d.ts +15 -0
- package/dist/lib/cloud/rush.js +10 -16
- package/dist/lib/cloud/stream.js +2 -0
- package/dist/lib/cloud/types.d.ts +21 -0
- package/dist/lib/cloud/types.js +81 -0
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +12 -2
- package/dist/lib/crabbox/lease.d.ts +19 -0
- package/dist/lib/crabbox/lease.js +22 -11
- package/dist/lib/crabbox/progress.d.ts +62 -0
- package/dist/lib/crabbox/progress.js +129 -0
- package/dist/lib/crabbox/runtimes.d.ts +38 -1
- package/dist/lib/crabbox/runtimes.js +98 -5
- package/dist/lib/daemon.d.ts +12 -9
- package/dist/lib/daemon.js +32 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +291 -101
- package/dist/lib/exec.js +20 -1
- package/dist/lib/feed-outcome.d.ts +101 -0
- package/dist/lib/feed-outcome.js +244 -0
- package/dist/lib/feed-policy.d.ts +30 -0
- package/dist/lib/feed-policy.js +133 -0
- package/dist/lib/feed.d.ts +180 -0
- package/dist/lib/feed.js +627 -0
- package/dist/lib/git.d.ts +17 -1
- package/dist/lib/git.js +20 -1
- package/dist/lib/hooks.js +529 -14
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +3 -3
- package/dist/lib/mailbox-gc.d.ts +22 -0
- package/dist/lib/mailbox-gc.js +161 -0
- package/dist/lib/mailbox.d.ts +26 -2
- package/dist/lib/mailbox.js +80 -5
- package/dist/lib/mcp.js +82 -0
- package/dist/lib/memory.d.ts +55 -0
- package/dist/lib/memory.js +274 -0
- package/dist/lib/notify.d.ts +16 -0
- package/dist/lib/notify.js +61 -0
- package/dist/lib/operator.d.ts +26 -0
- package/dist/lib/operator.js +107 -0
- package/dist/lib/plugins.d.ts +35 -0
- package/dist/lib/plugins.js +217 -0
- package/dist/lib/remote-agents-json.d.ts +14 -0
- package/dist/lib/remote-agents-json.js +94 -0
- package/dist/lib/resources/mcp.js +44 -0
- package/dist/lib/resources/memory.d.ts +15 -0
- package/dist/lib/resources/memory.js +46 -0
- package/dist/lib/resources/types.d.ts +2 -2
- package/dist/lib/rotate.js +2 -0
- package/dist/lib/runner.d.ts +43 -0
- package/dist/lib/runner.js +323 -74
- package/dist/lib/sandbox.js +6 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +21 -0
- package/dist/lib/secrets/agent.js +63 -1
- package/dist/lib/secrets/bundles.d.ts +33 -1
- package/dist/lib/secrets/bundles.js +76 -21
- package/dist/lib/secrets/icloud-import.d.ts +79 -0
- package/dist/lib/secrets/icloud-import.js +203 -0
- package/dist/lib/secrets/index.d.ts +142 -2
- package/dist/lib/secrets/index.js +695 -30
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- package/dist/lib/session/active.d.ts +18 -0
- package/dist/lib/session/active.js +47 -17
- package/dist/lib/session/db.d.ts +9 -1
- package/dist/lib/session/db.js +18 -3
- package/dist/lib/session/discover.d.ts +13 -0
- package/dist/lib/session/discover.js +32 -2
- package/dist/lib/session/parse.d.ts +8 -0
- package/dist/lib/session/parse.js +42 -21
- package/dist/lib/session/remote-active.js +8 -89
- package/dist/lib/session/state.d.ts +11 -0
- package/dist/lib/session/state.js +50 -1
- package/dist/lib/session/tail.d.ts +23 -4
- package/dist/lib/session/tail.js +34 -16
- package/dist/lib/session/throughput.d.ts +30 -0
- package/dist/lib/session/throughput.js +86 -0
- package/dist/lib/shim-heal.d.ts +12 -3
- package/dist/lib/shim-heal.js +12 -6
- package/dist/lib/staleness/detectors/subagents.js +57 -3
- package/dist/lib/staleness/writers/hooks.js +7 -3
- package/dist/lib/staleness/writers/subagents.js +37 -6
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/subagents.d.ts +52 -0
- package/dist/lib/subagents.js +315 -12
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/teams/worktree.d.ts +8 -0
- package/dist/lib/teams/worktree.js +8 -0
- package/dist/lib/types.d.ts +20 -2
- package/dist/lib/versions.js +38 -48
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- package/package.json +4 -3
- package/scripts/postinstall.js +61 -1
package/dist/commands/message.js
CHANGED
|
@@ -1,15 +1,119 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
2
3
|
import { die } from '../lib/format.js';
|
|
3
4
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
4
5
|
import { getTaskById, updateTaskStatus } from '../lib/cloud/store.js';
|
|
5
6
|
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
6
7
|
import { mailboxDir, enqueue } from '../lib/mailbox.js';
|
|
7
|
-
import { resolveMessageTarget } from '../lib/mailbox-target.js';
|
|
8
|
+
import { resolveMessageTarget, mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
9
|
+
import { blockIdForSession, listBlocks, readBlock, recordAnswer, recordMessageReceipt, } from '../lib/feed.js';
|
|
10
|
+
import { verifyOperatorIdentity } from '../lib/operator.js';
|
|
11
|
+
import { resolveAnswerRoute, resumeArgv, } from '../lib/answer-router.js';
|
|
12
|
+
import { injectIntoTerminal } from '../lib/terminal/inject.js';
|
|
13
|
+
/** Find the still-open block addressed to `mailboxId`, if any. */
|
|
14
|
+
function findOpenBlockForMailbox(mailboxId) {
|
|
15
|
+
// Fast path: the mailbox id is usually the session id, so the block id is
|
|
16
|
+
// directly derivable. This avoids scanning the whole feed store.
|
|
17
|
+
const direct = readBlock(blockIdForSession(mailboxId));
|
|
18
|
+
if (direct && direct.mailboxId === mailboxId)
|
|
19
|
+
return direct;
|
|
20
|
+
// Fallback: scan (agentId-based mailbox ids, rare).
|
|
21
|
+
return listBlocks().find((b) => b.mailboxId === mailboxId);
|
|
22
|
+
}
|
|
23
|
+
/** Live session whose mailbox id equals `mailboxId`. */
|
|
24
|
+
function findSessionForMailbox(mailboxId, sessions) {
|
|
25
|
+
return sessions.find((s) => mailboxIdForActiveSession(s) === mailboxId);
|
|
26
|
+
}
|
|
27
|
+
/** Claim first-answer-wins on the open block; dies if already answered / unauthorized. */
|
|
28
|
+
function claimBlockAnswer(block, opts) {
|
|
29
|
+
if (!block)
|
|
30
|
+
return;
|
|
31
|
+
const operatorId = opts.as;
|
|
32
|
+
// High-consequence answers require env-proven identity (AGENTS_OPERATOR_ID),
|
|
33
|
+
// not merely a caller-supplied known --as id (RUSH-1619).
|
|
34
|
+
const verified = verifyOperatorIdentity(operatorId);
|
|
35
|
+
const claim = recordAnswer(block.blockId, {
|
|
36
|
+
answeredBy: opts.from,
|
|
37
|
+
answeredFrom: opts.surface || 'cli',
|
|
38
|
+
operatorId,
|
|
39
|
+
verified,
|
|
40
|
+
});
|
|
41
|
+
if (!claim.ok) {
|
|
42
|
+
if ('unauthorized' in claim) {
|
|
43
|
+
die(`Not authorized: ${claim.reason}`);
|
|
44
|
+
}
|
|
45
|
+
const who = claim.existing.answeredFrom + (claim.existing.answeredBy ? ` (${claim.existing.answeredBy})` : '');
|
|
46
|
+
die(`This question was already answered by ${who}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function deliverViaMailbox(mailboxId, text, block, opts) {
|
|
50
|
+
const msgId = enqueue(mailboxDir(mailboxId), {
|
|
51
|
+
to: mailboxId,
|
|
52
|
+
text,
|
|
53
|
+
from: opts.from,
|
|
54
|
+
blockId: block?.blockId,
|
|
55
|
+
});
|
|
56
|
+
if (block) {
|
|
57
|
+
recordMessageReceipt(block.blockId, {
|
|
58
|
+
msgId,
|
|
59
|
+
status: 'queued',
|
|
60
|
+
at: new Date().toISOString(),
|
|
61
|
+
from: opts.from,
|
|
62
|
+
});
|
|
63
|
+
console.log(chalk.green(`Queued message ${msgId} for ${mailboxId}. `) +
|
|
64
|
+
chalk.dim(`Answer tied to ${block.blockId}; the agent will see it at its next tool call.`));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
console.log(chalk.green(`Queued message ${msgId} for ${mailboxId}. `) +
|
|
68
|
+
chalk.dim('The agent will see it at its next tool call.'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function deliverViaInject(route, mailboxId) {
|
|
72
|
+
if (!route.inject || route.payload == null) {
|
|
73
|
+
die(`Internal error: inject route missing target/payload for ${mailboxId}.`);
|
|
74
|
+
}
|
|
75
|
+
const result = await injectIntoTerminal(route.inject, route.payload, {
|
|
76
|
+
// Digit selection and free text both need Enter to submit the TUI choice.
|
|
77
|
+
enter: true,
|
|
78
|
+
// Digit+Enter as two writes is safer for Ink TUI.
|
|
79
|
+
combined: false,
|
|
80
|
+
});
|
|
81
|
+
if (!result.ok) {
|
|
82
|
+
die(`Failed to inject answer into ${route.inject.backend}: ${result.error ?? 'unknown error'}`);
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk.green(`Answered ${mailboxId} via ${route.inject.backend}. `) +
|
|
85
|
+
chalk.dim(route.reason));
|
|
86
|
+
}
|
|
87
|
+
async function deliverViaResume(route, mailboxId) {
|
|
88
|
+
if (route.kind !== 'resume') {
|
|
89
|
+
die(`Internal error: resume route incomplete for ${mailboxId}.`);
|
|
90
|
+
}
|
|
91
|
+
const argv = resumeArgv(route);
|
|
92
|
+
// Spawn the same agents binary the user invoked (process.argv[1]) so version
|
|
93
|
+
// pins and wrappers stay consistent. Detach so the resume can take over a TTY
|
|
94
|
+
// when interactive; for feed answers we pass the prompt non-interactively.
|
|
95
|
+
const bin = process.argv[1] ?? 'agents';
|
|
96
|
+
const child = spawn(process.execPath, [bin, ...argv], {
|
|
97
|
+
stdio: 'inherit',
|
|
98
|
+
env: process.env,
|
|
99
|
+
});
|
|
100
|
+
const code = await new Promise((resolve) => {
|
|
101
|
+
child.on('exit', (c) => resolve(c ?? 1));
|
|
102
|
+
child.on('error', () => resolve(1));
|
|
103
|
+
});
|
|
104
|
+
if (code !== 0) {
|
|
105
|
+
die(`Resume of ${mailboxId} exited with code ${code}. Tried: agents ${argv.join(' ')}`);
|
|
106
|
+
}
|
|
107
|
+
console.log(chalk.green(`Resumed ${mailboxId} with answer. `) +
|
|
108
|
+
chalk.dim(route.reason));
|
|
109
|
+
}
|
|
8
110
|
export function registerMessageCommand(program) {
|
|
9
111
|
program
|
|
10
112
|
.command('message <target> <text>')
|
|
11
|
-
.description('Send a message to a running agent (
|
|
113
|
+
.description('Send a message to a running or parked agent (mailbox / PTY-select / resume by runtime).')
|
|
12
114
|
.option('--from <who>', 'Label recorded as the sender of this message')
|
|
115
|
+
.option('--as <operator>', 'Verified operator id answering a high-consequence block')
|
|
116
|
+
.option('--surface <surface>', 'Surface that is sending this answer (feed, terminal, etc.)', 'cli')
|
|
13
117
|
.action(async (target, text, opts) => {
|
|
14
118
|
if (!target.trim()) {
|
|
15
119
|
die('Target must be a session/agent id or cloud task id. Run `agents sessions --active` to list running agents.');
|
|
@@ -32,9 +136,40 @@ export function registerMessageCommand(program) {
|
|
|
32
136
|
}
|
|
33
137
|
case 'local': {
|
|
34
138
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
139
|
+
const block = findOpenBlockForMailbox(res.id);
|
|
140
|
+
const session = findSessionForMailbox(res.id, sessions);
|
|
141
|
+
const route = resolveAnswerRoute({
|
|
142
|
+
mailboxId: res.id,
|
|
143
|
+
answer: text,
|
|
144
|
+
block,
|
|
145
|
+
session,
|
|
146
|
+
});
|
|
147
|
+
if (route.kind === 'refuse') {
|
|
148
|
+
die(route.reason);
|
|
149
|
+
}
|
|
150
|
+
// First-answer-wins for any path that closes an open block.
|
|
151
|
+
claimBlockAnswer(block, opts);
|
|
152
|
+
if (route.kind === 'mailbox') {
|
|
153
|
+
await deliverViaMailbox(res.id, text, block, opts);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (route.kind === 'tmux' || route.kind === 'iterm' || route.kind === 'pty') {
|
|
157
|
+
await deliverViaInject(route, res.id);
|
|
158
|
+
if (block) {
|
|
159
|
+
recordMessageReceipt(block.blockId, {
|
|
160
|
+
msgId: `inject-${Date.now()}`,
|
|
161
|
+
status: 'queued',
|
|
162
|
+
at: new Date().toISOString(),
|
|
163
|
+
from: opts.from,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (route.kind === 'resume') {
|
|
169
|
+
await deliverViaResume(route, res.id);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
die(`Unknown delivery route: ${route.kind}`);
|
|
38
173
|
}
|
|
39
174
|
catch (err) {
|
|
40
175
|
die(err.message);
|
|
@@ -156,6 +156,18 @@ export function registerRoutinesCommands(program) {
|
|
|
156
156
|
|
|
157
157
|
The background scheduler auto-starts the first time you add a routine.
|
|
158
158
|
Manage it with 'agents routines start|stop|status'.
|
|
159
|
+
|
|
160
|
+
Version / credit failover (same semantics as 'agents run'):
|
|
161
|
+
- Omit 'version:' to let the configured run strategy (default: balanced)
|
|
162
|
+
pick a healthy install and skip accounts that are out of credits or
|
|
163
|
+
rate-limited. Pin with 'version: 2.1.x' when you want one install only.
|
|
164
|
+
- Foreground 'agents routines run' re-dispatches to the next healthy
|
|
165
|
+
same-agent account when a mid-run rate/usage limit is detected.
|
|
166
|
+
- Detached/daemon fires use the pre-flight pick only (next tick re-selects).
|
|
167
|
+
- Diagnostic lines log which account was picked, which were skipped, and
|
|
168
|
+
each failover hop: look for "[agents] routine <name>:" in the run log.
|
|
169
|
+
- Headless Claude auth: store CLAUDE_CODE_OAUTH_TOKEN in the 'claude'
|
|
170
|
+
secrets bundle so the daemon can inject it into routine spawns.
|
|
159
171
|
`,
|
|
160
172
|
});
|
|
161
173
|
routinesCmd
|
|
@@ -20,5 +20,6 @@
|
|
|
20
20
|
* writes and produces the backup.
|
|
21
21
|
*/
|
|
22
22
|
import type { Command } from 'commander';
|
|
23
|
-
/** Register
|
|
23
|
+
/** Register the keychain-migration commands (`migrate-acl`, `rekey`) on the
|
|
24
|
+
* parent secrets Command. */
|
|
24
25
|
export declare function registerSecretsMigrateAclCommand(secrets: Command): void;
|
|
@@ -23,21 +23,24 @@ import chalk from 'chalk';
|
|
|
23
23
|
import * as crypto from 'crypto';
|
|
24
24
|
import * as fs from 'fs';
|
|
25
25
|
import * as path from 'path';
|
|
26
|
-
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, listOrphanedKeychainItems, migrateOrphanedKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
|
|
26
|
+
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, listOrphanedKeychainItems, migrateOrphanedKeychainItems, rekeyServiceNames, rekeyStatus, setKeychainToken, withRawKeychainServiceNames, } from '../lib/secrets/index.js';
|
|
27
27
|
import { getBackupsDir } from '../lib/state.js';
|
|
28
28
|
import { encryptBlob, MIN_PASSPHRASE_LEN } from '../lib/secrets/sync.js';
|
|
29
29
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
30
30
|
const ITEM_PREFIX = 'agents-cli.';
|
|
31
31
|
function enumerateItems() {
|
|
32
32
|
const seen = new Map();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
+
});
|
|
41
44
|
return [...seen.values()];
|
|
42
45
|
}
|
|
43
46
|
function writeEncryptedBackup(records, passphrase) {
|
|
@@ -63,6 +66,12 @@ async function promptPassphrase() {
|
|
|
63
66
|
return first;
|
|
64
67
|
}
|
|
65
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) {
|
|
66
75
|
const { item, value } = record;
|
|
67
76
|
// Delete + re-add to force macOS to bind a fresh ACL on the new item.
|
|
68
77
|
// SecItemUpdate preserves the existing ACL, so an in-place rewrite would
|
|
@@ -97,8 +106,10 @@ function migrateOne(record) {
|
|
|
97
106
|
}
|
|
98
107
|
return { item, status: 'ok' };
|
|
99
108
|
}
|
|
100
|
-
/** Register
|
|
109
|
+
/** Register the keychain-migration commands (`migrate-acl`, `rekey`) on the
|
|
110
|
+
* parent secrets Command. */
|
|
101
111
|
export function registerSecretsMigrateAclCommand(secrets) {
|
|
112
|
+
registerSecretsRekeyCommand(secrets);
|
|
102
113
|
secrets
|
|
103
114
|
.command('migrate-acl')
|
|
104
115
|
.description('Refresh legacy keychain ACLs and re-home items stranded in a stale access group. Dry-run by default.')
|
|
@@ -122,9 +133,14 @@ export function registerSecretsMigrateAclCommand(secrets) {
|
|
|
122
133
|
// pre-#279 non-concrete group, invisible to the pinned queries.
|
|
123
134
|
// Re-homed by the helper behind a single Touch ID.
|
|
124
135
|
// Default: only legacy stragglers for (a); `--all` forces a full rewrite.
|
|
125
|
-
|
|
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);
|
|
126
142
|
const items = names.map((item) => {
|
|
127
|
-
const localExists = hasKeychainToken(item);
|
|
143
|
+
const localExists = withRawKeychainServiceNames(() => hasKeychainToken(item));
|
|
128
144
|
return { item, sync: !localExists };
|
|
129
145
|
});
|
|
130
146
|
const orphans = listOrphanedKeychainItems(prefix);
|
|
@@ -160,7 +176,7 @@ export function registerSecretsMigrateAclCommand(secrets) {
|
|
|
160
176
|
// read failure here counts the legacy items as failed but does NOT abort —
|
|
161
177
|
// the orphan sweep (b) is independent and must still run.
|
|
162
178
|
if (items.length > 0) {
|
|
163
|
-
const fetched = getKeychainTokens(items.map((i) => i.item));
|
|
179
|
+
const fetched = withRawKeychainServiceNames(() => getKeychainTokens(items.map((i) => i.item)));
|
|
164
180
|
const records = [];
|
|
165
181
|
for (const { item, sync } of items) {
|
|
166
182
|
const value = fetched.get(item);
|
|
@@ -257,3 +273,62 @@ export function registerSecretsMigrateAclCommand(secrets) {
|
|
|
257
273
|
}
|
|
258
274
|
});
|
|
259
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
|
+
}
|
|
@@ -17,6 +17,28 @@ import { type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.j
|
|
|
17
17
|
* a filesystem path.
|
|
18
18
|
*/
|
|
19
19
|
export declare function readImportDotenv(from: string): string;
|
|
20
|
+
/** Where `secrets import` pulls keys from, parsed off the unified `--from`. */
|
|
21
|
+
export type ImportSource = {
|
|
22
|
+
kind: 'dotenv';
|
|
23
|
+
path: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: '1password';
|
|
26
|
+
vault?: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'icloud';
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Parse the unified `--from <source>` value: a .env path (`-` reads stdin),
|
|
32
|
+
* `1password:<vault>` (bare `1password` prompts for the vault), or `icloud`
|
|
33
|
+
* (legacy iCloud Keychain bundles). The deprecated `--from-1password --vault`
|
|
34
|
+
* pair maps onto the 1password source. A file literally named `icloud` or
|
|
35
|
+
* `1password` can still be imported via an explicit path (`./icloud`).
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseImportSource(opts: {
|
|
38
|
+
from?: string;
|
|
39
|
+
from1password?: boolean;
|
|
40
|
+
vault?: string;
|
|
41
|
+
}): ImportSource;
|
|
20
42
|
/**
|
|
21
43
|
* Build the remote `agents secrets unlock` argv for `unlock --host`. `--all`
|
|
22
44
|
* forwards verbatim; otherwise the explicit bundle names. A `--ttl` is passed
|