harnery 0.41.0 → 0.42.0
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/dist/commander.js +3 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +9 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +263 -51
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +43 -9
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/core/adapters/profiles.d.ts +9 -0
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +22 -1
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +60 -0
- package/dist/core/artifacts/delivery-card.js +9 -2
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +522 -49
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +128 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +2 -2
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +16 -0
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/hooks/adapter/events.d.ts +17 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +5 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +37 -7
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +62 -4
- package/dist/core/hooks/cli.js +45 -6
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +4 -1
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/gate.js +20 -1
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/package.json +6 -1
- package/schemas/config.schema.json +91 -0
- package/src/commander.ts +19 -0
- package/src/commands/agents.ts +14 -0
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse.ts +400 -57
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/disk.ts +114 -0
- package/src/commands/doctor.ts +52 -1
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +55 -5
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/core/adapters/profiles.ts +22 -1
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/state/live-coordination-view.ts +73 -0
- package/src/core/artifacts/delivery-card.ts +10 -2
- package/src/core/artifacts/index.ts +697 -44
- package/src/core/config.ts +183 -4
- package/src/core/diagnostics/bundle.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +16 -0
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/hooks/adapter/events.ts +19 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +116 -8
- package/src/core/hooks/cli.ts +47 -6
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +4 -1
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/gate.ts +22 -1
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local socket and process observation for the server registry.
|
|
3
|
+
*
|
|
4
|
+
* Linux reads procfs directly (no `ss`/`lsof` dependency, works under WSL).
|
|
5
|
+
* macOS shells out to `lsof` and `ps`. Other platforms report `unsupported`
|
|
6
|
+
* so callers can say the scan did not run rather than implying it found
|
|
7
|
+
* nothing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { readdirSync, readFileSync, readlinkSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
export type ScanSupport = "procfs" | "lsof" | "unsupported";
|
|
14
|
+
|
|
15
|
+
export interface ListeningSocket {
|
|
16
|
+
pid: number;
|
|
17
|
+
port: number;
|
|
18
|
+
address: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProcessInfo {
|
|
22
|
+
pid: number;
|
|
23
|
+
ppid: number | null;
|
|
24
|
+
command: string;
|
|
25
|
+
cwd: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function scanSupport(): ScanSupport {
|
|
29
|
+
if (process.platform === "linux") return "procfs";
|
|
30
|
+
if (process.platform === "darwin") return "lsof";
|
|
31
|
+
return "unsupported";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ProcSocketRow {
|
|
35
|
+
localAddress: string;
|
|
36
|
+
localPort: number;
|
|
37
|
+
state: string;
|
|
38
|
+
inode: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Parse `/proc/net/tcp` or `/proc/net/tcp6` content. */
|
|
42
|
+
export function parseProcNetTcp(content: string): ProcSocketRow[] {
|
|
43
|
+
const rows: ProcSocketRow[] = [];
|
|
44
|
+
for (const line of content.split("\n").slice(1)) {
|
|
45
|
+
const fields = line.trim().split(/\s+/);
|
|
46
|
+
if (fields.length < 10) continue;
|
|
47
|
+
const [address, portHex] = (fields[1] ?? "").split(":");
|
|
48
|
+
if (!address || !portHex) continue;
|
|
49
|
+
rows.push({
|
|
50
|
+
localAddress: decodeProcAddress(address),
|
|
51
|
+
localPort: Number.parseInt(portHex, 16),
|
|
52
|
+
state: fields[3] ?? "",
|
|
53
|
+
inode: fields[9] ?? "",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function decodeProcAddress(hex: string): string {
|
|
60
|
+
if (hex.length === 8) {
|
|
61
|
+
const bytes = hex.match(/../g)?.map((pair) => Number.parseInt(pair, 16)) ?? [];
|
|
62
|
+
return bytes.reverse().join(".");
|
|
63
|
+
}
|
|
64
|
+
if (/^0+$/.test(hex)) return "::";
|
|
65
|
+
if (hex === "00000000000000000000000001000000") return "::1";
|
|
66
|
+
if (hex.startsWith("0000000000000000FFFF0000")) return decodeProcAddress(hex.slice(24));
|
|
67
|
+
return `ipv6:${hex.toLowerCase()}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readProcSockets(): ProcSocketRow[] {
|
|
71
|
+
const rows: ProcSocketRow[] = [];
|
|
72
|
+
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
73
|
+
try {
|
|
74
|
+
rows.push(...parseProcNetTcp(readFileSync(file, "utf8")));
|
|
75
|
+
} catch {
|
|
76
|
+
// tcp6 is absent on IPv6-disabled kernels.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return rows;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Map socket inodes to the pids holding them (only processes we may read). */
|
|
83
|
+
function socketOwners(inodes: Set<string>): Map<string, number> {
|
|
84
|
+
const owners = new Map<string, number>();
|
|
85
|
+
if (inodes.size === 0) return owners;
|
|
86
|
+
let entries: string[];
|
|
87
|
+
try {
|
|
88
|
+
entries = readdirSync("/proc");
|
|
89
|
+
} catch {
|
|
90
|
+
return owners;
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (!/^\d+$/.test(entry)) continue;
|
|
94
|
+
let fds: string[];
|
|
95
|
+
try {
|
|
96
|
+
fds = readdirSync(`/proc/${entry}/fd`);
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
for (const fd of fds) {
|
|
101
|
+
let target: string;
|
|
102
|
+
try {
|
|
103
|
+
target = readlinkSync(`/proc/${entry}/fd/${fd}`);
|
|
104
|
+
} catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const match = /^socket:\[(\d+)\]$/.exec(target);
|
|
108
|
+
if (match?.[1] && inodes.has(match[1]) && !owners.has(match[1])) {
|
|
109
|
+
owners.set(match[1], Number(entry));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (owners.size === inodes.size) break;
|
|
113
|
+
}
|
|
114
|
+
return owners;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Listening TCP sockets whose owning process this user can see. */
|
|
118
|
+
export function listListeningSockets(): ListeningSocket[] {
|
|
119
|
+
const support = scanSupport();
|
|
120
|
+
if (support === "procfs") {
|
|
121
|
+
const listening = readProcSockets().filter((row) => row.state === "0A");
|
|
122
|
+
const owners = socketOwners(new Set(listening.map((row) => row.inode)));
|
|
123
|
+
const seen = new Set<string>();
|
|
124
|
+
const sockets: ListeningSocket[] = [];
|
|
125
|
+
for (const row of listening) {
|
|
126
|
+
const pid = owners.get(row.inode);
|
|
127
|
+
if (!pid) continue;
|
|
128
|
+
const key = `${pid}:${row.localPort}`;
|
|
129
|
+
if (seen.has(key)) continue;
|
|
130
|
+
seen.add(key);
|
|
131
|
+
sockets.push({ pid, port: row.localPort, address: row.localAddress });
|
|
132
|
+
}
|
|
133
|
+
return sockets.sort((a, b) => a.port - b.port);
|
|
134
|
+
}
|
|
135
|
+
if (support === "lsof") return lsofListeningSockets();
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Every listening TCP port on this machine, including ports whose owner this
|
|
141
|
+
* user cannot see (containers, other users). Used to avoid port collisions.
|
|
142
|
+
*/
|
|
143
|
+
export function listListeningPorts(): Set<number> {
|
|
144
|
+
const support = scanSupport();
|
|
145
|
+
if (support === "procfs")
|
|
146
|
+
return new Set(
|
|
147
|
+
readProcSockets()
|
|
148
|
+
.filter((row) => row.state === "0A")
|
|
149
|
+
.map((row) => row.localPort),
|
|
150
|
+
);
|
|
151
|
+
if (support === "lsof") return new Set(listListeningSockets().map((socket) => socket.port));
|
|
152
|
+
return new Set();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Parse `lsof -Fpn` output for listening sockets. */
|
|
156
|
+
export function parseLsofListen(output: string): ListeningSocket[] {
|
|
157
|
+
const sockets: ListeningSocket[] = [];
|
|
158
|
+
const seen = new Set<string>();
|
|
159
|
+
let pid = 0;
|
|
160
|
+
for (const line of output.split("\n")) {
|
|
161
|
+
if (line.startsWith("p")) pid = Number(line.slice(1));
|
|
162
|
+
else if (line.startsWith("n") && pid > 0) {
|
|
163
|
+
const match = /^(.*):(\d+)$/.exec(line.slice(1));
|
|
164
|
+
if (!match?.[2]) continue;
|
|
165
|
+
const port = Number(match[2]);
|
|
166
|
+
const key = `${pid}:${port}`;
|
|
167
|
+
if (seen.has(key)) continue;
|
|
168
|
+
seen.add(key);
|
|
169
|
+
sockets.push({ pid, port, address: (match[1] ?? "").replace(/^\[|\]$/g, "") });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return sockets.sort((a, b) => a.port - b.port);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Count established server-side connections per local port. A browser tab
|
|
177
|
+
* holding a live-reload socket or keep-alive connection counts as activity.
|
|
178
|
+
*/
|
|
179
|
+
export function establishedConnectionCounts(ports: number[]): Map<number, number> {
|
|
180
|
+
const counts = new Map<number, number>();
|
|
181
|
+
const wanted = new Set(ports.filter((port) => Number.isInteger(port) && port > 0));
|
|
182
|
+
if (wanted.size === 0) return counts;
|
|
183
|
+
const support = scanSupport();
|
|
184
|
+
if (support === "procfs") {
|
|
185
|
+
for (const row of readProcSockets()) {
|
|
186
|
+
// 01 ESTABLISHED; 03 SYN_RECV, where a deferred-accept server parks a
|
|
187
|
+
// connected client until its first bytes arrive.
|
|
188
|
+
if ((row.state !== "01" && row.state !== "03") || !wanted.has(row.localPort)) continue;
|
|
189
|
+
counts.set(row.localPort, (counts.get(row.localPort) ?? 0) + 1);
|
|
190
|
+
}
|
|
191
|
+
return counts;
|
|
192
|
+
}
|
|
193
|
+
if (support === "lsof") return lsofConnectionCounts(wanted);
|
|
194
|
+
return counts;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Listening sockets via `lsof` (the macOS path; also runs wherever lsof exists). */
|
|
198
|
+
export function lsofListeningSockets(): ListeningSocket[] {
|
|
199
|
+
return parseLsofListen(run("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-Fpn"]));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Server-side established connections per wanted local port, via `lsof`. */
|
|
203
|
+
export function lsofConnectionCounts(wanted: Set<number>): Map<number, number> {
|
|
204
|
+
const counts = new Map<number, number>();
|
|
205
|
+
const output = run("lsof", ["-nP", "-iTCP", "-sTCP:ESTABLISHED", "-Fn"]);
|
|
206
|
+
for (const line of output.split("\n")) {
|
|
207
|
+
const match = /^n.*?:(\d+)->/.exec(line);
|
|
208
|
+
const port = match?.[1] ? Number(match[1]) : 0;
|
|
209
|
+
if (wanted.has(port)) counts.set(port, (counts.get(port) ?? 0) + 1);
|
|
210
|
+
}
|
|
211
|
+
return counts;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Parent, command line, and working directory via `ps` and `lsof` (the macOS path). */
|
|
215
|
+
export function psProcessInfo(pid: number): ProcessInfo | null {
|
|
216
|
+
const ps = run("ps", ["-o", "ppid=,command=", "-p", String(pid)]).trim();
|
|
217
|
+
if (!ps) return null;
|
|
218
|
+
const match = /^(\d+)\s+(.*)$/.exec(ps);
|
|
219
|
+
const cwdLine = run("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"])
|
|
220
|
+
.split("\n")
|
|
221
|
+
.find((line) => line.startsWith("n"));
|
|
222
|
+
return {
|
|
223
|
+
pid,
|
|
224
|
+
ppid: match?.[1] ? Number(match[1]) : null,
|
|
225
|
+
command: match?.[2] ?? ps,
|
|
226
|
+
cwd: cwdLine ? cwdLine.slice(1) : null,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function readProcessInfo(pid: number): ProcessInfo | null {
|
|
231
|
+
if (process.platform === "linux") {
|
|
232
|
+
try {
|
|
233
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
234
|
+
const afterComm = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
235
|
+
const ppid = Number(afterComm[1]);
|
|
236
|
+
const command = readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").join(" ").trim();
|
|
237
|
+
let cwd: string | null = null;
|
|
238
|
+
try {
|
|
239
|
+
cwd = readlinkSync(`/proc/${pid}/cwd`);
|
|
240
|
+
} catch {
|
|
241
|
+
cwd = null;
|
|
242
|
+
}
|
|
243
|
+
return { pid, ppid: Number.isInteger(ppid) ? ppid : null, command, cwd };
|
|
244
|
+
} catch {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (process.platform === "darwin") return psProcessInfo(pid);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Parse `ps -o etime=` output (`[[dd-]hh:]mm:ss`) into seconds. */
|
|
253
|
+
export function parseElapsed(value: string): number | null {
|
|
254
|
+
const match = /^\s*(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)\s*$/.exec(value);
|
|
255
|
+
if (!match) return null;
|
|
256
|
+
const [, days, hours, minutes, seconds] = match;
|
|
257
|
+
return (
|
|
258
|
+
Number(days ?? 0) * 86_400 + Number(hours ?? 0) * 3_600 + Number(minutes) * 60 + Number(seconds)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** When a process started, from its elapsed run time; null when unavailable. */
|
|
263
|
+
export function processStartedAt(pid: number, now = Date.now()): string | null {
|
|
264
|
+
if (process.platform !== "linux" && process.platform !== "darwin") return null;
|
|
265
|
+
const elapsed = parseElapsed(run("ps", ["-o", "etime=", "-p", String(pid)]));
|
|
266
|
+
return elapsed === null ? null : new Date(now - elapsed * 1_000).toISOString();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The pid followed by its ancestors, nearest first (bounded). */
|
|
270
|
+
export function ancestry(pid: number, maxHops = 32): number[] {
|
|
271
|
+
const chain: number[] = [];
|
|
272
|
+
let current: number | null = pid;
|
|
273
|
+
while (current && current > 1 && chain.length < maxHops && !chain.includes(current)) {
|
|
274
|
+
chain.push(current);
|
|
275
|
+
current = readProcessInfo(current)?.ppid ?? null;
|
|
276
|
+
}
|
|
277
|
+
return chain;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function run(command: string, args: string[]): string {
|
|
281
|
+
const result = spawnSync(command, args, { encoding: "utf8", timeout: 10_000 });
|
|
282
|
+
return result.status === 0 || result.stdout ? (result.stdout ?? "") : "";
|
|
283
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tunnels as registry services.
|
|
3
|
+
*
|
|
4
|
+
* `tunnel up` and `tunnel reload` register the instance; `tunnel down`
|
|
5
|
+
* removes it. Tunnels started before the registry existed are adopted the next
|
|
6
|
+
* time the registry is read, so a long-lived tunnel never shows as an
|
|
7
|
+
* unregistered listener.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { gateLogFile, type TunnelState } from "../../lib/tunnel/state.ts";
|
|
12
|
+
import { resolveBinName } from "../config.ts";
|
|
13
|
+
import type { RegisterServerInput } from "./index.ts";
|
|
14
|
+
|
|
15
|
+
export function tunnelServerId(name: string): string {
|
|
16
|
+
return `tunnel-${name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-")}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The registry input for one tunnel instance. */
|
|
20
|
+
export function tunnelServerInput(state: TunnelState, coordRoot: string): RegisterServerInput {
|
|
21
|
+
const helpers = [state.cloudflared_pid, state.provider_pid].filter(
|
|
22
|
+
(pid): pid is number => typeof pid === "number" && pid > 0,
|
|
23
|
+
);
|
|
24
|
+
return {
|
|
25
|
+
id: tunnelServerId(state.name),
|
|
26
|
+
kind: "service",
|
|
27
|
+
type: "tunnel",
|
|
28
|
+
label: `Tunnel ${state.name} → ${state.target}${state.vhost ? ` (${state.vhost})` : ""}`,
|
|
29
|
+
url: state.url,
|
|
30
|
+
port: state.gate_port,
|
|
31
|
+
pid: state.gate_pid,
|
|
32
|
+
...(helpers.length ? { pids: helpers } : {}),
|
|
33
|
+
log: resolve(coordRoot, ".cache", "tunnel", gateLogFile(state.name)),
|
|
34
|
+
cwd: coordRoot,
|
|
35
|
+
stop_argv: [resolveBinName(coordRoot), "tunnel", "down", "--name", state.name],
|
|
36
|
+
started_at: state.started_at,
|
|
37
|
+
owner: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -476,6 +476,7 @@ function cacheFamilies(): HarneryStorageFamily[] {
|
|
|
476
476
|
["pid-map-cache", ".harnery/pid-map", "active native process observations"],
|
|
477
477
|
["remote-presence-cache", ".harnery/presence/remote", "presence relay observations"],
|
|
478
478
|
["event-v3-live-display", ".harnery/live/v3", "Event Ledger V3 canonical events"],
|
|
479
|
+
["local-server-registry", ".harnery/servers", "running local server processes"],
|
|
479
480
|
] as const;
|
|
480
481
|
return [
|
|
481
482
|
...definitions.map(([id, root, source]) =>
|
|
@@ -21,6 +21,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
21
21
|
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
22
22
|
import { isAbsolute, join, resolve } from "node:path";
|
|
23
23
|
import { pathToFileURL } from "node:url";
|
|
24
|
+
import { workflowWorkerFullAccessConfig } from "../config.ts";
|
|
24
25
|
import { snapshotRepo } from "../context/index.ts";
|
|
25
26
|
import type {
|
|
26
27
|
ExternalMutationRequest,
|
|
@@ -69,6 +70,7 @@ import type {
|
|
|
69
70
|
BlockedInput,
|
|
70
71
|
EngineOpts,
|
|
71
72
|
GitAdministrativeGrant,
|
|
73
|
+
ParentAccessEvidence,
|
|
72
74
|
RunReport,
|
|
73
75
|
SpawnFilesystemPolicy,
|
|
74
76
|
SpawnResult,
|
|
@@ -88,6 +90,11 @@ import type {
|
|
|
88
90
|
import { WORKFLOW_DIAGNOSTIC_ADMISSION_SCHEMA_VERSION } from "./types.ts";
|
|
89
91
|
import { parseStageOutput, validateAgainstSchema } from "./validate.ts";
|
|
90
92
|
import { freezeWorkflowWorkContext } from "./work-context.ts";
|
|
93
|
+
import {
|
|
94
|
+
decideWorkerAccess,
|
|
95
|
+
detectParentAccess,
|
|
96
|
+
parseWorkerFullAccessPolicy,
|
|
97
|
+
} from "./worker-access.ts";
|
|
91
98
|
import {
|
|
92
99
|
attestTerminal,
|
|
93
100
|
attestWorkspaceFailure,
|
|
@@ -464,6 +471,13 @@ async function executeWorkflow(
|
|
|
464
471
|
writableRoots: [...(requestedPolicy.writableRoots ?? []), ...gitGrantRoots],
|
|
465
472
|
}
|
|
466
473
|
: undefined;
|
|
474
|
+
// Worker full access (ADR 0192) is decided per launch. The host policy is
|
|
475
|
+
// read once per run; the parent probe runs at most once, and only when a
|
|
476
|
+
// launch has already cleared every cheaper condition.
|
|
477
|
+
const workerFullAccess = parseWorkerFullAccessPolicy(
|
|
478
|
+
workflowWorkerFullAccessConfig(opts.coordRoot),
|
|
479
|
+
);
|
|
480
|
+
let parentAccess: ParentAccessEvidence | undefined;
|
|
467
481
|
const executionRepoBefore = resumeState?.manifest.repository_before ?? snapshotRepo(executionCwd);
|
|
468
482
|
if (!resumeState) {
|
|
469
483
|
writeWorkflowRunManifest({
|
|
@@ -992,6 +1006,22 @@ async function executeWorkflow(
|
|
|
992
1006
|
|
|
993
1007
|
if (!spawnCountClaimed) agentsSpawned++;
|
|
994
1008
|
|
|
1009
|
+
const access = decideWorkerAccess({
|
|
1010
|
+
adapter,
|
|
1011
|
+
model: agentOpts.model,
|
|
1012
|
+
effort: agentOpts.effort,
|
|
1013
|
+
policy: workerFullAccess,
|
|
1014
|
+
filesystemPolicy,
|
|
1015
|
+
parent: () => {
|
|
1016
|
+
if (!parentAccess) {
|
|
1017
|
+
parentAccess = (opts.detectParentAccess ?? detectParentAccess)();
|
|
1018
|
+
transcript("worker_access.parent", { ...parentAccess });
|
|
1019
|
+
}
|
|
1020
|
+
return parentAccess;
|
|
1021
|
+
},
|
|
1022
|
+
});
|
|
1023
|
+
agentProof.access = access;
|
|
1024
|
+
|
|
995
1025
|
await acquire();
|
|
996
1026
|
try {
|
|
997
1027
|
await ensureDiagnosticAdmissionObserved();
|
|
@@ -1003,7 +1033,11 @@ async function executeWorkflow(
|
|
|
1003
1033
|
specialist: agentOpts.specialist ?? null,
|
|
1004
1034
|
model: agentOpts.model ?? null,
|
|
1005
1035
|
effort: agentOpts.effort ?? null,
|
|
1036
|
+
access,
|
|
1006
1037
|
});
|
|
1038
|
+
if (access.mode === "full-access") {
|
|
1039
|
+
log(`[access] ${id} ${adapter}: full access (${access.detail})`);
|
|
1040
|
+
}
|
|
1007
1041
|
log(`[${name}] ${currentStage || "(no stage)"} → ${id} [${adapter}] ${label}`);
|
|
1008
1042
|
|
|
1009
1043
|
// Headless children do not reliably fire adapter hooks. The engine owns
|
|
@@ -1033,7 +1067,10 @@ async function executeWorkflow(
|
|
|
1033
1067
|
runId,
|
|
1034
1068
|
agentId: id,
|
|
1035
1069
|
subscriptionOnly,
|
|
1036
|
-
|
|
1070
|
+
// A full-access launch has no sandbox to project a policy into.
|
|
1071
|
+
...(access.mode === "full-access"
|
|
1072
|
+
? { access: "full-access" as const }
|
|
1073
|
+
: { filesystemPolicy }),
|
|
1037
1074
|
});
|
|
1038
1075
|
agentProof.attempts = attempt;
|
|
1039
1076
|
agentProof.duration_ms += last.durationMs;
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* not, which is worse than refusing and worse than never offering the feature.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { builtinAdapterProfile } from "../adapters/profiles.ts";
|
|
17
18
|
import type { AdapterSandboxProjection } from "../adapters/types.ts";
|
|
18
|
-
import type { GitAdministrativeGrant, SpawnFilesystemPolicy } from "./types.ts";
|
|
19
|
+
import type { GitAdministrativeGrant, SpawnFilesystemPolicy, SpawnRequest } from "./types.ts";
|
|
19
20
|
import type { WorkspaceBinding } from "./workspaces/types.ts";
|
|
20
21
|
|
|
21
22
|
export class SandboxProjectionError extends Error {
|
|
@@ -25,7 +26,9 @@ export class SandboxProjectionError extends Error {
|
|
|
25
26
|
| "writable_roots_unrepresentable"
|
|
26
27
|
| "no_projection"
|
|
27
28
|
| "writable_root_escapes_workspace"
|
|
28
|
-
| "git_grant_unavailable"
|
|
29
|
+
| "git_grant_unavailable"
|
|
30
|
+
| "full_access_unrepresentable"
|
|
31
|
+
| "full_access_conflicts_with_policy";
|
|
29
32
|
|
|
30
33
|
constructor(adapter: string, reason: SandboxProjectionError["reason"], message: string) {
|
|
31
34
|
super(message);
|
|
@@ -148,3 +151,36 @@ export function resolveGitGrantRoots(
|
|
|
148
151
|
// repeated writable root is noise in the rendered argv and in proof.
|
|
149
152
|
return [...new Set([repository.gitdir.realpath, repository.common_dir.realpath])];
|
|
150
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The vendor arguments for an unsandboxed launch, or undefined for an ordinary
|
|
157
|
+
* sandboxed one (ADR 0192).
|
|
158
|
+
*
|
|
159
|
+
* The engine decides whether a worker qualifies; this only renders the result,
|
|
160
|
+
* and refuses rather than degrades. An adapter with no declared full-access
|
|
161
|
+
* rendering cannot honor the request, and a request that also carries a
|
|
162
|
+
* filesystem policy asks for two contradictory things, so both fail before
|
|
163
|
+
* launch instead of silently picking one.
|
|
164
|
+
*/
|
|
165
|
+
export function resolveFullAccessArgv(
|
|
166
|
+
adapter: string,
|
|
167
|
+
req: Pick<SpawnRequest, "access" | "filesystemPolicy">,
|
|
168
|
+
): readonly string[] | undefined {
|
|
169
|
+
if (req.access !== "full-access") return undefined;
|
|
170
|
+
if (req.filesystemPolicy) {
|
|
171
|
+
throw new SandboxProjectionError(
|
|
172
|
+
adapter,
|
|
173
|
+
"full_access_conflicts_with_policy",
|
|
174
|
+
`${adapter} was asked for full access and a "${req.filesystemPolicy.mode}" filesystem policy at once; a launch gets one or the other`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const rendering = builtinAdapterProfile(adapter)?.fullAccess;
|
|
178
|
+
if (!rendering) {
|
|
179
|
+
throw new SandboxProjectionError(
|
|
180
|
+
adapter,
|
|
181
|
+
"full_access_unrepresentable",
|
|
182
|
+
`${adapter} declares no full-access mode, so a worker on it cannot run without its sandbox`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return rendering.argv;
|
|
186
|
+
}
|
|
@@ -23,7 +23,7 @@ import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profil
|
|
|
23
23
|
import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
|
|
24
24
|
import { notFoundError } from "./adapters.ts";
|
|
25
25
|
import { buildChildEnv } from "./child-env.ts";
|
|
26
|
-
import { resolveSandboxProjection } from "./sandbox-projection.ts";
|
|
26
|
+
import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.ts";
|
|
27
27
|
import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.ts";
|
|
28
28
|
import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
|
|
29
29
|
|
|
@@ -56,6 +56,10 @@ export function buildClaudeInvocation(req: SpawnRequest): AdapterInvocation {
|
|
|
56
56
|
"--max-turns",
|
|
57
57
|
String(req.maxTurns),
|
|
58
58
|
];
|
|
59
|
+
// A qualified launch removes the permission prompt and the Bash sandbox
|
|
60
|
+
// (ADR 0192); an ordinary one leaves Claude Code's own settings in charge.
|
|
61
|
+
const fullAccess = resolveFullAccessArgv("claude-code", req);
|
|
62
|
+
if (fullAccess) argv.push(...fullAccess);
|
|
59
63
|
if (req.model) argv.push("--model", req.model);
|
|
60
64
|
if (req.effort) argv.push("--effort", req.effort);
|
|
61
65
|
return { argv };
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* experimental `--json` JSONL event stream.
|
|
11
11
|
* - `--skip-git-repo-check` keeps non-repo cwds working; `--sandbox
|
|
12
12
|
* workspace-write` matches workflow-stage expectations (children may edit).
|
|
13
|
+
* A launch the engine qualified for full access renders `--sandbox
|
|
14
|
+
* danger-full-access` instead (ADR 0192).
|
|
13
15
|
* - No per-run cost or session-id surface in this mode → both left undefined.
|
|
14
16
|
* - No max-turns equivalent → `maxTurns` is accepted and ignored (documented
|
|
15
17
|
* in the CLI docs page).
|
|
@@ -24,13 +26,28 @@ import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profil
|
|
|
24
26
|
import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
|
|
25
27
|
import { notFoundError } from "./adapters.ts";
|
|
26
28
|
import { buildChildEnv } from "./child-env.ts";
|
|
27
|
-
import { resolveSandboxProjection } from "./sandbox-projection.ts";
|
|
29
|
+
import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.ts";
|
|
28
30
|
import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.ts";
|
|
29
31
|
import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
|
|
30
32
|
|
|
31
33
|
export function buildCodexInvocation(req: SpawnRequest, resultFile?: string): AdapterInvocation {
|
|
32
34
|
validateAdapterEffort("codex", req.effort);
|
|
33
35
|
if (!resultFile) throw new Error("codex adapter requires a final-message result file");
|
|
36
|
+
const fullAccess = resolveFullAccessArgv("codex", req);
|
|
37
|
+
if (fullAccess) {
|
|
38
|
+
const argv = [
|
|
39
|
+
"codex",
|
|
40
|
+
"exec",
|
|
41
|
+
req.prompt,
|
|
42
|
+
"--output-last-message",
|
|
43
|
+
resultFile,
|
|
44
|
+
"--skip-git-repo-check",
|
|
45
|
+
...fullAccess,
|
|
46
|
+
];
|
|
47
|
+
if (req.model) argv.push("--model", req.model);
|
|
48
|
+
if (req.effort) argv.push("-c", `model_reasoning_effort=${JSON.stringify(req.effort)}`);
|
|
49
|
+
return { argv, resultFile };
|
|
50
|
+
}
|
|
34
51
|
// Default stays workspace-write so an unprojected request is unchanged.
|
|
35
52
|
const projection = req.filesystemPolicy
|
|
36
53
|
? resolveSandboxProjection(
|
|
@@ -24,7 +24,7 @@ import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profil
|
|
|
24
24
|
import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
|
|
25
25
|
import { notFoundError } from "./adapters.ts";
|
|
26
26
|
import { buildChildEnv } from "./child-env.ts";
|
|
27
|
-
import { resolveSandboxProjection } from "./sandbox-projection.ts";
|
|
27
|
+
import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.ts";
|
|
28
28
|
import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.ts";
|
|
29
29
|
import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
|
|
30
30
|
|
|
@@ -69,6 +69,10 @@ export function buildCursorInvocation(req: SpawnRequest): AdapterInvocation {
|
|
|
69
69
|
// commands. Neither flag maps host policy into child tools; that capability
|
|
70
70
|
// remains explicitly unsupported.
|
|
71
71
|
const argv = ["cursor-agent", "-p", req.prompt, "--output-format", "json", "--trust", "--force"];
|
|
72
|
+
// A qualified launch also turns the Cursor sandbox off (ADR 0192); an
|
|
73
|
+
// ordinary one leaves the configured sandbox setting untouched.
|
|
74
|
+
const fullAccess = resolveFullAccessArgv("cursor", req);
|
|
75
|
+
if (fullAccess) argv.push(...fullAccess);
|
|
72
76
|
if (req.model) argv.push("--model", req.model);
|
|
73
77
|
return { argv };
|
|
74
78
|
}
|
|
@@ -22,7 +22,7 @@ import { builtinAdapterProfile, validateAdapterEffort } from "../adapters/profil
|
|
|
22
22
|
import type { AdapterInvocation, AdapterRawResult } from "../adapters/types.ts";
|
|
23
23
|
import { notFoundError } from "./adapters.ts";
|
|
24
24
|
import { buildChildEnv } from "./child-env.ts";
|
|
25
|
-
import { resolveSandboxProjection } from "./sandbox-projection.ts";
|
|
25
|
+
import { resolveFullAccessArgv, resolveSandboxProjection } from "./sandbox-projection.ts";
|
|
26
26
|
import { isUpstreamFailureText, vendorFailureText } from "./spawn-failure.ts";
|
|
27
27
|
import type { Spawner, SpawnRequest, SpawnResult } from "./types.ts";
|
|
28
28
|
|
|
@@ -75,6 +75,9 @@ export function buildOpenCodeInvocation(req: SpawnRequest): AdapterInvocation {
|
|
|
75
75
|
req.filesystemPolicy,
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
|
+
// OpenCode declares no full-access rendering, so this refuses a full-access
|
|
79
|
+
// request instead of launching it as an ordinary one (ADR 0192).
|
|
80
|
+
resolveFullAccessArgv("opencode", req);
|
|
78
81
|
// `--auto` auto-approves permissions that are not explicitly denied, which a
|
|
79
82
|
// headless workflow child needs because it has no interactive approval
|
|
80
83
|
// channel; it does not map host policy into child tools (unsupported).
|
|
@@ -142,6 +142,9 @@ export interface WorkflowAgentProof {
|
|
|
142
142
|
* Absent ⇒ a work failure. Recorded even when a script's parallel() swallows
|
|
143
143
|
* the rejection, so the run-level class can still be derived from proof. */
|
|
144
144
|
class?: SpawnFailureClass;
|
|
145
|
+
/** Which vendor sandbox mode this agent launched with and why (ADR 0192).
|
|
146
|
+
* Absent on a cached agent, which launched nothing. */
|
|
147
|
+
access?: WorkerAccessDecision;
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
export interface WorkflowRepoSnapshot {
|
|
@@ -423,6 +426,57 @@ export interface SpawnRequest {
|
|
|
423
426
|
* is what shared-checkout runs use. An adapter that cannot represent the
|
|
424
427
|
* requested projection refuses before launch rather than downgrading. */
|
|
425
428
|
filesystemPolicy?: SpawnFilesystemPolicy;
|
|
429
|
+
/** Launch without the vendor sandbox (ADR 0192). Absent means sandboxed,
|
|
430
|
+
* which is the adapter's ordinary invocation. The engine sets this only after
|
|
431
|
+
* the host policy, the parent's own access, and the model floor all agree; a
|
|
432
|
+
* spawner refuses it when its adapter declares no full-access rendering, and
|
|
433
|
+
* refuses it alongside a filesystem policy, which it would contradict. */
|
|
434
|
+
access?: WorkerAccessMode;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Whether a workflow worker runs inside its vendor sandbox (ADR 0192).
|
|
439
|
+
*
|
|
440
|
+
* `sandboxed` is today's projection: read-only or workspace-write, with any
|
|
441
|
+
* declared writable roots and Git grant. `full-access` removes the vendor
|
|
442
|
+
* sandbox entirely and is only ever granted per launch, never as a run default.
|
|
443
|
+
*/
|
|
444
|
+
export type WorkerAccessMode = "sandboxed" | "full-access";
|
|
445
|
+
|
|
446
|
+
/** Why a worker got the access mode it did. `qualified` is the only reason that
|
|
447
|
+
* accompanies `full-access`; every other reason is a sandboxed launch. */
|
|
448
|
+
export type WorkerAccessReason =
|
|
449
|
+
| "qualified"
|
|
450
|
+
| "disabled"
|
|
451
|
+
| "config_invalid"
|
|
452
|
+
| "adapter_unrepresentable"
|
|
453
|
+
| "policy_read_only"
|
|
454
|
+
| "parent_sandboxed"
|
|
455
|
+
| "parent_unknown"
|
|
456
|
+
| "model_unset"
|
|
457
|
+
| "model_not_listed"
|
|
458
|
+
| "effort_unset"
|
|
459
|
+
| "effort_below_floor";
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* What Harnery could prove about the access of the session that launched this
|
|
463
|
+
* run (ADR 0192). Only positive evidence yields `full-access`; a missing,
|
|
464
|
+
* unreadable, or contradictory signal is `unknown`, which is treated exactly
|
|
465
|
+
* like `sandboxed`.
|
|
466
|
+
*/
|
|
467
|
+
export interface ParentAccessEvidence {
|
|
468
|
+
state: "full-access" | "sandboxed" | "unknown";
|
|
469
|
+
/** Adapter session markers found in the environment, in the order checked. */
|
|
470
|
+
adapters: string[];
|
|
471
|
+
detail: string;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** One launch's recorded access decision (ADR 0192). */
|
|
475
|
+
export interface WorkerAccessDecision {
|
|
476
|
+
mode: WorkerAccessMode;
|
|
477
|
+
reason: WorkerAccessReason;
|
|
478
|
+
/** Plain-language explanation naming the evidence the decision rests on. */
|
|
479
|
+
detail: string;
|
|
426
480
|
}
|
|
427
481
|
|
|
428
482
|
/**
|
|
@@ -437,8 +491,10 @@ export interface SpawnRequest {
|
|
|
437
491
|
*/
|
|
438
492
|
export type GitAdministrativeGrant = "none" | "shared-repository";
|
|
439
493
|
|
|
440
|
-
/** What the host has decided
|
|
441
|
-
*
|
|
494
|
+
/** What the host has decided a sandboxed child may write. There is no
|
|
495
|
+
* `full-access` mode here: removing the sandbox is a separate, per-launch
|
|
496
|
+
* decision carried by `SpawnRequest.access` (ADR 0192), because it depends on
|
|
497
|
+
* the parent's own access and the worker's model rather than on the run. */
|
|
442
498
|
export interface SpawnFilesystemPolicy {
|
|
443
499
|
mode: "read-only" | "workspace-write";
|
|
444
500
|
/** Explicit absolute paths the child may write, declared rather than derived
|
|
@@ -608,6 +664,10 @@ export interface EngineOpts {
|
|
|
608
664
|
allowApiBilling?: boolean;
|
|
609
665
|
/** Billing-probe override for tests (default: the real probeBilling). */
|
|
610
666
|
probeBilling?: BillingProber;
|
|
667
|
+
/** Parent-access probe override for tests (default: the real
|
|
668
|
+
* detectParentAccess). Consulted only when the host enables worker full
|
|
669
|
+
* access, and at most once per run (ADR 0192). */
|
|
670
|
+
detectParentAccess?: () => ParentAccessEvidence;
|
|
611
671
|
/** Capability claims used to state whether adapter-native tool evidence was
|
|
612
672
|
* available. Missing claims remain unknown. */
|
|
613
673
|
adapterEvidence?: Readonly<Record<AdapterName, AdapterEvidenceCapability | undefined>>;
|