@phnx-labs/agents-cli 1.22.103 → 1.22.105
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 +39 -0
- package/README.md +1 -1
- package/dist/commands/accounts.js +2 -2
- package/dist/commands/computer.d.ts +100 -79
- package/dist/commands/computer.js +290 -830
- package/dist/commands/run-device-picker.d.ts +58 -0
- package/dist/commands/run-device-picker.js +222 -0
- package/dist/commands/setup-computer.js +20 -1
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/view.js +4 -6
- package/dist/lib/account-catalog.d.ts +22 -1
- package/dist/lib/account-catalog.js +72 -38
- package/dist/lib/accounting/usage.js +18 -14
- package/dist/lib/agent-spec/agents.d.ts +1 -0
- package/dist/lib/agent-spec/agents.js +1 -1
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/computer/context.d.ts +83 -0
- package/dist/lib/computer/context.js +91 -0
- package/dist/lib/computer/policy.d.ts +46 -0
- package/dist/lib/computer/policy.js +160 -0
- package/dist/lib/computer/record.d.ts +38 -0
- package/dist/lib/computer/record.js +86 -0
- package/dist/lib/computer/sessions-list.js +6 -6
- package/dist/lib/computer-client.d.ts +150 -0
- package/dist/lib/computer-client.js +222 -0
- package/dist/lib/exec.js +35 -1
- package/dist/lib/harness/adapters/claude.d.ts +37 -0
- package/dist/lib/harness/adapters/claude.js +69 -0
- package/dist/lib/helper-download.d.ts +1 -1
- package/dist/lib/helper-download.js +1 -1
- package/dist/lib/helper-versions.d.ts +9 -4
- package/dist/lib/helper-versions.js +8 -8
- package/dist/lib/installations/shims.js +8 -4
- package/dist/lib/menubar/download-menubar.d.ts +2 -1
- package/dist/lib/menubar/download-menubar.js +2 -1
- package/dist/lib/secrets-client.d.ts +3 -3
- package/dist/lib/secrets-client.js +15 -38
- package/dist/lib/session/db.js +1 -1
- package/dist/lib/sha256-asset.d.ts +2 -1
- package/dist/lib/sha256-asset.js +2 -1
- package/dist/lib/ssh-tunnel.d.ts +61 -0
- package/dist/lib/ssh-tunnel.js +105 -0
- package/dist/lib/summarizer/summarize.d.ts +2 -2
- package/dist/lib/summarizer/summarize.js +10 -3
- package/package.json +2 -3
- package/dist/commands/computer-actions.d.ts +0 -55
- package/dist/commands/computer-actions.js +0 -594
- package/dist/computer.d.ts +0 -2
- package/dist/computer.js +0 -7
- package/dist/lib/computer/actions.d.ts +0 -36
- package/dist/lib/computer/actions.js +0 -162
- package/dist/lib/computer/computer-rpc.d.ts +0 -39
- package/dist/lib/computer/computer-rpc.js +0 -447
- package/dist/lib/computer/des.d.ts +0 -1
- package/dist/lib/computer/des.js +0 -114
- package/dist/lib/computer/dispatch.d.ts +0 -10
- package/dist/lib/computer/dispatch.js +0 -133
- package/dist/lib/computer/download.d.ts +0 -54
- package/dist/lib/computer/download.js +0 -83
- package/dist/lib/computer/loop.d.ts +0 -62
- package/dist/lib/computer/loop.js +0 -98
- package/dist/lib/computer/model.d.ts +0 -44
- package/dist/lib/computer/model.js +0 -157
- package/dist/lib/computer/rfb-client.d.ts +0 -53
- package/dist/lib/computer/rfb-client.js +0 -562
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -189
- package/dist/lib/computer/ssh-tunnel.js +0 -584
|
@@ -134,7 +134,7 @@ export function findInPath(command, options = {}) {
|
|
|
134
134
|
if (process.platform !== 'win32')
|
|
135
135
|
fs.accessSync(full, fs.constants.X_OK);
|
|
136
136
|
const native = resolveNativeBinaryPath(command, full, options);
|
|
137
|
-
if (native)
|
|
137
|
+
if (native && (!options.accept || options.accept(native)))
|
|
138
138
|
return native;
|
|
139
139
|
}
|
|
140
140
|
catch {
|
|
@@ -11,7 +11,7 @@ export { shellQuote };
|
|
|
11
11
|
// The `ssh -L` tunnel spawn is shared with `agents computer --device`; it lives in
|
|
12
12
|
// the single ssh-tunnel helper. Calling it with no options preserves this
|
|
13
13
|
// driver's original foreground, stderr-captured behavior exactly.
|
|
14
|
-
import { startSSHTunnel } from '../../
|
|
14
|
+
import { startSSHTunnel } from '../../ssh-tunnel.js';
|
|
15
15
|
import { encodePwshBase64 } from '../../pwsh.js';
|
|
16
16
|
/** Lifecycle for a registry-driven tunnel vs a profile the daemon launched. */
|
|
17
17
|
export function persistRemoteLifecycle(persistRemote) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.ts — everything agents-cli knows that the standalone `computer`
|
|
3
|
+
* engine cannot work out for itself, serialized as one JSON object onto fd 3.
|
|
4
|
+
*
|
|
5
|
+
* This is the whole contract of the consumer half, and it is deliberately four
|
|
6
|
+
* fields wide. The engine accepts exactly this shape:
|
|
7
|
+
*
|
|
8
|
+
* version `1`. The engine matches on it; a field added later must be
|
|
9
|
+
* optional so an older engine keeps working.
|
|
10
|
+
* permissions which apps are allowed, derived from `Computer(<bundle-id>)`
|
|
11
|
+
* rules in the agents permissions resource layer. The engine
|
|
12
|
+
* would have to re-learn resource layering to compute this.
|
|
13
|
+
* peers which executables the daemon accepts a connection from.
|
|
14
|
+
* target the `--device <name>` target, resolved against the fleet:
|
|
15
|
+
* devices registry, ssh identity, platform. The engine matches
|
|
16
|
+
* its own `--device <alias>` against this instead of reading a
|
|
17
|
+
* registry it does not have. Absent for a local invocation.
|
|
18
|
+
* session who is acting — actor id and agent session — so an action
|
|
19
|
+
* lands in the right session history.
|
|
20
|
+
*
|
|
21
|
+
* WHAT IS DELIBERATELY NOT HERE. The transport (`COMPUTER_HELPER_TCP`,
|
|
22
|
+
* `COMPUTER_HELPER_VNC`, `COMPUTER_HELPER_SOCKET`), the remote helper's auth
|
|
23
|
+
* token, and the policy-file paths are the ENGINE's: it provisions the Windows
|
|
24
|
+
* helper, mints and stores the token, opens the `ssh -L` tunnel and hydrates its
|
|
25
|
+
* own transport from the state it wrote. agents-cli publishing a loopback
|
|
26
|
+
* endpoint here (or on `COMPUTER_HELPER_TCP`) would be a second, drifting copy
|
|
27
|
+
* of that answer — and a copy without the token, which the daemon rejects with
|
|
28
|
+
* `auth_failed`. Service-manager safety (launchd/systemd registration under a
|
|
29
|
+
* redirected HOME) is likewise the standalone's own: it inherits `HOME` and
|
|
30
|
+
* `AGENTS_REAL_HOME` and renders its own manifest, so agents-cli neither
|
|
31
|
+
* computes a label nor issues a verdict for it.
|
|
32
|
+
*
|
|
33
|
+
* The context is PUSHED (written and closed) rather than exposed as a callback,
|
|
34
|
+
* so the engine never re-enters agents-cli and there is exactly one direction of
|
|
35
|
+
* dependency.
|
|
36
|
+
*/
|
|
37
|
+
/** A `--device <name>` target, resolved against the fleet. */
|
|
38
|
+
export interface ComputerTargetContext {
|
|
39
|
+
/** The device name as the user typed it. */
|
|
40
|
+
alias: string;
|
|
41
|
+
/** `user@host`, already validated against ssh option injection. */
|
|
42
|
+
host: string;
|
|
43
|
+
user: string;
|
|
44
|
+
/** Bare host — the ssh-config Host name or address, without the user. */
|
|
45
|
+
hostname: string;
|
|
46
|
+
platform: string;
|
|
47
|
+
/** Per-device ssh identity flags, in argv order. Possibly empty. */
|
|
48
|
+
sshArgs: string[];
|
|
49
|
+
}
|
|
50
|
+
/** Who is acting, so the engine can stamp the action it reports back. */
|
|
51
|
+
export interface ComputerSessionContext {
|
|
52
|
+
sessionId?: string;
|
|
53
|
+
launchId?: string;
|
|
54
|
+
actor: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ComputerContext {
|
|
57
|
+
version: 1;
|
|
58
|
+
permissions?: {
|
|
59
|
+
allow: string[];
|
|
60
|
+
};
|
|
61
|
+
peers: {
|
|
62
|
+
allow: string[];
|
|
63
|
+
};
|
|
64
|
+
target?: ComputerTargetContext;
|
|
65
|
+
session: ComputerSessionContext;
|
|
66
|
+
}
|
|
67
|
+
export interface BuildContextOptions {
|
|
68
|
+
/** `--device <name>`, if given. */
|
|
69
|
+
device?: string;
|
|
70
|
+
/** Direct host targeting, which bypasses fleet resolution. */
|
|
71
|
+
host?: string;
|
|
72
|
+
/** Resolved path of the standalone executable, for the peer allow list. */
|
|
73
|
+
computerBin?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the context handed to the engine on fd 3.
|
|
77
|
+
*
|
|
78
|
+
* `device` resolution goes through the shared fleet resolver and keeps the
|
|
79
|
+
* Windows expectation the computer subsystem has always enforced — a
|
|
80
|
+
* `--device` pointing at a Mac gets the same refusal as before, from the fleet
|
|
81
|
+
* layer that can actually see the device's platform.
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildComputerContext(opts?: BuildContextOptions): Promise<ComputerContext>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.ts — everything agents-cli knows that the standalone `computer`
|
|
3
|
+
* engine cannot work out for itself, serialized as one JSON object onto fd 3.
|
|
4
|
+
*
|
|
5
|
+
* This is the whole contract of the consumer half, and it is deliberately four
|
|
6
|
+
* fields wide. The engine accepts exactly this shape:
|
|
7
|
+
*
|
|
8
|
+
* version `1`. The engine matches on it; a field added later must be
|
|
9
|
+
* optional so an older engine keeps working.
|
|
10
|
+
* permissions which apps are allowed, derived from `Computer(<bundle-id>)`
|
|
11
|
+
* rules in the agents permissions resource layer. The engine
|
|
12
|
+
* would have to re-learn resource layering to compute this.
|
|
13
|
+
* peers which executables the daemon accepts a connection from.
|
|
14
|
+
* target the `--device <name>` target, resolved against the fleet:
|
|
15
|
+
* devices registry, ssh identity, platform. The engine matches
|
|
16
|
+
* its own `--device <alias>` against this instead of reading a
|
|
17
|
+
* registry it does not have. Absent for a local invocation.
|
|
18
|
+
* session who is acting — actor id and agent session — so an action
|
|
19
|
+
* lands in the right session history.
|
|
20
|
+
*
|
|
21
|
+
* WHAT IS DELIBERATELY NOT HERE. The transport (`COMPUTER_HELPER_TCP`,
|
|
22
|
+
* `COMPUTER_HELPER_VNC`, `COMPUTER_HELPER_SOCKET`), the remote helper's auth
|
|
23
|
+
* token, and the policy-file paths are the ENGINE's: it provisions the Windows
|
|
24
|
+
* helper, mints and stores the token, opens the `ssh -L` tunnel and hydrates its
|
|
25
|
+
* own transport from the state it wrote. agents-cli publishing a loopback
|
|
26
|
+
* endpoint here (or on `COMPUTER_HELPER_TCP`) would be a second, drifting copy
|
|
27
|
+
* of that answer — and a copy without the token, which the daemon rejects with
|
|
28
|
+
* `auth_failed`. Service-manager safety (launchd/systemd registration under a
|
|
29
|
+
* redirected HOME) is likewise the standalone's own: it inherits `HOME` and
|
|
30
|
+
* `AGENTS_REAL_HOME` and renders its own manifest, so agents-cli neither
|
|
31
|
+
* computes a label nor issues a verdict for it.
|
|
32
|
+
*
|
|
33
|
+
* The context is PUSHED (written and closed) rather than exposed as a callback,
|
|
34
|
+
* so the engine never re-enters agents-cli and there is exactly one direction of
|
|
35
|
+
* dependency.
|
|
36
|
+
*/
|
|
37
|
+
import { resolveActor } from '../actor.js';
|
|
38
|
+
import { loadComputerAllowList, loadDefaultPeers } from './policy.js';
|
|
39
|
+
import { resolveRemoteDevice } from '../ssh-tunnel.js';
|
|
40
|
+
/**
|
|
41
|
+
* Which agent session is acting. Same precedence the admission cache used
|
|
42
|
+
* before the extraction — the harness-native id first, then agents' own.
|
|
43
|
+
*/
|
|
44
|
+
function agentSessionId(env = process.env) {
|
|
45
|
+
return env.CODEX_THREAD_ID
|
|
46
|
+
|| env.CLAUDE_CODE_SESSION_ID
|
|
47
|
+
|| env.CLAUDE_SESSION_ID
|
|
48
|
+
|| env.AGENTS_SESSION_ID
|
|
49
|
+
|| env.AGENT_SESSION_ID
|
|
50
|
+
|| env.AGENTS_RUN_ID
|
|
51
|
+
|| undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the context handed to the engine on fd 3.
|
|
55
|
+
*
|
|
56
|
+
* `device` resolution goes through the shared fleet resolver and keeps the
|
|
57
|
+
* Windows expectation the computer subsystem has always enforced — a
|
|
58
|
+
* `--device` pointing at a Mac gets the same refusal as before, from the fleet
|
|
59
|
+
* layer that can actually see the device's platform.
|
|
60
|
+
*/
|
|
61
|
+
export async function buildComputerContext(opts = {}) {
|
|
62
|
+
let target;
|
|
63
|
+
if (opts.device) {
|
|
64
|
+
const resolved = await resolveRemoteDevice(opts.device, {
|
|
65
|
+
expectPlatform: 'windows',
|
|
66
|
+
forWhat: '`agents computer --device` drives the Windows computer-helper daemon, so it',
|
|
67
|
+
});
|
|
68
|
+
target = {
|
|
69
|
+
alias: opts.device,
|
|
70
|
+
host: resolved.target,
|
|
71
|
+
user: resolved.user,
|
|
72
|
+
hostname: resolved.host,
|
|
73
|
+
platform: resolved.device.platform,
|
|
74
|
+
sshArgs: resolved.identityArgs,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
version: 1,
|
|
79
|
+
...(!opts.device && !opts.host && !process.env.COMPUTER_HELPER_TCP && !process.env.COMPUTER_HELPER_VNC
|
|
80
|
+
? { permissions: { allow: loadComputerAllowList() } } : {}),
|
|
81
|
+
peers: { allow: loadDefaultPeers({ computerBin: opts.computerBin }) },
|
|
82
|
+
// Spread rather than assigned: a local invocation must not ship a `target`
|
|
83
|
+
// key at all, so the engine never has to distinguish absent from null.
|
|
84
|
+
...(target ? { target } : {}),
|
|
85
|
+
session: {
|
|
86
|
+
sessionId: agentSessionId(),
|
|
87
|
+
launchId: process.env.AGENT_LAUNCH_ID,
|
|
88
|
+
actor: resolveActor().id,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Resolve Agents permission groups and caller identities for the standalone
|
|
2
|
+
* engine. The engine alone writes helper policy and peer files. */
|
|
3
|
+
export declare function loadComputerAllowList(): string[];
|
|
4
|
+
/**
|
|
5
|
+
* Default peer set: the standalone `computer` executable, this `agents` CLI's
|
|
6
|
+
* own runtime, plus Rush.app if it's installed. realpath() the symlink chain so
|
|
7
|
+
* we record the on-disk path the helper will see via proc_pidpath, not the shim
|
|
8
|
+
* path.
|
|
9
|
+
*
|
|
10
|
+
* The standalone's path is the one that changed with PHNX-4075: the daemon's
|
|
11
|
+
* caller is now the engine process, not this CLI. `agents`' own execPath stays
|
|
12
|
+
* on the list because the engine may be a `.js` bin run through this same
|
|
13
|
+
* runtime (`invocation()` in computer-client.ts), in which case proc_pidpath
|
|
14
|
+
* still reports the runtime.
|
|
15
|
+
*
|
|
16
|
+
* Why path-based instead of codesign-team-id? The agents CLI is unsigned
|
|
17
|
+
* today (npm distribution), and even if we sign Rush.app the team-id
|
|
18
|
+
* check would need a separate roundtrip. Path is concrete and fast; the
|
|
19
|
+
* daemon already runs as the user so anyone who can swap a binary at
|
|
20
|
+
* these paths can do worse via other means.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadDefaultPeers(opts?: {
|
|
23
|
+
computerBin?: string;
|
|
24
|
+
}): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Parse a `host:port` VNC endpoint, defaulting the port to 5901. Pure.
|
|
27
|
+
*
|
|
28
|
+
* Kept on the consumer side because the `--vnc` FLAG is parsed here — the
|
|
29
|
+
* platform gate has to know whether a remote desktop was named before the
|
|
30
|
+
* engine is ever spawned (see `shouldBlockOffPlatform`). The RFB protocol
|
|
31
|
+
* implementation itself went to the engine.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseVncEndpoint(raw: string | undefined): {
|
|
34
|
+
host: string;
|
|
35
|
+
port: number;
|
|
36
|
+
} | null;
|
|
37
|
+
export declare function resolveTcpEndpoint(): {
|
|
38
|
+
host: string;
|
|
39
|
+
port: number;
|
|
40
|
+
token: string | null;
|
|
41
|
+
} | null;
|
|
42
|
+
export declare function resolveVncEndpoint(): {
|
|
43
|
+
host: string;
|
|
44
|
+
port: number;
|
|
45
|
+
password: string;
|
|
46
|
+
} | null;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/** Resolve Agents permission groups and caller identities for the standalone
|
|
2
|
+
* engine. The engine alone writes helper policy and peer files. */
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { getUserPermissionsDir, getPermissionsDir } from '../state.js';
|
|
6
|
+
// Walk all permission group YAMLs (user dir wins on name collision) and
|
|
7
|
+
// collect Computer(<bundle-id>) patterns from each group's `allow:` list.
|
|
8
|
+
// Returns distinct bundle ids. Line-by-line regex extraction matches
|
|
9
|
+
// buildPermissionsFromGroups: YAML parsers stumble on the nested quotes in
|
|
10
|
+
// some rule values, but the strict pattern below catches our shape cleanly.
|
|
11
|
+
export function loadComputerAllowList() {
|
|
12
|
+
const seenFiles = new Set();
|
|
13
|
+
const allowed = new Set();
|
|
14
|
+
for (const baseDir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
15
|
+
const groupsDir = path.join(baseDir, 'groups');
|
|
16
|
+
if (!fs.existsSync(groupsDir))
|
|
17
|
+
continue;
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = fs.readdirSync(groupsDir, { withFileTypes: true });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (!entry.isFile())
|
|
27
|
+
continue;
|
|
28
|
+
if (!entry.name.endsWith('.yml') && !entry.name.endsWith('.yaml'))
|
|
29
|
+
continue;
|
|
30
|
+
// User dir wins on filename collision.
|
|
31
|
+
const stem = entry.name.replace(/\.(yaml|yml)$/, '');
|
|
32
|
+
if (seenFiles.has(stem))
|
|
33
|
+
continue;
|
|
34
|
+
seenFiles.add(stem);
|
|
35
|
+
const filePath = path.join(groupsDir, entry.name);
|
|
36
|
+
let content;
|
|
37
|
+
try {
|
|
38
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// Strict regex: optional whitespace, dash, quoted Computer(<id>).
|
|
44
|
+
// Only honors `allow:` lines — `deny:` Computer patterns would be a
|
|
45
|
+
// contradiction (everything is deny-by-default already).
|
|
46
|
+
let inAllow = false;
|
|
47
|
+
for (const rawLine of content.split('\n')) {
|
|
48
|
+
const line = rawLine.replace(/\r$/, '');
|
|
49
|
+
const sectionMatch = line.match(/^(\w+)\s*:\s*$/);
|
|
50
|
+
if (sectionMatch) {
|
|
51
|
+
inAllow = sectionMatch[1] === 'allow';
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!inAllow)
|
|
55
|
+
continue;
|
|
56
|
+
const ruleMatch = line.match(/^\s*-\s*"Computer\(([^)]+)\)"\s*$/);
|
|
57
|
+
if (ruleMatch) {
|
|
58
|
+
const bundleId = ruleMatch[1].trim();
|
|
59
|
+
if (bundleId.length > 0)
|
|
60
|
+
allowed.add(bundleId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return [...allowed].sort();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Default peer set: the standalone `computer` executable, this `agents` CLI's
|
|
69
|
+
* own runtime, plus Rush.app if it's installed. realpath() the symlink chain so
|
|
70
|
+
* we record the on-disk path the helper will see via proc_pidpath, not the shim
|
|
71
|
+
* path.
|
|
72
|
+
*
|
|
73
|
+
* The standalone's path is the one that changed with PHNX-4075: the daemon's
|
|
74
|
+
* caller is now the engine process, not this CLI. `agents`' own execPath stays
|
|
75
|
+
* on the list because the engine may be a `.js` bin run through this same
|
|
76
|
+
* runtime (`invocation()` in computer-client.ts), in which case proc_pidpath
|
|
77
|
+
* still reports the runtime.
|
|
78
|
+
*
|
|
79
|
+
* Why path-based instead of codesign-team-id? The agents CLI is unsigned
|
|
80
|
+
* today (npm distribution), and even if we sign Rush.app the team-id
|
|
81
|
+
* check would need a separate roundtrip. Path is concrete and fast; the
|
|
82
|
+
* daemon already runs as the user so anyone who can swap a binary at
|
|
83
|
+
* these paths can do worse via other means.
|
|
84
|
+
*/
|
|
85
|
+
export function loadDefaultPeers(opts = {}) {
|
|
86
|
+
const out = new Set();
|
|
87
|
+
const add = (p) => {
|
|
88
|
+
try {
|
|
89
|
+
out.add(fs.realpathSync(p));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
out.add(p);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
// The standalone engine — the process that actually opens the socket now.
|
|
96
|
+
if (opts.computerBin)
|
|
97
|
+
add(opts.computerBin);
|
|
98
|
+
// The runtime currently running this CLI. Still a possible proc_pidpath when
|
|
99
|
+
// the engine is a .js bin executed through it.
|
|
100
|
+
if (process.execPath)
|
|
101
|
+
add(process.execPath);
|
|
102
|
+
// Rush.app — the consumer Electron client. Both the helper-binary and
|
|
103
|
+
// the main app binary are possible callers depending on how Rush wires
|
|
104
|
+
// the RPC client.
|
|
105
|
+
const rushCandidates = [
|
|
106
|
+
'/Applications/Rush.app/Contents/MacOS/Rush',
|
|
107
|
+
'/Applications/Rush.app/Contents/MacOS/Electron',
|
|
108
|
+
];
|
|
109
|
+
for (const p of rushCandidates) {
|
|
110
|
+
if (fs.existsSync(p))
|
|
111
|
+
add(p);
|
|
112
|
+
}
|
|
113
|
+
return [...out].sort();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse a `host:port` VNC endpoint, defaulting the port to 5901. Pure.
|
|
117
|
+
*
|
|
118
|
+
* Kept on the consumer side because the `--vnc` FLAG is parsed here — the
|
|
119
|
+
* platform gate has to know whether a remote desktop was named before the
|
|
120
|
+
* engine is ever spawned (see `shouldBlockOffPlatform`). The RFB protocol
|
|
121
|
+
* implementation itself went to the engine.
|
|
122
|
+
*/
|
|
123
|
+
export function parseVncEndpoint(raw) {
|
|
124
|
+
if (!raw || raw.length === 0)
|
|
125
|
+
return null;
|
|
126
|
+
const idx = raw.lastIndexOf(':');
|
|
127
|
+
const host = idx >= 0 ? raw.slice(0, idx) : raw;
|
|
128
|
+
const portStr = idx >= 0 ? raw.slice(idx + 1) : '5901';
|
|
129
|
+
const port = Number(portStr);
|
|
130
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
131
|
+
return null;
|
|
132
|
+
return { host: host || '127.0.0.1', port };
|
|
133
|
+
}
|
|
134
|
+
// Resolve the TCP endpoint for a remote daemon (the Windows helper), if
|
|
135
|
+
// configured. That helper binds loopback TCP and is reached over an `ssh -L`
|
|
136
|
+
// tunnel, so the endpoint is a local forwarded port. COMPUTER_HELPER_TCP is
|
|
137
|
+
// "host:port" (host defaults to 127.0.0.1); COMPUTER_HELPER_TOKEN is the shared
|
|
138
|
+
// secret sent in the first `auth` frame.
|
|
139
|
+
export function resolveTcpEndpoint() {
|
|
140
|
+
const raw = process.env.COMPUTER_HELPER_TCP;
|
|
141
|
+
if (!raw || raw.length === 0)
|
|
142
|
+
return null;
|
|
143
|
+
const [hostPart, portPart] = raw.includes(':') ? raw.split(':') : ['127.0.0.1', raw];
|
|
144
|
+
const port = Number(portPart);
|
|
145
|
+
if (!Number.isInteger(port) || port <= 0)
|
|
146
|
+
return null;
|
|
147
|
+
const token = process.env.COMPUTER_HELPER_TOKEN;
|
|
148
|
+
return { host: hostPart || '127.0.0.1', port, token: token && token.length > 0 ? token : null };
|
|
149
|
+
}
|
|
150
|
+
// Resolve the VNC/RFB endpoint for driving a remote GUI desktop over the RFB
|
|
151
|
+
// protocol (an x11vnc/Xvnc server — e.g. a headless Linux desktop or an LXD
|
|
152
|
+
// container exposing x11vnc on the host's Tailscale IP). COMPUTER_HELPER_VNC is
|
|
153
|
+
// "host:port" (port defaults to 5901); COMPUTER_HELPER_VNC_PASSWORD is the VNC
|
|
154
|
+
// password.
|
|
155
|
+
export function resolveVncEndpoint() {
|
|
156
|
+
const parsed = parseVncEndpoint(process.env.COMPUTER_HELPER_VNC);
|
|
157
|
+
if (!parsed)
|
|
158
|
+
return null;
|
|
159
|
+
return { ...parsed, password: process.env.COMPUTER_HELPER_VNC_PASSWORD ?? '' };
|
|
160
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* record.ts — turn the engine's NDJSON action events into agents-cli's own
|
|
3
|
+
* records: a feed event and a row in the computer-session history that
|
|
4
|
+
* `agents computer sessions` and `agents sessions --computer` read.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS STAYS HERE. The feed, the actor registry, and `sessions.db` are
|
|
7
|
+
* agents-cli state. Handing the standalone engine a writer for all three would
|
|
8
|
+
* have made it a second author of the session index — precisely the
|
|
9
|
+
* "one engine, one executor" rule the repo holds elsewhere. Instead the engine
|
|
10
|
+
* reports what it did on fd 4 and agents-cli, which owns those stores, records it.
|
|
11
|
+
*
|
|
12
|
+
* Before PHNX-4075 this was `emitComputerAction`, called inline by each verb in
|
|
13
|
+
* the same process. The behavior is unchanged; only the trigger moved from a
|
|
14
|
+
* function call to a line on a pipe.
|
|
15
|
+
*
|
|
16
|
+
* WHAT THE ENGINE OWNS, AND IS NOT REWRITTEN HERE: the action's identity. The
|
|
17
|
+
* engine mints the `invocationId` that groups a whole run into one session row,
|
|
18
|
+
* names the `host` it drove, and echoes back the session/launch/actor it was
|
|
19
|
+
* handed. Re-deriving any of those from this process would describe the CLI that
|
|
20
|
+
* spawned the engine rather than the run that happened — and for `--device` the
|
|
21
|
+
* two genuinely differ.
|
|
22
|
+
*/
|
|
23
|
+
import type { ComputerActionEvent } from '../computer-client.js';
|
|
24
|
+
/**
|
|
25
|
+
* Fallback grouping id for an engine that reported no `invocationId` of its own.
|
|
26
|
+
* One per `agents computer` process, so such a run still collapses to a single
|
|
27
|
+
* session row instead of N unrelated ones.
|
|
28
|
+
*/
|
|
29
|
+
export declare const COMPUTER_INVOCATION_ID: `${string}-${string}-${string}-${string}-${string}`;
|
|
30
|
+
/**
|
|
31
|
+
* Record one action the engine performed. Never throws: the action already
|
|
32
|
+
* happened and already reported its own success or failure on the engine's
|
|
33
|
+
* stderr, so a bookkeeping failure must not turn a successful click into a
|
|
34
|
+
* failed command.
|
|
35
|
+
*/
|
|
36
|
+
export declare function recordComputerAction(event: ComputerActionEvent, opts?: {
|
|
37
|
+
device?: string;
|
|
38
|
+
}): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* record.ts — turn the engine's NDJSON action events into agents-cli's own
|
|
3
|
+
* records: a feed event and a row in the computer-session history that
|
|
4
|
+
* `agents computer sessions` and `agents sessions --computer` read.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS STAYS HERE. The feed, the actor registry, and `sessions.db` are
|
|
7
|
+
* agents-cli state. Handing the standalone engine a writer for all three would
|
|
8
|
+
* have made it a second author of the session index — precisely the
|
|
9
|
+
* "one engine, one executor" rule the repo holds elsewhere. Instead the engine
|
|
10
|
+
* reports what it did on fd 4 and agents-cli, which owns those stores, records it.
|
|
11
|
+
*
|
|
12
|
+
* Before PHNX-4075 this was `emitComputerAction`, called inline by each verb in
|
|
13
|
+
* the same process. The behavior is unchanged; only the trigger moved from a
|
|
14
|
+
* function call to a line on a pipe.
|
|
15
|
+
*
|
|
16
|
+
* WHAT THE ENGINE OWNS, AND IS NOT REWRITTEN HERE: the action's identity. The
|
|
17
|
+
* engine mints the `invocationId` that groups a whole run into one session row,
|
|
18
|
+
* names the `host` it drove, and echoes back the session/launch/actor it was
|
|
19
|
+
* handed. Re-deriving any of those from this process would describe the CLI that
|
|
20
|
+
* spawned the engine rather than the run that happened — and for `--device` the
|
|
21
|
+
* two genuinely differ.
|
|
22
|
+
*/
|
|
23
|
+
import { randomUUID } from 'node:crypto';
|
|
24
|
+
import { emit as emitEvent } from '../feed/events.js';
|
|
25
|
+
import { recordComputerSession } from '../session/db.js';
|
|
26
|
+
import { resolveActor } from '../actor.js';
|
|
27
|
+
import { truncate } from '../feed/events.js';
|
|
28
|
+
import { TASK_PREVIEW_MAX_CHARS } from './sessions-list.js';
|
|
29
|
+
/**
|
|
30
|
+
* Fallback grouping id for an engine that reported no `invocationId` of its own.
|
|
31
|
+
* One per `agents computer` process, so such a run still collapses to a single
|
|
32
|
+
* session row instead of N unrelated ones.
|
|
33
|
+
*/
|
|
34
|
+
export const COMPUTER_INVOCATION_ID = randomUUID();
|
|
35
|
+
/**
|
|
36
|
+
* Record one action the engine performed. Never throws: the action already
|
|
37
|
+
* happened and already reported its own success or failure on the engine's
|
|
38
|
+
* stderr, so a bookkeeping failure must not turn a successful click into a
|
|
39
|
+
* failed command.
|
|
40
|
+
*/
|
|
41
|
+
export function recordComputerAction(event, opts = {}) {
|
|
42
|
+
const { event: _kind, command, invocationId,
|
|
43
|
+
// The ledger's `pid` is the EMITTING process's by construction (events.ts
|
|
44
|
+
// stamps `process.pid` over any payload value), so the engine's own pid
|
|
45
|
+
// cannot be carried in it. Dropped rather than passed in to be silently
|
|
46
|
+
// overwritten.
|
|
47
|
+
pid: _enginePid, host, sessionId, launchId, actor, ...rest } = event;
|
|
48
|
+
const runId = invocationId || COMPUTER_INVOCATION_ID;
|
|
49
|
+
// The driven machine. `host` is the field `sessions-list.ts` reads for a
|
|
50
|
+
// remote run; `opts.device` is the fallback for an engine that drove the
|
|
51
|
+
// device this CLI resolved but did not stamp it.
|
|
52
|
+
const drivenHost = host ?? opts.device;
|
|
53
|
+
// The task preview is bounded HERE, not upstream. agents-cli owns the ledger
|
|
54
|
+
// and therefore its retention/privacy rule (see sessions-list.ts): an engine
|
|
55
|
+
// that reported a full `--task` string must not be able to write an unbounded
|
|
56
|
+
// one into the session index.
|
|
57
|
+
const extra = typeof rest.task === 'string'
|
|
58
|
+
? { ...rest, task: truncate(rest.task, TASK_PREVIEW_MAX_CHARS) }
|
|
59
|
+
: rest;
|
|
60
|
+
try {
|
|
61
|
+
emitEvent('computer.action', {
|
|
62
|
+
command,
|
|
63
|
+
invocationId: runId,
|
|
64
|
+
...(drivenHost ? { host: drivenHost } : {}),
|
|
65
|
+
...(sessionId ? { sessionId } : {}),
|
|
66
|
+
...(launchId ? { launchId } : {}),
|
|
67
|
+
...extra,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Feed emission is best-effort; the action is already done.
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
recordComputerSession({
|
|
75
|
+
invocationId: runId,
|
|
76
|
+
sessionId: sessionId ?? process.env.AGENT_SESSION_ID ?? process.env.AGENTS_SESSION_ID,
|
|
77
|
+
launchId: launchId ?? process.env.AGENT_LAUNCH_ID,
|
|
78
|
+
actor: actor ?? resolveActor().id,
|
|
79
|
+
actionCount: 1,
|
|
80
|
+
taskPreview: typeof extra.task === 'string' ? extra.task : undefined,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Recording is best-effort; the action and its event are already done.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Read-only task/run history over the `computer.action` event ledger
|
|
3
3
|
* (`~/.agents/.history/events/YYYY-MM-DD/events.jsonl`, see `../events.ts`) —
|
|
4
4
|
* the durable, already-existing audit log every `agents computer <verb>`
|
|
5
|
-
* invocation
|
|
6
|
-
* the
|
|
7
|
-
*
|
|
5
|
+
* invocation writes through `computer/record.ts`'s `recordComputerAction()`,
|
|
6
|
+
* fed by the action events the standalone engine streams back (PHNX-4075).
|
|
7
|
+
* Backs both `agents computer sessions` and the
|
|
8
8
|
* `agents sessions --computer` alias.
|
|
9
9
|
*
|
|
10
10
|
* There is no separate capture directory the way browser tasks have
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* the ledger's 7 days, and one row per CLI process would otherwise grow without
|
|
25
25
|
* limit. It is metadata only. Nothing sensitive is persisted: `type` /
|
|
26
26
|
* `type-text` events already carry only `textLength`, never the typed text
|
|
27
|
-
* (see `
|
|
27
|
+
* (see `computer/record.ts` `recordComputerAction`) — the
|
|
28
28
|
* mission this module fulfils changes NONE of that. A `run --task`
|
|
29
29
|
* description is the agent's OWN instruction, not typed-into-a-target-app
|
|
30
30
|
* content (the same class of thing `agents sessions` already stores
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
* deliberate exception to (not a bypass of) the automatic prompt-redaction
|
|
36
36
|
* path in `events.ts` `sanitizePayload`.
|
|
37
37
|
*
|
|
38
|
-
* Grouping key: `
|
|
38
|
+
* Grouping key: `recordComputerAction()` stamps one random `invocationId` for
|
|
39
39
|
* the lifetime of the emitting CLI process. The event's own `pid` field is the emitting
|
|
40
40
|
* CLI PROCESS's pid, never the target app's (that's `targetPid` — see
|
|
41
|
-
* `computer
|
|
41
|
+
* `computer/record.ts` `recordComputerAction`, and its `#11` test guarding
|
|
42
42
|
* this). One `agents computer <verb>` invocation is one process, and
|
|
43
43
|
* `computer run`'s whole embedded observe/act/verify loop is ALSO one
|
|
44
44
|
* process. Grouping by `invocationId` gives exactly one row per CLI invocation without
|