@phnx-labs/agents-cli 1.20.34 → 1.20.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/LICENSE +185 -21
- package/README.md +13 -5
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.js +81 -0
- package/dist/commands/cost.js +17 -12
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +54 -97
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +283 -12
- package/dist/commands/hosts.js +37 -29
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +62 -16
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +196 -91
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +221 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +143 -1
- package/dist/commands/sessions.js +628 -80
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/view.d.ts +3 -0
- package/dist/commands/view.js +19 -8
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +82 -32
- package/dist/lib/agents.js +30 -9
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.d.ts +8 -0
- package/dist/lib/browser/drivers/ssh.js +77 -16
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +37 -12
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +183 -23
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +92 -0
- package/dist/lib/daemon.js +222 -18
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +10 -3
- package/dist/lib/events.js +101 -24
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +100 -12
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +135 -10
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +45 -13
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +74 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +51 -7
- package/dist/lib/hosts/ready.js +98 -25
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +22 -0
- package/dist/lib/hosts/tasks.js +29 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +26 -0
- package/dist/lib/picker.js +127 -0
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +61 -10
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +61 -0
- package/dist/lib/session/active.js +196 -17
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +204 -27
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +286 -2
- package/dist/lib/session/pid-registry.d.ts +22 -0
- package/dist/lib/session/pid-registry.js +104 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +153 -0
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +18 -8
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +1 -7
- package/dist/lib/session/sync/config.js +4 -11
- package/dist/lib/session/types.d.ts +18 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +44 -6
- package/dist/lib/shims.js +143 -57
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +47 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +62 -63
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +8 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +19 -0
- package/dist/lib/terminal/backends/index.js +31 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +17 -0
- package/dist/lib/terminal/index.js +8 -0
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/types.d.ts +7 -0
- package/dist/lib/versions.d.ts +30 -3
- package/dist/lib/versions.js +129 -18
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-agent, tamper-evident audit log of every dispatched run (issue #347).
|
|
3
|
+
*
|
|
4
|
+
* Every run that reaches the single dispatch chokepoint in `agents run`
|
|
5
|
+
* (src/commands/exec.ts) appends ONE record here. Records form a hash chain:
|
|
6
|
+
* each record embeds the previous record's `hash` as `prevHash`, and its own
|
|
7
|
+
* `hash` is `sha256(canonicalJSON(record-without-hash))`. Because `prevHash`
|
|
8
|
+
* is part of the hashed payload, rewriting or reordering any record breaks the
|
|
9
|
+
* chain from that point forward — `verifyAuditChain()` reports the first index
|
|
10
|
+
* that fails to reproduce.
|
|
11
|
+
*
|
|
12
|
+
* Storage is append-only JSONL under the durable-runtime bucket
|
|
13
|
+
* (`~/.agents/.history/audit/log.jsonl`), one record per line. `.history/` is
|
|
14
|
+
* machine-local, gitignored, and NEVER synced by `agents repo push/pull` — so
|
|
15
|
+
* the `repo` field (a git remote url that can embed an access token) never
|
|
16
|
+
* lands in a version-controlled DotAgents repo, and a cross-machine pull can't
|
|
17
|
+
* splice a foreign chain into this one. The write path is deliberately cheap
|
|
18
|
+
* and non-fatal: a logging failure must never crash a run (see
|
|
19
|
+
* `recordDispatchedRun`). Concurrent writers (parallel teams/routines dispatch)
|
|
20
|
+
* are serialized by an advisory lock file so the chain never forks — see
|
|
21
|
+
* `appendAuditRecord`.
|
|
22
|
+
*
|
|
23
|
+
* Governance context: this is the evidence trail an operator needs to answer
|
|
24
|
+
* "which agent, at which version, ran against which repo, and did it succeed?"
|
|
25
|
+
* — the kind of automated-decision logging the EU AI Act (Art. 12) expects of
|
|
26
|
+
* high-risk systems, kept tamper-evident so the log itself can be trusted.
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'fs';
|
|
29
|
+
import * as path from 'path';
|
|
30
|
+
import { spawnSync } from 'child_process';
|
|
31
|
+
import { sha256 } from '../staleness/fingerprint.js';
|
|
32
|
+
import { getHistoryDir } from '../state.js';
|
|
33
|
+
import { ensureLockTarget, withFileLock } from '../fs-atomic.js';
|
|
34
|
+
/** First record's `prevHash` — a fixed anchor so the chain has a root. */
|
|
35
|
+
export const GENESIS_HASH = 'GENESIS';
|
|
36
|
+
/**
|
|
37
|
+
* Absolute path to the append-only audit log. Lives under `.history/` — the
|
|
38
|
+
* durable-but-machine-local runtime bucket that is gitignored and never synced
|
|
39
|
+
* by `agents repo push/pull`. Keeping it here (not under a top-level, tracked
|
|
40
|
+
* `~/.agents/` path) means the token-bearing `repo` field can never leak into a
|
|
41
|
+
* version-controlled DotAgents repo, and no cross-machine pull can fork the chain.
|
|
42
|
+
*/
|
|
43
|
+
export function getAuditLogPath() {
|
|
44
|
+
return path.join(getHistoryDir(), 'audit', 'log.jsonl');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Deterministic JSON: object keys sorted recursively so the same logical
|
|
48
|
+
* record always serializes to the same bytes (hashing must be reproducible
|
|
49
|
+
* across processes and machines). Values here are all primitives, but the
|
|
50
|
+
* recursion keeps it correct if the record ever nests.
|
|
51
|
+
*/
|
|
52
|
+
function canonicalJSON(value) {
|
|
53
|
+
if (value === null || typeof value !== 'object')
|
|
54
|
+
return JSON.stringify(value);
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return '[' + value.map(canonicalJSON).join(',') + ']';
|
|
57
|
+
const obj = value;
|
|
58
|
+
const keys = Object.keys(obj).sort();
|
|
59
|
+
return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalJSON(obj[k])).join(',') + '}';
|
|
60
|
+
}
|
|
61
|
+
/** Compute the sealing hash for a record whose `prevHash` is already set. */
|
|
62
|
+
function hashRecord(record) {
|
|
63
|
+
return sha256(canonicalJSON(record));
|
|
64
|
+
}
|
|
65
|
+
/** Read + parse every record in the log, oldest-first. Missing file → []. */
|
|
66
|
+
function readRecords(logPath) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = fs.readFileSync(logPath, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return []; // no log yet
|
|
73
|
+
}
|
|
74
|
+
const records = [];
|
|
75
|
+
for (const line of raw.split('\n')) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (!trimmed)
|
|
78
|
+
continue;
|
|
79
|
+
records.push(JSON.parse(trimmed));
|
|
80
|
+
}
|
|
81
|
+
return records;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Append one record to the hash chain and return it. Links to the previous
|
|
85
|
+
* record's `hash` (or `GENESIS_HASH` for the first), computes the sealing
|
|
86
|
+
* hash, and writes a single JSONL line. Synchronous so the record is durable
|
|
87
|
+
* before the caller proceeds.
|
|
88
|
+
*
|
|
89
|
+
* Read-last-hash + append run under the canonical advisory file lock
|
|
90
|
+
* (`withFileLock`, backed by proper-lockfile). Without it, two concurrent
|
|
91
|
+
* writers — the norm under parallel teams/routines dispatch — both read the
|
|
92
|
+
* same last hash and both write `prevHash=H`, forking the chain into a false
|
|
93
|
+
* "tampered" verdict. The lock forces a total order so every record links off
|
|
94
|
+
* the genuinely-previous one. `ensureLockTarget` creates the file first because
|
|
95
|
+
* proper-lockfile locks an existing path.
|
|
96
|
+
*/
|
|
97
|
+
export function appendAuditRecord(entry, logPath = getAuditLogPath()) {
|
|
98
|
+
ensureLockTarget(logPath);
|
|
99
|
+
return withFileLock(logPath, () => {
|
|
100
|
+
const existing = readRecords(logPath);
|
|
101
|
+
const prevHash = existing.length ? existing[existing.length - 1].hash : GENESIS_HASH;
|
|
102
|
+
const unsealed = { ...entry, prevHash };
|
|
103
|
+
const record = { ...unsealed, hash: hashRecord(unsealed) };
|
|
104
|
+
fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
|
|
105
|
+
return record;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Walk the chain and confirm every record reproduces. A record is valid when
|
|
110
|
+
* (a) its `prevHash` matches the prior record's `hash` (GENESIS for the first)
|
|
111
|
+
* AND (b) recomputing its sealing hash from its own fields reproduces the
|
|
112
|
+
* stored `hash`. Returns the first failing index in `brokenAt`.
|
|
113
|
+
*/
|
|
114
|
+
export function verifyAuditChain(logPath = getAuditLogPath()) {
|
|
115
|
+
let records;
|
|
116
|
+
try {
|
|
117
|
+
records = readRecords(logPath);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
// A line that won't even parse is itself tamper evidence — the log is corrupt.
|
|
121
|
+
return { ok: false, brokenAt: 0 };
|
|
122
|
+
}
|
|
123
|
+
let expectedPrev = GENESIS_HASH;
|
|
124
|
+
for (let i = 0; i < records.length; i++) {
|
|
125
|
+
const r = records[i];
|
|
126
|
+
if (r.prevHash !== expectedPrev)
|
|
127
|
+
return { ok: false, brokenAt: i };
|
|
128
|
+
const { hash, ...unsealed } = r;
|
|
129
|
+
if (hashRecord(unsealed) !== hash)
|
|
130
|
+
return { ok: false, brokenAt: i };
|
|
131
|
+
expectedPrev = r.hash;
|
|
132
|
+
}
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
135
|
+
/** Read the whole chain, oldest-first. Exposed for `agents audit list`. */
|
|
136
|
+
export function readAuditLog(logPath = getAuditLogPath()) {
|
|
137
|
+
return readRecords(logPath);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Resolve a stable repo label for a run: the git remote origin url when the
|
|
141
|
+
* cwd is inside a repo with one, otherwise the cwd itself. Best-effort — any
|
|
142
|
+
* failure falls back to the cwd.
|
|
143
|
+
*/
|
|
144
|
+
function repoLabel(cwd) {
|
|
145
|
+
try {
|
|
146
|
+
const res = spawnSync('git', ['-C', cwd, 'config', '--get', 'remote.origin.url'], {
|
|
147
|
+
encoding: 'utf-8',
|
|
148
|
+
timeout: 2000,
|
|
149
|
+
});
|
|
150
|
+
const url = res.status === 0 ? res.stdout.trim() : '';
|
|
151
|
+
return url || cwd;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return cwd;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Record ONE dispatched run at the single exec chokepoint. Non-fatal by
|
|
159
|
+
* contract: any failure is caught and warned, never thrown — an audit-log
|
|
160
|
+
* hiccup must not crash a run that already finished.
|
|
161
|
+
*/
|
|
162
|
+
export function recordDispatchedRun(run) {
|
|
163
|
+
try {
|
|
164
|
+
appendAuditRecord({
|
|
165
|
+
ts: new Date().toISOString(),
|
|
166
|
+
agent: run.agent,
|
|
167
|
+
version: run.version,
|
|
168
|
+
repo: repoLabel(run.cwd),
|
|
169
|
+
mode: run.mode,
|
|
170
|
+
outcome: run.exitCode === 0 ? 'ok' : 'fail',
|
|
171
|
+
exit: run.exitCode,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
process.stderr.write(`[agents] audit log write failed: ${err.message}\n`);
|
|
176
|
+
}
|
|
177
|
+
}
|
package/dist/lib/auto-pull.js
CHANGED
|
@@ -14,6 +14,7 @@ import * as path from 'path';
|
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { getFetchCacheDir } from './state.js';
|
|
17
|
+
import { backgroundSpawnOptions } from './platform/process.js';
|
|
17
18
|
/** Where lock files and per-repo status markers live. */
|
|
18
19
|
function fetchStateDir() {
|
|
19
20
|
return getFetchCacheDir();
|
|
@@ -38,7 +39,7 @@ export function spawnDetachedSync() {
|
|
|
38
39
|
return;
|
|
39
40
|
try {
|
|
40
41
|
const child = spawn(process.execPath, [workerPath], {
|
|
41
|
-
|
|
42
|
+
...backgroundSpawnOptions(),
|
|
42
43
|
stdio: 'ignore',
|
|
43
44
|
env: process.env,
|
|
44
45
|
});
|
package/dist/lib/browser/cdp.js
CHANGED
|
@@ -219,7 +219,11 @@ export function verifyBrowserIdentity(reported, expected, port, host = 'localhos
|
|
|
219
219
|
// to a Comet instance doesn't trip a false "identity mismatch".
|
|
220
220
|
comet: ['comet', 'chrome'],
|
|
221
221
|
brave: ['brave', 'brave-browser', 'chrome'],
|
|
222
|
-
|
|
222
|
+
// Windows/Chromium Edge reports its /json/version "Browser" field as
|
|
223
|
+
// "Edg/<version>" (the `Edg` token, not `Edge`), which normalizeBrowserName
|
|
224
|
+
// reduces to "edg". Accept it so attaching to a real Edge doesn't trip a
|
|
225
|
+
// false identity mismatch (the profile enum only allows "edge").
|
|
226
|
+
edge: ['edge', 'edg', 'microsoft-edge', 'msedge', 'chrome'],
|
|
223
227
|
};
|
|
224
228
|
const accepted = matches[expected] || [expected];
|
|
225
229
|
if (accepted.includes(reported))
|
|
@@ -36,6 +36,16 @@ export declare function getRunningChromeInfo(profileName: string): {
|
|
|
36
36
|
pid: number;
|
|
37
37
|
port: number;
|
|
38
38
|
} | null;
|
|
39
|
+
/**
|
|
40
|
+
* Is a TCP port currently bound? `lsof` on POSIX, `netstat -ano` on Windows
|
|
41
|
+
* (lsof doesn't exist there). Returns false on any tooling error so port
|
|
42
|
+
* allocation degrades to "assume free" rather than throwing.
|
|
43
|
+
*
|
|
44
|
+
* Exported as the canonical cross-platform probe — `findFreeProfilePort`
|
|
45
|
+
* (profiles.ts) must use this rather than shelling out to lsof directly,
|
|
46
|
+
* or every port scans as free on Windows.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isPortInUse(port: number): boolean;
|
|
39
49
|
export declare function allocatePort(): number;
|
|
40
50
|
export interface PortOccupant {
|
|
41
51
|
pid: number;
|
|
@@ -39,7 +39,11 @@ const BROWSER_PATHS = {
|
|
|
39
39
|
`${WIN_PROGRAMFILES_X86}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
40
40
|
`${WIN_LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
41
41
|
],
|
|
42
|
-
comet: [
|
|
42
|
+
comet: [
|
|
43
|
+
`${WIN_PROGRAMFILES}\\Perplexity\\Comet\\Application\\comet.exe`,
|
|
44
|
+
`${WIN_PROGRAMFILES_X86}\\Perplexity\\Comet\\Application\\comet.exe`,
|
|
45
|
+
`${WIN_LOCALAPPDATA}\\Perplexity\\Comet\\Application\\comet.exe`,
|
|
46
|
+
],
|
|
43
47
|
chromium: [
|
|
44
48
|
`${WIN_LOCALAPPDATA}\\Chromium\\Application\\chrome.exe`,
|
|
45
49
|
],
|
|
@@ -155,8 +159,8 @@ export function findBrowserPath(browserType, customBinary) {
|
|
|
155
159
|
return resolveBrowserBinary(p);
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
|
-
if (browserType === 'comet' && platform
|
|
159
|
-
throw new Error('Browser "comet" is
|
|
162
|
+
if (browserType === 'comet' && platform === 'linux') {
|
|
163
|
+
throw new Error('Browser "comet" is not available on Linux (Comet ships macOS and Windows builds only). Use chrome, chromium, brave, or edge on this platform.');
|
|
160
164
|
}
|
|
161
165
|
throw new Error(`Browser "${browserType}" not found. Install it first.`);
|
|
162
166
|
}
|
|
@@ -171,7 +175,7 @@ const DEFAULT_BROWSER_PRIORITY = {
|
|
|
171
175
|
linux: ['chrome', 'chromium', 'brave', 'edge'],
|
|
172
176
|
// Windows: Edge is preinstalled on every supported build, so it's the
|
|
173
177
|
// reliable always-there default.
|
|
174
|
-
win32: ['edge', 'chrome', 'brave'],
|
|
178
|
+
win32: ['edge', 'chrome', 'brave', 'comet'],
|
|
175
179
|
};
|
|
176
180
|
/**
|
|
177
181
|
* Walk the per-platform priority list and return the first browser that's
|
|
@@ -290,11 +294,11 @@ export function killChrome(pid) {
|
|
|
290
294
|
// stop first (no /F → posts WM_CLOSE), and only force-kill the tree if that
|
|
291
295
|
// fails or the process is still around.
|
|
292
296
|
try {
|
|
293
|
-
execFileSync('taskkill', ['/pid', String(pid)], { stdio: 'ignore' });
|
|
297
|
+
execFileSync('taskkill', ['/pid', String(pid)], { stdio: 'ignore', windowsHide: true });
|
|
294
298
|
}
|
|
295
299
|
catch {
|
|
296
300
|
try {
|
|
297
|
-
execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' });
|
|
301
|
+
execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
|
|
298
302
|
}
|
|
299
303
|
catch {
|
|
300
304
|
// Process already dead
|
|
@@ -345,13 +349,18 @@ function sleep(ms) {
|
|
|
345
349
|
* Is a TCP port currently bound? `lsof` on POSIX, `netstat -ano` on Windows
|
|
346
350
|
* (lsof doesn't exist there). Returns false on any tooling error so port
|
|
347
351
|
* allocation degrades to "assume free" rather than throwing.
|
|
352
|
+
*
|
|
353
|
+
* Exported as the canonical cross-platform probe — `findFreeProfilePort`
|
|
354
|
+
* (profiles.ts) must use this rather than shelling out to lsof directly,
|
|
355
|
+
* or every port scans as free on Windows.
|
|
348
356
|
*/
|
|
349
|
-
function isPortInUse(port) {
|
|
357
|
+
export function isPortInUse(port) {
|
|
350
358
|
if (process.platform === 'win32') {
|
|
351
359
|
try {
|
|
352
360
|
const out = execFileSync('netstat', ['-ano', '-p', 'TCP'], {
|
|
353
361
|
encoding: 'utf8',
|
|
354
362
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
363
|
+
windowsHide: true,
|
|
355
364
|
});
|
|
356
365
|
// Lines look like: " TCP 0.0.0.0:9200 0.0.0.0:0 LISTENING 1234"
|
|
357
366
|
return out.split('\n').some((line) => {
|
|
@@ -393,6 +402,7 @@ export function getPortOccupant(port) {
|
|
|
393
402
|
const out = execFileSync('netstat', ['-ano', '-p', 'TCP'], {
|
|
394
403
|
encoding: 'utf8',
|
|
395
404
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
405
|
+
windowsHide: true,
|
|
396
406
|
});
|
|
397
407
|
let pid = 0;
|
|
398
408
|
for (const line of out.split('\n')) {
|
|
@@ -410,6 +420,7 @@ export function getPortOccupant(port) {
|
|
|
410
420
|
const tl = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], {
|
|
411
421
|
encoding: 'utf8',
|
|
412
422
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
423
|
+
windowsHide: true,
|
|
413
424
|
});
|
|
414
425
|
const m = tl.match(/^"([^"]+)"/);
|
|
415
426
|
if (m)
|
|
@@ -53,7 +53,15 @@ export declare function buildLaunchCmd(remoteOs: RemoteOs, browserType: string,
|
|
|
53
53
|
* (Get-NetTCPConnection → Stop-Process).
|
|
54
54
|
*/
|
|
55
55
|
export declare function buildKillCmd(remoteOs: RemoteOs, port: number): string;
|
|
56
|
+
export declare function ensureRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
|
|
56
57
|
export declare function restartRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
|
|
60
|
+
* WMI-spawned (Windows) / detached (posix) browser process is not orphaned when
|
|
61
|
+
* the ssh tunnel is torn down — killing only the tunnel left it running on
|
|
62
|
+
* win-mini after every `browser stop`. Best-effort: never rejects.
|
|
63
|
+
*/
|
|
64
|
+
export declare function killRemoteBrowser(user: string, host: string, remoteOs: RemoteOs, port: number): Promise<void>;
|
|
57
65
|
/**
|
|
58
66
|
* Identify whether a pid listening on our target local port is an SSH
|
|
59
67
|
* tunnel WE would have spawned for `host:remotePort`. Used so that two
|
|
@@ -48,12 +48,12 @@ export async function connectSSH(endpoint, profile) {
|
|
|
48
48
|
`is already in use by ${occupant.command} (pid ${occupant.pid}). ` +
|
|
49
49
|
`Either kill that process (\`kill ${occupant.pid}\`) or change the profile's port.`);
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
// A non-zero remote exit (missing exe, auth failure, bad launch) now rejects
|
|
52
|
+
// with the captured stderr and propagates. An already-running browser exits 0
|
|
53
|
+
// (the launch backgrounds/WMI-spawns regardless), so this does not spuriously
|
|
54
|
+
// fail the reconnect path — only genuine launch failures reach the caller
|
|
55
|
+
// instead of being swallowed and mis-reported later as a tunnel timeout.
|
|
56
|
+
await ensureRemoteBrowser(user, host, profile.browser, remotePort, remoteOs, profile.binary);
|
|
57
57
|
let tunnel;
|
|
58
58
|
if (occupant) {
|
|
59
59
|
// Reuse the existing tunnel rather than spawning a duplicate.
|
|
@@ -102,6 +102,11 @@ export async function connectSSH(endpoint, profile) {
|
|
|
102
102
|
pid: tunnelPid,
|
|
103
103
|
cleanup: () => {
|
|
104
104
|
cdp.close();
|
|
105
|
+
// Kill the remote browser BEFORE tearing down the tunnel. It runs on a
|
|
106
|
+
// separate ssh connection (independent of the tunnel we're about to
|
|
107
|
+
// kill), so tunnel teardown can't cut it off. Fire-and-forget — cleanup
|
|
108
|
+
// stays synchronous, and killRemoteBrowser never rejects.
|
|
109
|
+
killRemoteBrowser(user, host, remoteOs, remotePort).catch(() => { });
|
|
105
110
|
tunnel.kill();
|
|
106
111
|
clearProfileRuntime(profile.name);
|
|
107
112
|
},
|
|
@@ -177,9 +182,9 @@ export function encodePowerShell(script) {
|
|
|
177
182
|
* registry at runtime rather than relying on a bare `msedge` name.
|
|
178
183
|
*/
|
|
179
184
|
export function buildWindowsLaunchScript(browserType, port, customBinary) {
|
|
180
|
-
let
|
|
185
|
+
let exeStmts;
|
|
181
186
|
if (customBinary) {
|
|
182
|
-
|
|
187
|
+
exeStmts = [`$exe = ${psSingleQuote(customBinary)}`];
|
|
183
188
|
}
|
|
184
189
|
else if (browserType === 'custom') {
|
|
185
190
|
throw new Error('browser: custom requires a binary path in the profile');
|
|
@@ -188,13 +193,24 @@ export function buildWindowsLaunchScript(browserType, port, customBinary) {
|
|
|
188
193
|
const exeKey = WIN_BROWSER_APPPATH[browserType];
|
|
189
194
|
if (!exeKey)
|
|
190
195
|
throw new Error(`Unknown browser type for windows remote: ${browserType}`);
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
const appPath = `SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${exeKey}`;
|
|
197
|
+
// Per-user installs — the DEFAULT for Edge and Chrome — register App Paths
|
|
198
|
+
// under HKCU, not HKLM, so an HKLM-only lookup silently missed them and the
|
|
199
|
+
// launch failed. Resolve from HKLM first, then fall through to HKCU.
|
|
200
|
+
// Windows PowerShell 5.1 (what `powershell.exe` is) has no `??`, so the
|
|
201
|
+
// fallback is an explicit `if`. A missing exe `throw`s so the failure rides
|
|
202
|
+
// out as a non-zero ssh exit (surfaced by ensureRemoteBrowser) instead of a
|
|
203
|
+
// silent empty launch.
|
|
204
|
+
exeStmts = [
|
|
205
|
+
`$exe = (Get-ItemProperty 'HKLM:\\${appPath}' -EA SilentlyContinue).'(default)'`,
|
|
206
|
+
`if (-not $exe) { $exe = (Get-ItemProperty 'HKCU:\\${appPath}' -EA SilentlyContinue).'(default)' }`,
|
|
207
|
+
`if (-not $exe) { throw 'browser exe not found in HKLM/HKCU App Paths: ${exeKey}' }`,
|
|
208
|
+
];
|
|
193
209
|
}
|
|
194
210
|
// Keep the `--remote-allow-origins=http://127.0.0.1:${port}` literal in
|
|
195
211
|
// source — a test asserts CDP is never opened to `*`.
|
|
196
212
|
return [
|
|
197
|
-
|
|
213
|
+
...exeStmts,
|
|
198
214
|
`$cl = '"' + $exe + '" --remote-debugging-port=${port}` +
|
|
199
215
|
` --remote-allow-origins=http://127.0.0.1:${port}` +
|
|
200
216
|
` --disable-background-timer-throttling --user-data-dir="' + $env:TEMP + '\\agents-browser-${port}"'`,
|
|
@@ -242,21 +258,56 @@ export function buildKillCmd(remoteOs, port) {
|
|
|
242
258
|
}
|
|
243
259
|
return `pids=$(lsof -ti ${shellQuote(`:${port}`)} 2>/dev/null); [ -z "$pids" ] || kill -9 $pids 2>/dev/null || true`;
|
|
244
260
|
}
|
|
245
|
-
async function ensureRemoteBrowser(user, host, browserType, port, remoteOs, customBinary) {
|
|
261
|
+
export async function ensureRemoteBrowser(user, host, browserType, port, remoteOs, customBinary) {
|
|
246
262
|
const remoteCmd = buildLaunchCmd(remoteOs, browserType, port, customBinary);
|
|
247
263
|
return new Promise((resolve, reject) => {
|
|
264
|
+
// Capture stderr so a real launch failure (missing exe, auth denied, bad
|
|
265
|
+
// CreateProcess) surfaces here as a clear error. Previously stdio was
|
|
266
|
+
// ignored and this resolved on close regardless of exit code, so failures
|
|
267
|
+
// were swallowed and only re-emerged 8s later as a generic
|
|
268
|
+
// "SSH tunnel failed to establish".
|
|
248
269
|
const child = spawn('ssh', [
|
|
249
270
|
`${user}@${host}`,
|
|
250
271
|
'-o',
|
|
251
272
|
'BatchMode=yes',
|
|
252
273
|
remoteCmd,
|
|
253
|
-
], { stdio: 'ignore' });
|
|
254
|
-
|
|
255
|
-
child.on('
|
|
256
|
-
|
|
274
|
+
], { stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
|
|
275
|
+
let stderr = '';
|
|
276
|
+
child.stderr?.on('data', (d) => {
|
|
277
|
+
stderr += d.toString();
|
|
278
|
+
});
|
|
279
|
+
let settled = false;
|
|
280
|
+
const timer = setTimeout(() => {
|
|
281
|
+
if (settled)
|
|
282
|
+
return;
|
|
283
|
+
settled = true;
|
|
284
|
+
// ssh is still running the backgrounding launch command past our window.
|
|
285
|
+
// Killing the local ssh client does NOT reap the WMI/detached remote
|
|
286
|
+
// browser, so treat this as launched and let waitForPort confirm.
|
|
257
287
|
child.kill();
|
|
258
288
|
resolve();
|
|
259
289
|
}, 2000);
|
|
290
|
+
child.on('close', (code) => {
|
|
291
|
+
if (settled)
|
|
292
|
+
return;
|
|
293
|
+
settled = true;
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
if (code && code !== 0) {
|
|
296
|
+
const detail = stderr.trim();
|
|
297
|
+
reject(new Error(`Remote browser launch failed on ${host} (ssh exit ${code})` +
|
|
298
|
+
(detail ? `: ${detail}` : '')));
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
resolve();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
child.on('error', (err) => {
|
|
305
|
+
if (settled)
|
|
306
|
+
return;
|
|
307
|
+
settled = true;
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
reject(err);
|
|
310
|
+
});
|
|
260
311
|
});
|
|
261
312
|
}
|
|
262
313
|
export async function restartRemoteBrowser(user, host, browserType, port, remoteOs, customBinary) {
|
|
@@ -266,10 +317,20 @@ export async function restartRemoteBrowser(user, host, browserType, port, remote
|
|
|
266
317
|
await ensureRemoteBrowser(user, host, browserType, port, remoteOs, customBinary);
|
|
267
318
|
await sleep(1500);
|
|
268
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
|
|
322
|
+
* WMI-spawned (Windows) / detached (posix) browser process is not orphaned when
|
|
323
|
+
* the ssh tunnel is torn down — killing only the tunnel left it running on
|
|
324
|
+
* win-mini after every `browser stop`. Best-effort: never rejects.
|
|
325
|
+
*/
|
|
326
|
+
export function killRemoteBrowser(user, host, remoteOs, port) {
|
|
327
|
+
return runSSHCommand(user, host, buildKillCmd(remoteOs, port));
|
|
328
|
+
}
|
|
269
329
|
function runSSHCommand(user, host, cmd) {
|
|
270
330
|
return new Promise((resolve) => {
|
|
271
331
|
const child = spawn('ssh', [`${user}@${host}`, '-o', 'BatchMode=yes', cmd], {
|
|
272
332
|
stdio: 'ignore',
|
|
333
|
+
windowsHide: true,
|
|
273
334
|
});
|
|
274
335
|
child.on('close', () => resolve());
|
|
275
336
|
child.on('error', () => resolve());
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { NetworkRequest } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal HAR 1.2 shapes. We only produce what our request buffer can back —
|
|
4
|
+
* fields the buffer doesn't populate (headers, cookies, bodies, timings) are
|
|
5
|
+
* emitted with the spec-mandated empty defaults or `-1` sentinels so validators
|
|
6
|
+
* still accept the document.
|
|
7
|
+
*
|
|
8
|
+
* Spec: http://www.softwareishard.com/blog/har-12-spec/
|
|
9
|
+
*/
|
|
10
|
+
export interface HarLog {
|
|
11
|
+
version: '1.2';
|
|
12
|
+
creator: {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
};
|
|
16
|
+
pages: HarPage[];
|
|
17
|
+
entries: HarEntry[];
|
|
18
|
+
}
|
|
19
|
+
export interface HarPage {
|
|
20
|
+
startedDateTime: string;
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
pageTimings: {
|
|
24
|
+
onContentLoad: number;
|
|
25
|
+
onLoad: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface HarEntry {
|
|
29
|
+
startedDateTime: string;
|
|
30
|
+
time: number;
|
|
31
|
+
request: HarRequest;
|
|
32
|
+
response: HarResponse;
|
|
33
|
+
cache: Record<string, never>;
|
|
34
|
+
timings: HarTimings;
|
|
35
|
+
}
|
|
36
|
+
export interface HarRequest {
|
|
37
|
+
method: string;
|
|
38
|
+
url: string;
|
|
39
|
+
httpVersion: string;
|
|
40
|
+
cookies: never[];
|
|
41
|
+
headers: never[];
|
|
42
|
+
queryString: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
value: string;
|
|
45
|
+
}>;
|
|
46
|
+
headersSize: number;
|
|
47
|
+
bodySize: number;
|
|
48
|
+
}
|
|
49
|
+
export interface HarResponse {
|
|
50
|
+
status: number;
|
|
51
|
+
statusText: string;
|
|
52
|
+
httpVersion: string;
|
|
53
|
+
cookies: never[];
|
|
54
|
+
headers: never[];
|
|
55
|
+
content: {
|
|
56
|
+
size: number;
|
|
57
|
+
mimeType: string;
|
|
58
|
+
text?: string;
|
|
59
|
+
};
|
|
60
|
+
redirectURL: string;
|
|
61
|
+
headersSize: number;
|
|
62
|
+
bodySize: number;
|
|
63
|
+
}
|
|
64
|
+
export interface HarTimings {
|
|
65
|
+
send: number;
|
|
66
|
+
wait: number;
|
|
67
|
+
receive: number;
|
|
68
|
+
}
|
|
69
|
+
export interface BuildHarOptions {
|
|
70
|
+
creatorName?: string;
|
|
71
|
+
creatorVersion?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Serialize a buffered `NetworkRequest[]` into a HAR 1.2 document.
|
|
75
|
+
*
|
|
76
|
+
* The buffer only records `url`, `method`, `status`, `mimeType`, `timestamp`.
|
|
77
|
+
* Everything downstream — cookies, headers, bodies, per-phase timings — is
|
|
78
|
+
* unknown and must be emitted as the spec's "unknown" sentinels: `-1` for
|
|
79
|
+
* sizes / timings, empty arrays for cookies / headers. HAR validators accept
|
|
80
|
+
* this shape (browsers do the same for opaque cross-origin responses).
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildHar(requests: NetworkRequest[], opts?: BuildHarOptions): {
|
|
83
|
+
log: HarLog;
|
|
84
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a buffered `NetworkRequest[]` into a HAR 1.2 document.
|
|
3
|
+
*
|
|
4
|
+
* The buffer only records `url`, `method`, `status`, `mimeType`, `timestamp`.
|
|
5
|
+
* Everything downstream — cookies, headers, bodies, per-phase timings — is
|
|
6
|
+
* unknown and must be emitted as the spec's "unknown" sentinels: `-1` for
|
|
7
|
+
* sizes / timings, empty arrays for cookies / headers. HAR validators accept
|
|
8
|
+
* this shape (browsers do the same for opaque cross-origin responses).
|
|
9
|
+
*/
|
|
10
|
+
export function buildHar(requests, opts = {}) {
|
|
11
|
+
const creator = {
|
|
12
|
+
name: opts.creatorName ?? 'agents-cli',
|
|
13
|
+
version: opts.creatorVersion ?? '',
|
|
14
|
+
};
|
|
15
|
+
const startedAt = requests.length > 0 ? Math.min(...requests.map((r) => r.timestamp)) : Date.now();
|
|
16
|
+
const page = {
|
|
17
|
+
startedDateTime: new Date(startedAt).toISOString(),
|
|
18
|
+
id: 'page_1',
|
|
19
|
+
title: 'agents browser session',
|
|
20
|
+
pageTimings: { onContentLoad: -1, onLoad: -1 },
|
|
21
|
+
};
|
|
22
|
+
const entries = requests.map((r) => {
|
|
23
|
+
const queryString = extractQueryString(r.url);
|
|
24
|
+
const mimeType = r.mimeType ?? '';
|
|
25
|
+
return {
|
|
26
|
+
startedDateTime: new Date(r.timestamp).toISOString(),
|
|
27
|
+
time: -1,
|
|
28
|
+
request: {
|
|
29
|
+
method: r.method,
|
|
30
|
+
url: r.url,
|
|
31
|
+
httpVersion: 'HTTP/1.1',
|
|
32
|
+
cookies: [],
|
|
33
|
+
headers: [],
|
|
34
|
+
queryString,
|
|
35
|
+
headersSize: -1,
|
|
36
|
+
bodySize: -1,
|
|
37
|
+
},
|
|
38
|
+
response: {
|
|
39
|
+
status: r.status ?? 0,
|
|
40
|
+
statusText: '',
|
|
41
|
+
httpVersion: 'HTTP/1.1',
|
|
42
|
+
cookies: [],
|
|
43
|
+
headers: [],
|
|
44
|
+
content: { size: -1, mimeType },
|
|
45
|
+
redirectURL: '',
|
|
46
|
+
headersSize: -1,
|
|
47
|
+
bodySize: -1,
|
|
48
|
+
},
|
|
49
|
+
cache: {},
|
|
50
|
+
timings: { send: -1, wait: -1, receive: -1 },
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
log: {
|
|
55
|
+
version: '1.2',
|
|
56
|
+
creator,
|
|
57
|
+
pages: [page],
|
|
58
|
+
entries,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pull `?a=1&b=2` off a URL into HAR's `queryString` array. Returns `[]` when
|
|
64
|
+
* the URL has no query or fails to parse (opaque URLs like `data:` shouldn't
|
|
65
|
+
* abort the whole HAR).
|
|
66
|
+
*/
|
|
67
|
+
function extractQueryString(url) {
|
|
68
|
+
try {
|
|
69
|
+
const u = new URL(url);
|
|
70
|
+
const out = [];
|
|
71
|
+
u.searchParams.forEach((value, name) => out.push({ name, value }));
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -8,6 +8,9 @@ export declare class BrowserDaemonNotRunningError extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function formatBrowserDaemonNotRunningError(): string;
|
|
10
10
|
export declare function getSocketPath(): string;
|
|
11
|
+
/** Is the daemon reachable? A real connect probe on every platform — a socket
|
|
12
|
+
* file existing on disk is not proof a daemon is listening on it. */
|
|
13
|
+
export declare function isDaemonReachable(): Promise<boolean>;
|
|
11
14
|
export declare class BrowserIPCServer {
|
|
12
15
|
private server;
|
|
13
16
|
private service;
|