@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
package/dist/lib/exec.js
CHANGED
|
@@ -8,7 +8,7 @@ import { spawn } from 'child_process';
|
|
|
8
8
|
import { randomUUID } from 'crypto';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import { ALL_MODES } from './types.js';
|
|
11
|
+
import { ALL_MODES, REMOTE_INTERACTIVE_ENV } from './types.js';
|
|
12
12
|
import { AGENTS, findInPath } from './agents.js';
|
|
13
13
|
import { parseTimeout } from './scheduling/routines.js';
|
|
14
14
|
import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled, listInstalledVersions, resolveVersion } from './installations/versions.js';
|
|
@@ -1176,36 +1176,47 @@ export function isPaneKnownAliveFromQueryResult(code, stdout) {
|
|
|
1176
1176
|
* Decide whether to run an interactive agent INSIDE a detached tmux session on
|
|
1177
1177
|
* the shared socket (then attach the current TTY) instead of a bare spawn.
|
|
1178
1178
|
*
|
|
1179
|
-
*
|
|
1180
|
-
* `agents sessions --active` can tell co-located agents apart, and lets `agents
|
|
1181
|
-
* focus` re-attach a live session without forking it. Pure so the gate is unit-
|
|
1182
|
-
* tested independently of the (side-effecting) spawn.
|
|
1179
|
+
* Wrapping serves two independent purposes, and they are gated differently:
|
|
1183
1180
|
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1181
|
+
* - **Addressability** (`configEnabled`): a unique `%pane` handle so
|
|
1182
|
+
* `agents sessions --active` can tell co-located agents apart and `agents
|
|
1183
|
+
* focus` re-attaches without forking. That is a local preference — the
|
|
1184
|
+
* operator turns it on once tmux's mouse/clipboard/scrollback suits them.
|
|
1185
|
+
* - **Durability** (`remoteDispatch`): a run that arrived over SSH must outlive
|
|
1186
|
+
* the SSH client, because a bare remote spawn dies of SIGHUP the moment the
|
|
1187
|
+
* link blinks. That is not a preference, so it does not consult
|
|
1188
|
+
* `configEnabled` (RUSH-3125 — see {@link TmuxWrapContext.remoteDispatch}).
|
|
1189
|
+
*
|
|
1190
|
+
* The per-run opt-outs bind BOTH: `--raw` / `--no-tmux` / `AGENTS_NO_TMUX=1` are
|
|
1191
|
+
* explicit "I want the bare process" requests, and honouring them over the
|
|
1192
|
+
* durability rule keeps one escape hatch that always works.
|
|
1193
|
+
*
|
|
1194
|
+
* Pure, so the gate is unit-tested independently of the (side-effecting) spawn.
|
|
1192
1195
|
*/
|
|
1193
|
-
export function
|
|
1196
|
+
export function resolveTmuxWrap(ctx) {
|
|
1197
|
+
// A headless `-p` run has no TTY to attach, Windows has no tmux path, and
|
|
1198
|
+
// nesting tmux-in-tmux is pointless — none of these can wrap, and none of them
|
|
1199
|
+
// is a remote interactive agent whose life depends on it.
|
|
1194
1200
|
if (!ctx.interactive)
|
|
1195
|
-
return
|
|
1201
|
+
return { kind: 'bare' };
|
|
1196
1202
|
if (ctx.platform === 'win32')
|
|
1197
|
-
return
|
|
1203
|
+
return { kind: 'bare' };
|
|
1198
1204
|
if (ctx.inTmux)
|
|
1199
|
-
return
|
|
1205
|
+
return { kind: 'bare' };
|
|
1206
|
+
// Explicit opt-outs win over the durability rule: `--raw` exists precisely to
|
|
1207
|
+
// get the unwrapped process, and a flag that silently stopped working on a
|
|
1208
|
+
// remote box would be worse than an undurable run the user asked for.
|
|
1200
1209
|
if (ctx.raw)
|
|
1201
|
-
return
|
|
1210
|
+
return { kind: 'bare' };
|
|
1202
1211
|
if (ctx.noTmuxEnv)
|
|
1203
|
-
return
|
|
1204
|
-
if (!ctx.configEnabled)
|
|
1205
|
-
return
|
|
1212
|
+
return { kind: 'bare' };
|
|
1213
|
+
if (!ctx.configEnabled && !ctx.remoteDispatch)
|
|
1214
|
+
return { kind: 'bare' };
|
|
1215
|
+
// Fail loud rather than launch a remote agent that a blink would kill: the
|
|
1216
|
+
// caller refuses the run instead of starting work that cannot be recovered.
|
|
1206
1217
|
if (!ctx.tmuxAvailable)
|
|
1207
|
-
return
|
|
1208
|
-
return
|
|
1218
|
+
return ctx.remoteDispatch ? { kind: 'undurable' } : { kind: 'bare' };
|
|
1219
|
+
return { kind: 'wrap' };
|
|
1209
1220
|
}
|
|
1210
1221
|
/**
|
|
1211
1222
|
* Build the shell command that runs an agent inside a tmux pane with the exact
|
|
@@ -1225,6 +1236,29 @@ export function shouldWrapInTmux(ctx) {
|
|
|
1225
1236
|
* into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
|
|
1226
1237
|
* real values; the stored/informational copy uses the redacted form (RUSH-1758).
|
|
1227
1238
|
*/
|
|
1239
|
+
/**
|
|
1240
|
+
* True when `options.sessionId` is an id the HARNESS actually received, and so a
|
|
1241
|
+
* real, resumable handle — rather than one the launcher generated for its own
|
|
1242
|
+
* bookkeeping and the agent never saw.
|
|
1243
|
+
*
|
|
1244
|
+
* Mirrors exactly the two branches in buildExecCommand that put the id on the
|
|
1245
|
+
* command line: a native resume (any harness with a `resume` spec) and claude's
|
|
1246
|
+
* create-with-`--session-id`. Everything else — `agents run codex --session-id X`
|
|
1247
|
+
* without `--resume`, or a fresh non-claude launch whose real id only shows up
|
|
1248
|
+
* later via the SessionStart hook — leaves the agent with an id of its own
|
|
1249
|
+
* choosing, so `options.sessionId` must not be published as if it were that id.
|
|
1250
|
+
*
|
|
1251
|
+
* Keep in lockstep with buildExecCommand's resume/create branches: a harness
|
|
1252
|
+
* that gains a forced-session-id flag must be reflected here in the SAME change,
|
|
1253
|
+
* or this reports a genuine id as fabricated.
|
|
1254
|
+
*/
|
|
1255
|
+
export function isHarnessKnownSessionId(agent, sessionId, resume) {
|
|
1256
|
+
if (!sessionId)
|
|
1257
|
+
return false;
|
|
1258
|
+
if (resume && AGENT_COMMANDS[agent]?.resume)
|
|
1259
|
+
return true;
|
|
1260
|
+
return agent === 'claude';
|
|
1261
|
+
}
|
|
1228
1262
|
export function buildTmuxAgentCommand(executable, args, env, opts = {}) {
|
|
1229
1263
|
const agentCmd = [executable, ...args].map(shellQuote).join(' ');
|
|
1230
1264
|
// envFile: source the values instead of inlining them, so no VALUE ever lands
|
|
@@ -1430,8 +1464,13 @@ async function runInTmux(options, executable, args) {
|
|
|
1430
1464
|
const cmd = buildTmuxAgentCommand(executable, args, execEnv, { envFile });
|
|
1431
1465
|
const metaCmd = buildTmuxAgentCommand(executable, args, execEnv, { redactEnvValues: true });
|
|
1432
1466
|
const labels = { agent: options.agent };
|
|
1433
|
-
|
|
1467
|
+
// Only publish an id the harness actually received. createSession turns this
|
|
1468
|
+
// label into the `@ag_session_id` tmux option that status bars read, and a
|
|
1469
|
+
// launcher-internal id displayed there looks exactly like a resumable one
|
|
1470
|
+
// while `ag focus <id>` rejects it.
|
|
1471
|
+
if (isHarnessKnownSessionId(options.agent, options.sessionId, options.resume)) {
|
|
1434
1472
|
labels.sessionId = options.sessionId;
|
|
1473
|
+
}
|
|
1435
1474
|
// Only a launched pane sources-and-unlinks the env file. If createSession
|
|
1436
1475
|
// throws, the pane never runs, so the resolved secrets (incl. the master
|
|
1437
1476
|
// passphrase) would linger on disk — remove it on that failure path
|
|
@@ -1616,19 +1655,36 @@ async function spawnAgent(options) {
|
|
|
1616
1655
|
command: executable,
|
|
1617
1656
|
args: redactArgs(args.slice(0, 10)),
|
|
1618
1657
|
});
|
|
1619
|
-
// Interactive spawn-wrap:
|
|
1620
|
-
//
|
|
1621
|
-
//
|
|
1622
|
-
|
|
1658
|
+
// Interactive spawn-wrap: run the agent INSIDE a shared-socket tmux session
|
|
1659
|
+
// (then attach this TTY) so it gets a unique, addressable %pane — and, when
|
|
1660
|
+
// the run arrived over `--device`, so it survives the SSH link that carries
|
|
1661
|
+
// it. Every failed guard keeps the bare spawn below.
|
|
1662
|
+
const tmuxWrap = resolveTmuxWrap({
|
|
1623
1663
|
interactive,
|
|
1624
1664
|
platform: process.platform,
|
|
1625
1665
|
inTmux: !!process.env.TMUX,
|
|
1626
1666
|
raw: options.raw === true,
|
|
1627
1667
|
noTmuxEnv: process.env.AGENTS_NO_TMUX === '1',
|
|
1628
1668
|
configEnabled: isTmuxEnabled(),
|
|
1669
|
+
remoteDispatch: process.env[REMOTE_INTERACTIVE_ENV] === '1',
|
|
1629
1670
|
tmuxAvailable: isTmuxInstalled(),
|
|
1630
1671
|
});
|
|
1631
|
-
if (
|
|
1672
|
+
if (tmuxWrap.kind === 'undurable') {
|
|
1673
|
+
// Refuse rather than start work a blink would destroy. This is the ONLY
|
|
1674
|
+
// check — `ensureHostReady` gates a `--device` dispatch on the peer being
|
|
1675
|
+
// reachable and able to run the pinned agent, not on it having tmux, so
|
|
1676
|
+
// there is no launcher-side probe ahead of this one. Failing here still
|
|
1677
|
+
// fails before the agent starts, which is what matters; a pre-dispatch
|
|
1678
|
+
// probe would only move the message earlier, at the cost of a round trip on
|
|
1679
|
+
// every launch.
|
|
1680
|
+
const msg = `agents: ${machineId()} has no tmux, so a --device run here could not survive a dropped connection.\n`
|
|
1681
|
+
+ ` install it: (apt|dnf|brew) install tmux\n`
|
|
1682
|
+
+ ` or accept the risk: agents run … --device ${machineId()} --raw\n`;
|
|
1683
|
+
process.stderr.write(`\x1b[31m${msg}\x1b[0m`);
|
|
1684
|
+
timer.end({ exitCode: 1, status: 'failed', error: 'remote interactive run has no tmux for durability' });
|
|
1685
|
+
return { exitCode: 1, stdout: '', stderr: msg };
|
|
1686
|
+
}
|
|
1687
|
+
if (tmuxWrap.kind === 'wrap') {
|
|
1632
1688
|
timer.mark('startup');
|
|
1633
1689
|
try {
|
|
1634
1690
|
const result = await runInTmux(options, executable, args);
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
1
|
import { stripForeignConfigDir } from '../adapter.js';
|
|
3
2
|
export const cursorAdapter = {
|
|
4
3
|
id: 'cursor',
|
|
5
|
-
// Cursor
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// authenticates from its own token, isolated per run — no global ~/.cursor
|
|
11
|
-
// symlink swap, so concurrent runs on different accounts never clobber one
|
|
12
|
-
// another. cli-config.json (HOME-relative) has no override and stays on the
|
|
13
|
-
// shared home; only the token is per-account, which is what gates the login.
|
|
4
|
+
// Cursor defaults to one machine-global OS-keychain login on macOS, which
|
|
5
|
+
// ignores XDG_CONFIG_HOME. Select the file credential store; Cursor writes
|
|
6
|
+
// that store to HOME-relative ~/.cursor/auth.json, and buildExecEnv already
|
|
7
|
+
// swaps HOME to the selected version home. Existing keychain credentials
|
|
8
|
+
// remain untouched.
|
|
14
9
|
applyExecConfigEnv(result, ctx) {
|
|
15
10
|
if (ctx.versionHome) {
|
|
16
|
-
result.
|
|
11
|
+
result.AGENTS_REAL_HOME ||= result.HOME;
|
|
12
|
+
result.HOME = ctx.versionHome;
|
|
13
|
+
result.AGENT_CLI_CREDENTIAL_STORE = 'file';
|
|
17
14
|
}
|
|
18
15
|
stripForeignConfigDir(result);
|
|
19
16
|
},
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared on-demand download + verification for the macOS native helper `.app`
|
|
3
|
+
* bundles that ship as signed + notarized GitHub release assets on each helper's
|
|
4
|
+
* OWN tag (`ComputerHelper.app`, `MenubarHelper.app`, `Agents CLI.app`) — see
|
|
5
|
+
* `helper-versions.ts` for why they are no longer keyed to the CLI's tag.
|
|
6
|
+
*
|
|
7
|
+
* A `.app` is a directory, so each asset is a zip (`ditto -c -k --keepParent`);
|
|
8
|
+
* a fresh `npm i -g` machine with no bundled copy fetches the asset for the
|
|
9
|
+
* pinned helper version, verifies its sha256 against the published `.sha256`,
|
|
10
|
+
* then verifies the code signature (Developer ID Team + notarization — and, for
|
|
11
|
+
* the menu-bar helper, its designated requirement) before it is ever installed.
|
|
12
|
+
*
|
|
13
|
+
* `computer/download.ts` (ComputerHelper) and `menubar/download-menubar.ts`
|
|
14
|
+
* (MenubarHelper) are thin per-helper wrappers over the primitives here — one
|
|
15
|
+
* download+verify machinery, two specs — so a fix to the verify/download logic
|
|
16
|
+
* lands for both helpers at once.
|
|
17
|
+
*/
|
|
18
|
+
import { type HelperName } from './helper-versions.js';
|
|
19
|
+
/** GitHub repo whose `v<version>` releases carry the helper assets. */
|
|
20
|
+
export declare const HELPER_RELEASE_REPO = "phnx-labs/agents-cli";
|
|
21
|
+
/** Apple Developer ID Team every helper must be signed by ("Developer ID
|
|
22
|
+
* Application: Muqit Nawaz"). Defense in depth on top of `spctl` notarization. */
|
|
23
|
+
export declare const EXPECTED_TEAM_ID = "2HTP252L87";
|
|
24
|
+
/** One helper's identity + verification policy — everything that differs between
|
|
25
|
+
* the computer helper and the menu-bar helper. */
|
|
26
|
+
export interface HelperSpec {
|
|
27
|
+
/**
|
|
28
|
+
* Which helper this is — the key into the independent release train
|
|
29
|
+
* (`helper-versions.ts`). This is what makes the download URL a function of
|
|
30
|
+
* the HELPER's version rather than the CLI's.
|
|
31
|
+
*/
|
|
32
|
+
helper: HelperName;
|
|
33
|
+
/** The zipped `.app` release asset name, e.g. `ComputerHelper.app.zip`. */
|
|
34
|
+
assetName: string;
|
|
35
|
+
/** The bundle directory name once extracted, e.g. `ComputerHelper.app`. */
|
|
36
|
+
appName: string;
|
|
37
|
+
/** Cache dir components under `getCacheDir()`, e.g. `['computer', 'mac-helper']`. */
|
|
38
|
+
cacheSubdir: string[];
|
|
39
|
+
/** Developer ID Team the bundle must be signed by. */
|
|
40
|
+
expectedTeamId: string;
|
|
41
|
+
/**
|
|
42
|
+
* When set, the bundle's designated requirement MUST pin this
|
|
43
|
+
* CFBundleIdentifier in addition to `expectedTeamId`. Used by the menu-bar
|
|
44
|
+
* helper: macOS keys the Accessibility (TCC) grant to this requirement, so a
|
|
45
|
+
* substituted bundle whose DR does not pin (id, team) would silently revoke
|
|
46
|
+
* the user's grant on the next paste. Left undefined for the computer helper,
|
|
47
|
+
* which has no such upgrade-stable grant to protect (matching its behavior
|
|
48
|
+
* before this shared primitive existed).
|
|
49
|
+
*/
|
|
50
|
+
expectedBundleId?: string;
|
|
51
|
+
/** Local-build command named in the "asset missing" error, for a repo checkout. */
|
|
52
|
+
localBuildHint: string;
|
|
53
|
+
}
|
|
54
|
+
/** Cache dir for a downloaded helper, one subdir per release tag. */
|
|
55
|
+
export declare function helperCacheDir(spec: HelperSpec, version: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Release-asset URLs for one helper build, at that HELPER's own tag.
|
|
58
|
+
*
|
|
59
|
+
* The tag is `<helper>/v<version>` (e.g. `menubar/v1.0.0`), never the CLI's
|
|
60
|
+
* `v<cliVersion>`. Keying these to the CLI tag is what forced every CLI release
|
|
61
|
+
* to re-stage every helper asset, and simultaneously made a helper fix
|
|
62
|
+
* unreachable without a CLI release — see `helper-versions.ts` for the full why.
|
|
63
|
+
*
|
|
64
|
+
* `version` is a HELPER version. Passing a CLI version here yields a tag that
|
|
65
|
+
* does not exist; callers default it from {@link helperFloor}.
|
|
66
|
+
*/
|
|
67
|
+
export declare function helperAssetUrls(spec: HelperSpec, version: string): {
|
|
68
|
+
zip: string;
|
|
69
|
+
sha256: string;
|
|
70
|
+
};
|
|
71
|
+
/** Extract `TeamIdentifier=XXXX` from `codesign -dv --verbose=4` output (which is
|
|
72
|
+
* emitted on stderr). Returns null when absent (ad-hoc / unsigned). */
|
|
73
|
+
export declare function parseTeamId(codesignInfo: string): string | null;
|
|
74
|
+
/** Read the bundle's designated requirement string (`codesign -d --requirements -`).
|
|
75
|
+
* codesign writes it to stdout, but capture both streams so a diagnostic on
|
|
76
|
+
* stderr can never make the pin-check read empty and falsely pass. */
|
|
77
|
+
export declare function readDesignatedRequirement(appPath: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Pure predicate (no I/O) behind the DR pin: does the requirement string pin
|
|
80
|
+
* BOTH the expected CFBundleIdentifier and the Developer ID Team? Returns null
|
|
81
|
+
* when it does, else an actionable error message naming the missing pin. Kept
|
|
82
|
+
* pure so the truth table is unit-testable without a signed bundle or codesign.
|
|
83
|
+
* Mirrors the release-time gate in `scripts/verify-menubar-helper.sh` (identifier
|
|
84
|
+
* + team substring check against the same requirement string).
|
|
85
|
+
*/
|
|
86
|
+
export declare function checkDesignatedRequirement(req: string, bundleId: string, teamId: string): string | null;
|
|
87
|
+
/**
|
|
88
|
+
* Verify the bundle's designated requirement pins the expected CFBundleIdentifier
|
|
89
|
+
* AND Developer ID Team. macOS re-validates each new version against this stored
|
|
90
|
+
* requirement to keep an Accessibility grant alive across upgrades; a downloaded
|
|
91
|
+
* bundle whose DR drops either pin would silently revoke that grant on the next
|
|
92
|
+
* paste, so refuse it.
|
|
93
|
+
*/
|
|
94
|
+
export declare function verifyDesignatedRequirement(appPath: string, bundleId: string, teamId: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* Verify a helper `.app` bundle is intact, signed by the expected Developer ID
|
|
97
|
+
* Team, (for the menu-bar helper) pinned by its designated requirement, and
|
|
98
|
+
* notarized (Gatekeeper-accepted). Throws with an actionable message on any
|
|
99
|
+
* failure — a downloaded bundle is never trusted without this.
|
|
100
|
+
*/
|
|
101
|
+
export declare function verifyHelperApp(appPath: string, spec: HelperSpec): void;
|
|
102
|
+
/**
|
|
103
|
+
* Download a helper release asset for `version`, verify sha256, extract the
|
|
104
|
+
* `.app`, and verify its signature (+ DR pin when the spec requires one).
|
|
105
|
+
* Returns the path to the extracted bundle. A missing asset is a hard error
|
|
106
|
+
* naming the exact tag — never a silent fallback to another release.
|
|
107
|
+
*/
|
|
108
|
+
export declare function downloadHelperApp(spec: HelperSpec, version: string): Promise<string>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared on-demand download + verification for the macOS native helper `.app`
|
|
3
|
+
* bundles that ship as signed + notarized GitHub release assets on each helper's
|
|
4
|
+
* OWN tag (`ComputerHelper.app`, `MenubarHelper.app`, `Agents CLI.app`) — see
|
|
5
|
+
* `helper-versions.ts` for why they are no longer keyed to the CLI's tag.
|
|
6
|
+
*
|
|
7
|
+
* A `.app` is a directory, so each asset is a zip (`ditto -c -k --keepParent`);
|
|
8
|
+
* a fresh `npm i -g` machine with no bundled copy fetches the asset for the
|
|
9
|
+
* pinned helper version, verifies its sha256 against the published `.sha256`,
|
|
10
|
+
* then verifies the code signature (Developer ID Team + notarization — and, for
|
|
11
|
+
* the menu-bar helper, its designated requirement) before it is ever installed.
|
|
12
|
+
*
|
|
13
|
+
* `computer/download.ts` (ComputerHelper) and `menubar/download-menubar.ts`
|
|
14
|
+
* (MenubarHelper) are thin per-helper wrappers over the primitives here — one
|
|
15
|
+
* download+verify machinery, two specs — so a fix to the verify/download logic
|
|
16
|
+
* lands for both helpers at once.
|
|
17
|
+
*/
|
|
18
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
19
|
+
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import { Readable } from 'node:stream';
|
|
22
|
+
import { pipeline } from 'node:stream/promises';
|
|
23
|
+
import { getCacheDir } from './state.js';
|
|
24
|
+
import { parseSha256Asset, sha256File } from './sha256-asset.js';
|
|
25
|
+
import { helperTag } from './helper-versions.js';
|
|
26
|
+
/** GitHub repo whose `v<version>` releases carry the helper assets. */
|
|
27
|
+
export const HELPER_RELEASE_REPO = 'phnx-labs/agents-cli';
|
|
28
|
+
/** Apple Developer ID Team every helper must be signed by ("Developer ID
|
|
29
|
+
* Application: Muqit Nawaz"). Defense in depth on top of `spctl` notarization. */
|
|
30
|
+
export const EXPECTED_TEAM_ID = '2HTP252L87';
|
|
31
|
+
/** Cache dir for a downloaded helper, one subdir per release tag. */
|
|
32
|
+
export function helperCacheDir(spec, version) {
|
|
33
|
+
return path.join(getCacheDir(), ...spec.cacheSubdir, `v${version}`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Release-asset URLs for one helper build, at that HELPER's own tag.
|
|
37
|
+
*
|
|
38
|
+
* The tag is `<helper>/v<version>` (e.g. `menubar/v1.0.0`), never the CLI's
|
|
39
|
+
* `v<cliVersion>`. Keying these to the CLI tag is what forced every CLI release
|
|
40
|
+
* to re-stage every helper asset, and simultaneously made a helper fix
|
|
41
|
+
* unreachable without a CLI release — see `helper-versions.ts` for the full why.
|
|
42
|
+
*
|
|
43
|
+
* `version` is a HELPER version. Passing a CLI version here yields a tag that
|
|
44
|
+
* does not exist; callers default it from {@link helperFloor}.
|
|
45
|
+
*/
|
|
46
|
+
export function helperAssetUrls(spec, version) {
|
|
47
|
+
const base = `https://github.com/${HELPER_RELEASE_REPO}/releases/download/${helperTag(spec.helper, version)}`;
|
|
48
|
+
// Asset names must not contain spaces: GitHub rewrites a space to a dot on
|
|
49
|
+
// upload, so a spaced name is requested at a URL that 404s forever. Caught on
|
|
50
|
+
// `Agents CLI.app.zip`, which GitHub served as `Agents.CLI.app.zip`. Use an
|
|
51
|
+
// underscore -- GitHub preserves it verbatim.
|
|
52
|
+
if (spec.assetName.includes(' ')) {
|
|
53
|
+
throw new Error(`helper asset name ${JSON.stringify(spec.assetName)} contains a space; GitHub rewrites it to a dot on `
|
|
54
|
+
+ 'upload, so this URL would always 404. Use an underscore instead.');
|
|
55
|
+
}
|
|
56
|
+
return { zip: `${base}/${spec.assetName}`, sha256: `${base}/${spec.assetName}.sha256` };
|
|
57
|
+
}
|
|
58
|
+
/** Extract `TeamIdentifier=XXXX` from `codesign -dv --verbose=4` output (which is
|
|
59
|
+
* emitted on stderr). Returns null when absent (ad-hoc / unsigned). */
|
|
60
|
+
export function parseTeamId(codesignInfo) {
|
|
61
|
+
return codesignInfo.match(/TeamIdentifier=([A-Z0-9]+)/)?.[1] ?? null;
|
|
62
|
+
}
|
|
63
|
+
/** Read the bundle's designated requirement string (`codesign -d --requirements -`).
|
|
64
|
+
* codesign writes it to stdout, but capture both streams so a diagnostic on
|
|
65
|
+
* stderr can never make the pin-check read empty and falsely pass. */
|
|
66
|
+
export function readDesignatedRequirement(appPath) {
|
|
67
|
+
const r = spawnSync('/usr/bin/codesign', ['-d', '--requirements', '-', appPath], { encoding: 'utf8' });
|
|
68
|
+
return `${r.stdout ?? ''}${r.stderr ?? ''}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Pure predicate (no I/O) behind the DR pin: does the requirement string pin
|
|
72
|
+
* BOTH the expected CFBundleIdentifier and the Developer ID Team? Returns null
|
|
73
|
+
* when it does, else an actionable error message naming the missing pin. Kept
|
|
74
|
+
* pure so the truth table is unit-testable without a signed bundle or codesign.
|
|
75
|
+
* Mirrors the release-time gate in `scripts/verify-menubar-helper.sh` (identifier
|
|
76
|
+
* + team substring check against the same requirement string).
|
|
77
|
+
*/
|
|
78
|
+
export function checkDesignatedRequirement(req, bundleId, teamId) {
|
|
79
|
+
const trimmed = req.trim();
|
|
80
|
+
const shown = trimmed ? JSON.stringify(trimmed.slice(0, 200)) : '<none>';
|
|
81
|
+
if (!trimmed.includes(`identifier "${bundleId}"`)) {
|
|
82
|
+
return (`helper designated requirement does not pin bundle id "${bundleId}" (read: ${shown}). ` +
|
|
83
|
+
`Installing it would revoke the Accessibility grant. Refusing to install.`);
|
|
84
|
+
}
|
|
85
|
+
if (!trimmed.includes(teamId)) {
|
|
86
|
+
return (`helper designated requirement does not pin Developer ID Team ${teamId} (read: ${shown}). ` +
|
|
87
|
+
`Installing it would revoke the Accessibility grant. Refusing to install.`);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Verify the bundle's designated requirement pins the expected CFBundleIdentifier
|
|
93
|
+
* AND Developer ID Team. macOS re-validates each new version against this stored
|
|
94
|
+
* requirement to keep an Accessibility grant alive across upgrades; a downloaded
|
|
95
|
+
* bundle whose DR drops either pin would silently revoke that grant on the next
|
|
96
|
+
* paste, so refuse it.
|
|
97
|
+
*/
|
|
98
|
+
export function verifyDesignatedRequirement(appPath, bundleId, teamId) {
|
|
99
|
+
const err = checkDesignatedRequirement(readDesignatedRequirement(appPath), bundleId, teamId);
|
|
100
|
+
if (err)
|
|
101
|
+
throw new Error(err);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Verify a helper `.app` bundle is intact, signed by the expected Developer ID
|
|
105
|
+
* Team, (for the menu-bar helper) pinned by its designated requirement, and
|
|
106
|
+
* notarized (Gatekeeper-accepted). Throws with an actionable message on any
|
|
107
|
+
* failure — a downloaded bundle is never trusted without this.
|
|
108
|
+
*/
|
|
109
|
+
export function verifyHelperApp(appPath, spec) {
|
|
110
|
+
// 1. Structural + signature integrity.
|
|
111
|
+
try {
|
|
112
|
+
execFileSync('/usr/bin/codesign', ['--verify', '--deep', '--strict', appPath], { stdio: 'pipe' });
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
throw new Error(`code signature invalid for ${appPath}: ${e.message}`);
|
|
116
|
+
}
|
|
117
|
+
// 2. Team identity — must be our Developer ID, not some other valid signer.
|
|
118
|
+
// `codesign -dv` writes its details to STDERR even on success (exit 0), so we
|
|
119
|
+
// must read stderr, not stdout. spawnSync captures both streams regardless of
|
|
120
|
+
// exit code; execFileSync would return only (empty) stdout and the Team check
|
|
121
|
+
// would falsely reject every validly-signed helper.
|
|
122
|
+
const dv = spawnSync('/usr/bin/codesign', ['-dv', '--verbose=4', appPath], { encoding: 'utf8' });
|
|
123
|
+
const info = `${dv.stdout ?? ''}${dv.stderr ?? ''}`;
|
|
124
|
+
const team = parseTeamId(info);
|
|
125
|
+
if (team !== spec.expectedTeamId) {
|
|
126
|
+
throw new Error(`helper signed by unexpected Team (${team ?? 'none'}), expected ${spec.expectedTeamId}. Refusing to install.`);
|
|
127
|
+
}
|
|
128
|
+
// 3. Designated-requirement pin (menu-bar helper only) — before the Gatekeeper
|
|
129
|
+
// assessment so a DR mismatch fails with its specific, actionable message
|
|
130
|
+
// rather than a generic Gatekeeper rejection.
|
|
131
|
+
if (spec.expectedBundleId) {
|
|
132
|
+
verifyDesignatedRequirement(appPath, spec.expectedBundleId, spec.expectedTeamId);
|
|
133
|
+
}
|
|
134
|
+
// 4. Notarization / Gatekeeper — confirms Apple stapled a notarization ticket.
|
|
135
|
+
try {
|
|
136
|
+
execFileSync('/usr/sbin/spctl', ['--assess', '--type', 'execute', '--verbose', appPath], { stdio: 'pipe' });
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
throw new Error(`helper is not notarized / rejected by Gatekeeper: ${e.message}. Refusing to install.`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Download a helper release asset for `version`, verify sha256, extract the
|
|
144
|
+
* `.app`, and verify its signature (+ DR pin when the spec requires one).
|
|
145
|
+
* Returns the path to the extracted bundle. A missing asset is a hard error
|
|
146
|
+
* naming the exact tag — never a silent fallback to another release.
|
|
147
|
+
*/
|
|
148
|
+
export async function downloadHelperApp(spec, version) {
|
|
149
|
+
const dir = helperCacheDir(spec, version);
|
|
150
|
+
const cachedApp = path.join(dir, spec.appName);
|
|
151
|
+
if (fs.existsSync(cachedApp)) {
|
|
152
|
+
// Re-verify a cached bundle cheaply; a tampered cache must not be trusted.
|
|
153
|
+
verifyHelperApp(cachedApp, spec);
|
|
154
|
+
return cachedApp;
|
|
155
|
+
}
|
|
156
|
+
// Name the HELPER's tag, not `v${version}`: after the per-helper split that
|
|
157
|
+
// string was a tag that does not exist, so a 404 pointed the reader at the
|
|
158
|
+
// wrong place to look.
|
|
159
|
+
const tag = helperTag(spec.helper, version);
|
|
160
|
+
const { zip: zipUrl, sha256: shaUrl } = helperAssetUrls(spec, version);
|
|
161
|
+
const missing = (status, url) => new Error(`no ${spec.assetName} release asset for tag ${tag} (HTTP ${status} on ${url}). ` +
|
|
162
|
+
`The macOS helper ships as a GitHub release asset on its own helper tag; ` +
|
|
163
|
+
`from a repo checkout you can build it locally instead: ${spec.localBuildHint}`);
|
|
164
|
+
// Checksum first: it is tiny and 404s fast when the tag has no assets.
|
|
165
|
+
const shaRes = await fetch(shaUrl, { signal: AbortSignal.timeout(30_000) });
|
|
166
|
+
if (!shaRes.ok)
|
|
167
|
+
throw missing(shaRes.status, shaUrl);
|
|
168
|
+
const expected = parseSha256Asset(await shaRes.text());
|
|
169
|
+
console.error(`Downloading ${spec.assetName} ${tag} from GitHub releases...`);
|
|
170
|
+
const zipRes = await fetch(zipUrl, { signal: AbortSignal.timeout(15 * 60_000) });
|
|
171
|
+
if (!zipRes.ok || !zipRes.body)
|
|
172
|
+
throw missing(zipRes.status, zipUrl);
|
|
173
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
174
|
+
const partial = path.join(dir, `${spec.assetName}.download`);
|
|
175
|
+
try {
|
|
176
|
+
await pipeline(Readable.fromWeb(zipRes.body), fs.createWriteStream(partial));
|
|
177
|
+
const actual = await sha256File(partial);
|
|
178
|
+
if (actual !== expected) {
|
|
179
|
+
throw new Error(`sha256 mismatch for ${zipUrl}: expected ${expected}, got ${actual}`);
|
|
180
|
+
}
|
|
181
|
+
// Extract the zip (created with `ditto -c -k --keepParent`, so it contains
|
|
182
|
+
// <appName>/ at top level) into the version cache dir.
|
|
183
|
+
fs.rmSync(cachedApp, { recursive: true, force: true });
|
|
184
|
+
execFileSync('/usr/bin/ditto', ['-x', '-k', partial, dir], { stdio: 'pipe' });
|
|
185
|
+
if (!fs.existsSync(cachedApp)) {
|
|
186
|
+
throw new Error(`extracted asset did not contain ${spec.appName}`);
|
|
187
|
+
}
|
|
188
|
+
verifyHelperApp(cachedApp, spec);
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
fs.rmSync(partial, { force: true });
|
|
192
|
+
}
|
|
193
|
+
return cachedApp;
|
|
194
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which build of each native helper this CLI expects, and where its release
|
|
3
|
+
* lives — the module that decouples helper distribution from CLI releases.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `helperAssetUrls` used to build
|
|
6
|
+
* `releases/download/v${cliVersion}/<asset>`, keying every helper to the CLI's
|
|
7
|
+
* own tag. That is coupled in BOTH directions and each one hurts:
|
|
8
|
+
*
|
|
9
|
+
* - A CLI release had to re-stage every helper asset onto its new tag, or the
|
|
10
|
+
* download 404'd. That is what chained an ordinary release to a Mac.
|
|
11
|
+
* - A helper fix could not reach anyone without cutting a CLI release, because
|
|
12
|
+
* no URL existed that a shipped CLI would look at.
|
|
13
|
+
*
|
|
14
|
+
* Helpers now publish to their OWN tags (`<helper>/v<x.y.z>`) and this table
|
|
15
|
+
* records the FLOOR — the build this CLI was tested against. A floor, not an
|
|
16
|
+
* exact pin: a pin would fix the first problem and leave the second, since an
|
|
17
|
+
* improved helper would still need a CLI release to be reachable. The floor is
|
|
18
|
+
* what a channel manifest can later resolve *upward* from (newest >= floor),
|
|
19
|
+
* without another rewrite of the download path.
|
|
20
|
+
*
|
|
21
|
+
* The floor is also the offline answer: it names an IMMUTABLE tag that always
|
|
22
|
+
* exists, so a machine that cannot reach a manifest still has exactly one
|
|
23
|
+
* correct URL rather than a guess.
|
|
24
|
+
*
|
|
25
|
+
* Bumping an entry here is a deliberate act — it says "this CLI has been tested
|
|
26
|
+
* against that helper build". It is not a mirror of the CLI version and must
|
|
27
|
+
* never be derived from `getCliVersion()`.
|
|
28
|
+
*/
|
|
29
|
+
/** The helpers that have their own release train. */
|
|
30
|
+
export type HelperName = 'menubar' | 'keychain' | 'computer-mac' | 'computer-win';
|
|
31
|
+
/** One helper's release identity. */
|
|
32
|
+
export interface HelperRelease {
|
|
33
|
+
/** Tag prefix — the release is `<tagPrefix>/v<version>`. */
|
|
34
|
+
tagPrefix: string;
|
|
35
|
+
/**
|
|
36
|
+
* Lowest helper build this CLI is known to work with. Resolution may pick a
|
|
37
|
+
* NEWER build; it must never pick an older one.
|
|
38
|
+
*/
|
|
39
|
+
floor: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Floors, by helper.
|
|
43
|
+
*
|
|
44
|
+
* `menubar` and `keychain` start at 1.0.0 — the first builds published under
|
|
45
|
+
* their own tags rather than the CLI's. They are not "version 1 of the helper";
|
|
46
|
+
* they are version 1 of its independent release train.
|
|
47
|
+
*/
|
|
48
|
+
export declare const HELPER_RELEASES: Readonly<Record<HelperName, HelperRelease>>;
|
|
49
|
+
/** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
|
|
50
|
+
export declare function helperTag(helper: HelperName, version: string): string;
|
|
51
|
+
/** The floor build for one helper — the version used when nothing resolves higher. */
|
|
52
|
+
export declare function helperFloor(helper: HelperName): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which build of each native helper this CLI expects, and where its release
|
|
3
|
+
* lives — the module that decouples helper distribution from CLI releases.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. `helperAssetUrls` used to build
|
|
6
|
+
* `releases/download/v${cliVersion}/<asset>`, keying every helper to the CLI's
|
|
7
|
+
* own tag. That is coupled in BOTH directions and each one hurts:
|
|
8
|
+
*
|
|
9
|
+
* - A CLI release had to re-stage every helper asset onto its new tag, or the
|
|
10
|
+
* download 404'd. That is what chained an ordinary release to a Mac.
|
|
11
|
+
* - A helper fix could not reach anyone without cutting a CLI release, because
|
|
12
|
+
* no URL existed that a shipped CLI would look at.
|
|
13
|
+
*
|
|
14
|
+
* Helpers now publish to their OWN tags (`<helper>/v<x.y.z>`) and this table
|
|
15
|
+
* records the FLOOR — the build this CLI was tested against. A floor, not an
|
|
16
|
+
* exact pin: a pin would fix the first problem and leave the second, since an
|
|
17
|
+
* improved helper would still need a CLI release to be reachable. The floor is
|
|
18
|
+
* what a channel manifest can later resolve *upward* from (newest >= floor),
|
|
19
|
+
* without another rewrite of the download path.
|
|
20
|
+
*
|
|
21
|
+
* The floor is also the offline answer: it names an IMMUTABLE tag that always
|
|
22
|
+
* exists, so a machine that cannot reach a manifest still has exactly one
|
|
23
|
+
* correct URL rather than a guess.
|
|
24
|
+
*
|
|
25
|
+
* Bumping an entry here is a deliberate act — it says "this CLI has been tested
|
|
26
|
+
* against that helper build". It is not a mirror of the CLI version and must
|
|
27
|
+
* never be derived from `getCliVersion()`.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Floors, by helper.
|
|
31
|
+
*
|
|
32
|
+
* `menubar` and `keychain` start at 1.0.0 — the first builds published under
|
|
33
|
+
* their own tags rather than the CLI's. They are not "version 1 of the helper";
|
|
34
|
+
* they are version 1 of its independent release train.
|
|
35
|
+
*/
|
|
36
|
+
export const HELPER_RELEASES = {
|
|
37
|
+
menubar: { tagPrefix: 'menubar', floor: '1.0.0' },
|
|
38
|
+
keychain: { tagPrefix: 'keychain', floor: '1.0.0' },
|
|
39
|
+
'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
|
|
40
|
+
// The Windows helper is a bare .exe, not an .app bundle, so it does not share
|
|
41
|
+
// helper-download.ts's zip/codesign/notarize machinery -- but it has the same
|
|
42
|
+
// URL problem, so it shares the tag namespace and the floor.
|
|
43
|
+
'computer-win': { tagPrefix: 'computer-win', floor: '1.0.0' },
|
|
44
|
+
};
|
|
45
|
+
/** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
|
|
46
|
+
export function helperTag(helper, version) {
|
|
47
|
+
const release = HELPER_RELEASES[helper];
|
|
48
|
+
if (!release)
|
|
49
|
+
throw new Error(`unknown helper '${helper}' (want: ${Object.keys(HELPER_RELEASES).join(', ')})`);
|
|
50
|
+
return `${release.tagPrefix}/v${version}`;
|
|
51
|
+
}
|
|
52
|
+
/** The floor build for one helper — the version used when nothing resolves higher. */
|
|
53
|
+
export function helperFloor(helper) {
|
|
54
|
+
const release = HELPER_RELEASES[helper];
|
|
55
|
+
if (!release)
|
|
56
|
+
throw new Error(`unknown helper '${helper}' (want: ${Object.keys(HELPER_RELEASES).join(', ')})`);
|
|
57
|
+
return release.floor;
|
|
58
|
+
}
|
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -283,6 +283,17 @@ def should_fire():
|
|
|
283
283
|
if pm and pm not in allowed:
|
|
284
284
|
return False
|
|
285
285
|
|
|
286
|
+
v = m.get("permission_mode_not")
|
|
287
|
+
if v is not None:
|
|
288
|
+
denied = arr(v)
|
|
289
|
+
if denied:
|
|
290
|
+
# Negative form: naming the mode to SKIP keeps every unknown mode
|
|
291
|
+
# firing, so the failure direction is "ran unnecessarily", never
|
|
292
|
+
# "did not run". Absence of a mode fires, same as permission_mode.
|
|
293
|
+
pm = inp.get("permission_mode") or inp.get("permissionMode")
|
|
294
|
+
if pm and pm in denied:
|
|
295
|
+
return False
|
|
296
|
+
|
|
286
297
|
v = m.get("tool_args_match")
|
|
287
298
|
if v is not None:
|
|
288
299
|
ta = inp.get("tool_args")
|
|
@@ -3255,8 +3255,8 @@ function resolveSessionTrackerRoot() {
|
|
|
3255
3255
|
if (fs.existsSync(path.join(built, 'dist', 'hook.sh'))) {
|
|
3256
3256
|
return built;
|
|
3257
3257
|
}
|
|
3258
|
-
// Source layout:
|
|
3259
|
-
const source = path.resolve(here, '..', '..', '..', '..', '
|
|
3258
|
+
// Source layout: cli/src/lib/hooks/ -> repo root (4 up) -> packages/session-tracker
|
|
3259
|
+
const source = path.resolve(here, '..', '..', '..', '..', 'packages', 'session-tracker');
|
|
3260
3260
|
if (fs.existsSync(path.join(source, 'src', 'hook.sh'))) {
|
|
3261
3261
|
return source;
|
|
3262
3262
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* codex/grok/gemini `auth.json`-style files — so the box booted logged-in. Every
|
|
7
7
|
* one of those is a **rotating, interactive OAuth / session credential**, and
|
|
8
8
|
* copying it between devices is forbidden by the fleet-auth contract
|
|
9
|
-
* (`docs/specifications.md` SING-1b, `docs/
|
|
9
|
+
* (`docs/specifications.md` SING-1b, `docs/secrets.md`):
|
|
10
10
|
* a shared refresh token rotates server-side on the next refresh and invalidates
|
|
11
11
|
* every other copy — the "droid collapsed 10 boxes to 1 overnight" failure.
|
|
12
12
|
*
|