@phnx-labs/agents-cli 1.22.46 → 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 +663 -0
- package/LICENSE +102 -182
- package/README.md +96 -49
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +243 -25
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +711 -62
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +26 -24
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +107 -53
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- 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/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +7 -6
- 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 +4 -3
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +5 -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.d.ts +15 -0
- package/dist/commands/ssh.js +243 -18
- package/dist/commands/teams.js +34 -2
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +32 -62
- package/dist/commands/watchdog.js +9 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +84 -6
- package/dist/lib/accounting/usage.js +293 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +25 -15
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +40 -9
- package/dist/lib/browser/profiles.d.ts +104 -65
- package/dist/lib/browser/profiles.js +277 -165
- 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 +28 -2
- package/dist/lib/browser/remote-control.js +35 -3
- 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 +44 -11
- package/dist/lib/browser/runtime-state.js +98 -15
- package/dist/lib/browser/service.d.ts +65 -9
- package/dist/lib/browser/service.js +196 -61
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/browser/types.d.ts +12 -1
- 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/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- 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 +6 -11
- package/dist/lib/config-keys.js +18 -19
- 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/runner.js +15 -2
- 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 +22 -20
- package/dist/lib/device-config.d.ts +25 -10
- package/dist/lib/device-config.js +90 -39
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +26 -0
- package/dist/lib/devices/stats-cache.js +59 -7
- 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 +70 -14
- package/dist/lib/exec.js +110 -38
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- 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 +19 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- 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 +35 -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/hosts/registry.js +7 -0
- 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 +44 -7
- package/dist/lib/menubar/install-menubar.js +105 -23
- 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/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- 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/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.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.d.ts +67 -0
- package/dist/lib/session/active.js +86 -2
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +142 -8
- package/dist/lib/session/discover.d.ts +23 -1
- package/dist/lib/session/discover.js +44 -10
- 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/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- 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.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +17 -3
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +93 -1
- 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-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +19 -0
- package/dist/lib/session/trajectory-html.js +770 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +284 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +384 -0
- 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.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +6 -3
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +2 -2
- package/dist/lib/state.js +11 -5
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- 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.d.ts +46 -13
- package/dist/lib/tmux/session.js +204 -27
- 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 +34 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +18 -4
- package/dist/lib/usage-refresh.js +68 -19
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +5 -7
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
- 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 -24
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
- 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,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
|
@@ -275,6 +275,25 @@ def should_fire():
|
|
|
275
275
|
if not tn or tn not in allowed:
|
|
276
276
|
return False
|
|
277
277
|
|
|
278
|
+
v = m.get("permission_mode")
|
|
279
|
+
if v is not None:
|
|
280
|
+
allowed = arr(v)
|
|
281
|
+
if allowed:
|
|
282
|
+
pm = inp.get("permission_mode") or inp.get("permissionMode")
|
|
283
|
+
if pm and pm not in allowed:
|
|
284
|
+
return False
|
|
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
|
+
|
|
278
297
|
v = m.get("tool_args_match")
|
|
279
298
|
if v is not None:
|
|
280
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
|
}
|
|
@@ -20,6 +20,9 @@ export interface HookInput {
|
|
|
20
20
|
tool_name?: string;
|
|
21
21
|
tool_args?: unknown;
|
|
22
22
|
cwd?: string;
|
|
23
|
+
permission_mode?: string;
|
|
24
|
+
/** Grok-style camelCase spelling of permission_mode. */
|
|
25
|
+
permissionMode?: string;
|
|
23
26
|
}
|
|
24
27
|
export declare function isSafeHookRegex(source: string): boolean;
|
|
25
28
|
/**
|
package/dist/lib/hooks/match.js
CHANGED
|
@@ -131,6 +131,31 @@ export function shouldFire(matches, input) {
|
|
|
131
131
|
return false;
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
if (matches.permission_mode !== undefined) {
|
|
135
|
+
const allowed = arrayOf(matches.permission_mode);
|
|
136
|
+
if (allowed.length > 0) {
|
|
137
|
+
// Fail-open on absence: only some harnesses (Claude Code) report the
|
|
138
|
+
// live mode. An input with no mode field passes; an explicit value not
|
|
139
|
+
// in the allowed list skips.
|
|
140
|
+
const mode = input.permission_mode || input.permissionMode;
|
|
141
|
+
if (mode && !allowed.includes(mode))
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (matches.permission_mode_not !== undefined) {
|
|
146
|
+
const denied = arrayOf(matches.permission_mode_not);
|
|
147
|
+
if (denied.length > 0) {
|
|
148
|
+
// The negative form exists because the positive one cannot express
|
|
149
|
+
// "everywhere except plan" without enumerating every other mode — and an
|
|
150
|
+
// enumeration silently stops firing the moment a harness adds or renames
|
|
151
|
+
// one, which for a guard means it quietly stops guarding. Naming the mode
|
|
152
|
+
// to skip keeps every unknown mode firing, so the failure direction is
|
|
153
|
+
// "ran unnecessarily", never "did not run".
|
|
154
|
+
const mode = input.permission_mode || input.permissionMode;
|
|
155
|
+
if (mode && denied.includes(mode))
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
134
159
|
if (matches.tool_args_match !== undefined) {
|
|
135
160
|
const serialized = typeof input.tool_args === 'string'
|
|
136
161
|
? input.tool_args
|
|
@@ -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
|
*
|
|
@@ -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
|
*
|
|
@@ -37,8 +37,14 @@ export declare function withActorEnv(env?: Record<string, string>): Record<strin
|
|
|
37
37
|
* only reach the spawned agent's env and the remote `run auto` would re-pick.
|
|
38
38
|
* Shared by the interactive (runInteractiveOnHost) and detached
|
|
39
39
|
* (launchDetached) paths so both behave identically.
|
|
40
|
+
*
|
|
41
|
+
* `extra` carries the markers that are true of ONE path rather than both — the
|
|
42
|
+
* interactive dispatch adds REMOTE_INTERACTIVE_ENV, which the remote CLI reads
|
|
43
|
+
* to decide it must detach. It goes through this builder rather than being
|
|
44
|
+
* concatenated on at the call site so every remote env marker is exported the
|
|
45
|
+
* same way, in one place.
|
|
40
46
|
*/
|
|
41
|
-
export declare function remoteRunShellPrelude(agent: string): string;
|
|
47
|
+
export declare function remoteRunShellPrelude(agent: string, extra?: Record<string, string>): string;
|
|
42
48
|
/**
|
|
43
49
|
* Launch a detached login-shell command in its own Unix session/process group.
|
|
44
50
|
*
|
|
@@ -20,7 +20,7 @@ import { followHostTask } from './progress.js';
|
|
|
20
20
|
import { wrapHostCommandWithCredentials } from './credentials.js';
|
|
21
21
|
import { hostKeyCheckingOpts } from '../devices/known-hosts.js';
|
|
22
22
|
import { deriveMirroredCwd, homeRemainder, remoteCdPrefix } from '../project-root.js';
|
|
23
|
-
import { RUN_AUTO_KEYWORD, RUN_AUTO_HOST_RESOLVED_ENV } from '../types.js';
|
|
23
|
+
import { RUN_AUTO_KEYWORD, RUN_AUTO_HOST_RESOLVED_ENV, REMOTE_INTERACTIVE_ENV } from '../types.js';
|
|
24
24
|
// The home-relative portability helpers live in project-root.js (the canonical
|
|
25
25
|
// home-relative conversion home), reused by the interactive `agents ssh` login
|
|
26
26
|
// shell builder (devices/connect.ts) as well as this dispatch layer. Re-exported
|
|
@@ -75,10 +75,16 @@ export function withActorEnv(env) {
|
|
|
75
75
|
* only reach the spawned agent's env and the remote `run auto` would re-pick.
|
|
76
76
|
* Shared by the interactive (runInteractiveOnHost) and detached
|
|
77
77
|
* (launchDetached) paths so both behave identically.
|
|
78
|
+
*
|
|
79
|
+
* `extra` carries the markers that are true of ONE path rather than both — the
|
|
80
|
+
* interactive dispatch adds REMOTE_INTERACTIVE_ENV, which the remote CLI reads
|
|
81
|
+
* to decide it must detach. It goes through this builder rather than being
|
|
82
|
+
* concatenated on at the call site so every remote env marker is exported the
|
|
83
|
+
* same way, in one place.
|
|
78
84
|
*/
|
|
79
|
-
export function remoteRunShellPrelude(agent) {
|
|
85
|
+
export function remoteRunShellPrelude(agent, extra = {}) {
|
|
80
86
|
const guard = agent === RUN_AUTO_KEYWORD ? { [RUN_AUTO_HOST_RESOLVED_ENV]: '1' } : {};
|
|
81
|
-
const exports = posixEnvExports(withActorEnv(guard));
|
|
87
|
+
const exports = posixEnvExports(withActorEnv({ ...guard, ...extra }));
|
|
82
88
|
return exports ? `${exports}; ` : '';
|
|
83
89
|
}
|
|
84
90
|
/**
|
|
@@ -490,7 +496,13 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
490
496
|
// Forward actor provenance so the interactive remote run inherits it rather
|
|
491
497
|
// than re-resolving from this box's SSH_CONNECTION (RUSH-2028); a `run auto`
|
|
492
498
|
// dispatch also gets the chain-hop guard (remoteRunShellPrelude).
|
|
493
|
-
|
|
499
|
+
//
|
|
500
|
+
// REMOTE_INTERACTIVE_ENV rides the same prelude and is what makes the remote
|
|
501
|
+
// agent DETACHED: its stdio is this ssh link, so without the tmux wrap a
|
|
502
|
+
// blink SIGHUPs it and the in-flight turn is gone — while reconnect.ts is
|
|
503
|
+
// built to re-attach a pane it assumes survived (RUSH-3125). Set here, on the
|
|
504
|
+
// interactive path only: `launchDetached` already setsids the headless one.
|
|
505
|
+
const prelude = remoteRunShellPrelude(opts.agent, { [REMOTE_INTERACTIVE_ENV]: '1' });
|
|
494
506
|
let remoteCmd = `${prelude}${cwd}${invocation}`;
|
|
495
507
|
if (opts.copyCreds) {
|
|
496
508
|
remoteCmd = wrapHostCommandWithCredentials(remoteCmd, opts.copyCreds);
|
|
@@ -501,7 +513,14 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
501
513
|
const credHostKeyOpts = opts.copyCreds ? hostKeyCheckingOpts(true) : undefined;
|
|
502
514
|
return sshStream(target, remoteCmd, {
|
|
503
515
|
tty: process.stdin.isTTY,
|
|
504
|
-
|
|
516
|
+
// NOT multiplexed. `ControlPath=cm-%C` hashes only local host / remote host
|
|
517
|
+
// / port / user, so every agent tab pointed at a peer shares ONE master —
|
|
518
|
+
// and OpenSSH closes every channel on it the instant that master dies. One
|
|
519
|
+
// blink therefore ejected all six tabs on a box at once, which is what made
|
|
520
|
+
// a brief outage read as "all my agents exited" (RUSH-3125). The handshake
|
|
521
|
+
// this gives up is a one-time ~200ms on a session that runs for hours;
|
|
522
|
+
// probes and fan-outs keep the shared master, where it actually pays.
|
|
523
|
+
multiplex: false,
|
|
505
524
|
hostKeyOpts: credHostKeyOpts,
|
|
506
525
|
extraSshArgs: hostIdentityArgs(host),
|
|
507
526
|
});
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* supervisor outlives a dropped connection.
|
|
12
12
|
*
|
|
13
13
|
* Commands with their own richer `--device` handling (`run`/`sessions`/`feed`/
|
|
14
|
-
* `computer`/`secrets`/`logs`/…) are listed in {@link OWN_HOST_COMMANDS} and
|
|
14
|
+
* `computer`/`browser`/`secrets`/`logs`/…) are listed in {@link OWN_HOST_COMMANDS} and
|
|
15
15
|
* fall through to their local actions. Everything else either routes via this
|
|
16
16
|
* table or, when `--device` is present, exits with a clear
|
|
17
17
|
* "not supported" message — never commander's raw `unknown option`.
|
|
@@ -98,20 +98,13 @@ export declare function maybeRunOnHost(command: string, allArgs: string[], opts?
|
|
|
98
98
|
* the binary itself (the `browser`/`computer` bins, `dist/browser.js` etc.) rather
|
|
99
99
|
* than being the first argv token.
|
|
100
100
|
*
|
|
101
|
-
* `agents
|
|
102
|
-
* index.ts, but the standalone `browser`
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* On a local / self-host fall-through it rewrites `process.argv` to the ORIGINAL
|
|
110
|
-
* args minus the routing flags — never the synthetic command token — so the
|
|
111
|
-
* standalone commander program parses cleanly and never sees an unknown `--device`.
|
|
112
|
-
* When no routing flag is present it leaves `process.argv` untouched (the common
|
|
113
|
-
* case), so an unrelated flag like `--no-tty` is never stripped from a purely local
|
|
114
|
-
* run.
|
|
101
|
+
* `agents computer … --device <box>` still routes through {@link maybeRunOnHost}
|
|
102
|
+
* in index.ts, but the standalone `browser` / `computer` binaries never enter
|
|
103
|
+
* index.ts. Browser now owns `--device` (start binds the task; later verbs
|
|
104
|
+
* reject it), so this function leaves argv intact for OWN_HOST commands and
|
|
105
|
+
* only strips routing flags on `--help`/`--version`. Other standalone commands
|
|
106
|
+
* still synthesize the implicit command token and delegate to
|
|
107
|
+
* {@link maybeRunOnHost}.
|
|
115
108
|
*
|
|
116
109
|
* @param command the fixed command name the binary stands for (`'browser'`).
|
|
117
110
|
*/
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* supervisor outlives a dropped connection.
|
|
12
12
|
*
|
|
13
13
|
* Commands with their own richer `--device` handling (`run`/`sessions`/`feed`/
|
|
14
|
-
* `computer`/`secrets`/`logs`/…) are listed in {@link OWN_HOST_COMMANDS} and
|
|
14
|
+
* `computer`/`browser`/`secrets`/`logs`/…) are listed in {@link OWN_HOST_COMMANDS} and
|
|
15
15
|
* fall through to their local actions. Everything else either routes via this
|
|
16
16
|
* table or, when `--device` is present, exits with a clear
|
|
17
17
|
* "not supported" message — never commander's raw `unknown option`.
|
|
@@ -28,6 +28,7 @@ import { resolveRemoteOsSync } from './remote-os.js';
|
|
|
28
28
|
// 6–7× cheaper graph (RUSH-2374 proposal 2).
|
|
29
29
|
import { machineId } from '../machine-id.js';
|
|
30
30
|
import { isDeviceAuto, resolveDeviceAffinity } from '../smart-launch.js';
|
|
31
|
+
import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../devices/interactive-host.js';
|
|
31
32
|
import { flagValue, hasHostRoutingFlag } from './routing-flag.js';
|
|
32
33
|
import { loadDevices } from '../devices/registry.js';
|
|
33
34
|
import { isSelfHost } from '../devices/self-host.js';
|
|
@@ -100,7 +101,6 @@ export const REMOTE_PASSTHROUGH = {
|
|
|
100
101
|
tmux: {},
|
|
101
102
|
watchdog: {},
|
|
102
103
|
factory: {},
|
|
103
|
-
browser: {},
|
|
104
104
|
};
|
|
105
105
|
/**
|
|
106
106
|
* Commands that register and interpret `--device` themselves — must
|
|
@@ -115,6 +115,7 @@ export const OWN_HOST_COMMANDS = new Set([
|
|
|
115
115
|
'sessions',
|
|
116
116
|
'feed',
|
|
117
117
|
'computer',
|
|
118
|
+
'browser', // `--device` on start binds the task; later verbs resolve it from the task
|
|
118
119
|
'secrets',
|
|
119
120
|
'accounts', // `accounts sync --device` names the destination, not remote routing
|
|
120
121
|
'logs',
|
|
@@ -527,6 +528,21 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
527
528
|
process.stderr.write(chalk.gray(`[agents] device=auto → ${plan.host}\n`));
|
|
528
529
|
hostName = plan.host;
|
|
529
530
|
}
|
|
531
|
+
// `interactive` is the second affinity sentinel: the box the human sits at
|
|
532
|
+
// (`interactive.host`). Resolved here, ahead of the isSelfHost check below, for
|
|
533
|
+
// the same reason `auto` is — a pin naming THIS machine must run locally rather
|
|
534
|
+
// than self-SSH. It never falls back to the local box when unset: rendering to
|
|
535
|
+
// a screen nobody is watching is the exact failure the sentinel prevents.
|
|
536
|
+
if (isDeviceInteractive(hostName)) {
|
|
537
|
+
const pinned = resolveInteractiveDevice();
|
|
538
|
+
if (!pinned) {
|
|
539
|
+
process.stderr.write(`${interactiveUnsetError()}\n`);
|
|
540
|
+
process.exitCode = 1;
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
process.stderr.write(chalk.gray(`[agents] device=interactive → ${pinned}\n`));
|
|
544
|
+
hostName = pinned;
|
|
545
|
+
}
|
|
530
546
|
// Running against your own machine is just a local run — skip the SSH round-trip.
|
|
531
547
|
// Match EVERY identity the box answers to (short id, loopback, tailscale
|
|
532
548
|
// dnsName), not just machineId() — a `--device <self-dnsName>` used to slip past a
|
|
@@ -593,20 +609,13 @@ export async function maybeRunOnHost(command, allArgs, opts) {
|
|
|
593
609
|
* the binary itself (the `browser`/`computer` bins, `dist/browser.js` etc.) rather
|
|
594
610
|
* than being the first argv token.
|
|
595
611
|
*
|
|
596
|
-
* `agents
|
|
597
|
-
* index.ts, but the standalone `browser`
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
* On a local / self-host fall-through it rewrites `process.argv` to the ORIGINAL
|
|
605
|
-
* args minus the routing flags — never the synthetic command token — so the
|
|
606
|
-
* standalone commander program parses cleanly and never sees an unknown `--device`.
|
|
607
|
-
* When no routing flag is present it leaves `process.argv` untouched (the common
|
|
608
|
-
* case), so an unrelated flag like `--no-tty` is never stripped from a purely local
|
|
609
|
-
* run.
|
|
612
|
+
* `agents computer … --device <box>` still routes through {@link maybeRunOnHost}
|
|
613
|
+
* in index.ts, but the standalone `browser` / `computer` binaries never enter
|
|
614
|
+
* index.ts. Browser now owns `--device` (start binds the task; later verbs
|
|
615
|
+
* reject it), so this function leaves argv intact for OWN_HOST commands and
|
|
616
|
+
* only strips routing flags on `--help`/`--version`. Other standalone commands
|
|
617
|
+
* still synthesize the implicit command token and delegate to
|
|
618
|
+
* {@link maybeRunOnHost}.
|
|
610
619
|
*
|
|
611
620
|
* @param command the fixed command name the binary stands for (`'browser'`).
|
|
612
621
|
*/
|
|
@@ -616,6 +625,16 @@ export async function maybeRunStandaloneOnHost(command, opts) {
|
|
|
616
625
|
// local run keeps every flag it passed.
|
|
617
626
|
if (!hasHostRoutingFlag(rawArgs))
|
|
618
627
|
return false;
|
|
628
|
+
// Commands that interpret `--device` themselves (browser start binds the
|
|
629
|
+
// task→device index; later browser verbs reject the flag) must see it.
|
|
630
|
+
// Help/version still strip so `browser --device x --help` parses.
|
|
631
|
+
if (OWN_HOST_COMMANDS.has(command)) {
|
|
632
|
+
const helpOrVersion = rawArgs.some((a) => a === '--help' || a === '-h' || a === '--version' || a === '-V');
|
|
633
|
+
if (helpOrVersion) {
|
|
634
|
+
process.argv = [process.argv[0], process.argv[1], ...stripRoutingFlags(rawArgs, STRIP_SPECS)];
|
|
635
|
+
}
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
619
638
|
// Keep --help/--version local (docs must work without a reachable host), mirroring
|
|
620
639
|
// index.ts's `helpOrVersionRequested` guard, but still strip the routing flags
|
|
621
640
|
// below so commander doesn't choke on them.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Bridges the devices registry (`agents devices`, ~/.agents/.history/devices/
|
|
5
5
|
* registry.json) into the host pool behind the same `HostProvider` seam as
|
|
6
|
-
* `local` — the
|
|
6
|
+
* `local` — the Tailscale-backed device provider described in docs/fleet.md. With
|
|
7
7
|
* it, a machine registered once via `agents devices sync` becomes a resolvable
|
|
8
8
|
* `--device`/`--device` dispatch target, participates in capability routing, and
|
|
9
9
|
* is enumerable by target pickers — not just resolvable by exact name.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Bridges the devices registry (`agents devices`, ~/.agents/.history/devices/
|
|
5
5
|
* registry.json) into the host pool behind the same `HostProvider` seam as
|
|
6
|
-
* `local` — the
|
|
6
|
+
* `local` — the Tailscale-backed device provider described in docs/fleet.md. With
|
|
7
7
|
* it, a machine registered once via `agents devices sync` becomes a resolvable
|
|
8
8
|
* `--device`/`--device` dispatch target, participates in capability routing, and
|
|
9
9
|
* is enumerable by target pickers — not just resolvable by exact name.
|