@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* `crabbox` later is a one-line `providers.set(...)` with no caller changes.
|
|
7
7
|
*/
|
|
8
8
|
import { LocalHostProvider } from './providers/local.js';
|
|
9
|
+
import { getDevice } from '../devices/registry.js';
|
|
10
|
+
import { assertValidSshTarget } from '../ssh-exec.js';
|
|
9
11
|
const providers = new Map();
|
|
10
12
|
function initProviders() {
|
|
11
13
|
if (providers.size > 0)
|
|
@@ -39,8 +41,54 @@ export async function listAllHosts() {
|
|
|
39
41
|
return out;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
+
* Thrown when a device resolves but can't be used as an offload target because
|
|
45
|
+
* it authenticates with a password. The offload path runs over `sshExec`, whose
|
|
46
|
+
* `SSH_OPTS` force `BatchMode=yes` (no password prompts), so only key / ssh-config
|
|
47
|
+
* auth can carry a `--host` run. Named so the top-level catch prints the message
|
|
48
|
+
* cleanly instead of a stack trace.
|
|
49
|
+
*/
|
|
50
|
+
export class DeviceOffloadUnsupportedError extends Error {
|
|
51
|
+
constructor(name) {
|
|
52
|
+
super(`Device "${name}" uses password auth, which --host offload can't use yet ` +
|
|
53
|
+
`(runs go over ssh with BatchMode=yes). Switch it to key auth with ` +
|
|
54
|
+
`\`agents devices set ${name} --auth key\`, or enroll it as a host with ` +
|
|
55
|
+
`\`agents hosts add ${name}\`.`);
|
|
56
|
+
this.name = 'DeviceOffloadUnsupportedError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Bridge a registered device into a `Host` so `--host <device>` just works.
|
|
61
|
+
* A device's dnsName (preferred) or ip becomes the ssh address; `source: 'inline'`
|
|
62
|
+
* makes `sshTargetFor` emit `user@address`.
|
|
63
|
+
*/
|
|
64
|
+
function deviceToHost(device) {
|
|
65
|
+
if (device.auth.method === 'password') {
|
|
66
|
+
throw new DeviceOffloadUnsupportedError(device.name);
|
|
67
|
+
}
|
|
68
|
+
const address = device.address.dnsName ?? device.address.ip;
|
|
69
|
+
if (!address) {
|
|
70
|
+
throw new Error(`Device "${device.name}" has no address (Tailscale DNS name or IP) to reach it by.`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
name: device.name,
|
|
74
|
+
provider: 'local',
|
|
75
|
+
source: 'inline',
|
|
76
|
+
address,
|
|
77
|
+
user: device.user,
|
|
78
|
+
...(device.platform !== 'unknown' ? { os: device.platform } : {}),
|
|
79
|
+
enrolled: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a host name to a single host, or null if unknown. Resolution order:
|
|
84
|
+
* 1. host providers — the `agents hosts` registry (agents.yaml overlay + ssh-config)
|
|
85
|
+
* 2. the devices registry (`agents devices`) — a machine registered once with
|
|
86
|
+
* `agents devices sync` is reachable by `--host`/`--device` with no second enroll
|
|
87
|
+
* 3. an ad-hoc `user@host` (must contain `@`, validated) — nothing to register
|
|
88
|
+
*
|
|
89
|
+
* A bare unknown name returns null so capability-tag routing (`resolveHostByCap`)
|
|
90
|
+
* stays reachable: `--host gpu` must fall through to a cap lookup, not be misread
|
|
91
|
+
* as an ad-hoc target.
|
|
44
92
|
*/
|
|
45
93
|
export async function resolveHost(name) {
|
|
46
94
|
for (const provider of getAllProviders()) {
|
|
@@ -48,6 +96,14 @@ export async function resolveHost(name) {
|
|
|
48
96
|
if (host)
|
|
49
97
|
return host;
|
|
50
98
|
}
|
|
99
|
+
const device = await getDevice(name);
|
|
100
|
+
if (device)
|
|
101
|
+
return deviceToHost(device);
|
|
102
|
+
if (name.includes('@')) {
|
|
103
|
+
assertValidSshTarget(name);
|
|
104
|
+
const at = name.indexOf('@');
|
|
105
|
+
return { name, provider: 'local', source: 'inline', user: name.slice(0, at), address: name.slice(at + 1) };
|
|
106
|
+
}
|
|
51
107
|
return null;
|
|
52
108
|
}
|
|
53
109
|
/**
|
|
@@ -32,5 +32,66 @@ export declare const HOST_ROUTING_SPECS: StripSpec[];
|
|
|
32
32
|
* are quoted for the inner login shell, then the whole `agents …` invocation is
|
|
33
33
|
* quoted again so it survives `bash -lc <...>` — `bash -lc` so the remote login
|
|
34
34
|
* PATH resolves `agents`. An optional `cd` runs first for `--remote-cwd`.
|
|
35
|
+
*
|
|
36
|
+
* `os` selects the remote shell dialect: a Windows target gets a PowerShell
|
|
37
|
+
* invocation instead (ssh lands in cmd.exe/PowerShell there, where `bash -lc`
|
|
38
|
+
* does not exist). Anything else — including an unknown/absent OS — keeps the
|
|
39
|
+
* POSIX form, so linux/macos are byte-for-byte unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildRemoteAgentsInvocation(forwardedArgs: string[], remoteCwd?: string, os?: string): string;
|
|
42
|
+
/** The two remote shell dialects we build commands for. */
|
|
43
|
+
export type RemoteShell = 'posix' | 'powershell';
|
|
44
|
+
/**
|
|
45
|
+
* Pick the remote shell dialect from a recorded OS/platform string. A Windows
|
|
46
|
+
* host (device-registry `platform: 'windows'`, or an enrolled `HostEntry.os`
|
|
47
|
+
* that reads `windows`/`Windows`/`win32`/…) speaks PowerShell; everything else,
|
|
48
|
+
* including `undefined`/unknown, defaults to POSIX so linux/macos never regress.
|
|
49
|
+
*/
|
|
50
|
+
export declare function remoteShellFor(os: string | undefined): RemoteShell;
|
|
51
|
+
/**
|
|
52
|
+
* PowerShell single-quoted literal: wrap in `'…'` and double any embedded `'`.
|
|
53
|
+
* Single-quoted PS strings are fully literal (no `$var`, no backtick escapes),
|
|
54
|
+
* so this neutralises every metacharacter the same way POSIX `shellQuote` does.
|
|
55
|
+
*/
|
|
56
|
+
export declare function powershellQuote(s: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Encode a PowerShell script for `powershell -EncodedCommand`: base64 of its
|
|
59
|
+
* UTF-16LE bytes. The payload is a bare base64 word (no spaces or shell
|
|
60
|
+
* metacharacters), so it survives being handed to ssh as a single argument and
|
|
61
|
+
* re-parsed by the remote's cmd.exe/PowerShell with zero quoting hazards —
|
|
62
|
+
* the robust way to ship a complex command to a Windows box over SSH.
|
|
63
|
+
*/
|
|
64
|
+
export declare function encodePowershell(script: string): string;
|
|
65
|
+
/** Inverse of {@link encodePowershell} — decode an `-EncodedCommand` payload
|
|
66
|
+
* back to its script. Used by tests to assert on the built command. */
|
|
67
|
+
export declare function decodePowershell(encoded: string): string;
|
|
68
|
+
/** A single `agents …` invocation to run on a Windows remote. */
|
|
69
|
+
export interface WindowsAgentsCommand {
|
|
70
|
+
/** `agents` argv (command name NOT included; `agents` is prepended). */
|
|
71
|
+
args: string[];
|
|
72
|
+
/** Env vars scoped to this invocation (POSIX `VAR=val` ↔ PS `$env:VAR=…`). */
|
|
73
|
+
env?: Record<string, string>;
|
|
74
|
+
/** Directory to enter before running (`--remote-cwd`). */
|
|
75
|
+
cwd?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Append `exit $LASTEXITCODE` so a native `agents` exit code propagates out
|
|
78
|
+
* through `powershell.exe` (which otherwise exits 0 regardless). Default true;
|
|
79
|
+
* pass false for probes whose reachability keys off a sentinel, not the code.
|
|
80
|
+
*/
|
|
81
|
+
propagateExit?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The PowerShell script (pre-encoding) that {@link buildWindowsAgentsCommand}
|
|
85
|
+
* runs. Exposed so tests can decode the `-EncodedCommand` payload and compare
|
|
86
|
+
* against the exact script, without needing PowerShell on the test host.
|
|
87
|
+
*
|
|
88
|
+
* `& agents …` invokes the CLI from the machine PATH (Windows has no login
|
|
89
|
+
* shell, so there is no `bash -lc` equivalent — the shim is simply on PATH).
|
|
90
|
+
*/
|
|
91
|
+
export declare function windowsAgentsScript(cmd: WindowsAgentsCommand): string;
|
|
92
|
+
/**
|
|
93
|
+
* Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
|
|
94
|
+
* Windows remote: a `powershell -NoProfile -EncodedCommand <base64>` call. The
|
|
95
|
+
* Windows counterpart of `bash -lc '<...>'`, shared by every `--host` site.
|
|
35
96
|
*/
|
|
36
|
-
export declare function
|
|
97
|
+
export declare function buildWindowsAgentsCommand(cmd: WindowsAgentsCommand): string;
|
|
@@ -48,9 +48,78 @@ export const HOST_ROUTING_SPECS = [
|
|
|
48
48
|
* are quoted for the inner login shell, then the whole `agents …` invocation is
|
|
49
49
|
* quoted again so it survives `bash -lc <...>` — `bash -lc` so the remote login
|
|
50
50
|
* PATH resolves `agents`. An optional `cd` runs first for `--remote-cwd`.
|
|
51
|
+
*
|
|
52
|
+
* `os` selects the remote shell dialect: a Windows target gets a PowerShell
|
|
53
|
+
* invocation instead (ssh lands in cmd.exe/PowerShell there, where `bash -lc`
|
|
54
|
+
* does not exist). Anything else — including an unknown/absent OS — keeps the
|
|
55
|
+
* POSIX form, so linux/macos are byte-for-byte unchanged.
|
|
51
56
|
*/
|
|
52
|
-
export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd) {
|
|
57
|
+
export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd, os) {
|
|
58
|
+
if (remoteShellFor(os) === 'powershell') {
|
|
59
|
+
return buildWindowsAgentsCommand({ args: forwardedArgs, cwd: remoteCwd });
|
|
60
|
+
}
|
|
53
61
|
const inner = ['agents', ...forwardedArgs].map(shellQuote).join(' ');
|
|
54
62
|
const withCwd = remoteCwd ? `cd ${shellQuote(remoteCwd)} && ${inner}` : inner;
|
|
55
63
|
return `bash -lc ${shellQuote(withCwd)}`;
|
|
56
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Pick the remote shell dialect from a recorded OS/platform string. A Windows
|
|
67
|
+
* host (device-registry `platform: 'windows'`, or an enrolled `HostEntry.os`
|
|
68
|
+
* that reads `windows`/`Windows`/`win32`/…) speaks PowerShell; everything else,
|
|
69
|
+
* including `undefined`/unknown, defaults to POSIX so linux/macos never regress.
|
|
70
|
+
*/
|
|
71
|
+
export function remoteShellFor(os) {
|
|
72
|
+
return /^win/i.test((os ?? '').trim()) ? 'powershell' : 'posix';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* PowerShell single-quoted literal: wrap in `'…'` and double any embedded `'`.
|
|
76
|
+
* Single-quoted PS strings are fully literal (no `$var`, no backtick escapes),
|
|
77
|
+
* so this neutralises every metacharacter the same way POSIX `shellQuote` does.
|
|
78
|
+
*/
|
|
79
|
+
export function powershellQuote(s) {
|
|
80
|
+
return "'" + s.replace(/'/g, "''") + "'";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Encode a PowerShell script for `powershell -EncodedCommand`: base64 of its
|
|
84
|
+
* UTF-16LE bytes. The payload is a bare base64 word (no spaces or shell
|
|
85
|
+
* metacharacters), so it survives being handed to ssh as a single argument and
|
|
86
|
+
* re-parsed by the remote's cmd.exe/PowerShell with zero quoting hazards —
|
|
87
|
+
* the robust way to ship a complex command to a Windows box over SSH.
|
|
88
|
+
*/
|
|
89
|
+
export function encodePowershell(script) {
|
|
90
|
+
return Buffer.from(script, 'utf16le').toString('base64');
|
|
91
|
+
}
|
|
92
|
+
/** Inverse of {@link encodePowershell} — decode an `-EncodedCommand` payload
|
|
93
|
+
* back to its script. Used by tests to assert on the built command. */
|
|
94
|
+
export function decodePowershell(encoded) {
|
|
95
|
+
return Buffer.from(encoded, 'base64').toString('utf16le');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The PowerShell script (pre-encoding) that {@link buildWindowsAgentsCommand}
|
|
99
|
+
* runs. Exposed so tests can decode the `-EncodedCommand` payload and compare
|
|
100
|
+
* against the exact script, without needing PowerShell on the test host.
|
|
101
|
+
*
|
|
102
|
+
* `& agents …` invokes the CLI from the machine PATH (Windows has no login
|
|
103
|
+
* shell, so there is no `bash -lc` equivalent — the shim is simply on PATH).
|
|
104
|
+
*/
|
|
105
|
+
export function windowsAgentsScript(cmd) {
|
|
106
|
+
const { args, env, cwd, propagateExit = true } = cmd;
|
|
107
|
+
const parts = [];
|
|
108
|
+
if (env)
|
|
109
|
+
for (const [k, v] of Object.entries(env))
|
|
110
|
+
parts.push(`$env:${k} = ${powershellQuote(v)}`);
|
|
111
|
+
if (cwd)
|
|
112
|
+
parts.push(`Set-Location -LiteralPath ${powershellQuote(cwd)}`);
|
|
113
|
+
parts.push(`& ${['agents', ...args].map(powershellQuote).join(' ')}`);
|
|
114
|
+
if (propagateExit)
|
|
115
|
+
parts.push('exit $LASTEXITCODE');
|
|
116
|
+
return parts.join('; ');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the `ssh <target> <cmd>` string for one `agents …` invocation on a
|
|
120
|
+
* Windows remote: a `powershell -NoProfile -EncodedCommand <base64>` call. The
|
|
121
|
+
* Windows counterpart of `bash -lc '<...>'`, shared by every `--host` site.
|
|
122
|
+
*/
|
|
123
|
+
export function buildWindowsAgentsCommand(cmd) {
|
|
124
|
+
return `powershell -NoProfile -EncodedCommand ${encodePowershell(windowsAgentsScript(cmd))}`;
|
|
125
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a remote host's OS family so the SSH command layer can pick the right
|
|
3
|
+
* shell dialect (POSIX `bash -lc` vs Windows PowerShell). See `remoteShellFor`
|
|
4
|
+
* in `remote-cmd.ts` for how the string is consumed.
|
|
5
|
+
*
|
|
6
|
+
* Two sources, in priority order:
|
|
7
|
+
* 1. The device registry `platform` (`windows`/`linux`/`macos`), which is
|
|
8
|
+
* populated fleet-wide by Tailscale sync — the reliable answer for a box
|
|
9
|
+
* like `win-mini` that was discovered, not hand-enrolled.
|
|
10
|
+
* 2. The enrolled `HostEntry.os` overlay in agents.yaml (the `uname` captured
|
|
11
|
+
* at `agents hosts add` time), for hosts that live only in that overlay.
|
|
12
|
+
*
|
|
13
|
+
* Missing/unknown from both → `undefined`, which `remoteShellFor` maps to POSIX.
|
|
14
|
+
* Kept synchronous so the sync `agents sessions --host` fan-out can use it.
|
|
15
|
+
*/
|
|
16
|
+
/** Resolve the OS/platform string for a host name, or undefined if unknown. */
|
|
17
|
+
export declare function resolveRemoteOsSync(name: string): string | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a remote host's OS family so the SSH command layer can pick the right
|
|
3
|
+
* shell dialect (POSIX `bash -lc` vs Windows PowerShell). See `remoteShellFor`
|
|
4
|
+
* in `remote-cmd.ts` for how the string is consumed.
|
|
5
|
+
*
|
|
6
|
+
* Two sources, in priority order:
|
|
7
|
+
* 1. The device registry `platform` (`windows`/`linux`/`macos`), which is
|
|
8
|
+
* populated fleet-wide by Tailscale sync — the reliable answer for a box
|
|
9
|
+
* like `win-mini` that was discovered, not hand-enrolled.
|
|
10
|
+
* 2. The enrolled `HostEntry.os` overlay in agents.yaml (the `uname` captured
|
|
11
|
+
* at `agents hosts add` time), for hosts that live only in that overlay.
|
|
12
|
+
*
|
|
13
|
+
* Missing/unknown from both → `undefined`, which `remoteShellFor` maps to POSIX.
|
|
14
|
+
* Kept synchronous so the sync `agents sessions --host` fan-out can use it.
|
|
15
|
+
*/
|
|
16
|
+
import { loadDevicesSync } from '../devices/registry.js';
|
|
17
|
+
import { readMeta } from '../state.js';
|
|
18
|
+
/** Resolve the OS/platform string for a host name, or undefined if unknown. */
|
|
19
|
+
export function resolveRemoteOsSync(name) {
|
|
20
|
+
try {
|
|
21
|
+
const platform = loadDevicesSync()[name]?.platform;
|
|
22
|
+
if (platform && platform !== 'unknown')
|
|
23
|
+
return platform;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// A corrupt/unreadable device registry must never break command building —
|
|
27
|
+
// fall through to the host overlay and ultimately the POSIX default.
|
|
28
|
+
}
|
|
29
|
+
return readMeta().hosts?.[name]?.os;
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register host-dispatched runs (`agents run --host <h>`) into the LOCAL session
|
|
3
|
+
* index so they show up in `agents sessions` and are resolvable by id — even
|
|
4
|
+
* though the transcript itself lives on the remote host.
|
|
5
|
+
*
|
|
6
|
+
* The transcript file is remote, so the row is registered with an EMPTY
|
|
7
|
+
* `file_path`. The DB's stale-row filter keeps rows with no file_path
|
|
8
|
+
* (`!file_path || existsSync(file_path)` in db.ts querySessions) precisely for
|
|
9
|
+
* this "synthetic file_path" case, and the scanner never prunes session rows —
|
|
10
|
+
* so the entry survives rescans. `cwd` is the LOCAL directory the dispatch was
|
|
11
|
+
* issued from, so the run appears in that project's session listing like any
|
|
12
|
+
* local run. The `[host/<name>]` label mirrors the cloud path's
|
|
13
|
+
* `[cloud/<status>]` convention.
|
|
14
|
+
*/
|
|
15
|
+
import type { SessionMeta } from '../session/types.js';
|
|
16
|
+
import type { HostTask } from './tasks.js';
|
|
17
|
+
export interface HostSessionContext {
|
|
18
|
+
/** Local directory the `agents run --host` was invoked from. */
|
|
19
|
+
cwd: string;
|
|
20
|
+
/** Prompt the run was launched with, used for the session topic. */
|
|
21
|
+
prompt: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the SessionMeta for a host-dispatched run. Returns null when the run has
|
|
25
|
+
* no captured session id (nothing stable to key/resume on) or its agent isn't a
|
|
26
|
+
* known session agent. Pure — no I/O — so the mapping is unit-testable.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hostSessionMeta(task: HostTask, ctx: HostSessionContext): SessionMeta | null;
|
|
29
|
+
/**
|
|
30
|
+
* Register (or refresh) a host-dispatched run in the local session index. No-op
|
|
31
|
+
* when the task carries no session id. Best-effort: a failed write must never
|
|
32
|
+
* break the dispatch itself, which has already been launched on the host.
|
|
33
|
+
*/
|
|
34
|
+
export declare function registerHostSession(task: HostTask, ctx: HostSessionContext): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register host-dispatched runs (`agents run --host <h>`) into the LOCAL session
|
|
3
|
+
* index so they show up in `agents sessions` and are resolvable by id — even
|
|
4
|
+
* though the transcript itself lives on the remote host.
|
|
5
|
+
*
|
|
6
|
+
* The transcript file is remote, so the row is registered with an EMPTY
|
|
7
|
+
* `file_path`. The DB's stale-row filter keeps rows with no file_path
|
|
8
|
+
* (`!file_path || existsSync(file_path)` in db.ts querySessions) precisely for
|
|
9
|
+
* this "synthetic file_path" case, and the scanner never prunes session rows —
|
|
10
|
+
* so the entry survives rescans. `cwd` is the LOCAL directory the dispatch was
|
|
11
|
+
* issued from, so the run appears in that project's session listing like any
|
|
12
|
+
* local run. The `[host/<name>]` label mirrors the cloud path's
|
|
13
|
+
* `[cloud/<status>]` convention.
|
|
14
|
+
*/
|
|
15
|
+
import { upsertSession } from '../session/db.js';
|
|
16
|
+
import { SESSION_AGENTS } from '../session/types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Build the SessionMeta for a host-dispatched run. Returns null when the run has
|
|
19
|
+
* no captured session id (nothing stable to key/resume on) or its agent isn't a
|
|
20
|
+
* known session agent. Pure — no I/O — so the mapping is unit-testable.
|
|
21
|
+
*/
|
|
22
|
+
export function hostSessionMeta(task, ctx) {
|
|
23
|
+
const id = task.sessionId;
|
|
24
|
+
if (!id)
|
|
25
|
+
return null;
|
|
26
|
+
if (!SESSION_AGENTS.includes(task.agent))
|
|
27
|
+
return null;
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
shortId: id.slice(0, 8),
|
|
31
|
+
agent: task.agent,
|
|
32
|
+
timestamp: task.createdAt,
|
|
33
|
+
cwd: ctx.cwd,
|
|
34
|
+
// Remote transcript — no local file. Empty file_path is the sentinel the DB
|
|
35
|
+
// stale-filter treats as "always live" (see module doc).
|
|
36
|
+
filePath: '',
|
|
37
|
+
topic: ctx.prompt.split('\n')[0]?.slice(0, 120) || undefined,
|
|
38
|
+
label: `[host/${task.host}]`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register (or refresh) a host-dispatched run in the local session index. No-op
|
|
43
|
+
* when the task carries no session id. Best-effort: a failed write must never
|
|
44
|
+
* break the dispatch itself, which has already been launched on the host.
|
|
45
|
+
*/
|
|
46
|
+
export function registerHostSession(task, ctx) {
|
|
47
|
+
const meta = hostSessionMeta(task, ctx);
|
|
48
|
+
if (!meta)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
upsertSession(meta, '');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* index write is best-effort; the run is already live on the host */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -15,6 +15,13 @@ export interface HostTask {
|
|
|
15
15
|
agent: string;
|
|
16
16
|
prompt: string;
|
|
17
17
|
pid?: number;
|
|
18
|
+
/**
|
|
19
|
+
* The agent session id the remote run was launched with (Claude only — the
|
|
20
|
+
* only agent that accepts `--session-id` to force a NEW session's id). Lets
|
|
21
|
+
* `agents sessions`/resume-by-id map a discovered session back to the host it
|
|
22
|
+
* lives on. Absent for agents that don't take an explicit session id.
|
|
23
|
+
*/
|
|
24
|
+
sessionId?: string;
|
|
18
25
|
/** Remote paths (under the host's ~/.agents/.cache/hosts/). */
|
|
19
26
|
remoteLog: string;
|
|
20
27
|
remoteExit: string;
|
|
@@ -38,3 +45,10 @@ export declare function updateTask(id: string, patch: Partial<HostTask>): HostTa
|
|
|
38
45
|
*/
|
|
39
46
|
export declare function terminalPatch(code: number): Partial<HostTask>;
|
|
40
47
|
export declare function listTasks(): HostTask[];
|
|
48
|
+
/**
|
|
49
|
+
* Find the host task that launched a given agent session id, so a resume-by-id
|
|
50
|
+
* can re-dispatch to the host the session actually lives on. Newest task wins
|
|
51
|
+
* (listTasks is createdAt-desc) — a session id should be unique, but a re-run
|
|
52
|
+
* with the same forced id resolves to the most recent dispatch.
|
|
53
|
+
*/
|
|
54
|
+
export declare function findTaskBySessionId(sessionId: string): HostTask | null;
|
package/dist/lib/hosts/tasks.js
CHANGED
|
@@ -70,3 +70,18 @@ export function listTasks() {
|
|
|
70
70
|
}
|
|
71
71
|
return tasks.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Find the host task that launched a given agent session id, so a resume-by-id
|
|
75
|
+
* can re-dispatch to the host the session actually lives on. Newest task wins
|
|
76
|
+
* (listTasks is createdAt-desc) — a session id should be unique, but a re-run
|
|
77
|
+
* with the same forced id resolves to the most recent dispatch.
|
|
78
|
+
*/
|
|
79
|
+
export function findTaskBySessionId(sessionId) {
|
|
80
|
+
if (!sessionId)
|
|
81
|
+
return null;
|
|
82
|
+
for (const task of listTasks()) {
|
|
83
|
+
if (task.sessionId === sessionId)
|
|
84
|
+
return task;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents.lock` — a deterministic SHA-256 manifest of the resolved resource set
|
|
3
|
+
* at a project root, plus the `--frozen` verification that fails closed on any
|
|
4
|
+
* drift. This is the reproducible-CI slice of the governance work (#337); org
|
|
5
|
+
* version pins and bundle signing are deferred follow-ups.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Content hashing REUSES `fingerprintDir` / `fingerprintFile`
|
|
9
|
+
* (src/lib/staleness/fingerprint.ts) — the same two-tier fingerprinter every
|
|
10
|
+
* staleness checker uses. No new recursive file walker is introduced here.
|
|
11
|
+
* - The resource SET is the layered, resolved source union (project > user >
|
|
12
|
+
* system > extras), the same precedence `resolveResource` / `syncResourcesToVersion`
|
|
13
|
+
* read from. It is enumerated agent-independently on purpose: the lock must be
|
|
14
|
+
* reproducible on a fresh CI checkout that has no installed agent VERSIONS, so
|
|
15
|
+
* it snapshots the SOURCES sync copies FROM, not any per-version home.
|
|
16
|
+
*
|
|
17
|
+
* Lock shape (JSON): `{ "version": 1, "resources": { "<relpath>": "<sha256>" } }`,
|
|
18
|
+
* keys sorted so the file is byte-stable across machines and runs.
|
|
19
|
+
*/
|
|
20
|
+
export declare const LOCK_FILENAME = "agents.lock";
|
|
21
|
+
export declare const LOCK_VERSION: 1;
|
|
22
|
+
/**
|
|
23
|
+
* Resource kinds captured by the lock — the file/dir-backed layered resources
|
|
24
|
+
* shared across every agent. Presence-only kinds handled specially by the sync
|
|
25
|
+
* differ (plugins/promptcuts) are intentionally out of this first slice.
|
|
26
|
+
*/
|
|
27
|
+
export declare const LOCK_KINDS: readonly ["commands", "skills", "hooks", "rules", "mcp", "permissions", "subagents"];
|
|
28
|
+
/** The on-disk lockfile: relpath -> sha256 hex digest, keys sorted. */
|
|
29
|
+
export interface AgentsLock {
|
|
30
|
+
version: typeof LOCK_VERSION;
|
|
31
|
+
resources: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One resolved resource to fingerprint. `path` is an absolute file or directory;
|
|
35
|
+
* `key` is the stable relpath prefix under which its hashes are recorded
|
|
36
|
+
* (e.g. `skills/debug` for a directory, `commands/plan.md` for a single file).
|
|
37
|
+
*/
|
|
38
|
+
export interface LockSource {
|
|
39
|
+
path: string;
|
|
40
|
+
key: string;
|
|
41
|
+
}
|
|
42
|
+
/** Added/removed/changed keys between an expected lock and the live resources. */
|
|
43
|
+
export interface LockDiff {
|
|
44
|
+
/** Present in the live resources, absent from the lock. */
|
|
45
|
+
added: string[];
|
|
46
|
+
/** Present in the lock, absent from the live resources. */
|
|
47
|
+
removed: string[];
|
|
48
|
+
/** Present in both but the sha256 differs. */
|
|
49
|
+
changed: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Hash every resolved source into a flat `relpath -> sha256` map. Directories are
|
|
53
|
+
* expanded with `fingerprintDir` (every contained file, sorted); single files use
|
|
54
|
+
* `fingerprintFile`. Unreadable / missing sources are skipped — enumeration only
|
|
55
|
+
* ever hands us paths that existed at scan time.
|
|
56
|
+
*/
|
|
57
|
+
export declare function hashLockSources(sources: LockSource[]): Record<string, string>;
|
|
58
|
+
/** Build a fully-sorted lock object from a resolved source set. */
|
|
59
|
+
export declare function buildLock(sources: LockSource[]): AgentsLock;
|
|
60
|
+
/** Serialise a lock to the canonical, sorted, newline-terminated JSON form. */
|
|
61
|
+
export declare function serializeLock(lock: AgentsLock): string;
|
|
62
|
+
/** Absolute path to the lockfile for a project root. */
|
|
63
|
+
export declare function lockPath(projectRoot: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Read + validate an existing lock. Returns null ONLY when the file is absent —
|
|
66
|
+
* a present-but-malformed lock throws, so `--frozen` fails closed instead of
|
|
67
|
+
* silently treating corruption as "no lock".
|
|
68
|
+
*/
|
|
69
|
+
export declare function readLock(projectRoot: string): AgentsLock | null;
|
|
70
|
+
/** Write the lock to `<projectRoot>/agents.lock`, returning the path written. */
|
|
71
|
+
export declare function writeLock(projectRoot: string, lock: AgentsLock): string;
|
|
72
|
+
/** Diff a recorded lock against a freshly-computed one. */
|
|
73
|
+
export declare function diffLock(expected: AgentsLock, actual: AgentsLock): LockDiff;
|
|
74
|
+
/** True when a diff has no added/removed/changed entries. */
|
|
75
|
+
export declare function lockDiffIsClean(diff: LockDiff): boolean;
|
|
76
|
+
/** Verify a recorded lock against the live resolved sources. */
|
|
77
|
+
export declare function verifyLock(expected: AgentsLock, sources: LockSource[]): LockDiff;
|
|
78
|
+
/**
|
|
79
|
+
* The project root the lockfile lives at: the parent of the discovered project
|
|
80
|
+
* `.agents/` dir, or the cwd itself when none is found.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveProjectRoot(cwd?: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* Enumerate the resolved resource sources for a cwd: for each lockable kind, the
|
|
85
|
+
* winning entry per name across layers (project > user > system > extras), keyed
|
|
86
|
+
* `<kind>/<entryName>`. This is a single-level `readdir` per kind dir — the
|
|
87
|
+
* recursive fingerprinting is delegated to `fingerprintDir`.
|
|
88
|
+
*
|
|
89
|
+
* Entry names keep their extension in the key, so a hook script `foo.sh` and its
|
|
90
|
+
* sidecar `foo.yaml` stay distinct (a name-stripped enumerator would collide
|
|
91
|
+
* them). Dotfiles at the kind-dir top level are skipped as noise.
|
|
92
|
+
*/
|
|
93
|
+
export declare function enumerateLockSources(cwd?: string): LockSource[];
|