@phnx-labs/agents-cli 1.22.102 → 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 +31 -0
- package/README.md +1 -1
- package/dist/bootstrap.js +15 -0
- 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 +9 -9
- 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/menubar/install-menubar.d.ts +56 -0
- package/dist/lib/menubar/install-menubar.js +133 -17
- package/dist/lib/menubar/resolve-version.d.ts +82 -0
- package/dist/lib/menubar/resolve-version.js +133 -0
- package/dist/lib/profiles.js +13 -2
- package/dist/lib/secrets-client.d.ts +23 -3
- package/dist/lib/secrets-client.js +57 -57
- package/dist/lib/self-heal/checks/menubar-helper.d.ts +2 -0
- package/dist/lib/self-heal/checks/menubar-helper.js +21 -0
- package/dist/lib/self-heal/registry.js +3 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- 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
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
* is load-bearing: the standalone wraps fd 3 in a `net.Socket`, and a Socket
|
|
26
26
|
* over a NAMED FIFO reads the request but never fires EOF on macOS, so the
|
|
27
27
|
* older FIFO wiring hung the read loop for the full timeout. Bounded to
|
|
28
|
-
* `SYNC_SERVE_TIMEOUT_MS`
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* `SYNC_SERVE_TIMEOUT_MS` (30s — sized for a cold Node boot of the
|
|
29
|
+
* standalone on a loaded box, see the constant) so a broken standalone
|
|
30
|
+
* fails before the server's 60s deadline. POSIX only; Windows fails loud
|
|
31
|
+
* pointing at the async path.
|
|
31
32
|
*
|
|
32
33
|
* State root (MIG-1): the standalone selects its state root from `SECRETS_HOME`.
|
|
33
34
|
* agents-cli points it at the user agents dir (`~/.agents`) by default so the
|
|
@@ -59,20 +60,25 @@ const MAX_PROTOCOL_BYTES = 8 * 1024 * 1024;
|
|
|
59
60
|
/** Just over the server's own 60s deadline, so the server times out first. */
|
|
60
61
|
const SERVE_TIMEOUT_MS = 65_000;
|
|
61
62
|
/**
|
|
62
|
-
* The synchronous path
|
|
63
|
-
* the account-catalog rows, and
|
|
64
|
-
* `agents run`
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
63
|
+
* The synchronous path serves the surfaces that resolve secrets before they can
|
|
64
|
+
* continue — `agents view`, the account-catalog rows, and the account listing and
|
|
65
|
+
* setup-token read on the `agents run` launch path. Those must never hang for the
|
|
66
|
+
* standalone's own 60s deadline (the exact hang PHNX-3989 hit when the child ran
|
|
67
|
+
* under Bun), so the sync serve carries a hard bound below it instead of the
|
|
68
|
+
* async path's 65s. The bound covers a COLD PROCESS, not just the operation: every
|
|
69
|
+
* request spawns `secrets __serve` afresh, so it pays a Node boot plus the
|
|
70
|
+
* standalone's module load before the tens-of-milliseconds op runs: ~0.3s on a
|
|
71
|
+
* quiet box, 0.4–2.6s and occasionally more on a desktop at load average ~100
|
|
72
|
+
* (measured 2026-09-12), where the previous 3s bound turned load into a failed
|
|
73
|
+
* launch (`secrets request failed: spawnSync sh ETIMEDOUT` from `agents run`).
|
|
74
|
+
* 30s absorbs that boot under load and still fails a broken or wedged standalone
|
|
75
|
+
* well before its 60s self-deadline. It is NOT a fallback to the embedded engine
|
|
76
|
+
* (DIST-1); the standalone stays the only implementation. Exported so the
|
|
77
|
+
* real-standalone test can assert a round-trip beats the bound.
|
|
74
78
|
*/
|
|
75
|
-
const SYNC_SERVE_TIMEOUT_MS =
|
|
79
|
+
export const SYNC_SERVE_TIMEOUT_MS = 30_000;
|
|
80
|
+
/** Test seam: a hang test plants a never-answering standalone and must not wait 30s. */
|
|
81
|
+
let syncServeTimeoutMs = SYNC_SERVE_TIMEOUT_MS;
|
|
76
82
|
/** Serialize `Map`s the way the server's `decodeWire` expects to receive them. */
|
|
77
83
|
export function encodeWire(value) {
|
|
78
84
|
if (value instanceof Map) {
|
|
@@ -291,7 +297,18 @@ function parseResponse(raw) {
|
|
|
291
297
|
catch {
|
|
292
298
|
throw new SecretsClientError('INVALID_RESPONSE', `secrets returned a non-JSON response${describeNonJson(text)}`);
|
|
293
299
|
}
|
|
294
|
-
if (!parsed ||
|
|
300
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
301
|
+
throw new SecretsClientError('INVALID_RESPONSE', 'secrets returned a malformed response envelope');
|
|
302
|
+
}
|
|
303
|
+
// Every reply carries the protocol it was written against, so version
|
|
304
|
+
// negotiation rides the answer itself — there is no separate handshake spawn
|
|
305
|
+
// to pay a cold Node boot for (PHNX-4082). A mismatch is the standalone being
|
|
306
|
+
// too old or too new, which is actionable; anything else is a broken envelope.
|
|
307
|
+
if (typeof parsed.v === 'number' && parsed.v !== PROTOCOL_VERSION) {
|
|
308
|
+
throw new SecretsClientError('PROTOCOL_UNSUPPORTED', `secrets speaks protocol ${String(parsed.v)}; this agents-cli needs ${PROTOCOL_VERSION}. ` +
|
|
309
|
+
'Update the standalone CLI (npm i -g @phnx-labs/secrets-cli).');
|
|
310
|
+
}
|
|
311
|
+
if (parsed.v !== PROTOCOL_VERSION || typeof parsed.id !== 'string') {
|
|
295
312
|
throw new SecretsClientError('INVALID_RESPONSE', 'secrets returned a malformed response envelope');
|
|
296
313
|
}
|
|
297
314
|
if (parsed.ok)
|
|
@@ -413,13 +430,25 @@ function serveOnceSync(op, args, context) {
|
|
|
413
430
|
input: request,
|
|
414
431
|
stdio: ['pipe', 'pipe', 'inherit'],
|
|
415
432
|
env: buildServeEnv(),
|
|
416
|
-
timeout:
|
|
433
|
+
timeout: syncServeTimeoutMs,
|
|
417
434
|
maxBuffer: MAX_PROTOCOL_BYTES + 4096,
|
|
418
435
|
});
|
|
419
436
|
if (result.error) {
|
|
420
437
|
const err = result.error;
|
|
421
|
-
|
|
422
|
-
|
|
438
|
+
if (err.code === 'ETIMEDOUT') {
|
|
439
|
+
throw new SecretsClientError('TIMEOUT', `the standalone \`secrets\` CLI did not answer within ${Math.round(syncServeTimeoutMs / 1000)}s ` +
|
|
440
|
+
`(${[command, ...prefix, '__serve'].join(' ')}). The machine may be too loaded to boot it in time, or the install is broken: ` +
|
|
441
|
+
'check with `secrets --version`.');
|
|
442
|
+
}
|
|
443
|
+
// A standalone that exits before draining fd 3 leaves `spawnSync`'s stdin
|
|
444
|
+
// write with EPIPE, yet its exit status and everything it wrote to fd 4 are
|
|
445
|
+
// still returned. Like the async path (`input.on('error', () => {})`), the
|
|
446
|
+
// outcome is whatever reached fd 4 — an empty or non-JSON answer is the
|
|
447
|
+
// diagnostic, not the errno. Seen on the GitHub-hosted runner, where a
|
|
448
|
+
// planted `exit 0` standalone exits faster than the request is written.
|
|
449
|
+
if (err.code !== 'EPIPE') {
|
|
450
|
+
throw new SecretsClientError('SPAWN_FAILED', `secrets request failed: ${err.message}`);
|
|
451
|
+
}
|
|
423
452
|
}
|
|
424
453
|
const raw = result.stdout ?? Buffer.alloc(0);
|
|
425
454
|
if (raw.length > MAX_PROTOCOL_BYTES) {
|
|
@@ -427,45 +456,14 @@ function serveOnceSync(op, args, context) {
|
|
|
427
456
|
}
|
|
428
457
|
return parseResponse(raw);
|
|
429
458
|
}
|
|
430
|
-
// --- handshake (once per process) ------------------------------------------
|
|
431
|
-
let handshakeReady = false;
|
|
432
|
-
let handshakePromise = null;
|
|
433
|
-
function checkHandshake(result) {
|
|
434
|
-
const protocol = result?.protocol;
|
|
435
|
-
if (protocol !== PROTOCOL_VERSION) {
|
|
436
|
-
throw new SecretsClientError('PROTOCOL_UNSUPPORTED', `secrets speaks protocol ${String(protocol)}; this agents-cli needs ${PROTOCOL_VERSION}. ` +
|
|
437
|
-
'Update the standalone CLI (npm i -g @phnx-labs/secrets-cli).');
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
async function ensureHandshake() {
|
|
441
|
-
if (handshakeReady)
|
|
442
|
-
return;
|
|
443
|
-
if (!handshakePromise) {
|
|
444
|
-
handshakePromise = (async () => {
|
|
445
|
-
checkHandshake(await serveOnce('handshake', []));
|
|
446
|
-
handshakeReady = true;
|
|
447
|
-
})().catch((error) => {
|
|
448
|
-
handshakePromise = null;
|
|
449
|
-
throw error;
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
await handshakePromise;
|
|
453
|
-
}
|
|
454
|
-
function ensureHandshakeSync() {
|
|
455
|
-
if (handshakeReady)
|
|
456
|
-
return;
|
|
457
|
-
checkHandshake(serveOnceSync('handshake', []));
|
|
458
|
-
handshakeReady = true;
|
|
459
|
-
}
|
|
460
459
|
// --- primitives ------------------------------------------------------------
|
|
461
460
|
/**
|
|
462
461
|
* Send one operation to the standalone secrets CLI and await its typed result.
|
|
463
|
-
*
|
|
464
|
-
*
|
|
462
|
+
* Spawns `secrets __serve` once for the operation; `parseResponse` checks the
|
|
463
|
+
* protocol on the reply it already carries. Throws a {@link SecretsClientError}
|
|
465
464
|
* carrying the server's `{code, message}` on failure.
|
|
466
465
|
*/
|
|
467
466
|
export async function secretsRequest(op, args = [], context) {
|
|
468
|
-
await ensureHandshake();
|
|
469
467
|
return (await serveOnce(op, args, context));
|
|
470
468
|
}
|
|
471
469
|
/**
|
|
@@ -474,15 +472,17 @@ export async function secretsRequest(op, args = [], context) {
|
|
|
474
472
|
* Bounded by a spawn timeout. POSIX only.
|
|
475
473
|
*/
|
|
476
474
|
export function secretsRequestSync(op, args = [], context) {
|
|
477
|
-
ensureHandshakeSync();
|
|
478
475
|
return serveOnceSync(op, args, context);
|
|
479
476
|
}
|
|
480
|
-
/** Test hook: forget the cached binary
|
|
477
|
+
/** Test hook: forget the cached binary so a new env is re-resolved. */
|
|
481
478
|
export function _resetSecretsClientForTest() {
|
|
482
479
|
cachedBin = undefined;
|
|
483
|
-
handshakeReady = false;
|
|
484
|
-
handshakePromise = null;
|
|
485
480
|
requestCounter = 0;
|
|
481
|
+
syncServeTimeoutMs = SYNC_SERVE_TIMEOUT_MS;
|
|
482
|
+
}
|
|
483
|
+
/** Shorten the sync bound for a test that plants a hanging standalone; reset restores it. */
|
|
484
|
+
export function _setSyncServeTimeoutForTest(ms) {
|
|
485
|
+
syncServeTimeoutMs = ms;
|
|
486
486
|
}
|
|
487
487
|
// --- typed wrappers -------------------------------------------------------
|
|
488
488
|
//
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// menubar-helper check — keeps the installed AGI Menu on the newest published
|
|
2
|
+
// build. Discovery + trigger for the helper's auto-update: the daemon runs this
|
|
3
|
+
// on the periodic cadence; the same call runs right after `agents upgrade`.
|
|
4
|
+
// Detect-only under dryRun (what `agents doctor` shows); the repair is the
|
|
5
|
+
// verified download + atomic swap + restart in `updateMenubarHelperIfNewer`.
|
|
6
|
+
import { resultOf } from '../types.js';
|
|
7
|
+
export const menubarHelperCheck = {
|
|
8
|
+
id: 'menubar-helper',
|
|
9
|
+
title: 'AGI Menu helper is the newest published build',
|
|
10
|
+
platforms: ['darwin'],
|
|
11
|
+
cadence: 'periodic',
|
|
12
|
+
async run(ctx) {
|
|
13
|
+
const { updateMenubarHelperIfNewer } = await import('../../menubar/install-menubar.js');
|
|
14
|
+
const r = await updateMenubarHelperIfNewer({ dryRun: ctx.dryRun });
|
|
15
|
+
switch (r.outcome) {
|
|
16
|
+
case 'updated': return resultOf([r.detail], []);
|
|
17
|
+
case 'failed': return resultOf([], [`AGI Menu ${r.installed ?? '?'} → ${r.available}: ${r.detail}`]);
|
|
18
|
+
default: return resultOf([], []);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -11,6 +11,7 @@ import { shimsCheck } from './checks/shims.js';
|
|
|
11
11
|
import { shadowingCheck } from './checks/shadowing.js';
|
|
12
12
|
import { pathCheck } from './checks/path.js';
|
|
13
13
|
import { installStagingCheck } from './checks/install-staging.js';
|
|
14
|
+
import { menubarHelperCheck } from './checks/menubar-helper.js';
|
|
14
15
|
// Order matters: cheap structural fixes (shims, shadow adoption, PATH, generated
|
|
15
16
|
// hook wrappers) before the heavier resource reconciliation, so a freshly-
|
|
16
17
|
// repaired shim is in place first.
|
|
@@ -24,6 +25,8 @@ export const HEAL_CHECKS = [
|
|
|
24
25
|
hookManifestCheck,
|
|
25
26
|
resourcesCheck,
|
|
26
27
|
installStagingCheck,
|
|
28
|
+
// Last and network-touching: the menu-bar helper's auto-update (macOS only).
|
|
29
|
+
menubarHelperCheck,
|
|
27
30
|
];
|
|
28
31
|
/** Run the selected checks, isolating per-check failures. */
|
|
29
32
|
export async function runSelfHeal(opts = {}) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type HealCheckId = 'resources' | 'hook-runtime' | 'hook-manifest' | 'shims' | 'shadowing' | 'path' | 'install-staging';
|
|
1
|
+
export type HealCheckId = 'resources' | 'hook-runtime' | 'hook-manifest' | 'shims' | 'shadowing' | 'path' | 'install-staging' | 'menubar-helper';
|
|
2
2
|
/** When the daemon schedules a check. */
|
|
3
3
|
export type HealCadence = 'startup' | 'frequent' | 'periodic';
|
|
4
4
|
export interface HealCtx {
|
package/dist/lib/session/db.js
CHANGED
|
@@ -488,7 +488,7 @@ CREATE INDEX IF NOT EXISTS idx_browser_sessions_started ON browser_sessions(star
|
|
|
488
488
|
-- silently vanished on day 8. This row carries the same identity into the durable
|
|
489
489
|
-- store; the ledger is untouched and remains the audit log, with no second pruner.
|
|
490
490
|
--
|
|
491
|
-
-- Keyed on invocation_id, the id
|
|
491
|
+
-- Keyed on invocation_id, the id recordComputerAction stamps once per emitting CLI
|
|
492
492
|
-- process: one explicit verb is one row, and a whole "computer run" observe/act
|
|
493
493
|
-- loop is also one row. task_preview is already bounded by events.ts truncate()
|
|
494
494
|
-- before it is ever written, and typed-text content is never captured at all.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* sha256 helpers for verifying downloaded release assets.
|
|
3
3
|
*
|
|
4
4
|
* These live in their own LEAF module — importing only `node:crypto` and
|
|
5
|
-
* `node:fs` — on purpose. They used to sit in
|
|
5
|
+
* `node:fs` — on purpose. They used to sit in the computer subsystem's ssh-tunnel
|
|
6
|
+
* module (since extracted, PHNX-4075), whose
|
|
6
7
|
* own import graph reached `browser/drivers/ssh.ts` -> `browser/chrome.ts` ->
|
|
7
8
|
* the in-repo secrets engine's own keychain-helper downloader, which imported
|
|
8
9
|
* back into `helper-download.ts` while it was still evaluating — before
|
package/dist/lib/sha256-asset.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* sha256 helpers for verifying downloaded release assets.
|
|
3
3
|
*
|
|
4
4
|
* These live in their own LEAF module — importing only `node:crypto` and
|
|
5
|
-
* `node:fs` — on purpose. They used to sit in
|
|
5
|
+
* `node:fs` — on purpose. They used to sit in the computer subsystem's ssh-tunnel
|
|
6
|
+
* module (since extracted, PHNX-4075), whose
|
|
6
7
|
* own import graph reached `browser/drivers/ssh.ts` -> `browser/chrome.ts` ->
|
|
7
8
|
* the in-repo secrets engine's own keychain-helper downloader, which imported
|
|
8
9
|
* back into `helper-download.ts` while it was still evaluating — before
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ssh-tunnel.ts — the generic `ssh -L localPort:127.0.0.1:remotePort -N`
|
|
3
|
+
* port-forward, plus the fleet-device resolution that names its far end.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT LIVES HERE. These primitives used to sit in `lib/computer/ssh-tunnel.ts`
|
|
6
|
+
* alongside the Windows computer-helper provisioning, because `agents computer
|
|
7
|
+
* --device` was their second caller after the browser CDP driver. The computer
|
|
8
|
+
* engine has since moved to the standalone `computer` CLI (PHNX-4075), so a
|
|
9
|
+
* generic tunnel parked in a deleted subsystem's directory would have gone with
|
|
10
|
+
* it and taken `agents browser`'s remote path down. It is fleet plumbing — the
|
|
11
|
+
* devices registry, the hardened ssh baseline, a local loopback port — and
|
|
12
|
+
* belongs in the fleet layer, not under a feature.
|
|
13
|
+
*
|
|
14
|
+
* Its callers are thin: `browser/drivers/ssh.ts` holds a tunnel for one CDP
|
|
15
|
+
* session, and `lib/computer/context.ts` uses `resolveRemoteDevice` alone —
|
|
16
|
+
* `agents computer --device` resolves the fleet name here and forwards the
|
|
17
|
+
* answer to the standalone engine, which opens and owns its own tunnel (it
|
|
18
|
+
* holds the helper token that tunnel's transport needs).
|
|
19
|
+
*/
|
|
20
|
+
import { type ChildProcess } from 'child_process';
|
|
21
|
+
import { type DeviceProfile } from './devices/registry.js';
|
|
22
|
+
export interface StartTunnelOptions {
|
|
23
|
+
extraSshArgs?: string[];
|
|
24
|
+
}
|
|
25
|
+
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
26
|
+
*
|
|
27
|
+
* Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
|
|
28
|
+
* so the tunnel inherits the same options — crucially the keepalive, which lets
|
|
29
|
+
* a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
|
|
30
|
+
export declare function buildTunnelArgs(user: string, host: string, localPort: number, remotePort: number, extraSshArgs?: string[]): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Spawn `ssh -L localPort:127.0.0.1:remotePort -N user@host`.
|
|
33
|
+
*
|
|
34
|
+
* stderr is captured so a tunnel that dies inside 500ms rejects with the ssh
|
|
35
|
+
* error — the browser driver's original contract. The tunnel is held by this
|
|
36
|
+
* process for the lifetime of the session that opened it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function startSSHTunnel(user: string, host: string, localPort: number, remotePort: number, opts?: StartTunnelOptions): Promise<ChildProcess>;
|
|
39
|
+
/** One registered device, resolved to everything an ssh invocation needs. */
|
|
40
|
+
export interface ResolvedRemoteDevice {
|
|
41
|
+
device: DeviceProfile;
|
|
42
|
+
target: string;
|
|
43
|
+
user: string;
|
|
44
|
+
host: string;
|
|
45
|
+
/** Per-device identity flags (`-i <key> -o IdentitiesOnly=yes`), possibly empty. */
|
|
46
|
+
identityArgs: string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a registered device to its ssh pieces, or throw a clear error.
|
|
50
|
+
*
|
|
51
|
+
* `expectPlatform` is how a caller keeps a platform requirement it used to
|
|
52
|
+
* hard-code: `agents computer --device` drives the Windows helper daemon, so it
|
|
53
|
+
* passes `'windows'` and gets the same refusal as before. Callers with no
|
|
54
|
+
* platform requirement (the browser driver) omit it. The gate is a parameter
|
|
55
|
+
* rather than a baked-in check so this module stays fleet-generic — a
|
|
56
|
+
* hard-coded `windows` here would be a feature rule in shared plumbing.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveRemoteDevice(name: string, opts?: {
|
|
59
|
+
expectPlatform?: DeviceProfile['platform'];
|
|
60
|
+
forWhat?: string;
|
|
61
|
+
}): Promise<ResolvedRemoteDevice>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ssh-tunnel.ts — the generic `ssh -L localPort:127.0.0.1:remotePort -N`
|
|
3
|
+
* port-forward, plus the fleet-device resolution that names its far end.
|
|
4
|
+
*
|
|
5
|
+
* WHY IT LIVES HERE. These primitives used to sit in `lib/computer/ssh-tunnel.ts`
|
|
6
|
+
* alongside the Windows computer-helper provisioning, because `agents computer
|
|
7
|
+
* --device` was their second caller after the browser CDP driver. The computer
|
|
8
|
+
* engine has since moved to the standalone `computer` CLI (PHNX-4075), so a
|
|
9
|
+
* generic tunnel parked in a deleted subsystem's directory would have gone with
|
|
10
|
+
* it and taken `agents browser`'s remote path down. It is fleet plumbing — the
|
|
11
|
+
* devices registry, the hardened ssh baseline, a local loopback port — and
|
|
12
|
+
* belongs in the fleet layer, not under a feature.
|
|
13
|
+
*
|
|
14
|
+
* Its callers are thin: `browser/drivers/ssh.ts` holds a tunnel for one CDP
|
|
15
|
+
* session, and `lib/computer/context.ts` uses `resolveRemoteDevice` alone —
|
|
16
|
+
* `agents computer --device` resolves the fleet name here and forwards the
|
|
17
|
+
* answer to the standalone engine, which opens and owns its own tunnel (it
|
|
18
|
+
* holds the helper token that tunnel's transport needs).
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from 'child_process';
|
|
21
|
+
import { SSH_OPTS, assertValidSshTarget } from './ssh-exec.js';
|
|
22
|
+
import { getDevice } from './devices/registry.js';
|
|
23
|
+
import { deviceIdentityArgs, sshTargetFor } from './devices/connect.js';
|
|
24
|
+
import { hostNameFor } from './devices/ssh-config.js';
|
|
25
|
+
/** Build the ssh argv (after the `ssh` program name) for an `-L` tunnel. Pure.
|
|
26
|
+
*
|
|
27
|
+
* Composes the shared hardened baseline (`SSH_OPTS`) rather than re-listing it,
|
|
28
|
+
* so the tunnel inherits the same options — crucially the keepalive, which lets
|
|
29
|
+
* a dropped `-N` tunnel exit instead of lingering as a zombie on the laptop. */
|
|
30
|
+
export function buildTunnelArgs(user, host, localPort, remotePort, extraSshArgs = []) {
|
|
31
|
+
return [
|
|
32
|
+
'-L',
|
|
33
|
+
`${localPort}:127.0.0.1:${remotePort}`,
|
|
34
|
+
`${user}@${host}`,
|
|
35
|
+
'-N',
|
|
36
|
+
...extraSshArgs,
|
|
37
|
+
...SSH_OPTS,
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Spawn `ssh -L localPort:127.0.0.1:remotePort -N user@host`.
|
|
42
|
+
*
|
|
43
|
+
* stderr is captured so a tunnel that dies inside 500ms rejects with the ssh
|
|
44
|
+
* error — the browser driver's original contract. The tunnel is held by this
|
|
45
|
+
* process for the lifetime of the session that opened it.
|
|
46
|
+
*/
|
|
47
|
+
export function startSSHTunnel(user, host, localPort, remotePort, opts = {}) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
// `user`/`host` can originate from a browser ssh:// profile or a device
|
|
50
|
+
// record. buildTunnelArgs places `${user}@${host}` before `-N`/SSH_OPTS, so
|
|
51
|
+
// a `-`-leading user would be parsed as an ssh option flag (option
|
|
52
|
+
// injection). Validate at the spawn sink so every caller is covered; reject
|
|
53
|
+
// (rather than throw synchronously) to keep the Promise contract.
|
|
54
|
+
try {
|
|
55
|
+
assertValidSshTarget(`${user}@${host}`);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
reject(err);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const args = buildTunnelArgs(user, host, localPort, remotePort, opts.extraSshArgs);
|
|
62
|
+
const tunnel = spawn('ssh', args, {
|
|
63
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
64
|
+
detached: false,
|
|
65
|
+
windowsHide: true,
|
|
66
|
+
});
|
|
67
|
+
let stderr = '';
|
|
68
|
+
tunnel.stderr?.on('data', (data) => {
|
|
69
|
+
stderr += data.toString();
|
|
70
|
+
});
|
|
71
|
+
tunnel.on('error', (err) => {
|
|
72
|
+
reject(new Error(`SSH tunnel failed: ${err.message}`));
|
|
73
|
+
});
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
if (tunnel.killed)
|
|
76
|
+
reject(new Error(`SSH tunnel died: ${stderr}`));
|
|
77
|
+
else
|
|
78
|
+
resolve(tunnel);
|
|
79
|
+
}, 500);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a registered device to its ssh pieces, or throw a clear error.
|
|
84
|
+
*
|
|
85
|
+
* `expectPlatform` is how a caller keeps a platform requirement it used to
|
|
86
|
+
* hard-code: `agents computer --device` drives the Windows helper daemon, so it
|
|
87
|
+
* passes `'windows'` and gets the same refusal as before. Callers with no
|
|
88
|
+
* platform requirement (the browser driver) omit it. The gate is a parameter
|
|
89
|
+
* rather than a baked-in check so this module stays fleet-generic — a
|
|
90
|
+
* hard-coded `windows` here would be a feature rule in shared plumbing.
|
|
91
|
+
*/
|
|
92
|
+
export async function resolveRemoteDevice(name, opts = {}) {
|
|
93
|
+
const device = await getDevice(name);
|
|
94
|
+
if (!device) {
|
|
95
|
+
throw new Error(`Unknown device '${name}'. Register it with \`agents devices add\` / \`agents devices sync\`, then retry.`);
|
|
96
|
+
}
|
|
97
|
+
if (opts.expectPlatform && device.platform !== opts.expectPlatform) {
|
|
98
|
+
const what = opts.forWhat ?? `this command`;
|
|
99
|
+
throw new Error(`Device '${name}' is ${device.platform}, not ${opts.expectPlatform}. ${what} needs a ${opts.expectPlatform} device.`);
|
|
100
|
+
}
|
|
101
|
+
const target = sshTargetFor(device); // validates address + injection guard
|
|
102
|
+
const host = hostNameFor(device); // sshTargetFor already threw if absent
|
|
103
|
+
const user = device.user || process.env.USER || 'Administrator';
|
|
104
|
+
return { device, target, user, host, identityArgs: deviceIdentityArgs(device) };
|
|
105
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A pure boundary: given the session's first user turn (the goal source) and its
|
|
5
5
|
* live progress (todos / plan / phase off the state engine), it asks a local
|
|
6
|
-
* Anthropic-wire endpoint (Ollama / vLLM / LiteLLM
|
|
7
|
-
*
|
|
6
|
+
* Anthropic-wire endpoint (Ollama / vLLM / LiteLLM) for a strict JSON
|
|
7
|
+
* `{goal, checkpoints, checklist}`
|
|
8
8
|
* and validates the shape. On ANY failure — network, non-2xx, non-JSON, wrong
|
|
9
9
|
* shape — it returns `undefined`, and the caller records `summaryState: 'skipped'`.
|
|
10
10
|
*
|
|
@@ -3,15 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A pure boundary: given the session's first user turn (the goal source) and its
|
|
5
5
|
* live progress (todos / plan / phase off the state engine), it asks a local
|
|
6
|
-
* Anthropic-wire endpoint (Ollama / vLLM / LiteLLM
|
|
7
|
-
*
|
|
6
|
+
* Anthropic-wire endpoint (Ollama / vLLM / LiteLLM) for a strict JSON
|
|
7
|
+
* `{goal, checkpoints, checklist}`
|
|
8
8
|
* and validates the shape. On ANY failure — network, non-2xx, non-JSON, wrong
|
|
9
9
|
* shape — it returns `undefined`, and the caller records `summaryState: 'skipped'`.
|
|
10
10
|
*
|
|
11
11
|
* NEVER called on the request path: only the background SessionSummarizerService
|
|
12
12
|
* invokes it, debounced and reader-gated.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* The `anthropic-version` header this request sends. Declared here because this
|
|
16
|
+
* is now its only caller: it used to be imported from the computer subsystem's
|
|
17
|
+
* model client, which left with the standalone `computer` engine (PHNX-4075).
|
|
18
|
+
* A shared constants module for one string used in one request would be more
|
|
19
|
+
* indirection than the string.
|
|
20
|
+
*/
|
|
21
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
15
22
|
const SYSTEM_PROMPT = [
|
|
16
23
|
'You summarize a coding-agent session for an operator dashboard.',
|
|
17
24
|
'Reply with ONLY a single JSON object, no prose and no code fences, of exactly this shape:',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.104",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
"bin": {
|
|
18
18
|
"agents": "dist/index.js",
|
|
19
19
|
"ag": "dist/index.js",
|
|
20
|
-
"browser": "dist/browser.js"
|
|
21
|
-
"computer": "dist/computer.js"
|
|
20
|
+
"browser": "dist/browser.js"
|
|
22
21
|
},
|
|
23
22
|
"files": [
|
|
24
23
|
"dist/**/*.js",
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { type ComputerClient, type RPCResponse } from '../lib/computer/computer-rpc.js';
|
|
3
|
-
export { emitComputerAction, pickTarget, resolveTargetPidDecision, type AppInfo } from '../lib/computer/actions.js';
|
|
4
|
-
export declare function parseXY(s: string, flag: string): {
|
|
5
|
-
x: number;
|
|
6
|
-
y: number;
|
|
7
|
-
};
|
|
8
|
-
export declare function buildElementOrCoords(opts: {
|
|
9
|
-
id?: string;
|
|
10
|
-
x?: number;
|
|
11
|
-
y?: number;
|
|
12
|
-
}): {
|
|
13
|
-
ok: true;
|
|
14
|
-
params: Record<string, unknown>;
|
|
15
|
-
} | {
|
|
16
|
-
ok: false;
|
|
17
|
-
error: string;
|
|
18
|
-
};
|
|
19
|
-
export declare function buildRaiseParams(opts: {
|
|
20
|
-
windowId?: number;
|
|
21
|
-
title?: string;
|
|
22
|
-
}): Record<string, unknown>;
|
|
23
|
-
export declare const CHAR_DELAY_MIN_MS = 1;
|
|
24
|
-
export declare const CHAR_DELAY_MAX_MS = 250;
|
|
25
|
-
export declare function clampCharDelay(ms: number | undefined): number | undefined;
|
|
26
|
-
export declare function buildWaitParams(opts: {
|
|
27
|
-
duration?: number;
|
|
28
|
-
id?: string;
|
|
29
|
-
until?: string;
|
|
30
|
-
role?: string;
|
|
31
|
-
label?: string;
|
|
32
|
-
identifier?: string;
|
|
33
|
-
timeout?: number;
|
|
34
|
-
}): {
|
|
35
|
-
ok: true;
|
|
36
|
-
params: Record<string, unknown>;
|
|
37
|
-
} | {
|
|
38
|
-
ok: false;
|
|
39
|
-
error: string;
|
|
40
|
-
};
|
|
41
|
-
export declare function withClient<T>(fn: (client: ComputerClient) => Promise<T>): Promise<T>;
|
|
42
|
-
export declare function unwrap(r: RPCResponse): Record<string, unknown>;
|
|
43
|
-
export declare function focusStealNotes(opts: {
|
|
44
|
-
id?: string;
|
|
45
|
-
x?: number;
|
|
46
|
-
y?: number;
|
|
47
|
-
raise?: boolean;
|
|
48
|
-
}): string[];
|
|
49
|
-
export declare function shouldRaise(opts: {
|
|
50
|
-
id?: string;
|
|
51
|
-
raise?: boolean;
|
|
52
|
-
}): boolean;
|
|
53
|
-
export declare function appPathIsElectron(appPath: string | null, exists?: (p: string) => boolean): boolean;
|
|
54
|
-
export declare function electronWebviewTip(appLabel: string): string;
|
|
55
|
-
export declare function registerActionCommands(program: Command): void;
|