@phnx-labs/agents-cli 1.22.4 → 1.22.5
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 +28 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/events.js +8 -0
- package/dist/commands/exec.js +7 -5
- package/dist/commands/inspect.js +18 -5
- package/dist/commands/models.d.ts +1 -1
- package/dist/commands/models.js +68 -9
- package/dist/commands/view.d.ts +12 -0
- package/dist/commands/view.js +2 -0
- package/dist/commands/webhook.js +8 -4
- package/dist/lib/browser/chrome.d.ts +12 -0
- package/dist/lib/browser/chrome.js +27 -11
- package/dist/lib/cloud/antigravity.js +8 -2
- package/dist/lib/crabbox/cli.js +72 -30
- package/dist/lib/event-stream.d.ts +3 -0
- package/dist/lib/event-stream.js +5 -0
- package/dist/lib/events.d.ts +2 -0
- package/dist/lib/events.js +6 -1
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +19 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tier-overrides.d.ts +43 -0
- package/dist/lib/model-tier-overrides.js +97 -0
- package/dist/lib/model-tiers.d.ts +13 -7
- package/dist/lib/model-tiers.js +104 -35
- package/dist/lib/models.d.ts +16 -0
- package/dist/lib/models.js +17 -3
- 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/index.d.ts +29 -0
- package/dist/lib/secrets/index.js +32 -0
- package/dist/lib/secrets/mcp.js +8 -4
- package/dist/lib/session/sync/config.d.ts +12 -11
- package/dist/lib/session/sync/config.js +40 -38
- package/dist/lib/share/config.js +4 -0
- package/dist/lib/types.d.ts +10 -0
- package/dist/lib/versions.js +35 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.5
|
|
4
|
+
|
|
5
|
+
- **`agents events` can now filter by `--session <id>` and `--bundle <name>` — trace which agent/session triggered a secret access.** Every event already carries the provenance `sessionId`, and secrets events carry the `bundle` in their payload, but neither was queryable: you could see *that* the `share` bundle was read, not *which session* read it. `--session` (wired to the engine's existing `sessionId` filter) and `--bundle` (a new payload filter across both the operational log and the activity stream) close that gap. `agents events --module secrets --bundle share --session <id>` answers "which agent read the share bundle" — the attribution the Touch ID storm investigation needed, since the macOS biometric sheet itself emits no event. Source: `apps/cli/src/lib/event-stream.ts`, `apps/cli/src/commands/events.ts`, `apps/cli/docs/06-observability.md`.
|
|
6
|
+
|
|
7
|
+
- **`agents feed post --blocked` records now survive the agent's next Stop.** The feed-publish hook cleared the per-session block file on every `Stop`/`SessionEnd`/`PostToolUse`, which silently dropped a declared (`--blocked`) block the moment the agent parked it and its turn ended — exactly when the owner still needs to see and answer it. Declared blocks are now exempt from the lifecycle clear and stay in `agents feed` until they are actually answered (a terminal reply or `recordAnswer`); question/notification/approval blocks still clear as before. Source: `apps/cli/src/lib/feed.ts`.
|
|
8
|
+
|
|
9
|
+
- **`agents add grok@latest` no longer lets a second grok account silently displace a first account's install.** Grok's version directories are keyed by upstream release number alone, not by account — so two different grok accounts that both self-update to the same identical release ("latest") were landing on the SAME on-disk `versions/grok/<version>/` directory. The second account's credentials would overwrite the first's in that shared directory, even though `agents view grok` still listed both accounts as separately installed. `installVersion` now detects this before finalizing the install: if the target version's home already has a signed-in account whose identity differs from the account driving the current update, it refuses with a clear error instead of silently corrupting the first account's install. Source: `apps/cli/src/lib/versions.ts`.
|
|
10
|
+
|
|
11
|
+
- **The stray "Agents CLI needs to authenticate to continue" Touch ID sheet now heals itself.** An old keychain helper (before the metadata/hmackey no-ACL migration fix) could re-stamp the internal HMAC-key item (`agents-cli.hmackey`) with a biometry ACL. That item is read *before every hashed keychain lookup*, so a damaged copy popped a generic, context-less Touch ID sheet on nearly any command that touched secrets — `agents devices list`, a background agent, a session hook — at seemingly random times. The migration fix stopped the re-stamping but never un-stamped an already-damaged item, and once hashed naming is active nothing re-stored it, so it prompted forever. Now, on the first read where hashing is active, an un-healed HMAC-key record is re-stored no-ACL exactly once (`healHmacKeyNoAclOnce`, gated by a `healedNoAcl` flag so it never churns the keychain afterward) — one last prompt on the read that heals it, then silent. Existing damaged machines can also fix it immediately with `agents secrets rekey`. Source: `apps/cli/src/lib/secrets/index.ts`.
|
|
12
|
+
|
|
13
|
+
- **`agents inspect` and `view --json` now report isolation honestly.** Found by diffing
|
|
14
|
+
every command's output between an isolated-only and a normal install. `inspect` printed
|
|
15
|
+
the bare-shim path unconditionally, so an isolated copy — which deliberately has no
|
|
16
|
+
shim, that being the guarantee — was shown sitting on the user's PATH; it now reports
|
|
17
|
+
`(none — isolated installs stay off PATH)` and `shim: null` in JSON. `inspect` also
|
|
18
|
+
showed only `default: false` for an isolated copy, hiding that it *was* the selected
|
|
19
|
+
one; it now carries `isolated` and `isolatedDefault`, and the header reads
|
|
20
|
+
`[isolated default]`. `view --json` had no isolation signal at all, so tooling could not
|
|
21
|
+
distinguish a sandboxed copy from one that owns the launcher and real config — its
|
|
22
|
+
version entries gain `isolated` and `isIsolatedDefault`. Source:
|
|
23
|
+
`apps/cli/src/commands/inspect.ts`, `apps/cli/src/commands/view.ts`.
|
|
24
|
+
|
|
25
|
+
- **`agents models` is now a scannable tier menu, and you can override a tier with a command.** The tier map (`cheap|default|best|ultra` → model + `~$/Mtok`) prints for every installed harness by default; the raw model list moved behind `--all`. When the auto-guess is wrong (subscription harnesses with no price signal), pin the right model without hand-editing YAML: `agents models tier set <agent[@version]> <tier> <model>` (e.g. `agents models tier set kimi best kimi-code/k3`), `tier clear`, `tier list`. Overrides live under `model.tiers` in `agents.yaml` (same selector shape as `run.defaults`) and resolve most-specific-first — `<agent>:<version>` → `<agent>:*` → auto; an overridden id a version doesn't ship falls back to auto, and `agents models` marks a pinned tier `[override]`. Ships a curated Kimi ladder (`k2.7-highspeed` < `k2.7-coding` < `k3`) so it's right by default. Also fixes two extraction bugs: bumps the model-catalog cache schema so a freshly-upgraded box re-extracts instead of serving a stale "No models extracted" for 24h, and stops the id-scan from listing bare legacy ids like `claude-opus-4` (#1892). Source: `apps/cli/src/lib/model-tier-overrides.ts`, `apps/cli/src/lib/model-tiers.ts`, `apps/cli/src/commands/models.ts`, `apps/cli/src/lib/models.ts`, `apps/cli/docs/model-tiers.md`.
|
|
26
|
+
|
|
27
|
+
- **`agents run --secrets <bundle>` never raises a Touch ID sheet on launch — even with a tty (a second storm source).** The `--secrets` injection read gated on `isHeadlessSecretsContext()`, which is FALSE for an interactive run — so an `agents run … --interactive` launch (the watchdog fires `agents run auto --interactive` every ~2 min via routine + menu-bar tick) could pop Touch ID for a keychain `hold` bundle, piling up helper sheets. An agent launch must never prompt regardless of tty (SEC-13): the read is now always `agentOnly` — it resolves from the broker (or a no-ACL bundle) and otherwise fails fast naming `agents secrets unlock <bundle>`, matching the behavior the code's own comment already described. Unchanged: the explicit `agents share` / `agents share setup` commands (`readWriteTokenFromBundle`, `readCloudflareCreds`) still honor the interactive/headless gate — those are user-initiated, not agent launches. Source: `apps/cli/src/commands/exec.ts`.
|
|
28
|
+
|
|
29
|
+
- **Background and read-only secret reads never raise Touch ID (SEC-13).** Every non-user-initiated secret read now resolves `agentOnly` — from the secrets broker or a no-ACL bundle — and, on a locked `hold`/`always` keychain bundle, fails fast naming `agents secrets unlock <bundle>` instead of popping a Touch ID sheet on the interactive launcher. This closes the rest of the per-launch prompt storm that #1905 fixed only for `--secrets` injection. Covered: session-sync (`r2.backups`, read on every daemon cycle — degrades to no-transport when locked, re-checked each cycle for fast pickup once unlocked), the `--lease` crabbox provider token (resolved once up front and memoized, so a locked bundle fails loud once and the ready-wait poll never re-issues the read), browser-profile secrets on launch (skipped silently, launch proceeds), cloud dispatch (`cloud:antigravity`, fails loud with the unlock hint), the `webhook serve` receiver, and the `get_secret` MCP tool (throws as the tool error). Unchanged and still interactive-gated: the user-initiated `agents secrets get/export`, `agents browser type`, `agents exec --secrets`, the `ssh` askpass, and the explicit `agents share` / `agents share setup` provisioning reads — a human running those in a plain terminal still gets a prompt. Source: `apps/cli/src/lib/session/sync/config.ts`, `apps/cli/src/lib/crabbox/cli.ts`, `apps/cli/src/lib/browser/chrome.ts`, `apps/cli/src/lib/cloud/antigravity.ts`, `apps/cli/src/lib/secrets/mcp.ts`, `apps/cli/src/commands/webhook.ts`.
|
|
30
|
+
|
|
3
31
|
## 1.22.4
|
|
4
32
|
|
|
5
33
|
- **Background processes no longer storm macOS Touch ID sheets (secrets-touchid-storm).**
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/events.js
CHANGED
|
@@ -192,6 +192,8 @@ export function registerEventsCommand(program) {
|
|
|
192
192
|
.option('--command <path>', 'Only this command path — prefix match (e.g. "teams create")')
|
|
193
193
|
.option('--event <type>', 'Only this typed event (repeatable, e.g. secrets.get, secrets.unlocked, pr.opened)', collect, [])
|
|
194
194
|
.option('--agent <name>', 'Only events tagged with this agent')
|
|
195
|
+
.option('--session <id>', 'Only events from this session (the provenance sessionId) — e.g. trace which session read a secret')
|
|
196
|
+
.option('--bundle <name>', 'Only events carrying this bundle in their payload — e.g. `--module secrets --bundle share` for every read of the share bundle')
|
|
195
197
|
.option('--since <time>', 'Only events newer than this (e.g. 2h, 7d, or ISO date)')
|
|
196
198
|
.option('--audit', 'Operational events only (skip agent activity)')
|
|
197
199
|
.option('--limit <n>', 'Max records to show; 0 for no cap (default 50)', '50')
|
|
@@ -204,6 +206,10 @@ Examples:
|
|
|
204
206
|
agents events --audit Operational events only (secrets / teams / ...)
|
|
205
207
|
agents events --event pr.opened --since 7d
|
|
206
208
|
agents events --module secrets Every secret accessed or revealed
|
|
209
|
+
agents events --module secrets --bundle share
|
|
210
|
+
Every read of the share bundle (which agent, which session)
|
|
211
|
+
agents events --bundle share --session <id>
|
|
212
|
+
Trace one session's reads of a bundle
|
|
207
213
|
agents events -f Live tail (operational)
|
|
208
214
|
agents events --event pr.opened --since 30d --limit 0 --json
|
|
209
215
|
Every match — use --limit 0 whenever you
|
|
@@ -228,6 +234,8 @@ Examples:
|
|
|
228
234
|
startDate,
|
|
229
235
|
eventTypes: options.event && options.event.length ? options.event : undefined,
|
|
230
236
|
agent: options.agent,
|
|
237
|
+
sessionId: options.session,
|
|
238
|
+
bundle: options.bundle,
|
|
231
239
|
command: options.command,
|
|
232
240
|
module: options.module,
|
|
233
241
|
limit: limit === undefined ? undefined : limit + 1,
|
package/dist/commands/exec.js
CHANGED
|
@@ -1567,7 +1567,7 @@ export function registerRunCommand(program) {
|
|
|
1567
1567
|
await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
|
|
1568
1568
|
process.exit(resumeExit);
|
|
1569
1569
|
}
|
|
1570
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported
|
|
1570
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES, collectHarnessCandidates, pickHarnessWeighted, classifyHarnessCandidates, formatHarnessPickBanner, formatNoHealthyHarnessError, formatNoHealthyAccountError }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports }, { shareRuntimeEnv },] = await Promise.all([
|
|
1571
1571
|
import('../lib/exec.js'),
|
|
1572
1572
|
import('../lib/agents.js'),
|
|
1573
1573
|
import('../lib/profiles.js'),
|
|
@@ -2239,11 +2239,13 @@ export function registerRunCommand(program) {
|
|
|
2239
2239
|
agent,
|
|
2240
2240
|
keys: secretsKeysSubset,
|
|
2241
2241
|
allowExpired: options.allowExpired,
|
|
2242
|
-
// The harness identity scopes any cached grant.
|
|
2243
|
-
// requesting agent wait for interactive approval — an agent launch
|
|
2242
|
+
// The harness identity scopes any cached grant. An agent launch
|
|
2244
2243
|
// resolves broker-only and fails fast naming
|
|
2245
|
-
// `agents secrets unlock <bundle>` (bundles.ts:interactiveUnlock)
|
|
2246
|
-
|
|
2244
|
+
// `agents secrets unlock <bundle>` (bundles.ts:interactiveUnlock) — it
|
|
2245
|
+
// MUST NOT raise a Touch ID sheet regardless of tty (SEC-13). Gating on
|
|
2246
|
+
// isHeadlessSecretsContext() left `--interactive` launches (the watchdog's
|
|
2247
|
+
// `agents run auto --interactive`) able to prompt, piling up helper sheets.
|
|
2248
|
+
agentOnly: true,
|
|
2247
2249
|
});
|
|
2248
2250
|
const entries = describeBundle(bundle);
|
|
2249
2251
|
const counts = {};
|
package/dist/commands/inspect.js
CHANGED
|
@@ -24,7 +24,7 @@ import { supports } from '../lib/capabilities.js';
|
|
|
24
24
|
import { resolveConfiguredModel } from '../lib/models.js';
|
|
25
25
|
import { resolveSingleAgentTarget, AgentSpecError } from '../lib/agent-spec/index.js';
|
|
26
26
|
import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
|
|
27
|
-
import { getVersionHomePath } from '../lib/versions.js';
|
|
27
|
+
import { getVersionHomePath, isVersionIsolated, getIsolatedDefault, } from '../lib/versions.js';
|
|
28
28
|
import { getShimsDir, getVersionedAliasPath } from '../lib/shims.js';
|
|
29
29
|
import { getAgentResources, listResources, } from '../lib/resources.js';
|
|
30
30
|
import { listHookEntriesFromDir } from '../lib/hooks.js';
|
|
@@ -560,8 +560,19 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
560
560
|
const cliState = await getCliState(agent).catch(() => null);
|
|
561
561
|
const configSymlink = path.join(os.homedir(), `.${agent}`);
|
|
562
562
|
const configTarget = readSymlinkSafe(configSymlink);
|
|
563
|
-
|
|
563
|
+
// Report the bare shim only when it is actually there. An isolated install
|
|
564
|
+
// deliberately has none — that is the promise — so printing the path
|
|
565
|
+
// unconditionally told the user this copy sits on their PATH when it does not.
|
|
566
|
+
// Same failure mode as `agents view` claiming an isolated copy was `(global)`.
|
|
567
|
+
const shimPathIfPresent = fs.existsSync(path.join(getShimsDir(), AGENTS[agent].cliCommand))
|
|
568
|
+
? path.join(getShimsDir(), AGENTS[agent].cliCommand)
|
|
569
|
+
: null;
|
|
564
570
|
const aliasPath = getVersionedAliasPath(agent, version);
|
|
571
|
+
const isolated = isVersionIsolated(agent, version);
|
|
572
|
+
// `default` means the GLOBAL default, which an isolated copy can never be. Report
|
|
573
|
+
// the isolated pointer separately rather than leaving it invisible: it is what a
|
|
574
|
+
// bare `agents run <agent>` resolves to, so "default: false" alone is misleading.
|
|
575
|
+
const isIsolatedDefault = isolated && getIsolatedDefault(agent) === version;
|
|
565
576
|
const capabilities = collectCapabilities(agent, version);
|
|
566
577
|
const itemsByKind = options.brief ? null : collectItemsByKind(agent, versionHome);
|
|
567
578
|
const hookByScript = options.brief ? null : hookManifestByScript(loadCentralHookManifest());
|
|
@@ -574,9 +585,11 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
574
585
|
agent,
|
|
575
586
|
version,
|
|
576
587
|
default: isDefault,
|
|
588
|
+
isolated,
|
|
589
|
+
isolatedDefault: isIsolatedDefault,
|
|
577
590
|
home: versionHome,
|
|
578
591
|
configSymlink: configTarget ? { from: configSymlink, to: configTarget } : null,
|
|
579
|
-
shim:
|
|
592
|
+
shim: shimPathIfPresent,
|
|
580
593
|
alias: aliasPath,
|
|
581
594
|
strategy,
|
|
582
595
|
installedShim: cliState?.installed === true ? cliState.path : null,
|
|
@@ -590,12 +603,12 @@ async function renderSummary(agent, version, versionHome, options) {
|
|
|
590
603
|
// Plain text — model sits right beside the version, same priority (no label).
|
|
591
604
|
const configuredModel = resolveConfiguredModel(agent, version)?.model;
|
|
592
605
|
const modelPart = configuredModel ? ' ' + chalk.yellow(configuredModel) : '';
|
|
593
|
-
const head = `${chalk.bold(agent)} ${chalk.gray('@')} ${chalk.cyan(version)}${modelPart}${isDefault ? ' ' + chalk.green('[default]') : ''}`;
|
|
606
|
+
const head = `${chalk.bold(agent)} ${chalk.gray('@')} ${chalk.cyan(version)}${modelPart}${isDefault ? ' ' + chalk.green('[default]') : ''}${isolated ? ' ' + chalk.gray(isIsolatedDefault ? '[isolated default]' : '[isolated]') : ''}`;
|
|
594
607
|
console.log('\n' + head + '\n');
|
|
595
608
|
const rows = [
|
|
596
609
|
['install', versionHome],
|
|
597
610
|
['config', configTarget ? `${configSymlink} ${chalk.gray('→')} ${configTarget}` : chalk.gray('(no symlink)')],
|
|
598
|
-
['shim',
|
|
611
|
+
['shim', shimPathIfPresent ?? chalk.gray(isolated ? '(none — isolated installs stay off PATH)' : '(none)')],
|
|
599
612
|
['alias', aliasPath],
|
|
600
613
|
['strategy', strategy],
|
|
601
614
|
];
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* each agent's CLI bundle and displays IDs, aliases, and metadata.
|
|
7
7
|
*/
|
|
8
8
|
import type { Command } from 'commander';
|
|
9
|
-
/** Register the
|
|
9
|
+
/** Register the `agents models` command + its `tier` override subcommands. */
|
|
10
10
|
export declare function registerModelsCommand(program: Command): void;
|
|
11
11
|
export declare function formatModelSourceLine(kind: string, sourcePath: string, width?: number): string;
|
|
12
12
|
export declare function formatModelAliasLines(parts: string[], width?: number): string[];
|
package/dist/commands/models.js
CHANGED
|
@@ -12,6 +12,7 @@ import { resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.j
|
|
|
12
12
|
import { listInstalledVersions, getGlobalDefault, resolveVersion, resolveVersionAlias } from '../lib/versions.js';
|
|
13
13
|
import { getModelCatalog, locateModelSource } from '../lib/models.js';
|
|
14
14
|
import { resolveTierMap, MODEL_TIERS } from '../lib/model-tiers.js';
|
|
15
|
+
import { setTierOverride, clearTierOverride, listTierOverrides } from '../lib/model-tier-overrides.js';
|
|
15
16
|
import { getModelPricing } from '../lib/pricing/index.js';
|
|
16
17
|
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
17
18
|
import { wrapJoined } from './inspect.js';
|
|
@@ -37,14 +38,15 @@ function fallbackPathVersion(agent) {
|
|
|
37
38
|
const m = real.match(/\/versions\/([^/]+)\//);
|
|
38
39
|
return m ? m[1] : 'installed';
|
|
39
40
|
}
|
|
40
|
-
/** Register the
|
|
41
|
+
/** Register the `agents models` command + its `tier` override subcommands. */
|
|
41
42
|
export function registerModelsCommand(program) {
|
|
42
|
-
program
|
|
43
|
-
.command('models [agentSpec]'
|
|
44
|
-
.description('
|
|
45
|
-
.option('--
|
|
46
|
-
.option('--
|
|
47
|
-
.option('--
|
|
43
|
+
const models = program
|
|
44
|
+
.command('models [agentSpec]')
|
|
45
|
+
.description('Show the cost-tier map (cheap|default|best|ultra) for installed harnesses; pin overrides with `tier set`.')
|
|
46
|
+
.option('--all', 'Show the full raw model catalog, not just the tier map')
|
|
47
|
+
.option('--cloud', 'Show per-cloud IDs (Claude only; implies --all)')
|
|
48
|
+
.option('--reasoning', 'Show reasoning levels per model (Codex only; implies --all)')
|
|
49
|
+
.option('--json', 'Output catalog + tiers as JSON')
|
|
48
50
|
.action(async (agentSpec, options) => {
|
|
49
51
|
const targets = await resolveTargets(agentSpec);
|
|
50
52
|
if (targets.length === 0)
|
|
@@ -59,13 +61,63 @@ export function registerModelsCommand(program) {
|
|
|
59
61
|
console.log(JSON.stringify(out, null, 2));
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
64
|
+
const all = options.all || options.cloud || options.reasoning;
|
|
62
65
|
let printed = 0;
|
|
63
66
|
for (const { agent, version, isDefault } of targets) {
|
|
64
67
|
if (printed > 0)
|
|
65
68
|
console.log();
|
|
66
|
-
printCatalog(agent, version, isDefault, options);
|
|
69
|
+
printCatalog(agent, version, isDefault, { ...options, all });
|
|
67
70
|
printed++;
|
|
68
71
|
}
|
|
72
|
+
if (!agentSpec && !all) {
|
|
73
|
+
console.log(chalk.gray('\n `agents models <agent>` for one harness · `--all` for the full model list'));
|
|
74
|
+
console.log(chalk.gray(' `agents models tier set <agent> <tier> <model>` to override a tier'));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
// Override subcommands. These WRITE agents.yaml so the user never hand-edits it;
|
|
78
|
+
// resolution is exact `<agent>@<version>` over `<agent>` over the auto guess.
|
|
79
|
+
const tier = models
|
|
80
|
+
.command('tier')
|
|
81
|
+
.description('Override which model a cost tier resolves to (per harness, or per agent@version).');
|
|
82
|
+
tier
|
|
83
|
+
.command('set <selector> <tier> <model>')
|
|
84
|
+
.description('Pin a tier to a model. selector: <agent> or <agent>@<version> (e.g. kimi, kimi@0.19.2).')
|
|
85
|
+
.action((selector, tierName, model) => {
|
|
86
|
+
try {
|
|
87
|
+
const entry = setTierOverride(selector, tierName, model);
|
|
88
|
+
console.log(chalk.green(`✓ ${entry.selector} ${chalk.cyan(tierName.toLowerCase())} → ${chalk.bold(model)}`));
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error(chalk.red(err.message));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
tier
|
|
96
|
+
.command('clear <selector> [tier]')
|
|
97
|
+
.description('Clear one tier (or all tiers) back to the auto guess.')
|
|
98
|
+
.action((selector, tierName) => {
|
|
99
|
+
try {
|
|
100
|
+
const changed = clearTierOverride(selector, tierName);
|
|
101
|
+
console.log(changed ? chalk.green(`✓ cleared ${selector}${tierName ? ` ${tierName}` : ''}`) : chalk.gray('nothing to clear'));
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.error(chalk.red(err.message));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
tier
|
|
109
|
+
.command('list')
|
|
110
|
+
.description('List all configured tier overrides.')
|
|
111
|
+
.action(() => {
|
|
112
|
+
const entries = listTierOverrides();
|
|
113
|
+
if (entries.length === 0) {
|
|
114
|
+
console.log(chalk.gray('No tier overrides — every tier uses the auto guess. Set one with `agents models tier set`.'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const e of entries) {
|
|
118
|
+
const parts = Object.entries(e.tiers).map(([t, m]) => `${chalk.cyan(t)}=${m}`);
|
|
119
|
+
console.log(`${chalk.bold(e.selector.padEnd(18))} ${parts.join(' ')}`);
|
|
120
|
+
}
|
|
69
121
|
});
|
|
70
122
|
}
|
|
71
123
|
/** Resolve the agent spec into one or more (agent, version) pairs to inspect. */
|
|
@@ -144,6 +196,12 @@ function printCatalog(agent, version, isDefault, options) {
|
|
|
144
196
|
console.log(chalk.yellow(` No models extracted from ${src.kind} at ${src.path}.`));
|
|
145
197
|
return;
|
|
146
198
|
}
|
|
199
|
+
// The tier map above is what an agent reads. Keep the raw catalog behind --all
|
|
200
|
+
// so `agents models` stays a scannable menu instead of a 30-id dump.
|
|
201
|
+
if (!options.all) {
|
|
202
|
+
console.log(chalk.gray(` ${catalog.models.length} models · \`agents models ${agent} --all\` for the full list`));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
147
205
|
console.log(chalk.gray(formatModelSourceLine(src.kind, shortPath(src.path))));
|
|
148
206
|
if (Object.keys(catalog.aliases).length > 0) {
|
|
149
207
|
const parts = Object.entries(catalog.aliases).map(([alias, id]) => `${chalk.cyan(alias)}=${id}`);
|
|
@@ -202,7 +260,8 @@ function printTiers(agent, version) {
|
|
|
202
260
|
continue;
|
|
203
261
|
const eff = r.effort ? chalk.gray(` @${r.effort}`) : '';
|
|
204
262
|
const clamp = r.clampedFrom ? chalk.gray(' (clamped)') : '';
|
|
205
|
-
|
|
263
|
+
const over = r.source === 'override' ? chalk.yellow(' [override]') : '';
|
|
264
|
+
console.log(` ${chalk.cyan(t.padEnd(8))} ${chalk.bold(r.model)}${eff}${priceLabel(r.model)}${clamp}${over}`);
|
|
206
265
|
}
|
|
207
266
|
console.log();
|
|
208
267
|
}
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -51,6 +51,18 @@ export declare function renderHarnessBlocks(harnesses: ProfileSummary[], install
|
|
|
51
51
|
export interface ViewJsonVersion {
|
|
52
52
|
version: string;
|
|
53
53
|
isDefault: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Installed with `--isolated`. The human view has shown this since the isolated
|
|
56
|
+
* tag landed, but JSON carried no signal at all, so tooling could not tell a
|
|
57
|
+
* sandboxed copy from one that owns the user's launcher and real config.
|
|
58
|
+
*/
|
|
59
|
+
isolated: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The isolated copy a bare `agents run <agent>` resolves to. Separate from
|
|
62
|
+
* `isDefault`, which is the GLOBAL default an isolated version can never be —
|
|
63
|
+
* reporting only `isDefault: false` hid the fact that this one is selected.
|
|
64
|
+
*/
|
|
65
|
+
isIsolatedDefault: boolean;
|
|
54
66
|
signedIn: boolean;
|
|
55
67
|
email: string | null;
|
|
56
68
|
accountId?: string | null;
|
package/dist/commands/view.js
CHANGED
|
@@ -1242,6 +1242,8 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
|
1242
1242
|
const entry = {
|
|
1243
1243
|
version,
|
|
1244
1244
|
isDefault: version === globalDefault,
|
|
1245
|
+
isolated: isVersionIsolated(agentId, version),
|
|
1246
|
+
isIsolatedDefault: getIsolatedDefault(agentId) === version,
|
|
1245
1247
|
signedIn: info.signedIn,
|
|
1246
1248
|
email: info.email,
|
|
1247
1249
|
accountId: info.accountId,
|
package/dist/commands/webhook.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { readAndResolveBundleEnv
|
|
3
|
+
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
4
4
|
import { createFileDeliveryStore, startWebhookServer } from '../lib/triggers/webhook.js';
|
|
5
5
|
import { getRuntimeStateDir } from '../lib/state.js';
|
|
6
6
|
const DEFAULT_HOST = '127.0.0.1';
|
|
@@ -12,9 +12,13 @@ function positiveInt(value, fallback) {
|
|
|
12
12
|
function readWebhookSecrets(bundleName) {
|
|
13
13
|
const { env } = readAndResolveBundleEnv(bundleName, {
|
|
14
14
|
caller: 'webhook serve',
|
|
15
|
-
// `webhook serve` is a long-running background server
|
|
16
|
-
//
|
|
17
|
-
agentOnly:
|
|
15
|
+
// `webhook serve` is a long-running background server started to receive
|
|
16
|
+
// signed webhooks, not a human at a Touch ID sheet — so the read is always
|
|
17
|
+
// `agentOnly` (SEC-13: never pop biometry on its own). A `never`/no-ACL or
|
|
18
|
+
// broker-held bundle resolves silently; a locked bundle THROWS the actionable
|
|
19
|
+
// "unlock <name>" message, which propagates and fails the start LOUD rather
|
|
20
|
+
// than popping an unanswerable prompt.
|
|
21
|
+
agentOnly: true,
|
|
18
22
|
});
|
|
19
23
|
const secrets = {};
|
|
20
24
|
if (env.GITHUB_WEBHOOK_SECRET)
|
|
@@ -38,6 +38,18 @@ export interface LaunchResult {
|
|
|
38
38
|
port: number;
|
|
39
39
|
wsUrl: string;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a browser-profile secrets bundle into an env map for the child, or an
|
|
43
|
+
* EMPTY map when the bundle is absent, locked, or otherwise unreadable — never a
|
|
44
|
+
* throw and never a prompt. Injecting profile secrets on launch is a BACKGROUND
|
|
45
|
+
* read (the agent is spawning the browser, not a human at a Touch ID sheet), so
|
|
46
|
+
* the read is `agentOnly` (SEC-13: never pop biometry on its own): a `never`/no-ACL
|
|
47
|
+
* or broker-held bundle resolves silently; a locked `hold`/`always` bundle throws,
|
|
48
|
+
* and we swallow it so the launch proceeds without those secrets (an agent that
|
|
49
|
+
* needs them can `agents secrets unlock <bundle>`). Exported so the no-prompt
|
|
50
|
+
* behavior is testable on the real keychain path without spawning a browser.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveProfileSecretsEnv(secrets?: string): Record<string, string>;
|
|
41
53
|
export declare function launchBrowser(profileName: string, browserType: BrowserType, port: number, options?: ChromeOptions, secrets?: string, customBinary?: string, isElectron?: boolean): Promise<LaunchResult>;
|
|
42
54
|
export declare function attachToChrome(port: number): Promise<string>;
|
|
43
55
|
export declare function killChrome(pid: number): void;
|
|
@@ -4,7 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import { getProfileRuntimeDir } from './profiles.js';
|
|
6
6
|
import { discoverBrowserWsUrl, registerPipeTransport } from './cdp.js';
|
|
7
|
-
import { readAndResolveBundleEnv, bundleExists
|
|
7
|
+
import { readAndResolveBundleEnv, bundleExists } from '../secrets/bundles.js';
|
|
8
8
|
import { writeProfileRuntime, readProfileRuntime } from './runtime-state.js';
|
|
9
9
|
// Windows install roots. Resolve from the environment (fall back to the usual
|
|
10
10
|
// defaults) so per-user installs under %LOCALAPPDATA% and 64-bit Program Files
|
|
@@ -225,6 +225,29 @@ export function listInstalledBrowsers(platform = os.platform()) {
|
|
|
225
225
|
}
|
|
226
226
|
return found;
|
|
227
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Resolve a browser-profile secrets bundle into an env map for the child, or an
|
|
230
|
+
* EMPTY map when the bundle is absent, locked, or otherwise unreadable — never a
|
|
231
|
+
* throw and never a prompt. Injecting profile secrets on launch is a BACKGROUND
|
|
232
|
+
* read (the agent is spawning the browser, not a human at a Touch ID sheet), so
|
|
233
|
+
* the read is `agentOnly` (SEC-13: never pop biometry on its own): a `never`/no-ACL
|
|
234
|
+
* or broker-held bundle resolves silently; a locked `hold`/`always` bundle throws,
|
|
235
|
+
* and we swallow it so the launch proceeds without those secrets (an agent that
|
|
236
|
+
* needs them can `agents secrets unlock <bundle>`). Exported so the no-prompt
|
|
237
|
+
* behavior is testable on the real keychain path without spawning a browser.
|
|
238
|
+
*/
|
|
239
|
+
export function resolveProfileSecretsEnv(secrets) {
|
|
240
|
+
if (!secrets || !bundleExists(secrets))
|
|
241
|
+
return {};
|
|
242
|
+
try {
|
|
243
|
+
const { env } = readAndResolveBundleEnv(secrets, { caller: 'browser profile', agentOnly: true });
|
|
244
|
+
return env;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Bundle locked or failed to resolve — launch without secrets (no prompt).
|
|
248
|
+
return {};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
228
251
|
export async function launchBrowser(profileName, browserType, port, options = {}, secrets, customBinary,
|
|
229
252
|
// `electron: true` distinguishes Notion / VS Code-style apps from
|
|
230
253
|
// regular Chrome — purely informational, stored in meta.json so the
|
|
@@ -279,16 +302,9 @@ isElectron = false) {
|
|
|
279
302
|
: []),
|
|
280
303
|
...(options.args || []),
|
|
281
304
|
];
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const { env: bundleEnv } = readAndResolveBundleEnv(secrets, { caller: 'browser profile', agentOnly: isHeadlessSecretsContext() });
|
|
286
|
-
env = { ...env, ...bundleEnv };
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
// Bundle failed to resolve, continue without secrets
|
|
290
|
-
}
|
|
291
|
-
}
|
|
305
|
+
// Profile secrets: agentOnly (SEC-13) — a locked bundle resolves to an empty map
|
|
306
|
+
// (launch proceeds without them), never a Touch ID prompt. See resolveProfileSecretsEnv.
|
|
307
|
+
const env = { ...process.env, ...resolveProfileSecretsEnv(secrets) };
|
|
292
308
|
const child = spawn(browserPath, args, {
|
|
293
309
|
detached: true,
|
|
294
310
|
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* back to GEMINI_API_KEY / GOOGLE_API_KEY in the environment.
|
|
21
21
|
*/
|
|
22
22
|
import { resolveDispatchRepos, normalizeProviderStatus } from './types.js';
|
|
23
|
-
import { readAndResolveBundleEnv
|
|
23
|
+
import { readAndResolveBundleEnv } from '../secrets/bundles.js';
|
|
24
24
|
const INTERACTIONS_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions';
|
|
25
25
|
const DEFAULT_MODEL = 'antigravity-preview-05-2026';
|
|
26
26
|
const KEY_NAMES = ['GEMINI_API_KEY', 'GOOGLE_API_KEY'];
|
|
@@ -65,7 +65,13 @@ export class AntigravityCloudProvider {
|
|
|
65
65
|
resolveApiKey() {
|
|
66
66
|
if (this.secretsBundle) {
|
|
67
67
|
try {
|
|
68
|
-
|
|
68
|
+
// Cloud dispatch resolves the key on its own (no human at a sheet), so the
|
|
69
|
+
// read is always `agentOnly` (SEC-13: never pop Touch ID on its own). A
|
|
70
|
+
// locked bundle THROWS the actionable "unlock <name>" message, which the
|
|
71
|
+
// catch below re-raises verbatim — dispatch genuinely needs the key, so it
|
|
72
|
+
// fails LOUD with the unlock hint rather than swallowing it into a wrong
|
|
73
|
+
// path. A `never`/no-ACL or broker-held bundle resolves silently.
|
|
74
|
+
const { env } = readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:antigravity', agentOnly: true });
|
|
69
75
|
for (const k of KEY_NAMES) {
|
|
70
76
|
if (env[k])
|
|
71
77
|
return env[k];
|
package/dist/lib/crabbox/cli.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* from a secrets bundle when one is configured (see `crabboxEnv`).
|
|
13
13
|
*/
|
|
14
14
|
import { spawn, spawnSync } from 'child_process';
|
|
15
|
-
import { readAndResolveBundleEnv,
|
|
15
|
+
import { readAndResolveBundleEnv, listBundles, bundleExists } from '../secrets/bundles.js';
|
|
16
16
|
import { readMeta, writeMeta } from '../state.js';
|
|
17
17
|
/** Locate the crabbox binary, or throw an actionable error. */
|
|
18
18
|
export function findCrabbox() {
|
|
@@ -73,10 +73,14 @@ function resolveTailscaleBundleMemo() {
|
|
|
73
73
|
* and the single-key subset read (`keys: [ts.key]`) is rejected by
|
|
74
74
|
* `canCacheResolvedEnv` for broker auto-cache — so without this memo a
|
|
75
75
|
* non-broker-held tailscale bundle re-read the keychain on EVERY call (and,
|
|
76
|
-
* pre-guard, could pop a Touch ID sheet each time).
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
76
|
+
* pre-guard, could pop a Touch ID sheet each time). The read is always
|
|
77
|
+
* `agentOnly: true` (broker-only, SEC-13): tailscale is opt-in plumbing, a
|
|
78
|
+
* `--lease` run is headless by contract, and even an interactive `--lease`
|
|
79
|
+
* invocation must never pop an unwatched Touch ID sheet for this best-effort
|
|
80
|
+
* read — a locked bundle degrades silently to a public-network lease via the
|
|
81
|
+
* catch below. One read per process, success or failure (a failed read
|
|
82
|
+
* memoizes as undefined: the catch below already degrades to a public-network
|
|
83
|
+
* lease, retrying mid-process only repeats the same failure).
|
|
80
84
|
*/
|
|
81
85
|
let tailscaleValueMemo;
|
|
82
86
|
function resolveTailscaleKeyValueMemo(ts) {
|
|
@@ -86,7 +90,7 @@ function resolveTailscaleKeyValueMemo(ts) {
|
|
|
86
90
|
const { env } = readAndResolveBundleEnv(ts.name, {
|
|
87
91
|
caller: 'agents run --lease (crabbox tailscale)',
|
|
88
92
|
keys: [ts.key],
|
|
89
|
-
agentOnly:
|
|
93
|
+
agentOnly: true,
|
|
90
94
|
});
|
|
91
95
|
value = env[ts.key];
|
|
92
96
|
}
|
|
@@ -102,6 +106,7 @@ export function resetCrabboxSecretsMemosForTest() {
|
|
|
102
106
|
tailscaleBundleMemo = undefined;
|
|
103
107
|
tailscaleValueMemo = undefined;
|
|
104
108
|
leaseBundleMemo = undefined;
|
|
109
|
+
leaseEnvMemo = undefined;
|
|
105
110
|
}
|
|
106
111
|
/**
|
|
107
112
|
* The secrets bundle to feed crabbox, resolved in priority order:
|
|
@@ -150,6 +155,59 @@ function resolveLeaseBundleMemo() {
|
|
|
150
155
|
leaseBundleMemo = { value: resolveLeaseBundle() };
|
|
151
156
|
return leaseBundleMemo.value;
|
|
152
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Process-lifetime memo for the RESOLVED provider-token env, resolved ONCE up
|
|
160
|
+
* front. `crabboxEnv` is called on every `crabboxWaitReady` poll iteration
|
|
161
|
+
* (crabboxWaitReady → crabboxFind → crabboxList → crabboxEnv), so without this
|
|
162
|
+
* memo the lease-token keychain read re-ran every ~5s of the ready wait — the
|
|
163
|
+
* per-poll storm this fix exists to kill. The read is now `agentOnly: true`
|
|
164
|
+
* (SEC-13: a `--lease` run is headless by contract and must never pop an
|
|
165
|
+
* unwatched Touch ID sheet). A locked `hold`/`always` bundle makes that read
|
|
166
|
+
* THROW the actionable "unlock <name>" message; we memoize the thrown error too
|
|
167
|
+
* and re-raise it on every subsequent call, so the failure surfaces loud ONCE
|
|
168
|
+
* up front (crabboxWarmup / the first crabboxList, before any poll loop) and the
|
|
169
|
+
* loop never re-issues the read. A `never`/no-ACL or broker-held bundle resolves
|
|
170
|
+
* silently. `undefined` when no lease bundle is configured (crabbox uses its own
|
|
171
|
+
* `crabbox login`). The memo (success OR failure) is cleared per test by
|
|
172
|
+
* resetCrabboxSecretsMemosForTest.
|
|
173
|
+
*/
|
|
174
|
+
let leaseEnvMemo;
|
|
175
|
+
function resolveLeaseEnvMemo(explicitBundle) {
|
|
176
|
+
if (!leaseEnvMemo) {
|
|
177
|
+
const resolved = explicitBundle
|
|
178
|
+
? { name: explicitBundle }
|
|
179
|
+
: resolveLeaseBundleMemo();
|
|
180
|
+
if (!resolved) {
|
|
181
|
+
leaseEnvMemo = {};
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
try {
|
|
185
|
+
// Auto-detected bundle → inject ONLY the provider token key(s) (least
|
|
186
|
+
// privilege; an unrelated bundle can't leak its other secrets into crabbox).
|
|
187
|
+
// An explicitly-named bundle (env/config or `opts.secretsBundle`) injects
|
|
188
|
+
// whole — the user chose it. Same resolver `agents secrets exec` uses.
|
|
189
|
+
const { env } = readAndResolveBundleEnv(resolved.name, {
|
|
190
|
+
caller: 'agents run --lease (crabbox)',
|
|
191
|
+
keys: resolved.keys,
|
|
192
|
+
// --lease is headless by contract and a locked bundle must fail loud with
|
|
193
|
+
// an unlock hint, NEVER pop a Touch ID sheet — the read cannot be answered
|
|
194
|
+
// in a background lease (SEC-13).
|
|
195
|
+
agentOnly: true,
|
|
196
|
+
});
|
|
197
|
+
leaseEnvMemo = { env };
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
leaseEnvMemo = {
|
|
201
|
+
error: new Error(`Could not load secrets bundle "${resolved.name}" for crabbox: ${e.message}. ` +
|
|
202
|
+
`Fix the bundle (agents secrets view ${resolved.name}) or unset lease.secretsBundle to use crabbox's own login.`),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (leaseEnvMemo.error)
|
|
208
|
+
throw leaseEnvMemo.error;
|
|
209
|
+
return leaseEnvMemo.env;
|
|
210
|
+
}
|
|
153
211
|
/** Persist `lease.secretsBundle` in agents config so `--lease` needs no env var. */
|
|
154
212
|
export function setLeaseSecretsBundle(name) {
|
|
155
213
|
const meta = readMeta();
|
|
@@ -159,30 +217,14 @@ export function setLeaseSecretsBundle(name) {
|
|
|
159
217
|
/** Build the child env for crabbox, injecting a secrets bundle when configured. */
|
|
160
218
|
export function crabboxEnv(opts) {
|
|
161
219
|
const out = { ...process.env };
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// whole — the user chose it. Same resolver `agents secrets exec` uses.
|
|
171
|
-
const { env } = readAndResolveBundleEnv(resolved.name, {
|
|
172
|
-
caller: 'agents run --lease (crabbox)',
|
|
173
|
-
keys: resolved.keys,
|
|
174
|
-
// --lease is headless by contract and crabboxEnv is called several times
|
|
175
|
-
// per run (list/wait/spawn/stop) — resolve broker-only so a keychain bundle
|
|
176
|
-
// can't pop repeated unwatched Touch ID sheets mid-lease.
|
|
177
|
-
agentOnly: isHeadlessSecretsContext(),
|
|
178
|
-
});
|
|
179
|
-
Object.assign(out, env);
|
|
180
|
-
}
|
|
181
|
-
catch (e) {
|
|
182
|
-
throw new Error(`Could not load secrets bundle "${resolved.name}" for crabbox: ${e.message}. ` +
|
|
183
|
-
`Fix the bundle (agents secrets view ${resolved.name}) or unset lease.secretsBundle to use crabbox's own login.`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
220
|
+
// The lease provider-token read is resolved ONCE up front and memoized (env or
|
|
221
|
+
// thrown error) for the process — crabboxEnv runs on every crabboxWaitReady
|
|
222
|
+
// poll, so re-reading here was the per-poll storm. A locked bundle re-raises
|
|
223
|
+
// the memoized "unlock <name>" error every call, so the failure surfaces loud
|
|
224
|
+
// on the first crabboxEnv (before any poll loop) and never re-issues the read.
|
|
225
|
+
const leaseEnv = resolveLeaseEnvMemo(opts.secretsBundle);
|
|
226
|
+
if (leaseEnv)
|
|
227
|
+
Object.assign(out, leaseEnv);
|
|
186
228
|
// Tailscale plumbing (F5): inject CRABBOX_TAILSCALE_AUTH_KEY from a bundle that
|
|
187
229
|
// declares a tailscale auth key, when the ambient env doesn't already set one.
|
|
188
230
|
// Best-effort and opt-in — a missing/unreadable tailscale bundle never fails a
|
|
@@ -20,6 +20,9 @@ export interface UnifiedQuery {
|
|
|
20
20
|
agent?: string;
|
|
21
21
|
/** Only events stamped with this session id (payload `sessionId`, the provenance floor). */
|
|
22
22
|
sessionId?: string;
|
|
23
|
+
/** Only events carrying this bundle name in their payload (e.g. secrets events).
|
|
24
|
+
* Combined with `sessionId`, answers "which session read this secrets bundle". */
|
|
25
|
+
bundle?: string;
|
|
23
26
|
caller?: string;
|
|
24
27
|
command?: string;
|
|
25
28
|
module?: string;
|