@phnx-labs/agents-cli 1.22.62 → 1.22.64
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 +37 -0
- package/dist/commands/accounts.d.ts +1 -1
- package/dist/commands/accounts.js +33 -7
- package/dist/commands/exec.js +40 -0
- package/dist/commands/feed.d.ts +1 -1
- package/dist/commands/feed.js +2 -1
- package/dist/lib/accounting/rotate.d.ts +22 -0
- package/dist/lib/accounting/rotate.js +20 -0
- package/dist/lib/claude-account-token.d.ts +26 -0
- package/dist/lib/claude-account-token.js +65 -11
- package/dist/lib/event-families.js +1 -1
- package/dist/lib/exec.d.ts +66 -1
- package/dist/lib/exec.js +104 -8
- package/dist/lib/feed/events.d.ts +1 -1
- package/dist/lib/feed/events.js +5 -0
- package/dist/lib/feed-broadcast.d.ts +9 -0
- package/dist/lib/feed-broadcast.js +30 -1
- package/dist/lib/identity/client.d.ts +5 -4
- package/dist/lib/identity/client.js +6 -5
- package/dist/lib/resource-inventory.d.ts +1 -1
- package/dist/lib/resource-inventory.js +1 -1
- package/dist/lib/session/active.d.ts +10 -1
- package/dist/lib/session/active.js +8 -0
- package/dist/lib/session/digest.js +8 -2
- package/dist/lib/session/parse.d.ts +4 -0
- package/dist/lib/session/parse.js +16 -11
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +6 -0
- package/dist/lib/share/worker-template.js +268 -12
- package/dist/lib/types.d.ts +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.64
|
|
4
|
+
|
|
5
|
+
- **`agents accounts attach` bootstraps a keychain-less Linux worker (PHNX-3502).**
|
|
6
|
+
Attaching a Claude account to a version home on a headless worker now seeds the
|
|
7
|
+
home's identity and writes its `.oauth_token` from the account's non-rotating
|
|
8
|
+
setup-token already synced in the `auth` bundle, instead of refusing because the
|
|
9
|
+
home was never interactively signed in. This is what lets all accounts show as
|
|
10
|
+
logged in — and interactive worker runs authenticate (the shim's Linux
|
|
11
|
+
`.oauth_token` fallback) instead of dropping to the login screen. No rotating
|
|
12
|
+
OAuth credential is ever copied. Source: `apps/cli/src/commands/accounts.ts`,
|
|
13
|
+
`apps/cli/src/lib/claude-account-token.ts`.
|
|
14
|
+
|
|
15
|
+
- Fixed Rush-backed feed channel sinks failing on Linux workers by handing delivery to a reachable macOS fleet peer with the Keychain-bound transport.
|
|
16
|
+
|
|
17
|
+
- **Auto-produce the origin/main release attestation on merge (RUSH-2666).** A new
|
|
18
|
+
`attest-main.yml` workflow runs the full suite on every push to `main` and uploads
|
|
19
|
+
the exact-tree attestation + pretested tarball to a rolling `main-attestations`
|
|
20
|
+
GitHub Release, keyed by tree hash. `release.sh` now prefetches that proof into the
|
|
21
|
+
local store before waiting, so an ordinary release promotes without running the
|
|
22
|
+
suite inline — no more human running the suite by hand to unwedge a release. Purely
|
|
23
|
+
additive and fail-safe: on any fetch miss or error it falls back to exactly the
|
|
24
|
+
prior poll-then-`require` behavior, and a fetched proof is trusted only because the
|
|
25
|
+
existing exact-tree `require` re-verifies it. Source: `cli/scripts/release.sh`,
|
|
26
|
+
`.github/workflows/attest-main.yml`.
|
|
27
|
+
|
|
28
|
+
- **Phoenix ID now uses the branded `id.byphoenix.com` API hostname by default (PHNX-3543).** New CLI sessions start and poll device authorization, resolve `whoami`, and bind managed share/traces Workers against the same canonical Phoenix ID base. The legacy hostname remains live for older installed clients. `PHOENIX_ID_BASE` remains an environment override for development and private deployments. Source: `src/lib/identity/client.ts`.
|
|
29
|
+
|
|
30
|
+
- **A pre-launch `run.launch` event makes a launch into a logged-out version visible instead of silent.** `agents run <agent> --device auto --strategy balanced` (the VS Code "New Claude" flow) launched a version that was LOGGED OUT on the target box: `--device auto` (`applyDeviceAutoToOptions`) only ensures SOME account is ready on the device, not that the SPECIFIC version launched is signed in there, and the failure was invisible — the pinned/default path in `resolveRunVersion` returns without emitting `rotation.resolved`, and `run.dispatched` only fires at run FINALIZE (post-exit), but a logged-out agent sits at the login screen and never finalizes, so nothing was recorded. A new `run.launch` event is now emitted RIGHT BEFORE the harness child is spawned, on the device that will run it, so it fires even when the agent then sits stuck at a login screen. Both live launch paths are covered by one shared emitter: `spawnAgent` (after the tmux-durability gate, for the tmux-wrapped AND bare spawns) and the Windows `execShimPassthrough` shim (`resolvedVia: 'shim'`), which was the second blind spot. Payload: `module: 'run'`, `agent`, `version`, `strategy`, `signedIn` (the launchable-signed-in verdict for the SPECIFIC launched version on THIS device — REUSED from the rotated pick's `rotationResult.picked.signedIn` when the command already computed it, else derived via the new `isVersionLaunchableHere` helper, which applies the same `getVersionHomePath` -> `getAccountInfo` -> `isLaunchableSignedIn` gate as `collectRunCandidates`), `launchedLoggedOut` (the headline flag, `signedIn === false`), `email`, and `resolvedVia`; the device hostname is auto-stamped by `emit()`. It sits in the AUDIT lane — the sibling of `run.dispatched` and the more reliable stuck-launch signal — so `agents events --level audit --include runs` surfaces it. Purely additive observability — the emit is best-effort (it can never break a launch) and no routing/launch behavior changes; refusing to launch a logged-out version is a separate follow-up. Source: `cli/src/lib/exec.ts`, `cli/src/lib/accounting/rotate.ts`, `cli/src/lib/feed/events.ts`, `cli/src/lib/event-families.ts`, `cli/src/commands/exec.ts`.
|
|
31
|
+
|
|
32
|
+
- **Live session rows carry outcome-card metrics and deliverables (PHNX-3574).** `agents sessions --active --json` and `agents sessions watch --json` now enrich each live row with indexed `tokenCount`, `durationMs`, and `subAgentCount`, plus created plan/artifact documents detected by the existing bounded transcript-tail state engine. Thin clients such as AGI EXT can render the initial request, progress, deliverables, team fan-out, runtime, and token use from the one canonical stream without polling or parsing transcripts themselves. Source: `cli/src/lib/session/active.ts`, `cli/src/lib/session/state.ts`.
|
|
33
|
+
|
|
34
|
+
## 1.22.63
|
|
35
|
+
|
|
36
|
+
- **Managed share endpoint enforces a per-user storage quota, object limit, per-file size cap, and publish rate limit (PHNX-3542).** The managed `share.agents-cli.sh` Worker authenticated any Phoenix ID bearer and then accepted **unbounded** writes into shared R2 — no quota, no rate limit, no size cap — which blocked opening publishing to third parties. Each managed (Phoenix-identity) publish now charges a per-user usage ledger stored in R2 at `__usage/<owner>` (a conditional-put CAS object, mirroring the existing `__views`/`__handles` precedent — no Durable Object, no new binding): free tier is 200 MiB total, 150 canonical pages, 20 MiB per file, and 60 publishes/hour. Enforcement measures the **real request body** (bounded-buffered so a streaming body can't exceed the cap) and rejects on the true size **before any write**, so a spoofed-low declared size can't bypass the caps or destroy an existing page. It **fails loud** — `413` for a file, object-count, or byte-quota overage, `429` (with `Retry-After`) for the rate limit — and refunds bytes + object count on delete and on lazy expiry. Covers/views are server-generated overhead and excluded from the quota. BYO (`WRITE_TOKEN`) publishes write to the operator's own bucket at their own cost and are **unaffected** (a deliberate, documented policy). A `SHARE_PLANS` map is the seam for future paid tiers (billing follow-up PHNX-3569). Source: `cli/src/lib/share/worker-template.ts`.
|
|
37
|
+
|
|
38
|
+
- Let feed channel sinks customize their delivered body with existing post placeholders, including fail-closed `{ticket}` routing for clickable tracker links in team channels.
|
|
39
|
+
|
|
3
40
|
## 1.22.62
|
|
4
41
|
|
|
5
42
|
- **Owner notifications fan out across the configured normal-severity channels (PHNX-3567).** `agents send --to owner`, deprecated `agents notify`, monitor notifications, and an important feed's owner sink now attempt every addressable entry named by `owner.policy.normal` in `humans.yaml`, instead of silently selecting only the first. Each Rush-backed destination that cannot deliver on a Linux worker forwards its explicit channel and target to a capable Mac, avoiding both shell quoting and policy re-expansion/duplicate sends. Partial failures stay visible while successful channels still deliver; legacy single-channel configs retain their old behavior. Source: `cli/src/lib/humans.ts`, `cli/src/lib/notify.ts`, `cli/src/lib/channels/owner-forward.ts`, `cli/src/lib/feed-broadcast.ts`.
|
|
@@ -37,7 +37,7 @@ export declare function classifyAttachTarget(target: string): AttachTarget;
|
|
|
37
37
|
* the credential in the keychain, so `resolveClaudeSetupToken` returns null there and this
|
|
38
38
|
* is a no-op off Linux.
|
|
39
39
|
*/
|
|
40
|
-
export declare function writeClaudeInteractiveOauthToken(target: AttachTarget, targetAgent: AgentId): void;
|
|
40
|
+
export declare function writeClaudeInteractiveOauthToken(target: AttachTarget, targetAgent: AgentId, email?: string): void;
|
|
41
41
|
export declare function parseBundleKey(raw: string): {
|
|
42
42
|
bundle: string;
|
|
43
43
|
key: string;
|
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { password, select } from '@inquirer/prompts';
|
|
5
|
-
import { resolveClaudeSetupToken } from '../lib/claude-account-token.js';
|
|
5
|
+
import { readClaudeAccountEmail, resolveClaudeSetupToken, resolveClaudeSetupTokenForEmail, seedClaudeWorkerHomeIdentity } from '../lib/claude-account-token.js';
|
|
6
6
|
import { setHelpSections } from '../lib/help.js';
|
|
7
7
|
import { readMeta, updateMeta } from '../lib/state.js';
|
|
8
8
|
import { ALL_AGENT_IDS, getAccountInfo, resolveAgentName } from '../lib/agents.js';
|
|
@@ -91,12 +91,17 @@ export function classifyAttachTarget(target) {
|
|
|
91
91
|
* the credential in the keychain, so `resolveClaudeSetupToken` returns null there and this
|
|
92
92
|
* is a no-op off Linux.
|
|
93
93
|
*/
|
|
94
|
-
export function writeClaudeInteractiveOauthToken(target, targetAgent) {
|
|
94
|
+
export function writeClaudeInteractiveOauthToken(target, targetAgent, email) {
|
|
95
95
|
if (process.platform !== 'linux' || targetAgent !== 'claude' || target.kind !== 'installation')
|
|
96
96
|
return;
|
|
97
97
|
const versionHome = getVersionHomePath('claude', target.version);
|
|
98
98
|
const tokenPath = path.join(versionHome, '.claude', '.oauth_token');
|
|
99
|
-
|
|
99
|
+
// Resolve by the attached account's email when known (a freshly-seeded worker
|
|
100
|
+
// home the `.claude.json` read below could not key on yet), else by the home's
|
|
101
|
+
// own recorded identity for a re-point/detach.
|
|
102
|
+
const token = email
|
|
103
|
+
? resolveClaudeSetupTokenForEmail(email, versionHome)
|
|
104
|
+
: resolveClaudeSetupToken(versionHome);
|
|
100
105
|
// A re-point (attach B over A, or a detach) can leave no setup-token resolving for
|
|
101
106
|
// this version — B's may not be minted yet. A leftover file from the previous binding
|
|
102
107
|
// would silently authenticate interactive runs as the OLD account (the shim's Linux
|
|
@@ -536,9 +541,30 @@ agents run codex#work`,
|
|
|
536
541
|
else {
|
|
537
542
|
if (t.kind !== 'installation')
|
|
538
543
|
throw new Error(`${account.agent} authentication is per-version. Attach '${account.name}' to a specific ${account.agent}@<version>.`);
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
544
|
+
const versionHome = getVersionHomePath(t.agent, t.version);
|
|
545
|
+
// The literal email keys the account's `auth`-bundle setup-token. It lives
|
|
546
|
+
// in `identityLabel` — `identityKey` is a synthetic composite
|
|
547
|
+
// (`claude:account=<uuid>:org=<uuid>`, agents.ts nativeIdentityKey), never
|
|
548
|
+
// the address, so it must NOT be used to derive the token key.
|
|
549
|
+
const accountEmail = account.identityLabel;
|
|
550
|
+
// Headless-worker bootstrap: a keychain-less Linux worker home never had
|
|
551
|
+
// an interactive login, so its `.claude.json` carries no identity and
|
|
552
|
+
// `nativeIdentityFromSource` would reject the attach — yet the account's
|
|
553
|
+
// non-rotating setup-token is already fleet-synced in the `auth` bundle.
|
|
554
|
+
// Seed the identity (email only, no rotating credential) so the token
|
|
555
|
+
// resolves; `writeClaudeInteractiveOauthToken` then writes `.oauth_token`.
|
|
556
|
+
if (process.platform === 'linux' &&
|
|
557
|
+
account.agent === 'claude' &&
|
|
558
|
+
accountEmail &&
|
|
559
|
+
!readClaudeAccountEmail(versionHome) &&
|
|
560
|
+
resolveClaudeSetupTokenForEmail(accountEmail)) {
|
|
561
|
+
seedClaudeWorkerHomeIdentity(versionHome, accountEmail);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
const identity = await nativeIdentityFromSource(target);
|
|
565
|
+
if (identity.identityKey !== account.identityKey)
|
|
566
|
+
throw new Error(`'${target}' is signed in to a different identity than account '${account.name}'.`);
|
|
567
|
+
}
|
|
542
568
|
}
|
|
543
569
|
}
|
|
544
570
|
else {
|
|
@@ -546,7 +572,7 @@ agents run codex#work`,
|
|
|
546
572
|
getAccountProvider(account.provider).envFor(targetAgent, account.auth);
|
|
547
573
|
}
|
|
548
574
|
bindAccount(name, target);
|
|
549
|
-
writeClaudeInteractiveOauthToken(t, targetAgent);
|
|
575
|
+
writeClaudeInteractiveOauthToken(t, targetAgent, account.kind === 'native' && account.agent === 'claude' ? account.identityLabel : undefined);
|
|
550
576
|
console.log(chalk.green(`Attached ${account.name} to ${target}.`));
|
|
551
577
|
});
|
|
552
578
|
});
|
package/dist/commands/exec.js
CHANGED
|
@@ -2356,6 +2356,18 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2356
2356
|
// synthesize a same-agent fallback chain from the other healthy accounts
|
|
2357
2357
|
// (issue #348). Stays null unless a non-pinned strategy actually rotated.
|
|
2358
2358
|
let rotationResult = null;
|
|
2359
|
+
// Precomputed launchable-signed-in verdict for the ACTUAL launched
|
|
2360
|
+
// candidate, fed to the pre-launch `run.launch` event so it need not
|
|
2361
|
+
// re-probe. Sourced per resolution branch from the candidate that WON, not
|
|
2362
|
+
// the original auto-pick: the interactive picker (RUSH-2334 / PHNX-2526)
|
|
2363
|
+
// deliberately lets the user launch a LOGGED-OUT account, which is a
|
|
2364
|
+
// different candidate than `rotationResult.picked` — reading the verdict off
|
|
2365
|
+
// the auto-pick there would report `launchedLoggedOut:false` for a version
|
|
2366
|
+
// that is actually logged out, the exact false-negative this event exists to
|
|
2367
|
+
// prevent. Left undefined for pinned-default / explicit-pin so emitRunLaunch
|
|
2368
|
+
// falls back to probing the version home itself.
|
|
2369
|
+
let launchSignedIn;
|
|
2370
|
+
let launchEmail;
|
|
2359
2371
|
// Set when the zero-healthy path already announced a deliberate
|
|
2360
2372
|
// launch-to-sign-in, so the login preflight below does not repeat it.
|
|
2361
2373
|
let signInLaunch = false;
|
|
@@ -2460,6 +2472,11 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2460
2472
|
if (!selected)
|
|
2461
2473
|
return;
|
|
2462
2474
|
version = selected.version;
|
|
2475
|
+
// Source the run.launch verdict from the account the user ACTUALLY
|
|
2476
|
+
// picked — the picker may deliberately return a logged-out one
|
|
2477
|
+
// (RUSH-2334), so it can differ from rotationResult.picked.
|
|
2478
|
+
launchSignedIn = selected.signedIn;
|
|
2479
|
+
launchEmail = selected.email;
|
|
2463
2480
|
// Keep the rotation so mid-run failover can still cascade across
|
|
2464
2481
|
// the other (stale) healthy accounts after a real rejection.
|
|
2465
2482
|
rotationResult = resolved.rotation;
|
|
@@ -2476,6 +2493,12 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2476
2493
|
else if (resolved.version) {
|
|
2477
2494
|
version = resolved.version;
|
|
2478
2495
|
rotationResult = resolved.rotation;
|
|
2496
|
+
// The auto-pick already computed the launchable-signed-in verdict for
|
|
2497
|
+
// this exact version via the same gate — reuse it for run.launch.
|
|
2498
|
+
if (resolved.rotation) {
|
|
2499
|
+
launchSignedIn = resolved.rotation.picked.signedIn;
|
|
2500
|
+
launchEmail = resolved.rotation.picked.email;
|
|
2501
|
+
}
|
|
2479
2502
|
// A balanced/available pick of a PROVIDER account (setup-token /
|
|
2480
2503
|
// API-key) carries `providerAccount`. Resolve its env through the
|
|
2481
2504
|
// same `resolveSpawnAccount` path an explicit `--account` uses, so
|
|
@@ -2818,6 +2841,23 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2818
2841
|
// forwards `--emit-session-id`): print the resolved session id as a
|
|
2819
2842
|
// stdout sentinel so the launcher captures the id this run coined.
|
|
2820
2843
|
emitSessionId: options.emitSessionId === true,
|
|
2844
|
+
// Observability-only: carried onto the pre-launch `run.launch` event so
|
|
2845
|
+
// the stream records HOW this version was chosen. Neither affects the
|
|
2846
|
+
// spawn. `resolvedVia` attributes the version source cheaply — an
|
|
2847
|
+
// explicit @version pin, a strategy rotation, or the pinned default.
|
|
2848
|
+
strategy,
|
|
2849
|
+
resolvedVia: rawVersion
|
|
2850
|
+
? 'explicit-pin'
|
|
2851
|
+
: rotationResult
|
|
2852
|
+
? 'rotated'
|
|
2853
|
+
: 'pinned-default',
|
|
2854
|
+
// Launchable-signed-in verdict for the ACTUAL launched candidate, set per
|
|
2855
|
+
// resolution branch above (auto-pick from rotation.picked; interactive
|
|
2856
|
+
// picker from the user's `selected`, which may be logged out). Undefined
|
|
2857
|
+
// for a pinned-default / explicit-pin launch, where spawnAgent probes the
|
|
2858
|
+
// version home itself.
|
|
2859
|
+
launchSignedIn,
|
|
2860
|
+
launchEmail,
|
|
2821
2861
|
};
|
|
2822
2862
|
if (options.interactive && options.headless) {
|
|
2823
2863
|
console.error(chalk.red('--interactive and --headless are mutually exclusive. Pass one, or neither (mode is inferred from prompt presence).'));
|
package/dist/commands/feed.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { type OpenBlock } from '../lib/feed/feed.js';
|
|
|
20
20
|
import { type OutcomeGroup, type SessionOutcomeHint } from '../lib/feed-outcome.js';
|
|
21
21
|
import { filterBlocksForFeed } from '../lib/ask-classifier.js';
|
|
22
22
|
import { type FeedSessionSignal } from '../lib/feed-ranking.js';
|
|
23
|
-
export declare const FEED_POST_HELP = "\nExamples:\n # Title (subject) + body. Phone broadcasts put title first, body after a\n # blank line, then a \"Sent from agent/session on host\" footer.\n agents feed post --title \"CHANGELOG pushed\" \"Watching CI and mac-mini E2E\"\n agents feed post --title \"Cover ready\" \"render at ./out/cover.png\" --attach ./out/cover.png\n agents feed post --title \"Ready for review\" \"PR opened, waiting on prix-cloud\" --json\n\n # Worth interrupting someone over - reaches sinks gated on minLevel: important:\n agents feed post --title \"npm token expired\" \"Cannot publish the release\" --level important\n\n # Also raise a local desktop banner on THIS machine (same notifier as run\n # --notify), on top of any configured broadcast - useful when you are at the box:\n agents feed post --title \"Build green\" \"all checks passed\" --notify\n\n # Stuck: opens a needs-you block and always broadcasts at important:\n agents feed post --title \"Force-push denied\" \"git-guard blocked PR #1749\" --blocked\n agents feed post --title \"Publish or wait?\" \"npm publish now or after review\" --blocked --option publish --option wait\n agents feed post --title \"Delete preview env?\" \"stale preview still running\" --blocked --default \"leave it\"\n\n # Exhaust self-serve FIRST. A block is for what you genuinely cannot do:\n # a credential only the user holds, a decision only they can make, an\n # approval only they can give. Not \"should I do the obvious next step?\".\n\n # Outside a run, pass the session explicitly:\n agents feed post --title \"Manual note\" \"context for the next agent\" --session 00998b0e-2d15-4d2f-a58b-974a886c9b47\n\nIdentity (session, agent, host, runtime, pid, launchId) is stamped automatically\nand rides the phone footer of feed.broadcast {message}. Domain facts (tickets,\nPRs) are not CLI flags - the ticket is joined from the session index at post\ntime. No em-dashes in title/body - they are scrubbed on the way out.\n\nConfigure where a post is mirrored under feed.broadcast in agents.yaml - see\ndocs/observability.md. A milestone is always recorded, but it does not text\nthe owner when the sink has minLevel: important. Add --level important for a\nphone-worthy successful update. Use --blocked only when work cannot continue.\nThe owner destination comes from humans.yaml; do not duplicate it in agents.yaml.\n";
|
|
23
|
+
export declare const FEED_POST_HELP = "\nExamples:\n # Title (subject) + body. Phone broadcasts put title first, body after a\n # blank line, then a \"Sent from agent/session on host\" footer.\n agents feed post --title \"CHANGELOG pushed\" \"Watching CI and mac-mini E2E\"\n agents feed post --title \"Cover ready\" \"render at ./out/cover.png\" --attach ./out/cover.png\n agents feed post --title \"Ready for review\" \"PR opened, waiting on prix-cloud\" --json\n\n # Worth interrupting someone over - reaches sinks gated on minLevel: important:\n agents feed post --title \"npm token expired\" \"Cannot publish the release\" --level important\n\n # Also raise a local desktop banner on THIS machine (same notifier as run\n # --notify), on top of any configured broadcast - useful when you are at the box:\n agents feed post --title \"Build green\" \"all checks passed\" --notify\n\n # Stuck: opens a needs-you block and always broadcasts at important:\n agents feed post --title \"Force-push denied\" \"git-guard blocked PR #1749\" --blocked\n agents feed post --title \"Publish or wait?\" \"npm publish now or after review\" --blocked --option publish --option wait\n agents feed post --title \"Delete preview env?\" \"stale preview still running\" --blocked --default \"leave it\"\n\n # Exhaust self-serve FIRST. A block is for what you genuinely cannot do:\n # a credential only the user holds, a decision only they can make, an\n # approval only they can give. Not \"should I do the obvious next step?\".\n\n # Outside a run, pass the session explicitly:\n agents feed post --title \"Manual note\" \"context for the next agent\" --session 00998b0e-2d15-4d2f-a58b-974a886c9b47\n\nIdentity (session, agent, host, runtime, pid, launchId) is stamped automatically\nand rides the phone footer of feed.broadcast {message}. Domain facts (tickets,\nPRs) are not CLI flags - the ticket is joined from the session index at post\ntime. No em-dashes in title/body - they are scrubbed on the way out.\n\nConfigure where a post is mirrored under feed.broadcast in agents.yaml - see\ndocs/observability.md. A channel sink may set message: with placeholders such\nas {message} and {ticket}; a missing placeholder skips that sink. A milestone is always recorded, but it does not text\nthe owner when the sink has minLevel: important. Add --level important for a\nphone-worthy successful update. Use --blocked only when work cannot continue.\nThe owner destination comes from humans.yaml; do not duplicate it in agents.yaml.\n";
|
|
24
24
|
export declare const FEED_NO_FANOUT_ENV = "AGENTS_FEED_LOCAL";
|
|
25
25
|
/** Right-hand masthead summary: `N blocks · M agents`. */
|
|
26
26
|
export declare function formatFeedMastheadRight(blocks: OpenBlock[]): string;
|
package/dist/commands/feed.js
CHANGED
|
@@ -57,7 +57,8 @@ PRs) are not CLI flags - the ticket is joined from the session index at post
|
|
|
57
57
|
time. No em-dashes in title/body - they are scrubbed on the way out.
|
|
58
58
|
|
|
59
59
|
Configure where a post is mirrored under feed.broadcast in agents.yaml - see
|
|
60
|
-
docs/observability.md. A
|
|
60
|
+
docs/observability.md. A channel sink may set message: with placeholders such
|
|
61
|
+
as {message} and {ticket}; a missing placeholder skips that sink. A milestone is always recorded, but it does not text
|
|
61
62
|
the owner when the sink has minLevel: important. Add --level important for a
|
|
62
63
|
phone-worthy successful update. Use --blocked only when work cannot continue.
|
|
63
64
|
The owner destination comes from humans.yaml; do not duplicate it in agents.yaml.
|
|
@@ -131,6 +131,28 @@ export declare function setGlobalRunStrategy(agent: AgentId, strategy: RunStrate
|
|
|
131
131
|
* existing `signedIn` signal.
|
|
132
132
|
*/
|
|
133
133
|
export declare function isLaunchableSignedIn(signedIn: boolean, presence: Pick<CredentialPresence, 'knownLocation' | 'perVersion'>): boolean;
|
|
134
|
+
/** Launchable-signed-in verdict for ONE specific version on THIS device. */
|
|
135
|
+
export interface VersionLaunchState {
|
|
136
|
+
/** True iff this exact version home can spawn a signed-in agent right now. */
|
|
137
|
+
launchable: boolean;
|
|
138
|
+
/** The version home's account email when launchable, else null. */
|
|
139
|
+
email: string | null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Whether a SPECIFIC installed version is launchable-signed-in on THIS device,
|
|
143
|
+
* plus the account email when it is. Mirrors EXACTLY the per-version gate
|
|
144
|
+
* {@link collectRunCandidates} applies (getVersionHomePath -> getAccountInfo ->
|
|
145
|
+
* {@link isLaunchableSignedIn} over {@link credentialPresence}), so the
|
|
146
|
+
* pre-launch `run.launch` event can report the same signed-in verdict the
|
|
147
|
+
* balanced router computes for that version.
|
|
148
|
+
*
|
|
149
|
+
* The point is to make a launch into a logged-out version VISIBLE at spawn time:
|
|
150
|
+
* `--device auto` only guarantees SOME account is ready on the device, not that
|
|
151
|
+
* the specific version launched is signed in there (the yosemite-m3 2.1.219
|
|
152
|
+
* incident — 2.1.219 was logged out, the router correctly excluded it, yet it
|
|
153
|
+
* launched). Non-fatal by construction: callers wrap it best-effort.
|
|
154
|
+
*/
|
|
155
|
+
export declare function isVersionLaunchableHere(agent: AgentId, version: string): Promise<VersionLaunchState>;
|
|
134
156
|
/**
|
|
135
157
|
* How old a usage snapshot may be and still settle a routing DECISION.
|
|
136
158
|
*
|
|
@@ -98,6 +98,26 @@ export function isLaunchableSignedIn(signedIn, presence) {
|
|
|
98
98
|
return true;
|
|
99
99
|
return presence.perVersion;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether a SPECIFIC installed version is launchable-signed-in on THIS device,
|
|
103
|
+
* plus the account email when it is. Mirrors EXACTLY the per-version gate
|
|
104
|
+
* {@link collectRunCandidates} applies (getVersionHomePath -> getAccountInfo ->
|
|
105
|
+
* {@link isLaunchableSignedIn} over {@link credentialPresence}), so the
|
|
106
|
+
* pre-launch `run.launch` event can report the same signed-in verdict the
|
|
107
|
+
* balanced router computes for that version.
|
|
108
|
+
*
|
|
109
|
+
* The point is to make a launch into a logged-out version VISIBLE at spawn time:
|
|
110
|
+
* `--device auto` only guarantees SOME account is ready on the device, not that
|
|
111
|
+
* the specific version launched is signed in there (the yosemite-m3 2.1.219
|
|
112
|
+
* incident — 2.1.219 was logged out, the router correctly excluded it, yet it
|
|
113
|
+
* launched). Non-fatal by construction: callers wrap it best-effort.
|
|
114
|
+
*/
|
|
115
|
+
export async function isVersionLaunchableHere(agent, version) {
|
|
116
|
+
const home = getVersionHomePath(agent, version);
|
|
117
|
+
const info = await getAccountInfo(agent, home);
|
|
118
|
+
const launchable = isLaunchableSignedIn(info.signedIn, credentialPresence(agent, home));
|
|
119
|
+
return { launchable, email: launchable ? info.email : null };
|
|
120
|
+
}
|
|
101
121
|
function isAvailableEligible(candidate) {
|
|
102
122
|
return isRotationEligible(candidate);
|
|
103
123
|
}
|
|
@@ -22,3 +22,29 @@ export declare function readClaudeAccountEmail(home?: string): string | null;
|
|
|
22
22
|
* authenticate with the shareable setup-token, not the ACL-bound login item.
|
|
23
23
|
*/
|
|
24
24
|
export declare function resolveClaudeSetupToken(home?: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a long-lived setup-token for an EXPLICIT account email, independent of
|
|
27
|
+
* any version home's `.claude.json`. This is what lets `agents accounts attach`
|
|
28
|
+
* provision a headless worker home that has never had an interactive login: the
|
|
29
|
+
* account's non-rotating setup-token is already fleet-synced in the file-based
|
|
30
|
+
* `auth` bundle, keyed by email ({@link claudeAccountTokenKey}), so we can write
|
|
31
|
+
* the home's `.oauth_token` from it without the circular
|
|
32
|
+
* "read the home's email to resolve the home's token" dependency that
|
|
33
|
+
* {@link resolveClaudeSetupToken} has. Same file-backed-only, fail-closed,
|
|
34
|
+
* fingerprint-stable read as the home-keyed path — it is the shared core.
|
|
35
|
+
*
|
|
36
|
+
* `cacheKey` scopes the process-local token cache; callers pass a version home
|
|
37
|
+
* so a home-keyed and email-keyed read of the same account share nothing stale.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveClaudeSetupTokenForEmail(email: string, cacheKey?: string): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Seed a keychain-less Linux worker's Claude version-home identity so an account's
|
|
42
|
+
* fleet-synced setup-token resolves for it. A worker home never had an interactive
|
|
43
|
+
* browser login, so its `.claude.json` carries no `oauthAccount.emailAddress` and
|
|
44
|
+
* the account reads "signed out" even though its non-rotating setup-token is present
|
|
45
|
+
* in the `auth` bundle. This writes ONLY the descriptive identity (the email), merged
|
|
46
|
+
* into both `.claude.json` locations Claude Code reads, preserving every other field.
|
|
47
|
+
* It never copies a rotating OAuth credential (`.credentials.json`) — the setup-token
|
|
48
|
+
* stays the credential of record.
|
|
49
|
+
*/
|
|
50
|
+
export declare function seedClaudeWorkerHomeIdentity(versionHome: string, email: string): void;
|
|
@@ -76,12 +76,32 @@ export function readClaudeAccountEmail(home) {
|
|
|
76
76
|
* authenticate with the shareable setup-token, not the ACL-bound login item.
|
|
77
77
|
*/
|
|
78
78
|
export function resolveClaudeSetupToken(home) {
|
|
79
|
+
// Require a known account (email) up front: without it we cannot key a
|
|
80
|
+
// per-account token, and we must NOT fall back to a bare shared key that
|
|
81
|
+
// would misapply one account's setup-token to another.
|
|
82
|
+
const email = readClaudeAccountEmail(home);
|
|
83
|
+
if (!email)
|
|
84
|
+
return null;
|
|
85
|
+
return resolveClaudeSetupTokenForEmail(email, home ?? os.homedir());
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resolve a long-lived setup-token for an EXPLICIT account email, independent of
|
|
89
|
+
* any version home's `.claude.json`. This is what lets `agents accounts attach`
|
|
90
|
+
* provision a headless worker home that has never had an interactive login: the
|
|
91
|
+
* account's non-rotating setup-token is already fleet-synced in the file-based
|
|
92
|
+
* `auth` bundle, keyed by email ({@link claudeAccountTokenKey}), so we can write
|
|
93
|
+
* the home's `.oauth_token` from it without the circular
|
|
94
|
+
* "read the home's email to resolve the home's token" dependency that
|
|
95
|
+
* {@link resolveClaudeSetupToken} has. Same file-backed-only, fail-closed,
|
|
96
|
+
* fingerprint-stable read as the home-keyed path — it is the shared core.
|
|
97
|
+
*
|
|
98
|
+
* `cacheKey` scopes the process-local token cache; callers pass a version home
|
|
99
|
+
* so a home-keyed and email-keyed read of the same account share nothing stale.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveClaudeSetupTokenForEmail(email, cacheKey) {
|
|
79
102
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// would misapply one account's setup-token to another.
|
|
83
|
-
const email = readClaudeAccountEmail(home);
|
|
84
|
-
if (!email)
|
|
103
|
+
const trimmed = email.trim();
|
|
104
|
+
if (!trimmed)
|
|
85
105
|
return null;
|
|
86
106
|
if (!bundleExists(AUTH_BUNDLE))
|
|
87
107
|
return null;
|
|
@@ -92,25 +112,26 @@ export function resolveClaudeSetupToken(home) {
|
|
|
92
112
|
// hint that the seeded setup-token was being ignored.
|
|
93
113
|
throw new ReservedBundleWrongBackendError(AUTH_BUNDLE, backend);
|
|
94
114
|
}
|
|
95
|
-
const
|
|
96
|
-
const
|
|
115
|
+
const key = claudeAccountTokenKey(trimmed);
|
|
116
|
+
const ck = cacheKey ?? `email:${trimmed}`;
|
|
117
|
+
const item = secretsKeychainItem(AUTH_BUNDLE, key);
|
|
97
118
|
const credentialPath = fileStoreItemPath(item);
|
|
98
119
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
99
120
|
const before = credentialFingerprint(credentialPath);
|
|
100
|
-
const cached = setupTokenCache.get(
|
|
121
|
+
const cached = setupTokenCache.get(ck);
|
|
101
122
|
if (cached?.credentialPath === credentialPath && cached.fingerprint === before) {
|
|
102
123
|
return cached.token;
|
|
103
124
|
}
|
|
104
125
|
if (before === 'missing') {
|
|
105
|
-
setupTokenCache.set(
|
|
126
|
+
setupTokenCache.set(ck, { credentialPath, fingerprint: before, token: null });
|
|
106
127
|
return null;
|
|
107
128
|
}
|
|
108
129
|
const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
|
|
109
|
-
const v = (env[
|
|
130
|
+
const v = (env[key] ?? '').trim();
|
|
110
131
|
const token = v.length > 0 && isValidClaudeSetupToken(v) ? v : null;
|
|
111
132
|
const after = credentialFingerprint(credentialPath);
|
|
112
133
|
if (before === after) {
|
|
113
|
-
setupTokenCache.set(
|
|
134
|
+
setupTokenCache.set(ck, { credentialPath, fingerprint: after, token });
|
|
114
135
|
return token;
|
|
115
136
|
}
|
|
116
137
|
}
|
|
@@ -124,3 +145,36 @@ export function resolveClaudeSetupToken(home) {
|
|
|
124
145
|
return null;
|
|
125
146
|
}
|
|
126
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Seed a keychain-less Linux worker's Claude version-home identity so an account's
|
|
150
|
+
* fleet-synced setup-token resolves for it. A worker home never had an interactive
|
|
151
|
+
* browser login, so its `.claude.json` carries no `oauthAccount.emailAddress` and
|
|
152
|
+
* the account reads "signed out" even though its non-rotating setup-token is present
|
|
153
|
+
* in the `auth` bundle. This writes ONLY the descriptive identity (the email), merged
|
|
154
|
+
* into both `.claude.json` locations Claude Code reads, preserving every other field.
|
|
155
|
+
* It never copies a rotating OAuth credential (`.credentials.json`) — the setup-token
|
|
156
|
+
* stays the credential of record.
|
|
157
|
+
*/
|
|
158
|
+
export function seedClaudeWorkerHomeIdentity(versionHome, email) {
|
|
159
|
+
const trimmed = email.trim();
|
|
160
|
+
if (!trimmed)
|
|
161
|
+
return;
|
|
162
|
+
for (const p of [
|
|
163
|
+
path.join(versionHome, '.claude', '.claude.json'),
|
|
164
|
+
path.join(versionHome, '.claude.json'),
|
|
165
|
+
]) {
|
|
166
|
+
let doc = {};
|
|
167
|
+
try {
|
|
168
|
+
doc = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Missing or unreadable at this location — write a fresh minimal document.
|
|
172
|
+
}
|
|
173
|
+
const existing = (doc.oauthAccount && typeof doc.oauthAccount === 'object'
|
|
174
|
+
? doc.oauthAccount
|
|
175
|
+
: {});
|
|
176
|
+
doc.oauthAccount = { ...existing, emailAddress: trimmed };
|
|
177
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
178
|
+
fs.writeFileSync(p, JSON.stringify(doc));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -36,7 +36,7 @@ export function parseFamilyList(raw, flagName) {
|
|
|
36
36
|
/** Command-churn event kinds. */
|
|
37
37
|
export const COMMAND_EVENT_TYPES = ['command.start', 'command.end'];
|
|
38
38
|
/** Run-dispatch outcome kinds (replaces the separate audit/log.jsonl product). */
|
|
39
|
-
export const RUN_EVENT_TYPES = ['run.dispatched', 'agent.run.end'];
|
|
39
|
+
export const RUN_EVENT_TYPES = ['run.dispatched', 'run.launch', 'agent.run.end'];
|
|
40
40
|
/**
|
|
41
41
|
* Fold family include/exclude into a UnifiedQuery.
|
|
42
42
|
* Precedence: family narrows sources/types; field filters (module, event, …)
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AgentId, Mode } from './types.js';
|
|
1
|
+
import type { AgentId, Mode, RunStrategy } from './types.js';
|
|
2
|
+
import { type EventPayload } from './feed/events.js';
|
|
2
3
|
import { type UsernsStatus } from './linux-userns.js';
|
|
3
4
|
/**
|
|
4
5
|
* Agent execution modes. Canonical name `skip` (dangerously skip permissions);
|
|
@@ -212,6 +213,31 @@ export interface ExecOptions {
|
|
|
212
213
|
* session. Also forced off by AGENTS_NO_TMUX=1. No effect on headless runs.
|
|
213
214
|
*/
|
|
214
215
|
raw?: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* The run strategy that resolved this launch (pinned/available/balanced).
|
|
218
|
+
* Observability-only: threaded from the `agents run` command purely so the
|
|
219
|
+
* pre-launch `run.launch` event can record HOW the version was chosen. Never
|
|
220
|
+
* read by the spawn itself.
|
|
221
|
+
*/
|
|
222
|
+
strategy?: RunStrategy;
|
|
223
|
+
/**
|
|
224
|
+
* How the launched version was resolved (e.g. 'pinned-default', 'rotated',
|
|
225
|
+
* 'explicit-pin'), when cheaply determinable. Observability-only, carried on
|
|
226
|
+
* `run.launch`. Optional — omitted when the caller can't attribute it.
|
|
227
|
+
*/
|
|
228
|
+
resolvedVia?: string;
|
|
229
|
+
/**
|
|
230
|
+
* Precomputed launchable-signed-in verdict for the launched version, supplied
|
|
231
|
+
* by a caller that already computed it via the identical gate (a rotated pick
|
|
232
|
+
* carries `rotationResult.picked.signedIn`). When present, `run.launch` uses it
|
|
233
|
+
* instead of re-probing the version home — removing a double fs read on the hot
|
|
234
|
+
* path and any disagreement window. `undefined` means "not precomputed" (the
|
|
235
|
+
* pinned-default / shim paths), which makes `emitRunLaunch` fall back to
|
|
236
|
+
* {@link isVersionLaunchableHere}. Observability-only.
|
|
237
|
+
*/
|
|
238
|
+
launchSignedIn?: boolean | null;
|
|
239
|
+
/** Precomputed account email companion to {@link launchSignedIn}. */
|
|
240
|
+
launchEmail?: string | null;
|
|
215
241
|
}
|
|
216
242
|
/**
|
|
217
243
|
* Identity a custom-harness run stamps on env / pid-registry / sidecars.
|
|
@@ -550,6 +576,45 @@ export declare function writeTmuxEnvFile(env: NodeJS.ProcessEnv, filePath: strin
|
|
|
550
576
|
* `[detached]` the pane-died hook otherwise leaves behind.
|
|
551
577
|
*/
|
|
552
578
|
export declare function formatPaneTail(raw: string, maxLines?: number): string;
|
|
579
|
+
/**
|
|
580
|
+
* Spawn an agent process and return its exit code plus a tee'd copy of stderr.
|
|
581
|
+
*
|
|
582
|
+
* Stderr is always piped so the caller can inspect it (e.g., for rate-limit
|
|
583
|
+
* detection) while also forwarding every chunk to process.stderr in real time --
|
|
584
|
+
* the user sees the same output they would with stdio: 'inherit'. Stdout keeps
|
|
585
|
+
* the original behavior: 'pipe' when downstream output is piped (so `agents
|
|
586
|
+
* run ... | ...` composes cleanly), otherwise 'inherit' so TTY output is
|
|
587
|
+
* unbuffered.
|
|
588
|
+
*/
|
|
589
|
+
/** Inputs the pre-launch `run.launch` payload is built from. */
|
|
590
|
+
export interface RunLaunchInput {
|
|
591
|
+
agent: AgentId;
|
|
592
|
+
harnessName?: string;
|
|
593
|
+
/** The version being launched, or undefined when none could be resolved. */
|
|
594
|
+
version?: string;
|
|
595
|
+
strategy?: RunStrategy;
|
|
596
|
+
/**
|
|
597
|
+
* Whether the launched version is launchable-signed-in on THIS device
|
|
598
|
+
* ({@link isVersionLaunchableHere}). `null` when the verdict is unknown — a
|
|
599
|
+
* missing verdict must NOT be read as logged out.
|
|
600
|
+
*/
|
|
601
|
+
signedIn: boolean | null;
|
|
602
|
+
/** Account email of the version home when signed in, else null. */
|
|
603
|
+
email: string | null;
|
|
604
|
+
/** How the version was resolved (explicit-pin / rotated / pinned-default). */
|
|
605
|
+
resolvedVia?: string;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Build the `run.launch` event payload. Pure and exported so the signedIn ->
|
|
609
|
+
* launchedLoggedOut mapping is unit-testable without spawning. Mirrors the
|
|
610
|
+
* buildRotationDecisionEvent / emitRotationDecision split in rotate.ts.
|
|
611
|
+
*
|
|
612
|
+
* `launchedLoggedOut` is the headline flag — true ONLY when `signedIn` was
|
|
613
|
+
* resolved to false (a launch into a logged-out version, the yosemite-m3 2.1.219
|
|
614
|
+
* incident). An UNKNOWN verdict (`signedIn === null`) is never treated as
|
|
615
|
+
* logged out.
|
|
616
|
+
*/
|
|
617
|
+
export declare function buildRunLaunchPayload(input: RunLaunchInput): EventPayload;
|
|
553
618
|
/** Exit code spawnAgent resolves with when a run is killed for crossing a budget cap. */
|
|
554
619
|
export declare const BUDGET_KILL_EXIT_CODE = 7;
|
|
555
620
|
/**
|