@phnx-labs/agents-cli 1.20.76 → 1.20.78
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 +264 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +143 -9
- package/dist/commands/exec.js +87 -19
- package/dist/commands/repo.js +8 -0
- package/dist/commands/routines.js +275 -15
- package/dist/commands/secrets.js +65 -17
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +128 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-picker.d.ts +28 -1
- package/dist/commands/sessions-picker.js +222 -15
- package/dist/commands/sessions.d.ts +117 -4
- package/dist/commands/sessions.js +350 -62
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +48 -3
- package/dist/index.js +5 -6
- package/dist/lib/activity.d.ts +5 -4
- package/dist/lib/activity.js +450 -36
- package/dist/lib/agents.d.ts +0 -21
- package/dist/lib/agents.js +0 -37
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/cloud/session-index.js +2 -2
- package/dist/lib/daemon.d.ts +9 -32
- package/dist/lib/daemon.js +70 -84
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +16 -1
- package/dist/lib/devices/fleet.js +10 -2
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +27 -4
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/git.d.ts +13 -0
- package/dist/lib/git.js +102 -4
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hosts/dispatch.d.ts +9 -0
- package/dist/lib/hosts/dispatch.js +24 -4
- package/dist/lib/hosts/passthrough.js +7 -2
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +33 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +4 -0
- package/dist/lib/hosts/run-target.js +5 -1
- package/dist/lib/hosts/session-index.js +3 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +9 -0
- package/dist/lib/menubar/install-menubar.js +14 -0
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/overdue.d.ts +4 -5
- package/dist/lib/overdue.js +8 -41
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +68 -0
- package/dist/lib/routines.js +74 -7
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +156 -28
- package/dist/lib/sandbox.js +0 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +11 -4
- package/dist/lib/secrets/agent.js +46 -24
- package/dist/lib/secrets/bundles.d.ts +21 -7
- package/dist/lib/secrets/bundles.js +82 -38
- package/dist/lib/secrets/index.d.ts +11 -2
- package/dist/lib/secrets/index.js +21 -9
- package/dist/lib/secrets/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +16 -2
- package/dist/lib/secrets/session-store.js +118 -15
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/session/active.d.ts +17 -0
- package/dist/lib/session/active.js +105 -7
- package/dist/lib/session/db.d.ts +43 -0
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +28 -3
- package/dist/lib/session/discover.js +110 -14
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/parse.js +11 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.js +18 -16
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +94 -12
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/state.d.ts +7 -3
- package/dist/lib/state.js +20 -5
- package/dist/lib/types.d.ts +10 -0
- package/package.json +1 -1
|
@@ -31,7 +31,8 @@ import { emit } from '../events.js';
|
|
|
31
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
|
-
import {
|
|
34
|
+
import { GLOBAL_HARNESS } from './scope.js';
|
|
35
|
+
import { resolveSession, deleteSession } from './session-store.js';
|
|
35
36
|
import { createHash } from 'node:crypto';
|
|
36
37
|
const keychainStore = {
|
|
37
38
|
has: hasKeychainToken,
|
|
@@ -1020,15 +1021,19 @@ export function resolveBundleEnv(bundle, _opts = {}) {
|
|
|
1020
1021
|
* True when the current process is a background / non-interactive context that
|
|
1021
1022
|
* must NEVER raise a Keychain biometry prompt on the interactive user's screen —
|
|
1022
1023
|
* a prompt nobody is watching. Two signals, either sufficient:
|
|
1023
|
-
* - `AGENTS_RUNTIME` is `headless` or `
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1024
|
+
* - `AGENTS_RUNTIME` is `headless`, `teams`, or `terminal` — i.e. ANY agent
|
|
1025
|
+
* launch, interactive included, and inherited by everything spawned beneath
|
|
1026
|
+
* one (set on the child env by `agents run --headless`, scheduled routines,
|
|
1027
|
+
* teammates, and interactive runs — see exec.ts:430, runner.ts,
|
|
1028
|
+
* teams/agents.ts).
|
|
1026
1029
|
* - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
|
|
1027
1030
|
* stdio is redirected to a log — e.g. a release script run in the
|
|
1028
1031
|
* background as `( ... ) >log 2>&1 </dev/null`).
|
|
1029
1032
|
* `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
|
|
1030
|
-
* even in a non-TTY context. An
|
|
1031
|
-
*
|
|
1033
|
+
* even in a non-TTY context. An `eval "$(agents secrets export X)"` typed in a
|
|
1034
|
+
* PLAIN shell has no AGENTS_RUNTIME, so it is not classified headless and still
|
|
1035
|
+
* prompts. Run beneath an agent it inherits AGENTS_RUNTIME and resolves
|
|
1036
|
+
* broker-only — the agent, not the human, is the caller there.
|
|
1032
1037
|
*
|
|
1033
1038
|
* Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
|
|
1034
1039
|
* broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
|
|
@@ -1039,9 +1044,13 @@ export function resolveBundleEnv(bundle, _opts = {}) {
|
|
|
1039
1044
|
*
|
|
1040
1045
|
* A read in a macOS headless context resolves broker-only (agentOnly) and fails
|
|
1041
1046
|
* fast with an actionable error instead of hijacking Touch ID. This generalizes
|
|
1042
|
-
* the per-caller pattern
|
|
1047
|
+
* the per-caller broker-only pattern used across the headless secrets readers.
|
|
1043
1048
|
*/
|
|
1044
|
-
export function isHeadlessSecretsContext(env = process.env, platform = process.platform
|
|
1049
|
+
export function isHeadlessSecretsContext(env = process.env, platform = process.platform,
|
|
1050
|
+
// Injected so the TTY branch below is testable: it is the branch that decides a
|
|
1051
|
+
// plain human shell still prompts, which is this guard's entire safety argument,
|
|
1052
|
+
// and reading process.* directly made it unreachable from a test.
|
|
1053
|
+
tty = { stdin: process.stdin.isTTY, stdout: process.stdout.isTTY }) {
|
|
1045
1054
|
if (platform !== 'darwin')
|
|
1046
1055
|
return false; // no biometry prompt to suppress off-darwin
|
|
1047
1056
|
const override = env.AGENTS_SECRETS_NO_PROMPT;
|
|
@@ -1049,10 +1058,21 @@ export function isHeadlessSecretsContext(env = process.env, platform = process.p
|
|
|
1049
1058
|
return true;
|
|
1050
1059
|
if (override === '0')
|
|
1051
1060
|
return false;
|
|
1061
|
+
// Every AGENT-LAUNCH runtime resolves broker-only, interactive included.
|
|
1062
|
+
// `terminal` was missing, which made an agent terminal the one launch path
|
|
1063
|
+
// still allowed to pop Touch ID: exec.ts sets AGENTS_RUNTIME='terminal' for an
|
|
1064
|
+
// interactive run (exec.ts:430), that fell through to the TTY check below, and
|
|
1065
|
+
// a TTY meant "a human is watching, so prompting is fine". It is not fine —
|
|
1066
|
+
// opening a terminal is not a request to authenticate, and a launch that needs
|
|
1067
|
+
// a locked bundle should say so and point at `agents secrets unlock`, not grab
|
|
1068
|
+
// the fingerprint sensor. AGENTS_RUNTIME is INHERITED by everything spawned under
|
|
1069
|
+
// an agent, so `agents secrets export` run beneath one resolves broker-only too —
|
|
1070
|
+
// correctly: there the agent, not the human, is the caller. A plain shell carries
|
|
1071
|
+
// no AGENTS_RUNTIME, so a person running it themselves still gets the sheet.
|
|
1052
1072
|
const runtime = env.AGENTS_RUNTIME;
|
|
1053
|
-
if (runtime === 'headless' || runtime === 'teams')
|
|
1073
|
+
if (runtime === 'headless' || runtime === 'teams' || runtime === 'terminal')
|
|
1054
1074
|
return true;
|
|
1055
|
-
return !
|
|
1075
|
+
return !tty.stdin && !tty.stdout;
|
|
1056
1076
|
}
|
|
1057
1077
|
/**
|
|
1058
1078
|
* Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
|
|
@@ -1078,7 +1098,12 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1078
1098
|
// file-backed bundle has none to dedup. The never-unlocked path is a single
|
|
1079
1099
|
// stat (agentSocketExists) so it costs nothing when the agent isn't running.
|
|
1080
1100
|
if (backend === 'keychain' && !opts.noAgent && process.env.AGENTS_SECRETS_NO_AGENT !== '1') {
|
|
1081
|
-
|
|
1101
|
+
// The scope this reader asks under. Falls back to the GLOBAL scope, not to a
|
|
1102
|
+
// literal `'cli'` harness — the broker and the durable store both resolve
|
|
1103
|
+
// own-harness → global (bundleScopeChain), so an unscoped unlock is visible
|
|
1104
|
+
// here whether this process was launched by an agent or typed in a terminal.
|
|
1105
|
+
const harness = opts.agent || process.env.AGENTS_AGENT_NAME || GLOBAL_HARNESS;
|
|
1106
|
+
const hit = agentGetSync(name, harness);
|
|
1082
1107
|
if (hit) {
|
|
1083
1108
|
// The agent stores the FULL bundle env. Apply the same subset filter and
|
|
1084
1109
|
// expiry gate as the slow path — without this, `--secrets-keys X` would
|
|
@@ -1102,13 +1127,16 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1102
1127
|
// Touch ID. Serve from it and re-warm the broker, so a warm bundle stays warm
|
|
1103
1128
|
// across restart — this fixes BOTH the interactive re-prompt and the headless
|
|
1104
1129
|
// throw below (which now fires only when there is genuinely no session).
|
|
1105
|
-
const
|
|
1106
|
-
if (
|
|
1130
|
+
const resolved = resolveSession(name, Date.now(), harness);
|
|
1131
|
+
if (resolved) {
|
|
1132
|
+
const session = resolved.entry;
|
|
1107
1133
|
const filtered = filterAgentHitBySubsetAndExpiry({ bundle: session.bundle, env: session.env }, opts);
|
|
1108
1134
|
stampLastUsed(filtered.bundle);
|
|
1109
1135
|
// Re-warm the broker with the remaining TTL so later reads hit RAM and
|
|
1110
|
-
// `agents secrets status` is honest.
|
|
1111
|
-
|
|
1136
|
+
// `agents secrets status` is honest. Re-warm under the scope the grant was
|
|
1137
|
+
// MADE in (resolved.harness), never the asking scope — re-warming a global
|
|
1138
|
+
// grant as `claude` would silently narrow it for every other harness.
|
|
1139
|
+
agentAutoLoadSync(name, session.bundle, session.env, Math.max(1, session.expiresAt - Date.now()), resolved.harness);
|
|
1112
1140
|
emit('secrets.get', {
|
|
1113
1141
|
module: 'secrets',
|
|
1114
1142
|
bundle: name,
|
|
@@ -1120,32 +1148,30 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1120
1148
|
return filtered;
|
|
1121
1149
|
}
|
|
1122
1150
|
}
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
//
|
|
1133
|
-
//
|
|
1134
|
-
|
|
1151
|
+
// Never/no-ACL bundles remain prompt-free regardless. No agent launch — harness,
|
|
1152
|
+
// teammate, routine, or the always-on daemon — may raise the sheet itself.
|
|
1153
|
+
// Explicit opt-in ONLY — a deliberate NARROWING of the agent-triggered approval
|
|
1154
|
+
// added in RUSH-2032 (b99796f8 removed this throw so an agent could raise the
|
|
1155
|
+
// sheet itself; 4eeada68 generalized the daemon rule into `!interactiveUnlock`).
|
|
1156
|
+
// That default — true whenever an agent name was present — was the spec, not a
|
|
1157
|
+
// bug. It is unwanted: each keychain read runs in its own helper process, so the
|
|
1158
|
+
// biometric assertion never reuses and one agent launch meant one sheet per
|
|
1159
|
+
// bundle. `agentOnly` decides alone now; a human in a plain shell carries no
|
|
1160
|
+
// AGENTS_RUNTIME, so isHeadlessSecretsContext() is false, agentOnly is false, the
|
|
1161
|
+
// guard never fires, and they still get their prompt. No caller passes this flag;
|
|
1162
|
+
// it remains the seam for a future unlock path that wants the sheet on purpose.
|
|
1163
|
+
const interactiveUnlock = opts.interactiveUnlock ?? false;
|
|
1164
|
+
if (opts.agentOnly && backend === 'keychain' && !interactiveUnlock) {
|
|
1135
1165
|
let noAclBundle = false;
|
|
1136
1166
|
try {
|
|
1137
1167
|
noAclBundle = bundlePolicy(readBundle(name)) === 'never';
|
|
1138
1168
|
}
|
|
1139
|
-
catch {
|
|
1140
|
-
noAclBundle = false;
|
|
1141
|
-
}
|
|
1169
|
+
catch { /* fail closed */ }
|
|
1142
1170
|
if (!noAclBundle) {
|
|
1143
|
-
throw new Error(`Secrets bundle '${name}' is not unlocked in the secrets agent
|
|
1144
|
-
`
|
|
1145
|
-
`
|
|
1146
|
-
`first, or set AGENTS_SECRETS_NO_PROMPT=0 to force an interactive prompt.`);
|
|
1171
|
+
throw new Error(`Secrets bundle '${name}' is not unlocked in the secrets agent. ` +
|
|
1172
|
+
`Run 'agents secrets unlock ${name}' in a terminal first — an agent launch ` +
|
|
1173
|
+
`never raises a Touch ID sheet on its own.`);
|
|
1147
1174
|
}
|
|
1148
|
-
// never/no-ACL ⇒ fall through to the prompt-less keychain read below.
|
|
1149
1175
|
}
|
|
1150
1176
|
if (backend === 'file')
|
|
1151
1177
|
assertFileBackendUsable(name);
|
|
@@ -1164,12 +1190,20 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1164
1190
|
const reason = opts.caller
|
|
1165
1191
|
? `read ${name} secrets (for ${opts.caller})`
|
|
1166
1192
|
: `read ${name} secrets`;
|
|
1167
|
-
void reason;
|
|
1168
1193
|
// secretItems are storage names as enumerated (opaque hashed names on macOS
|
|
1169
1194
|
// with #316 hashing active, cleartext elsewhere); metaItem is cleartext and
|
|
1170
1195
|
// hashed inside getBatch. Deduped because the hashed enumeration spans the
|
|
1171
1196
|
// bundle's whole namespace.
|
|
1172
|
-
const fetched =
|
|
1197
|
+
const fetched = backend === 'keychain'
|
|
1198
|
+
? getKeychainTokens([...new Set([metaItem, ...secretItems])], {
|
|
1199
|
+
agent: opts.agent || process.env.AGENTS_AGENT_NAME || 'Agents CLI',
|
|
1200
|
+
bundle: name,
|
|
1201
|
+
reason: opts.caller ? `to ${opts.caller}` : reason,
|
|
1202
|
+
duration: opts.duration || humanUnlockDuration(secretsHoldMs()),
|
|
1203
|
+
defaultPolicy: secretsDefaultPolicy(),
|
|
1204
|
+
forceDuration: Boolean(opts.duration),
|
|
1205
|
+
})
|
|
1206
|
+
: store.getBatch([...new Set([metaItem, ...secretItems])]);
|
|
1173
1207
|
const json = fetched.get(metaItem);
|
|
1174
1208
|
if (json === undefined) {
|
|
1175
1209
|
// For a file-backed bundle the metadata item is on disk (that's how
|
|
@@ -1297,7 +1331,7 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1297
1331
|
bundlePolicy(bundle) === 'daily' &&
|
|
1298
1332
|
secretsAgentAutoEnabled() &&
|
|
1299
1333
|
canCacheResolvedEnv(bundle, selectedKeys, opts.keyMode)) {
|
|
1300
|
-
agentAutoLoadSync(name, bundle, env, secretsHoldMs());
|
|
1334
|
+
agentAutoLoadSync(name, bundle, env, secretsHoldMs(), opts.agent || process.env.AGENTS_AGENT_NAME || GLOBAL_HARNESS);
|
|
1301
1335
|
}
|
|
1302
1336
|
return { bundle, env };
|
|
1303
1337
|
}
|
|
@@ -1306,6 +1340,16 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1306
1340
|
throw err;
|
|
1307
1341
|
}
|
|
1308
1342
|
}
|
|
1343
|
+
export function humanUnlockDuration(ms) {
|
|
1344
|
+
const days = Math.round(ms / (24 * 60 * 60 * 1000));
|
|
1345
|
+
if (days >= 1)
|
|
1346
|
+
return `${days} day${days === 1 ? '' : 's'}`;
|
|
1347
|
+
const hours = Math.round(ms / (60 * 60 * 1000));
|
|
1348
|
+
if (hours >= 1)
|
|
1349
|
+
return `${hours} hour${hours === 1 ? '' : 's'}`;
|
|
1350
|
+
const minutes = Math.max(1, Math.round(ms / (60 * 1000)));
|
|
1351
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
1352
|
+
}
|
|
1309
1353
|
// Build a keychain ref expression from a bundle+key pair, for storage in the bundle metadata.
|
|
1310
1354
|
export function keychainRef(key) {
|
|
1311
1355
|
return `keychain:${key}`;
|
|
@@ -210,7 +210,16 @@ export declare function hasKeychainToken(item: string): boolean;
|
|
|
210
210
|
* call in the same process). For bundles, prefer getKeychainTokens() so a
|
|
211
211
|
* single biometric prompt covers every key in the batch.
|
|
212
212
|
*/
|
|
213
|
-
export
|
|
213
|
+
export interface KeychainReadContext {
|
|
214
|
+
agent?: string;
|
|
215
|
+
bundle?: string;
|
|
216
|
+
reason?: string;
|
|
217
|
+
duration?: string;
|
|
218
|
+
defaultPolicy?: 'daily' | 'always' | 'never';
|
|
219
|
+
forceDuration?: boolean;
|
|
220
|
+
}
|
|
221
|
+
export declare function keychainOperationPrompt(context?: KeychainReadContext): string;
|
|
222
|
+
export declare function getKeychainToken(item: string, context?: KeychainReadContext): string;
|
|
214
223
|
/**
|
|
215
224
|
* Batch-read multiple keychain items behind a single Touch ID prompt. The
|
|
216
225
|
* macOS helper holds one LAContext for its whole process: the first protected
|
|
@@ -221,7 +230,7 @@ export declare function getKeychainToken(item: string): string;
|
|
|
221
230
|
* On Linux or when a test backend is installed, falls back to individual
|
|
222
231
|
* lookups — no biometric prompt path on those platforms.
|
|
223
232
|
*/
|
|
224
|
-
export declare function getKeychainTokens(items: string[]): Map<string, string>;
|
|
233
|
+
export declare function getKeychainTokens(items: string[], context?: KeychainReadContext): Map<string, string>;
|
|
225
234
|
/** Store or update a secret value in the keychain/keyring. Device-local;
|
|
226
235
|
* biometry-gated on macOS. `opts.noAcl` (the `never` prompt-policy) writes our
|
|
227
236
|
* item WITHOUT the biometry access control so later reads are fully silent — it
|
|
@@ -707,14 +707,14 @@ export function hasKeychainToken(item) {
|
|
|
707
707
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
708
708
|
}).status === 0;
|
|
709
709
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
export function getKeychainToken(item) {
|
|
710
|
+
export function keychainOperationPrompt(context = {}) {
|
|
711
|
+
const agent = context.agent || 'Agents CLI';
|
|
712
|
+
const bundle = context.bundle ? ` the '${context.bundle}' bundle` : ' secrets';
|
|
713
|
+
const duration = context.duration ? ` for ${context.duration}` : '';
|
|
714
|
+
const reason = context.reason ? ` ${context.reason}` : '';
|
|
715
|
+
return `${agent} is requesting to unlock${bundle}${duration}${reason}.`;
|
|
716
|
+
}
|
|
717
|
+
export function getKeychainToken(item, context = {}) {
|
|
718
718
|
// Errors keep the requested (human-readable) name; the storage name may be
|
|
719
719
|
// an opaque hash.
|
|
720
720
|
const requested = item;
|
|
@@ -739,6 +739,11 @@ export function getKeychainToken(item) {
|
|
|
739
739
|
}
|
|
740
740
|
const bin = getKeychainHelperPath();
|
|
741
741
|
const result = spawnSync(bin, ['get', item, os.userInfo().username], {
|
|
742
|
+
env: {
|
|
743
|
+
...process.env,
|
|
744
|
+
AGENTS_KEYCHAIN_PROMPT: keychainOperationPrompt(context),
|
|
745
|
+
AGENTS_KEYCHAIN_PROMPT_BASE: keychainOperationPrompt({ ...context, duration: undefined }),
|
|
746
|
+
},
|
|
742
747
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
743
748
|
});
|
|
744
749
|
if (result.status === 1)
|
|
@@ -764,7 +769,7 @@ export function getKeychainToken(item) {
|
|
|
764
769
|
* On Linux or when a test backend is installed, falls back to individual
|
|
765
770
|
* lookups — no biometric prompt path on those platforms.
|
|
766
771
|
*/
|
|
767
|
-
export function getKeychainTokens(items) {
|
|
772
|
+
export function getKeychainTokens(items, context = {}) {
|
|
768
773
|
const result = new Map();
|
|
769
774
|
if (items.length === 0)
|
|
770
775
|
return result;
|
|
@@ -811,6 +816,13 @@ export function getKeychainTokens(items) {
|
|
|
811
816
|
}
|
|
812
817
|
const bin = getKeychainHelperPath();
|
|
813
818
|
const child = spawnSync(bin, ['get-batch', os.userInfo().username, ...storageItems], {
|
|
819
|
+
env: {
|
|
820
|
+
...process.env,
|
|
821
|
+
AGENTS_KEYCHAIN_PROMPT: keychainOperationPrompt(context),
|
|
822
|
+
AGENTS_KEYCHAIN_PROMPT_BASE: keychainOperationPrompt({ ...context, duration: undefined }),
|
|
823
|
+
AGENTS_KEYCHAIN_DEFAULT_POLICY: context.defaultPolicy || 'daily',
|
|
824
|
+
AGENTS_KEYCHAIN_FORCE_DURATION: context.forceDuration ? '1' : '0',
|
|
825
|
+
},
|
|
814
826
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
815
827
|
});
|
|
816
828
|
if (child.status === 4) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness scoping for an unlocked bundle — the shared vocabulary of the broker
|
|
3
|
+
* (agent.ts), the durable session store (session-store.ts), and the read path
|
|
4
|
+
* (bundles.ts).
|
|
5
|
+
*
|
|
6
|
+
* A grant is stored under a scope and read under a scope; the two must agree or
|
|
7
|
+
* the bundle is invisible. `agents secrets unlock --for <agent>` exists to NARROW
|
|
8
|
+
* a grant to one harness, so an unlock without it is global by definition.
|
|
9
|
+
*
|
|
10
|
+
* This module deliberately has NO imports: agent.ts and session-store.ts already
|
|
11
|
+
* import each other, and hanging the scope constants off either one would close
|
|
12
|
+
* that cycle — under ESM a cyclic `const` read can land in the temporal dead zone
|
|
13
|
+
* and throw at runtime even though tsc is happy.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Scope of an unlock that was not narrowed with `--for`: readable by every
|
|
17
|
+
* harness. Not a valid harness name, so it can never collide with one.
|
|
18
|
+
*/
|
|
19
|
+
export declare const GLOBAL_HARNESS = "*";
|
|
20
|
+
/**
|
|
21
|
+
* Scopes a reader consults, most specific first: its own harness, then the global
|
|
22
|
+
* grant. This is the resolution order of the scoped-grant model — a narrow
|
|
23
|
+
* `--for claude` unlock stays claude-only while an unscoped unlock serves
|
|
24
|
+
* everyone — not a fallback papering over a miss.
|
|
25
|
+
*/
|
|
26
|
+
export declare function bundleScopeChain(harness: string | undefined): string[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness scoping for an unlocked bundle — the shared vocabulary of the broker
|
|
3
|
+
* (agent.ts), the durable session store (session-store.ts), and the read path
|
|
4
|
+
* (bundles.ts).
|
|
5
|
+
*
|
|
6
|
+
* A grant is stored under a scope and read under a scope; the two must agree or
|
|
7
|
+
* the bundle is invisible. `agents secrets unlock --for <agent>` exists to NARROW
|
|
8
|
+
* a grant to one harness, so an unlock without it is global by definition.
|
|
9
|
+
*
|
|
10
|
+
* This module deliberately has NO imports: agent.ts and session-store.ts already
|
|
11
|
+
* import each other, and hanging the scope constants off either one would close
|
|
12
|
+
* that cycle — under ESM a cyclic `const` read can land in the temporal dead zone
|
|
13
|
+
* and throw at runtime even though tsc is happy.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Scope of an unlock that was not narrowed with `--for`: readable by every
|
|
17
|
+
* harness. Not a valid harness name, so it can never collide with one.
|
|
18
|
+
*/
|
|
19
|
+
export const GLOBAL_HARNESS = '*';
|
|
20
|
+
/**
|
|
21
|
+
* Scopes a reader consults, most specific first: its own harness, then the global
|
|
22
|
+
* grant. This is the resolution order of the scoped-grant model — a narrow
|
|
23
|
+
* `--for claude` unlock stays claude-only while an unscoped unlock serves
|
|
24
|
+
* everyone — not a fallback papering over a miss.
|
|
25
|
+
*/
|
|
26
|
+
export function bundleScopeChain(harness) {
|
|
27
|
+
const own = harness || GLOBAL_HARNESS;
|
|
28
|
+
return own === GLOBAL_HARNESS ? [GLOBAL_HARNESS] : [own, GLOBAL_HARNESS];
|
|
29
|
+
}
|
|
@@ -38,12 +38,14 @@ export interface SessionEntry {
|
|
|
38
38
|
expiresAt: number;
|
|
39
39
|
/** true only for `--durable` unlocks — survives SLEEP. */
|
|
40
40
|
sleepPersist: boolean;
|
|
41
|
+
harness?: string;
|
|
41
42
|
}
|
|
42
43
|
/** Metadata for one held bundle, kept in the index so we can rehydrate / prune
|
|
43
44
|
* without reading every blob. */
|
|
44
45
|
export interface SessionIndexMeta {
|
|
45
46
|
expiresAt: number;
|
|
46
47
|
sleepPersist: boolean;
|
|
48
|
+
harness?: string;
|
|
47
49
|
}
|
|
48
50
|
export interface SessionIndex {
|
|
49
51
|
bundles: Record<string, SessionIndexMeta>;
|
|
@@ -72,10 +74,22 @@ export declare function readIndex(): SessionIndex;
|
|
|
72
74
|
export declare function writeIndex(index: SessionIndex): void;
|
|
73
75
|
/** Persist one unlocked bundle: write its blob no-ACL and record it in the index. */
|
|
74
76
|
export declare function saveSession(name: string, entry: SessionEntry): void;
|
|
77
|
+
/**
|
|
78
|
+
* Read a session honoring the scope chain: the caller's own harness first, then
|
|
79
|
+
* the global grant (see bundleScopeChain in scope.ts). This is the durable-store
|
|
80
|
+
* twin of the broker's `get` — both must resolve identically, or a bundle would
|
|
81
|
+
* be readable from RAM but not after a restart.
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveSession(name: string, now?: number, harness?: string): {
|
|
84
|
+
entry: SessionEntry;
|
|
85
|
+
harness: string;
|
|
86
|
+
} | null;
|
|
75
87
|
/** Read one session blob by known name. Null when absent/expired/malformed. */
|
|
76
|
-
export declare function loadSession(name: string, now?: number): SessionEntry | null;
|
|
88
|
+
export declare function loadSession(name: string, now?: number, harness?: string): SessionEntry | null;
|
|
89
|
+
/** Remove every persisted harness grant for one bundle. */
|
|
90
|
+
export declare function deleteBundleSessions(name: string): void;
|
|
77
91
|
/** Delete one bundle's session blob and prune it from the index. */
|
|
78
|
-
export declare function deleteSession(name: string): void;
|
|
92
|
+
export declare function deleteSession(name: string, harness?: string): void;
|
|
79
93
|
/** Delete every session blob + the index (for `secrets lock --all`). */
|
|
80
94
|
export declare function deleteAllSessions(): void;
|
|
81
95
|
/** Rehydrate every unexpired session into `[name, entry]` pairs for the broker to
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* persistence is an optimization, not a correctness dependency.
|
|
26
26
|
*/
|
|
27
27
|
import { getKeychainToken, setKeychainToken, deleteKeychainToken, isKeychainBackendOverridden } from './index.js';
|
|
28
|
+
import { GLOBAL_HARNESS, bundleScopeChain } from './scope.js';
|
|
28
29
|
/** Prefix for all durable session items (device-local, no-ACL). */
|
|
29
30
|
export const SESSION_ITEM_PREFIX = 'agents-cli.session.';
|
|
30
31
|
/** Fixed-name index item — the ONLY thing we ever need to find without a known
|
|
@@ -82,8 +83,8 @@ export function removeEntry(index, name) {
|
|
|
82
83
|
function shouldPersist() {
|
|
83
84
|
return process.platform === 'darwin' || isKeychainBackendOverridden();
|
|
84
85
|
}
|
|
85
|
-
function sessionBlobItem(name) {
|
|
86
|
-
return `${SESSION_ITEM_PREFIX}${name}`;
|
|
86
|
+
function sessionBlobItem(name, harness) {
|
|
87
|
+
return `${SESSION_ITEM_PREFIX}${harness}.${name}`;
|
|
87
88
|
}
|
|
88
89
|
/** Read the session index by its fixed name. `{bundles:{}}` when absent/unreadable. */
|
|
89
90
|
export function readIndex() {
|
|
@@ -116,24 +117,40 @@ export function saveSession(name, entry) {
|
|
|
116
117
|
if (!shouldPersist())
|
|
117
118
|
return;
|
|
118
119
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
const harness = entry.harness || GLOBAL_HARNESS;
|
|
121
|
+
const key = `${harness}:${name}`;
|
|
122
|
+
setKeychainToken(sessionBlobItem(name, harness), JSON.stringify({ ...entry, harness }), { noAcl: true });
|
|
123
|
+
writeIndex(upsertEntry(readIndex(), key, { expiresAt: entry.expiresAt, sleepPersist: entry.sleepPersist, harness }));
|
|
121
124
|
}
|
|
122
125
|
catch {
|
|
123
126
|
/* best-effort — persistence is an optimization */
|
|
124
127
|
}
|
|
125
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Read a session honoring the scope chain: the caller's own harness first, then
|
|
131
|
+
* the global grant (see bundleScopeChain in scope.ts). This is the durable-store
|
|
132
|
+
* twin of the broker's `get` — both must resolve identically, or a bundle would
|
|
133
|
+
* be readable from RAM but not after a restart.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveSession(name, now = Date.now(), harness = GLOBAL_HARNESS) {
|
|
136
|
+
for (const scope of bundleScopeChain(harness)) {
|
|
137
|
+
const entry = loadSession(name, now, scope);
|
|
138
|
+
if (entry)
|
|
139
|
+
return { entry, harness: scope };
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
126
143
|
/** Read one session blob by known name. Null when absent/expired/malformed. */
|
|
127
|
-
export function loadSession(name, now = Date.now()) {
|
|
144
|
+
export function loadSession(name, now = Date.now(), harness = GLOBAL_HARNESS) {
|
|
128
145
|
if (!shouldPersist())
|
|
129
146
|
return null;
|
|
130
147
|
try {
|
|
131
|
-
const raw = getKeychainToken(sessionBlobItem(name));
|
|
148
|
+
const raw = getKeychainToken(sessionBlobItem(name, harness));
|
|
132
149
|
const entry = JSON.parse(raw);
|
|
133
150
|
if (!entry || typeof entry !== 'object' || !entry.bundle || !entry.env)
|
|
134
151
|
return null;
|
|
135
152
|
if (now >= entry.expiresAt) {
|
|
136
|
-
deleteSession(name); // drop expired on read
|
|
153
|
+
deleteSession(name, harness); // drop expired on read
|
|
137
154
|
return null;
|
|
138
155
|
}
|
|
139
156
|
return entry;
|
|
@@ -142,13 +159,32 @@ export function loadSession(name, now = Date.now()) {
|
|
|
142
159
|
return null;
|
|
143
160
|
}
|
|
144
161
|
}
|
|
162
|
+
/** Remove every persisted harness grant for one bundle. */
|
|
163
|
+
export function deleteBundleSessions(name) {
|
|
164
|
+
if (!shouldPersist())
|
|
165
|
+
return;
|
|
166
|
+
const index = readIndex();
|
|
167
|
+
let next = index;
|
|
168
|
+
for (const [key, meta] of Object.entries(index.bundles)) {
|
|
169
|
+
const scopedName = key.includes(':') ? key.split(':').slice(1).join(':') : key;
|
|
170
|
+
if (scopedName !== name)
|
|
171
|
+
continue;
|
|
172
|
+
const harness = meta.harness || GLOBAL_HARNESS;
|
|
173
|
+
try {
|
|
174
|
+
deleteKeychainToken(key.includes(':') ? sessionBlobItem(name, harness) : `${SESSION_ITEM_PREFIX}${name}`);
|
|
175
|
+
}
|
|
176
|
+
catch { /* keep going */ }
|
|
177
|
+
next = removeEntry(next, key);
|
|
178
|
+
}
|
|
179
|
+
writeIndex(next);
|
|
180
|
+
}
|
|
145
181
|
/** Delete one bundle's session blob and prune it from the index. */
|
|
146
|
-
export function deleteSession(name) {
|
|
182
|
+
export function deleteSession(name, harness = GLOBAL_HARNESS) {
|
|
147
183
|
if (!shouldPersist())
|
|
148
184
|
return;
|
|
149
185
|
try {
|
|
150
|
-
deleteKeychainToken(sessionBlobItem(name));
|
|
151
|
-
writeIndex(removeEntry(readIndex(), name));
|
|
186
|
+
deleteKeychainToken(sessionBlobItem(name, harness));
|
|
187
|
+
writeIndex(removeEntry(readIndex(), `${harness}:${name}`));
|
|
152
188
|
}
|
|
153
189
|
catch {
|
|
154
190
|
/* best-effort */
|
|
@@ -160,8 +196,12 @@ export function deleteAllSessions() {
|
|
|
160
196
|
return;
|
|
161
197
|
try {
|
|
162
198
|
for (const name of Object.keys(readIndex().bundles)) {
|
|
199
|
+
const meta = readIndex().bundles[name];
|
|
200
|
+
const bundleName = name.includes(':') ? name.split(':').slice(1).join(':') : name;
|
|
163
201
|
try {
|
|
164
|
-
deleteKeychainToken(
|
|
202
|
+
deleteKeychainToken(name.includes(':')
|
|
203
|
+
? sessionBlobItem(bundleName, meta.harness || GLOBAL_HARNESS)
|
|
204
|
+
: `${SESSION_ITEM_PREFIX}${bundleName}`);
|
|
165
205
|
}
|
|
166
206
|
catch { /* keep going */ }
|
|
167
207
|
}
|
|
@@ -180,19 +220,81 @@ export function rehydrateSessions(now = Date.now()) {
|
|
|
180
220
|
const out = [];
|
|
181
221
|
try {
|
|
182
222
|
const index = readIndex();
|
|
223
|
+
// One-time source migration from the pre-harness layout. Move each legacy
|
|
224
|
+
// bundle-name index/blob to the explicit cli scope, then delete the old blob.
|
|
225
|
+
let migratedIndex = index;
|
|
226
|
+
for (const [key, meta] of Object.entries(index.bundles)) {
|
|
227
|
+
if (key.includes(':'))
|
|
228
|
+
continue;
|
|
229
|
+
try {
|
|
230
|
+
const raw = getKeychainToken(`${SESSION_ITEM_PREFIX}${key}`);
|
|
231
|
+
const legacy = JSON.parse(raw);
|
|
232
|
+
setKeychainToken(sessionBlobItem(key, GLOBAL_HARNESS), JSON.stringify({ ...legacy, harness: GLOBAL_HARNESS }), { noAcl: true });
|
|
233
|
+
deleteKeychainToken(`${SESSION_ITEM_PREFIX}${key}`);
|
|
234
|
+
migratedIndex = upsertEntry(removeEntry(migratedIndex, key), `${GLOBAL_HARNESS}:${key}`, {
|
|
235
|
+
expiresAt: meta.expiresAt,
|
|
236
|
+
sleepPersist: meta.sleepPersist,
|
|
237
|
+
harness: GLOBAL_HARNESS,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
catch { /* malformed/absent legacy entry is pruned below */ }
|
|
241
|
+
}
|
|
242
|
+
// Second source migration: `cli`-scoped grants predate the global scope. `cli`
|
|
243
|
+
// was never a harness — it was the default when no AGENTS_AGENT_NAME was set,
|
|
244
|
+
// i.e. "unlocked from a terminal for general use", which is exactly the global
|
|
245
|
+
// grant. Rewriting them means an unlock a user already paid Touch ID for keeps
|
|
246
|
+
// working after upgrade instead of silently becoming unreadable to every agent.
|
|
247
|
+
for (const [key, meta] of Object.entries(migratedIndex.bundles)) {
|
|
248
|
+
if (!key.startsWith('cli:'))
|
|
249
|
+
continue;
|
|
250
|
+
const bundleName = key.slice('cli:'.length);
|
|
251
|
+
const globalKey = `${GLOBAL_HARNESS}:${bundleName}`;
|
|
252
|
+
// A global grant for this bundle can already exist: the user re-runs
|
|
253
|
+
// `unlock` on the new code (which writes the global scope) before the
|
|
254
|
+
// broker restarts to run this migration. The existing global grant is the
|
|
255
|
+
// NEWER one, so the stale `cli` entry is discarded, never merged over it —
|
|
256
|
+
// overwriting would restore a superseded token and, if the stale entry had
|
|
257
|
+
// expired, hand its expiry to the fresh grant so the prune below deletes a
|
|
258
|
+
// valid unlock outright.
|
|
259
|
+
if (migratedIndex.bundles[globalKey]) {
|
|
260
|
+
try {
|
|
261
|
+
deleteKeychainToken(sessionBlobItem(bundleName, 'cli'));
|
|
262
|
+
}
|
|
263
|
+
catch { /* already gone */ }
|
|
264
|
+
migratedIndex = removeEntry(migratedIndex, key);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const raw = getKeychainToken(sessionBlobItem(bundleName, 'cli'));
|
|
269
|
+
const legacy = JSON.parse(raw);
|
|
270
|
+
setKeychainToken(sessionBlobItem(bundleName, GLOBAL_HARNESS), JSON.stringify({ ...legacy, harness: GLOBAL_HARNESS }), { noAcl: true });
|
|
271
|
+
deleteKeychainToken(sessionBlobItem(bundleName, 'cli'));
|
|
272
|
+
migratedIndex = upsertEntry(removeEntry(migratedIndex, key), globalKey, {
|
|
273
|
+
expiresAt: meta.expiresAt,
|
|
274
|
+
sleepPersist: meta.sleepPersist,
|
|
275
|
+
harness: GLOBAL_HARNESS,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch { /* malformed/absent entry is pruned below */ }
|
|
279
|
+
}
|
|
280
|
+
writeIndex(migratedIndex);
|
|
281
|
+
index.bundles = migratedIndex.bundles;
|
|
183
282
|
const { survivors, expiredNames } = pruneExpired(index, now);
|
|
184
283
|
for (const name of expiredNames) {
|
|
284
|
+
const meta = index.bundles[name];
|
|
185
285
|
try {
|
|
186
|
-
deleteKeychainToken(sessionBlobItem(name));
|
|
286
|
+
deleteKeychainToken(sessionBlobItem(name.split(':').slice(1).join(':'), meta.harness || GLOBAL_HARNESS));
|
|
187
287
|
}
|
|
188
288
|
catch { /* keep going */ }
|
|
189
289
|
}
|
|
190
290
|
if (expiredNames.length)
|
|
191
291
|
writeIndex(survivors);
|
|
192
292
|
for (const name of selectRehydratable(survivors, now)) {
|
|
193
|
-
const
|
|
293
|
+
const meta = survivors.bundles[name];
|
|
294
|
+
const bundleName = name.split(':').slice(1).join(':');
|
|
295
|
+
const entry = loadSession(bundleName, now, meta.harness || GLOBAL_HARNESS);
|
|
194
296
|
if (entry)
|
|
195
|
-
out.push({ name, entry });
|
|
297
|
+
out.push({ name: bundleName, entry });
|
|
196
298
|
}
|
|
197
299
|
}
|
|
198
300
|
catch {
|
|
@@ -208,8 +310,9 @@ export function pruneSessionsOnSleep() {
|
|
|
208
310
|
try {
|
|
209
311
|
const { survivors, deletedNames } = pruneOnSleep(readIndex());
|
|
210
312
|
for (const name of deletedNames) {
|
|
313
|
+
const meta = readIndex().bundles[name];
|
|
211
314
|
try {
|
|
212
|
-
deleteKeychainToken(sessionBlobItem(name));
|
|
315
|
+
deleteKeychainToken(sessionBlobItem(name.split(':').slice(1).join(':'), meta.harness || GLOBAL_HARNESS));
|
|
213
316
|
}
|
|
214
317
|
catch { /* keep going */ }
|
|
215
318
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The subset of a `secrets.get` event this heuristic reads. */
|
|
2
|
+
export interface SecretGetRecord {
|
|
3
|
+
/** Bundle name the read targeted. */
|
|
4
|
+
bundle?: string;
|
|
5
|
+
/** Where the value came from: `agent` (broker hit) / `session` (durable
|
|
6
|
+
* unlock) are SILENT; anything else hit the keychain and thus prompted. */
|
|
7
|
+
source?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PromptedBundle {
|
|
10
|
+
name: string;
|
|
11
|
+
/** Number of prompting (keychain) reads in the window. */
|
|
12
|
+
count: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* From recent `secrets.get` records, find bundles read often enough via the
|
|
16
|
+
* keychain (i.e. NOT served silently by the broker or a durable session) to be
|
|
17
|
+
* worth unlocking once. A bundle currently `held` is already silent, so it is
|
|
18
|
+
* excluded; the caller further drops `never`/no-ACL bundles (which never prompt,
|
|
19
|
+
* so unlocking them is a no-op).
|
|
20
|
+
*
|
|
21
|
+
* @param records recent `secrets.get` events (any order)
|
|
22
|
+
* @param held bundle names the broker currently holds (silent reads)
|
|
23
|
+
* @param opts.minReads minimum prompting reads to surface a bundle (default 3)
|
|
24
|
+
*/
|
|
25
|
+
export declare function frequentlyPromptedBundles(records: SecretGetRecord[], held: Set<string>, opts?: {
|
|
26
|
+
minReads?: number;
|
|
27
|
+
}): PromptedBundle[];
|