@phnx-labs/agents-cli 1.22.1 → 1.22.3
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 +46 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/exec.js +1 -1
- package/dist/commands/projects.js +25 -1
- package/dist/commands/secrets.js +9 -5
- package/dist/commands/teams.js +2 -2
- package/dist/commands/watchdog.js +64 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/projects.d.ts +15 -0
- package/dist/lib/projects.js +12 -0
- package/dist/lib/rotate.d.ts +16 -1
- package/dist/lib/rotate.js +33 -7
- 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/bundles.d.ts +20 -0
- package/dist/lib/secrets/bundles.js +50 -0
- package/dist/lib/share/config.d.ts +14 -6
- package/dist/lib/share/config.js +23 -5
- package/dist/lib/types.d.ts +10 -0
- package/dist/lib/watchdog/rotate.d.ts +218 -0
- package/dist/lib/watchdog/rotate.js +378 -0
- package/dist/lib/watchdog/runner.d.ts +33 -1
- package/dist/lib/watchdog/runner.js +303 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.3
|
|
4
|
+
|
|
5
|
+
- **Menubar home-base self-test accepts `MenubarHelper-universal`.** The
|
|
6
|
+
signed-helper gate required `executablePath` to end in exactly `MenubarHelper`,
|
|
7
|
+
but lipo production builds name the binary `MenubarHelper-universal`, so every
|
|
8
|
+
1.22.2 publish on mac-mini failed after the release PR had already merged and
|
|
9
|
+
tagged. Source: `menubar/Sources/MenubarHelper/ChildProcessSelfTest.swift`.
|
|
10
|
+
|
|
11
|
+
## 1.22.2
|
|
12
|
+
|
|
13
|
+
- **Menu bar recovers from a stale single-instance lock instead of staying dead.**
|
|
14
|
+
The helper's lock fd is now opened `O_CLOEXEC`, so a spawned `doctor` child can
|
|
15
|
+
never inherit it and hold the `menubar.lock` flock after the helper crashes; and
|
|
16
|
+
`SingleInstance.acquire` now self-heals — when the flock is held but no live
|
|
17
|
+
`MenubarHelper` owns it (a leaked orphan / dead pid), it reaps the orphan and
|
|
18
|
+
retries rather than exiting as "already running". Previously a leaked orphan
|
|
19
|
+
bricked the menu bar until reboot. The headless Swift self-tests (single-instance
|
|
20
|
+
+ child-process) now run as a build gate (`menubar/scripts/test-menubar.sh`),
|
|
21
|
+
which nothing invoked before. Source: `apps/cli/menubar/Sources/MenubarHelper/SingleInstance.swift`.
|
|
22
|
+
|
|
23
|
+
- **`agents projects` definitions can now carry goals — the OKR-shaped "why".**
|
|
24
|
+
A project serves one or more `goals[]`, each an `objective` (the outcome) plus an
|
|
25
|
+
optional `measure` (the key result). Set them at scaffold time with
|
|
26
|
+
`agents projects add <name> --goal "objective:measure"` (repeatable), replace them
|
|
27
|
+
later with `agents projects set <name> --goal …`, or hand-edit the YAML. Goals show
|
|
28
|
+
on the `status` card (compact) and in `projects view` (in full), and survive a
|
|
29
|
+
`--from-linear` re-import like every other hand-set field. Milestones (pulled from
|
|
30
|
+
Linear) remain the dated checkpoints toward these goals. Source:
|
|
31
|
+
`apps/cli/src/lib/projects.ts`, `apps/cli/src/commands/projects.ts`.
|
|
32
|
+
|
|
33
|
+
- **Balanced rotation no longer picks version homes that only inherit the active login.**
|
|
34
|
+
`getAccountInfo` falls back to the active/global HOME credential so `agents view`
|
|
35
|
+
still shows who is signed in when a version home has no auth file of its own.
|
|
36
|
+
Launch paths isolate config (`GROK_HOME`, `CODEX_HOME`, …) to the per-version home,
|
|
37
|
+
so those empty homes died at spawn with "Not signed in" after balanced picked them
|
|
38
|
+
(observed: `grok@0.2.118` with no `auth.json` looking signed-in via `~/.grok` →
|
|
39
|
+
`0.2.32`). Rotation now requires a real per-version credential when we know where
|
|
40
|
+
it lives (`credentialPresence.perVersion`). Source: `src/lib/rotate.ts`
|
|
41
|
+
(`isLaunchableSignedIn`, `collectRunCandidates`).
|
|
42
|
+
|
|
43
|
+
- **A `never`-policy secrets bundle now actually stays silent — no more Touch ID for a bundle you set to silent, and no more double prompt.** Two bugs made `agents secrets policy <b> never` a lie. (1) The command rewrote only the bundle *metadata*, never the value items — but macOS gates each read on the item's own ACL, not the tier label, so a bundle created under `hold`/`always` kept its biometry ACL and kept popping Touch ID forever after the switch. `policy` now reconciles the value items to the new tier (`reAclBundleItems`): tightening to `never` re-stores them no-ACL (a single last prompt to read them once), and loosening back re-attaches the gate. (2) The signed keychain helper's just-in-time migration (`migrateInline`/`rehomeOrphan`) re-stamped a biometry ACL onto *every* `agents-cli.*` item it touched on read — including bundle metadata and the HMAC key, which are supposed to be silent — so a metadata/hmackey read in its own helper process raised a *second* Touch ID sheet on top of the value read. The migration now re-adds silent items (metadata, hmackey) without an ACL, so metadata enumeration and the pre-value hmackey read never prompt. Net: unlock/read a `never` bundle once and it stays silent through sleep, reboot, 30+ days, an agents-cli upgrade, and a macOS upgrade — with no Touch ID and no passphrase. The `never` tier's durability and attribution guarantees are now written into the `§Secrets` spec (SEC-19, SEC-27, SEC-28). Source: `apps/cli/src/commands/secrets.ts`, `apps/cli/src/lib/secrets/bundles.ts`, `apps/cli/src/lib/secrets/keychain-helper.swift`, `apps/cli/docs/specifications.md`.
|
|
44
|
+
|
|
45
|
+
- **The per-run Touch ID storm is fixed: `agents run` no longer pops a sheet to auto-inject the share token.** On every `agents run`, `shareRuntimeEnv` auto-reads the `share` bundle's R2 write token to hand it to the spawned agent — and because `share` is a keychain bundle that is rarely broker-held, an interactive read spawned the helper and raised Touch ID on EVERY launch. Auto-injecting a token on an agent launch is a background convenience, not a user-initiated secret access, so it must never raise a sheet (SEC-13): the read is now always `agentOnly` — it resolves the token from the injected env or an already-held / no-ACL bundle and silently returns nothing otherwise (the agent can still publish via its own explicit `agents share`). And a NEW `share` bundle now defaults to the `never` tier (no biometry ACL) — the write token is low-sensitivity automation infra — so auto-share is silent with no unlock at all; an existing bundle keeps its tier (change it with `agents secrets policy share never`, which now actually strips the ACL). Source: `apps/cli/src/lib/share/config.ts`, `apps/cli/src/commands/exec.ts`.
|
|
46
|
+
|
|
47
|
+
- **The daemon watchdog now rotates rate-limited sessions in place.** A stalled session whose transcript tail shows a hard account limit ("You've hit your weekly limit · resets …", "usage limit reached", "out of credits") is rotated instead of nudged: the tick gates on the same first-party healthy-account selection `agents run auto` makes (`collectHarnessCandidates` + `pickHarnessWeighted` — zero healthy logs one `rotate` skip event per cooldown window to `watchdog.log` and leaves the terminal untouched), injects the harness's exit sequence (claude: Esc, Ctrl+C, Ctrl+C; codex/gemini/cursor/opencode: Ctrl+C twice), relaunches `agents run auto --interactive --session-id <uuid>` in the SAME tab via the inject rail, then — once the new session's TUI is live — injects the resume replay for the old session. Readiness is the new session's transcript (primary) or a fresh active session correlated by cwd + machine, never an unrelated one; the wait is bounded (60s), and on timeout the session is flagged with a bare-shell message pointing at a manual `agents run auto` and suppressed for 15m before retry — never blind-typed into. The state machine (`exiting → launching → awaiting-tui → replaying → done | failed`) persists at `~/.agents/.cache/state/watchdog/rotate/<sessionId>.json` and spans ticks via a post-loop sweep. Config: **`agents watchdog rotate on|off`** writes `watchdog.rotate` in `~/.agents/agents.yaml` (default on; rotate-only, nudging is unaffected), honored per tick; `agents watchdog status` / `--json` report the rotate config and every persisted rotate state. This replaces the Factory extension's own watchdog rotate loop, which is being deleted in the companion change. Source: `apps/cli/src/lib/watchdog/rotate.ts`, `apps/cli/src/lib/watchdog/runner.ts`, `apps/cli/src/commands/watchdog.ts`.
|
|
48
|
+
|
|
3
49
|
## 1.22.1
|
|
4
50
|
|
|
5
51
|
- **`agents doctor` de-noise: never-synced and cross-version hook drift are warnings, not criticals (RUSH-2162).** The CRITICAL section now holds only "needs you now" problems — a logged-out account, or a hook/plugin missing from a version you keep synced. A version that was never synced (an old/unused install with nothing installed) and a hook that merely *differs* across versions (installed but stale) are surfaced as WARNINGs instead, cutting the critical count on a busy machine from ~11 to the handful that actually need action. Source: `apps/cli/src/lib/devices/doctor-findings.ts`.
|
package/README.md
CHANGED
|
@@ -382,6 +382,8 @@ agents watchdog --watch # daemon loop: a tick every --interval
|
|
|
382
382
|
|
|
383
383
|
`agents watchdog` detects a stalled session, resolves the *exact* terminal split it lives in (tmux, iTerm, VSCodium, or a raw pty), and injects a nudge -- `Continue.` by default, or set `--text`. It's dry by default; `--nudge` acts on a single tick, and `agents watchdog enable` flips global auto-nudge on so `--watch` injects on its own. Steer a single run with `agents watchdog policy <id> off | keep | handsoff`.
|
|
384
384
|
|
|
385
|
+
A stalled session whose tail shows a hard account limit ("You've hit your weekly limit · resets …") is **rotated in place** instead of nudged: the watchdog gates on the same healthy-account selection `agents run auto` makes (zero healthy → one skip event per cooldown window, terminal untouched), injects the harness's exit sequence, relaunches `agents run auto --interactive --session-id <uuid>` in the *same* tab, then replays the old session's resume once the new TUI is live. Default on; `agents watchdog rotate off` disables it (nudging stays on).
|
|
386
|
+
|
|
385
387
|
---
|
|
386
388
|
|
|
387
389
|
## Sync the fleet
|
package/dist/bin/agents
CHANGED
|
Binary file
|
package/dist/commands/cloud.js
CHANGED
package/dist/commands/exec.js
CHANGED
|
@@ -2261,7 +2261,7 @@ export function registerRunCommand(program) {
|
|
|
2261
2261
|
}
|
|
2262
2262
|
}
|
|
2263
2263
|
const autoShareEnv = options.autoSecrets !== false
|
|
2264
|
-
? shareRuntimeEnv(
|
|
2264
|
+
? shareRuntimeEnv()
|
|
2265
2265
|
: undefined;
|
|
2266
2266
|
// Merge order (later wins): profile env < auto share token < secrets bundles < --env K=V.
|
|
2267
2267
|
// Profile carries provider auth; secrets bundles carry user-defined
|
|
@@ -59,6 +59,17 @@ function parseContextFlag(raw) {
|
|
|
59
59
|
return { path: raw.trim(), purpose: '' };
|
|
60
60
|
return { path: raw.slice(0, i).trim(), purpose: raw.slice(i + 1).trim() };
|
|
61
61
|
}
|
|
62
|
+
/** `objective:measure` → a goal. The measure is optional; the objective may contain colons only after the first is claimed. */
|
|
63
|
+
function parseGoalFlag(raw) {
|
|
64
|
+
const i = raw.indexOf(':');
|
|
65
|
+
if (i === -1)
|
|
66
|
+
return { objective: raw.trim() };
|
|
67
|
+
const measure = raw.slice(i + 1).trim();
|
|
68
|
+
const goal = { objective: raw.slice(0, i).trim() };
|
|
69
|
+
if (measure)
|
|
70
|
+
goal.measure = measure;
|
|
71
|
+
return goal;
|
|
72
|
+
}
|
|
62
73
|
/**
|
|
63
74
|
* Best-effort `owner/repo` from a repo's origin remote.
|
|
64
75
|
*
|
|
@@ -383,6 +394,9 @@ detail = false) {
|
|
|
383
394
|
}
|
|
384
395
|
// `view` prints these in full (path + purpose, label + URL) right below, so
|
|
385
396
|
// the compact one-line summaries would just say the same thing twice.
|
|
397
|
+
if (!detail && def.goals?.length) {
|
|
398
|
+
console.log(` ${chalk.dim('goal')} ${def.goals.map((g) => g.objective).join(' · ')}`);
|
|
399
|
+
}
|
|
386
400
|
if (!detail && def.contexts?.length) {
|
|
387
401
|
console.log(` ${chalk.dim('context')} ${def.contexts.map((c) => c.path).join(' · ')}`);
|
|
388
402
|
}
|
|
@@ -467,6 +481,7 @@ export function registerProjectsCommands(program) {
|
|
|
467
481
|
.option('--path <subdir>', 'Default cwd for agents (a monorepo subdir)')
|
|
468
482
|
.option('--repo <owner/repo>', 'Primary GitHub slug (defaults to the origin remote)')
|
|
469
483
|
.option('--context <path:purpose...>', 'A described starting point; repeatable')
|
|
484
|
+
.option('--goal <objective:measure...>', 'An outcome the project serves; repeatable')
|
|
470
485
|
.option('--linear <url-or-id>', 'Linear project URL or id')
|
|
471
486
|
.option('--force', 'Overwrite an existing definition')
|
|
472
487
|
.action(async (name, opts) => {
|
|
@@ -497,6 +512,8 @@ export function registerProjectsCommands(program) {
|
|
|
497
512
|
def.repo = repo;
|
|
498
513
|
if (opts.context?.length)
|
|
499
514
|
def.contexts = opts.context.map(parseContextFlag);
|
|
515
|
+
if (opts.goal?.length)
|
|
516
|
+
def.goals = opts.goal.map(parseGoalFlag);
|
|
500
517
|
if (opts.linear) {
|
|
501
518
|
def.linear = /^https?:/.test(opts.linear) ? { url: opts.linear } : { projectId: opts.linear };
|
|
502
519
|
}
|
|
@@ -564,6 +581,8 @@ export function registerProjectsCommands(program) {
|
|
|
564
581
|
const where = [rp.subpath ? `subpath ${rp.subpath}` : undefined, rp.path].filter(Boolean).join(' · ');
|
|
565
582
|
console.log(` ${chalk.dim('repo')} ${rp.slug}${where ? chalk.dim(` (${where})`) : ''}`);
|
|
566
583
|
}
|
|
584
|
+
for (const g of def.goals ?? [])
|
|
585
|
+
console.log(` ${chalk.dim('goal')} ${g.objective}${g.measure ? chalk.dim(` · ${g.measure}`) : ''}`);
|
|
567
586
|
for (const c of def.contexts ?? [])
|
|
568
587
|
console.log(` ${chalk.dim('context')} ${chalk.cyan(c.path)} ${chalk.dim('—')} ${c.purpose}`);
|
|
569
588
|
for (const ig of def.integrations ?? []) {
|
|
@@ -742,6 +761,7 @@ export function registerProjectsCommands(program) {
|
|
|
742
761
|
.option('--root <path>', 'Repo / monorepo root')
|
|
743
762
|
.option('--path <subdir>', 'Default cwd for agents (a monorepo subdir)')
|
|
744
763
|
.option('--description <text>', 'One-line description shown on the card')
|
|
764
|
+
.option('--goal <objective:measure...>', 'Replace the goals this project serves; repeatable')
|
|
745
765
|
.action((name, opts) => {
|
|
746
766
|
const def = loadProjectDef(name);
|
|
747
767
|
if (!def) {
|
|
@@ -749,7 +769,7 @@ export function registerProjectsCommands(program) {
|
|
|
749
769
|
process.exit(1);
|
|
750
770
|
}
|
|
751
771
|
const fields = ['repo', 'root', 'path', 'description'].filter((k) => opts[k] !== undefined);
|
|
752
|
-
if (fields.length === 0) {
|
|
772
|
+
if (fields.length === 0 && !opts.goal?.length) {
|
|
753
773
|
console.error(chalk.red('Nothing to set. Pass a field, e.g. --repo <owner/repo>.'));
|
|
754
774
|
process.exit(1);
|
|
755
775
|
}
|
|
@@ -762,6 +782,8 @@ export function registerProjectsCommands(program) {
|
|
|
762
782
|
def.root = opts.root;
|
|
763
783
|
if (opts.description !== undefined)
|
|
764
784
|
def.description = opts.description;
|
|
785
|
+
if (opts.goal?.length)
|
|
786
|
+
def.goals = opts.goal.map(parseGoalFlag);
|
|
765
787
|
if (opts.path !== undefined) {
|
|
766
788
|
// `--path` is a subdir OF the root, so without one there is nothing to
|
|
767
789
|
// hang it off. A def imported with `--from-linear` that found no local
|
|
@@ -780,6 +802,8 @@ export function registerProjectsCommands(program) {
|
|
|
780
802
|
console.log(chalk.green(`Updated ${def.name}`));
|
|
781
803
|
for (const f of fields)
|
|
782
804
|
console.log(chalk.gray(` ${f} ${f === 'path' ? def.defaultPath : def[f]}`));
|
|
805
|
+
if (opts.goal?.length)
|
|
806
|
+
console.log(chalk.gray(` goals ${def.goals?.map((g) => g.objective).join(' · ')}`));
|
|
783
807
|
});
|
|
784
808
|
// ---- link ----
|
|
785
809
|
projects
|
package/dist/commands/secrets.js
CHANGED
|
@@ -18,7 +18,7 @@ import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
|
18
18
|
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, } from '../lib/secrets/remote.js';
|
|
19
19
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
20
20
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
21
|
-
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, readBundleIfDecryptable, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
21
|
+
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
|
|
22
22
|
import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
|
|
23
23
|
import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
|
|
24
24
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
@@ -2738,10 +2738,14 @@ Examples:
|
|
|
2738
2738
|
return;
|
|
2739
2739
|
}
|
|
2740
2740
|
bundle.policy = next;
|
|
2741
|
-
//
|
|
2742
|
-
//
|
|
2743
|
-
//
|
|
2744
|
-
|
|
2741
|
+
// Re-store the VALUE items so their keychain ACL matches the new tier —
|
|
2742
|
+
// NOT just the metadata. macOS gates each read on the item's own ACL
|
|
2743
|
+
// (SEC-19); a metadata-only write would leave a hold/always bundle's
|
|
2744
|
+
// biometry ACL in place and keep popping Touch ID forever after a switch
|
|
2745
|
+
// to `never`. reAclBundleItems reads once (the last prompt this bundle
|
|
2746
|
+
// raises when tightening to `never`), rewrites no-ACL, and evicts any
|
|
2747
|
+
// broker-held copy so the change takes effect on the very next read.
|
|
2748
|
+
reAclBundleItems(bundle);
|
|
2745
2749
|
console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
|
|
2746
2750
|
if (next === 'hold') {
|
|
2747
2751
|
console.log(chalk.gray('Held by the secrets-agent for ~7 days after one unlock (auto-cache is on by default; disable with `secrets.agent.auto: false` in agents.yaml).'));
|
package/dist/commands/teams.js
CHANGED
|
@@ -1539,7 +1539,7 @@ export function registerTeamsCommands(program) {
|
|
|
1539
1539
|
repo: opts.repo,
|
|
1540
1540
|
branch: opts.branch,
|
|
1541
1541
|
model: a.model ?? undefined,
|
|
1542
|
-
env: shareRuntimeEnv(
|
|
1542
|
+
env: shareRuntimeEnv(),
|
|
1543
1543
|
};
|
|
1544
1544
|
const cloudTask = await prov.dispatch(dispatchOpts);
|
|
1545
1545
|
return { cloudSessionId: cloudTask.id };
|
|
@@ -1557,7 +1557,7 @@ export function registerTeamsCommands(program) {
|
|
|
1557
1557
|
repo: opts.repo,
|
|
1558
1558
|
branch: opts.branch,
|
|
1559
1559
|
model: opts.model,
|
|
1560
|
-
env: shareRuntimeEnv(
|
|
1560
|
+
env: shareRuntimeEnv(),
|
|
1561
1561
|
};
|
|
1562
1562
|
try {
|
|
1563
1563
|
const cloudTask = await prov.dispatch(dispatchOpts);
|
|
@@ -29,6 +29,7 @@ import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
|
29
29
|
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
30
30
|
import { ensureWatchdogRoutine, isWatchdogRoutineEnabled, watchdogRoutineExists, WATCHDOG_ROUTINE_NAME, WATCHDOG_ROUTINE_SCHEDULE, } from '../lib/watchdog/routine.js';
|
|
31
31
|
import { runWatchdogTick, writePolicySentinel, DEFAULT_THRESHOLDS, } from '../lib/watchdog/runner.js';
|
|
32
|
+
import { isWatchdogRotateEnabled, listRotateStates, setWatchdogRotateEnabled } from '../lib/watchdog/rotate.js';
|
|
32
33
|
/** Default state dir the runner and these subcommands share. */
|
|
33
34
|
function stateDir() {
|
|
34
35
|
return path.join(getRuntimeStateDir(), 'watchdog');
|
|
@@ -79,6 +80,8 @@ async function runMailboxGc(sessions) {
|
|
|
79
80
|
function colorForOutcome(o) {
|
|
80
81
|
if (o.injected)
|
|
81
82
|
return chalk.green;
|
|
83
|
+
if (o.decision === 'rotate')
|
|
84
|
+
return chalk.magenta;
|
|
82
85
|
if (o.addressable === false)
|
|
83
86
|
return chalk.yellow;
|
|
84
87
|
if (o.stall === 'stalled' && o.decision === 'nudge')
|
|
@@ -92,12 +95,14 @@ function printTick(result, willInject) {
|
|
|
92
95
|
console.log(`${chalk.bold('watchdog')} ${mode} ` +
|
|
93
96
|
`${counts.total} live · ${counts.stalled} stalled · ` +
|
|
94
97
|
`${chalk.green(String(counts.nudged))} nudged · ` +
|
|
95
|
-
`${chalk.yellow(String(counts.unaddressable))} un-addressable`
|
|
98
|
+
`${chalk.yellow(String(counts.unaddressable))} un-addressable` +
|
|
99
|
+
(counts.rotating > 0 ? ` · ${chalk.magenta(String(counts.rotating))} rotating` : ''));
|
|
96
100
|
for (const o of result.outcomes) {
|
|
97
101
|
const tag = o.injected ? 'NUDGED'
|
|
98
|
-
: o.
|
|
99
|
-
: o.
|
|
100
|
-
: '
|
|
102
|
+
: o.decision === 'rotate' ? (o.rotatePhase === 'failed' ? 'ROTATE-FAIL' : 'ROTATE')
|
|
103
|
+
: o.addressable === false ? 'FLAGGED'
|
|
104
|
+
: o.decision === 'nudge' ? 'WOULD-NUDGE'
|
|
105
|
+
: 'skip';
|
|
101
106
|
const c = colorForOutcome(o);
|
|
102
107
|
const who = o.label || o.sessionId?.slice(0, 8) || o.kind;
|
|
103
108
|
const where = o.host ? chalk.dim(`[${o.host}]`) : '';
|
|
@@ -194,8 +199,14 @@ export function registerWatchdogCommand(program) {
|
|
|
194
199
|
# Turn on the ALWAYS-ON watchdog (a daemon-fired routine)
|
|
195
200
|
agents watchdog enable
|
|
196
201
|
|
|
202
|
+
# Show the routine, rotate config, and any in-flight in-place rotates
|
|
203
|
+
agents watchdog status
|
|
204
|
+
|
|
197
205
|
# Leave one session detected-but-untouched
|
|
198
206
|
agents watchdog policy <sessionId> handsoff
|
|
207
|
+
|
|
208
|
+
# Opt out of in-place rotate only (nudging stays on)
|
|
209
|
+
agents watchdog rotate off
|
|
199
210
|
`,
|
|
200
211
|
notes: `
|
|
201
212
|
Decision path: a cheap deterministic pre-filter resolves the obvious cases
|
|
@@ -217,6 +228,20 @@ export function registerWatchdogCommand(program) {
|
|
|
217
228
|
Ghostty with no tmux) is flagged for the menu-bar and SKIPPED -- never a
|
|
218
229
|
guessed or frontmost target.
|
|
219
230
|
|
|
231
|
+
Rotate: a stalled session whose tail shows a HARD account limit ("You've
|
|
232
|
+
hit your weekly limit - resets ...") is ROTATED IN PLACE instead of nudged:
|
|
233
|
+
the tick gates on the same healthy-account selection 'agents run auto'
|
|
234
|
+
makes (zero healthy -> one skip event per cooldown window, terminal
|
|
235
|
+
untouched), injects the harness's exit sequence, relaunches
|
|
236
|
+
'agents run auto --interactive --session-id <uuid>' in the SAME tab, waits
|
|
237
|
+
(bounded, 60s) for the new TUI, then injects the resume replay for the old
|
|
238
|
+
session. On timeout the session is flagged and never blind-typed into; the
|
|
239
|
+
flag says the terminal may sit at a bare shell and needs a manual
|
|
240
|
+
'agents run auto'. A failed rotate is suppressed for 15m before retry.
|
|
241
|
+
Default ON; disable with 'agents watchdog rotate off' (writes
|
|
242
|
+
'watchdog.rotate: off' to ~/.agents/agents.yaml; nudging stays on).
|
|
243
|
+
State machine: ~/.agents/.cache/state/watchdog/rotate/<sessionId>.json.
|
|
244
|
+
|
|
220
245
|
Always-on: 'agents watchdog enable' creates + enables a 'watchdog' command
|
|
221
246
|
routine ('${WATCHDOG_ROUTINE_SCHEDULE}' -> agents watchdog --nudge) and reloads the
|
|
222
247
|
daemon; 'disable' pauses it. Inspect it with 'agents routines list'. Defaults
|
|
@@ -244,6 +269,20 @@ export function registerWatchdogCommand(program) {
|
|
|
244
269
|
await reloadDaemonForRoutine(false);
|
|
245
270
|
console.log(chalk.yellow('watchdog: DISABLED (routine paused)'));
|
|
246
271
|
});
|
|
272
|
+
cmd.command('rotate <state>')
|
|
273
|
+
.description('Turn in-place rotate of rate-limited sessions on|off (watchdog.rotate in agents.yaml). ' +
|
|
274
|
+
'Rotate-only: nudging stays on — unlike `watchdog disable`, which pauses the whole watchdog.')
|
|
275
|
+
.action((state) => {
|
|
276
|
+
const s = state.toLowerCase();
|
|
277
|
+
if (s !== 'on' && s !== 'off') {
|
|
278
|
+
console.error(chalk.red(`invalid state '${state}'. Use: on | off`));
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
setWatchdogRotateEnabled(s === 'on');
|
|
283
|
+
console.log(`watchdog: rotate ${s === 'on' ? chalk.green('ON') : chalk.yellow('OFF')} ` +
|
|
284
|
+
chalk.dim(`(watchdog.rotate: ${s} in agents.yaml)`));
|
|
285
|
+
});
|
|
247
286
|
cmd.command('status')
|
|
248
287
|
.description('Show whether the always-on watchdog routine is enabled and where state is written.')
|
|
249
288
|
.option('--json', 'Emit status as JSON (for the menu-bar / scripts)')
|
|
@@ -254,11 +293,31 @@ export function registerWatchdogCommand(program) {
|
|
|
254
293
|
// read it correctly regardless of which command commander bound it to.
|
|
255
294
|
const json = command.optsWithGlobals().json === true;
|
|
256
295
|
const on = isWatchdogRoutineEnabled();
|
|
296
|
+
const rotate = isWatchdogRotateEnabled() ? 'on' : 'off';
|
|
297
|
+
const rotates = listRotateStates(stateDir());
|
|
298
|
+
const inflight = rotates.filter((r) => r.phase !== 'done' && r.phase !== 'failed');
|
|
257
299
|
if (json) {
|
|
258
|
-
console.log(JSON.stringify({
|
|
300
|
+
console.log(JSON.stringify({
|
|
301
|
+
enabled: on,
|
|
302
|
+
routine: WATCHDOG_ROUTINE_NAME,
|
|
303
|
+
stateDir: stateDir(),
|
|
304
|
+
rotate,
|
|
305
|
+
rotates: rotates.map((r) => ({
|
|
306
|
+
sessionId: r.sessionId,
|
|
307
|
+
newSessionId: r.newSessionId,
|
|
308
|
+
agent: r.agent,
|
|
309
|
+
phase: r.phase,
|
|
310
|
+
updatedAtMs: r.updatedAtMs,
|
|
311
|
+
error: r.error,
|
|
312
|
+
})),
|
|
313
|
+
}));
|
|
259
314
|
return;
|
|
260
315
|
}
|
|
261
316
|
console.log(`always-on watchdog: ${on ? chalk.green('ON') : chalk.dim('off')} (routine '${WATCHDOG_ROUTINE_NAME}')`);
|
|
317
|
+
console.log(`rotate: ${rotate === 'on' ? chalk.green('on') : chalk.yellow('off')} (watchdog.rotate in agents.yaml) · ${inflight.length} in-flight`);
|
|
318
|
+
for (const r of inflight) {
|
|
319
|
+
console.log(` ${chalk.magenta(r.phase.padEnd(12))} ${chalk.bold(r.sessionId.slice(0, 8))} → ${r.newSessionId.slice(0, 8)}${r.error ? chalk.red(` ${r.error}`) : ''}`);
|
|
320
|
+
}
|
|
262
321
|
console.log(`state dir: ${chalk.dim(stateDir())}`);
|
|
263
322
|
});
|
|
264
323
|
// --- per-session policy ----------------------------------------------------
|
|
Binary file
|
|
Binary file
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -51,6 +51,19 @@ export interface ProjectContext {
|
|
|
51
51
|
/** One line on how this subtree relates to the project. */
|
|
52
52
|
purpose: string;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A project goal — the OKR-shaped "why". A project serves one or more goals: a
|
|
56
|
+
* qualitative `objective` ("Ship agents-cli 2.0") and an optional `measure`, the
|
|
57
|
+
* key result that says whether it's landing ("fleet on 2.x", "p95 < 200ms"). The
|
|
58
|
+
* goal is the outcome the work is chasing; milestones (dated checkpoints, pulled
|
|
59
|
+
* from Linear) and live work (agents / PRs / artifacts) are how far along it is.
|
|
60
|
+
*/
|
|
61
|
+
export interface ProjectGoal {
|
|
62
|
+
/** The outcome, in a line. */
|
|
63
|
+
objective: string;
|
|
64
|
+
/** Optional key result — how success is measured. */
|
|
65
|
+
measure?: string;
|
|
66
|
+
}
|
|
54
67
|
/** An external context source hung off the project (surfaced in `projects show`). */
|
|
55
68
|
export interface ProjectIntegration {
|
|
56
69
|
/** e.g. `gdrive`, `notion`, `figma`, `url`. */
|
|
@@ -73,6 +86,8 @@ export interface ProjectDef {
|
|
|
73
86
|
repos?: ProjectRepo[];
|
|
74
87
|
/** Described starting points inside the project. */
|
|
75
88
|
contexts?: ProjectContext[];
|
|
89
|
+
/** The outcomes this project serves (OKR-shaped); a project may have several. */
|
|
90
|
+
goals?: ProjectGoal[];
|
|
76
91
|
/** External context sources (Drive, docs, …). */
|
|
77
92
|
integrations?: ProjectIntegration[];
|
|
78
93
|
/** Linear project link — reuses the existing GraphQL path. */
|
package/dist/lib/projects.js
CHANGED
|
@@ -121,6 +121,18 @@ export function validateProjectDef(raw, sourceName) {
|
|
|
121
121
|
return [];
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
+
if (Array.isArray(o.goals)) {
|
|
125
|
+
def.goals = o.goals.flatMap((g) => {
|
|
126
|
+
if (g && typeof g === 'object' && typeof g.objective === 'string') {
|
|
127
|
+
const gg = g;
|
|
128
|
+
const goal = { objective: gg.objective };
|
|
129
|
+
if (typeof gg.measure === 'string')
|
|
130
|
+
goal.measure = gg.measure;
|
|
131
|
+
return [goal];
|
|
132
|
+
}
|
|
133
|
+
return [];
|
|
134
|
+
});
|
|
135
|
+
}
|
|
124
136
|
if (Array.isArray(o.integrations)) {
|
|
125
137
|
def.integrations = o.integrations.flatMap((i) => {
|
|
126
138
|
if (i &&
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentId, RunStrategy } from './types.js';
|
|
8
8
|
import type { FallbackEntry } from './exec.js';
|
|
9
|
-
import { type AccountInfo } from './agents.js';
|
|
9
|
+
import { type AccountInfo, type CredentialPresence } from './agents.js';
|
|
10
10
|
import { type UsageSnapshot } from './usage.js';
|
|
11
11
|
export interface RotateCandidate {
|
|
12
12
|
agent: AgentId;
|
|
@@ -75,6 +75,21 @@ export declare function getProjectRunStrategy(agent: AgentId, startPath: string)
|
|
|
75
75
|
export declare function getConfiguredRunStrategy(agent: AgentId, startPath?: string): RunStrategy;
|
|
76
76
|
/** Persist the global run strategy used by bare `agents run <agent>`. */
|
|
77
77
|
export declare function setGlobalRunStrategy(agent: AgentId, strategy: RunStrategy): void;
|
|
78
|
+
/**
|
|
79
|
+
* Whether a version home can actually authenticate a launch.
|
|
80
|
+
*
|
|
81
|
+
* `getAccountInfo` falls back to the active/global HOME when a version home has
|
|
82
|
+
* no credential of its own, so `agents view` still shows who is logged in. Launch
|
|
83
|
+
* paths isolate config (GROK_HOME, CODEX_HOME, KIMI_CODE_HOME, CLAUDE_CONFIG_DIR,
|
|
84
|
+
* …) to the per-version home, so a home that only "inherits" the active login
|
|
85
|
+
* cannot spawn a signed-in agent — balanced kept picking those empty homes and
|
|
86
|
+
* the run died on "Not signed in".
|
|
87
|
+
*
|
|
88
|
+
* When we know where the credential lives (`knownLocation`), require it under
|
|
89
|
+
* THIS version home. When we don't (keychain-only / unmapped agents), trust the
|
|
90
|
+
* existing `signedIn` signal.
|
|
91
|
+
*/
|
|
92
|
+
export declare function isLaunchableSignedIn(signedIn: boolean, presence: Pick<CredentialPresence, 'knownLocation' | 'perVersion'>): boolean;
|
|
78
93
|
/**
|
|
79
94
|
* How old a usage snapshot may be and still settle a routing DECISION.
|
|
80
95
|
*
|
package/dist/lib/rotate.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import { accountDisplayLabel, getAccountInfo, ALL_AGENT_IDS } from './agents.js';
|
|
9
|
+
import { accountDisplayLabel, getAccountInfo, credentialPresence, ALL_AGENT_IDS, } from './agents.js';
|
|
10
10
|
import { readMeta, writeMeta, getHelpersDir } from './state.js';
|
|
11
11
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
|
|
12
12
|
import { getProjectRunConfigs } from './run-config.js';
|
|
@@ -68,6 +68,27 @@ export function setGlobalRunStrategy(agent, strategy) {
|
|
|
68
68
|
function isRotationEligible(candidate) {
|
|
69
69
|
return candidate.signedIn && hasUsageAvailable(candidate);
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether a version home can actually authenticate a launch.
|
|
73
|
+
*
|
|
74
|
+
* `getAccountInfo` falls back to the active/global HOME when a version home has
|
|
75
|
+
* no credential of its own, so `agents view` still shows who is logged in. Launch
|
|
76
|
+
* paths isolate config (GROK_HOME, CODEX_HOME, KIMI_CODE_HOME, CLAUDE_CONFIG_DIR,
|
|
77
|
+
* …) to the per-version home, so a home that only "inherits" the active login
|
|
78
|
+
* cannot spawn a signed-in agent — balanced kept picking those empty homes and
|
|
79
|
+
* the run died on "Not signed in".
|
|
80
|
+
*
|
|
81
|
+
* When we know where the credential lives (`knownLocation`), require it under
|
|
82
|
+
* THIS version home. When we don't (keychain-only / unmapped agents), trust the
|
|
83
|
+
* existing `signedIn` signal.
|
|
84
|
+
*/
|
|
85
|
+
export function isLaunchableSignedIn(signedIn, presence) {
|
|
86
|
+
if (!signedIn)
|
|
87
|
+
return false;
|
|
88
|
+
if (!presence.knownLocation)
|
|
89
|
+
return true;
|
|
90
|
+
return presence.perVersion;
|
|
91
|
+
}
|
|
71
92
|
function isAvailableEligible(candidate) {
|
|
72
93
|
return isRotationEligible(candidate);
|
|
73
94
|
}
|
|
@@ -484,17 +505,22 @@ export async function collectRunCandidates(agent) {
|
|
|
484
505
|
// one per installed version, every time `agents run` cold-starts. If
|
|
485
506
|
// claude's stored token has actually expired, the spawned agent detects
|
|
486
507
|
// it at its own startup and re-auths; that's the correct UX.
|
|
508
|
+
//
|
|
509
|
+
// Gate signedIn on a real per-version credential when we know where it
|
|
510
|
+
// lives — see isLaunchableSignedIn. Do not reuse the active-home fallback
|
|
511
|
+
// identity for routing, or empty version homes look healthy and die at spawn.
|
|
512
|
+
const launchable = isLaunchableSignedIn(info.signedIn, credentialPresence(agent, home));
|
|
487
513
|
return {
|
|
488
514
|
agent,
|
|
489
515
|
version,
|
|
490
516
|
home,
|
|
491
517
|
info,
|
|
492
|
-
accountKey: info.accountKey,
|
|
493
|
-
accountLabel: accountDisplayLabel(info),
|
|
494
|
-
email: info.email,
|
|
495
|
-
usageStatus: info.usageStatus,
|
|
496
|
-
plan: info.plan,
|
|
497
|
-
signedIn:
|
|
518
|
+
accountKey: launchable ? info.accountKey : null,
|
|
519
|
+
accountLabel: launchable ? accountDisplayLabel(info) : '',
|
|
520
|
+
email: launchable ? info.email : null,
|
|
521
|
+
usageStatus: launchable ? info.usageStatus : null,
|
|
522
|
+
plan: launchable ? info.plan : null,
|
|
523
|
+
signedIn: launchable,
|
|
498
524
|
lastActive: info.lastActive,
|
|
499
525
|
};
|
|
500
526
|
}));
|
|
Binary file
|
|
Binary file
|
|
@@ -344,6 +344,26 @@ export interface RotateOptions {
|
|
|
344
344
|
* unless `clearMeta` or a `meta` patch is supplied.
|
|
345
345
|
*/
|
|
346
346
|
export declare function rotateBundleSecret(bundle: SecretsBundle, key: string, opts: RotateOptions): void;
|
|
347
|
+
/**
|
|
348
|
+
* Reconcile a bundle's keychain-backed VALUE items to its CURRENT policy, then
|
|
349
|
+
* write the (always no-ACL) metadata.
|
|
350
|
+
*
|
|
351
|
+
* `writeBundle` only rewrites the metadata item, so a policy change alone leaves
|
|
352
|
+
* every value item carrying the ACL it was created with — and macOS gates each
|
|
353
|
+
* read on the ITEM's ACL, not the bundle's declared tier (spec SEC-19). Without
|
|
354
|
+
* this reconcile, `agents secrets policy <b> never` reports "silent" while the
|
|
355
|
+
* still-ACL'd value keeps popping Touch ID on every read, forever.
|
|
356
|
+
*
|
|
357
|
+
* hold/always -> never strips the biometry ACL (helper `set-no-acl`: delete+add)
|
|
358
|
+
* never -> hold/always re-attaches it (helper `set`)
|
|
359
|
+
*
|
|
360
|
+
* The current values are read in ONE batch, so the reconcile costs at most a
|
|
361
|
+
* single Touch ID — the last prompt a hold->never bundle will ever raise (a
|
|
362
|
+
* never->* flip reads silently, since the items are already no-ACL). No-op on the
|
|
363
|
+
* ACL to write for non-keychain backends (file/vault have no biometry concept),
|
|
364
|
+
* and a metadata-only write when the bundle has no keychain-backed values.
|
|
365
|
+
*/
|
|
366
|
+
export declare function reAclBundleItems(bundle: SecretsBundle): void;
|
|
347
367
|
/** Options for renameBundle. */
|
|
348
368
|
export interface RenameOptions {
|
|
349
369
|
/** When true, overwrite an existing destination bundle (purges its keychain items first). */
|
|
@@ -1376,6 +1376,56 @@ export function rotateBundleSecret(bundle, key, opts) {
|
|
|
1376
1376
|
}
|
|
1377
1377
|
writeBundle(bundle);
|
|
1378
1378
|
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Reconcile a bundle's keychain-backed VALUE items to its CURRENT policy, then
|
|
1381
|
+
* write the (always no-ACL) metadata.
|
|
1382
|
+
*
|
|
1383
|
+
* `writeBundle` only rewrites the metadata item, so a policy change alone leaves
|
|
1384
|
+
* every value item carrying the ACL it was created with — and macOS gates each
|
|
1385
|
+
* read on the ITEM's ACL, not the bundle's declared tier (spec SEC-19). Without
|
|
1386
|
+
* this reconcile, `agents secrets policy <b> never` reports "silent" while the
|
|
1387
|
+
* still-ACL'd value keeps popping Touch ID on every read, forever.
|
|
1388
|
+
*
|
|
1389
|
+
* hold/always -> never strips the biometry ACL (helper `set-no-acl`: delete+add)
|
|
1390
|
+
* never -> hold/always re-attaches it (helper `set`)
|
|
1391
|
+
*
|
|
1392
|
+
* The current values are read in ONE batch, so the reconcile costs at most a
|
|
1393
|
+
* single Touch ID — the last prompt a hold->never bundle will ever raise (a
|
|
1394
|
+
* never->* flip reads silently, since the items are already no-ACL). No-op on the
|
|
1395
|
+
* ACL to write for non-keychain backends (file/vault have no biometry concept),
|
|
1396
|
+
* and a metadata-only write when the bundle has no keychain-backed values.
|
|
1397
|
+
*/
|
|
1398
|
+
export function reAclBundleItems(bundle) {
|
|
1399
|
+
if ((bundle.backend ?? 'keychain') !== 'keychain') {
|
|
1400
|
+
// No biometry ACL off the keychain backend — only metadata needs persisting.
|
|
1401
|
+
writeBundle(bundle);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const store = itemStore('keychain');
|
|
1405
|
+
// keychainItemsForBundle already returns ONLY keychain-backed value items
|
|
1406
|
+
// (via parseBundleValue), so no extra ref-shape filtering here.
|
|
1407
|
+
const entries = keychainItemsForBundle(bundle);
|
|
1408
|
+
if (entries.length === 0) {
|
|
1409
|
+
// Literal/ref-only bundle: nothing to re-ACL, just refresh metadata.
|
|
1410
|
+
writeBundle(bundle);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
// One batched read = at most one Touch ID for the whole reconcile.
|
|
1414
|
+
const values = store.getBatch(entries.map((e) => e.item));
|
|
1415
|
+
const rewrite = new Map();
|
|
1416
|
+
for (const { item } of entries) {
|
|
1417
|
+
const value = values.get(item);
|
|
1418
|
+
// A key present in metadata but with no readable value item is real
|
|
1419
|
+
// corruption, not something to silently skip (no fallbacks — fail loud).
|
|
1420
|
+
if (value === undefined) {
|
|
1421
|
+
throw new Error(`Cannot change policy for '${bundle.name}': a keychain value is missing or unreadable. Rotate that key, then retry.`);
|
|
1422
|
+
}
|
|
1423
|
+
rewrite.set(item, value);
|
|
1424
|
+
}
|
|
1425
|
+
// writeBundleWithItems re-stores each value with { noAcl: policy === 'never' }
|
|
1426
|
+
// and the metadata no-ACL (metadata-last), and evicts any broker-held copy.
|
|
1427
|
+
writeBundleWithItems(bundle, rewrite);
|
|
1428
|
+
}
|
|
1379
1429
|
/**
|
|
1380
1430
|
* Rename a bundle: move metadata + every keychain-backed value to a new name.
|
|
1381
1431
|
*
|
|
@@ -32,12 +32,20 @@ export declare function readWriteTokenFromBundle(): string;
|
|
|
32
32
|
/** Read the raw write token from injected env first, then the local bundle.
|
|
33
33
|
* Throws with an actionable message if absent (run setup/join first). */
|
|
34
34
|
export declare function readWriteToken(): string;
|
|
35
|
-
/** Best-effort runtime env for spawned agents. Never throws
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
/** Best-effort runtime env for spawned agents. Never throws AND never prompts.
|
|
36
|
+
*
|
|
37
|
+
* Auto-injecting the share write token on every `agents run` is a background
|
|
38
|
+
* convenience, NOT a user-initiated secret access — so it MUST NOT raise a Touch
|
|
39
|
+
* ID sheet (SEC-13: an agent launch never pops biometry on its own). This was the
|
|
40
|
+
* per-run prompt storm: `share` is a keychain bundle that is rarely broker-held,
|
|
41
|
+
* so an interactive read here spawned the helper and popped Touch ID on EVERY
|
|
42
|
+
* launch. The read is now always `agentOnly` — it resolves the token only from the
|
|
43
|
+
* injected env or an already-held / no-ACL bundle, and silently returns undefined
|
|
44
|
+
* otherwise (the caller runs without auto-share; the agent can still publish via
|
|
45
|
+
* its own `agents share`). To get zero-friction auto-share with no prompt: unlock
|
|
46
|
+
* once (`agents secrets unlock share`) or make it no-ACL (`agents secrets policy
|
|
47
|
+
* share never`). */
|
|
48
|
+
export declare function shareRuntimeEnv(): Record<string, string> | undefined;
|
|
41
49
|
/** Cloudflare API credentials for provisioning, read from `cloudflare` (or a
|
|
42
50
|
* user-named bundle). Fuzzy-matches key names so it works across bundle layouts. */
|
|
43
51
|
export declare function readCloudflareCreds(bundle?: string, override?: {
|