@phnx-labs/agents-cli 1.22.89 → 1.22.91
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 +93 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +38 -0
- package/dist/lib/accounts/migrate.js +99 -0
- package/dist/lib/auth-mint.d.ts +22 -1
- package/dist/lib/auth-mint.js +85 -10
- package/dist/lib/daemon/auth-sync-service.js +2 -0
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +40 -29
- package/dist/lib/secrets-client.d.ts +13 -0
- package/dist/lib/secrets-client.js +17 -2
- package/dist/lib/secrets-policy.d.ts +17 -0
- package/dist/lib/secrets-policy.js +14 -1
- package/dist/lib/secrets-types.d.ts +8 -0
- package/dist/lib/sessions-client.d.ts +13 -0
- package/dist/lib/sessions-client.js +107 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +7 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.91
|
|
4
|
+
|
|
5
|
+
- **`agents sessions` search/list execs the standalone `sessions` CLI (PHNX-4012).** Read queries intercept in `index.ts` before bootstrap so they skip the 6K-line in-process engine. Lifecycle verbs (`resume`, `stop`, `inject`, `watch`, `--active`, `--markdown`) still load the in-repo command. The `sessions` alias shim is retired like `secrets` so it cannot recurse. Install: `npm i -g @phnx-labs/sessions-cli`. Source: `cli/src/index.ts`, `cli/src/lib/sessions-client.ts`, `cli/scripts/postinstall.js`.
|
|
6
|
+
|
|
7
|
+
## 1.22.90
|
|
8
|
+
|
|
9
|
+
- **`accounts migrate --apply` no longer aborts on a worker whose accounts already hold slots (PHNX-3940).** The daemon's auth-sync provisions a durable slot per account on every worker, so `--apply` hit `Slot already exists` on the first credential-bearing legacy home and cleaned nothing. The plan now routes such a home to `trash` (restorable with `agents trash restore`) with the reason "account already holds a provisioned slot on this device", rewrites any `<harness>@<label>` binding to the account, and clears the stale home record; a canonical install in that state keeps its binary, has its stale home moved to `~/.agents/.history/trash/homes/…` (manifest key `<harness>@<label>#home`), and gets an empty home back. A regular file at the slot path is still not a slot, so the apply-time guard still fails loud on that corruption. Source: `cli/src/lib/accounts/migrate.ts`.
|
|
10
|
+
|
|
11
|
+
- **AGI Menu: Sessions window, feed-driven RECENT, Notifications submenu (PHNX-4003).**
|
|
12
|
+
`Cmd-Shift-I` (or the new **Sessions** row at the top of the dropdown) opens a
|
|
13
|
+
persistent Sessions window: live sessions across the fleet grouped by project on
|
|
14
|
+
the left, with **Recent / Needs you / Running / Done today** filter chips, a
|
|
15
|
+
`Cmd-F` search (label, topic, project, ticket, device), and a device strip that
|
|
16
|
+
marks stale heartbeats; a session card on the right shows the phase, PR chip,
|
|
17
|
+
request excerpt, progress tally, latest action, today's rendered artifacts with an
|
|
18
|
+
**Open** button, and the attention block when the agent is waiting — numbered
|
|
19
|
+
option buttons, permission Allow/Deny, plan-review Approve/Send back — answered
|
|
20
|
+
atomically through `agents feed answer`. A reply field routes by capability
|
|
21
|
+
(`agents feed answer`, `agents sessions inject`, `agents cloud message`,
|
|
22
|
+
`agents teams message`) and shows the CLI's own result inline; keys `1`–`9`,
|
|
23
|
+
`Return`, `Cmd-Return`, `Cmd-K` answer, reply, open the terminal, or nudge. The
|
|
24
|
+
dropdown's RECENT section now renders the same feed-driven rows (status dot,
|
|
25
|
+
`n/N` checklist, `working · 18h · host`, latest action + PR chip) grouped by
|
|
26
|
+
project, each opening the window on that session, and a **Notifications**
|
|
27
|
+
submenu lists today's feed banners unanswered-first. Blocked sessions leave the
|
|
28
|
+
NEEDS YOU strip, which keeps only load, scheduler, and routine triage.
|
|
29
|
+
Source: `cli/menubar/Sources/MenubarHelper/SessionsWindow.swift`,
|
|
30
|
+
`cli/menubar/Sources/MenubarHelper/SessionCard.swift`,
|
|
31
|
+
`cli/menubar/Sources/MenubarHelper/SessionRowModel.swift`,
|
|
32
|
+
`cli/menubar/Sources/MenubarHelper/Reply.swift`,
|
|
33
|
+
`cli/menubar/Sources/MenubarHelper/RecentSectionBuilder.swift`.
|
|
34
|
+
|
|
35
|
+
- **AGI Menu delivers attention banners on `UNUserNotificationCenter` with real
|
|
36
|
+
action buttons (PHNX-4004).** The deprecated `NSUserNotification` toast is
|
|
37
|
+
replaced for actionable notices: the banner carries the mapped category, a
|
|
38
|
+
time-sensitive interruption level for a blocked ask, a per-session thread so a
|
|
39
|
+
session's banners group, and the harness avatar as an attachment. The
|
|
40
|
+
persistent menu-bar instance is the delegate; a tapped action runs one bounded
|
|
41
|
+
`agents feed answer` / `agents open` command off the delegate thread and posts
|
|
42
|
+
a "Could not deliver your reply" follow-up when it fails, or a "Could not open
|
|
43
|
+
the report/PR" follow-up when a done banner carried no link for that button.
|
|
44
|
+
When macOS declines notification authorization it degrades to the old plain
|
|
45
|
+
banner so a notice is never lost.
|
|
46
|
+
Source: `menubar/Sources/MenubarHelper/Notifier.swift`,
|
|
47
|
+
`menubar/Sources/MenubarHelper/AgentAvatar.swift`.
|
|
48
|
+
|
|
49
|
+
- **AGI Menu palette: dispatch form with remembered defaults, project pulse, and session-id minting (PHNX-4005).**
|
|
50
|
+
The `Cmd-Shift-O` palette now carries the dimensions a dispatch actually needs
|
|
51
|
+
below its text field — **mode** (Plan / Auto / Edit), **run on** (This Mac,
|
|
52
|
+
`auto`, or a fleet device by name), **surface** (Interactive / Headless), and
|
|
53
|
+
**watchdog** (Off / Keep moving / Hands-off) — collapsed to one summary line
|
|
54
|
+
(`Claude 2.1.263 · this-mac · agi · Auto · Interactive · Keep moving`) with a
|
|
55
|
+
`▸ details` disclosure. Every dimension is **remembered per project**, so
|
|
56
|
+
switching projects restores the agent, mode, and target you last used there and
|
|
57
|
+
one keystroke dispatches. `Cmd-P` switches **this one dispatch** to Plan without
|
|
58
|
+
touching the remembered defaults. A Claude dispatch mints its session id and
|
|
59
|
+
passes `--session-id`, which lets the palette apply a non-default watchdog policy
|
|
60
|
+
at dispatch and register a **launching placeholder** with the feed stream: the
|
|
61
|
+
row shows in the project pulse strip as `◌ launching · <name>` (and in any
|
|
62
|
+
window that reads the feed rows) until the real session reports, and reads
|
|
63
|
+
"did not start" — with the CLI's own stderr — if the launch exits non-zero or
|
|
64
|
+
no row arrives within 60 s. Under the project row a **pulse strip** shows the
|
|
65
|
+
headline milestone from `agents projects status`, open tickets and PRs, live
|
|
66
|
+
running / need-you / idle counts with NOW chips (not-progressing first), and
|
|
67
|
+
the NEXT UP ticket (`Cmd-N` attaches).
|
|
68
|
+
Source: `cli/menubar/Sources/MenubarHelper/DispatchForm.swift`,
|
|
69
|
+
`cli/menubar/Sources/MenubarHelper/DispatchDefaults.swift`,
|
|
70
|
+
`cli/menubar/Sources/MenubarHelper/ProjectPulse.swift`,
|
|
71
|
+
`cli/menubar/Sources/MenubarHelper/FeedStream.swift`,
|
|
72
|
+
`cli/menubar/Sources/MenubarHelper/AgentsCLI.swift`.
|
|
73
|
+
|
|
74
|
+
- **AGI Menu palette: find a screenshot by the text on it (PHNX-4006).**
|
|
75
|
+
Every capture in the palette's screenshot folders (the system `screencapture`
|
|
76
|
+
location, CleanShot's export path, and the clip attachments dir) is now OCR'd
|
|
77
|
+
once, on-device, and indexed so you can search screenshots by the words on them.
|
|
78
|
+
With the palette key, `Cmd-Shift-F` focuses a search field above the strip:
|
|
79
|
+
typing filters the thumbnails to captures whose recognized text matches every
|
|
80
|
+
token (case-insensitive, tokenized AND), newest first, with an `n of m match`
|
|
81
|
+
hint; clearing it restores the newest-six strip. A **more** disclosure expands
|
|
82
|
+
the strip into a grid grouped by hour (last 24 h) then day — each cell shows the
|
|
83
|
+
thumbnail, the time, and the first recognized line — where a click attaches and a
|
|
84
|
+
double-click previews the full image in Preview.app. Nothing leaves the machine:
|
|
85
|
+
recognition runs through Vision locally (`.fast`, no language correction) on a
|
|
86
|
+
background queue, images over 20 MB are skipped and the rest downsampled to
|
|
87
|
+
2000 px before recognition, each capture is keyed by the SHA-256 of its bytes so
|
|
88
|
+
a rename or duplicate is never OCR'd twice, a deleted capture drops out of the
|
|
89
|
+
index (search and grid never return it, and the next scan prunes its row), and
|
|
90
|
+
the index is a local SQLite file at `~/.agents/.history/menubar/screenshots.db`.
|
|
91
|
+
Source:
|
|
92
|
+
`cli/menubar/Sources/MenubarHelper/ScreenshotIndex.swift`,
|
|
93
|
+
`cli/menubar/Sources/MenubarHelper/ScreenshotGrid.swift`,
|
|
94
|
+
`cli/menubar/Sources/MenubarHelper/PromptPanel.swift`.
|
|
95
|
+
|
|
3
96
|
## 1.22.89
|
|
4
97
|
|
|
5
98
|
- **Worker daemons now actually relaunch onto a new agents-cli release, so a fix
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* - `__usage-ingest` / `__usage-export`
|
|
17
17
|
* - `__harness-update-run`
|
|
18
18
|
* - `__daemon-run`
|
|
19
|
+
* - `sessions` (read queries only — PHNX-4012)
|
|
19
20
|
*
|
|
20
21
|
* The synchronous secrets-broker fast paths (`__secrets-get` / `__secrets-ping`
|
|
21
22
|
* / `__secrets-lock`) and `__vault-age-helper` moved out of this CLI entirely
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* - `__usage-ingest` / `__usage-export`
|
|
17
17
|
* - `__harness-update-run`
|
|
18
18
|
* - `__daemon-run`
|
|
19
|
+
* - `sessions` (read queries only — PHNX-4012)
|
|
19
20
|
*
|
|
20
21
|
* The synchronous secrets-broker fast paths (`__secrets-get` / `__secrets-ping`
|
|
21
22
|
* / `__secrets-lock`) and `__vault-age-helper` moved out of this CLI entirely
|
|
@@ -131,6 +132,43 @@ if (process.argv[2] === '__daemon-run') {
|
|
|
131
132
|
}
|
|
132
133
|
process.exit(process.exitCode ?? 0);
|
|
133
134
|
}
|
|
135
|
+
// PHNX-4012: search/list/id lookup of `agents sessions` execs the standalone
|
|
136
|
+
// `sessions` binary without loading bootstrap or the 6K-line sessions module.
|
|
137
|
+
// Lifecycle verbs (resume/stop/inject/watch/--active/--markdown) fall through.
|
|
138
|
+
if (process.argv[2] === 'sessions') {
|
|
139
|
+
const forwarded = process.argv.slice(3);
|
|
140
|
+
const { isReadQuery, resolveSessionsBin, invocation, SessionsClientError, SESSIONS_INSTALL_HINT, } = await import('./lib/sessions-client.js');
|
|
141
|
+
if (isReadQuery(forwarded)) {
|
|
142
|
+
const { spawnSync } = await import('node:child_process');
|
|
143
|
+
let bin = null;
|
|
144
|
+
try {
|
|
145
|
+
bin = resolveSessionsBin();
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
// No standalone on this box: fall through to the in-repo engine below
|
|
149
|
+
// rather than refusing the query. The fast path is an optimization for a
|
|
150
|
+
// box that has `sessions` installed (skips bootstrap and the sessions
|
|
151
|
+
// module); a box without it must still answer `agents sessions --json`
|
|
152
|
+
// exactly as it did before PHNX-4012 — every worker in the fleet and
|
|
153
|
+
// every CI runner is such a box until @phnx-labs/sessions-cli is
|
|
154
|
+
// published, and refusing here turned main's full suite red.
|
|
155
|
+
if (!(err instanceof SessionsClientError && err.code === 'SESSIONS_BIN_MISSING'))
|
|
156
|
+
throw err;
|
|
157
|
+
if (process.env.AGENTS_SESSIONS_FASTPATH_HINT !== '0') {
|
|
158
|
+
process.stderr.write(`agents: standalone \`sessions\` not installed; using the in-process engine (${SESSIONS_INSTALL_HINT})\n`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (bin) {
|
|
162
|
+
const { command, prefix } = invocation(bin);
|
|
163
|
+
const res = spawnSync(command, [...prefix, ...forwarded], { stdio: 'inherit' });
|
|
164
|
+
if (res.error) {
|
|
165
|
+
process.stderr.write(`Failed to run \`sessions\`: ${res.error.message}\n`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
process.exit(res.status ?? 1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
134
172
|
// Full CLI: commander tree, update checks, migrations, parse. Static imports
|
|
135
173
|
// inside bootstrap.js evaluate only when we reach this line.
|
|
136
174
|
await import('./bootstrap.js');
|
|
@@ -163,6 +163,30 @@ async function duplicateTrashLabels(items) {
|
|
|
163
163
|
function plannedTrashPath(agent, label) {
|
|
164
164
|
return path.join(getHistoryDir(), 'trash', 'versions', agent, label, '<stamp>');
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Where a canonical install's stale `home/` goes when its account already holds
|
|
168
|
+
* a slot: the binary and the (recreated, empty) home stay under `versions/`, so
|
|
169
|
+
* the old home cannot use the versions trash — `agents trash restore` would put a
|
|
170
|
+
* whole install back over a live one. Restoring is a plain move back; the
|
|
171
|
+
* migration manifest records the path under `<agent>@<label>#home`.
|
|
172
|
+
*/
|
|
173
|
+
function homeTrashDir(agent, label) {
|
|
174
|
+
return path.join(getHistoryDir(), 'trash', 'homes', agent, label);
|
|
175
|
+
}
|
|
176
|
+
function plannedHomeTrashPath(agent, label) {
|
|
177
|
+
return path.join(homeTrashDir(agent, label), '<stamp>');
|
|
178
|
+
}
|
|
179
|
+
function trashHome(agent, item) {
|
|
180
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
181
|
+
const dest = path.join(homeTrashDir(agent, item.label), stamp);
|
|
182
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
183
|
+
if (!fs.existsSync(item.home)) {
|
|
184
|
+
throw new Error(`Home ${item.home} is missing; cannot trash it.`);
|
|
185
|
+
}
|
|
186
|
+
fs.renameSync(item.home, dest);
|
|
187
|
+
fs.mkdirSync(item.home, { recursive: true, mode: 0o700 });
|
|
188
|
+
return dest;
|
|
189
|
+
}
|
|
166
190
|
function emptyDir(dir) {
|
|
167
191
|
if (!fs.existsSync(dir))
|
|
168
192
|
return true;
|
|
@@ -180,6 +204,23 @@ function assertSlotAbsent(agent, accountId) {
|
|
|
180
204
|
}
|
|
181
205
|
return dest;
|
|
182
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* True when this device already holds a populated slot directory for the
|
|
209
|
+
* account. The daemon's auth-sync provisions durable worker slots on its own
|
|
210
|
+
* (PHNX-3940), so by the time an operator runs `--apply` a worker commonly has
|
|
211
|
+
* a slot for every account whose legacy per-version home is still on disk.
|
|
212
|
+
* A regular file at the slot path is NOT a slot: the plan keeps routing such a
|
|
213
|
+
* home to `slot` so the apply-time guard fails loud on the corruption.
|
|
214
|
+
*/
|
|
215
|
+
function provisionedSlotExists(agent, accountId) {
|
|
216
|
+
const dest = slotDir(agent, accountId);
|
|
217
|
+
try {
|
|
218
|
+
return fs.statSync(dest).isDirectory() && fs.readdirSync(dest).length > 0;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
183
224
|
function takenNames(meta) {
|
|
184
225
|
const taken = new Set();
|
|
185
226
|
for (const row of listNativeAccounts(meta)) {
|
|
@@ -255,6 +296,42 @@ async function planHarness(agent, deps) {
|
|
|
255
296
|
});
|
|
256
297
|
continue;
|
|
257
298
|
}
|
|
299
|
+
if (item.hasCredential && item.accountId && provisionedSlotExists(agent, item.accountId)) {
|
|
300
|
+
// The account already owns a populated slot on this device, so this
|
|
301
|
+
// per-version home is a stale copy of a credential the slot now carries.
|
|
302
|
+
// Routing it to `slot` would only trip `assertSlotAbsent` and abort the
|
|
303
|
+
// whole apply with nothing done — the state every auth-synced worker was
|
|
304
|
+
// in. Trash it instead (`agents trash restore` reverses). The canonical
|
|
305
|
+
// install keeps its binary and ends up with an empty home, exactly as it
|
|
306
|
+
// does when its home is the one that moves into the slot: two on-disk
|
|
307
|
+
// copies of one credential is not an end state the migration leaves.
|
|
308
|
+
if (canonical && item.label === canonical.label) {
|
|
309
|
+
actions.push({
|
|
310
|
+
kind: 'canonical',
|
|
311
|
+
label: item.label,
|
|
312
|
+
release: item.release,
|
|
313
|
+
reason: 'canonical install (binary kept); account already holds a slot — stale home trashed, empty home recreated',
|
|
314
|
+
accountId: item.accountId,
|
|
315
|
+
identityKey: item.identityKey,
|
|
316
|
+
email: item.email,
|
|
317
|
+
sessionCount: item.sessionCount,
|
|
318
|
+
pathMoves: [{ from: item.home, to: plannedHomeTrashPath(agent, item.label) }],
|
|
319
|
+
});
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
actions.push({
|
|
323
|
+
kind: 'trash',
|
|
324
|
+
label: item.label,
|
|
325
|
+
release: item.release,
|
|
326
|
+
reason: 'account already holds a provisioned slot on this device — stale home trashed',
|
|
327
|
+
accountId: item.accountId,
|
|
328
|
+
identityKey: item.identityKey,
|
|
329
|
+
email: item.email,
|
|
330
|
+
sessionCount: item.sessionCount,
|
|
331
|
+
pathMoves: [{ from: item.dir, to: plannedTrashPath(agent, item.label) }],
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
258
335
|
if (item.hasCredential && item.identityKey) {
|
|
259
336
|
const dest = item.accountId ? slotDir(agent, item.accountId) : slotDir(agent, '<new-account>');
|
|
260
337
|
actions.push({
|
|
@@ -494,6 +571,21 @@ export async function applyAccountMigration(agents = [...ALL_AGENT_IDS], deps =
|
|
|
494
571
|
if (kept)
|
|
495
572
|
labelToAccount.set(item.label, kept);
|
|
496
573
|
}
|
|
574
|
+
for (const action of h.actions.filter((a) => a.kind === 'canonical' && a.accountId)) {
|
|
575
|
+
// Canonical install whose account already holds a slot: hand the stale
|
|
576
|
+
// home off to the homes trash and leave an empty home behind, mirroring
|
|
577
|
+
// the slot branch's end state for a canonical home.
|
|
578
|
+
const item = byLabel.get(action.label);
|
|
579
|
+
if (!item)
|
|
580
|
+
throw new Error(`Plan named ${h.agent}@${action.label} but inventory has no such install.`);
|
|
581
|
+
const trashPath = trashHome(h.agent, item);
|
|
582
|
+
remaps.push({ from: item.home, to: trashPath });
|
|
583
|
+
labelToAccount.set(item.label, action.accountId);
|
|
584
|
+
movedAccountIds.push(action.accountId);
|
|
585
|
+
entry.trashed.push({ label: item.label, reason: 'stale canonical home — account already holds a provisioned slot', trashPath });
|
|
586
|
+
manifest.map[`${h.agent}@${item.label}#home`] = trashPath;
|
|
587
|
+
persistManifest(manifestPath, manifest);
|
|
588
|
+
}
|
|
497
589
|
for (const action of trashActions) {
|
|
498
590
|
const item = byLabel.get(action.label);
|
|
499
591
|
if (!item)
|
|
@@ -504,6 +596,13 @@ export async function applyAccountMigration(agents = [...ALL_AGENT_IDS], deps =
|
|
|
504
596
|
removeVersionedAlias(h.agent, item.label);
|
|
505
597
|
remaps.push({ from: item.dir, to: trashPath });
|
|
506
598
|
stillPresent.delete(item.label);
|
|
599
|
+
if (action.accountId) {
|
|
600
|
+
// A home trashed because its account already holds a slot: anything
|
|
601
|
+
// bound to this version label follows the account, and the stale
|
|
602
|
+
// home record clears so spawn resolves through the slot alone.
|
|
603
|
+
labelToAccount.set(item.label, action.accountId);
|
|
604
|
+
movedAccountIds.push(action.accountId);
|
|
605
|
+
}
|
|
507
606
|
entry.trashed.push({ label: item.label, reason: action.reason, trashPath });
|
|
508
607
|
manifest.map[`${h.agent}@${item.label}`] = trashPath;
|
|
509
608
|
persistManifest(manifestPath, manifest);
|
package/dist/lib/auth-mint.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* stored and optionally synced. Interactive mint is Claude-only; every other
|
|
14
14
|
* harness fails loud with the command that actually provisions it.
|
|
15
15
|
*/
|
|
16
|
-
import type { AgentId } from './types.js';
|
|
16
|
+
import type { AgentId, Meta } from './types.js';
|
|
17
17
|
import { type CredentialAccount } from './account-registry.js';
|
|
18
18
|
import { type StorableCredentialKind } from './secrets-policy.js';
|
|
19
19
|
import { type DriveOptions, type PtyDriver } from './fleet/remote-login.js';
|
|
@@ -110,6 +110,27 @@ export declare function seedReservedStoreKey(harness: AgentId, kind: StorableCre
|
|
|
110
110
|
bundle: string;
|
|
111
111
|
key: string;
|
|
112
112
|
};
|
|
113
|
+
export interface AdoptLegacyReservedItemsResult {
|
|
114
|
+
adopted: Array<{
|
|
115
|
+
bundle: string;
|
|
116
|
+
key: string;
|
|
117
|
+
}>;
|
|
118
|
+
errors: Array<{
|
|
119
|
+
bundle: string;
|
|
120
|
+
key: string;
|
|
121
|
+
message: string;
|
|
122
|
+
}>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Adopt reserved-store keys written by 1.22.84–1.22.89 as bare file items (no
|
|
126
|
+
* bundle record) into the file-backed bundle the daemon push reads. Every
|
|
127
|
+
* `accounts add codex|grok|cursor` from those releases left its worker key in
|
|
128
|
+
* that shape on the laptop, unpushable; the item name is the same in both
|
|
129
|
+
* shapes, so adoption re-seeds the bundle from the raw value in place and
|
|
130
|
+
* nothing already on a worker changes. Idempotent: a key the bundle already
|
|
131
|
+
* carries is skipped. Local data repair only — it never talks to a peer.
|
|
132
|
+
*/
|
|
133
|
+
export declare function adoptLegacyReservedStoreItems(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): AdoptLegacyReservedItemsResult;
|
|
113
134
|
/**
|
|
114
135
|
* Create or rotate the named provider account that `agents run --account` and
|
|
115
136
|
* `agents accounts sync` consume. Existing account of a different kind fails
|
package/dist/lib/auth-mint.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveAgentName } from './agents.js';
|
|
2
|
-
import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, invalidateClaudeSetupTokenCache, } from './claude-account-token.js';
|
|
3
|
-
import { addAccount, findAccount, readAccountRegistry, setAccountSecret } from './account-registry.js';
|
|
4
|
-
import { bundleBackendSync, bundleExists, bundleExistsSync, keychainRef, pushBundleToHost, readBundleSync, rotateBundleSecretSync, secretsKeychainItem,
|
|
2
|
+
import { AUTH_BUNDLE, claudeAccountTokenKey, isValidClaudeSetupToken, readClaudeAccountEmail, readReservedCredential, invalidateClaudeSetupTokenCache, } from './claude-account-token.js';
|
|
3
|
+
import { addAccount, findAccount, listNativeAccounts, readAccountRegistry, setAccountSecret } from './account-registry.js';
|
|
4
|
+
import { bundleBackendSync, isSecretsClientError, bundleExists, bundleExistsSync, keychainRef, pushBundleToHost, readBundleSync, rotateBundleSecretSync, secretsKeychainItem, writeBundleWithItemsSync, } from './secrets-client.js';
|
|
5
5
|
import { assertStorableCredentialKind, reservedStoreName, } from './secrets-policy.js';
|
|
6
6
|
import { getBinaryPath, getGlobalDefault, getVersionHomePath, listInstalledVersions } from './installations/versions.js';
|
|
7
7
|
import { shellQuote } from './ssh-exec.js';
|
|
@@ -243,13 +243,88 @@ export function seedReservedStoreKey(harness, kind, key, value) {
|
|
|
243
243
|
const cleaned = value.trim();
|
|
244
244
|
if (!cleaned)
|
|
245
245
|
throw new Error(`Empty ${kind} for reserved store '${name}' key ${key}.`);
|
|
246
|
-
// A `__<harness>__` reserved store is
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
// A `__<harness>__` reserved store is a real FILE-backed, policy-`never` bundle
|
|
247
|
+
// — the same shape as the legacy `auth` bundle — because the only transport to
|
|
248
|
+
// a worker is the ordinary bundle push (`syncReservedStores` →
|
|
249
|
+
// `pushBundleToHost`), which reads the store as a bundle and imports it
|
|
250
|
+
// remotely as one. It used to be written as a bare `store.set` item with no
|
|
251
|
+
// bundle record (the standalone rejected the `__`-wrapped name at the time), so
|
|
252
|
+
// the push could never read it and no worker received a Cursor/Codex/Grok key.
|
|
253
|
+
// The item name is unchanged (`agents-cli.secrets.__<harness>__.<KEY>`), so
|
|
254
|
+
// `readReservedCredential` and every worker that already holds the raw item
|
|
255
|
+
// keep resolving it. Requires -labs/secrets-cli >= 0.1.1 (reserved-shape
|
|
256
|
+
// bundle names).
|
|
257
|
+
const item = secretsKeychainItem(name, key);
|
|
258
|
+
try {
|
|
259
|
+
if (bundleExistsSync(name)) {
|
|
260
|
+
const backend = bundleBackendSync(name);
|
|
261
|
+
if (backend !== 'file') {
|
|
262
|
+
throw new Error(`Reserved store '${name}' exists with backend '${backend}', but worker provisioning only reads a FILE-backed store. Recreate it with: agents secrets delete ${name} --yes`);
|
|
263
|
+
}
|
|
264
|
+
const bundle = readBundleSync(name);
|
|
265
|
+
if (key in bundle.vars) {
|
|
266
|
+
rotateBundleSecretSync(bundle, key, { newValue: cleaned, meta: { type: 'token' } });
|
|
267
|
+
return { bundle: name, key };
|
|
268
|
+
}
|
|
269
|
+
bundle.vars[key] = keychainRef(key);
|
|
270
|
+
if (!bundle.meta)
|
|
271
|
+
bundle.meta = {};
|
|
272
|
+
bundle.meta[key] = { type: 'token' };
|
|
273
|
+
writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
|
|
274
|
+
return { bundle: name, key };
|
|
275
|
+
}
|
|
276
|
+
const bundle = {
|
|
277
|
+
name,
|
|
278
|
+
backend: 'file',
|
|
279
|
+
policy: 'never',
|
|
280
|
+
description: `Reserved ${harness} worker credentials (${kind}), one key per account; pushed to worker devices by the daemon. Never a native OAuth session.`,
|
|
281
|
+
vars: { [key]: keychainRef(key) },
|
|
282
|
+
meta: { [key]: { type: 'token' } },
|
|
283
|
+
};
|
|
284
|
+
writeBundleWithItemsSync(bundle, new Map([[item, cleaned]]));
|
|
285
|
+
return { bundle: name, key };
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
// A standalone older than 0.1.1 rejects the `__<harness>__` bundle name and
|
|
289
|
+
// the client only sees a sanitized code. Name the real requirement instead
|
|
290
|
+
// of leaving `accounts add` to fail with `OPERATION_FAILED`.
|
|
291
|
+
if (isSecretsClientError(err, 'OPERATION_FAILED')) {
|
|
292
|
+
throw new Error(`Could not write the reserved store '${name}' as a bundle: ${err.message}. agents-cli needs @phnx-labs/secrets-cli 0.1.1 or newer, which accepts the __<harness>__ bundle name: npm i -g @phnx-labs/secrets-cli@latest`);
|
|
293
|
+
}
|
|
294
|
+
throw err;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Adopt reserved-store keys written by 1.22.84–1.22.89 as bare file items (no
|
|
299
|
+
* bundle record) into the file-backed bundle the daemon push reads. Every
|
|
300
|
+
* `accounts add codex|grok|cursor` from those releases left its worker key in
|
|
301
|
+
* that shape on the laptop, unpushable; the item name is the same in both
|
|
302
|
+
* shapes, so adoption re-seeds the bundle from the raw value in place and
|
|
303
|
+
* nothing already on a worker changes. Idempotent: a key the bundle already
|
|
304
|
+
* carries is skipped. Local data repair only — it never talks to a peer.
|
|
305
|
+
*/
|
|
306
|
+
export function adoptLegacyReservedStoreItems(meta) {
|
|
307
|
+
const result = { adopted: [], errors: [] };
|
|
308
|
+
for (const account of listNativeAccounts(meta)) {
|
|
309
|
+
const cred = account.workerCredential;
|
|
310
|
+
if (!cred || !cred.bundle.startsWith('__'))
|
|
311
|
+
continue;
|
|
312
|
+
if (cred.kind !== 'setup-token' && cred.kind !== 'api-key')
|
|
313
|
+
continue;
|
|
314
|
+
try {
|
|
315
|
+
if (bundleExistsSync(cred.bundle) && cred.key in readBundleSync(cred.bundle).vars)
|
|
316
|
+
continue;
|
|
317
|
+
const value = readReservedCredential(cred.bundle, cred.key);
|
|
318
|
+
if (value === null)
|
|
319
|
+
continue;
|
|
320
|
+
seedReservedStoreKey(account.agent, cred.kind, cred.key, value);
|
|
321
|
+
result.adopted.push({ bundle: cred.bundle, key: cred.key });
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
result.errors.push({ bundle: cred.bundle, key: cred.key, message: err.message });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
253
328
|
}
|
|
254
329
|
/**
|
|
255
330
|
* Create or rotate the named provider account that `agents run --account` and
|
|
@@ -75,6 +75,8 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
75
75
|
// the only transport — provisioning (above) writes only locally (invariant 1).
|
|
76
76
|
try {
|
|
77
77
|
const stores = await syncReservedStores();
|
|
78
|
+
if (stores.adopted.length > 0)
|
|
79
|
+
ctx.log('INFO', `auth-sync: adopted ${stores.adopted.length} legacy reserved item(s) into their bundle: ${stores.adopted.map((a) => `${a.bundle} ${a.key}`).join(', ')}`);
|
|
78
80
|
for (const p of stores.pushed)
|
|
79
81
|
ctx.log('INFO', `auth-sync: pushed ${p.bundle} (${p.keys.length} key(s)) to ${p.device}`);
|
|
80
82
|
for (const err of stores.errors)
|
|
@@ -179,7 +179,7 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
179
179
|
* sorted alphabetically before the real self-updated grok binary and
|
|
180
180
|
* was silently launched instead.
|
|
181
181
|
*/
|
|
182
|
-
export declare const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
182
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 20;
|
|
183
183
|
/**
|
|
184
184
|
* Agents whose config directory can be relocated by an environment variable
|
|
185
185
|
* (the per-agent `managedEnv` block in `generateVersionedAliasScript` below).
|
|
@@ -1095,7 +1095,7 @@ export function removeShim(agent) {
|
|
|
1095
1095
|
// v18 — Cursor aliases select the file credential store and swap HOME to the
|
|
1096
1096
|
// version home because current Cursor writes auth.json under ~/.cursor
|
|
1097
1097
|
// and ignores XDG_CONFIG_HOME for credential storage.
|
|
1098
|
-
export const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
1098
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 20;
|
|
1099
1099
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
1100
1100
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
1101
1101
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -1220,52 +1220,56 @@ export function generateVersionedAliasScript(agent, version) {
|
|
|
1220
1220
|
# already sources this same adapter block, so sharing it keeps the two in sync.
|
|
1221
1221
|
# plain (not exported) so it feeds shimConfigEnvBash below but never leaks into the
|
|
1222
1222
|
# launched agent process — matching the main shim's convention.
|
|
1223
|
-
VERSION_DIR="$
|
|
1223
|
+
VERSION_DIR="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}"
|
|
1224
1224
|
${resolveHarnessAdapter(agent).shimConfigEnvBash?.({ configDirName }) ?? ''}`
|
|
1225
1225
|
: agent === 'codex'
|
|
1226
|
-
? codexHomeShimBash(`$
|
|
1226
|
+
? codexHomeShimBash(`$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}`, `$AGENTS_REAL_HOME/.agents/.codex-homes/${version}`)
|
|
1227
1227
|
: agent === 'copilot'
|
|
1228
1228
|
? `
|
|
1229
1229
|
# Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
|
|
1230
1230
|
# session-state, logs). Point direct aliases at the versioned home so per-
|
|
1231
1231
|
# version MCP and session state are isolated.
|
|
1232
|
-
export COPILOT_HOME="$
|
|
1232
|
+
export COPILOT_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
1233
1233
|
`
|
|
1234
1234
|
: agent === 'grok'
|
|
1235
1235
|
? `
|
|
1236
1236
|
# Grok Build uses GROK_HOME to isolate its entire configuration tree (skills,
|
|
1237
1237
|
# hooks, plugins, agents, memory, sessions, config.toml, MCP). Point direct
|
|
1238
1238
|
# aliases at the versioned home for isolation parity with the main shim.
|
|
1239
|
-
export GROK_HOME="$
|
|
1239
|
+
export GROK_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
1240
1240
|
`
|
|
1241
1241
|
: agent === 'opencode'
|
|
1242
1242
|
? `
|
|
1243
1243
|
# OpenCode reads plugins, agents, commands, and other config-directory
|
|
1244
1244
|
# resources from OPENCODE_CONFIG_DIR. Point direct aliases at the versioned
|
|
1245
1245
|
# global config tree where agents-cli syncs OpenCode resources.
|
|
1246
|
-
export OPENCODE_CONFIG_DIR="$
|
|
1246
|
+
export OPENCODE_CONFIG_DIR="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/.config/opencode"
|
|
1247
1247
|
`
|
|
1248
1248
|
: agent === 'kimi'
|
|
1249
1249
|
? `
|
|
1250
1250
|
# Kimi Code CLI honors KIMI_CODE_HOME to relocate ~/.kimi-code (config.toml,
|
|
1251
1251
|
# mcp.json, sessions, skills, hooks). Point direct aliases at the versioned home.
|
|
1252
|
-
export KIMI_CODE_HOME="$
|
|
1252
|
+
export KIMI_CODE_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
1253
1253
|
`
|
|
1254
1254
|
: agent === 'muse'
|
|
1255
1255
|
? `
|
|
1256
1256
|
# Muse Code: no dedicated config env var. Pin XDG so config/sessions live under
|
|
1257
1257
|
# the version home as real directories (not via the adopt symlink at
|
|
1258
1258
|
# ~/.config/muse, which Muse rejects with SymlinkOrReparse).
|
|
1259
|
-
export XDG_CONFIG_HOME="$
|
|
1260
|
-
export XDG_DATA_HOME="$
|
|
1259
|
+
export XDG_CONFIG_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/.config"
|
|
1260
|
+
export XDG_DATA_HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home/.local/share"
|
|
1261
1261
|
`
|
|
1262
1262
|
: agent === 'cursor'
|
|
1263
1263
|
? `
|
|
1264
1264
|
# Cursor defaults to one machine-global OS-keychain login on macOS. Force its
|
|
1265
1265
|
# file store and swap HOME, so ~/.cursor/auth.json belongs to this
|
|
1266
1266
|
# version and direct aliases cannot fall through to the shared keychain login.
|
|
1267
|
-
|
|
1268
|
-
|
|
1267
|
+
# A spawner that already chose HOME (an account slot, PHNX-3940 T5 — it left
|
|
1268
|
+
# the real home in AGENTS_REAL_HOME) keeps its choice: re-swapping here would
|
|
1269
|
+
# silently discard the slot.
|
|
1270
|
+
if [ "$HOME" = "$AGENTS_REAL_HOME" ]; then
|
|
1271
|
+
export HOME="$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}/home"
|
|
1272
|
+
fi
|
|
1269
1273
|
export AGENT_CLI_CREDENTIAL_STORE="file"
|
|
1270
1274
|
`
|
|
1271
1275
|
: '';
|
|
@@ -1283,7 +1287,7 @@ export AGENT_CLI_CREDENTIAL_STORE="file"
|
|
|
1283
1287
|
// alias's sibling dispatcher shim and re-execs forever.
|
|
1284
1288
|
// This template is unix-only — on Windows the .cmd companion delegates to
|
|
1285
1289
|
// "agents __shim" which resolves via getBinaryPath() instead.
|
|
1286
|
-
const versionDir = `$
|
|
1290
|
+
const versionDir = `$AGENTS_REAL_HOME/.agents/.history/versions/${agent}/${version}`;
|
|
1287
1291
|
const binaryResolution = agent === 'grok'
|
|
1288
1292
|
? `# Grok ships its native binary in the versioned home's .grok/downloads (or,
|
|
1289
1293
|
# for pre-fix installs, the global ~/.grok/downloads), not node_modules.
|
|
@@ -1295,7 +1299,7 @@ if [ -d "$GROK_DOWNLOADS" ]; then
|
|
|
1295
1299
|
fi
|
|
1296
1300
|
# Fall back to the global grok home (binary installed without GROK_HOME set).
|
|
1297
1301
|
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
1298
|
-
GROK_GLOBAL_DOWNLOADS="$
|
|
1302
|
+
GROK_GLOBAL_DOWNLOADS="$AGENTS_REAL_HOME/.grok/downloads"
|
|
1299
1303
|
if [ -d "$GROK_GLOBAL_DOWNLOADS" ]; then
|
|
1300
1304
|
BINARY=$(_resolve_grok_binary "$GROK_GLOBAL_DOWNLOADS" "${version}")
|
|
1301
1305
|
fi
|
|
@@ -1306,31 +1310,31 @@ fi
|
|
|
1306
1310
|
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
1307
1311
|
BINARY=$(command -v grok 2>/dev/null || echo "")
|
|
1308
1312
|
case "$BINARY" in
|
|
1309
|
-
"$
|
|
1313
|
+
"$AGENTS_REAL_HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
1310
1314
|
esac
|
|
1311
1315
|
fi`
|
|
1312
1316
|
: agent === 'droid'
|
|
1313
1317
|
? `# Droid (Factory AI) installs a standalone native binary at ~/.local/bin/droid;
|
|
1314
1318
|
# there is no npm package and nothing lands in node_modules/.bin. The PATH
|
|
1315
1319
|
# fallback refuses anything under our shims dir to avoid an infinite re-exec.
|
|
1316
|
-
DROID_BINARY="$
|
|
1320
|
+
DROID_BINARY="$AGENTS_REAL_HOME/.local/bin/droid"
|
|
1317
1321
|
if [ -x "$DROID_BINARY" ]; then
|
|
1318
1322
|
BINARY="$DROID_BINARY"
|
|
1319
1323
|
else
|
|
1320
1324
|
BINARY=$(command -v droid 2>/dev/null || echo "")
|
|
1321
1325
|
case "$BINARY" in
|
|
1322
|
-
"$
|
|
1326
|
+
"$AGENTS_REAL_HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
1323
1327
|
esac
|
|
1324
1328
|
fi`
|
|
1325
1329
|
: agent === 'muse'
|
|
1326
1330
|
? `# Muse Code installs a self-updating launcher at ~/.local/bin/muse.
|
|
1327
|
-
MUSE_BINARY="$
|
|
1331
|
+
MUSE_BINARY="$AGENTS_REAL_HOME/.local/bin/muse"
|
|
1328
1332
|
if [ -x "$MUSE_BINARY" ]; then
|
|
1329
1333
|
BINARY="$MUSE_BINARY"
|
|
1330
1334
|
else
|
|
1331
1335
|
BINARY=$(command -v muse 2>/dev/null || echo "")
|
|
1332
1336
|
case "$BINARY" in
|
|
1333
|
-
"$
|
|
1337
|
+
"$AGENTS_REAL_HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
1334
1338
|
esac
|
|
1335
1339
|
fi`
|
|
1336
1340
|
: agent === 'warp'
|
|
@@ -1339,7 +1343,7 @@ fi`
|
|
|
1339
1343
|
# PATH, refusing anything under our shims dir.
|
|
1340
1344
|
BINARY=$(command -v warp 2>/dev/null || echo "")
|
|
1341
1345
|
case "$BINARY" in
|
|
1342
|
-
"$
|
|
1346
|
+
"$AGENTS_REAL_HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
1343
1347
|
esac`
|
|
1344
1348
|
: `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
|
|
1345
1349
|
return `#!/bin/bash
|
|
@@ -1347,22 +1351,32 @@ esac`
|
|
|
1347
1351
|
# ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
|
|
1348
1352
|
# Direct alias for ${agentConfig.name}@${version}
|
|
1349
1353
|
|
|
1354
|
+
# The real home. A spawner may have swapped HOME already — an account slot
|
|
1355
|
+
# launch (PHNX-3940 T5) sets HOME to the slot dir and leaves the real home in
|
|
1356
|
+
# AGENTS_REAL_HOME — so every agents-owned path below (the installation, the
|
|
1357
|
+
# version home, the shims dir) is anchored here, never on whatever HOME is now.
|
|
1358
|
+
# Anchoring on HOME made a slot launch of cursor#<name> answer "not installed"
|
|
1359
|
+
# and the launch lease fail with "No installation directory".
|
|
1360
|
+
export AGENTS_REAL_HOME="\${AGENTS_REAL_HOME:-$HOME}"
|
|
1361
|
+
|
|
1350
1362
|
${binaryResolution}
|
|
1351
1363
|
|
|
1352
1364
|
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
1353
1365
|
echo "agents: ${agent}@${version} not installed" >&2
|
|
1354
1366
|
exit 1
|
|
1355
1367
|
fi
|
|
1356
|
-
${managedEnv}
|
|
1357
1368
|
|
|
1358
1369
|
# Register a launch lease for THIS pid before the exec below (PHNX-3940) — see
|
|
1359
1370
|
# generateShimScript's identical call for what this closes and why it fails
|
|
1360
|
-
# closed rather than falling through on error. \$\$ survives exec.
|
|
1361
|
-
|
|
1371
|
+
# closed rather than falling through on error. \$\$ survives exec. It runs
|
|
1372
|
+
# under the REAL home, before any harness HOME swap below: agents-cli's own
|
|
1373
|
+
# state root is $HOME/.agents, so a swapped HOME cannot find the installation.
|
|
1374
|
+
if ! HOME="$AGENTS_REAL_HOME" ${agentsBin} __launch-lease "${agent}" "${version}" "\$\$"; then
|
|
1362
1375
|
echo "agents: could not safely coordinate this launch with a possibly in-progress update of ${agent}@${version}." >&2
|
|
1363
1376
|
echo " Check: agents update ${agent}@${version} --check Retry once any update finishes." >&2
|
|
1364
1377
|
exit 1
|
|
1365
1378
|
fi
|
|
1379
|
+
${managedEnv}
|
|
1366
1380
|
|
|
1367
1381
|
${resolveHarnessAdapter(agent).shimExecTail?.(launchArgs) ?? `exec "$BINARY"${launchArgs} "$@"`}
|
|
1368
1382
|
`;
|
|
@@ -2167,14 +2181,11 @@ export function listShimFileNames() {
|
|
|
2167
2181
|
}
|
|
2168
2182
|
/**
|
|
2169
2183
|
* Legacy command shims that are wrong even when their baked install is alive.
|
|
2170
|
-
* `secrets
|
|
2171
|
-
* passthrough to the STANDALONE
|
|
2172
|
-
* re-enters
|
|
2173
|
-
* first on PATH and shadows the real `@phnx-labs/secrets-cli` bin, so it is
|
|
2174
|
-
* pruned unconditionally (the liveness check below is what kept it alive across
|
|
2175
|
-
* every in-place upgrade from 1.22.84).
|
|
2184
|
+
* `secrets` (PHNX-3989) and `sessions` (PHNX-4012): the shim `exec`s
|
|
2185
|
+
* `agents <name>`, which is a passthrough to the STANDALONE binary — so the
|
|
2186
|
+
* shim re-enters itself (the 1.22.85 fork bomb). Pruned unconditionally.
|
|
2176
2187
|
*/
|
|
2177
|
-
const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets']);
|
|
2188
|
+
const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets', 'sessions']);
|
|
2178
2189
|
/**
|
|
2179
2190
|
* Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
|
|
2180
2191
|
* alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
|
|
@@ -185,6 +185,19 @@ export declare function storeDelete(backend: SecretsBackend, item: string): Prom
|
|
|
185
185
|
export declare function remoteResolveEnv(target: string, bundle: string, opts?: {
|
|
186
186
|
osLookupName?: string;
|
|
187
187
|
}): Promise<Record<string, string>>;
|
|
188
|
+
/**
|
|
189
|
+
* The user agents dir as the RECEIVING agents-cli resolves it — `~/.agents` on
|
|
190
|
+
* every box, the same root {@link buildServeEnv} pins the local standalone to
|
|
191
|
+
* (MIG-1). A push must name it, because the remote `secrets` otherwise runs
|
|
192
|
+
* under its own default `~/.secrets`: the import succeeds, the file store there
|
|
193
|
+
* is keyed by a different machine-local passphrase, and the receiving daemon's
|
|
194
|
+
* `readReservedCredential` never finds the bundle (yosemite-m0, 2026-09-07:
|
|
195
|
+
* `pushed __cursor__` on the publisher, `no readable durable key` on the worker).
|
|
196
|
+
* Remote-relative on purpose — the local absolute dir means nothing over there.
|
|
197
|
+
*/
|
|
198
|
+
export declare const REMOTE_USER_AGENTS_DIR = "~/.agents";
|
|
199
|
+
/** Fill in the remote state root unless the caller named one. Pure, for the seam test. */
|
|
200
|
+
export declare function withRemoteStateRoot(opts: PushBundleOptions): PushBundleOptions;
|
|
188
201
|
export declare function pushBundleToHost(bundle: string, host: string, opts: PushBundleOptions): Promise<PushBundleResult>;
|
|
189
202
|
export declare function pushBundleToHostAsync(bundle: string, host: string, opts: PushBundleOptions): Promise<PushBundleResult>;
|
|
190
203
|
export declare function listRemoteBundles(context?: SecretsContext): Promise<RemoteBundleSummary[]>;
|
|
@@ -646,11 +646,26 @@ export function storeDelete(backend, item) {
|
|
|
646
646
|
export function remoteResolveEnv(target, bundle, opts) {
|
|
647
647
|
return secretsRequest('remote.remoteResolveEnv', [target, bundle, opts ?? {}]);
|
|
648
648
|
}
|
|
649
|
+
/**
|
|
650
|
+
* The user agents dir as the RECEIVING agents-cli resolves it — `~/.agents` on
|
|
651
|
+
* every box, the same root {@link buildServeEnv} pins the local standalone to
|
|
652
|
+
* (MIG-1). A push must name it, because the remote `secrets` otherwise runs
|
|
653
|
+
* under its own default `~/.secrets`: the import succeeds, the file store there
|
|
654
|
+
* is keyed by a different machine-local passphrase, and the receiving daemon's
|
|
655
|
+
* `readReservedCredential` never finds the bundle (yosemite-m0, 2026-09-07:
|
|
656
|
+
* `pushed __cursor__` on the publisher, `no readable durable key` on the worker).
|
|
657
|
+
* Remote-relative on purpose — the local absolute dir means nothing over there.
|
|
658
|
+
*/
|
|
659
|
+
export const REMOTE_USER_AGENTS_DIR = '~/.agents';
|
|
660
|
+
/** Fill in the remote state root unless the caller named one. Pure, for the seam test. */
|
|
661
|
+
export function withRemoteStateRoot(opts) {
|
|
662
|
+
return { ...opts, remoteSecretsHome: opts.remoteSecretsHome ?? REMOTE_USER_AGENTS_DIR };
|
|
663
|
+
}
|
|
649
664
|
export function pushBundleToHost(bundle, host, opts) {
|
|
650
|
-
return secretsRequest('push.pushBundleToHost', [bundle, host, opts]);
|
|
665
|
+
return secretsRequest('push.pushBundleToHost', [bundle, host, withRemoteStateRoot(opts)]);
|
|
651
666
|
}
|
|
652
667
|
export function pushBundleToHostAsync(bundle, host, opts) {
|
|
653
|
-
return secretsRequest('push.pushBundleToHostAsync', [bundle, host, opts]);
|
|
668
|
+
return secretsRequest('push.pushBundleToHostAsync', [bundle, host, withRemoteStateRoot(opts)]);
|
|
654
669
|
}
|
|
655
670
|
// sync.* (transport pull, used by the `agents sync --secrets` umbrella stage)
|
|
656
671
|
export function listRemoteBundles(context) {
|
|
@@ -171,6 +171,11 @@ export declare function readDeliveryMemo(root?: string): Record<string, string>;
|
|
|
171
171
|
export declare function peerPresentKeys(peer: string, accounts: ReservedSyncAccount[], memo: Record<string, string>, legacyAuthReady: boolean): Record<string, ReadonlySet<string>>;
|
|
172
172
|
export interface ReservedStoreSyncResult {
|
|
173
173
|
publisher: string | null;
|
|
174
|
+
/** Legacy raw reserved items adopted into their bundle on this box before planning (local repair). */
|
|
175
|
+
adopted: Array<{
|
|
176
|
+
bundle: string;
|
|
177
|
+
key: string;
|
|
178
|
+
}>;
|
|
174
179
|
pushed: Array<{
|
|
175
180
|
device: string;
|
|
176
181
|
bundle: string;
|
|
@@ -197,6 +202,18 @@ export interface ReservedStoreSyncDeps {
|
|
|
197
202
|
localReady?: boolean;
|
|
198
203
|
/** Does THIS publisher hold (bundle, key) to push? Defaults to a local bundle read. */
|
|
199
204
|
hasLocalKey?: (bundle: string, key: string) => boolean;
|
|
205
|
+
/** Adopt pre-bundle raw reserved items locally before planning; defaults to `adoptLegacyReservedStoreItems`. */
|
|
206
|
+
adoptLegacy?: (meta: Pick<Meta, 'accounts' | 'deviceAccounts'>) => Promise<{
|
|
207
|
+
adopted: Array<{
|
|
208
|
+
bundle: string;
|
|
209
|
+
key: string;
|
|
210
|
+
}>;
|
|
211
|
+
errors: Array<{
|
|
212
|
+
bundle: string;
|
|
213
|
+
key: string;
|
|
214
|
+
message: string;
|
|
215
|
+
}>;
|
|
216
|
+
}>;
|
|
200
217
|
push?: (bundle: string, host: string) => Promise<PushBundleResult>;
|
|
201
218
|
sshTarget?: (device: DeviceProfile) => string;
|
|
202
219
|
}
|
|
@@ -375,11 +375,24 @@ function defaultHasLocalKey(bundle, key) {
|
|
|
375
375
|
* there is no second scheduler. The daemon runs this each tick.
|
|
376
376
|
*/
|
|
377
377
|
export async function syncReservedStores(deps = {}) {
|
|
378
|
-
const result = { publisher: null, pushed: [], skipped: [], errors: [] };
|
|
378
|
+
const result = { publisher: null, adopted: [], pushed: [], skipped: [], errors: [] };
|
|
379
379
|
const localName = deps.localName ?? machineId();
|
|
380
380
|
const localNorm = normalizeHost(localName);
|
|
381
381
|
const root = deps.userAgentsDir ?? getUserAgentsDir();
|
|
382
382
|
const meta = (deps.readMetaFn ?? readMeta)();
|
|
383
|
+
// Local repair first: a reserved key written by 1.22.84–1.22.89 is a bare
|
|
384
|
+
// file item with no bundle record, which the push below cannot read. Adopt
|
|
385
|
+
// it into its bundle here so the plan sees it; nothing leaves the box.
|
|
386
|
+
const adopt = deps.adoptLegacy ?? (async (m) => (await import('./auth-mint.js')).adoptLegacyReservedStoreItems(m));
|
|
387
|
+
try {
|
|
388
|
+
const adoption = await adopt(meta);
|
|
389
|
+
result.adopted.push(...adoption.adopted);
|
|
390
|
+
for (const err of adoption.errors)
|
|
391
|
+
result.errors.push({ device: localName, message: `adopt ${err.bundle} ${err.key}: ${err.message}` });
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
result.errors.push({ device: localName, message: `adopt legacy reserved items: ${err.message}` });
|
|
395
|
+
}
|
|
383
396
|
const hasLocalKey = deps.hasLocalKey ?? defaultHasLocalKey;
|
|
384
397
|
const targets = reservedSyncTargets(meta).filter((t) => hasLocalKey(t.bundle, t.key));
|
|
385
398
|
const devices = (deps.listDevices ?? defaultDevices)().filter((d) => normalizeHost(d.name) !== localNorm);
|
|
@@ -155,6 +155,14 @@ export interface PushBundleOptions {
|
|
|
155
155
|
literalValues?: Record<string, string>;
|
|
156
156
|
/** Per-SSH-operation deadline. Async daemon callers must set this explicitly. */
|
|
157
157
|
timeoutMs?: number;
|
|
158
|
+
/**
|
|
159
|
+
* State root (`SECRETS_HOME`) the remote `secrets` runs under for the whole
|
|
160
|
+
* push (import, read-back verify, literal restoration); remote-relative, a
|
|
161
|
+
* leading `~/` resolves against the remote user's home. The client wrapper
|
|
162
|
+
* fills in the user agents dir (`REMOTE_USER_AGENTS_DIR`) so a push lands
|
|
163
|
+
* where the receiving agents-cli reads (MIG-1 on both ends).
|
|
164
|
+
*/
|
|
165
|
+
remoteSecretsHome?: string;
|
|
158
166
|
}
|
|
159
167
|
export interface PushBundleResult {
|
|
160
168
|
ok: boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class SessionsClientError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
constructor(code: string, message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare function resolveSessionsBin(): string;
|
|
6
|
+
export declare function invocation(bin: string): {
|
|
7
|
+
command: string;
|
|
8
|
+
prefix: string[];
|
|
9
|
+
};
|
|
10
|
+
export declare function isReadQuery(args: string[]): boolean;
|
|
11
|
+
/** Test seam: drop the memoized bin so PATH fixtures can re-resolve. */
|
|
12
|
+
export declare function _resetSessionsClientForTest(): void;
|
|
13
|
+
export declare const SESSIONS_INSTALL_HINT = "npm i -g @phnx-labs/sessions-cli";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessions-client.ts — agents-cli's process client for the standalone
|
|
3
|
+
* `sessions` CLI (PHNX-4012). This client never falls back itself: a missing
|
|
4
|
+
* binary throws `SESSIONS_BIN_MISSING` (loud, DIST-1). The caller decides what
|
|
5
|
+
* that means — the read fast-path in `index.ts` falls through to the in-repo
|
|
6
|
+
* `lib/session` engine when the standalone is not installed (every worker and
|
|
7
|
+
* CI runner, until @phnx-labs/sessions-cli is published), and takes the fast
|
|
8
|
+
* path when it is.
|
|
9
|
+
*
|
|
10
|
+
* Bin resolution uses `findInPath`, which skips `~/.agents/.cache/shims`.
|
|
11
|
+
* That skip is load-bearing: the leftover `sessions` alias shim execs
|
|
12
|
+
* `agents sessions`, and resolving it would recurse (the 1.22.85 secrets
|
|
13
|
+
* fork bomb with the names swapped — agi-cli#3532).
|
|
14
|
+
*/
|
|
15
|
+
import { findInPath } from './agent-spec/agents.js';
|
|
16
|
+
const INSTALL_HINT = 'npm i -g @phnx-labs/sessions-cli';
|
|
17
|
+
export class SessionsClientError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
constructor(code, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.name = 'SessionsClientError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
let cachedBin;
|
|
26
|
+
export function resolveSessionsBin() {
|
|
27
|
+
if (cachedBin)
|
|
28
|
+
return cachedBin;
|
|
29
|
+
const explicit = process.env.SESSIONS_BIN?.trim();
|
|
30
|
+
const resolved = explicit && explicit.length > 0 ? explicit : findInPath('sessions');
|
|
31
|
+
if (!resolved) {
|
|
32
|
+
throw new SessionsClientError('SESSIONS_BIN_MISSING', 'The standalone `sessions` CLI was not found. Install it with:\n' +
|
|
33
|
+
` ${INSTALL_HINT}\n` +
|
|
34
|
+
'or point $SESSIONS_BIN at its executable.');
|
|
35
|
+
}
|
|
36
|
+
cachedBin = resolved;
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
export function invocation(bin) {
|
|
40
|
+
if (/\.[mc]?js$/.test(bin))
|
|
41
|
+
return { command: process.execPath, prefix: [bin] };
|
|
42
|
+
return { command: bin, prefix: [] };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Allowlist of what sessions-cli v1 actually implements. Anything else —
|
|
46
|
+
* picker (no args), `--since`, `-D`, `--waiting`, `render`, `stats`,
|
|
47
|
+
* lifecycle verbs — stays on the in-repo engine. A denylist leaked those
|
|
48
|
+
* through `allowUnknownOption()` as FTS tokens (PR review on #3554).
|
|
49
|
+
*/
|
|
50
|
+
const READ_FLAGS = new Set([
|
|
51
|
+
'--json',
|
|
52
|
+
'--local',
|
|
53
|
+
'--no-interactive',
|
|
54
|
+
]);
|
|
55
|
+
const ENGINE_VERBS = new Set([
|
|
56
|
+
'resume',
|
|
57
|
+
'detach',
|
|
58
|
+
'stop',
|
|
59
|
+
'inject',
|
|
60
|
+
'fork',
|
|
61
|
+
'backfill',
|
|
62
|
+
'migrate',
|
|
63
|
+
'share',
|
|
64
|
+
'export',
|
|
65
|
+
'import',
|
|
66
|
+
'optimize',
|
|
67
|
+
'trace',
|
|
68
|
+
'insights',
|
|
69
|
+
'bookmark',
|
|
70
|
+
'tail',
|
|
71
|
+
'watch',
|
|
72
|
+
'preview',
|
|
73
|
+
'attach',
|
|
74
|
+
'focus',
|
|
75
|
+
'render',
|
|
76
|
+
'stats',
|
|
77
|
+
'go',
|
|
78
|
+
'reconnect',
|
|
79
|
+
'migrations',
|
|
80
|
+
'export',
|
|
81
|
+
]);
|
|
82
|
+
export function isReadQuery(args) {
|
|
83
|
+
if (args.length === 0)
|
|
84
|
+
return false;
|
|
85
|
+
for (let i = 0; i < args.length; i++) {
|
|
86
|
+
const arg = args[i];
|
|
87
|
+
if (arg === '--limit' || arg === '--agent') {
|
|
88
|
+
i += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg.startsWith('--limit=') || arg.startsWith('--agent='))
|
|
92
|
+
continue;
|
|
93
|
+
if (arg.startsWith('-')) {
|
|
94
|
+
if (!READ_FLAGS.has(arg))
|
|
95
|
+
return false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ENGINE_VERBS.has(arg))
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
/** Test seam: drop the memoized bin so PATH fixtures can re-resolve. */
|
|
104
|
+
export function _resetSessionsClientForTest() {
|
|
105
|
+
cachedBin = undefined;
|
|
106
|
+
}
|
|
107
|
+
export const SESSIONS_INSTALL_HINT = INSTALL_HINT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.91",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -50,16 +50,15 @@ function shellQuote(value) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Shorthands that delegate to the installed agents-cli entrypoint.
|
|
53
|
-
const ALIASES = ['
|
|
53
|
+
const ALIASES = ['browser', 'pty', 'teams'];
|
|
54
54
|
|
|
55
55
|
// Aliases this script USED to write and must now remove on every install.
|
|
56
|
-
// `secrets
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
const RETIRED_ALIASES = ['secrets'];
|
|
56
|
+
// `secrets` (PHNX-3989) and `sessions` (PHNX-4012): those names belong to the
|
|
57
|
+
// standalone CLIs. An alias shim that `exec`s `agents <name>` re-enters the
|
|
58
|
+
// passthrough and shadows the real binary because the shims dir sits first on
|
|
59
|
+
// PATH — the 1.22.85 fork bomb. Postinstall is the upgrade path that actually
|
|
60
|
+
// runs, so it prunes here.
|
|
61
|
+
const RETIRED_ALIASES = ['secrets', 'sessions'];
|
|
63
62
|
|
|
64
63
|
function removeRetiredAliasShims() {
|
|
65
64
|
for (const name of RETIRED_ALIASES) {
|