@phnx-labs/agents-cli 1.22.47 → 1.22.49
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 +401 -17
- package/README.md +66 -32
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +2 -0
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +206 -32
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/browser.js +501 -103
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +0 -20
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/exec.js +52 -15
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +48 -5
- package/dist/commands/go.d.ts +3 -0
- package/dist/commands/go.js +16 -3
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +4 -3
- package/dist/commands/mailboxes.js +4 -3
- package/dist/commands/menubar.js +11 -4
- package/dist/commands/projects.d.ts +16 -1
- package/dist/commands/projects.js +58 -64
- package/dist/commands/resume.js +1 -1
- package/dist/commands/route.js +4 -4
- package/dist/commands/routines.js +3 -2
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-trace.js +1 -1
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/setup-secrets.js +23 -0
- package/dist/commands/share.d.ts +20 -5
- package/dist/commands/share.js +152 -52
- package/dist/commands/ssh.js +54 -7
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.js +17 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/accounting/usage.d.ts +14 -3
- package/dist/lib/accounting/usage.js +64 -18
- package/dist/lib/agent-spec/agents.js +11 -9
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.js +8 -3
- package/dist/lib/browser/profiles.d.ts +35 -101
- package/dist/lib/browser/profiles.js +130 -301
- package/dist/lib/browser/registry.d.ts +39 -0
- package/dist/lib/browser/registry.js +149 -0
- package/dist/lib/browser/remote-control.d.ts +8 -6
- package/dist/lib/browser/remote-control.js +8 -6
- package/dist/lib/browser/resolve-target.d.ts +68 -0
- package/dist/lib/browser/resolve-target.js +250 -0
- package/dist/lib/browser/runtime-state.d.ts +40 -16
- package/dist/lib/browser/runtime-state.js +92 -25
- package/dist/lib/browser/service.d.ts +28 -9
- package/dist/lib/browser/service.js +139 -62
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/byok-usage.js +2 -2
- package/dist/lib/claude-statusline.d.ts +29 -0
- package/dist/lib/claude-statusline.js +139 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +1 -1
- package/dist/lib/cloud/rush.js +1 -1
- package/dist/lib/computer/computer-rpc.js +2 -2
- package/dist/lib/computer/download.d.ts +15 -15
- package/dist/lib/computer/download.js +35 -98
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
- package/dist/lib/computer/ssh-tunnel.js +12 -22
- package/dist/lib/config-keys.d.ts +2 -8
- package/dist/lib/config-keys.js +2 -13
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +2 -2
- package/dist/lib/crabbox/lease.d.ts +9 -0
- package/dist/lib/crabbox/lease.js +70 -15
- package/dist/lib/crabbox/runtimes.js +1 -1
- package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
- package/dist/lib/daemon/account-state-daemon-service.js +26 -0
- package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
- package/dist/lib/daemon/browser-ipc-service.js +46 -0
- package/dist/lib/daemon/daemon.d.ts +3 -0
- package/dist/lib/daemon/daemon.js +212 -444
- package/dist/lib/daemon/device-probe-service.d.ts +26 -0
- package/dist/lib/daemon/device-probe-service.js +47 -0
- package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
- package/dist/lib/daemon/keychain-reap-service.js +32 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
- package/dist/lib/daemon/monitor-engine-service.js +29 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
- package/dist/lib/daemon/secrets-broker-service.js +66 -0
- package/dist/lib/daemon/self-heal-service.d.ts +27 -0
- package/dist/lib/daemon/self-heal-service.js +48 -0
- package/dist/lib/daemon/service.d.ts +102 -0
- package/dist/lib/daemon/service.js +98 -0
- package/dist/lib/daemon/session-index-service.d.ts +22 -0
- package/dist/lib/daemon/session-index-service.js +40 -0
- package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
- package/dist/lib/daemon/state-dir-check-service.js +63 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +298 -0
- package/dist/lib/daemon/watchdog-service.d.ts +19 -0
- package/dist/lib/daemon/watchdog-service.js +40 -0
- package/dist/lib/daemon-health.d.ts +15 -0
- package/dist/lib/daemon-health.js +29 -5
- package/dist/lib/daemon-services.d.ts +8 -1
- package/dist/lib/daemon-services.js +55 -0
- package/dist/lib/daemon-ticks.d.ts +13 -2
- package/dist/lib/daemon-ticks.js +20 -18
- package/dist/lib/device-config.d.ts +0 -2
- package/dist/lib/device-config.js +1 -23
- package/dist/lib/devices/registry.d.ts +1 -1
- package/dist/lib/devices/registry.js +1 -1
- package/dist/lib/devices/stats-cache.d.ts +20 -0
- package/dist/lib/devices/stats-cache.js +49 -5
- package/dist/lib/devices/worker-pick.d.ts +48 -0
- package/dist/lib/devices/worker-pick.js +113 -0
- package/dist/lib/exec.d.ts +67 -13
- package/dist/lib/exec.js +85 -29
- package/dist/lib/harness/adapters/cursor.js +8 -11
- package/dist/lib/helper-download.d.ts +108 -0
- package/dist/lib/helper-download.js +194 -0
- package/dist/lib/helper-versions.d.ts +52 -0
- package/dist/lib/helper-versions.js +58 -0
- package/dist/lib/hooks/cache.js +11 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hosts/credentials.d.ts +1 -1
- package/dist/lib/hosts/credentials.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +7 -1
- package/dist/lib/hosts/dispatch.js +24 -5
- package/dist/lib/hosts/passthrough.d.ts +8 -15
- package/dist/lib/hosts/passthrough.js +19 -16
- package/dist/lib/hosts/providers/devices.d.ts +1 -1
- package/dist/lib/hosts/providers/devices.js +1 -1
- package/dist/lib/hosts/reconnect.d.ts +179 -21
- package/dist/lib/hosts/reconnect.js +266 -45
- package/dist/lib/installations/migrate.d.ts +4 -5
- package/dist/lib/installations/migrate.js +6 -7
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -13
- package/dist/lib/installations/versions.js +25 -2
- package/dist/lib/linear-autoclose.d.ts +1 -1
- package/dist/lib/linear-autoclose.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/menubar/download-menubar.d.ts +47 -0
- package/dist/lib/menubar/download-menubar.js +60 -0
- package/dist/lib/menubar/install-menubar.d.ts +31 -7
- package/dist/lib/menubar/install-menubar.js +79 -21
- package/dist/lib/menubar/notify-desktop.d.ts +3 -3
- package/dist/lib/menubar/notify-desktop.js +6 -6
- package/dist/lib/model-tiers.d.ts +1 -1
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/project-pull.d.ts +1 -1
- package/dist/lib/project-pull.js +2 -2
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/projects.js +1 -1
- package/dist/lib/sandbox.js +6 -7
- package/dist/lib/secrets/download-keychain.d.ts +47 -0
- package/dist/lib/secrets/download-keychain.js +70 -0
- package/dist/lib/secrets/install-helper.d.ts +20 -1
- package/dist/lib/secrets/install-helper.js +44 -3
- package/dist/lib/session/active.js +17 -1
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +138 -6
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +33 -5
- package/dist/lib/session/highlights.d.ts +49 -1
- package/dist/lib/session/highlights.js +95 -0
- package/dist/lib/session/host-link.d.ts +16 -2
- package/dist/lib/session/host-link.js +7 -1
- package/dist/lib/session/parse.js +33 -1
- package/dist/lib/session/remote/remote-list.d.ts +9 -0
- package/dist/lib/session/remote/remote-list.js +19 -2
- package/dist/lib/session/remote/watch.js +11 -2
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +88 -0
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/tool-calls.d.ts +2 -0
- package/dist/lib/session/tool-calls.js +1 -1
- package/dist/lib/session/trajectory-html.d.ts +5 -1
- package/dist/lib/session/trajectory-html.js +42 -22
- package/dist/lib/session/trajectory-text.js +9 -2
- package/dist/lib/session/trajectory.js +53 -4
- package/dist/lib/session/types.d.ts +15 -0
- package/dist/lib/sha256-asset.d.ts +26 -0
- package/dist/lib/sha256-asset.js +41 -0
- package/dist/lib/share/backend.d.ts +95 -0
- package/dist/lib/share/backend.js +139 -0
- package/dist/lib/share/delete.d.ts +6 -2
- package/dist/lib/share/delete.js +8 -9
- package/dist/lib/share/html.d.ts +22 -0
- package/dist/lib/share/html.js +88 -0
- package/dist/lib/share/provision.d.ts +18 -4
- package/dist/lib/share/provision.js +38 -4
- package/dist/lib/share/publish.d.ts +39 -15
- package/dist/lib/share/publish.js +55 -26
- package/dist/lib/share/worker-template.js +171 -11
- package/dist/lib/sqlite.js +16 -3
- package/dist/lib/ssh-exec.d.ts +65 -0
- package/dist/lib/ssh-exec.js +120 -4
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +7 -3
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/terminal/types.js +1 -1
- package/dist/lib/tmux/orphan-reap.js +1 -1
- package/dist/lib/tmux/session.js +31 -0
- package/dist/lib/traces/backend.d.ts +29 -0
- package/dist/lib/traces/backend.js +46 -0
- package/dist/lib/traces/classify.d.ts +52 -0
- package/dist/lib/traces/classify.js +72 -0
- package/dist/lib/traces/config.d.ts +10 -0
- package/dist/lib/traces/config.js +2 -0
- package/dist/lib/traces/provision.d.ts +19 -0
- package/dist/lib/traces/provision.js +30 -0
- package/dist/lib/traces/sync.d.ts +150 -0
- package/dist/lib/traces/sync.js +447 -0
- package/dist/lib/traces/worker-template.d.ts +4 -0
- package/dist/lib/traces/worker-template.js +159 -0
- package/dist/lib/types.d.ts +18 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-refresh.d.ts +1 -1
- package/dist/lib/usage-refresh.js +11 -8
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +4 -6
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/usage-fleet.d.ts +0 -32
- package/dist/lib/usage-fleet.js +0 -125
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental sync of derived, redacted SessionTrajectory blobs to the
|
|
3
|
+
* agents-traces R2 store.
|
|
4
|
+
*
|
|
5
|
+
* Security invariant: only the derived signal (steps + gaps + stats +
|
|
6
|
+
* errorCount + programTimeShare) is uploaded — no raw transcript text.
|
|
7
|
+
* redactSecrets() is called inside buildTrajectory() before we PUT.
|
|
8
|
+
*
|
|
9
|
+
* Sync gate: `sessions.db` `file_mtime_ms` is the source of truth. A ledger
|
|
10
|
+
* at `getRuntimeStateDir()/traces-sync.json` records the last sync timestamp
|
|
11
|
+
* per device so re-runs skip unchanged sessions.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { getDB, readSessionInsights, readSessionTopics, writeSessionInsights, writeSessionTopics, } from '../session/db.js';
|
|
17
|
+
import { parseSession } from '../session/parse.js';
|
|
18
|
+
import { buildTrajectory } from '../session/trajectory.js';
|
|
19
|
+
import { computeInsightFacets } from '../session/insights.js';
|
|
20
|
+
import { knownSecretValuesFromEnv, redactSecrets } from '../redact.js';
|
|
21
|
+
import { getRuntimeStateDir } from '../state.js';
|
|
22
|
+
import { resolveTracesBackend } from './backend.js';
|
|
23
|
+
import { classifyCause, classifyTopic, computeDriftSignal, } from './classify.js';
|
|
24
|
+
/** Push derived, redacted trajectories for this device to the traces store. */
|
|
25
|
+
export async function syncTraces(opts = {}) {
|
|
26
|
+
const dryRun = opts.dryRun === true;
|
|
27
|
+
const outDir = opts.outDir;
|
|
28
|
+
if (dryRun && !outDir) {
|
|
29
|
+
throw new Error('traces sync --dry-run requires --out <dir>');
|
|
30
|
+
}
|
|
31
|
+
// A dry-run computes locally and writes to disk: no Phoenix backend needed.
|
|
32
|
+
const backend = dryRun ? null : resolveTracesBackend();
|
|
33
|
+
const owner = backend?.userId ?? 'local';
|
|
34
|
+
const ledger = readSyncLedger();
|
|
35
|
+
const db = getDB();
|
|
36
|
+
const device = localDevice();
|
|
37
|
+
// Scope to this machine only — the DB can contain peer rows mirrored from the
|
|
38
|
+
// fleet; uploading those under this device's prefix would corrupt the index.
|
|
39
|
+
// NULL machine rows are legacy local sessions (pre-machine-field). A dry-run
|
|
40
|
+
// ignores the incremental watermark so the export covers every session.
|
|
41
|
+
const sinceMtime = dryRun ? 0 : (ledger.lastSyncMtime ?? 0);
|
|
42
|
+
const rows = db
|
|
43
|
+
.prepare('SELECT * FROM sessions WHERE (machine = ? OR machine IS NULL) AND file_mtime_ms > ? ORDER BY file_mtime_ms ASC')
|
|
44
|
+
.all(device, sinceMtime);
|
|
45
|
+
const limited = opts.limit !== undefined ? rows.slice(0, opts.limit) : rows;
|
|
46
|
+
const knownSecrets = knownSecretValuesFromEnv();
|
|
47
|
+
let uploaded = 0;
|
|
48
|
+
let skipped = 0;
|
|
49
|
+
let errors = 0;
|
|
50
|
+
// Advance the watermark only to the max mtime of successfully uploaded sessions
|
|
51
|
+
// so failures are retried on the next run.
|
|
52
|
+
let maxSuccessMtime = ledger.lastSyncMtime ?? 0;
|
|
53
|
+
if (dryRun && outDir) {
|
|
54
|
+
fs.mkdirSync(path.join(outDir, 'sessions'), { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
for (const row of limited) {
|
|
57
|
+
if (!row.file_path) {
|
|
58
|
+
skipped++;
|
|
59
|
+
maxSuccessMtime = Math.max(maxSuccessMtime, row.file_mtime_ms ?? 0);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let traj;
|
|
63
|
+
try {
|
|
64
|
+
const session = rowToMeta(row);
|
|
65
|
+
const events = parseSession(row.file_path, row.agent);
|
|
66
|
+
traj = buildTrajectory(events, session, { redact: true, knownSecrets });
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
errors++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
if (dryRun && outDir) {
|
|
74
|
+
fs.writeFileSync(path.join(outDir, 'sessions', `${row.id}.json`), JSON.stringify(buildSessionDetail(traj)));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
await putSessionTrace(backend, device, row.id, traj);
|
|
78
|
+
}
|
|
79
|
+
uploaded++;
|
|
80
|
+
maxSuccessMtime = Math.max(maxSuccessMtime, row.file_mtime_ms ?? 0);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
errors++;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!opts.skipIndex) {
|
|
87
|
+
try {
|
|
88
|
+
const allRows = db
|
|
89
|
+
.prepare('SELECT * FROM sessions WHERE machine = ? OR machine IS NULL')
|
|
90
|
+
.all(device);
|
|
91
|
+
// Seed rolling bucket history from the previous shard so drift signals accumulate.
|
|
92
|
+
let prevShard = null;
|
|
93
|
+
if (dryRun && outDir) {
|
|
94
|
+
const prevPath = path.join(outDir, 'index.json');
|
|
95
|
+
try {
|
|
96
|
+
prevShard = JSON.parse(fs.readFileSync(prevPath, 'utf8'));
|
|
97
|
+
}
|
|
98
|
+
catch { /* first run — no prior shard */ }
|
|
99
|
+
}
|
|
100
|
+
else if (backend) {
|
|
101
|
+
prevShard = await getIndexShard(backend, device);
|
|
102
|
+
}
|
|
103
|
+
const shard = buildIndexShard(allRows, device, owner, prevShard);
|
|
104
|
+
if (dryRun && outDir) {
|
|
105
|
+
fs.writeFileSync(path.join(outDir, 'index.json'), JSON.stringify(shard, null, 2));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
await putIndexShard(backend, device, owner, shard);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// index PUT/write failure is not fatal — the per-session data is already written
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// A dry-run never advances the incremental watermark: it is a read-only export.
|
|
116
|
+
if (!dryRun) {
|
|
117
|
+
writeSyncLedger({ lastSyncMtime: maxSuccessMtime });
|
|
118
|
+
}
|
|
119
|
+
return { uploaded, skipped, errors };
|
|
120
|
+
}
|
|
121
|
+
function percentile(values, ratio) {
|
|
122
|
+
if (values.length === 0)
|
|
123
|
+
return 0;
|
|
124
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
125
|
+
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)];
|
|
126
|
+
}
|
|
127
|
+
function failureDescription(call, cause) {
|
|
128
|
+
if (cause === 'guard')
|
|
129
|
+
return /main-branch-guard/i.test(`${call.error_code ?? ''} ${call.error ?? ''}`)
|
|
130
|
+
? 'main branch guard' : 'git guard';
|
|
131
|
+
if (cause === 'hook')
|
|
132
|
+
return 'auto-mode hook denial';
|
|
133
|
+
if (call.status_code != null)
|
|
134
|
+
return `HTTP ${call.status_code}`;
|
|
135
|
+
if (call.exit_code != null)
|
|
136
|
+
return `exit ${call.exit_code}`;
|
|
137
|
+
if (call.error_code)
|
|
138
|
+
return 'tool error code';
|
|
139
|
+
if (call.parse_error)
|
|
140
|
+
return 'parse error';
|
|
141
|
+
return 'tool error';
|
|
142
|
+
}
|
|
143
|
+
function attentionFlags(errorCount, facets) {
|
|
144
|
+
const flags = [];
|
|
145
|
+
if (errorCount > 0)
|
|
146
|
+
flags.push(`${errorCount} error${errorCount === 1 ? '' : 's'}`);
|
|
147
|
+
const friction = facets?.frictionSignals ?? {};
|
|
148
|
+
const corrections = facets?.correctionSignals ?? {};
|
|
149
|
+
const retryCount = Object.entries(friction)
|
|
150
|
+
.filter(([key]) => key.startsWith('failed tool loop:'))
|
|
151
|
+
.reduce((sum, [, count]) => sum + count, 0);
|
|
152
|
+
if (retryCount > 0)
|
|
153
|
+
flags.push('retry loop');
|
|
154
|
+
const stall = Object.entries(friction).find(([key, count]) => key.startsWith('silent stall:') && count > 0);
|
|
155
|
+
if (stall)
|
|
156
|
+
flags.push(stall[0].replace('silent stall: ', 'stalled '));
|
|
157
|
+
const correctionCount = Object.values(corrections).reduce((sum, count) => sum + count, 0);
|
|
158
|
+
if (correctionCount > 0)
|
|
159
|
+
flags.push(`${correctionCount} correction${correctionCount === 1 ? '' : 's'}`);
|
|
160
|
+
return flags;
|
|
161
|
+
}
|
|
162
|
+
/** Build the redacted rich console shard from indexed metadata and derived caches. */
|
|
163
|
+
export function buildIndexShard(rows, device, owner, prevShard) {
|
|
164
|
+
const db = getDB();
|
|
165
|
+
const knownSecrets = knownSecretValuesFromEnv();
|
|
166
|
+
const ids = rows.map((row) => row.id);
|
|
167
|
+
const calls = [];
|
|
168
|
+
for (let i = 0; i < ids.length; i += 400) {
|
|
169
|
+
const chunk = ids.slice(i, i + 400);
|
|
170
|
+
calls.push(...db.prepare(`
|
|
171
|
+
SELECT session_id, tool, outcome, exit_code, status_code, error_code, error, parse_error
|
|
172
|
+
FROM tool_calls
|
|
173
|
+
WHERE session_id IN (${chunk.map(() => '?').join(',')})
|
|
174
|
+
`).all(...chunk));
|
|
175
|
+
}
|
|
176
|
+
const toolMix = new Map();
|
|
177
|
+
const errorCounts = new Map();
|
|
178
|
+
for (const call of calls) {
|
|
179
|
+
const mix = toolMix.get(call.session_id) ?? {};
|
|
180
|
+
mix[call.tool] = (mix[call.tool] ?? 0) + 1;
|
|
181
|
+
toolMix.set(call.session_id, mix);
|
|
182
|
+
if (call.outcome === 'error') {
|
|
183
|
+
errorCounts.set(call.session_id, (errorCounts.get(call.session_id) ?? 0) + 1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const topics = readSessionTopics(ids);
|
|
187
|
+
const missingTopics = rows.filter((row) => !topics.has(row.id)).map((row) => {
|
|
188
|
+
const topic = classifyTopic({
|
|
189
|
+
cwd: row.cwd,
|
|
190
|
+
gitBranch: row.git_branch,
|
|
191
|
+
topic: row.topic,
|
|
192
|
+
label: row.label,
|
|
193
|
+
toolMix: toolMix.get(row.id),
|
|
194
|
+
});
|
|
195
|
+
topics.set(row.id, topic);
|
|
196
|
+
return { id: row.id, fileMtimeMs: row.file_mtime_ms, fileSize: row.file_size, topic };
|
|
197
|
+
});
|
|
198
|
+
writeSessionTopics(missingTopics);
|
|
199
|
+
const insights = readSessionInsights(ids);
|
|
200
|
+
const missingInsights = [];
|
|
201
|
+
for (const row of rows.filter((candidate) => !insights.has(candidate.id))) {
|
|
202
|
+
try {
|
|
203
|
+
const events = parseSession(row.file_path, row.agent);
|
|
204
|
+
const facets = computeInsightFacets(events);
|
|
205
|
+
insights.set(row.id, facets);
|
|
206
|
+
missingInsights.push({
|
|
207
|
+
id: row.id,
|
|
208
|
+
fileMtimeMs: row.file_mtime_ms,
|
|
209
|
+
fileSize: row.file_size,
|
|
210
|
+
facets,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
writeSessionInsights(missingInsights);
|
|
218
|
+
const needsAttention = rows.flatMap((row) => {
|
|
219
|
+
const facets = insights.get(row.id);
|
|
220
|
+
const errorCount = errorCounts.get(row.id) ?? 0;
|
|
221
|
+
const flags = attentionFlags(errorCount, facets);
|
|
222
|
+
if (flags.length === 0)
|
|
223
|
+
return [];
|
|
224
|
+
const friction = Object.values(facets?.frictionSignals ?? {}).reduce((sum, count) => sum + count, 0);
|
|
225
|
+
const corrections = Object.values(facets?.correctionSignals ?? {}).reduce((sum, count) => sum + count, 0);
|
|
226
|
+
return [{
|
|
227
|
+
id: row.id,
|
|
228
|
+
title: redactSecrets(row.label ?? row.topic ?? topics.get(row.id)?.label ?? 'Untitled session', knownSecrets),
|
|
229
|
+
repo: row.project ?? (row.cwd ? path.basename(row.cwd) : 'unknown'),
|
|
230
|
+
device,
|
|
231
|
+
agent: row.agent,
|
|
232
|
+
model: row.model ?? 'unknown',
|
|
233
|
+
severity: errorCount * 2 + friction * 3 + corrections * 2,
|
|
234
|
+
flags,
|
|
235
|
+
}];
|
|
236
|
+
}).sort((a, b) => b.severity - a.severity || a.id.localeCompare(b.id));
|
|
237
|
+
const topicCounts = new Map();
|
|
238
|
+
for (const topic of topics.values()) {
|
|
239
|
+
const current = topicCounts.get(topic.key) ?? { ...topic, count: 0 };
|
|
240
|
+
current.count++;
|
|
241
|
+
topicCounts.set(topic.key, current);
|
|
242
|
+
}
|
|
243
|
+
const failedCalls = calls.filter((call) => call.outcome === 'error');
|
|
244
|
+
const byCause = { real: 0, guard: 0, hook: 0 };
|
|
245
|
+
const failureCounts = new Map();
|
|
246
|
+
for (const call of failedCalls) {
|
|
247
|
+
const cause = classifyCause(call);
|
|
248
|
+
const desc = failureDescription(call, cause);
|
|
249
|
+
byCause[cause]++;
|
|
250
|
+
const key = `${call.tool}\u0000${desc}\u0000${cause}`;
|
|
251
|
+
const current = failureCounts.get(key) ?? { tool: call.tool, desc, cause, count: 0 };
|
|
252
|
+
current.count++;
|
|
253
|
+
failureCounts.set(key, current);
|
|
254
|
+
}
|
|
255
|
+
const durations = rows.flatMap((row) => row.duration_ms == null ? [] : [row.duration_ms]);
|
|
256
|
+
// Build today's per-bucket stats for the rolling drift window.
|
|
257
|
+
const todayDate = new Date().toISOString().slice(0, 10);
|
|
258
|
+
const todayStats = [...topicCounts.values()].map(({ key }) => {
|
|
259
|
+
const sessionsInBucket = [...topics.entries()]
|
|
260
|
+
.filter(([, t]) => t.key === key)
|
|
261
|
+
.map(([id]) => id);
|
|
262
|
+
const bucketCalls = calls.filter((c) => sessionsInBucket.includes(c.session_id));
|
|
263
|
+
const bucketErrors = bucketCalls.filter((c) => c.outcome === 'error').length;
|
|
264
|
+
const errorRate = bucketCalls.length === 0 ? 0 : bucketErrors / bucketCalls.length;
|
|
265
|
+
const stallCount = sessionsInBucket.filter((id) => {
|
|
266
|
+
const facets = insights.get(id);
|
|
267
|
+
return Object.entries(facets?.frictionSignals ?? {})
|
|
268
|
+
.some(([k, v]) => k.startsWith('silent stall:') && v > 0);
|
|
269
|
+
}).length;
|
|
270
|
+
const stallRate = sessionsInBucket.length === 0 ? 0 : stallCount / sessionsInBucket.length;
|
|
271
|
+
return { key, date: todayDate, count: topicCounts.get(key).count, errorRate, stallRate };
|
|
272
|
+
});
|
|
273
|
+
const prevHistory = prevShard?.bucketHistory ?? [];
|
|
274
|
+
const bucketHistory = [...prevHistory, todayStats].slice(-14);
|
|
275
|
+
const driftSignals = computeDriftSignal(prevHistory, todayStats);
|
|
276
|
+
return {
|
|
277
|
+
schema: 1,
|
|
278
|
+
device,
|
|
279
|
+
syncedAt: Date.now(),
|
|
280
|
+
owner,
|
|
281
|
+
stats: {
|
|
282
|
+
sessionsImported: rows.length,
|
|
283
|
+
medianMs: percentile(durations, 0.5),
|
|
284
|
+
p90Ms: percentile(durations, 0.9),
|
|
285
|
+
needAttention: needsAttention.length,
|
|
286
|
+
toolErrorRate: calls.length === 0 ? 0 : failedCalls.length / calls.length,
|
|
287
|
+
},
|
|
288
|
+
needsAttention,
|
|
289
|
+
topics: [...topicCounts.values()].sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)),
|
|
290
|
+
failures: {
|
|
291
|
+
byToolError: [...failureCounts.values()].sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)),
|
|
292
|
+
byCause,
|
|
293
|
+
},
|
|
294
|
+
bucketHistory,
|
|
295
|
+
driftSignals,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** Plain-language summary of the friction in a run, or null when it ran clean. */
|
|
299
|
+
function buildWhereItWentWrong(traj) {
|
|
300
|
+
const errorSteps = traj.steps.filter((s) => s.outcome === 'error');
|
|
301
|
+
const biggestGap = traj.gaps.reduce((max, g) => (!max || g.durationMs > max.durationMs ? g : max), null);
|
|
302
|
+
const parts = [];
|
|
303
|
+
if (errorSteps.length > 0) {
|
|
304
|
+
const first = errorSteps[0];
|
|
305
|
+
const who = first.tool ?? first.lane;
|
|
306
|
+
parts.push(`${errorSteps.length} tool error${errorSteps.length === 1 ? '' : 's'} (first: ${who} — ${first.label})`);
|
|
307
|
+
}
|
|
308
|
+
if (biggestGap && biggestGap.durationMs >= 60_000) {
|
|
309
|
+
parts.push(`stalled ${Math.round(biggestGap.durationMs / 60_000)}m`);
|
|
310
|
+
}
|
|
311
|
+
if (parts.length === 0)
|
|
312
|
+
return null;
|
|
313
|
+
return `This run hit ${parts.join('; ')}.`;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Map the derived trajectory to the console's SessionDetail shape, stripping
|
|
317
|
+
* local-machine PII (full cwd, account) that would expose filesystem paths if
|
|
318
|
+
* written to R2. `repo` is the cwd basename only.
|
|
319
|
+
*/
|
|
320
|
+
export function buildSessionDetail(traj) {
|
|
321
|
+
const s = traj.session;
|
|
322
|
+
const stats = traj.stats;
|
|
323
|
+
const repo = s.project ?? (s.cwd ? path.basename(s.cwd) : 'unknown');
|
|
324
|
+
return {
|
|
325
|
+
schema: 1,
|
|
326
|
+
id: s.id,
|
|
327
|
+
meta: {
|
|
328
|
+
spanMs: traj.spanMs,
|
|
329
|
+
turns: (stats.userTurns ?? 0) + (stats.assistantTurns ?? 0),
|
|
330
|
+
tools: stats.toolCount ?? 0,
|
|
331
|
+
errorCount: traj.errorCount,
|
|
332
|
+
tokens: stats.outputTokens ?? 0,
|
|
333
|
+
costUsd: s.costUsd ?? 0,
|
|
334
|
+
outcome: traj.errorCount > 0 ? 'errored' : 'completed',
|
|
335
|
+
repo,
|
|
336
|
+
agent: s.agent,
|
|
337
|
+
model: s.model ?? 'unknown',
|
|
338
|
+
},
|
|
339
|
+
steps: traj.steps,
|
|
340
|
+
gaps: traj.gaps,
|
|
341
|
+
truncatedSteps: traj.truncatedSteps,
|
|
342
|
+
whereItWentWrong: buildWhereItWentWrong(traj),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
async function getIndexShard(backend, device) {
|
|
346
|
+
const url = `${backend.baseUrl}/${backend.userId}/${device}/index.json`;
|
|
347
|
+
try {
|
|
348
|
+
const res = await fetch(url, {
|
|
349
|
+
headers: { authorization: `Bearer ${backend.token}` },
|
|
350
|
+
});
|
|
351
|
+
if (!res.ok)
|
|
352
|
+
return null;
|
|
353
|
+
return await res.json();
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function putSessionTrace(backend, device, sessionId, traj) {
|
|
360
|
+
const url = `${backend.baseUrl}/${backend.userId}/${device}/sessions/${sessionId}.json`;
|
|
361
|
+
const res = await fetch(url, {
|
|
362
|
+
method: 'PUT',
|
|
363
|
+
headers: {
|
|
364
|
+
authorization: `Bearer ${backend.token}`,
|
|
365
|
+
'content-type': 'application/json; charset=utf-8',
|
|
366
|
+
},
|
|
367
|
+
body: JSON.stringify(buildSessionDetail(traj)),
|
|
368
|
+
});
|
|
369
|
+
if (!res.ok) {
|
|
370
|
+
throw new Error(`PUT ${url} → ${res.status}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function putIndexShard(backend, device, owner, shard) {
|
|
374
|
+
const full = { ...shard, device, owner, syncedAt: Date.now() };
|
|
375
|
+
const url = `${backend.baseUrl}/${backend.userId}/${device}/index.json`;
|
|
376
|
+
const res = await fetch(url, {
|
|
377
|
+
method: 'PUT',
|
|
378
|
+
headers: {
|
|
379
|
+
authorization: `Bearer ${backend.token}`,
|
|
380
|
+
'content-type': 'application/json; charset=utf-8',
|
|
381
|
+
},
|
|
382
|
+
body: JSON.stringify(full),
|
|
383
|
+
});
|
|
384
|
+
if (!res.ok) {
|
|
385
|
+
throw new Error(`PUT ${url} → ${res.status}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function ledgerPath() {
|
|
389
|
+
return path.join(getRuntimeStateDir(), 'traces-sync.json');
|
|
390
|
+
}
|
|
391
|
+
export function readSyncLedger() {
|
|
392
|
+
try {
|
|
393
|
+
const raw = fs.readFileSync(ledgerPath(), 'utf8');
|
|
394
|
+
return JSON.parse(raw);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return {};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
export function writeSyncLedger(ledger) {
|
|
401
|
+
const p = ledgerPath();
|
|
402
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
403
|
+
fs.writeFileSync(p, JSON.stringify(ledger, null, 2) + '\n', 'utf8');
|
|
404
|
+
}
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// Local device name
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
function localDevice() {
|
|
409
|
+
return (process.env['AGENTS_SYNC_MACHINE_ID'] ?? os.hostname()).toLowerCase().replace(/\.local$/, '');
|
|
410
|
+
}
|
|
411
|
+
/** Minimal subset of rowToMeta needed to satisfy buildTrajectory's SessionMeta param. */
|
|
412
|
+
function rowToMeta(row) {
|
|
413
|
+
const SESSION_RUN_MODES = ['plan', 'edit', 'auto', 'skip'];
|
|
414
|
+
return {
|
|
415
|
+
id: row.id,
|
|
416
|
+
shortId: row.short_id,
|
|
417
|
+
agent: row.agent,
|
|
418
|
+
origin: row.origin === 'routine' ? 'routine' : 'cli',
|
|
419
|
+
routineName: row.routine_name ?? undefined,
|
|
420
|
+
routineRunId: row.routine_run_id ?? undefined,
|
|
421
|
+
timestamp: row.timestamp,
|
|
422
|
+
lastActivity: row.last_activity ?? undefined,
|
|
423
|
+
project: row.project ?? undefined,
|
|
424
|
+
cwd: row.cwd ?? undefined,
|
|
425
|
+
filePath: row.file_path,
|
|
426
|
+
gitBranch: row.git_branch ?? undefined,
|
|
427
|
+
messageCount: row.message_count ?? undefined,
|
|
428
|
+
tokenCount: row.token_count ?? undefined,
|
|
429
|
+
outputTokens: row.output_tokens ?? undefined,
|
|
430
|
+
inputTokens: row.input_tokens ?? undefined,
|
|
431
|
+
cacheReadTokens: row.cache_read_tokens ?? undefined,
|
|
432
|
+
cacheWriteTokens: row.cache_write_tokens ?? undefined,
|
|
433
|
+
costUsd: row.cost_usd ?? undefined,
|
|
434
|
+
costUsdNoCache: row.cost_usd_nocache ?? undefined,
|
|
435
|
+
durationMs: row.duration_ms ?? undefined,
|
|
436
|
+
model: row.model ?? undefined,
|
|
437
|
+
toolCallCount: row.tool_call_count ?? undefined,
|
|
438
|
+
version: row.version ?? undefined,
|
|
439
|
+
account: row.account ?? undefined,
|
|
440
|
+
accountKey: row.account_key ?? undefined,
|
|
441
|
+
accountOrg: row.account_org ?? undefined,
|
|
442
|
+
mode: SESSION_RUN_MODES.includes(row.mode ?? '') ? row.mode : undefined,
|
|
443
|
+
topic: row.topic ?? undefined,
|
|
444
|
+
label: row.label ?? undefined,
|
|
445
|
+
machine: row.machine ?? undefined,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The Cloudflare Worker that fronts the R2 traces bucket.
|
|
2
|
+
//
|
|
3
|
+
// SECURITY: unlike agents-share, there is NO public GET path here. Every route
|
|
4
|
+
// — PUT and GET — requires a Phoenix bearer. The caller must be the record's
|
|
5
|
+
// owner (path prefix == verified userId). A missing bearer → 401; a wrong
|
|
6
|
+
// owner → 403; public cache headers are NEVER emitted.
|
|
7
|
+
//
|
|
8
|
+
// Routes (all require Phoenix bearer + userId owner match):
|
|
9
|
+
// PUT /<userId>/<device>/index.json — per-device stats shard
|
|
10
|
+
// PUT /<userId>/<device>/sessions/<id>.json — per-session SessionDetail
|
|
11
|
+
// GET /<userId>/... — returns the stored object
|
|
12
|
+
// GET / — 200 description (no data)
|
|
13
|
+
//
|
|
14
|
+
// Emitted as a string so it compiles into dist/** and ships with no
|
|
15
|
+
// package.json#files change. `provisionTraces.ts` uploads this verbatim as an
|
|
16
|
+
// ES-module Worker with a BUCKET (R2) binding + a PHOENIX_ID_BASE secret.
|
|
17
|
+
/**
|
|
18
|
+
* Render the traces Worker source. Pure — bindings are wired at deploy time.
|
|
19
|
+
*/
|
|
20
|
+
export function renderTracesWorkerScript() {
|
|
21
|
+
return `// GENERATED by agents-cli agents traces — do not edit; edit
|
|
22
|
+
// src/lib/traces/worker-template.ts and re-provision.
|
|
23
|
+
export const hooks = {
|
|
24
|
+
verifyPhoenixToken: defaultVerifyPhoenixToken,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
async fetch(request, env) {
|
|
29
|
+
const url = new URL(request.url);
|
|
30
|
+
const path = decodeURIComponent(url.pathname.replace(/^\\/+/, ''));
|
|
31
|
+
|
|
32
|
+
if (!path) {
|
|
33
|
+
return new Response(
|
|
34
|
+
'agents traces — PUT /<userId>/<device>/index.json or sessions/<id>.json to upload; GET the same path to read. All routes require a Phoenix bearer.',
|
|
35
|
+
{ status: 200, headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'private, no-store' } },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (request.method === 'PUT') {
|
|
40
|
+
return handlePut(request, env, path);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
44
|
+
return handleGet(request, env, path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return json({ error: 'method not allowed' }, 405);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
async function handlePut(request, env, path) {
|
|
52
|
+
const auth = await authorizeWrite(request, env);
|
|
53
|
+
if (auth.error) return auth.error;
|
|
54
|
+
|
|
55
|
+
const segments = path.split('/').filter(Boolean);
|
|
56
|
+
const expected = auth.owner;
|
|
57
|
+
if (!expected || segments[0] !== expected) {
|
|
58
|
+
return json({ error: 'namespace mismatch', owner: expected }, 403);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const contentType = request.headers.get('content-type') || 'application/json; charset=utf-8';
|
|
62
|
+
await env.BUCKET.put(path, request.body, {
|
|
63
|
+
httpMetadata: { contentType },
|
|
64
|
+
customMetadata: {
|
|
65
|
+
owner: auth.owner,
|
|
66
|
+
uploadedAt: new Date().toISOString(),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const headers = new Headers();
|
|
71
|
+
headers.set('cache-control', 'private, no-store');
|
|
72
|
+
return new Response(JSON.stringify({ ok: true, key: path }), {
|
|
73
|
+
status: 200,
|
|
74
|
+
headers,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function handleGet(request, env, path) {
|
|
79
|
+
const auth = await authorizeRead(request, env, path);
|
|
80
|
+
if (auth.error) return auth.error;
|
|
81
|
+
|
|
82
|
+
const object = await env.BUCKET.get(path);
|
|
83
|
+
if (!object) return json({ error: 'not found' }, 404);
|
|
84
|
+
|
|
85
|
+
const headers = new Headers(object.httpMetadata ?? {});
|
|
86
|
+
headers.set('cache-control', 'private, no-store');
|
|
87
|
+
return new Response(object.body, { headers });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Require a Phoenix bearer that owns the path prefix.
|
|
91
|
+
async function authorizeRead(request, env, path) {
|
|
92
|
+
const claims = await hooks.verifyPhoenixToken(request, env);
|
|
93
|
+
if (!claims) return { error: json({ error: 'unauthorized' }, 401) };
|
|
94
|
+
const ownerId = path.split('/').filter(Boolean)[0] || '';
|
|
95
|
+
if (!ownerId || ownerId !== claims.userId) {
|
|
96
|
+
return { error: json({ error: 'forbidden' }, 403) };
|
|
97
|
+
}
|
|
98
|
+
return { claims };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Two legitimate principals for writes: static WRITE_TOKEN first (BYO),
|
|
102
|
+
// otherwise Phoenix bearer. 401 when neither authenticates.
|
|
103
|
+
async function authorizeWrite(request, env) {
|
|
104
|
+
const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
|
|
105
|
+
if (!presented) return { error: json({ error: 'unauthorized' }, 401) };
|
|
106
|
+
if (env.WRITE_TOKEN && safeEqual(presented, env.WRITE_TOKEN)) {
|
|
107
|
+
return { kind: 'byo', owner: env.TRACES_NAMESPACE || 'byo' };
|
|
108
|
+
}
|
|
109
|
+
const claims = await hooks.verifyPhoenixToken(request, env);
|
|
110
|
+
if (claims && typeof claims.userId === 'string' && claims.userId) {
|
|
111
|
+
return { kind: 'phoenix', owner: claims.userId };
|
|
112
|
+
}
|
|
113
|
+
return { error: json({ error: 'unauthorized' }, 401) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function defaultVerifyPhoenixToken(request, env) {
|
|
117
|
+
const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
|
|
118
|
+
if (!presented) return null;
|
|
119
|
+
const base = typeof env.PHOENIX_ID_BASE === 'string' ? env.PHOENIX_ID_BASE.replace(/\\/+$/, '') : '';
|
|
120
|
+
if (!base) return null;
|
|
121
|
+
let res;
|
|
122
|
+
try {
|
|
123
|
+
res = await fetch(base + '/api/v1/auth/me', {
|
|
124
|
+
method: 'GET',
|
|
125
|
+
headers: { Authorization: 'Bearer ' + presented },
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (!res.ok) return null;
|
|
131
|
+
let body;
|
|
132
|
+
try {
|
|
133
|
+
body = await res.json();
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (!body || typeof body.userId !== 'string' || !body.userId) return null;
|
|
138
|
+
return { userId: body.userId, email: typeof body.email === 'string' ? body.email : '' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function json(body, status = 200) {
|
|
142
|
+
return new Response(JSON.stringify(body), {
|
|
143
|
+
status,
|
|
144
|
+
headers: {
|
|
145
|
+
'content-type': 'application/json; charset=utf-8',
|
|
146
|
+
'cache-control': 'private, no-store',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Constant-time comparison so timing attacks can't probe a static WRITE_TOKEN.
|
|
152
|
+
function safeEqual(a, b) {
|
|
153
|
+
if (a.length !== b.length) return false;
|
|
154
|
+
let diff = 0;
|
|
155
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
156
|
+
return diff === 0;
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -29,6 +29,20 @@ export declare const RUN_AUTO_KEYWORD = "auto";
|
|
|
29
29
|
* process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
|
|
30
30
|
*/
|
|
31
31
|
export declare const RUN_AUTO_HOST_RESOLVED_ENV = "AGENTS_RUN_AUTO_HOST_RESOLVED";
|
|
32
|
+
/**
|
|
33
|
+
* Env var a host dispatcher exports into the remote SHELL when it opens an
|
|
34
|
+
* INTERACTIVE `--device` run: tells the remote CLI that this agent's stdio is a
|
|
35
|
+
* network link, so the run must be detached (tmux-wrapped) to outlive it.
|
|
36
|
+
*
|
|
37
|
+
* Rides the same shell-export prelude as {@link RUN_AUTO_HOST_RESOLVED_ENV},
|
|
38
|
+
* and for the same reason — exec.ts `resolveTmuxWrap` reads the remote CLI's own
|
|
39
|
+
* `process.env`, which `--env` flags (they reach only the spawned AGENT) never
|
|
40
|
+
* touch.
|
|
41
|
+
*
|
|
42
|
+
* Set ONLY on the interactive path. A headless `--device` dispatch is already
|
|
43
|
+
* detached with setsid by `launchDetached`, so it neither needs nor gets this.
|
|
44
|
+
*/
|
|
45
|
+
export declare const REMOTE_INTERACTIVE_ENV = "AGENTS_REMOTE_INTERACTIVE";
|
|
32
46
|
/** Per-agent run strategy config. */
|
|
33
47
|
export interface AgentRunConfig {
|
|
34
48
|
strategy?: RunStrategy;
|
|
@@ -958,30 +972,15 @@ export interface Meta {
|
|
|
958
972
|
*/
|
|
959
973
|
hooks?: Record<string, ManifestHook>;
|
|
960
974
|
/**
|
|
961
|
-
* Browser
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*/
|
|
965
|
-
browser?: Record<string, BrowserProfileConfig>;
|
|
966
|
-
/**
|
|
967
|
-
* Machine-local browser profiles — written to `config:`-adjacent
|
|
968
|
-
* `browser:` in `~/.agents/devices/<machine>/agents.yaml`, never the synced
|
|
969
|
-
* file.
|
|
970
|
-
*
|
|
971
|
-
* The auto-detected `default` profile lives here because it is inherently
|
|
972
|
-
* machine-specific: it carries an absolute `binary:` path and a port chosen by
|
|
973
|
-
* scanning THIS machine. Sharing it fleet-wide meant a macOS box wrote
|
|
974
|
-
* `/Applications/...`, a Linux box found it unlaunchable and rewrote
|
|
975
|
-
* `/usr/bin/chromium-browser`, and the two flipped the tracked agents.yaml back
|
|
976
|
-
* and forth on every `agents browser start` — the single largest source of
|
|
977
|
-
* churn on that file. User-created named profiles stay central and still sync.
|
|
975
|
+
* Browser profiles declared by this machine. Written as `browser:` in
|
|
976
|
+
* `~/.agents/devices/<machine>/agents.yaml`. The fleet registry is the union
|
|
977
|
+
* of every device file; central `agents.yaml` is not a browser-profile store.
|
|
978
978
|
*/
|
|
979
979
|
deviceBrowser?: Record<string, BrowserProfileConfig>;
|
|
980
980
|
/**
|
|
981
981
|
* User-scope config block (`config:` in central agents.yaml). Holds the
|
|
982
982
|
* user-scope keys from the device-config registry (`lib/device-config.ts`) —
|
|
983
|
-
* currently `interactiveHost
|
|
984
|
-
* `agents repo push/pull`.
|
|
983
|
+
* currently `interactiveHost`. Syncs fleet-wide via `agents repo push/pull`.
|
|
985
984
|
* Device-scope keys live in the per-device doc
|
|
986
985
|
* `devices/<name>/agents.yaml` `config:` block, layered over the fleet-wide
|
|
987
986
|
* defaults in {@link Meta.fleet} (`fleet.defaults.config`).
|
package/dist/lib/types.js
CHANGED
|
@@ -22,6 +22,20 @@ export const RUN_AUTO_KEYWORD = 'auto';
|
|
|
22
22
|
* process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
|
|
23
23
|
*/
|
|
24
24
|
export const RUN_AUTO_HOST_RESOLVED_ENV = 'AGENTS_RUN_AUTO_HOST_RESOLVED';
|
|
25
|
+
/**
|
|
26
|
+
* Env var a host dispatcher exports into the remote SHELL when it opens an
|
|
27
|
+
* INTERACTIVE `--device` run: tells the remote CLI that this agent's stdio is a
|
|
28
|
+
* network link, so the run must be detached (tmux-wrapped) to outlive it.
|
|
29
|
+
*
|
|
30
|
+
* Rides the same shell-export prelude as {@link RUN_AUTO_HOST_RESOLVED_ENV},
|
|
31
|
+
* and for the same reason — exec.ts `resolveTmuxWrap` reads the remote CLI's own
|
|
32
|
+
* `process.env`, which `--env` flags (they reach only the spawned AGENT) never
|
|
33
|
+
* touch.
|
|
34
|
+
*
|
|
35
|
+
* Set ONLY on the interactive path. A headless `--device` dispatch is already
|
|
36
|
+
* detached with setsid by `launchDetached`, so it neither needs nor gets this.
|
|
37
|
+
*/
|
|
38
|
+
export const REMOTE_INTERACTIVE_ENV = 'AGENTS_REMOTE_INTERACTIVE';
|
|
25
39
|
/** Every canonical mode in declaration order. Useful for iteration / validation. */
|
|
26
40
|
export const ALL_MODES = ['plan', 'edit', 'auto', 'skip'];
|
|
27
41
|
/** Canonical system repo cloned into ~/.agents/.system/. */
|