@phnx-labs/agents-cli 1.22.69 → 1.22.71
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 +34 -0
- package/README.md +34 -9
- package/dist/bootstrap.js +4 -4
- package/dist/commands/accounts.js +23 -7
- package/dist/commands/exec.js +2 -2
- package/dist/commands/import.js +2 -2
- package/dist/commands/models.js +2 -2
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/repo.js +2 -2
- package/dist/commands/rules.js +1 -1
- package/dist/commands/send.js +9 -3
- package/dist/commands/sessions-export.d.ts +5 -1
- package/dist/commands/sessions-export.js +100 -24
- package/dist/commands/sessions-import.d.ts +2 -1
- package/dist/commands/sessions-import.js +85 -21
- package/dist/commands/traces.js +1 -1
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-registry.d.ts +3 -3
- package/dist/lib/account-registry.js +25 -7
- package/dist/lib/accounting/usage-sync.d.ts +1 -1
- package/dist/lib/accounting/usage-sync.js +3 -3
- package/dist/lib/acp/client.d.ts +1 -1
- package/dist/lib/acp/client.js +12 -1
- package/dist/lib/acp/harnesses.js +1 -1
- package/dist/lib/add-dir.js +0 -2
- package/dist/lib/agent-cli-commands.js +0 -2
- package/dist/lib/agent-spec/agents.d.ts +1 -1
- package/dist/lib/agent-spec/agents.js +2 -83
- package/dist/lib/browser/ipc.d.ts +34 -0
- package/dist/lib/browser/ipc.js +149 -20
- package/dist/lib/browser/remote-control.d.ts +6 -3
- package/dist/lib/browser/remote-control.js +6 -3
- package/dist/lib/browser/service.d.ts +4 -1
- package/dist/lib/browser/service.js +7 -1
- package/dist/lib/browser/types.d.ts +3 -1
- package/dist/lib/channels/registry.d.ts +6 -0
- package/dist/lib/channels/send.d.ts +11 -2
- package/dist/lib/channels/send.js +11 -2
- package/dist/lib/cloud/rush.d.ts +10 -2
- package/dist/lib/cloud/rush.js +15 -9
- package/dist/lib/daemon/auth-sync-service.js +1 -1
- package/dist/lib/daemon/browser-task-reap-service.js +1 -1
- package/dist/lib/daemon/daemon.js +41 -11
- package/dist/lib/daemon/heartbeat-service.js +3 -3
- package/dist/lib/daemon/keychain-reap-service.js +1 -1
- package/dist/lib/daemon/runner.d.ts +18 -1
- package/dist/lib/daemon/runner.js +237 -80
- package/dist/lib/daemon/self-heal-service.js +13 -3
- package/dist/lib/daemon/self-update-service.d.ts +174 -0
- package/dist/lib/daemon/self-update-service.js +353 -0
- package/dist/lib/daemon/state-dir-check-service.js +3 -3
- package/dist/lib/daemon/usage-sync-service.js +1 -1
- package/dist/lib/daemon/watchdog-service.js +4 -4
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +12 -1
- package/dist/lib/device-config.js +63 -13
- package/dist/lib/exec-bounded.d.ts +52 -0
- package/dist/lib/exec-bounded.js +113 -0
- package/dist/lib/exec.d.ts +2 -2
- package/dist/lib/exec.js +3 -34
- package/dist/lib/feed/events.d.ts +22 -14
- package/dist/lib/feed/events.js +84 -44
- package/dist/lib/feed-broadcast.d.ts +12 -29
- package/dist/lib/feed-broadcast.js +28 -27
- package/dist/lib/fleet-shared-state.d.ts +12 -5
- package/dist/lib/fleet-shared-state.js +50 -20
- package/dist/lib/fs-atomic.d.ts +11 -0
- package/dist/lib/fs-atomic.js +60 -0
- package/dist/lib/hooks/install.js +0 -87
- package/dist/lib/hosts/reconcile.d.ts +11 -4
- package/dist/lib/hosts/reconcile.js +31 -5
- package/dist/lib/installations/strategies.js +1 -1
- package/dist/lib/mcp-registry.js +0 -13
- package/dist/lib/mcp.js +2 -2
- package/dist/lib/model-tiers.js +1 -1
- package/dist/lib/models.js +0 -63
- package/dist/lib/notify.d.ts +11 -0
- package/dist/lib/notify.js +17 -4
- package/dist/lib/owner-message.d.ts +46 -3
- package/dist/lib/owner-message.js +26 -6
- package/dist/lib/permissions-registry.d.ts +0 -2
- package/dist/lib/permissions-registry.js +3 -50
- package/dist/lib/permissions.d.ts +3 -17
- package/dist/lib/permissions.js +4 -73
- package/dist/lib/project-resources.d.ts +12 -0
- package/dist/lib/project-resources.js +129 -0
- package/dist/lib/routine-process-cleanup.d.ts +2 -2
- package/dist/lib/routine-process-cleanup.js +45 -34
- package/dist/lib/rush-session.d.ts +19 -0
- package/dist/lib/rush-session.js +24 -0
- package/dist/lib/secrets/drivers/rush.js +2 -1
- package/dist/lib/secrets/reaper.d.ts +2 -2
- package/dist/lib/secrets/reaper.js +13 -10
- package/dist/lib/secrets/reserved-sync.d.ts +1 -1
- package/dist/lib/secrets/reserved-sync.js +4 -4
- package/dist/lib/self-update.d.ts +21 -8
- package/dist/lib/self-update.js +54 -31
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/sync/backend.d.ts +61 -0
- package/dist/lib/session/sync/backend.js +89 -0
- package/dist/lib/session/sync/managed-config.d.ts +29 -0
- package/dist/lib/session/sync/managed-config.js +23 -0
- package/dist/lib/session/sync/managed-key.d.ts +45 -0
- package/dist/lib/session/sync/managed-key.js +128 -0
- package/dist/lib/session/sync/net-client.d.ts +65 -0
- package/dist/lib/session/sync/net-client.js +117 -0
- package/dist/lib/session/sync/provision.d.ts +19 -0
- package/dist/lib/session/sync/provision.js +38 -0
- package/dist/lib/session/sync/r2.d.ts +5 -2
- package/dist/lib/session/sync/r2.js +5 -2
- package/dist/lib/session/sync/worker-template.d.ts +6 -0
- package/dist/lib/session/sync/worker-template.js +847 -0
- package/dist/lib/sink-format.d.ts +34 -0
- package/dist/lib/sink-format.js +17 -0
- package/dist/lib/staleness/detectors/permissions.js +0 -20
- package/dist/lib/staleness/writers/commands.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +2 -2
- package/dist/lib/subagents-registry.js +2 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -37
- package/dist/lib/tmux/orphan-reap.js +6 -4
- package/dist/lib/tmux/session.js +4 -1
- package/dist/lib/traces/classify.d.ts +8 -1
- package/dist/lib/traces/insights.d.ts +13 -1
- package/dist/lib/traces/insights.js +78 -3
- package/dist/lib/traces/sync.js +8 -3
- package/dist/lib/traces/worker-template.js +9 -5
- package/dist/lib/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -27,8 +27,11 @@ import { listLocalTranscripts, objectKey, SYNC_AGENTS } from '../lib/session/syn
|
|
|
27
27
|
import { machineId } from '../lib/machine-id.js';
|
|
28
28
|
import { getHistoryDir } from '../lib/state.js';
|
|
29
29
|
import { isSyncConfigured, loadR2Config } from '../lib/session/sync/config.js';
|
|
30
|
-
import { resolveSyncEncKey, generateSyncEncKey } from '../lib/session/sync/transcript-crypto.js';
|
|
30
|
+
import { resolveSyncEncKey, generateSyncEncKey, isTranscriptEnvelope, } from '../lib/session/sync/transcript-crypto.js';
|
|
31
31
|
import { R2Client } from '../lib/session/sync/r2.js';
|
|
32
|
+
import { resolveSessionsBackend } from '../lib/session/sync/backend.js';
|
|
33
|
+
import { SessionsHttpClient } from '../lib/session/sync/net-client.js';
|
|
34
|
+
import { resolveManagedBackupKey } from '../lib/session/sync/managed-key.js';
|
|
32
35
|
import { buildRecord, makeHeader, mergeRecords, serializeBundle, writeBundleFile, specForAgent, } from '../lib/session/bundle.js';
|
|
33
36
|
import { knownSecretValuesFromEnv } from '../lib/redact.js';
|
|
34
37
|
import { pullBundlesFromHosts } from '../lib/session/remote-bundle.js';
|
|
@@ -42,7 +45,8 @@ export function registerSessionsExportCommand(sessionsCmd) {
|
|
|
42
45
|
.option('-o, --output <path>', 'Write the bundle to this file')
|
|
43
46
|
.option('--stdout', 'Write the bundle to stdout (for piping into `sessions import -`)')
|
|
44
47
|
.option('--encrypt', 'Seal each transcript body with AES-256-GCM before writing')
|
|
45
|
-
.option('--to-r2', 'Back the selected sessions up
|
|
48
|
+
.option('--to-r2', 'Back the selected sessions up off-box (managed Phoenix store when signed in; your own r2.backups bucket with --byo) instead of a local file')
|
|
49
|
+
.option('--byo', 'With --to-r2: force your own r2.backups bucket instead of the managed Phoenix store');
|
|
46
50
|
setHelpSections(cmd, {
|
|
47
51
|
examples: `# Bundle the last week of sessions to a file
|
|
48
52
|
agents sessions export --since 7d -o week.bundle
|
|
@@ -53,17 +57,22 @@ agents sessions export 4f8a2b1c 9d3e7a55 -o pair.bundle
|
|
|
53
57
|
# Encrypt + pipe straight into another machine over SSH
|
|
54
58
|
agents sessions export --since 7d --stdout --encrypt | agents ssh boxB 'agents sessions import - --decrypt <key>'
|
|
55
59
|
|
|
56
|
-
# Back the last month up off-box
|
|
57
|
-
agents sessions export --since 30d --to-r2
|
|
60
|
+
# Back the last month up off-box (managed Phoenix store — no bucket to set up)
|
|
61
|
+
agents sessions export --since 30d --to-r2
|
|
62
|
+
|
|
63
|
+
# Or to your own r2.backups bucket (R2_SYNC_ENC_KEY keeps it zero-knowledge)
|
|
64
|
+
agents sessions export --since 30d --to-r2 --byo`,
|
|
58
65
|
notes: `Selection uses the same flags as 'agents sessions' (--since, -n/--limit, --all,
|
|
59
66
|
-a/--agent, --no-redact). Bundles are self-describing NDJSON: a header line + one
|
|
60
67
|
line per transcript file. Secrets are redacted by default. Dir-shaped sessions
|
|
61
68
|
(Kimi) carry all their files. Restore with 'agents sessions import'.
|
|
62
69
|
|
|
63
|
-
--to-r2
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
--to-r2 backs each session up off-box, one encrypted object per transcript keyed
|
|
71
|
+
by machine/agent/session. When you are signed in ('agents auth login') it uploads
|
|
72
|
+
to the MANAGED Phoenix store — no Cloudflare or r2.backups bucket to set up — and
|
|
73
|
+
every body is sealed with a per-account key (mandatory; never plaintext). --byo
|
|
74
|
+
forces your own r2.backups bucket instead; with R2_SYNC_ENC_KEY, neither Phoenix
|
|
75
|
+
nor the storage provider can decrypt it. Restore with 'agents sessions import --from-r2'.`,
|
|
67
76
|
});
|
|
68
77
|
cmd.action(async (selectors, _options, command) => {
|
|
69
78
|
await runExport(selectors, command);
|
|
@@ -71,13 +80,42 @@ same bundle with 'agents sessions import --from-r2'.`,
|
|
|
71
80
|
}
|
|
72
81
|
async function runExport(selectors, command) {
|
|
73
82
|
const g = command.optsWithGlobals();
|
|
74
|
-
// --to-r2 preflight:
|
|
75
|
-
//
|
|
76
|
-
//
|
|
83
|
+
// --to-r2 preflight. MANAGED-FIRST: a signed-in user backs up to the managed
|
|
84
|
+
// Phoenix store with no bucket to set up; --byo (or an unauthenticated user
|
|
85
|
+
// with an r2.backups bundle) uses their own bucket. isSyncConfigured() is
|
|
86
|
+
// consulted ONLY on the BYO path (it reads the keychain); the managed path
|
|
87
|
+
// authenticates with the Phoenix session and skips it entirely.
|
|
88
|
+
let sessionsClient;
|
|
89
|
+
let managedClient;
|
|
90
|
+
let managedBackupUserId;
|
|
91
|
+
if (g.byo && !g.toR2) {
|
|
92
|
+
process.stderr.write(chalk.red('--byo is only valid with --to-r2.\n'));
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
77
95
|
if (g.toR2) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
process.
|
|
96
|
+
if (g.host && g.host.length > 0) {
|
|
97
|
+
process.stderr.write(chalk.red("--to-r2 backs up THIS machine's sessions; it cannot be combined with --device.\n"));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const backend = resolveSessionsBackend({ byo: g.byo });
|
|
102
|
+
if (backend.kind === 'managed') {
|
|
103
|
+
managedClient = new SessionsHttpClient({ baseUrl: backend.baseUrl, userId: backend.userId, token: backend.token });
|
|
104
|
+
sessionsClient = managedClient;
|
|
105
|
+
managedBackupUserId = backend.userId;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Preserve the explicit BYO gate used by the on-demand backup path and
|
|
109
|
+
// daemon sync cycle. resolveSessionsBackend already loaded the bundle,
|
|
110
|
+
// so this is a cached, non-prompting verification.
|
|
111
|
+
const gateErr = r2ExportGateError(g, isSyncConfigured());
|
|
112
|
+
if (gateErr)
|
|
113
|
+
throw new Error(gateErr);
|
|
114
|
+
sessionsClient = new R2Client(backend.r2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
process.stderr.write(chalk.red(`Session backup: ${err.message}\n`));
|
|
81
119
|
process.exit(1);
|
|
82
120
|
}
|
|
83
121
|
}
|
|
@@ -142,9 +180,23 @@ async function runExport(selectors, command) {
|
|
|
142
180
|
process.exit(1);
|
|
143
181
|
}
|
|
144
182
|
// 4. Resolve encryption key (opt-in) + redaction (default on via parent --no-redact).
|
|
145
|
-
//
|
|
146
|
-
// unrecoverable on a fresh box)
|
|
147
|
-
|
|
183
|
+
// A BYO R2 backup uses the fleet-shared R2_SYNC_ENC_KEY (an ephemeral key would
|
|
184
|
+
// be unrecoverable on a fresh box). A MANAGED backup resolves a per-account DEK
|
|
185
|
+
// that is MANDATORY and never null — the managed path never uploads plaintext —
|
|
186
|
+
// minting + escrowing one on first use so a fresh box recovers it with zero setup.
|
|
187
|
+
let encryptKey;
|
|
188
|
+
if (g.toR2 && managedBackupUserId) {
|
|
189
|
+
encryptKey = await resolveManagedBackupKey(managedClient, managedBackupUserId);
|
|
190
|
+
}
|
|
191
|
+
else if (g.toR2) {
|
|
192
|
+
encryptKey = resolveR2BackupKey();
|
|
193
|
+
}
|
|
194
|
+
else if (g.encrypt) {
|
|
195
|
+
encryptKey = resolveExportKey();
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
encryptKey = null;
|
|
199
|
+
}
|
|
148
200
|
const redact = g.redact !== false;
|
|
149
201
|
// Value-aware redaction: mask live credential values already in the
|
|
150
202
|
// environment (e.g. an injected secrets bundle) verbatim, whatever their
|
|
@@ -172,7 +224,7 @@ async function runExport(selectors, command) {
|
|
|
172
224
|
records,
|
|
173
225
|
});
|
|
174
226
|
if (g.toR2) {
|
|
175
|
-
await uploadToR2(header, records);
|
|
227
|
+
await uploadToR2(header, records, sessionsClient);
|
|
176
228
|
return;
|
|
177
229
|
}
|
|
178
230
|
emitBundle(header, records, g);
|
|
@@ -205,13 +257,28 @@ export function r2ExportGateError(g, isConfigured) {
|
|
|
205
257
|
}
|
|
206
258
|
return null;
|
|
207
259
|
}
|
|
208
|
-
export async function uploadToR2(header, records) {
|
|
260
|
+
export async function uploadToR2(header, records, resolvedClient) {
|
|
261
|
+
// The resolved client (managed HTTP or BYO R2) is passed in by the command.
|
|
262
|
+
// The no-client overload preserves the original BYO-only signature for the
|
|
263
|
+
// direct unit test (loadR2Config() from the r2.backups bundle).
|
|
209
264
|
let client;
|
|
210
|
-
|
|
211
|
-
client =
|
|
265
|
+
if (resolvedClient) {
|
|
266
|
+
client = resolvedClient;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
try {
|
|
270
|
+
client = new R2Client(loadR2Config());
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
process.stderr.write(chalk.red(`R2 backup: ${err.message}\n`));
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
212
276
|
}
|
|
213
|
-
|
|
214
|
-
|
|
277
|
+
const encryptionError = client.kind === 'managed'
|
|
278
|
+
? managedUploadEncryptionError(header, records)
|
|
279
|
+
: null;
|
|
280
|
+
if (encryptionError) {
|
|
281
|
+
process.stderr.write(chalk.red(`${encryptionError}\n`));
|
|
215
282
|
process.exit(1);
|
|
216
283
|
}
|
|
217
284
|
let uploaded = 0;
|
|
@@ -234,7 +301,16 @@ export async function uploadToR2(header, records) {
|
|
|
234
301
|
uploaded++;
|
|
235
302
|
}
|
|
236
303
|
process.stderr.write(chalk.green(`Backed up ${header.sessions} session${header.sessions === 1 ? '' : 's'} ` +
|
|
237
|
-
`(${uploaded} object${uploaded === 1 ? '' : 's'}${header.encrypted ? ', encrypted' : ', UNENCRYPTED'}) →
|
|
304
|
+
`(${uploaded} object${uploaded === 1 ? '' : 's'}${header.encrypted ? ', encrypted' : ', UNENCRYPTED'}) → ` +
|
|
305
|
+
`${client.kind === 'managed' ? 'managed Phoenix store' : 'R2'}.\n`));
|
|
306
|
+
}
|
|
307
|
+
/** Fail-closed guard at the managed transport boundary, not only at record construction. */
|
|
308
|
+
export function managedUploadEncryptionError(header, records) {
|
|
309
|
+
if (!header.encrypted ||
|
|
310
|
+
records.some(record => !record.encrypted || !isTranscriptEnvelope(record.body))) {
|
|
311
|
+
return 'Managed session backups require AES-256-GCM envelopes; refusing to upload plaintext.';
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
238
314
|
}
|
|
239
315
|
/** R2 object key for one record — dir-shaped agents key by relKey, file-shaped by session. */
|
|
240
316
|
export function r2KeyForRecord(rec) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import { type SessionsBackupClient } from '../lib/session/sync/net-client.js';
|
|
2
3
|
import { type ParsedBundle } from '../lib/session/bundle.js';
|
|
3
4
|
export declare function registerSessionsImportCommand(sessionsCmd: Command): void;
|
|
4
5
|
/**
|
|
@@ -16,4 +17,4 @@ export declare function registerSessionsImportCommand(sessionsCmd: Command): voi
|
|
|
16
17
|
* the command and its test drive the same decision.
|
|
17
18
|
*/
|
|
18
19
|
export declare function r2ImportGateError(fromR2: boolean, isConfigured: boolean): string | null;
|
|
19
|
-
export declare function pullFromR2(): Promise<ParsedBundle>;
|
|
20
|
+
export declare function pullFromR2(resolvedClient?: SessionsBackupClient): Promise<ParsedBundle>;
|
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import { isSyncConfigured, loadR2Config } from '../lib/session/sync/config.js';
|
|
14
|
-
import { resolveSyncEncKey } from '../lib/session/sync/transcript-crypto.js';
|
|
14
|
+
import { isTranscriptEnvelope, resolveSyncEncKey } from '../lib/session/sync/transcript-crypto.js';
|
|
15
15
|
import { R2Client } from '../lib/session/sync/r2.js';
|
|
16
16
|
import { SESSIONS_PREFIX } from '../lib/session/sync/agents.js';
|
|
17
|
+
import { resolveSessionsBackend } from '../lib/session/sync/backend.js';
|
|
18
|
+
import { SessionsHttpClient } from '../lib/session/sync/net-client.js';
|
|
19
|
+
import { resolveManagedBackupKey } from '../lib/session/sync/managed-key.js';
|
|
17
20
|
import { parseBundle, planImport, writeImport, mergeRecords, makeHeader, } from '../lib/session/bundle.js';
|
|
18
21
|
import { pullBundlesFromHosts } from '../lib/session/remote-bundle.js';
|
|
19
22
|
import { setHelpSections } from '../lib/help.js';
|
|
@@ -25,7 +28,8 @@ export function registerSessionsImportCommand(sessionsCmd) {
|
|
|
25
28
|
.option('--overwrite', 'Replace local files that differ from the bundle (default: keep local)')
|
|
26
29
|
.option('--decrypt [key]', 'Decrypt an encrypted bundle (key optional if the r2.backups sync key is configured)')
|
|
27
30
|
.option('--from-host <target...>', 'Pull sessions live from remote peer(s) over SSH instead of a file (repeatable)')
|
|
28
|
-
.option('--from-r2', 'Restore session backups from
|
|
31
|
+
.option('--from-r2', 'Restore session backups from the off-box store (managed Phoenix store when signed in; your own r2.backups bucket with --byo)')
|
|
32
|
+
.option('--byo', 'With --from-r2: restore from your own r2.backups bucket instead of the managed Phoenix store');
|
|
29
33
|
setHelpSections(cmd, {
|
|
30
34
|
examples: `# Preview what a bundle would restore
|
|
31
35
|
agents sessions import week.bundle --dry-run
|
|
@@ -46,9 +50,12 @@ they show up in 'agents sessions' tagged with that machine and never overwrite
|
|
|
46
50
|
your own local sessions. Byte-exact duplicates are skipped. --from-host reuses
|
|
47
51
|
the same SSH transport as the cross-machine listing (no R2, no daemon).
|
|
48
52
|
|
|
49
|
-
--from-r2 downloads every session backup
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
--from-r2 downloads every session backup from the off-box store and restores it
|
|
54
|
+
through the same placement. When signed in it restores from the MANAGED Phoenix
|
|
55
|
+
store and decrypts with your per-account key (recovered automatically, even on a
|
|
56
|
+
fresh box); --byo restores from your own r2.backups bucket with its shared
|
|
57
|
+
R2_SYNC_ENC_KEY (or pass --decrypt <key>). It is the inverse of
|
|
58
|
+
'sessions export --to-r2'.`,
|
|
52
59
|
});
|
|
53
60
|
cmd.action(async (bundlePath, options, command) => {
|
|
54
61
|
const g = command.optsWithGlobals();
|
|
@@ -58,13 +65,39 @@ pass --decrypt <key>). It is the inverse of 'sessions export --to-r2'.`,
|
|
|
58
65
|
async function runImport(bundlePath, options, g, command) {
|
|
59
66
|
// 1. Obtain the bundle — from R2, remote peer(s), stdin, or a file.
|
|
60
67
|
let bundle;
|
|
68
|
+
// A managed restore recovers the per-account DEK (from the local cache or the
|
|
69
|
+
// Worker escrow) and decrypts with it; BYO falls back to the r2.backups key.
|
|
70
|
+
let managedDecryptKey;
|
|
71
|
+
if (options.byo && !options.fromR2) {
|
|
72
|
+
process.stderr.write(chalk.red('--byo is only valid with --from-r2.\n'));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
61
75
|
if (options.fromR2) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
let client;
|
|
77
|
+
let managedClient;
|
|
78
|
+
let managedUserId;
|
|
79
|
+
try {
|
|
80
|
+
const backend = resolveSessionsBackend({ byo: options.byo });
|
|
81
|
+
if (backend.kind === 'managed') {
|
|
82
|
+
managedClient = new SessionsHttpClient({ baseUrl: backend.baseUrl, userId: backend.userId, token: backend.token });
|
|
83
|
+
client = managedClient;
|
|
84
|
+
managedUserId = backend.userId;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const gateErr = r2ImportGateError(true, isSyncConfigured());
|
|
88
|
+
if (gateErr)
|
|
89
|
+
throw new Error(gateErr);
|
|
90
|
+
client = new R2Client(backend.r2);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
process.stderr.write(chalk.red(`R2 restore: ${err.message}\n`));
|
|
65
95
|
process.exit(1);
|
|
66
96
|
}
|
|
67
|
-
bundle = await pullFromR2();
|
|
97
|
+
bundle = await pullFromR2(client);
|
|
98
|
+
if (managedUserId) {
|
|
99
|
+
managedDecryptKey = await resolveManagedBackupKey(managedClient, managedUserId);
|
|
100
|
+
}
|
|
68
101
|
}
|
|
69
102
|
else if (options.fromHost && options.fromHost.length > 0) {
|
|
70
103
|
bundle = await pullForImport(options.fromHost, bundlePath, g, command);
|
|
@@ -98,8 +131,12 @@ async function runImport(bundlePath, options, g, command) {
|
|
|
98
131
|
process.exit(1);
|
|
99
132
|
}
|
|
100
133
|
}
|
|
101
|
-
// 3. Resolve the decryption key if the bundle is encrypted.
|
|
102
|
-
|
|
134
|
+
// 3. Resolve the decryption key if the bundle is encrypted. A managed restore
|
|
135
|
+
// already recovered the per-account DEK; otherwise fall back to an explicit
|
|
136
|
+
// --decrypt <key> or the shared r2.backups key.
|
|
137
|
+
const decryptKey = bundle.header.encrypted
|
|
138
|
+
? (managedDecryptKey ?? resolveDecryptKey(options.decrypt))
|
|
139
|
+
: null;
|
|
103
140
|
// 4. Plan.
|
|
104
141
|
let plan;
|
|
105
142
|
try {
|
|
@@ -193,21 +230,33 @@ export function r2ImportGateError(fromR2, isConfigured) {
|
|
|
193
230
|
}
|
|
194
231
|
return null;
|
|
195
232
|
}
|
|
196
|
-
export async function pullFromR2() {
|
|
233
|
+
export async function pullFromR2(resolvedClient) {
|
|
234
|
+
// The resolved client (managed HTTP or BYO R2) is passed in by the command; the
|
|
235
|
+
// no-client overload preserves the original BYO-only signature (loadR2Config)
|
|
236
|
+
// for the direct unit test. The managed client's list() ignores the prefix and
|
|
237
|
+
// enumerates the whole owner namespace, so one call site serves both.
|
|
197
238
|
let client;
|
|
198
239
|
let bucket;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
bucket =
|
|
202
|
-
client = new R2Client(cfg);
|
|
240
|
+
if (resolvedClient) {
|
|
241
|
+
client = resolvedClient;
|
|
242
|
+
bucket = resolvedClient.kind === 'managed' ? 'managed' : 'byo';
|
|
203
243
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
244
|
+
else {
|
|
245
|
+
try {
|
|
246
|
+
const cfg = loadR2Config();
|
|
247
|
+
bucket = cfg.bucket;
|
|
248
|
+
client = new R2Client(cfg);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
process.stderr.write(chalk.red(`R2 restore: ${err.message}\n`));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
207
254
|
}
|
|
208
255
|
let keys;
|
|
209
256
|
try {
|
|
210
|
-
keys =
|
|
257
|
+
keys = client.kind === 'managed'
|
|
258
|
+
? await client.list()
|
|
259
|
+
: await client.list(SESSIONS_PREFIX);
|
|
211
260
|
}
|
|
212
261
|
catch (err) {
|
|
213
262
|
process.stderr.write(chalk.red(`R2 restore: listing failed: ${err.message}\n`));
|
|
@@ -239,6 +288,19 @@ export async function pullFromR2() {
|
|
|
239
288
|
skipped++;
|
|
240
289
|
continue;
|
|
241
290
|
}
|
|
291
|
+
// Managed transcripts are mandatory AES-256-GCM (SES-51). A plaintext object
|
|
292
|
+
// in the managed store is a contract violation — an older/buggy writer or
|
|
293
|
+
// tampering — so fail loud rather than importing readable plaintext. Check
|
|
294
|
+
// the header AND every record body (matching the Worker's per-record
|
|
295
|
+
// isEncryptedManagedBundle boundary guard), never just the header flag.
|
|
296
|
+
if (client.kind === 'managed') {
|
|
297
|
+
const plaintext = !parsed.header.encrypted
|
|
298
|
+
|| parsed.records.some(rec => !rec.encrypted || !isTranscriptEnvelope(rec.body));
|
|
299
|
+
if (plaintext) {
|
|
300
|
+
process.stderr.write(chalk.red(`R2 restore: managed backup ${key} is not encrypted — managed transcripts are always sealed; refusing to import plaintext.\n`));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
242
304
|
records.push(...parsed.records);
|
|
243
305
|
if (parsed.header.encrypted)
|
|
244
306
|
encryptedAny = true;
|
|
@@ -250,7 +312,9 @@ export async function pullFromR2() {
|
|
|
250
312
|
}
|
|
251
313
|
const deduped = mergeRecords([records]);
|
|
252
314
|
if (deduped.length === 0) {
|
|
253
|
-
process.stderr.write(chalk.red(
|
|
315
|
+
process.stderr.write(chalk.red(bucket === 'managed'
|
|
316
|
+
? 'No session backups found in the managed store.\n'
|
|
317
|
+
: `No session backups found in R2 bucket '${bucket}'.\n`));
|
|
254
318
|
process.exit(1);
|
|
255
319
|
}
|
|
256
320
|
const header = makeHeader({
|
package/dist/commands/traces.js
CHANGED
|
@@ -182,7 +182,7 @@ export function registerTracesCommands(program) {
|
|
|
182
182
|
.description("Sync this device's derived, redacted trajectories to your Phoenix account");
|
|
183
183
|
const setupCmd = tracesCmd
|
|
184
184
|
.command('setup')
|
|
185
|
-
.description('Provision
|
|
185
|
+
.description('(advanced) Provision a self-hosted Cloudflare Worker + R2 bucket for traces — NOT required; `agents auth login` syncs to the managed endpoint with zero setup')
|
|
186
186
|
.option('--bundle <name>', 'secrets bundle holding the Cloudflare API token', DEFAULT_CF_BUNDLE)
|
|
187
187
|
.option('--worker <name>', 'Worker name', DEFAULT_WORKER_NAME)
|
|
188
188
|
.option('--bucket <name>', 'R2 bucket name', DEFAULT_BUCKET_NAME)
|
|
@@ -35,10 +35,8 @@ export const NATIVE_ACCOUNT_CAPABILITIES = {
|
|
|
35
35
|
copilot: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
36
36
|
openclaw: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
37
37
|
amp: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
38
|
-
kiro: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
39
38
|
goose: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
40
39
|
hermes: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
41
|
-
pi: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
42
40
|
warp: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
43
41
|
};
|
|
44
42
|
export function nativeAccountCapability(agent) {
|
|
@@ -54,12 +54,12 @@ export declare function listNativeAccounts(meta: Pick<Meta, 'accounts' | 'device
|
|
|
54
54
|
* running before the body, so callers that only need a native lookup must be
|
|
55
55
|
* able to omit it.
|
|
56
56
|
*/
|
|
57
|
-
export declare function findUnifiedAccount(nameOrId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, doc?: AccountRegistryDocument): UnifiedAccount | null;
|
|
57
|
+
export declare function findUnifiedAccount(nameOrId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, doc?: AccountRegistryDocument, preferAgent?: AgentId): UnifiedAccount | null;
|
|
58
58
|
export declare function addNativeAccount(name: string, agent: AgentId, identityKey: string, identityLabel: string | undefined, scope: 'version' | 'device'): NativeAccount;
|
|
59
59
|
/** Create or replace the version-independent label for one native identity. */
|
|
60
60
|
export declare function labelNativeAccount(agent: AgentId, identityKey: string, identityLabel: string | undefined, label: string | undefined, scope: 'version' | 'device'): NativeAccount;
|
|
61
|
-
export declare function bindAccount(nameOrId: string, target: string): UnifiedAccount;
|
|
62
|
-
export declare function unbindAccount(nameOrId: string, target: string): void;
|
|
61
|
+
export declare function bindAccount(nameOrId: string, target: string, preferAgent?: AgentId): UnifiedAccount;
|
|
62
|
+
export declare function unbindAccount(nameOrId: string, target: string, preferAgent?: AgentId): void;
|
|
63
63
|
/** Every target bound to `accountId`: this box's device-doc bindings merged over
|
|
64
64
|
* the fleet-shared central bindings (PHNX-3315). */
|
|
65
65
|
export declare function accountBindings(accountId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): string[];
|
|
@@ -159,9 +159,18 @@ export function listNativeAccounts(meta) {
|
|
|
159
159
|
* running before the body, so callers that only need a native lookup must be
|
|
160
160
|
* able to omit it.
|
|
161
161
|
*/
|
|
162
|
-
export function findUnifiedAccount(nameOrId, meta, doc) {
|
|
162
|
+
export function findUnifiedAccount(nameOrId, meta, doc, preferAgent) {
|
|
163
163
|
const needle = nameOrId.toLowerCase();
|
|
164
|
-
const
|
|
164
|
+
const matches = listNativeAccounts(meta).filter(account => account.id === nameOrId || account.name.toLowerCase() === needle || account.identityLabel?.toLowerCase() === needle);
|
|
165
|
+
// `identityLabel` defaults to the login's own identifier (the email), so ONE
|
|
166
|
+
// selector legitimately matches several harnesses: `muqsitnawaz@gmail.com` is a
|
|
167
|
+
// claude login AND a codex login. Un-scoped, `.find()` returned whichever row the
|
|
168
|
+
// merged store happened to order first, so `agents run claude#<email>` died with
|
|
169
|
+
// "Account 'personal' is a codex login and cannot authenticate the claude harness"
|
|
170
|
+
// while that identity's own claude login sat right there. Prefer the harness being
|
|
171
|
+
// launched; fall back to the first match so a management lookup with no harness in
|
|
172
|
+
// hand (rename/remove/view) behaves exactly as before.
|
|
173
|
+
const native = matches.find(account => account.agent === preferAgent) ?? matches[0];
|
|
165
174
|
if (native)
|
|
166
175
|
return native;
|
|
167
176
|
const provider = findAccount(nameOrId, doc ?? readAccountRegistry());
|
|
@@ -253,9 +262,13 @@ export function labelNativeAccount(agent, identityKey, identityLabel, label, sco
|
|
|
253
262
|
});
|
|
254
263
|
return { ...matches[0], name: resolvedLabel, identityLabel, scope: rowScope };
|
|
255
264
|
}
|
|
256
|
-
export function bindAccount(nameOrId, target) {
|
|
265
|
+
export function bindAccount(nameOrId, target, preferAgent) {
|
|
257
266
|
const meta = readMeta();
|
|
258
|
-
|
|
267
|
+
// Scope to the harness being bound to: a bare identity selector matches every
|
|
268
|
+
// harness it is signed into, so without this the binding could persist against
|
|
269
|
+
// a different harness's row than the caller validated (the attach command
|
|
270
|
+
// resolves `targetAgent` and passes it here).
|
|
271
|
+
const account = findUnifiedAccount(nameOrId, meta, undefined, preferAgent);
|
|
259
272
|
if (!account)
|
|
260
273
|
throw new Error(`Unknown account '${nameOrId}'.`);
|
|
261
274
|
// A binding follows its account: one that targets a device-scoped native login
|
|
@@ -275,9 +288,11 @@ export function bindAccount(nameOrId, target) {
|
|
|
275
288
|
}
|
|
276
289
|
return account;
|
|
277
290
|
}
|
|
278
|
-
export function unbindAccount(nameOrId, target) {
|
|
291
|
+
export function unbindAccount(nameOrId, target, preferAgent) {
|
|
279
292
|
const meta = readMeta();
|
|
280
|
-
|
|
293
|
+
// Same scoping as bindAccount: detach the row on the harness the caller means,
|
|
294
|
+
// not whichever the store happened to order first for a colliding identity.
|
|
295
|
+
const account = findUnifiedAccount(nameOrId, meta, undefined, preferAgent);
|
|
281
296
|
if (!account)
|
|
282
297
|
throw new Error(`Unknown account '${nameOrId}'.`);
|
|
283
298
|
const inCentral = meta.accounts?.bindings?.[target] === account.id;
|
|
@@ -527,7 +542,10 @@ export function resolveSpawnAccount(explicit, agent, version, meta, opts = {}) {
|
|
|
527
542
|
const selection = resolveAccountSelection(explicit, agent, meta, { useDefault: opts.useDefault, target });
|
|
528
543
|
if (!selection)
|
|
529
544
|
return null;
|
|
530
|
-
|
|
545
|
+
// Scope the lookup to the harness being launched: a bare identity selector
|
|
546
|
+
// (`claude#muqsitnawaz@gmail.com`) matches every harness that identity is signed
|
|
547
|
+
// into, and only this one can authenticate the spawn.
|
|
548
|
+
const unified = findUnifiedAccount(selection.id, meta, undefined, agent);
|
|
531
549
|
if (!unified) {
|
|
532
550
|
// A stale per-harness default is a preference, not a hard requirement: the
|
|
533
551
|
// machine stays runnable by falling back to balanced rotation. Bindings and
|
|
@@ -28,7 +28,7 @@ export interface PublishUsageSnapshotResult {
|
|
|
28
28
|
path: string | null;
|
|
29
29
|
}
|
|
30
30
|
/** Publish this headed device's stable usage snapshot into its owned store file. */
|
|
31
|
-
export declare function publishUsageSnapshotToSharedStore(options?: PublishUsageSnapshotOptions): PublishUsageSnapshotResult
|
|
31
|
+
export declare function publishUsageSnapshotToSharedStore(options?: PublishUsageSnapshotOptions): Promise<PublishUsageSnapshotResult>;
|
|
32
32
|
export interface ConsumeUsageSnapshotsOptions {
|
|
33
33
|
userAgentsDir?: string;
|
|
34
34
|
cachePath?: string;
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* There is deliberately no device-to-device SSH in this module or its tick.
|
|
9
9
|
*/
|
|
10
10
|
import { isHeadedDeviceRole, listConfiguredDeviceRoles, selfConfiguredDeviceRole } from '../device-config.js';
|
|
11
|
-
import { readFleetSharedDeviceStates,
|
|
11
|
+
import { readFleetSharedDeviceStates, updateFleetSharedDeviceStateAsync, } from '../fleet-shared-state.js';
|
|
12
12
|
import { getUserAgentsDir } from '../state.js';
|
|
13
13
|
import { machineId, normalizeHost } from '../session/sync/config.js';
|
|
14
14
|
import { exportClaudeUsageCacheRows, ingestPeerClaudeUsageRows, } from './usage.js';
|
|
15
15
|
/** Publish this headed device's stable usage snapshot into its owned store file. */
|
|
16
|
-
export function publishUsageSnapshotToSharedStore(options = {}) {
|
|
16
|
+
export async function publishUsageSnapshotToSharedStore(options = {}) {
|
|
17
17
|
const result = {
|
|
18
18
|
published: false,
|
|
19
19
|
changed: false,
|
|
@@ -32,7 +32,7 @@ export function publishUsageSnapshotToSharedStore(options = {}) {
|
|
|
32
32
|
return result;
|
|
33
33
|
}
|
|
34
34
|
try {
|
|
35
|
-
const write =
|
|
35
|
+
const write = await updateFleetSharedDeviceStateAsync(options.device ?? machineId(), { usage: { rows } }, options.userAgentsDir ?? getUserAgentsDir());
|
|
36
36
|
result.published = true;
|
|
37
37
|
result.changed = write.changed;
|
|
38
38
|
result.path = write.path;
|
package/dist/lib/acp/client.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type SessionNotification } from '@zed-industries/agent-client-protocol';
|
|
13
13
|
import type { AgentId } from '../types.js';
|
|
14
|
-
import type
|
|
14
|
+
import { type ExecMode } from '../exec.js';
|
|
15
15
|
export interface AcpRunOptions {
|
|
16
16
|
agent: AgentId;
|
|
17
17
|
prompt: string;
|
package/dist/lib/acp/client.js
CHANGED
|
@@ -15,6 +15,7 @@ import { promises as fs } from 'fs';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { ClientSideConnection, ndJsonStream, } from '@zed-industries/agent-client-protocol';
|
|
17
17
|
import { getAcpSpec, supportsAcp } from './harnesses.js';
|
|
18
|
+
import { buildExecEnv } from '../exec.js';
|
|
18
19
|
const PROTOCOL_VERSION = 1;
|
|
19
20
|
/**
|
|
20
21
|
* Runs a single prompt turn against an ACP-capable agent and streams updates
|
|
@@ -25,10 +26,20 @@ export async function runAcp(opts) {
|
|
|
25
26
|
throw new Error(`Agent '${opts.agent}' does not support ACP. Use direct exec instead.`);
|
|
26
27
|
}
|
|
27
28
|
const spec = getAcpSpec(opts.agent);
|
|
29
|
+
// Build the harness's exec env the SAME way `agents run` does, instead of
|
|
30
|
+
// handing it a raw process.env. buildExecEnv is what injects the per-account
|
|
31
|
+
// `CLAUDE_CODE_OAUTH_TOKEN` on a worker device (gated by device role: a
|
|
32
|
+
// headed/personal box still defers to its native login). Without this, an ACP
|
|
33
|
+
// launch inherited no setup-token and fell through to the version home's
|
|
34
|
+
// copied native `.credentials.json` — which expires in ~15h and 401s once its
|
|
35
|
+
// refresh chain dies, exactly the "logged out on a worker" report (PHNX-3681).
|
|
36
|
+
// A verified precedence test shows the env token wins over a present
|
|
37
|
+
// `.credentials.json`, so injecting here is sufficient — the stale file no
|
|
38
|
+
// longer decides the session.
|
|
28
39
|
const child = spawn(spec.command, spec.args, {
|
|
29
40
|
cwd: opts.cwd,
|
|
30
41
|
stdio: ['pipe', 'pipe', 'inherit'],
|
|
31
|
-
env:
|
|
42
|
+
env: buildExecEnv({ agent: opts.agent, cwd: opts.cwd, mode: opts.mode, effort: 'auto', interactive: true }),
|
|
32
43
|
});
|
|
33
44
|
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
34
45
|
const client = buildClient(opts);
|
|
@@ -52,7 +52,7 @@ export const ACP_HARNESSES = {
|
|
|
52
52
|
},
|
|
53
53
|
// antigravity: no documented ACP support (May 2026).
|
|
54
54
|
// goose: ACP over HTTP via `goosed`, not a clean stdio subcommand.
|
|
55
|
-
// copilot
|
|
55
|
+
// copilot: excluded for now (not installed in the reference environment,
|
|
56
56
|
// no local verification possible).
|
|
57
57
|
// amp: not on the ACP agents list.
|
|
58
58
|
};
|
package/dist/lib/add-dir.js
CHANGED
|
@@ -33,11 +33,9 @@ export const ADD_DIR_STRATEGY = {
|
|
|
33
33
|
// No multi-root CLI surface today (single --dir / project path).
|
|
34
34
|
opencode: 'none',
|
|
35
35
|
gemini: 'none',
|
|
36
|
-
pi: 'none',
|
|
37
36
|
openclaw: 'none',
|
|
38
37
|
copilot: 'none',
|
|
39
38
|
amp: 'none',
|
|
40
|
-
kiro: 'none',
|
|
41
39
|
goose: 'none',
|
|
42
40
|
antigravity: 'none',
|
|
43
41
|
droid: 'none',
|
|
@@ -104,7 +104,7 @@ export declare const MANAGED_AGENT_IDS: AgentId[];
|
|
|
104
104
|
* `curl … | sh` / `brew install` script that carries NO version token — the
|
|
105
105
|
* installer can only ever fetch the *current* release, and the binary then keeps
|
|
106
106
|
* itself up to date in place (droid, grok, antigravity, cursor, hermes,
|
|
107
|
-
*
|
|
107
|
+
* goose). There is no semver to pin, so agents-cli must not model these as
|
|
108
108
|
* having multiple installable version-homes the way it does for npm-packaged
|
|
109
109
|
* agents (claude, codex, kimi, …).
|
|
110
110
|
*
|