@phnx-labs/agents-cli 1.22.103 → 1.22.104
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 +27 -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/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
|
@@ -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
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* computer-client.ts — the ONE process client through which agents-cli talks to
|
|
3
|
+
* the standalone `computer` CLI (PHNX-4075).
|
|
4
|
+
*
|
|
5
|
+
* This is the agents-owned half of the computer extraction, and it is
|
|
6
|
+
* deliberately small. agents-cli no longer carries a helper daemon, an RPC
|
|
7
|
+
* transport, an element cache, an RFB client, or an autonomous loop — the
|
|
8
|
+
* standalone engine owns all of it, exactly as `secrets` took the keychain
|
|
9
|
+
* engine (PHNX-3989) and `sessions` took the transcript engine (PHNX-4012).
|
|
10
|
+
* What stays here is what only the fleet CLI can know: which apps the
|
|
11
|
+
* permissions layer allows, which device a `--device` name resolves to, who the
|
|
12
|
+
* acting session is, and where an action must be recorded.
|
|
13
|
+
*
|
|
14
|
+
* THERE IS NO FALLBACK. A missing executable throws `COMPUTER_BIN_MISSING` with
|
|
15
|
+
* install guidance (DIST-1) rather than silently driving a bundled engine —
|
|
16
|
+
* agents-cli has none to drive, and a fallback would re-couple the two release
|
|
17
|
+
* trains this extraction exists to separate.
|
|
18
|
+
*
|
|
19
|
+
* Transport — inherited-fd passthrough, not request/response:
|
|
20
|
+
*
|
|
21
|
+
* The engine's ENVIRONMENT is inherited verbatim — no overlay. Transport
|
|
22
|
+
* selection (`COMPUTER_HELPER_TCP`, `COMPUTER_HELPER_VNC`,
|
|
23
|
+
* `COMPUTER_HELPER_SOCKET`) is the engine's: it opens the `--device` tunnel and
|
|
24
|
+
* hydrates its own endpoint AND the auth token that goes with it. Publishing a
|
|
25
|
+
* bare endpoint from here would hand the daemon a connection it then rejects
|
|
26
|
+
* with `auth_failed`.
|
|
27
|
+
*
|
|
28
|
+
* - stdio 0/1/2 are INHERITED. The engine owns the user's terminal: its
|
|
29
|
+
* stdout is the command's stdout, its `--json` is the command's `--json`,
|
|
30
|
+
* its prompts reach a real tty. agents-cli never re-formats engine output,
|
|
31
|
+
* which is what keeps the surface honest as the engine evolves.
|
|
32
|
+
* - fd 3 (`COMPUTER_CONTEXT_FD`) carries ONE JSON object — the consumer
|
|
33
|
+
* context built by `lib/computer/context.ts` — written and closed
|
|
34
|
+
* immediately, so the engine reads to EOF and proceeds.
|
|
35
|
+
* - fd 4 (`COMPUTER_EVENTS_FD`) carries NDJSON action events back: one JSON
|
|
36
|
+
* object per line, each an action the engine actually performed. agents-cli
|
|
37
|
+
* turns those into feed events and `sessions --computer` history
|
|
38
|
+
* (`lib/computer/record.ts`). The engine may emit none; it must never block
|
|
39
|
+
* on this pipe.
|
|
40
|
+
*
|
|
41
|
+
* Both fds are anonymous pipes on the child's side, the same shape
|
|
42
|
+
* `secrets-client.ts` settled on after a named FIFO wedged macOS reads. The
|
|
43
|
+
* context is pushed rather than pulled so the engine needs no callback into
|
|
44
|
+
* agents-cli — one direction each way, no reentrancy.
|
|
45
|
+
*/
|
|
46
|
+
/** fd the engine reads its one-shot JSON context from. */
|
|
47
|
+
export declare const COMPUTER_CONTEXT_FD = 3;
|
|
48
|
+
/** fd the engine writes NDJSON action events to. */
|
|
49
|
+
export declare const COMPUTER_EVENTS_FD = 4;
|
|
50
|
+
export declare class ComputerClientError extends Error {
|
|
51
|
+
code: string;
|
|
52
|
+
constructor(code: string, message: string);
|
|
53
|
+
}
|
|
54
|
+
export declare function isComputerClientError(err: unknown): err is ComputerClientError;
|
|
55
|
+
export declare function isStandaloneComputer(bin: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the standalone executable. `COMPUTER_BIN` wins so a dev build can be
|
|
58
|
+
* driven without touching PATH.
|
|
59
|
+
*
|
|
60
|
+
* Resolution uses `findInPath`, which skips `~/.agents/.cache/shims`. That skip
|
|
61
|
+
* is load-bearing here for the same reason it is in `sessions-client.ts`: a
|
|
62
|
+
* leftover `computer` alias shim execs `agents computer`, and resolving it would
|
|
63
|
+
* recurse into this process (the 1.22.85 secrets fork bomb, agi-cli#3532).
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveComputerBin(): string;
|
|
66
|
+
/** A `.js` bin is run through this runtime; a real executable is exec'd directly. */
|
|
67
|
+
export declare function invocation(bin: string): {
|
|
68
|
+
command: string;
|
|
69
|
+
prefix: string[];
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* One action the engine performed, as it appears on the NDJSON events fd.
|
|
73
|
+
*
|
|
74
|
+
* This is the engine's wire shape, not a translation of it: the engine emits
|
|
75
|
+
* `{event: "computer.action", command, invocationId, pid, targetPid, bundle,
|
|
76
|
+
* host, task, sessionId, launchId, actor}`. `command` — not `verb` — is the
|
|
77
|
+
* field that names the action, and it is what marks a line as an action event.
|
|
78
|
+
*/
|
|
79
|
+
export interface ComputerActionEvent {
|
|
80
|
+
/** Always `computer.action` on this stream. */
|
|
81
|
+
event?: string;
|
|
82
|
+
/** The verb the engine ran (`click`, `type`, `screenshot`, …). */
|
|
83
|
+
command: string;
|
|
84
|
+
/** The engine's own id for this run — the grouping key for a session row. */
|
|
85
|
+
invocationId?: string;
|
|
86
|
+
/** The engine process's pid. */
|
|
87
|
+
pid?: number;
|
|
88
|
+
/** pid of the app the action targeted, when the engine resolved one. */
|
|
89
|
+
targetPid?: number;
|
|
90
|
+
/** Bundle id / app identifier the action targeted. */
|
|
91
|
+
bundle?: string;
|
|
92
|
+
/** The driven device for a `--device` invocation; absent when local. */
|
|
93
|
+
host?: string;
|
|
94
|
+
/** `run --task` description, only on the task marker. */
|
|
95
|
+
task?: string;
|
|
96
|
+
/** Identity, echoed back from the context this CLI handed the engine. */
|
|
97
|
+
sessionId?: string;
|
|
98
|
+
launchId?: string;
|
|
99
|
+
actor?: string;
|
|
100
|
+
/** Free-form detail the engine attaches (coordinates, text length, …). */
|
|
101
|
+
[key: string]: unknown;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Split a growing buffer into complete NDJSON lines. Pure so the framing rules —
|
|
105
|
+
* blank lines skipped, a non-JSON line dropped rather than crashing the CLI, a
|
|
106
|
+
* trailing partial line carried forward — are unit-testable without a spawn.
|
|
107
|
+
*
|
|
108
|
+
* A malformed line is dropped, not thrown: these events are telemetry riding
|
|
109
|
+
* alongside a user-visible action that already happened. Failing the command
|
|
110
|
+
* because its receipt was unreadable would be strictly worse than losing the
|
|
111
|
+
* receipt. The action itself already failed loud on its own channel if it failed.
|
|
112
|
+
*/
|
|
113
|
+
export declare function parseEventLines(buffer: string): {
|
|
114
|
+
events: ComputerActionEvent[];
|
|
115
|
+
rest: string;
|
|
116
|
+
};
|
|
117
|
+
export interface RunComputerOptions {
|
|
118
|
+
/** argv handed to the standalone, after the program name. */
|
|
119
|
+
argv: string[];
|
|
120
|
+
/** The consumer context serialized onto fd 3. */
|
|
121
|
+
context: unknown;
|
|
122
|
+
/** Called once per action event the engine reports on fd 4. */
|
|
123
|
+
onEvent?: (event: ComputerActionEvent) => void;
|
|
124
|
+
/**
|
|
125
|
+
* Capture the engine's stdout instead of inheriting the terminal.
|
|
126
|
+
*
|
|
127
|
+
* Used only where agents-cli must READ an answer rather than show it — the
|
|
128
|
+
* `agents setup computer` wizard polling `status --json` for trust. Verbs
|
|
129
|
+
* never capture: re-printing engine output would make agents-cli a formatter
|
|
130
|
+
* for a surface it no longer owns.
|
|
131
|
+
*/
|
|
132
|
+
capture?: boolean;
|
|
133
|
+
}
|
|
134
|
+
export interface RunComputerResult {
|
|
135
|
+
exitCode: number;
|
|
136
|
+
/** Engine stdout, only when `capture` was set. */
|
|
137
|
+
stdout: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Run the standalone engine with the consumer context on fd 3 and the action
|
|
141
|
+
* event stream on fd 4. Resolves with the engine's exit code; the caller
|
|
142
|
+
* propagates it so `agents computer` exits exactly as the engine did.
|
|
143
|
+
*
|
|
144
|
+
* Throws `COMPUTER_BIN_MISSING` when the standalone is not installed. Every
|
|
145
|
+
* other failure is the engine's own, reported on the inherited stderr.
|
|
146
|
+
*/
|
|
147
|
+
export declare function runComputer(opts: RunComputerOptions): Promise<RunComputerResult>;
|
|
148
|
+
/** Test seam: drop the memoized bin so PATH fixtures can re-resolve. */
|
|
149
|
+
export declare function _resetComputerClientForTest(): void;
|
|
150
|
+
export declare const COMPUTER_INSTALL_HINT = "npm i -g @phnx-labs/computer-cli";
|