@phnx-labs/agents-cli 1.22.4 → 1.22.6

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/bin/agents +0 -0
  3. package/dist/commands/events.js +8 -0
  4. package/dist/commands/exec.js +18 -6
  5. package/dist/commands/inspect.js +18 -5
  6. package/dist/commands/models.d.ts +1 -1
  7. package/dist/commands/models.js +68 -9
  8. package/dist/commands/monitors.js +1 -1
  9. package/dist/commands/view.d.ts +12 -0
  10. package/dist/commands/view.js +2 -0
  11. package/dist/commands/webhook.js +8 -4
  12. package/dist/index.js +2 -1
  13. package/dist/lib/browser/chrome.d.ts +12 -0
  14. package/dist/lib/browser/chrome.js +27 -11
  15. package/dist/lib/cloud/antigravity.js +8 -2
  16. package/dist/lib/crabbox/cli.js +72 -30
  17. package/dist/lib/event-stream.d.ts +3 -0
  18. package/dist/lib/event-stream.js +5 -0
  19. package/dist/lib/events.d.ts +2 -0
  20. package/dist/lib/events.js +6 -1
  21. package/dist/lib/feed.d.ts +1 -1
  22. package/dist/lib/feed.js +19 -0
  23. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  24. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  25. package/dist/lib/model-tier-overrides.d.ts +43 -0
  26. package/dist/lib/model-tier-overrides.js +97 -0
  27. package/dist/lib/model-tiers.d.ts +13 -7
  28. package/dist/lib/model-tiers.js +104 -35
  29. package/dist/lib/models.d.ts +16 -0
  30. package/dist/lib/models.js +17 -3
  31. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  32. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  33. package/dist/lib/secrets/index.d.ts +29 -0
  34. package/dist/lib/secrets/index.js +32 -0
  35. package/dist/lib/secrets/mcp.js +8 -4
  36. package/dist/lib/session/sync/config.d.ts +12 -11
  37. package/dist/lib/session/sync/config.js +40 -38
  38. package/dist/lib/share/config.js +4 -0
  39. package/dist/lib/types.d.ts +10 -0
  40. package/dist/lib/versions.js +35 -0
  41. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.6
4
+
5
+ - **Cost tiers are ignored (with a clear warning) for profile runs.** A profile's model comes from its endpoint (e.g. Kimi/DeepSeek/GLM via `agents run <profile>`), not the host harness's catalog — so passing `--model cheap|default|best|ultra` to a profile used to resolve against the *host* harness and forward an incompatible model id to the profile's endpoint. Now a tier on a profile run is discarded with a standout warning and the profile's configured model is used. Concrete `--model <id>` on a profile is unchanged. Source: `apps/cli/src/commands/exec.ts`.
6
+
7
+ ## 1.22.5
8
+
9
+ - **`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`.
10
+
11
+ - **`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`.
12
+
13
+ - **`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`.
14
+
15
+ - **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`.
16
+
17
+ - **`agents inspect` and `view --json` now report isolation honestly.** Found by diffing
18
+ every command's output between an isolated-only and a normal install. `inspect` printed
19
+ the bare-shim path unconditionally, so an isolated copy — which deliberately has no
20
+ shim, that being the guarantee — was shown sitting on the user's PATH; it now reports
21
+ `(none — isolated installs stay off PATH)` and `shim: null` in JSON. `inspect` also
22
+ showed only `default: false` for an isolated copy, hiding that it *was* the selected
23
+ one; it now carries `isolated` and `isolatedDefault`, and the header reads
24
+ `[isolated default]`. `view --json` had no isolation signal at all, so tooling could not
25
+ distinguish a sandboxed copy from one that owns the launcher and real config — its
26
+ version entries gain `isolated` and `isIsolatedDefault`. Source:
27
+ `apps/cli/src/commands/inspect.ts`, `apps/cli/src/commands/view.ts`.
28
+
29
+ - **`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`.
30
+
31
+ - **`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`.
32
+
33
+ - **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`.
34
+
3
35
  ## 1.22.4
4
36
 
5
37
  - **Background processes no longer storm macOS Touch ID sheets (secrets-touchid-storm).**
package/dist/bin/agents CHANGED
Binary file
@@ -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,
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { Option } from 'commander';
9
9
  import chalk from 'chalk';
10
+ import { isTierToken } from '../lib/model-tiers.js';
10
11
  import { RUN_AUTO_KEYWORD } from '../lib/types.js';
11
12
  import { setHelpSections } from '../lib/help.js';
12
13
  import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
@@ -1567,7 +1568,7 @@ export function registerRunCommand(program) {
1567
1568
  await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
1568
1569
  process.exit(resumeExit);
1569
1570
  }
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, isHeadlessSecretsContext }, { 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
+ 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
1572
  import('../lib/exec.js'),
1572
1573
  import('../lib/agents.js'),
1573
1574
  import('../lib/profiles.js'),
@@ -2239,11 +2240,13 @@ export function registerRunCommand(program) {
2239
2240
  agent,
2240
2241
  keys: secretsKeysSubset,
2241
2242
  allowExpired: options.allowExpired,
2242
- // The harness identity scopes any cached grant. It no longer lets the
2243
- // requesting agent wait for interactive approval — an agent launch
2243
+ // The harness identity scopes any cached grant. An agent launch
2244
2244
  // resolves broker-only and fails fast naming
2245
- // `agents secrets unlock <bundle>` (bundles.ts:interactiveUnlock).
2246
- agentOnly: isHeadlessSecretsContext(),
2245
+ // `agents secrets unlock <bundle>` (bundles.ts:interactiveUnlock) — it
2246
+ // MUST NOT raise a Touch ID sheet regardless of tty (SEC-13). Gating on
2247
+ // isHeadlessSecretsContext() left `--interactive` launches (the watchdog's
2248
+ // `agents run auto --interactive`) able to prompt, piling up helper sheets.
2249
+ agentOnly: true,
2247
2250
  });
2248
2251
  const entries = describeBundle(bundle);
2249
2252
  const counts = {};
@@ -2273,10 +2276,19 @@ export function registerRunCommand(program) {
2273
2276
  ? { ...(profileEnv ?? {}), ...(autoShareEnv ?? {}), ...secretsEnv, ...(userEnv ?? {}) }
2274
2277
  : undefined;
2275
2278
  const modelSource = runCmd.getOptionValueSource('model');
2276
- const model = options.model
2279
+ let model = options.model
2277
2280
  ?? (!fromProfile && modelSource === undefined
2278
2281
  ? (workflowModel ?? (options.fallback ? undefined : runDefaults.model))
2279
2282
  : undefined);
2283
+ // Cost tiers (cheap|default|best|ultra) resolve against a harness's own model
2284
+ // catalog. A profile's model comes from its endpoint, not the host harness, so a
2285
+ // tier here would forward an incompatible host-harness model to a different API.
2286
+ // Discard it loudly and let the profile's configured model stand.
2287
+ if (fromProfile && model && isTierToken(model)) {
2288
+ process.stderr.write(chalk.yellow(`[agents] --model ${model}: cost tiers don't apply to profile '${rawAgent}' ` +
2289
+ `(its model comes from the endpoint) — ignoring the tier, using the profile's configured model\n`));
2290
+ model = undefined;
2291
+ }
2280
2292
  const execOptions = {
2281
2293
  agent,
2282
2294
  version,
@@ -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
- const shimPath = path.join(getShimsDir(), AGENTS[agent].cliCommand);
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: shimPath,
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', shimPath],
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 hidden `agents models` command. */
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[];
@@ -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 hidden `agents models` command. */
41
+ /** Register the `agents models` command + its `tier` override subcommands. */
41
42
  export function registerModelsCommand(program) {
42
- program
43
- .command('models [agentSpec]', { hidden: true })
44
- .description('List models supported by an installed agent version (internal/debug)')
45
- .option('--cloud', 'Show per-cloud IDs (Claude only)')
46
- .option('--reasoning', 'Show reasoning levels per model (Codex only)')
47
- .option('--json', 'Output catalog as JSON')
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
- console.log(` ${chalk.cyan(t.padEnd(8))} ${chalk.bold(r.model)}${eff}${priceLabel(r.model)}${clamp}`);
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
  }
@@ -257,7 +257,7 @@ export function registerMonitorsCommands(program) {
257
257
  # SSL cert issued → notify (poll an HTTPS endpoint every 8h)
258
258
  agents monitors add cert-issued \\
259
259
  --poll-http 'https://secure.ssl.com/team/.../co-ec1l5dgjofa' 8h \\
260
- --match 'issued' --notify telegram --device zion
260
+ --match 'issued' --notify --device zion
261
261
 
262
262
  # Dry-run: evaluate the source once and show what it would emit (no action)
263
263
  agents monitors test ci-red
@@ -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;
@@ -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,
@@ -1,6 +1,6 @@
1
1
  import * as path from 'path';
2
2
  import chalk from 'chalk';
3
- import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
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; when started detached
16
- // (no TTY) it must resolve broker-only rather than pop an unanswerable prompt.
17
- agentOnly: isHeadlessSecretsContext(),
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)
package/dist/index.js CHANGED
@@ -321,7 +321,8 @@ Diagnostics:
321
321
  perf Latency rollups (hooks, commands, runs) from the disposable perf warehouse
322
322
 
323
323
  Config sync:
324
- pull Clone or pull the system repo at ~/.agents/.system/
324
+ repo pull [alias] Git pull a repo (system | user | <extra>)
325
+ sync [agent] Re-materialize installed version homes; --local to skip fetching
325
326
  repo init --path <dir> Scaffold your own editable repo from a template
326
327
  repo add <path|gh:user/repo> Merge an extra repo after the system repo
327
328
  lock [--frozen] Write/verify agents.lock (SHA-256 of resolved resources); --frozen fails on drift
@@ -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, isHeadlessSecretsContext } from '../secrets/bundles.js';
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
- let env = { ...process.env };
283
- if (secrets && bundleExists(secrets)) {
284
- try {
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, isHeadlessSecretsContext } from '../secrets/bundles.js';
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
- const { env } = readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:antigravity', agentOnly: isHeadlessSecretsContext() });
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];