@phnx-labs/agents-cli 1.22.1 → 1.22.4
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 +66 -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/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +42 -14
- 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 +25 -30
- package/dist/lib/secrets/bundles.js +84 -60
- package/dist/lib/secrets/headless.d.ts +39 -0
- package/dist/lib/secrets/headless.js +63 -0
- package/dist/lib/secrets/index.d.ts +10 -0
- package/dist/lib/secrets/index.js +90 -6
- package/dist/lib/secrets/read-backoff.d.ts +27 -0
- package/dist/lib/secrets/read-backoff.js +64 -0
- package/dist/lib/secrets/session-store.js +6 -4
- package/dist/lib/secrets/vault.js +3 -1
- 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/usage.js +3 -1
- 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,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.4
|
|
4
|
+
|
|
5
|
+
- **Background processes no longer storm macOS Touch ID sheets (secrets-touchid-storm).**
|
|
6
|
+
Raw keychain item reads — a profile's provider token on `agents run <profile>`,
|
|
7
|
+
the Claude OAuth read behind `agents view`, any `getKeychainToken` caller — now
|
|
8
|
+
fail fast with an actionable error naming the item when the process is
|
|
9
|
+
non-interactive (an agent runtime, or a TTY-less background spawn like the
|
|
10
|
+
Factory extension host's `agents view` poll), instead of raising a sheet nobody
|
|
11
|
+
is watching. A cancelled or failed interactive read opens a 5-minute back-off
|
|
12
|
+
memo (`~/.agents/.cache/keychain-read-backoff/`) so a polling caller can't
|
|
13
|
+
re-prompt every few seconds; any successful read or write clears it. Reads that
|
|
14
|
+
are prompt-free by construction (bundle metadata, `never`-policy bundles, the
|
|
15
|
+
unlock session store, the OAuth token cache) attest their no-ACL write and are
|
|
16
|
+
unaffected. crabbox's tailscale key is now read at most once per process
|
|
17
|
+
instead of on every `crabboxEnv` call (list/wait/spawn/stop). Source:
|
|
18
|
+
`apps/cli/src/lib/secrets/index.ts`, `apps/cli/src/lib/secrets/headless.ts`,
|
|
19
|
+
`apps/cli/src/lib/secrets/read-backoff.ts`,
|
|
20
|
+
`apps/cli/src/lib/secrets/bundles.ts`, `apps/cli/src/lib/crabbox/cli.ts`,
|
|
21
|
+
`apps/cli/docs/specifications.md` (SEC-13, SEC-27).
|
|
22
|
+
|
|
23
|
+
## 1.22.3
|
|
24
|
+
|
|
25
|
+
- **Menubar home-base self-test accepts `MenubarHelper-universal`.** The
|
|
26
|
+
signed-helper gate required `executablePath` to end in exactly `MenubarHelper`,
|
|
27
|
+
but lipo production builds name the binary `MenubarHelper-universal`, so every
|
|
28
|
+
1.22.2 publish on mac-mini failed after the release PR had already merged and
|
|
29
|
+
tagged. Source: `menubar/Sources/MenubarHelper/ChildProcessSelfTest.swift`.
|
|
30
|
+
|
|
31
|
+
## 1.22.2
|
|
32
|
+
|
|
33
|
+
- **Menu bar recovers from a stale single-instance lock instead of staying dead.**
|
|
34
|
+
The helper's lock fd is now opened `O_CLOEXEC`, so a spawned `doctor` child can
|
|
35
|
+
never inherit it and hold the `menubar.lock` flock after the helper crashes; and
|
|
36
|
+
`SingleInstance.acquire` now self-heals — when the flock is held but no live
|
|
37
|
+
`MenubarHelper` owns it (a leaked orphan / dead pid), it reaps the orphan and
|
|
38
|
+
retries rather than exiting as "already running". Previously a leaked orphan
|
|
39
|
+
bricked the menu bar until reboot. The headless Swift self-tests (single-instance
|
|
40
|
+
+ child-process) now run as a build gate (`menubar/scripts/test-menubar.sh`),
|
|
41
|
+
which nothing invoked before. Source: `apps/cli/menubar/Sources/MenubarHelper/SingleInstance.swift`.
|
|
42
|
+
|
|
43
|
+
- **`agents projects` definitions can now carry goals — the OKR-shaped "why".**
|
|
44
|
+
A project serves one or more `goals[]`, each an `objective` (the outcome) plus an
|
|
45
|
+
optional `measure` (the key result). Set them at scaffold time with
|
|
46
|
+
`agents projects add <name> --goal "objective:measure"` (repeatable), replace them
|
|
47
|
+
later with `agents projects set <name> --goal …`, or hand-edit the YAML. Goals show
|
|
48
|
+
on the `status` card (compact) and in `projects view` (in full), and survive a
|
|
49
|
+
`--from-linear` re-import like every other hand-set field. Milestones (pulled from
|
|
50
|
+
Linear) remain the dated checkpoints toward these goals. Source:
|
|
51
|
+
`apps/cli/src/lib/projects.ts`, `apps/cli/src/commands/projects.ts`.
|
|
52
|
+
|
|
53
|
+
- **Balanced rotation no longer picks version homes that only inherit the active login.**
|
|
54
|
+
`getAccountInfo` falls back to the active/global HOME credential so `agents view`
|
|
55
|
+
still shows who is signed in when a version home has no auth file of its own.
|
|
56
|
+
Launch paths isolate config (`GROK_HOME`, `CODEX_HOME`, …) to the per-version home,
|
|
57
|
+
so those empty homes died at spawn with "Not signed in" after balanced picked them
|
|
58
|
+
(observed: `grok@0.2.118` with no `auth.json` looking signed-in via `~/.grok` →
|
|
59
|
+
`0.2.32`). Rotation now requires a real per-version credential when we know where
|
|
60
|
+
it lives (`credentialPresence.perVersion`). Source: `src/lib/rotate.ts`
|
|
61
|
+
(`isLaunchableSignedIn`, `collectRunCandidates`).
|
|
62
|
+
|
|
63
|
+
- **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`.
|
|
64
|
+
|
|
65
|
+
- **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`.
|
|
66
|
+
|
|
67
|
+
- **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`.
|
|
68
|
+
|
|
3
69
|
## 1.22.1
|
|
4
70
|
|
|
5
71
|
- **`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 ----------------------------------------------------
|
|
@@ -83,6 +83,8 @@ export declare function pickTailscaleBundleFromList(bundles: SecretsBundle[]): {
|
|
|
83
83
|
name: string;
|
|
84
84
|
key: string;
|
|
85
85
|
} | undefined;
|
|
86
|
+
/** Test seam: drop every crabboxEnv secrets memo so each test resolves fresh. */
|
|
87
|
+
export declare function resetCrabboxSecretsMemosForTest(): void;
|
|
86
88
|
/** A resolved lease bundle: its name, plus (auto-detect only) the exact keys to inject. */
|
|
87
89
|
export interface ResolvedLeaseBundle {
|
|
88
90
|
name: string;
|
package/dist/lib/crabbox/cli.js
CHANGED
|
@@ -67,6 +67,42 @@ function resolveTailscaleBundleMemo() {
|
|
|
67
67
|
}
|
|
68
68
|
return tailscaleBundleMemo.value;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Process-lifetime memo for the RESOLVED tailscale key value, next to the pick
|
|
72
|
+
* memo above. `crabboxEnv` runs several times per lease (list/wait/spawn/stop),
|
|
73
|
+
* and the single-key subset read (`keys: [ts.key]`) is rejected by
|
|
74
|
+
* `canCacheResolvedEnv` for broker auto-cache — so without this memo a
|
|
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). One read per process,
|
|
77
|
+
* success or failure (a failed read memoizes as undefined: the catch below
|
|
78
|
+
* already degrades to a public-network lease, retrying mid-process only
|
|
79
|
+
* repeats the same failure).
|
|
80
|
+
*/
|
|
81
|
+
let tailscaleValueMemo;
|
|
82
|
+
function resolveTailscaleKeyValueMemo(ts) {
|
|
83
|
+
if (!tailscaleValueMemo) {
|
|
84
|
+
let value;
|
|
85
|
+
try {
|
|
86
|
+
const { env } = readAndResolveBundleEnv(ts.name, {
|
|
87
|
+
caller: 'agents run --lease (crabbox tailscale)',
|
|
88
|
+
keys: [ts.key],
|
|
89
|
+
agentOnly: isHeadlessSecretsContext(),
|
|
90
|
+
});
|
|
91
|
+
value = env[ts.key];
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* best-effort — tailscale is opt-in plumbing, never blocks a public lease */
|
|
95
|
+
}
|
|
96
|
+
tailscaleValueMemo = { value };
|
|
97
|
+
}
|
|
98
|
+
return tailscaleValueMemo.value;
|
|
99
|
+
}
|
|
100
|
+
/** Test seam: drop every crabboxEnv secrets memo so each test resolves fresh. */
|
|
101
|
+
export function resetCrabboxSecretsMemosForTest() {
|
|
102
|
+
tailscaleBundleMemo = undefined;
|
|
103
|
+
tailscaleValueMemo = undefined;
|
|
104
|
+
leaseBundleMemo = undefined;
|
|
105
|
+
}
|
|
70
106
|
/**
|
|
71
107
|
* The secrets bundle to feed crabbox, resolved in priority order:
|
|
72
108
|
* 1. `AGENTS_LEASE_SECRETS_BUNDLE` env var — explicit, no keychain
|
|
@@ -151,23 +187,15 @@ export function crabboxEnv(opts) {
|
|
|
151
187
|
// declares a tailscale auth key, when the ambient env doesn't already set one.
|
|
152
188
|
// Best-effort and opt-in — a missing/unreadable tailscale bundle never fails a
|
|
153
189
|
// public-network lease; `crabboxWarmup({ netMode: 'tailscale' })` is what decides
|
|
154
|
-
// whether the key is actually used.
|
|
190
|
+
// whether the key is actually used. The resolved value is memoized per process
|
|
191
|
+
// (see resolveTailscaleKeyValueMemo) so repeated crabboxEnv calls read the
|
|
192
|
+
// keychain at most once.
|
|
155
193
|
if (!out.CRABBOX_TAILSCALE_AUTH_KEY) {
|
|
156
194
|
const ts = resolveTailscaleBundleMemo();
|
|
157
195
|
if (ts) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
keys: [ts.key],
|
|
162
|
-
agentOnly: isHeadlessSecretsContext(),
|
|
163
|
-
});
|
|
164
|
-
const value = env[ts.key];
|
|
165
|
-
if (value)
|
|
166
|
-
out.CRABBOX_TAILSCALE_AUTH_KEY = value;
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
/* best-effort — tailscale is opt-in plumbing, never blocks a public lease */
|
|
170
|
-
}
|
|
196
|
+
const value = resolveTailscaleKeyValueMemo(ts);
|
|
197
|
+
if (value)
|
|
198
|
+
out.CRABBOX_TAILSCALE_AUTH_KEY = value;
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
201
|
return out;
|
|
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
|