@modootoday/pmcp 0.11.0 → 0.13.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/README.md +92 -494
- package/dist/cli.js +9307 -3930
- package/dist/{embed-BBhv3y5x.d.ts → embed-BW6AWD1e.d.ts} +1 -1
- package/dist/harness-egress-bridge.d.ts +2 -0
- package/dist/harness-egress-bridge.js +74 -0
- package/dist/harness-launch-gate.d.ts +2 -0
- package/dist/harness-launch-gate.js +1187 -0
- package/dist/harness-pane-runner.d.ts +2 -0
- package/dist/harness-pane-runner.js +118 -0
- package/dist/harness-worker.d.ts +2 -0
- package/dist/harness-worker.js +2737 -0
- package/dist/http.d.ts +3 -3
- package/dist/http.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1 -1
- package/dist/stdio.d.ts +1 -1
- package/dist/stdio.js +1 -1
- package/dist/tui-worker.d.ts +2 -0
- package/dist/tui-worker.js +5973 -0
- package/docs/embeddings.md +125 -0
- package/docs/hosting.md +46 -0
- package/docs/mailbox.md +59 -0
- package/docs/project-configuration.md +203 -0
- package/docs/skills.md +74 -0
- package/docs/terminal-harness.md +393 -0
- package/docs/terminal-workspace.md +116 -0
- package/package.json +3 -2
- package/dist/{server-CwWcRjqz.d.ts → server-9w4R1caM.d.ts} +4 -4
|
@@ -0,0 +1,2737 @@
|
|
|
1
|
+
// src/harness/application/worker.ts
|
|
2
|
+
import { unlinkSync as unlinkSync4 } from "fs";
|
|
3
|
+
|
|
4
|
+
// src/harness/groups/store.ts
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import {
|
|
7
|
+
closeSync,
|
|
8
|
+
constants,
|
|
9
|
+
fstatSync,
|
|
10
|
+
fsyncSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
writeFileSync
|
|
16
|
+
} from "fs";
|
|
17
|
+
import { dirname, join } from "path";
|
|
18
|
+
|
|
19
|
+
// src/harness/validation.ts
|
|
20
|
+
function object(value) {
|
|
21
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
22
|
+
throw new Error("invalid_object");
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function text(value, name) {
|
|
27
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
28
|
+
throw new Error(`invalid_${name}`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function integer(value, name, min, max) {
|
|
33
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min || value > max) {
|
|
34
|
+
throw new Error(`invalid_${name}`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function strings(value, name) {
|
|
39
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 64) {
|
|
40
|
+
throw new Error(`invalid_${name}`);
|
|
41
|
+
}
|
|
42
|
+
return value.map((entry) => text(entry, name));
|
|
43
|
+
}
|
|
44
|
+
function id(value, name) {
|
|
45
|
+
const result = text(value, name);
|
|
46
|
+
if (!/^[a-zA-Z0-9-]{1,80}$/.test(result)) throw new Error(`invalid_${name}`);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/harness/runtimes/docker-policy.ts
|
|
51
|
+
import { isAbsolute as isAbsolute3, posix as posix3 } from "path";
|
|
52
|
+
|
|
53
|
+
// src/harness/runtimes/registry.ts
|
|
54
|
+
var runtimeExecutables = {
|
|
55
|
+
"codex-cli": "codex",
|
|
56
|
+
"claude-code": "claude",
|
|
57
|
+
"gemini-cli": "gemini",
|
|
58
|
+
"grok-cli": "grok",
|
|
59
|
+
antigravity: "agy"
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/harness/runtimes/egress-policy.ts
|
|
63
|
+
import { isAbsolute, posix } from "path";
|
|
64
|
+
function egressPolicy(value) {
|
|
65
|
+
const policy = object(value);
|
|
66
|
+
const hosts = strings(policy.hosts, "egress_hosts").map(
|
|
67
|
+
(host) => host.toLowerCase()
|
|
68
|
+
);
|
|
69
|
+
if (hosts.length > 16 || new Set(hosts).size !== hosts.length)
|
|
70
|
+
throw new Error("invalid_egress_hosts");
|
|
71
|
+
for (const host of hosts) {
|
|
72
|
+
if (host.length > 253 || !host.includes(".") || /^[0-9.]+$/.test(host) || !host.split(".").every(
|
|
73
|
+
(label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)
|
|
74
|
+
) || /\.(local|localhost|internal|invalid|test)$/.test(host))
|
|
75
|
+
throw new Error("invalid_egress_host");
|
|
76
|
+
}
|
|
77
|
+
if (policy.bridgeExecutable === void 0) return { hosts };
|
|
78
|
+
const executable = text(policy.bridgeExecutable, "egress_bridge_executable");
|
|
79
|
+
if (!isAbsolute(executable) || posix.normalize(executable) !== executable || /[\r\n\x1b]/.test(executable))
|
|
80
|
+
throw new Error("invalid_egress_bridge_executable");
|
|
81
|
+
return { hosts, bridgeExecutable: executable };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/harness/runtimes/private-profiles.ts
|
|
85
|
+
import { isAbsolute as isAbsolute2, posix as posix2 } from "path";
|
|
86
|
+
function profileName(value) {
|
|
87
|
+
const name = text(value, "private_profile");
|
|
88
|
+
if (!/^[a-z][a-z0-9-]{0,31}$/.test(name))
|
|
89
|
+
throw new Error("invalid_private_profile_name");
|
|
90
|
+
return name;
|
|
91
|
+
}
|
|
92
|
+
function privateProfiles(value) {
|
|
93
|
+
const entries = Object.entries(object(value));
|
|
94
|
+
if (!entries.length || entries.length > 16)
|
|
95
|
+
throw new Error("invalid_private_profile_grants");
|
|
96
|
+
const result = {};
|
|
97
|
+
for (const [name, entry] of entries) {
|
|
98
|
+
profileName(name);
|
|
99
|
+
const grant = object(entry);
|
|
100
|
+
const runtime = text(grant.runtime, "private_profile_runtime");
|
|
101
|
+
const directory = absolute(grant.directory, "private_profile_directory");
|
|
102
|
+
const lockExecutable = absolute(
|
|
103
|
+
grant.lockExecutable,
|
|
104
|
+
"private_profile_lock_executable"
|
|
105
|
+
);
|
|
106
|
+
const identity = grant.identity === void 0 ? void 0 : text(grant.identity, "private_profile_identity");
|
|
107
|
+
if (identity !== void 0 && !/^\d+:\d+:\d+:\d+$/.test(identity))
|
|
108
|
+
throw new Error("invalid_private_profile_identity");
|
|
109
|
+
result[name] = {
|
|
110
|
+
runtime,
|
|
111
|
+
directory,
|
|
112
|
+
lockExecutable,
|
|
113
|
+
...identity ? { identity } : {}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (new Set(Object.values(result).map((grant) => grant.directory)).size !== entries.length)
|
|
117
|
+
throw new Error("duplicate_private_profile_directory");
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function absolute(value, field) {
|
|
121
|
+
const path = text(value, field);
|
|
122
|
+
if (!isAbsolute2(path) || posix2.normalize(path) !== path || /[\r\n\x1b]/.test(path))
|
|
123
|
+
throw new Error("invalid_private_profile_path");
|
|
124
|
+
return path;
|
|
125
|
+
}
|
|
126
|
+
function selectedProfile(policy, name, runtime) {
|
|
127
|
+
if (name === void 0) return;
|
|
128
|
+
const grant = policy.privateProfiles?.[profileName(name)];
|
|
129
|
+
if (!grant || grant.runtime !== runtime || !grant.identity)
|
|
130
|
+
throw new Error("private_profile_not_granted");
|
|
131
|
+
return grant;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/harness/runtimes/docker-policy.ts
|
|
135
|
+
function dockerPolicy(value) {
|
|
136
|
+
const policy = object(value);
|
|
137
|
+
if (policy.schemaVersion !== 1)
|
|
138
|
+
throw new Error("invalid_docker_policy_version");
|
|
139
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(text(policy.image, "docker_image")))
|
|
140
|
+
throw new Error("docker_image_requires_digest");
|
|
141
|
+
if (policy.network !== "none")
|
|
142
|
+
throw new Error("docker_network_not_qualified");
|
|
143
|
+
if (policy.workspaceAccess !== "read-only" && policy.workspaceAccess !== "read-write")
|
|
144
|
+
throw new Error("invalid_docker_workspace_access");
|
|
145
|
+
const commands = object(policy.commands);
|
|
146
|
+
const entries = Object.entries(commands);
|
|
147
|
+
if (!entries.length || entries.length > 5)
|
|
148
|
+
throw new Error("invalid_docker_runtime_commands");
|
|
149
|
+
const parsed = {};
|
|
150
|
+
for (const [runtime, value2] of entries) {
|
|
151
|
+
if (!Object.hasOwn(runtimeExecutables, runtime))
|
|
152
|
+
throw new Error("unsupported_docker_runtime");
|
|
153
|
+
const command2 = strings(value2, "docker_runtime_command");
|
|
154
|
+
const executable = command2[0];
|
|
155
|
+
if (!isAbsolute3(executable) || posix3.normalize(executable) !== executable || command2.some((argument) => /[\r\n\x1b]/.test(argument)))
|
|
156
|
+
throw new Error("invalid_docker_runtime_command");
|
|
157
|
+
parsed[runtime] = command2;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
schemaVersion: 1,
|
|
161
|
+
image: text(policy.image, "docker_image"),
|
|
162
|
+
network: "none",
|
|
163
|
+
workspaceAccess: policy.workspaceAccess,
|
|
164
|
+
commands: parsed,
|
|
165
|
+
...policy.egress === void 0 ? {} : { egress: egressPolicy(policy.egress) },
|
|
166
|
+
...policy.privateProfiles === void 0 ? {} : { privateProfiles: privateProfiles(policy.privateProfiles) }
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/harness/groups/validation.ts
|
|
171
|
+
function member(value, allowed) {
|
|
172
|
+
if (!allowed.includes(value)) throw new Error("invalid_state_value");
|
|
173
|
+
}
|
|
174
|
+
function records(value) {
|
|
175
|
+
if (!Array.isArray(value) || value.length > 4096)
|
|
176
|
+
throw new Error("invalid_state_collection");
|
|
177
|
+
return value.map(object);
|
|
178
|
+
}
|
|
179
|
+
function unique(values) {
|
|
180
|
+
if (new Set(values).size !== values.length)
|
|
181
|
+
throw new Error("duplicate_state_identity");
|
|
182
|
+
}
|
|
183
|
+
function validateState(value) {
|
|
184
|
+
const state = object(value);
|
|
185
|
+
const group = object(state.group);
|
|
186
|
+
member(group.schemaVersion, [1]);
|
|
187
|
+
for (const field of ["owner", "id", "generation"]) id(group[field], field);
|
|
188
|
+
text(group.projectRoot, "project_root");
|
|
189
|
+
strings(group.allowedRuntimes, "allowed_runtimes");
|
|
190
|
+
member(group.profile, ["native", "docker"]);
|
|
191
|
+
if (group.profile === "docker") dockerPolicy(group.dockerPolicy);
|
|
192
|
+
if (group.profile === "native" && group.dockerPolicy !== void 0)
|
|
193
|
+
throw new Error("docker_policy_requires_docker_profile");
|
|
194
|
+
member(group.state, ["ready", "paused", "stopped"]);
|
|
195
|
+
integer(group.memoryMb, "memory_budget", 128, 4096);
|
|
196
|
+
integer(group.maxActive, "max_active", 1, 5);
|
|
197
|
+
const sessions = records(group.sessions);
|
|
198
|
+
for (const session of sessions) {
|
|
199
|
+
id(session.id, "session_id");
|
|
200
|
+
member(session.role, ["main", "worker"]);
|
|
201
|
+
member(session.stopped, [true, false]);
|
|
202
|
+
}
|
|
203
|
+
unique(sessions.map((session) => session.id));
|
|
204
|
+
if (sessions.filter((s) => s.role === "main" && !s.stopped).length > 1)
|
|
205
|
+
throw new Error("multiple_active_main_sessions");
|
|
206
|
+
const leases = records(state.leases);
|
|
207
|
+
for (const lease of leases) {
|
|
208
|
+
id(lease.token, "control_token");
|
|
209
|
+
id(lease.controller, "controller");
|
|
210
|
+
member(lease.generation, [group.generation]);
|
|
211
|
+
member(lease.mode, ["cli", "native"]);
|
|
212
|
+
member(
|
|
213
|
+
lease.sessionId,
|
|
214
|
+
sessions.map((session) => session.id)
|
|
215
|
+
);
|
|
216
|
+
if (!Number.isFinite(Date.parse(text(lease.expiresAt, "control_expiry"))))
|
|
217
|
+
throw new Error("invalid_control_expiry");
|
|
218
|
+
}
|
|
219
|
+
unique(leases.map((lease) => lease.token));
|
|
220
|
+
unique(leases.map((lease) => lease.sessionId));
|
|
221
|
+
const deliveries = records(state.deliveries);
|
|
222
|
+
for (const delivery of deliveries) {
|
|
223
|
+
if (delivery.controller !== void 0)
|
|
224
|
+
id(delivery.controller, "controller");
|
|
225
|
+
id(delivery.requestId, "request_id");
|
|
226
|
+
id(delivery.sessionId, "session_id");
|
|
227
|
+
id(delivery.generation, "generation");
|
|
228
|
+
member(delivery.operation, ["write", "submit"]);
|
|
229
|
+
member(delivery.state, ["intent", "input_written", "delivery_uncertain"]);
|
|
230
|
+
if (!/^[a-f0-9]{64}$/.test(text(delivery.digest, "delivery_digest")))
|
|
231
|
+
throw new Error("invalid_delivery_digest");
|
|
232
|
+
}
|
|
233
|
+
unique(deliveries.map((delivery) => delivery.requestId));
|
|
234
|
+
return state;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/harness/groups/store.ts
|
|
238
|
+
function privateJson(path, value) {
|
|
239
|
+
const temporary = `${path}.${randomUUID()}.new`;
|
|
240
|
+
const descriptor = openSync(temporary, "wx", 384);
|
|
241
|
+
try {
|
|
242
|
+
writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}
|
|
243
|
+
`);
|
|
244
|
+
fsyncSync(descriptor);
|
|
245
|
+
} finally {
|
|
246
|
+
closeSync(descriptor);
|
|
247
|
+
}
|
|
248
|
+
renameSync(temporary, path);
|
|
249
|
+
const directory = openSync(dirname(path), "r");
|
|
250
|
+
try {
|
|
251
|
+
fsyncSync(directory);
|
|
252
|
+
} finally {
|
|
253
|
+
closeSync(directory);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function readPrivateJson(path) {
|
|
257
|
+
const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
258
|
+
try {
|
|
259
|
+
const stat = fstatSync(descriptor);
|
|
260
|
+
if (!stat.isFile() || (stat.mode & 63) !== 0 || stat.size > 2097152)
|
|
261
|
+
throw new Error("unsafe_state_file");
|
|
262
|
+
if (stat.uid !== process.getuid?.()) throw new Error("foreign_state_file");
|
|
263
|
+
return JSON.parse(readFileSync(descriptor, "utf8"));
|
|
264
|
+
} finally {
|
|
265
|
+
closeSync(descriptor);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
var GroupStore = class {
|
|
269
|
+
path;
|
|
270
|
+
state;
|
|
271
|
+
constructor(path) {
|
|
272
|
+
this.path = realpathSync(path);
|
|
273
|
+
this.state = validateState(readPrivateJson(path));
|
|
274
|
+
const metadata = object(
|
|
275
|
+
readPrivateJson(join(dirname(this.path), "harness.json"))
|
|
276
|
+
);
|
|
277
|
+
if (this.state.group.schemaVersion !== 1 || this.state.group.owner !== metadata.owner) {
|
|
278
|
+
throw new Error("foreign_group");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
save() {
|
|
282
|
+
validateState(this.state);
|
|
283
|
+
privateJson(this.path, this.state);
|
|
284
|
+
}
|
|
285
|
+
session(id2) {
|
|
286
|
+
if (!this.state.group.sessions.some((entry) => entry.id === id2)) {
|
|
287
|
+
throw new Error("foreign_session");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// src/harness/control/leases.ts
|
|
293
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
294
|
+
import { join as join12 } from "path";
|
|
295
|
+
|
|
296
|
+
// src/harness/adapters/process/command.ts
|
|
297
|
+
import {
|
|
298
|
+
execFileSync
|
|
299
|
+
} from "child_process";
|
|
300
|
+
function command(binary, args, options = {}) {
|
|
301
|
+
return execFileSync(binary, args, {
|
|
302
|
+
encoding: "utf8",
|
|
303
|
+
timeout: 1e4,
|
|
304
|
+
maxBuffer: 256 * 1024,
|
|
305
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
306
|
+
...options
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function quote(value) {
|
|
310
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/harness/adapters/process/identity.ts
|
|
314
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
315
|
+
function processIdentity(pid) {
|
|
316
|
+
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
317
|
+
throw new Error("invalid_process_id");
|
|
318
|
+
const stat = readFileSync2(`/proc/${pid}/stat`, "utf8");
|
|
319
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
320
|
+
if (!/^\d+$/.test(fields[19] ?? ""))
|
|
321
|
+
throw new Error("process_identity_unknown");
|
|
322
|
+
return `${pid}:${fields[19]}`;
|
|
323
|
+
}
|
|
324
|
+
function controllerExited(identity) {
|
|
325
|
+
if (!/^\d+:\d+$/.test(identity))
|
|
326
|
+
throw new Error("invalid_controller_identity");
|
|
327
|
+
const [pid] = identity.split(":");
|
|
328
|
+
try {
|
|
329
|
+
return processIdentity(Number(pid)) !== identity;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (error.code === "ENOENT") return true;
|
|
332
|
+
throw new Error("controller_liveness_unknown", { cause: error });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/harness/adapters/docker/claims.ts
|
|
337
|
+
import { createHash } from "crypto";
|
|
338
|
+
|
|
339
|
+
// src/harness/adapters/docker/identity.ts
|
|
340
|
+
var containerLabels = {
|
|
341
|
+
owner: "pmcp.harness.owner",
|
|
342
|
+
nonce: "pmcp.harness.nonce",
|
|
343
|
+
role: "pmcp.harness.role"
|
|
344
|
+
};
|
|
345
|
+
function containerName(owner, sessionId) {
|
|
346
|
+
return `pmcp-harness-${owner.slice(0, 8)}-${sessionId}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/harness/adapters/docker/claims.ts
|
|
350
|
+
function resourceClaim(value, owner) {
|
|
351
|
+
const record = object(value);
|
|
352
|
+
if (record.kind !== "docker" || record.owner !== owner)
|
|
353
|
+
throw new Error("foreign_container_resource");
|
|
354
|
+
if (!/^[a-f0-9]{64}$/.test(text(record.id, "container_id")))
|
|
355
|
+
throw new Error("invalid_container_id");
|
|
356
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(text(record.image, "container_image")))
|
|
357
|
+
throw new Error("invalid_container_image");
|
|
358
|
+
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(
|
|
359
|
+
text(record.nonce, "container_nonce")
|
|
360
|
+
))
|
|
361
|
+
throw new Error("invalid_container_nonce");
|
|
362
|
+
if (record.role !== "main" && record.role !== "worker")
|
|
363
|
+
throw new Error("invalid_container_role");
|
|
364
|
+
if (!/^[a-f0-9]{64}$/.test(
|
|
365
|
+
text(record.configurationDigest, "container_configuration")
|
|
366
|
+
))
|
|
367
|
+
throw new Error("invalid_container_configuration");
|
|
368
|
+
integer(record.memoryMb, "container_memory", 32, 1472);
|
|
369
|
+
integer(record.cpuQuota, "container_cpu", 1, 5e4);
|
|
370
|
+
if (record.managed !== void 0 && record.managed !== true)
|
|
371
|
+
throw new Error("invalid_container_custody");
|
|
372
|
+
if (record.privateProfile !== void 0) {
|
|
373
|
+
profileName(record.privateProfile);
|
|
374
|
+
if (record.managed !== true)
|
|
375
|
+
throw new Error("private_profile_requires_cli_owned_creation");
|
|
376
|
+
}
|
|
377
|
+
return record;
|
|
378
|
+
}
|
|
379
|
+
function assertResourceOwner(resource, actual) {
|
|
380
|
+
const config = object(actual.Config);
|
|
381
|
+
const labels = object(config.Labels);
|
|
382
|
+
if (actual.Id !== resource.id || actual.Image !== resource.image || labels[containerLabels.owner] !== resource.owner || labels[containerLabels.nonce] !== resource.nonce || labels[containerLabels.role] !== resource.role)
|
|
383
|
+
throw new Error("foreign_container_identity");
|
|
384
|
+
}
|
|
385
|
+
function configurationDigest(actual) {
|
|
386
|
+
const host = object(actual.HostConfig);
|
|
387
|
+
if (host.OomKillDisable !== false && host.OomKillDisable !== null)
|
|
388
|
+
throw new Error("container_oom_protection_disabled");
|
|
389
|
+
if (!Array.isArray(actual.Mounts))
|
|
390
|
+
throw new Error("invalid_container_mounts");
|
|
391
|
+
const mounts = actual.Mounts.map(object).sort(
|
|
392
|
+
(left, right) => text(left.Destination, "mount_destination").localeCompare(
|
|
393
|
+
text(right.Destination, "mount_destination")
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
return createHash("sha256").update(
|
|
397
|
+
JSON.stringify(
|
|
398
|
+
canonicalValue({
|
|
399
|
+
config: actual.Config,
|
|
400
|
+
host: { ...host, OomKillDisable: false },
|
|
401
|
+
mounts
|
|
402
|
+
})
|
|
403
|
+
)
|
|
404
|
+
).digest("hex");
|
|
405
|
+
}
|
|
406
|
+
function canonicalValue(value) {
|
|
407
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
408
|
+
if (value === null || typeof value !== "object") return value;
|
|
409
|
+
return Object.fromEntries(
|
|
410
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalValue(entry)])
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/harness/adapters/docker/observation.ts
|
|
415
|
+
function inspectResource(resource, enforceConfiguration = true) {
|
|
416
|
+
const result = JSON.parse(command("docker", ["inspect", resource.id]));
|
|
417
|
+
if (!Array.isArray(result) || result.length !== 1)
|
|
418
|
+
throw new Error("invalid_container_inspection");
|
|
419
|
+
const actual = object(result[0]);
|
|
420
|
+
assertResourceOwner(resource, actual);
|
|
421
|
+
if (enforceConfiguration && configurationDigest(actual) !== resource.configurationDigest)
|
|
422
|
+
throw new Error("container_configuration_changed");
|
|
423
|
+
return actual;
|
|
424
|
+
}
|
|
425
|
+
function resourceAbsent(error, id2) {
|
|
426
|
+
const failure = error;
|
|
427
|
+
const message = String(failure.stderr).toLowerCase();
|
|
428
|
+
return ["object", "container"].some(
|
|
429
|
+
(kind) => message.includes(`no such ${kind}: ${id2}`)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
function assertResourceFresh(resource) {
|
|
433
|
+
const state = object(inspectResource(resource).State);
|
|
434
|
+
if (state.Status !== "created" || state.Running !== false || state.Pid !== 0)
|
|
435
|
+
throw new Error("container_requires_fresh_creation");
|
|
436
|
+
}
|
|
437
|
+
function resourceRunning(resource) {
|
|
438
|
+
let actual;
|
|
439
|
+
try {
|
|
440
|
+
actual = inspectResource(resource);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (resourceAbsent(error, resource.id)) return false;
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
const state = object(actual.State);
|
|
446
|
+
if (typeof state.Running !== "boolean")
|
|
447
|
+
throw new Error("unknown_container_lifecycle");
|
|
448
|
+
return state.Running;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/harness/adapters/tmux/connection.ts
|
|
452
|
+
var Connection = class {
|
|
453
|
+
constructor(records2) {
|
|
454
|
+
this.records = records2;
|
|
455
|
+
}
|
|
456
|
+
records;
|
|
457
|
+
tmux(args, options = {}) {
|
|
458
|
+
return command(
|
|
459
|
+
"tmux",
|
|
460
|
+
["-S", this.records.socket, "-f", this.records.configPath, ...args],
|
|
461
|
+
options
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
alive() {
|
|
465
|
+
try {
|
|
466
|
+
this.tmux(["list-sessions"]);
|
|
467
|
+
return true;
|
|
468
|
+
} catch (error) {
|
|
469
|
+
const failure = error;
|
|
470
|
+
if (/no server running|No such file or directory|no sessions/.test(
|
|
471
|
+
String(failure.stderr ?? failure.message)
|
|
472
|
+
))
|
|
473
|
+
return false;
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
inspect(id2) {
|
|
478
|
+
const record = this.records.read(id2);
|
|
479
|
+
const target = this.tmux([
|
|
480
|
+
"display-message",
|
|
481
|
+
"-p",
|
|
482
|
+
"-t",
|
|
483
|
+
record.paneId,
|
|
484
|
+
"#{pid}|#{pane_id}|#{pane_pid}|#{pane_dead}|#{@pmcp-owner}|#{@pmcp-session}|#{pane_width}|#{pane_height}|#{session_id}|#{window_id}"
|
|
485
|
+
]).trim().split("|");
|
|
486
|
+
const [
|
|
487
|
+
serverPid,
|
|
488
|
+
paneId,
|
|
489
|
+
panePid,
|
|
490
|
+
dead,
|
|
491
|
+
owner,
|
|
492
|
+
sessionId,
|
|
493
|
+
width,
|
|
494
|
+
height,
|
|
495
|
+
tmuxSessionId,
|
|
496
|
+
windowId
|
|
497
|
+
] = target;
|
|
498
|
+
if (processIdentity(Number(serverPid)) !== record.serverIdentity)
|
|
499
|
+
throw new Error("stale_server");
|
|
500
|
+
if (paneId !== record.paneId || Number(panePid) !== record.panePid || tmuxSessionId !== record.tmuxSessionId || windowId !== record.windowId)
|
|
501
|
+
throw new Error("stale_pane");
|
|
502
|
+
if (owner !== this.records.owner || sessionId !== id2)
|
|
503
|
+
throw new Error("foreign_pane");
|
|
504
|
+
return {
|
|
505
|
+
...record,
|
|
506
|
+
alive: dead === "0" && (!record.resource || resourceRunning(record.resource)),
|
|
507
|
+
width: Number(width),
|
|
508
|
+
height: Number(height)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// src/harness/adapters/tmux/records.ts
|
|
514
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
515
|
+
import {
|
|
516
|
+
lstatSync,
|
|
517
|
+
mkdirSync,
|
|
518
|
+
readdirSync,
|
|
519
|
+
realpathSync as realpathSync2,
|
|
520
|
+
writeFileSync as writeFileSync2
|
|
521
|
+
} from "fs";
|
|
522
|
+
import { homedir } from "os";
|
|
523
|
+
import { join as join2 } from "path";
|
|
524
|
+
function stateDirectory() {
|
|
525
|
+
return join2(
|
|
526
|
+
process.env.XDG_STATE_HOME ?? join2(homedir(), ".local/state"),
|
|
527
|
+
"pmcp/harness"
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
function runtimeUnit(id2) {
|
|
531
|
+
if (!/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(id2))
|
|
532
|
+
throw new Error("invalid_session_id");
|
|
533
|
+
return `pmcp-${id2}.scope`;
|
|
534
|
+
}
|
|
535
|
+
function budgetSlice(owner) {
|
|
536
|
+
runtimeUnit(owner);
|
|
537
|
+
return `pmcp${owner.replaceAll("-", "")}.slice`;
|
|
538
|
+
}
|
|
539
|
+
var RecordStore = class _RecordStore {
|
|
540
|
+
directory;
|
|
541
|
+
socket;
|
|
542
|
+
metadataPath;
|
|
543
|
+
configPath;
|
|
544
|
+
owner;
|
|
545
|
+
constructor(directory) {
|
|
546
|
+
const stat = lstatSync(directory);
|
|
547
|
+
if (!stat.isDirectory() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0)
|
|
548
|
+
throw new Error("unsafe_group_directory");
|
|
549
|
+
this.directory = realpathSync2(directory);
|
|
550
|
+
this.socket = join2(this.directory, "s");
|
|
551
|
+
if (Buffer.byteLength(this.socket) > 100)
|
|
552
|
+
throw new Error("socket_path_too_long");
|
|
553
|
+
this.metadataPath = join2(this.directory, "harness.json");
|
|
554
|
+
this.configPath = join2(this.directory, "tmux.conf");
|
|
555
|
+
this.owner = text(
|
|
556
|
+
object(readPrivateJson(this.metadataPath)).owner,
|
|
557
|
+
"owner"
|
|
558
|
+
);
|
|
559
|
+
runtimeUnit(this.owner);
|
|
560
|
+
}
|
|
561
|
+
static create() {
|
|
562
|
+
const root = stateDirectory();
|
|
563
|
+
mkdirSync(root, { recursive: true, mode: 448 });
|
|
564
|
+
const directory = join2(root, randomUUID2().slice(0, 16));
|
|
565
|
+
if (Buffer.byteLength(join2(directory, "s")) > 100)
|
|
566
|
+
throw new Error("socket_path_too_long");
|
|
567
|
+
mkdirSync(directory, { mode: 448 });
|
|
568
|
+
mkdirSync(join2(directory, "sessions"), { mode: 448 });
|
|
569
|
+
privateJson(join2(directory, "harness.json"), {
|
|
570
|
+
owner: randomUUID2(),
|
|
571
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
572
|
+
});
|
|
573
|
+
writeFileSync2(
|
|
574
|
+
join2(directory, "tmux.conf"),
|
|
575
|
+
[
|
|
576
|
+
"set -g remain-on-exit on",
|
|
577
|
+
"set -g history-limit 2000",
|
|
578
|
+
"set -g default-terminal tmux-256color",
|
|
579
|
+
"set -g status off",
|
|
580
|
+
"set -g mouse off",
|
|
581
|
+
"set -g destroy-unattached off",
|
|
582
|
+
""
|
|
583
|
+
].join("\n"),
|
|
584
|
+
{ flag: "wx", mode: 384 }
|
|
585
|
+
);
|
|
586
|
+
return new _RecordStore(directory);
|
|
587
|
+
}
|
|
588
|
+
path(id2) {
|
|
589
|
+
runtimeUnit(id2);
|
|
590
|
+
return join2(this.directory, "sessions", `${id2}.json`);
|
|
591
|
+
}
|
|
592
|
+
read(id2) {
|
|
593
|
+
const record = object(readPrivateJson(this.path(id2)));
|
|
594
|
+
if (record.id !== id2 || record.owner !== this.owner || record.socket !== this.socket || record.unit !== runtimeUnit(id2))
|
|
595
|
+
throw new Error("foreign_backend_record");
|
|
596
|
+
for (const [field, pattern] of [
|
|
597
|
+
["paneId", /^%\d+$/],
|
|
598
|
+
["windowId", /^@\d+$/],
|
|
599
|
+
["tmuxSessionId", /^\$\d+$/],
|
|
600
|
+
["serverIdentity", /^\d+:\d+$/]
|
|
601
|
+
]) {
|
|
602
|
+
if (!pattern.test(text(record[field], field)))
|
|
603
|
+
throw new Error("invalid_terminal_identity");
|
|
604
|
+
}
|
|
605
|
+
if (!Number.isSafeInteger(record.panePid) || Number(record.panePid) < 1)
|
|
606
|
+
throw new Error("invalid_pane_process");
|
|
607
|
+
if (record.resource !== void 0)
|
|
608
|
+
resourceClaim(record.resource, this.owner);
|
|
609
|
+
return record;
|
|
610
|
+
}
|
|
611
|
+
all() {
|
|
612
|
+
return readdirSync(join2(this.directory, "sessions")).filter((name) => name.endsWith(".json")).map((name) => this.read(name.slice(0, -5)));
|
|
613
|
+
}
|
|
614
|
+
save(record) {
|
|
615
|
+
privateJson(this.path(record.id), record);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// src/harness/adapters/tmux/lifecycle.ts
|
|
620
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
621
|
+
import { existsSync as existsSync4 } from "fs";
|
|
622
|
+
import { setTimeout } from "timers/promises";
|
|
623
|
+
|
|
624
|
+
// src/harness/adapters/process/artifact.ts
|
|
625
|
+
import { fileURLToPath } from "url";
|
|
626
|
+
var sources = {
|
|
627
|
+
worker: "../../application/worker.ts",
|
|
628
|
+
"pane-runner": "./pane-runner.ts",
|
|
629
|
+
"launch-gate": "./launch-gate.ts",
|
|
630
|
+
"egress-bridge": "../egress/bridge-runtime.ts"
|
|
631
|
+
};
|
|
632
|
+
function artifact(name) {
|
|
633
|
+
if (import.meta.url.endsWith(".ts"))
|
|
634
|
+
return fileURLToPath(new URL(sources[name], import.meta.url));
|
|
635
|
+
return fileURLToPath(new URL(`./harness-${name}.js`, import.meta.url));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/harness/adapters/tmux/lifecycle.ts
|
|
639
|
+
import { join as join10 } from "path";
|
|
640
|
+
|
|
641
|
+
// src/harness/adapters/systemd/units.ts
|
|
642
|
+
function unitActive(unit) {
|
|
643
|
+
let state;
|
|
644
|
+
try {
|
|
645
|
+
state = command("systemctl", [
|
|
646
|
+
"--user",
|
|
647
|
+
"show",
|
|
648
|
+
unit,
|
|
649
|
+
"--property=ActiveState",
|
|
650
|
+
"--value"
|
|
651
|
+
]).trim();
|
|
652
|
+
} catch (error) {
|
|
653
|
+
const failure = error;
|
|
654
|
+
if (/not found|could not be found|No such/i.test(String(failure.stderr)))
|
|
655
|
+
return false;
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
return ["active", "activating", "deactivating"].includes(state);
|
|
659
|
+
}
|
|
660
|
+
function stopUnit(unit) {
|
|
661
|
+
if (!unitActive(unit)) return;
|
|
662
|
+
try {
|
|
663
|
+
command("systemctl", ["--user", "stop", unit]);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
const stderr = String(error.stderr);
|
|
666
|
+
if (!stderr.includes(unit) || !/not loaded|not found|could not be found/i.test(stderr))
|
|
667
|
+
throw error;
|
|
668
|
+
if (unitActive(unit)) throw error;
|
|
669
|
+
}
|
|
670
|
+
if (unitActive(unit)) throw new Error("runtime_stop_unconfirmed");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/harness/adapters/tmux/launch-identity.ts
|
|
674
|
+
function assertProof(connection, proof) {
|
|
675
|
+
const record = proof.session;
|
|
676
|
+
const fields = connection.tmux([
|
|
677
|
+
"display-message",
|
|
678
|
+
"-p",
|
|
679
|
+
"-t",
|
|
680
|
+
record.paneId,
|
|
681
|
+
"#{pid}|#{session_id}|#{window_id}|#{pane_id}|#{pane_pid}|#{@pmcp-owner}|#{@pmcp-session}|#{@pmcp-launch}"
|
|
682
|
+
]).trim().split("|");
|
|
683
|
+
if (processIdentity(Number(fields[0])) !== record.serverIdentity)
|
|
684
|
+
throw new Error("stale_launch_server");
|
|
685
|
+
if (fields[1] !== record.tmuxSessionId || fields[2] !== record.windowId || fields[3] !== record.paneId || Number(fields[4]) !== record.panePid)
|
|
686
|
+
throw new Error("stale_launch_pane");
|
|
687
|
+
if (fields[5] !== record.owner || fields[6] !== record.id || fields[7] !== proof.nonce)
|
|
688
|
+
throw new Error("foreign_launch_pane");
|
|
689
|
+
}
|
|
690
|
+
function panePresent(connection, proof) {
|
|
691
|
+
if (!connection.alive()) return false;
|
|
692
|
+
const rows = connection.tmux(["list-panes", "-a", "-F", "#{pid}|#{pane_id}"]).trim().split("\n");
|
|
693
|
+
const server = Number(rows[0]?.split("|")[0]);
|
|
694
|
+
if (processIdentity(server) !== proof.session.serverIdentity)
|
|
695
|
+
throw new Error("stale_launch_server");
|
|
696
|
+
return rows.some((row) => row.split("|")[1] === proof.session.paneId);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/harness/launch/journal.ts
|
|
700
|
+
import { createHash as createHash2 } from "crypto";
|
|
701
|
+
import { existsSync, readdirSync as readdirSync2 } from "fs";
|
|
702
|
+
import { join as join3 } from "path";
|
|
703
|
+
var LaunchJournal = class {
|
|
704
|
+
constructor(records2, id2) {
|
|
705
|
+
this.records = records2;
|
|
706
|
+
this.id = id2;
|
|
707
|
+
runtimeUnit(id2);
|
|
708
|
+
}
|
|
709
|
+
records;
|
|
710
|
+
id;
|
|
711
|
+
path(kind) {
|
|
712
|
+
if (!["intent", "proof", "grant", "committed", "aborted"].includes(kind))
|
|
713
|
+
throw new Error("invalid_launch_artifact");
|
|
714
|
+
return join3(this.records.directory, `${this.id}.${kind}.json`);
|
|
715
|
+
}
|
|
716
|
+
intent() {
|
|
717
|
+
const value = object(readPrivateJson(this.path("intent")));
|
|
718
|
+
if (value.id !== this.id || value.owner !== this.records.owner || value.socket !== this.records.socket || value.unit !== runtimeUnit(this.id) || value.slice !== budgetSlice(this.records.owner))
|
|
719
|
+
throw new Error("foreign_launch_intent");
|
|
720
|
+
if (!/^\d+:\d+$/.test(text(value.controllerIdentity, "controller")))
|
|
721
|
+
throw new Error("invalid_launch_controller");
|
|
722
|
+
for (const field of ["groupId", "generation", "nonce"])
|
|
723
|
+
runtimeUnit(text(value[field], field));
|
|
724
|
+
if (value.role !== "main" && value.role !== "worker")
|
|
725
|
+
throw new Error("invalid_launch_role");
|
|
726
|
+
strings(value.argv, "argv");
|
|
727
|
+
text(value.cwd, "cwd");
|
|
728
|
+
text(value.runtime, "runtime");
|
|
729
|
+
text(value.createdAt, "created_at");
|
|
730
|
+
integer(value.memoryMb, "session_memory", 32, 1536);
|
|
731
|
+
if (value.resource !== void 0)
|
|
732
|
+
resourceClaim(value.resource, this.records.owner);
|
|
733
|
+
if (value.dockerCreation !== void 0) {
|
|
734
|
+
const creation = object(value.dockerCreation);
|
|
735
|
+
const config = object(creation.configuration);
|
|
736
|
+
const labels = object(config.Labels);
|
|
737
|
+
if (creation.egress !== void 0) egressPolicy(creation.egress);
|
|
738
|
+
const bridge = creation.egress === void 0 ? void 0 : egressPolicy(creation.egress).bridgeExecutable;
|
|
739
|
+
if (bridge && !/^[a-f0-9]{64}$/.test(String(creation.bridgeSha256)))
|
|
740
|
+
throw new Error("invalid_docker_bridge_digest");
|
|
741
|
+
if (!bridge && creation.bridgeSha256 !== void 0)
|
|
742
|
+
throw new Error("ungranted_docker_bridge_digest");
|
|
743
|
+
const policy = new GroupStore(join3(this.records.directory, "group.json")).state.group.dockerPolicy;
|
|
744
|
+
const profile = selectedProfile(
|
|
745
|
+
policy,
|
|
746
|
+
creation.privateProfile,
|
|
747
|
+
text(value.runtime, "runtime")
|
|
748
|
+
);
|
|
749
|
+
const expectedState = profile?.directory ?? join3(
|
|
750
|
+
this.records.directory,
|
|
751
|
+
"containers",
|
|
752
|
+
text(value.nonce, "nonce"),
|
|
753
|
+
"profile"
|
|
754
|
+
);
|
|
755
|
+
if (value.resource !== void 0 && object(value.resource).privateProfile !== creation.privateProfile)
|
|
756
|
+
throw new Error("launch_private_profile_mismatch");
|
|
757
|
+
if (creation.name !== containerName(this.records.owner, this.id) || creation.nonce !== value.nonce || creation.state !== expectedState || labels[containerLabels.owner] !== this.records.owner || labels[containerLabels.role] !== value.role || labels[containerLabels.nonce] !== value.nonce)
|
|
758
|
+
throw new Error("foreign_container_creation");
|
|
759
|
+
}
|
|
760
|
+
return value;
|
|
761
|
+
}
|
|
762
|
+
proof() {
|
|
763
|
+
const intent = this.intent();
|
|
764
|
+
const value = object(readPrivateJson(this.path("proof")));
|
|
765
|
+
const session = object(value.session);
|
|
766
|
+
if (value.nonce !== intent.nonce || session.id !== intent.id || session.owner !== intent.owner || session.socket !== intent.socket || session.unit !== intent.unit || session.cwd !== intent.cwd || session.memoryMb !== intent.memoryMb || session.runtime !== intent.runtime || session.createdAt !== intent.createdAt || JSON.stringify(session.resource) !== JSON.stringify(intent.resource))
|
|
767
|
+
throw new Error("foreign_launch_proof");
|
|
768
|
+
for (const [field, pattern] of [
|
|
769
|
+
["paneId", /^%\d+$/],
|
|
770
|
+
["windowId", /^@\d+$/],
|
|
771
|
+
["tmuxSessionId", /^\$\d+$/],
|
|
772
|
+
["serverIdentity", /^\d+:\d+$/]
|
|
773
|
+
]) {
|
|
774
|
+
if (!pattern.test(text(session[field], field)))
|
|
775
|
+
throw new Error("invalid_launch_identity");
|
|
776
|
+
}
|
|
777
|
+
integer(session.panePid, "pane_process", 1, Number.MAX_SAFE_INTEGER);
|
|
778
|
+
if (!/^\d+:\d+$/.test(text(value.runnerIdentity, "runner")))
|
|
779
|
+
throw new Error("invalid_launch_runner");
|
|
780
|
+
return value;
|
|
781
|
+
}
|
|
782
|
+
grant() {
|
|
783
|
+
const intent = this.intent();
|
|
784
|
+
return {
|
|
785
|
+
id: intent.id,
|
|
786
|
+
owner: intent.owner,
|
|
787
|
+
groupId: intent.groupId,
|
|
788
|
+
generation: intent.generation,
|
|
789
|
+
nonce: intent.nonce,
|
|
790
|
+
proofDigest: createHash2("sha256").update(JSON.stringify(this.proof())).digest("hex")
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
assertRegistered() {
|
|
794
|
+
const intent = this.intent();
|
|
795
|
+
const group = new GroupStore(join3(this.records.directory, "group.json")).state.group;
|
|
796
|
+
if (group.id !== intent.groupId || group.generation !== intent.generation)
|
|
797
|
+
throw new Error("launch_group_generation_mismatch");
|
|
798
|
+
if (intent.dockerCreation && JSON.stringify(intent.dockerCreation.egress) !== JSON.stringify(group.dockerPolicy?.egress))
|
|
799
|
+
throw new Error("launch_egress_grant_mismatch");
|
|
800
|
+
if (group.state !== "ready" || !group.allowedRuntimes.includes(intent.runtime))
|
|
801
|
+
throw new Error("launch_group_not_ready");
|
|
802
|
+
if (!group.sessions.some(
|
|
803
|
+
(session) => session.id === intent.id && session.role === intent.role && !session.stopped
|
|
804
|
+
))
|
|
805
|
+
throw new Error("launch_registration_missing");
|
|
806
|
+
const record = this.records.read(this.id);
|
|
807
|
+
if (JSON.stringify(record) !== JSON.stringify(this.proof().session))
|
|
808
|
+
throw new Error("launch_record_mismatch");
|
|
809
|
+
}
|
|
810
|
+
authorize() {
|
|
811
|
+
if (existsSync(this.path("aborted"))) throw new Error("launch_was_aborted");
|
|
812
|
+
this.assertRegistered();
|
|
813
|
+
privateJson(this.path("grant"), this.grant());
|
|
814
|
+
}
|
|
815
|
+
assertGrant() {
|
|
816
|
+
if (existsSync(this.path("aborted"))) throw new Error("launch_was_aborted");
|
|
817
|
+
const grant = object(readPrivateJson(this.path("grant")));
|
|
818
|
+
if (JSON.stringify(grant) !== JSON.stringify(this.grant()))
|
|
819
|
+
throw new Error("launch_grant_mismatch");
|
|
820
|
+
}
|
|
821
|
+
commit() {
|
|
822
|
+
this.assertRegistered();
|
|
823
|
+
this.assertGrant();
|
|
824
|
+
privateJson(this.path("committed"), this.grant());
|
|
825
|
+
}
|
|
826
|
+
assertCommitted() {
|
|
827
|
+
this.assertGrant();
|
|
828
|
+
const committed = object(readPrivateJson(this.path("committed")));
|
|
829
|
+
if (JSON.stringify(committed) !== JSON.stringify(this.grant()))
|
|
830
|
+
throw new Error("launch_commit_mismatch");
|
|
831
|
+
}
|
|
832
|
+
abortRecord() {
|
|
833
|
+
if (!existsSync(this.path("aborted"))) return;
|
|
834
|
+
const value = object(readPrivateJson(this.path("aborted")));
|
|
835
|
+
if (value.id !== this.id || value.owner !== this.records.owner || !["stopping", "stopped"].includes(String(value.state)))
|
|
836
|
+
throw new Error("foreign_launch_abort");
|
|
837
|
+
return value;
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
function launchJournals(records2) {
|
|
841
|
+
return readdirSync2(records2.directory).filter((name) => name.endsWith(".intent.json")).map((name) => new LaunchJournal(records2, name.slice(0, -12)));
|
|
842
|
+
}
|
|
843
|
+
function pendingLaunches(records2) {
|
|
844
|
+
return launchJournals(records2).filter((journal) => {
|
|
845
|
+
const aborted = journal.abortRecord();
|
|
846
|
+
if (aborted) return aborted.state !== "stopped";
|
|
847
|
+
if (!existsSync(journal.path("committed"))) return true;
|
|
848
|
+
journal.assertCommitted();
|
|
849
|
+
return false;
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/harness/launch/recovery.ts
|
|
854
|
+
import { existsSync as existsSync3 } from "fs";
|
|
855
|
+
|
|
856
|
+
// src/harness/adapters/docker/lifecycle.ts
|
|
857
|
+
function stopResource(resource) {
|
|
858
|
+
let actual;
|
|
859
|
+
try {
|
|
860
|
+
actual = inspectResource(resource, false);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
if (resourceAbsent(error, resource.id)) return;
|
|
863
|
+
throw error;
|
|
864
|
+
}
|
|
865
|
+
const state = object(actual.State);
|
|
866
|
+
if (state.Running === true)
|
|
867
|
+
command("docker", ["stop", "--timeout", "2", resource.id]);
|
|
868
|
+
if (object(inspectResource(resource, false).State).Running !== false)
|
|
869
|
+
throw new Error("container_stop_unconfirmed");
|
|
870
|
+
if (resource.managed) {
|
|
871
|
+
command("docker", ["rm", resource.id]);
|
|
872
|
+
try {
|
|
873
|
+
inspectResource(resource, false);
|
|
874
|
+
} catch (error) {
|
|
875
|
+
if (resourceAbsent(error, resource.id)) return;
|
|
876
|
+
throw error;
|
|
877
|
+
}
|
|
878
|
+
throw new Error("container_removal_unconfirmed");
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/harness/adapters/docker/creation.ts
|
|
883
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
884
|
+
import { join as join9 } from "path";
|
|
885
|
+
|
|
886
|
+
// src/harness/adapters/docker/admission.ts
|
|
887
|
+
import { lstatSync as lstatSync5, realpathSync as realpathSync6 } from "fs";
|
|
888
|
+
import { join as join6, relative as relative3 } from "path";
|
|
889
|
+
|
|
890
|
+
// src/harness/adapters/docker/profile.ts
|
|
891
|
+
import { homedir as homedir3 } from "os";
|
|
892
|
+
import { lstatSync as lstatSync3, realpathSync as realpathSync4 } from "fs";
|
|
893
|
+
import { relative as relative2 } from "path";
|
|
894
|
+
|
|
895
|
+
// src/harness/adapters/docker/private-profile.ts
|
|
896
|
+
import { homedir as homedir2 } from "os";
|
|
897
|
+
import {
|
|
898
|
+
closeSync as closeSync2,
|
|
899
|
+
constants as constants2,
|
|
900
|
+
existsSync as existsSync2,
|
|
901
|
+
fstatSync as fstatSync2,
|
|
902
|
+
lstatSync as lstatSync2,
|
|
903
|
+
openSync as openSync2,
|
|
904
|
+
realpathSync as realpathSync3,
|
|
905
|
+
statfsSync
|
|
906
|
+
} from "fs";
|
|
907
|
+
import { join as join4, relative } from "path";
|
|
908
|
+
var privateProfileLock = "profile.lock";
|
|
909
|
+
function overlap(left, right) {
|
|
910
|
+
const scope = relative(left, right);
|
|
911
|
+
return scope === "" || scope !== ".." && !scope.startsWith("../");
|
|
912
|
+
}
|
|
913
|
+
function profileIdentity(directory, projectRoot) {
|
|
914
|
+
if (directory === "/" || directory === realpathSync3(homedir2()) || overlap(directory, projectRoot) || overlap(projectRoot, directory) || realpathSync3(directory) !== directory)
|
|
915
|
+
throw new Error("private_profile_scope_not_admitted");
|
|
916
|
+
const stat = lstatSync2(directory);
|
|
917
|
+
if (!stat.isDirectory() || stat.uid !== process.getuid?.() || (stat.mode & 511) !== 448)
|
|
918
|
+
throw new Error("unsafe_private_profile_directory");
|
|
919
|
+
if ([26985, 4283649346, 4266872130].includes(statfsSync(directory).type))
|
|
920
|
+
throw new Error("remote_private_profile_not_admitted");
|
|
921
|
+
const lock = openSync2(
|
|
922
|
+
join4(directory, privateProfileLock),
|
|
923
|
+
constants2.O_RDONLY | constants2.O_NOFOLLOW
|
|
924
|
+
);
|
|
925
|
+
try {
|
|
926
|
+
const value = fstatSync2(lock);
|
|
927
|
+
if (!value.isFile() || value.nlink !== 1 || value.uid !== process.getuid?.() || (value.mode & 511) !== 384)
|
|
928
|
+
throw new Error("unsafe_private_profile_lock");
|
|
929
|
+
return `${stat.dev}:${stat.ino}:${value.dev}:${value.ino}`;
|
|
930
|
+
} finally {
|
|
931
|
+
closeSync2(lock);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function assertProfileIdentity(grant, projectRoot) {
|
|
935
|
+
if (!grant.identity || profileIdentity(grant.directory, projectRoot) !== grant.identity)
|
|
936
|
+
throw new Error("private_profile_identity_changed");
|
|
937
|
+
}
|
|
938
|
+
function assertProfileAvailable(grant, projectRoot) {
|
|
939
|
+
assertProfileIdentity(grant, projectRoot);
|
|
940
|
+
try {
|
|
941
|
+
command("flock", [
|
|
942
|
+
"--exclusive",
|
|
943
|
+
"--nonblock",
|
|
944
|
+
"--conflict-exit-code",
|
|
945
|
+
"73",
|
|
946
|
+
join4(grant.directory, privateProfileLock),
|
|
947
|
+
"true"
|
|
948
|
+
]);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (error.status === 73)
|
|
951
|
+
throw new Error("private_profile_busy");
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/harness/adapters/docker/profile.ts
|
|
957
|
+
function assertLocalDocker() {
|
|
958
|
+
if (process.env.DOCKER_HOST)
|
|
959
|
+
throw new Error("explicit_docker_host_not_admitted");
|
|
960
|
+
const endpoint = JSON.parse(
|
|
961
|
+
command("docker", [
|
|
962
|
+
"context",
|
|
963
|
+
"inspect",
|
|
964
|
+
"--format",
|
|
965
|
+
"{{json .Endpoints.docker.Host}}"
|
|
966
|
+
])
|
|
967
|
+
);
|
|
968
|
+
if (new URL(endpoint).protocol !== "unix:")
|
|
969
|
+
throw new Error("remote_docker_context_not_admitted");
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/harness/adapters/docker/mounts.ts
|
|
973
|
+
import { lstatSync as lstatSync4, realpathSync as realpathSync5 } from "fs";
|
|
974
|
+
import { join as join5 } from "path";
|
|
975
|
+
function assertContainerMounts(actual, statePath, projectRoot, directory, nonce, policy, privateProfile, runtime) {
|
|
976
|
+
const state = text(statePath, "container_state_directory");
|
|
977
|
+
const profile = selectedProfile(policy, privateProfile, runtime ?? "");
|
|
978
|
+
if (profile) assertProfileIdentity(profile, projectRoot);
|
|
979
|
+
const expected = profile?.directory ?? join5(directory, "containers", nonce, "profile");
|
|
980
|
+
if (state !== expected || realpathSync5(state) !== expected)
|
|
981
|
+
throw new Error("foreign_container_state_directory");
|
|
982
|
+
const stat = lstatSync4(state);
|
|
983
|
+
if (!stat.isDirectory() || stat.uid !== process.getuid?.() || (stat.mode & 63) !== 0)
|
|
984
|
+
throw new Error("unsafe_container_state_directory");
|
|
985
|
+
if (!Array.isArray(actual.Mounts) || actual.Mounts.length !== 2)
|
|
986
|
+
throw new Error("ungranted_container_mount");
|
|
987
|
+
if (new Set(actual.Mounts.map((value) => object(value).Destination)).size !== 2)
|
|
988
|
+
throw new Error("ungranted_container_mount");
|
|
989
|
+
const expectedMounts = [
|
|
990
|
+
{
|
|
991
|
+
Source: projectRoot,
|
|
992
|
+
Destination: "/workspace",
|
|
993
|
+
RW: policy.workspaceAccess === "read-write"
|
|
994
|
+
},
|
|
995
|
+
{ Source: state, Destination: "/state", RW: true }
|
|
996
|
+
];
|
|
997
|
+
for (const value of actual.Mounts) {
|
|
998
|
+
const mount = object(value);
|
|
999
|
+
if (mount.Type !== "bind" || mount.Propagation !== "rprivate" || !expectedMounts.some(
|
|
1000
|
+
(expected2) => mount.Source === expected2.Source && mount.Destination === expected2.Destination && mount.RW === expected2.RW
|
|
1001
|
+
))
|
|
1002
|
+
throw new Error("ungranted_container_mount");
|
|
1003
|
+
}
|
|
1004
|
+
const host = object(actual.HostConfig);
|
|
1005
|
+
if (!Array.isArray(host.Mounts) || host.Mounts.length !== 2 || host.Mounts.some(
|
|
1006
|
+
(value) => object(object(value).BindOptions).NonRecursive !== true
|
|
1007
|
+
))
|
|
1008
|
+
throw new Error("recursive_container_mount_not_admitted");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/harness/adapters/docker/security.ts
|
|
1012
|
+
function assertContainerIsolation(host, network) {
|
|
1013
|
+
if (host.ReadonlyRootfs !== true || host.Privileged !== false || host.NetworkMode !== network || host.CgroupnsMode !== "private")
|
|
1014
|
+
throw new Error("invalid_container_isolation_policy");
|
|
1015
|
+
if (JSON.stringify(host.CapDrop) !== '["ALL"]' || JSON.stringify(host.SecurityOpt) !== '["no-new-privileges"]' || object(host.RestartPolicy).Name !== "no")
|
|
1016
|
+
throw new Error("invalid_container_execution_policy");
|
|
1017
|
+
if (host.CapAdd !== null || host.PidMode !== "" || host.UTSMode !== "" || host.UsernsMode !== "" || host.CgroupParent !== "" || host.IpcMode !== "private" || hasDevices(host.Devices) || hasDevices(host.DeviceRequests) || host.AutoRemove !== false || host.VolumesFrom !== null || host.GroupAdd !== null || host.Binds !== null || Object.keys(object(host.PortBindings)).length)
|
|
1018
|
+
throw new Error("ungranted_container_host_authority");
|
|
1019
|
+
}
|
|
1020
|
+
function hasDevices(value) {
|
|
1021
|
+
if (value === null) return false;
|
|
1022
|
+
return !Array.isArray(value) || value.length !== 0;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/harness/adapters/docker/entrypoint.ts
|
|
1026
|
+
function bridgeName(nonce) {
|
|
1027
|
+
return `.pmcp-egress-${nonce.slice(0, 8)}.mjs`;
|
|
1028
|
+
}
|
|
1029
|
+
function containerEntrypoint(policy, runtime, nonce, privateProfile) {
|
|
1030
|
+
const native = policy.commands[runtime];
|
|
1031
|
+
if (!native) throw new Error("docker_runtime_command_not_granted");
|
|
1032
|
+
const command2 = policy.egress?.bridgeExecutable ? [
|
|
1033
|
+
policy.egress.bridgeExecutable,
|
|
1034
|
+
`/state/${bridgeName(nonce)}`,
|
|
1035
|
+
"--",
|
|
1036
|
+
...native
|
|
1037
|
+
] : native;
|
|
1038
|
+
const profile = selectedProfile(policy, privateProfile, runtime);
|
|
1039
|
+
if (!profile) return command2;
|
|
1040
|
+
return [
|
|
1041
|
+
profile.lockExecutable,
|
|
1042
|
+
"--exclusive",
|
|
1043
|
+
"--nonblock",
|
|
1044
|
+
"--close",
|
|
1045
|
+
"--conflict-exit-code",
|
|
1046
|
+
"73",
|
|
1047
|
+
`/state/${privateProfileLock}`,
|
|
1048
|
+
...command2
|
|
1049
|
+
];
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/harness/adapters/docker/admission.ts
|
|
1053
|
+
function admitResource(path, group, role, directory, memoryMb, runtime) {
|
|
1054
|
+
if (group.profile !== "docker" || !group.dockerPolicy)
|
|
1055
|
+
throw new Error("container_requires_docker_profile");
|
|
1056
|
+
const { projectRoot } = group;
|
|
1057
|
+
assertLocalDocker();
|
|
1058
|
+
const inputPath = text(path, "container_file");
|
|
1059
|
+
if (lstatSync5(inputPath).isSymbolicLink())
|
|
1060
|
+
throw new Error("symlink_container_file_not_admitted");
|
|
1061
|
+
const source2 = realpathSync6(inputPath);
|
|
1062
|
+
const scope = relative3(projectRoot, source2);
|
|
1063
|
+
if (scope === ".." || scope.startsWith("../"))
|
|
1064
|
+
throw new Error("container_file_outside_project");
|
|
1065
|
+
const claim = object(readPrivateJson(source2));
|
|
1066
|
+
if (claim.privateProfile !== void 0)
|
|
1067
|
+
throw new Error("private_profile_requires_cli_owned_creation");
|
|
1068
|
+
return admitClaim(claim, group, role, directory, memoryMb, runtime);
|
|
1069
|
+
}
|
|
1070
|
+
function admitClaim(value, group, role, directory, memoryMb, runtime, cwd = group.projectRoot) {
|
|
1071
|
+
if (group.profile !== "docker" || !group.dockerPolicy)
|
|
1072
|
+
throw new Error("container_requires_docker_profile");
|
|
1073
|
+
assertLocalDocker();
|
|
1074
|
+
const policy = group.dockerPolicy;
|
|
1075
|
+
const { owner, projectRoot } = group;
|
|
1076
|
+
const claim = object(value);
|
|
1077
|
+
if (claim.owner !== owner || claim.role !== role || claim.runtime !== runtime)
|
|
1078
|
+
throw new Error("foreign_container_resource");
|
|
1079
|
+
const resource = {
|
|
1080
|
+
kind: "docker",
|
|
1081
|
+
id: text(claim.id, "container_id"),
|
|
1082
|
+
owner,
|
|
1083
|
+
nonce: text(claim.nonce, "container_nonce"),
|
|
1084
|
+
role,
|
|
1085
|
+
image: text(claim.image, "container_image"),
|
|
1086
|
+
configurationDigest: "",
|
|
1087
|
+
memoryMb: 0,
|
|
1088
|
+
cpuQuota: 0,
|
|
1089
|
+
...claim.privateProfile === void 0 ? {} : { privateProfile: profileName(claim.privateProfile) }
|
|
1090
|
+
};
|
|
1091
|
+
if (!/^[a-f0-9]{64}$/.test(resource.id) || !/^sha256:[a-f0-9]{64}$/.test(resource.image) || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(
|
|
1092
|
+
resource.nonce
|
|
1093
|
+
))
|
|
1094
|
+
throw new Error("invalid_container_resource");
|
|
1095
|
+
const actual = inspectResource(resource, false);
|
|
1096
|
+
const state = object(actual.State);
|
|
1097
|
+
const config = object(actual.Config);
|
|
1098
|
+
const host = object(actual.HostConfig);
|
|
1099
|
+
if (resource.image !== policy.image || JSON.stringify(config.Entrypoint) !== JSON.stringify(
|
|
1100
|
+
containerEntrypoint(
|
|
1101
|
+
policy,
|
|
1102
|
+
runtime,
|
|
1103
|
+
resource.nonce,
|
|
1104
|
+
resource.privateProfile
|
|
1105
|
+
)
|
|
1106
|
+
))
|
|
1107
|
+
throw new Error("container_runtime_image_not_granted");
|
|
1108
|
+
if (config.WorkingDir !== join6("/workspace", relative3(projectRoot, cwd)))
|
|
1109
|
+
throw new Error("container_cwd_not_granted");
|
|
1110
|
+
assertContainerMounts(
|
|
1111
|
+
actual,
|
|
1112
|
+
claim.state,
|
|
1113
|
+
projectRoot,
|
|
1114
|
+
directory,
|
|
1115
|
+
resource.nonce,
|
|
1116
|
+
policy,
|
|
1117
|
+
resource.privateProfile,
|
|
1118
|
+
runtime
|
|
1119
|
+
);
|
|
1120
|
+
if (state.Status !== "created" || state.Pid !== 0 || state.Running !== false)
|
|
1121
|
+
throw new Error("container_requires_fresh_creation");
|
|
1122
|
+
if (config.User !== `${process.getuid?.()}:${process.getgid?.()}` || config.Tty !== true || config.OpenStdin !== true || config.StdinOnce !== false || !Array.isArray(config.Env) || JSON.stringify(
|
|
1123
|
+
config.Env.filter(
|
|
1124
|
+
(value2) => typeof value2 === "string" && value2.startsWith("HOME=")
|
|
1125
|
+
)
|
|
1126
|
+
) !== '["HOME=/state"]')
|
|
1127
|
+
throw new Error("invalid_container_terminal_policy");
|
|
1128
|
+
assertContainerIsolation(host, policy.network);
|
|
1129
|
+
resource.memoryMb = integer(
|
|
1130
|
+
Number(host.Memory) / 1048576,
|
|
1131
|
+
"container_memory",
|
|
1132
|
+
32,
|
|
1133
|
+
1472
|
|
1134
|
+
);
|
|
1135
|
+
if (host.MemorySwap !== host.Memory || memoryMb - resource.memoryMb < 64)
|
|
1136
|
+
throw new Error("container_proxy_memory_unreserved");
|
|
1137
|
+
integer(host.PidsLimit, "container_tasks", 1, 128);
|
|
1138
|
+
if (host.CpuPeriod !== 1e5)
|
|
1139
|
+
throw new Error("invalid_container_cpu_period");
|
|
1140
|
+
resource.cpuQuota = integer(host.CpuQuota, "container_cpu", 1, 5e4);
|
|
1141
|
+
resource.configurationDigest = configurationDigest(actual);
|
|
1142
|
+
return resource;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/harness/adapters/docker/engine.ts
|
|
1146
|
+
import { request } from "http";
|
|
1147
|
+
var DockerEngine = class {
|
|
1148
|
+
async create(name, configuration) {
|
|
1149
|
+
assertLocalDocker();
|
|
1150
|
+
const endpoint = JSON.parse(
|
|
1151
|
+
command("docker", [
|
|
1152
|
+
"context",
|
|
1153
|
+
"inspect",
|
|
1154
|
+
"--format",
|
|
1155
|
+
"{{json .Endpoints.docker.Host}}"
|
|
1156
|
+
])
|
|
1157
|
+
);
|
|
1158
|
+
const version = command("docker", [
|
|
1159
|
+
"version",
|
|
1160
|
+
"--format",
|
|
1161
|
+
"{{.Server.APIVersion}}"
|
|
1162
|
+
]).trim();
|
|
1163
|
+
if (!/^1\.\d+$/.test(version))
|
|
1164
|
+
throw new Error("invalid_docker_api_version");
|
|
1165
|
+
const payload = JSON.stringify(configuration);
|
|
1166
|
+
return new Promise((resolve2, reject) => {
|
|
1167
|
+
const req = request(
|
|
1168
|
+
{
|
|
1169
|
+
socketPath: new URL(endpoint).pathname,
|
|
1170
|
+
path: `/v${version}/containers/create?name=${encodeURIComponent(name)}`,
|
|
1171
|
+
method: "POST",
|
|
1172
|
+
headers: {
|
|
1173
|
+
"Content-Type": "application/json",
|
|
1174
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
(response) => {
|
|
1178
|
+
const chunks = [];
|
|
1179
|
+
let bytes = 0;
|
|
1180
|
+
response.on("data", (chunk) => {
|
|
1181
|
+
bytes += chunk.length;
|
|
1182
|
+
if (bytes > 65536) {
|
|
1183
|
+
req.destroy(new Error("docker_response_too_large"));
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
chunks.push(chunk);
|
|
1187
|
+
});
|
|
1188
|
+
response.once("error", reject);
|
|
1189
|
+
response.once("end", () => {
|
|
1190
|
+
if (response.statusCode !== 201) {
|
|
1191
|
+
reject(new Error(`docker_create_status_${response.statusCode}`));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
try {
|
|
1195
|
+
const id2 = text(
|
|
1196
|
+
object(JSON.parse(Buffer.concat(chunks).toString("utf8"))).Id,
|
|
1197
|
+
"container_id"
|
|
1198
|
+
);
|
|
1199
|
+
if (!/^[a-f0-9]{64}$/.test(id2))
|
|
1200
|
+
throw new Error("invalid_container_id");
|
|
1201
|
+
resolve2(id2);
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
reject(error);
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
);
|
|
1208
|
+
req.setTimeout(
|
|
1209
|
+
1e4,
|
|
1210
|
+
() => req.destroy(new Error("docker_create_timeout"))
|
|
1211
|
+
);
|
|
1212
|
+
req.once("error", reject);
|
|
1213
|
+
req.end(payload);
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
// src/harness/adapters/docker/creation-plan.ts
|
|
1219
|
+
import { join as join8, relative as relative4 } from "path";
|
|
1220
|
+
|
|
1221
|
+
// src/harness/adapters/docker/bridge-artifact.ts
|
|
1222
|
+
import { createHash as createHash3 } from "crypto";
|
|
1223
|
+
import {
|
|
1224
|
+
closeSync as closeSync3,
|
|
1225
|
+
constants as constants3,
|
|
1226
|
+
fstatSync as fstatSync3,
|
|
1227
|
+
fsyncSync as fsyncSync2,
|
|
1228
|
+
openSync as openSync3,
|
|
1229
|
+
readFileSync as readFileSync3,
|
|
1230
|
+
writeFileSync as writeFileSync3
|
|
1231
|
+
} from "fs";
|
|
1232
|
+
import { join as join7 } from "path";
|
|
1233
|
+
function source() {
|
|
1234
|
+
const path = artifact("egress-bridge");
|
|
1235
|
+
if (!path.endsWith(".js"))
|
|
1236
|
+
throw new Error("docker_bridge_requires_compiled_cli");
|
|
1237
|
+
return readFileSync3(path);
|
|
1238
|
+
}
|
|
1239
|
+
function digest(value) {
|
|
1240
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
1241
|
+
}
|
|
1242
|
+
function bridgeDigest() {
|
|
1243
|
+
return digest(source());
|
|
1244
|
+
}
|
|
1245
|
+
function prepareBridge(creation) {
|
|
1246
|
+
if (!creation.egress?.bridgeExecutable) return;
|
|
1247
|
+
const content = source();
|
|
1248
|
+
if (digest(content) !== creation.bridgeSha256)
|
|
1249
|
+
throw new Error("docker_bridge_source_changed");
|
|
1250
|
+
const descriptor = openSync3(
|
|
1251
|
+
join7(creation.state, bridgeName(creation.nonce)),
|
|
1252
|
+
"wx",
|
|
1253
|
+
384
|
|
1254
|
+
);
|
|
1255
|
+
try {
|
|
1256
|
+
writeFileSync3(descriptor, content);
|
|
1257
|
+
fsyncSync2(descriptor);
|
|
1258
|
+
} finally {
|
|
1259
|
+
closeSync3(descriptor);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function assertBridge(creation) {
|
|
1263
|
+
if (!creation.egress?.bridgeExecutable) return;
|
|
1264
|
+
const descriptor = openSync3(
|
|
1265
|
+
join7(creation.state, bridgeName(creation.nonce)),
|
|
1266
|
+
constants3.O_RDONLY | constants3.O_NOFOLLOW
|
|
1267
|
+
);
|
|
1268
|
+
try {
|
|
1269
|
+
const stat = fstatSync3(descriptor);
|
|
1270
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.nlink !== 1 || (stat.mode & 63) !== 0 || stat.size > 2097152 || digest(readFileSync3(descriptor)) !== creation.bridgeSha256)
|
|
1271
|
+
throw new Error("docker_bridge_artifact_changed");
|
|
1272
|
+
} finally {
|
|
1273
|
+
closeSync3(descriptor);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/harness/adapters/docker/creation-plan.ts
|
|
1278
|
+
function creationPlan(group, directory, id2, nonce, role, runtime, cwd, preparation) {
|
|
1279
|
+
if (group.profile !== "docker" || !group.dockerPolicy)
|
|
1280
|
+
throw new Error("container_requires_docker_profile");
|
|
1281
|
+
assertLocalDocker();
|
|
1282
|
+
const policy = group.dockerPolicy;
|
|
1283
|
+
const images = JSON.parse(
|
|
1284
|
+
command("docker", ["image", "inspect", policy.image])
|
|
1285
|
+
);
|
|
1286
|
+
if (!Array.isArray(images) || images.length !== 1 || object(images[0]).Id !== policy.image)
|
|
1287
|
+
throw new Error("docker_image_not_available");
|
|
1288
|
+
const imageConfig = object(object(images[0]).Config);
|
|
1289
|
+
const environment = imageConfig.Env;
|
|
1290
|
+
if (!Array.isArray(environment) || environment.some((entry) => typeof entry !== "string"))
|
|
1291
|
+
throw new Error("invalid_container_image_environment");
|
|
1292
|
+
const profile = selectedProfile(policy, preparation.privateProfile, runtime);
|
|
1293
|
+
if (profile) assertProfileAvailable(profile, group.projectRoot);
|
|
1294
|
+
const state = profile?.directory ?? join8(directory, "containers", nonce, "profile");
|
|
1295
|
+
return {
|
|
1296
|
+
name: containerName(group.owner, id2),
|
|
1297
|
+
nonce,
|
|
1298
|
+
state,
|
|
1299
|
+
...preparation.privateProfile ? { privateProfile: preparation.privateProfile } : {},
|
|
1300
|
+
...policy.egress ? { egress: policy.egress } : {},
|
|
1301
|
+
...policy.egress?.bridgeExecutable ? { bridgeSha256: bridgeDigest() } : {},
|
|
1302
|
+
configuration: {
|
|
1303
|
+
Image: policy.image,
|
|
1304
|
+
Entrypoint: containerEntrypoint(
|
|
1305
|
+
policy,
|
|
1306
|
+
runtime,
|
|
1307
|
+
nonce,
|
|
1308
|
+
preparation.privateProfile
|
|
1309
|
+
),
|
|
1310
|
+
Cmd: preparation.args.length ? preparation.args : null,
|
|
1311
|
+
WorkingDir: join8("/workspace", relative4(group.projectRoot, cwd)),
|
|
1312
|
+
User: `${process.getuid?.()}:${process.getgid?.()}`,
|
|
1313
|
+
Env: [
|
|
1314
|
+
...environment.filter(
|
|
1315
|
+
(entry) => !/^(HOME|TERM|PMCP_HTTPS_PROXY_SOCKET)=/.test(entry)
|
|
1316
|
+
),
|
|
1317
|
+
"HOME=/state",
|
|
1318
|
+
"TERM=xterm-256color",
|
|
1319
|
+
...policy.egress ? [`PMCP_HTTPS_PROXY_SOCKET=/state/e${nonce.slice(0, 8)}`] : []
|
|
1320
|
+
],
|
|
1321
|
+
Tty: true,
|
|
1322
|
+
OpenStdin: true,
|
|
1323
|
+
StdinOnce: false,
|
|
1324
|
+
AttachStdin: false,
|
|
1325
|
+
AttachStdout: true,
|
|
1326
|
+
AttachStderr: true,
|
|
1327
|
+
Labels: {
|
|
1328
|
+
...imageConfig.Labels ?? {},
|
|
1329
|
+
[containerLabels.owner]: group.owner,
|
|
1330
|
+
[containerLabels.nonce]: nonce,
|
|
1331
|
+
[containerLabels.role]: role
|
|
1332
|
+
},
|
|
1333
|
+
HostConfig: {
|
|
1334
|
+
NetworkMode: policy.network,
|
|
1335
|
+
CgroupnsMode: "private",
|
|
1336
|
+
ReadonlyRootfs: true,
|
|
1337
|
+
CapDrop: ["ALL"],
|
|
1338
|
+
SecurityOpt: ["no-new-privileges"],
|
|
1339
|
+
Memory: preparation.memoryMb * 1048576,
|
|
1340
|
+
MemorySwap: preparation.memoryMb * 1048576,
|
|
1341
|
+
PidsLimit: 64,
|
|
1342
|
+
CpuPeriod: 1e5,
|
|
1343
|
+
CpuQuota: preparation.cpuQuota,
|
|
1344
|
+
RestartPolicy: { Name: "no" },
|
|
1345
|
+
LogConfig: { Type: "none" },
|
|
1346
|
+
Tmpfs: { "/tmp": "rw,nosuid,noexec,size=16m" },
|
|
1347
|
+
Mounts: [
|
|
1348
|
+
{
|
|
1349
|
+
Type: "bind",
|
|
1350
|
+
Source: group.projectRoot,
|
|
1351
|
+
Target: "/workspace",
|
|
1352
|
+
...policy.workspaceAccess === "read-only" ? { ReadOnly: true } : {},
|
|
1353
|
+
BindOptions: { NonRecursive: true }
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
Type: "bind",
|
|
1357
|
+
Source: state,
|
|
1358
|
+
Target: "/state",
|
|
1359
|
+
BindOptions: { NonRecursive: true }
|
|
1360
|
+
}
|
|
1361
|
+
]
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
function assertCreationConfiguration(actual, creation) {
|
|
1367
|
+
if (actual.Name !== `/${creation.name}`)
|
|
1368
|
+
throw new Error("foreign_container_creation");
|
|
1369
|
+
const expected = creation.configuration;
|
|
1370
|
+
const config = object(actual.Config);
|
|
1371
|
+
const host = object(actual.HostConfig);
|
|
1372
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
1373
|
+
if (key === "HostConfig" || key === "Image") continue;
|
|
1374
|
+
if (!matchesFields(config[key], value))
|
|
1375
|
+
throw new Error(`container_creation_configuration_changed_${key}`);
|
|
1376
|
+
}
|
|
1377
|
+
for (const [key, value] of Object.entries(object(expected.HostConfig))) {
|
|
1378
|
+
if (!matchesFields(host[key], value))
|
|
1379
|
+
throw new Error(`container_creation_configuration_changed_${key}`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function matchesFields(actual, expected) {
|
|
1383
|
+
if (Array.isArray(expected))
|
|
1384
|
+
return Array.isArray(actual) && actual.length === expected.length && expected.every((value2, index) => matchesFields(actual[index], value2));
|
|
1385
|
+
if (expected === null || typeof expected !== "object")
|
|
1386
|
+
return actual === expected;
|
|
1387
|
+
if (actual === null || typeof actual !== "object" || Array.isArray(actual))
|
|
1388
|
+
return false;
|
|
1389
|
+
const value = actual;
|
|
1390
|
+
return Object.entries(expected).every(
|
|
1391
|
+
([key, field]) => matchesFields(value[key], field)
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/harness/adapters/docker/creation.ts
|
|
1396
|
+
async function prepareCreation(journal) {
|
|
1397
|
+
const intent = journal.intent();
|
|
1398
|
+
const creation = intent.dockerCreation;
|
|
1399
|
+
if (!creation) throw new Error("container_creation_missing");
|
|
1400
|
+
const group = new GroupStore(join9(journal.records.directory, "group.json")).state.group;
|
|
1401
|
+
const profile = selectedProfile(
|
|
1402
|
+
group.dockerPolicy,
|
|
1403
|
+
creation.privateProfile,
|
|
1404
|
+
intent.runtime
|
|
1405
|
+
);
|
|
1406
|
+
if (profile) assertProfileAvailable(profile, group.projectRoot);
|
|
1407
|
+
if (!profile) mkdirSync2(creation.state, { recursive: true, mode: 448 });
|
|
1408
|
+
prepareBridge(creation);
|
|
1409
|
+
assertBridge(creation);
|
|
1410
|
+
const id2 = await new DockerEngine().create(
|
|
1411
|
+
creation.name,
|
|
1412
|
+
creation.configuration
|
|
1413
|
+
);
|
|
1414
|
+
const current = new GroupStore(join9(journal.records.directory, "group.json")).state.group;
|
|
1415
|
+
if (current.id !== intent.groupId || current.generation !== intent.generation)
|
|
1416
|
+
throw new Error("launch_group_generation_mismatch");
|
|
1417
|
+
const resource = {
|
|
1418
|
+
...admitClaim(
|
|
1419
|
+
{
|
|
1420
|
+
id: id2,
|
|
1421
|
+
owner: intent.owner,
|
|
1422
|
+
role: intent.role,
|
|
1423
|
+
runtime: intent.runtime,
|
|
1424
|
+
image: current.dockerPolicy?.image,
|
|
1425
|
+
nonce: creation.nonce,
|
|
1426
|
+
state: creation.state,
|
|
1427
|
+
...creation.privateProfile ? { privateProfile: creation.privateProfile } : {}
|
|
1428
|
+
},
|
|
1429
|
+
current,
|
|
1430
|
+
intent.role,
|
|
1431
|
+
journal.records.directory,
|
|
1432
|
+
intent.memoryMb,
|
|
1433
|
+
intent.runtime,
|
|
1434
|
+
intent.cwd
|
|
1435
|
+
),
|
|
1436
|
+
managed: true
|
|
1437
|
+
};
|
|
1438
|
+
assertCreationConfiguration(
|
|
1439
|
+
object(JSON.parse(command("docker", ["inspect", id2]))[0]),
|
|
1440
|
+
creation
|
|
1441
|
+
);
|
|
1442
|
+
privateJson(journal.path("intent"), {
|
|
1443
|
+
...intent,
|
|
1444
|
+
resource,
|
|
1445
|
+
argv: [intent.argv[0], "start", "--attach", "--interactive", id2]
|
|
1446
|
+
});
|
|
1447
|
+
return resource;
|
|
1448
|
+
}
|
|
1449
|
+
function findCreation(journal) {
|
|
1450
|
+
const intent = journal.intent();
|
|
1451
|
+
const creation = intent.dockerCreation;
|
|
1452
|
+
if (!creation) return intent.resource;
|
|
1453
|
+
assertLocalDocker();
|
|
1454
|
+
let actual;
|
|
1455
|
+
try {
|
|
1456
|
+
const values = JSON.parse(
|
|
1457
|
+
command("docker", ["inspect", creation.name])
|
|
1458
|
+
);
|
|
1459
|
+
if (!Array.isArray(values) || values.length !== 1)
|
|
1460
|
+
throw new Error("invalid_container_inspection");
|
|
1461
|
+
actual = object(values[0]);
|
|
1462
|
+
} catch (error) {
|
|
1463
|
+
if (resourceAbsent(error, creation.name)) return;
|
|
1464
|
+
throw error;
|
|
1465
|
+
}
|
|
1466
|
+
const resource = {
|
|
1467
|
+
kind: "docker",
|
|
1468
|
+
id: text(actual.Id, "container_id"),
|
|
1469
|
+
owner: intent.owner,
|
|
1470
|
+
nonce: creation.nonce,
|
|
1471
|
+
role: intent.role,
|
|
1472
|
+
image: text(creation.configuration.Image, "container_image"),
|
|
1473
|
+
configurationDigest: configurationDigest(actual),
|
|
1474
|
+
memoryMb: Number(object(actual.HostConfig).Memory) / 1048576,
|
|
1475
|
+
cpuQuota: Number(object(actual.HostConfig).CpuQuota),
|
|
1476
|
+
managed: true,
|
|
1477
|
+
...creation.privateProfile ? { privateProfile: creation.privateProfile } : {}
|
|
1478
|
+
};
|
|
1479
|
+
assertResourceOwner(resource, actual);
|
|
1480
|
+
assertCreationConfiguration(actual, creation);
|
|
1481
|
+
if (!intent.resource) {
|
|
1482
|
+
const group = new GroupStore(join9(journal.records.directory, "group.json")).state.group;
|
|
1483
|
+
return {
|
|
1484
|
+
...admitClaim(
|
|
1485
|
+
{ ...resource, runtime: intent.runtime, state: creation.state },
|
|
1486
|
+
group,
|
|
1487
|
+
intent.role,
|
|
1488
|
+
journal.records.directory,
|
|
1489
|
+
intent.memoryMb,
|
|
1490
|
+
intent.runtime,
|
|
1491
|
+
intent.cwd
|
|
1492
|
+
),
|
|
1493
|
+
managed: true
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
if (intent.resource && (resource.id !== intent.resource.id || resource.configurationDigest !== intent.resource.configurationDigest))
|
|
1497
|
+
throw new Error("container_creation_configuration_changed");
|
|
1498
|
+
return resource;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/harness/launch/recovery.ts
|
|
1502
|
+
function abortLaunch(connection, journal) {
|
|
1503
|
+
const intent = journal.intent();
|
|
1504
|
+
if (!controllerExited(intent.controllerIdentity))
|
|
1505
|
+
throw new Error("launch_controller_alive");
|
|
1506
|
+
const previous = journal.abortRecord();
|
|
1507
|
+
const resource = intent.dockerCreation ? findCreation(journal) : intent.resource;
|
|
1508
|
+
if (previous?.state === "stopped") {
|
|
1509
|
+
if (resource) stopResource(resource);
|
|
1510
|
+
if (unitActive(intent.unit)) throw new Error("runtime_stop_unconfirmed");
|
|
1511
|
+
return {
|
|
1512
|
+
stopped: true,
|
|
1513
|
+
alreadyStopped: true,
|
|
1514
|
+
adopted: false,
|
|
1515
|
+
inputReplayed: false
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
if (!existsSync3(journal.path("proof"))) {
|
|
1519
|
+
const paneUnknown = connection.alive() && (!intent.dockerCreation || connection.tmux(["list-sessions", "-F", "#{session_name}"]).trim().split("\n").includes(`pmcp-${intent.id}`));
|
|
1520
|
+
if (paneUnknown || unitActive(intent.unit))
|
|
1521
|
+
throw new Error("launch_identity_unknown");
|
|
1522
|
+
if (resource) stopResource(resource);
|
|
1523
|
+
privateJson(journal.path("aborted"), {
|
|
1524
|
+
id: intent.id,
|
|
1525
|
+
owner: intent.owner,
|
|
1526
|
+
state: "stopped",
|
|
1527
|
+
noRuntimeBorn: true
|
|
1528
|
+
});
|
|
1529
|
+
return { stopped: true, adopted: false, inputReplayed: false };
|
|
1530
|
+
}
|
|
1531
|
+
const proof = journal.proof();
|
|
1532
|
+
const present = panePresent(connection, proof);
|
|
1533
|
+
if (present) assertProof(connection, proof);
|
|
1534
|
+
if (resource) stopResource(resource);
|
|
1535
|
+
privateJson(journal.path("aborted"), {
|
|
1536
|
+
id: intent.id,
|
|
1537
|
+
owner: intent.owner,
|
|
1538
|
+
state: "stopping"
|
|
1539
|
+
});
|
|
1540
|
+
stopUnit(intent.unit);
|
|
1541
|
+
if (present) {
|
|
1542
|
+
assertProof(connection, proof);
|
|
1543
|
+
connection.tmux(["kill-pane", "-t", proof.session.paneId]);
|
|
1544
|
+
}
|
|
1545
|
+
if (existsSync3(journal.records.path(intent.id)))
|
|
1546
|
+
journal.records.save({
|
|
1547
|
+
...proof.session,
|
|
1548
|
+
stoppedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1549
|
+
});
|
|
1550
|
+
privateJson(journal.path("aborted"), {
|
|
1551
|
+
id: intent.id,
|
|
1552
|
+
owner: intent.owner,
|
|
1553
|
+
state: "stopped"
|
|
1554
|
+
});
|
|
1555
|
+
return { stopped: true, adopted: false, inputReplayed: false };
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// src/harness/adapters/tmux/lifecycle.ts
|
|
1559
|
+
async function start(connection, spec) {
|
|
1560
|
+
const records2 = connection.records;
|
|
1561
|
+
if (spec.budgetSlice !== budgetSlice(records2.owner))
|
|
1562
|
+
throw new Error("foreign_budget_slice");
|
|
1563
|
+
integer(spec.memoryMb, "session_memory", 32, 1536);
|
|
1564
|
+
runtimeUnit(spec.groupId);
|
|
1565
|
+
runtimeUnit(spec.generation);
|
|
1566
|
+
if (spec.role !== "main" && spec.role !== "worker")
|
|
1567
|
+
throw new Error("invalid_launch_role");
|
|
1568
|
+
const id2 = randomUUID3();
|
|
1569
|
+
const nonce = randomUUID3();
|
|
1570
|
+
const journal = new LaunchJournal(records2, id2);
|
|
1571
|
+
const dockerCreation = spec.dockerPreparation ? creationPlan(
|
|
1572
|
+
new GroupStore(join10(records2.directory, "group.json")).state.group,
|
|
1573
|
+
records2.directory,
|
|
1574
|
+
id2,
|
|
1575
|
+
nonce,
|
|
1576
|
+
spec.role,
|
|
1577
|
+
spec.runtime,
|
|
1578
|
+
spec.cwd,
|
|
1579
|
+
spec.dockerPreparation
|
|
1580
|
+
) : void 0;
|
|
1581
|
+
privateJson(journal.path("intent"), {
|
|
1582
|
+
id: id2,
|
|
1583
|
+
owner: records2.owner,
|
|
1584
|
+
controllerIdentity: processIdentity(process.pid),
|
|
1585
|
+
groupId: spec.groupId,
|
|
1586
|
+
generation: spec.generation,
|
|
1587
|
+
role: spec.role,
|
|
1588
|
+
nonce,
|
|
1589
|
+
unit: runtimeUnit(id2),
|
|
1590
|
+
slice: spec.budgetSlice,
|
|
1591
|
+
socket: records2.socket,
|
|
1592
|
+
runtime: spec.runtime,
|
|
1593
|
+
memoryMb: spec.memoryMb,
|
|
1594
|
+
argv: spec.argv,
|
|
1595
|
+
cwd: spec.cwd,
|
|
1596
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1597
|
+
...spec.resource ? { resource: spec.resource } : {},
|
|
1598
|
+
...dockerCreation ? { dockerCreation } : {}
|
|
1599
|
+
});
|
|
1600
|
+
if (dockerCreation) await prepareCreation(journal);
|
|
1601
|
+
const intent = journal.intent();
|
|
1602
|
+
const argv = [
|
|
1603
|
+
"systemd-run",
|
|
1604
|
+
"--user",
|
|
1605
|
+
"--scope",
|
|
1606
|
+
"--collect",
|
|
1607
|
+
"--quiet",
|
|
1608
|
+
`--unit=${intent.unit}`,
|
|
1609
|
+
`--slice=${intent.slice}`,
|
|
1610
|
+
`--property=MemoryMax=${spec.memoryMb - (intent.resource?.memoryMb ?? 0)}M`,
|
|
1611
|
+
"--property=TasksMax=128",
|
|
1612
|
+
"--property=CPUQuota=100%",
|
|
1613
|
+
process.execPath,
|
|
1614
|
+
artifact("launch-gate"),
|
|
1615
|
+
records2.directory,
|
|
1616
|
+
id2
|
|
1617
|
+
];
|
|
1618
|
+
connection.tmux([
|
|
1619
|
+
"new-session",
|
|
1620
|
+
"-d",
|
|
1621
|
+
"-x",
|
|
1622
|
+
"160",
|
|
1623
|
+
"-y",
|
|
1624
|
+
"42",
|
|
1625
|
+
"-s",
|
|
1626
|
+
`pmcp-${id2}`,
|
|
1627
|
+
"-c",
|
|
1628
|
+
spec.cwd,
|
|
1629
|
+
`exec ${argv.map(quote).join(" ")}`
|
|
1630
|
+
]);
|
|
1631
|
+
const deadline = Date.now() + 5e3;
|
|
1632
|
+
while (!existsSync4(journal.path("proof"))) {
|
|
1633
|
+
if (Date.now() >= deadline) throw new Error("launch_proof_timeout");
|
|
1634
|
+
await setTimeout(25);
|
|
1635
|
+
}
|
|
1636
|
+
const proof = journal.proof();
|
|
1637
|
+
assertProof(connection, proof);
|
|
1638
|
+
records2.save(proof.session);
|
|
1639
|
+
return proof.session;
|
|
1640
|
+
}
|
|
1641
|
+
function stop(connection, id2) {
|
|
1642
|
+
const journal = new LaunchJournal(connection.records, id2);
|
|
1643
|
+
if (existsSync4(journal.path("intent"))) {
|
|
1644
|
+
abortLaunch(connection, journal);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const record = connection.inspect(id2);
|
|
1648
|
+
if (record.resource) stopResource(record.resource);
|
|
1649
|
+
stopUnit(record.unit);
|
|
1650
|
+
connection.inspect(id2);
|
|
1651
|
+
connection.tmux(["kill-pane", "-t", record.paneId]);
|
|
1652
|
+
connection.records.save({ ...record, stoppedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1653
|
+
}
|
|
1654
|
+
function cleanup(connection) {
|
|
1655
|
+
for (const journal of pendingLaunches(connection.records))
|
|
1656
|
+
abortLaunch(connection, journal);
|
|
1657
|
+
const records2 = connection.records.all();
|
|
1658
|
+
if (connection.alive()) {
|
|
1659
|
+
const panes = connection.tmux(["list-panes", "-a", "-F", "#{@pmcp-owner}|#{@pmcp-session}"]).trim().split("\n");
|
|
1660
|
+
if (panes.some(
|
|
1661
|
+
(pane) => !records2.some((record) => pane === `${record.owner}|${record.id}`)
|
|
1662
|
+
))
|
|
1663
|
+
throw new Error("cleanup_foreign_pane");
|
|
1664
|
+
for (const record of records2) {
|
|
1665
|
+
if (!record.stoppedAt) connection.inspect(record.id);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
for (const record of records2) {
|
|
1669
|
+
if (record.resource) stopResource(record.resource);
|
|
1670
|
+
if (record.stoppedAt || !existsSync4(
|
|
1671
|
+
new LaunchJournal(connection.records, record.id).path("intent")
|
|
1672
|
+
)) {
|
|
1673
|
+
stopUnit(record.unit);
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
stop(connection, record.id);
|
|
1677
|
+
}
|
|
1678
|
+
if (connection.alive()) connection.tmux(["kill-server"]);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// src/harness/adapters/tmux/input.ts
|
|
1682
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1683
|
+
import { closeSync as closeSync4, openSync as openSync4, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
1684
|
+
import { join as join11 } from "path";
|
|
1685
|
+
function validateText(text3) {
|
|
1686
|
+
if (typeof text3 !== "string" || Buffer.byteLength(text3) > 16384)
|
|
1687
|
+
throw new Error("invalid_text");
|
|
1688
|
+
if (/[\x00-\x08\x0b-\x1f\x7f]/.test(text3))
|
|
1689
|
+
throw new Error("control_character_rejected");
|
|
1690
|
+
if (/[\r\n\t]/.test(text3))
|
|
1691
|
+
throw new Error("unqualified_terminal_control_input");
|
|
1692
|
+
}
|
|
1693
|
+
function guarded(connection, session, args) {
|
|
1694
|
+
const owner = `#{==:#{@pmcp-owner},${session.owner}}`;
|
|
1695
|
+
const identity = `#{==:#{@pmcp-session},${session.id}}`;
|
|
1696
|
+
const process2 = `#{==:#{pane_pid},${session.panePid}}`;
|
|
1697
|
+
const alive = "#{==:#{pane_dead},0}";
|
|
1698
|
+
const condition = `#{&&:${owner},#{&&:${identity},#{&&:${process2},${alive}}}}`;
|
|
1699
|
+
const output = connection.tmux([
|
|
1700
|
+
"if-shell",
|
|
1701
|
+
"-F",
|
|
1702
|
+
"-t",
|
|
1703
|
+
session.paneId,
|
|
1704
|
+
condition,
|
|
1705
|
+
args.map(quote).join(" "),
|
|
1706
|
+
"display-message -p delivery_uncertain"
|
|
1707
|
+
]);
|
|
1708
|
+
if (output.includes("delivery_uncertain"))
|
|
1709
|
+
throw new Error("delivery_uncertain");
|
|
1710
|
+
}
|
|
1711
|
+
function write(connection, id2, text3, submit) {
|
|
1712
|
+
validateText(text3);
|
|
1713
|
+
const session = connection.inspect(id2);
|
|
1714
|
+
if (!session.alive) throw new Error("runtime_exited");
|
|
1715
|
+
const lock = join11(connection.records.directory, `${id2}.input.lock`);
|
|
1716
|
+
const descriptor = openSync4(lock, "wx", 384);
|
|
1717
|
+
const buffer = `pmcp-${randomUUID4()}`;
|
|
1718
|
+
try {
|
|
1719
|
+
writeFileSync4(
|
|
1720
|
+
descriptor,
|
|
1721
|
+
JSON.stringify({ id: id2, controller: processIdentity(process.pid) })
|
|
1722
|
+
);
|
|
1723
|
+
if (text3.length > 0) {
|
|
1724
|
+
connection.tmux(["load-buffer", "-b", buffer, "-"], { input: text3 });
|
|
1725
|
+
connection.inspect(id2);
|
|
1726
|
+
guarded(connection, session, [
|
|
1727
|
+
"paste-buffer",
|
|
1728
|
+
"-d",
|
|
1729
|
+
"-p",
|
|
1730
|
+
"-r",
|
|
1731
|
+
"-b",
|
|
1732
|
+
buffer,
|
|
1733
|
+
"-t",
|
|
1734
|
+
session.paneId
|
|
1735
|
+
]);
|
|
1736
|
+
}
|
|
1737
|
+
if (submit) {
|
|
1738
|
+
connection.inspect(id2);
|
|
1739
|
+
guarded(connection, session, [
|
|
1740
|
+
"send-keys",
|
|
1741
|
+
"-t",
|
|
1742
|
+
session.paneId,
|
|
1743
|
+
"Enter"
|
|
1744
|
+
]);
|
|
1745
|
+
}
|
|
1746
|
+
return {
|
|
1747
|
+
id: id2,
|
|
1748
|
+
verdict: "input_written",
|
|
1749
|
+
submitted: submit,
|
|
1750
|
+
processingConfirmed: false
|
|
1751
|
+
};
|
|
1752
|
+
} finally {
|
|
1753
|
+
try {
|
|
1754
|
+
connection.tmux(["delete-buffer", "-b", buffer]);
|
|
1755
|
+
} catch {
|
|
1756
|
+
}
|
|
1757
|
+
closeSync4(descriptor);
|
|
1758
|
+
unlinkSync(lock);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
function interrupt(connection, id2) {
|
|
1762
|
+
const session = connection.inspect(id2);
|
|
1763
|
+
if (!session.alive) throw new Error("runtime_exited");
|
|
1764
|
+
guarded(connection, session, ["send-keys", "-t", session.paneId, "C-c"]);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/harness/adapters/tmux/read.ts
|
|
1768
|
+
function read(connection, id2, options = {}) {
|
|
1769
|
+
const lines = integer(options.lines ?? 100, "lines", 1, 2e3);
|
|
1770
|
+
const maxBytes = integer(options.maxBytes ?? 16384, "max_bytes", 1, 65536);
|
|
1771
|
+
const session = connection.inspect(id2);
|
|
1772
|
+
const flags = options.alternate === true ? ["-a"] : ["-S", `-${lines}`];
|
|
1773
|
+
const output = connection.tmux([
|
|
1774
|
+
"capture-pane",
|
|
1775
|
+
"-p",
|
|
1776
|
+
"-J",
|
|
1777
|
+
"-t",
|
|
1778
|
+
session.paneId,
|
|
1779
|
+
...flags
|
|
1780
|
+
]);
|
|
1781
|
+
const bytes = Buffer.from(output);
|
|
1782
|
+
const truncated = bytes.length > maxBytes;
|
|
1783
|
+
const content = truncated ? bytes.subarray(bytes.length - maxBytes).toString("utf8").replace(/^\uFFFD+/, "") : output;
|
|
1784
|
+
return { content, source: "terminal-screen", truncated };
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/harness/adapters/tmux/backend.ts
|
|
1788
|
+
var Backend = class {
|
|
1789
|
+
constructor(recordStore) {
|
|
1790
|
+
this.recordStore = recordStore;
|
|
1791
|
+
this.connection = new Connection(recordStore);
|
|
1792
|
+
this.owner = recordStore.owner;
|
|
1793
|
+
this.socket = recordStore.socket;
|
|
1794
|
+
this.runDirectory = recordStore.directory;
|
|
1795
|
+
this.metadataPath = recordStore.metadataPath;
|
|
1796
|
+
}
|
|
1797
|
+
recordStore;
|
|
1798
|
+
connection;
|
|
1799
|
+
owner;
|
|
1800
|
+
socket;
|
|
1801
|
+
runDirectory;
|
|
1802
|
+
metadataPath;
|
|
1803
|
+
records() {
|
|
1804
|
+
return this.recordStore.all();
|
|
1805
|
+
}
|
|
1806
|
+
start(spec) {
|
|
1807
|
+
return start(this.connection, spec);
|
|
1808
|
+
}
|
|
1809
|
+
inspect(id2) {
|
|
1810
|
+
return this.connection.inspect(id2);
|
|
1811
|
+
}
|
|
1812
|
+
read(id2, options) {
|
|
1813
|
+
return read(this.connection, id2, options);
|
|
1814
|
+
}
|
|
1815
|
+
validateText(text3) {
|
|
1816
|
+
validateText(text3);
|
|
1817
|
+
}
|
|
1818
|
+
write(id2, text3, options = {}) {
|
|
1819
|
+
return write(this.connection, id2, text3, options.submit === true);
|
|
1820
|
+
}
|
|
1821
|
+
interrupt(id2) {
|
|
1822
|
+
interrupt(this.connection, id2);
|
|
1823
|
+
}
|
|
1824
|
+
stop(id2) {
|
|
1825
|
+
stop(this.connection, id2);
|
|
1826
|
+
}
|
|
1827
|
+
cleanup() {
|
|
1828
|
+
cleanup(this.connection);
|
|
1829
|
+
}
|
|
1830
|
+
tmux(args) {
|
|
1831
|
+
return this.connection.tmux(args);
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
function openBackend(directory) {
|
|
1835
|
+
return new Backend(new RecordStore(directory));
|
|
1836
|
+
}
|
|
1837
|
+
function assertOwnedRecords(backend) {
|
|
1838
|
+
backend.records();
|
|
1839
|
+
}
|
|
1840
|
+
function writableClients(backend, sessionId) {
|
|
1841
|
+
const clients = backend.tmux(["list-clients", "-F", "#{session_id}|#{client_readonly}"]).trim();
|
|
1842
|
+
if (!clients) return 0;
|
|
1843
|
+
return clients.split("\n").filter((line) => line === `${sessionId}|0`).length;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// src/harness/control/leases.ts
|
|
1847
|
+
function resolveLease(context, file, allowExpired = false) {
|
|
1848
|
+
const credential = object(readPrivateJson(file));
|
|
1849
|
+
const lease = context.store.state.leases.find(
|
|
1850
|
+
(entry) => entry.token === credential.token
|
|
1851
|
+
);
|
|
1852
|
+
if (!lease || lease.generation !== context.store.state.group.generation)
|
|
1853
|
+
throw new Error("stale_control");
|
|
1854
|
+
if (!Number.isFinite(Date.parse(lease.expiresAt)))
|
|
1855
|
+
throw new Error("invalid_control_expiry");
|
|
1856
|
+
if (!allowExpired && Date.parse(lease.expiresAt) <= Date.now())
|
|
1857
|
+
throw new Error("expired_control");
|
|
1858
|
+
context.store.session(lease.sessionId);
|
|
1859
|
+
context.backend.inspect(lease.sessionId);
|
|
1860
|
+
return lease;
|
|
1861
|
+
}
|
|
1862
|
+
function acquireControl(context, sessionId, controller, mode) {
|
|
1863
|
+
const { store, backend } = context;
|
|
1864
|
+
if (store.state.group.state === "stopped") throw new Error("group_stopped");
|
|
1865
|
+
store.session(sessionId);
|
|
1866
|
+
const observation = backend.inspect(sessionId);
|
|
1867
|
+
if (!observation.alive) throw new Error("runtime_exited");
|
|
1868
|
+
const current = store.state.leases.find(
|
|
1869
|
+
(entry) => entry.sessionId === sessionId
|
|
1870
|
+
);
|
|
1871
|
+
if (current && Date.parse(current.expiresAt) > Date.now())
|
|
1872
|
+
throw new Error("control_busy");
|
|
1873
|
+
if (writableClients(backend, observation.tmuxSessionId) > 0)
|
|
1874
|
+
throw new Error("native_writer_active");
|
|
1875
|
+
if (mode !== "cli" && mode !== "native")
|
|
1876
|
+
throw new Error("invalid_control_mode");
|
|
1877
|
+
const lease = {
|
|
1878
|
+
token: randomUUID5(),
|
|
1879
|
+
sessionId,
|
|
1880
|
+
generation: store.state.group.generation,
|
|
1881
|
+
controller: id(controller, "controller"),
|
|
1882
|
+
mode,
|
|
1883
|
+
expiresAt: new Date(Date.now() + 6e4).toISOString()
|
|
1884
|
+
};
|
|
1885
|
+
store.state.leases = store.state.leases.filter(
|
|
1886
|
+
(entry) => entry.sessionId !== sessionId
|
|
1887
|
+
);
|
|
1888
|
+
store.state.leases.push(lease);
|
|
1889
|
+
store.save();
|
|
1890
|
+
const leaseFile = join12(backend.runDirectory, `lease-${randomUUID5()}.json`);
|
|
1891
|
+
privateJson(leaseFile, { token: lease.token });
|
|
1892
|
+
return { sessionId, controller, mode, expiresAt: lease.expiresAt, leaseFile };
|
|
1893
|
+
}
|
|
1894
|
+
function controlOperation(context, action, leaseFile) {
|
|
1895
|
+
const lease = resolveLease(context, leaseFile, action === "release");
|
|
1896
|
+
if (action === "renew") {
|
|
1897
|
+
lease.expiresAt = new Date(Date.now() + 6e4).toISOString();
|
|
1898
|
+
context.store.save();
|
|
1899
|
+
return { sessionId: lease.sessionId, expiresAt: lease.expiresAt };
|
|
1900
|
+
}
|
|
1901
|
+
if (action !== "release") throw new Error("unsupported_control_operation");
|
|
1902
|
+
const session = context.backend.inspect(lease.sessionId);
|
|
1903
|
+
if (writableClients(context.backend, session.tmuxSessionId) > 0)
|
|
1904
|
+
throw new Error("native_writer_active");
|
|
1905
|
+
context.store.state.leases = context.store.state.leases.filter(
|
|
1906
|
+
(entry) => entry.token !== lease.token
|
|
1907
|
+
);
|
|
1908
|
+
context.store.save();
|
|
1909
|
+
return { sessionId: lease.sessionId, released: true, inputReplayed: false };
|
|
1910
|
+
}
|
|
1911
|
+
function attachment(context, sessionId, mode, leaseFile) {
|
|
1912
|
+
context.store.session(sessionId);
|
|
1913
|
+
const session = context.backend.inspect(sessionId);
|
|
1914
|
+
if (!session.alive) throw new Error("runtime_exited");
|
|
1915
|
+
if (mode === "observe")
|
|
1916
|
+
return {
|
|
1917
|
+
socket: context.backend.socket,
|
|
1918
|
+
session: session.tmuxSessionId,
|
|
1919
|
+
readOnly: true
|
|
1920
|
+
};
|
|
1921
|
+
if (mode !== "write") throw new Error("invalid_attach_mode");
|
|
1922
|
+
const lease = resolveLease(context, text(leaseFile, "lease_file"));
|
|
1923
|
+
if (lease.sessionId !== sessionId || lease.mode !== "native")
|
|
1924
|
+
throw new Error("native_control_required");
|
|
1925
|
+
if (writableClients(context.backend, session.tmuxSessionId) > 0)
|
|
1926
|
+
throw new Error("native_writer_active");
|
|
1927
|
+
return {
|
|
1928
|
+
socket: context.backend.socket,
|
|
1929
|
+
session: session.tmuxSessionId,
|
|
1930
|
+
readOnly: false
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// src/harness/delivery/service.ts
|
|
1935
|
+
import { createHash as createHash4 } from "crypto";
|
|
1936
|
+
function deliver(context, operation, leaseFile, requestId, text3) {
|
|
1937
|
+
const { store, backend } = context;
|
|
1938
|
+
const lease = resolveLease(context, leaseFile);
|
|
1939
|
+
if (lease.mode !== "cli") throw new Error("native_control_held");
|
|
1940
|
+
const session = backend.inspect(lease.sessionId);
|
|
1941
|
+
if (writableClients(backend, session.tmuxSessionId) > 0)
|
|
1942
|
+
throw new Error("native_writer_active");
|
|
1943
|
+
if (operation !== "write" && operation !== "submit")
|
|
1944
|
+
throw new Error("unsupported_input_operation");
|
|
1945
|
+
if (operation === "submit" && text3 !== "")
|
|
1946
|
+
throw new Error("submit_does_not_accept_text");
|
|
1947
|
+
backend.validateText(text3);
|
|
1948
|
+
if (/[\r\n\t]/.test(text3))
|
|
1949
|
+
throw new Error("unqualified_terminal_control_input");
|
|
1950
|
+
id(requestId, "request_id");
|
|
1951
|
+
const digest2 = createHash4("sha256").update(
|
|
1952
|
+
JSON.stringify({
|
|
1953
|
+
operation,
|
|
1954
|
+
text: text3,
|
|
1955
|
+
sessionId: lease.sessionId,
|
|
1956
|
+
generation: lease.generation
|
|
1957
|
+
})
|
|
1958
|
+
).digest("hex");
|
|
1959
|
+
const previous = store.state.deliveries.find(
|
|
1960
|
+
(entry) => entry.requestId === requestId
|
|
1961
|
+
);
|
|
1962
|
+
if (previous) {
|
|
1963
|
+
if (previous.digest !== digest2) throw new Error("request_conflict");
|
|
1964
|
+
return {
|
|
1965
|
+
...previous,
|
|
1966
|
+
ok: previous.state === "input_written",
|
|
1967
|
+
duplicate: true,
|
|
1968
|
+
resendAllowed: false,
|
|
1969
|
+
processingConfirmed: false
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
if (store.state.deliveries.some(
|
|
1973
|
+
(entry) => entry.sessionId === lease.sessionId && entry.state !== "input_written"
|
|
1974
|
+
)) {
|
|
1975
|
+
throw new Error("session_delivery_uncertain");
|
|
1976
|
+
}
|
|
1977
|
+
const delivery = {
|
|
1978
|
+
controller: lease.controller,
|
|
1979
|
+
requestId,
|
|
1980
|
+
sessionId: lease.sessionId,
|
|
1981
|
+
generation: lease.generation,
|
|
1982
|
+
digest: digest2,
|
|
1983
|
+
operation,
|
|
1984
|
+
state: "intent"
|
|
1985
|
+
};
|
|
1986
|
+
store.state.deliveries.push(delivery);
|
|
1987
|
+
store.save();
|
|
1988
|
+
try {
|
|
1989
|
+
const receipt = backend.write(lease.sessionId, text3, {
|
|
1990
|
+
submit: operation === "submit"
|
|
1991
|
+
});
|
|
1992
|
+
delivery.state = "input_written";
|
|
1993
|
+
store.save();
|
|
1994
|
+
return {
|
|
1995
|
+
...receipt,
|
|
1996
|
+
requestId,
|
|
1997
|
+
generation: lease.generation,
|
|
1998
|
+
resendAllowed: false
|
|
1999
|
+
};
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
delivery.state = "delivery_uncertain";
|
|
2002
|
+
store.save();
|
|
2003
|
+
throw error;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
function interrupt2(context, leaseFile) {
|
|
2007
|
+
const lease = resolveLease(context, leaseFile);
|
|
2008
|
+
if (lease.mode !== "cli") throw new Error("native_control_held");
|
|
2009
|
+
const session = context.backend.inspect(lease.sessionId);
|
|
2010
|
+
if (writableClients(context.backend, session.tmuxSessionId) > 0)
|
|
2011
|
+
throw new Error("native_writer_active");
|
|
2012
|
+
context.backend.interrupt(lease.sessionId);
|
|
2013
|
+
return {
|
|
2014
|
+
sessionId: lease.sessionId,
|
|
2015
|
+
signal: "terminal-interrupt",
|
|
2016
|
+
processingConfirmed: false
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// src/harness/groups/service.ts
|
|
2021
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2022
|
+
import { realpathSync as realpathSync7, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
2023
|
+
import { join as join15 } from "path";
|
|
2024
|
+
|
|
2025
|
+
// src/harness/adapters/systemd/budget.ts
|
|
2026
|
+
import { join as join13 } from "path";
|
|
2027
|
+
async function budgetOperation(directory, operation) {
|
|
2028
|
+
const backend = openBackend(directory);
|
|
2029
|
+
const path = join13(directory, "runtime-policy.json");
|
|
2030
|
+
const metadata = object(readPrivateJson(backend.metadataPath));
|
|
2031
|
+
const slice = budgetSlice(backend.owner);
|
|
2032
|
+
if (operation.action === "configure") {
|
|
2033
|
+
if (metadata.budgetSlice || backend.records().length)
|
|
2034
|
+
throw new Error("budget_already_initialized");
|
|
2035
|
+
const memoryMb2 = integer(operation.memoryMb, "memory_budget", 128, 4096);
|
|
2036
|
+
const maxActive = integer(operation.maxActive, "max_active", 1, 5);
|
|
2037
|
+
privateJson(backend.metadataPath, { ...metadata, budgetSlice: slice });
|
|
2038
|
+
privateJson(path, { owner: backend.owner, slice, memoryMb: memoryMb2, maxActive });
|
|
2039
|
+
command("systemctl", ["--user", "start", slice]);
|
|
2040
|
+
command("systemctl", [
|
|
2041
|
+
"--user",
|
|
2042
|
+
"set-property",
|
|
2043
|
+
"--runtime",
|
|
2044
|
+
slice,
|
|
2045
|
+
`MemoryMax=${memoryMb2}M`,
|
|
2046
|
+
`MemoryHigh=${Math.floor(memoryMb2 * 0.8)}M`,
|
|
2047
|
+
"TasksMax=256",
|
|
2048
|
+
"CPUQuota=100%"
|
|
2049
|
+
]);
|
|
2050
|
+
return { ok: true };
|
|
2051
|
+
}
|
|
2052
|
+
const policy = object(readPrivateJson(path));
|
|
2053
|
+
if (policy.owner !== backend.owner || metadata.budgetSlice !== slice || policy.slice !== slice)
|
|
2054
|
+
throw new Error("foreign_budget_policy");
|
|
2055
|
+
if (operation.action === "cleanup") {
|
|
2056
|
+
backend.cleanup();
|
|
2057
|
+
command("systemctl", ["--user", "stop", slice]);
|
|
2058
|
+
command("systemctl", ["--user", "revert", slice]);
|
|
2059
|
+
return { ok: true, budgetStopped: true };
|
|
2060
|
+
}
|
|
2061
|
+
if (operation.action === "stop") {
|
|
2062
|
+
backend.stop(text(operation.id, "session_id"));
|
|
2063
|
+
return { ok: true, stopped: true };
|
|
2064
|
+
}
|
|
2065
|
+
if (operation.action !== "start") throw new Error("invalid_budget_operation");
|
|
2066
|
+
const active = backend.records().filter((record) => {
|
|
2067
|
+
if (record.resource && resourceRunning(record.resource) && !backend.inspect(record.id).alive)
|
|
2068
|
+
throw new Error("container_running_without_proxy");
|
|
2069
|
+
if (record.stoppedAt) {
|
|
2070
|
+
if (unitActive(record.unit)) throw new Error("runtime_stop_unconfirmed");
|
|
2071
|
+
return false;
|
|
2072
|
+
}
|
|
2073
|
+
const observation = backend.inspect(record.id);
|
|
2074
|
+
if (!observation.alive && unitActive(record.unit))
|
|
2075
|
+
throw new Error("runtime_lifecycle_uncertain");
|
|
2076
|
+
return observation.alive;
|
|
2077
|
+
});
|
|
2078
|
+
if (active.length >= integer(policy.maxActive, "max_active", 1, 5))
|
|
2079
|
+
throw new Error("runtime_capacity_exceeded");
|
|
2080
|
+
const spec = object(operation.spec);
|
|
2081
|
+
const memoryMb = integer(spec.memoryMb, "session_memory", 32, 1536);
|
|
2082
|
+
const resource = spec.resource === void 0 ? void 0 : resourceClaim(spec.resource, backend.owner);
|
|
2083
|
+
const dockerPreparation = spec.dockerPreparation;
|
|
2084
|
+
const cpuQuota = resource?.cpuQuota ?? dockerPreparation?.cpuQuota;
|
|
2085
|
+
if (cpuQuota !== void 0) {
|
|
2086
|
+
const reservedCpu = backend.records().filter((record) => !record.stoppedAt).reduce((total, record) => total + (record.resource?.cpuQuota ?? 0), 0);
|
|
2087
|
+
if (reservedCpu + cpuQuota > 5e4)
|
|
2088
|
+
throw new Error("aggregate_container_cpu_budget_exceeded");
|
|
2089
|
+
}
|
|
2090
|
+
if (active.reduce((total, session2) => total + session2.memoryMb, 0) + memoryMb > integer(policy.memoryMb, "memory_budget", 128, 4096))
|
|
2091
|
+
throw new Error("aggregate_memory_budget_exceeded");
|
|
2092
|
+
if (resource || dockerPreparation)
|
|
2093
|
+
command("systemctl", [
|
|
2094
|
+
"--user",
|
|
2095
|
+
"set-property",
|
|
2096
|
+
"--runtime",
|
|
2097
|
+
slice,
|
|
2098
|
+
"CPUQuota=50%"
|
|
2099
|
+
]);
|
|
2100
|
+
const session = await backend.start({
|
|
2101
|
+
runtime: text(spec.runtime, "runtime"),
|
|
2102
|
+
argv: spec.argv,
|
|
2103
|
+
cwd: text(spec.cwd, "cwd"),
|
|
2104
|
+
memoryMb,
|
|
2105
|
+
budgetSlice: slice,
|
|
2106
|
+
groupId: text(spec.groupId, "group_id"),
|
|
2107
|
+
generation: text(spec.generation, "generation"),
|
|
2108
|
+
role: spec.role,
|
|
2109
|
+
...resource ? { resource } : {},
|
|
2110
|
+
...dockerPreparation ? { dockerPreparation } : {}
|
|
2111
|
+
});
|
|
2112
|
+
return { ok: true, session };
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// src/harness/adapters/systemd/configure.ts
|
|
2116
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2117
|
+
import { join as join14 } from "path";
|
|
2118
|
+
function configureBudget(context, input) {
|
|
2119
|
+
const { store, backend } = context;
|
|
2120
|
+
assertOwnedRecords(backend);
|
|
2121
|
+
const group = store.state.group;
|
|
2122
|
+
const memoryMb = integer(
|
|
2123
|
+
input.memoryMb ?? group.memoryMb,
|
|
2124
|
+
"memory_budget",
|
|
2125
|
+
128,
|
|
2126
|
+
4096
|
|
2127
|
+
);
|
|
2128
|
+
const maxActive = integer(
|
|
2129
|
+
input.maxActive ?? group.maxActive,
|
|
2130
|
+
"max_active",
|
|
2131
|
+
1,
|
|
2132
|
+
5
|
|
2133
|
+
);
|
|
2134
|
+
const active = backend.records().filter((record) => !record.stoppedAt && backend.inspect(record.id).alive);
|
|
2135
|
+
if (active.length > maxActive)
|
|
2136
|
+
throw new Error("capacity_below_active_sessions");
|
|
2137
|
+
if (active.reduce((total, session) => total + session.memoryMb, 0) > memoryMb)
|
|
2138
|
+
throw new Error("budget_below_reserved_memory");
|
|
2139
|
+
const path = join14(backend.runDirectory, "runtime-policy.json");
|
|
2140
|
+
const policy = object(readPrivateJson(path));
|
|
2141
|
+
const metadata = object(readPrivateJson(backend.metadataPath));
|
|
2142
|
+
const slice = budgetSlice(backend.owner);
|
|
2143
|
+
if (policy.owner !== backend.owner || policy.slice !== slice || metadata.budgetSlice !== slice)
|
|
2144
|
+
throw new Error("foreign_budget_policy");
|
|
2145
|
+
execFileSync2(
|
|
2146
|
+
"systemctl",
|
|
2147
|
+
[
|
|
2148
|
+
"--user",
|
|
2149
|
+
"set-property",
|
|
2150
|
+
"--runtime",
|
|
2151
|
+
slice,
|
|
2152
|
+
`MemoryMax=${memoryMb}M`,
|
|
2153
|
+
`MemoryHigh=${Math.floor(memoryMb * 0.8)}M`
|
|
2154
|
+
],
|
|
2155
|
+
{ timeout: 5e3 }
|
|
2156
|
+
);
|
|
2157
|
+
privateJson(path, {
|
|
2158
|
+
...policy,
|
|
2159
|
+
memoryMb,
|
|
2160
|
+
maxActive,
|
|
2161
|
+
memoryHighMb: Math.floor(memoryMb * 0.8)
|
|
2162
|
+
});
|
|
2163
|
+
return { memoryMb, maxActive };
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/harness/adapters/process/executable.ts
|
|
2167
|
+
import { accessSync, constants as constants4, statSync } from "fs";
|
|
2168
|
+
import { delimiter, resolve } from "path";
|
|
2169
|
+
function locateExecutable(binary, cwd = process.cwd()) {
|
|
2170
|
+
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
2171
|
+
if (!directory) continue;
|
|
2172
|
+
const path = resolve(cwd, directory, binary);
|
|
2173
|
+
try {
|
|
2174
|
+
if (!statSync(path).isFile()) continue;
|
|
2175
|
+
accessSync(path, constants4.X_OK);
|
|
2176
|
+
return path;
|
|
2177
|
+
} catch {
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/harness/groups/service.ts
|
|
2185
|
+
async function groupOperation(context, action, input = {}) {
|
|
2186
|
+
const { store, backend } = context;
|
|
2187
|
+
if (action === "inspect") return { group: store.state.group };
|
|
2188
|
+
if (action === "configure") {
|
|
2189
|
+
if (store.state.group.state === "stopped") throw new Error("group_stopped");
|
|
2190
|
+
if (input.profile !== void 0 || input.dockerPolicyFile !== void 0)
|
|
2191
|
+
throw new Error("profile_change_requires_new_group");
|
|
2192
|
+
const allowedRuntimes = input.allowedRuntimes === void 0 ? store.state.group.allowedRuntimes : strings(input.allowedRuntimes, "allowed_runtimes");
|
|
2193
|
+
if (allowedRuntimes.some(
|
|
2194
|
+
(runtime) => !store.state.group.allowedRuntimes.includes(runtime)
|
|
2195
|
+
))
|
|
2196
|
+
throw new Error("runtime_grant_expansion_requires_new_group");
|
|
2197
|
+
Object.assign(store.state.group, configureBudget(context, input), {
|
|
2198
|
+
allowedRuntimes
|
|
2199
|
+
});
|
|
2200
|
+
store.save();
|
|
2201
|
+
return { group: store.state.group, nativePermissionsChanged: false };
|
|
2202
|
+
}
|
|
2203
|
+
if (action === "pause" || action === "resume") {
|
|
2204
|
+
if (store.state.group.state === "stopped") throw new Error("group_stopped");
|
|
2205
|
+
store.state.group.state = action === "pause" ? "paused" : "ready";
|
|
2206
|
+
store.save();
|
|
2207
|
+
return { group: store.state.group, runtimesInterrupted: false };
|
|
2208
|
+
}
|
|
2209
|
+
if (action !== "stop") throw new Error("unsupported_group_operation");
|
|
2210
|
+
if (store.state.group.state !== "stopped") {
|
|
2211
|
+
assertOwnedRecords(backend);
|
|
2212
|
+
await budgetOperation(backend.runDirectory, { action: "cleanup" });
|
|
2213
|
+
store.state.group.state = "stopped";
|
|
2214
|
+
store.state.leases = [];
|
|
2215
|
+
store.state.group.sessions.forEach((session) => {
|
|
2216
|
+
session.stopped = true;
|
|
2217
|
+
});
|
|
2218
|
+
store.save();
|
|
2219
|
+
}
|
|
2220
|
+
return { group: store.state.group, inputReplayed: false };
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// src/harness/observation/options.ts
|
|
2224
|
+
function readOptions(input) {
|
|
2225
|
+
if (input === void 0) return {};
|
|
2226
|
+
const allowed = ["lines", "maxBytes", "alternate"];
|
|
2227
|
+
if (Object.keys(input).some((key) => !allowed.includes(key)))
|
|
2228
|
+
throw new Error("invalid_read_option");
|
|
2229
|
+
if (input.alternate !== void 0 && typeof input.alternate !== "boolean")
|
|
2230
|
+
throw new Error("invalid_alternate");
|
|
2231
|
+
return {
|
|
2232
|
+
...input.lines === void 0 ? {} : { lines: integer(input.lines, "lines", 1, 2e3) },
|
|
2233
|
+
...input.maxBytes === void 0 ? {} : { maxBytes: integer(input.maxBytes, "max_bytes", 1, 65536) },
|
|
2234
|
+
...input.alternate === void 0 ? {} : { alternate: input.alternate }
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// src/harness/observation/service.ts
|
|
2239
|
+
function listSessions(context) {
|
|
2240
|
+
const { store, backend } = context;
|
|
2241
|
+
const sessions = store.state.group.sessions.map((reference) => {
|
|
2242
|
+
if (reference.stopped) return { ...reference, observed: "stopped" };
|
|
2243
|
+
try {
|
|
2244
|
+
return {
|
|
2245
|
+
...backend.inspect(reference.id),
|
|
2246
|
+
role: reference.role,
|
|
2247
|
+
observed: "terminal-process"
|
|
2248
|
+
};
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
return {
|
|
2251
|
+
...reference,
|
|
2252
|
+
observed: "unavailable",
|
|
2253
|
+
reason: error instanceof Error ? error.message : "observation_failed"
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
return {
|
|
2258
|
+
generation: store.state.group.generation,
|
|
2259
|
+
sessions,
|
|
2260
|
+
providerStateInferred: false
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
function observeSession(context, action, sessionId, input) {
|
|
2264
|
+
context.store.session(sessionId);
|
|
2265
|
+
if (action === "inspect") {
|
|
2266
|
+
return {
|
|
2267
|
+
session: context.backend.inspect(sessionId),
|
|
2268
|
+
providerStateInferred: false
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
if (action !== "read") throw new Error("unsupported_observation_operation");
|
|
2272
|
+
return {
|
|
2273
|
+
sessionId,
|
|
2274
|
+
...context.backend.read(sessionId, readOptions(input)),
|
|
2275
|
+
providerStateInferred: false
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
function inspectDeliveries(context, requestId) {
|
|
2279
|
+
const deliveries = context.store.state.deliveries.filter(
|
|
2280
|
+
(entry) => !requestId || entry.requestId === requestId
|
|
2281
|
+
);
|
|
2282
|
+
if (requestId && deliveries.length === 0)
|
|
2283
|
+
throw new Error("request_not_found");
|
|
2284
|
+
return {
|
|
2285
|
+
generation: context.store.state.group.generation,
|
|
2286
|
+
deliveries,
|
|
2287
|
+
processingConfirmed: false
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// src/harness/recovery/service.ts
|
|
2292
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
2293
|
+
|
|
2294
|
+
// src/harness/adapters/process/recover-lock.ts
|
|
2295
|
+
import { existsSync as existsSync5, lstatSync as lstatSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
2296
|
+
import { join as join16 } from "path";
|
|
2297
|
+
function recoverInputLock(backend, sessionId) {
|
|
2298
|
+
backend.inspect(sessionId);
|
|
2299
|
+
const path = join16(backend.runDirectory, `${sessionId}.input.lock`);
|
|
2300
|
+
if (!existsSync5(path)) return;
|
|
2301
|
+
const before = lstatSync6(path);
|
|
2302
|
+
const record = object(readPrivateJson(path));
|
|
2303
|
+
if (record.id !== sessionId) throw new Error("foreign_writer_lock");
|
|
2304
|
+
if (!controllerExited(text(record.controller, "controller_identity")))
|
|
2305
|
+
throw new Error("writer_still_alive");
|
|
2306
|
+
if (lstatSync6(path).ino !== before.ino)
|
|
2307
|
+
throw new Error("writer_lock_changed");
|
|
2308
|
+
unlinkSync3(path);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// src/harness/sessions/service.ts
|
|
2312
|
+
import { realpathSync as realpathSync8 } from "fs";
|
|
2313
|
+
import { relative as relative5 } from "path";
|
|
2314
|
+
|
|
2315
|
+
// src/harness/launch/service.ts
|
|
2316
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2317
|
+
function assertLaunchAdmission(records2) {
|
|
2318
|
+
for (const journal of launchJournals(records2)) {
|
|
2319
|
+
if (journal.abortRecord()?.state === "stopped" && unitActive(journal.intent().unit))
|
|
2320
|
+
throw new Error("runtime_stop_unconfirmed");
|
|
2321
|
+
}
|
|
2322
|
+
if (pendingLaunches(records2).length)
|
|
2323
|
+
throw new Error("launch_requires_reconciliation");
|
|
2324
|
+
}
|
|
2325
|
+
function authorizeSession(context, sessionId) {
|
|
2326
|
+
const journal = new LaunchJournal(context.backend.recordStore, sessionId);
|
|
2327
|
+
if (journal.intent().controllerIdentity !== processIdentity(process.pid))
|
|
2328
|
+
throw new Error("foreign_launch_controller");
|
|
2329
|
+
assertProof(new Connection(context.backend.recordStore), journal.proof());
|
|
2330
|
+
const resource = journal.intent().resource;
|
|
2331
|
+
if (resource) assertResourceFresh(resource);
|
|
2332
|
+
journal.authorize();
|
|
2333
|
+
journal.commit();
|
|
2334
|
+
}
|
|
2335
|
+
function inspectPendingStarts(context) {
|
|
2336
|
+
return pendingLaunches(context.backend.recordStore).map((journal) => ({
|
|
2337
|
+
sessionId: journal.id,
|
|
2338
|
+
role: journal.intent().role,
|
|
2339
|
+
runtime: journal.intent().runtime,
|
|
2340
|
+
controllerExited: controllerExited(journal.intent().controllerIdentity),
|
|
2341
|
+
proofPresent: existsSync6(journal.path("proof")),
|
|
2342
|
+
recordPresent: existsSync6(journal.records.path(journal.id)),
|
|
2343
|
+
registered: context.store.state.group.sessions.some(
|
|
2344
|
+
(session) => session.id === journal.id
|
|
2345
|
+
),
|
|
2346
|
+
authorizationPresent: existsSync6(journal.path("grant")),
|
|
2347
|
+
processingConfirmed: false,
|
|
2348
|
+
inputReplayed: false
|
|
2349
|
+
}));
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// src/harness/sessions/service.ts
|
|
2353
|
+
async function startSession(context, input) {
|
|
2354
|
+
const { store, backend } = context;
|
|
2355
|
+
const group = store.state.group;
|
|
2356
|
+
assertOwnedRecords(backend);
|
|
2357
|
+
assertLaunchAdmission(backend.recordStore);
|
|
2358
|
+
if (backend.records().some(
|
|
2359
|
+
(record) => !record.stoppedAt && !group.sessions.some((session2) => session2.id === record.id)
|
|
2360
|
+
)) {
|
|
2361
|
+
throw new Error("unregistered_session_requires_recovery");
|
|
2362
|
+
}
|
|
2363
|
+
if (group.state !== "ready") throw new Error("group_not_ready");
|
|
2364
|
+
const runtime = text(input.runtime, "runtime");
|
|
2365
|
+
if (!group.allowedRuntimes.includes(runtime))
|
|
2366
|
+
throw new Error("runtime_not_granted");
|
|
2367
|
+
const role = input.role;
|
|
2368
|
+
if (role !== "main" && role !== "worker") throw new Error("invalid_role");
|
|
2369
|
+
if (role === "main" && group.sessions.some(
|
|
2370
|
+
(session2) => session2.role === "main" && !session2.stopped
|
|
2371
|
+
)) {
|
|
2372
|
+
throw new Error("main_already_assigned");
|
|
2373
|
+
}
|
|
2374
|
+
if (role === "worker" && !group.sessions.some(
|
|
2375
|
+
(session2) => session2.role === "main" && !session2.stopped
|
|
2376
|
+
)) {
|
|
2377
|
+
throw new Error("main_required");
|
|
2378
|
+
}
|
|
2379
|
+
if (role === "worker") {
|
|
2380
|
+
const main = group.sessions.find(
|
|
2381
|
+
(session2) => session2.role === "main" && !session2.stopped
|
|
2382
|
+
);
|
|
2383
|
+
if (!backend.inspect(main.id).alive) throw new Error("main_unavailable");
|
|
2384
|
+
}
|
|
2385
|
+
const cwd = realpathSync8(text(input.cwd ?? group.projectRoot, "cwd"));
|
|
2386
|
+
const scope = relative5(group.projectRoot, cwd);
|
|
2387
|
+
if (scope === ".." || scope.startsWith("../"))
|
|
2388
|
+
throw new Error("cwd_outside_project");
|
|
2389
|
+
const memoryMb = integer(input.memoryMb ?? 384, "session_memory", 32, 1536);
|
|
2390
|
+
const executable = runtimeExecutables[runtime];
|
|
2391
|
+
if (!executable) throw new Error("unsupported_runtime");
|
|
2392
|
+
const args = input.args === void 0 || Array.isArray(input.args) && input.args.length === 0 ? [] : strings(input.args, "runtime_args");
|
|
2393
|
+
if (group.profile === "native" && input.containerFile !== void 0)
|
|
2394
|
+
throw new Error("container_requires_docker_profile");
|
|
2395
|
+
if (group.dockerPolicy?.egress && input.containerFile !== void 0)
|
|
2396
|
+
throw new Error("egress_requires_cli_owned_creation");
|
|
2397
|
+
const privateProfile = input.privateProfile === void 0 ? void 0 : profileName(input.privateProfile);
|
|
2398
|
+
if (privateProfile && (group.profile !== "docker" || input.containerFile !== void 0))
|
|
2399
|
+
throw new Error("private_profile_requires_cli_owned_creation");
|
|
2400
|
+
if (privateProfile) {
|
|
2401
|
+
const profile = selectedProfile(
|
|
2402
|
+
group.dockerPolicy,
|
|
2403
|
+
privateProfile,
|
|
2404
|
+
runtime
|
|
2405
|
+
);
|
|
2406
|
+
assertProfileAvailable(profile, group.projectRoot);
|
|
2407
|
+
}
|
|
2408
|
+
const resource = input.containerFile === void 0 ? void 0 : admitResource(
|
|
2409
|
+
input.containerFile,
|
|
2410
|
+
group,
|
|
2411
|
+
role,
|
|
2412
|
+
backend.runDirectory,
|
|
2413
|
+
memoryMb,
|
|
2414
|
+
runtime
|
|
2415
|
+
);
|
|
2416
|
+
if (resource && args.length)
|
|
2417
|
+
throw new Error("container_args_are_fixed_at_creation");
|
|
2418
|
+
if (resource && backend.records().some((record) => record.resource?.id === resource.id))
|
|
2419
|
+
throw new Error("container_already_bound");
|
|
2420
|
+
const dockerPreparation = group.profile === "docker" && !resource ? {
|
|
2421
|
+
args,
|
|
2422
|
+
memoryMb: integer(memoryMb - 64, "container_memory", 32, 1472),
|
|
2423
|
+
cpuQuota: integer(
|
|
2424
|
+
input.containerCpuQuota ?? Math.floor(5e4 / group.maxActive),
|
|
2425
|
+
"container_cpu",
|
|
2426
|
+
1,
|
|
2427
|
+
5e4
|
|
2428
|
+
),
|
|
2429
|
+
...privateProfile ? { privateProfile } : {}
|
|
2430
|
+
} : void 0;
|
|
2431
|
+
const executablePath = locateExecutable(
|
|
2432
|
+
resource || dockerPreparation ? "docker" : executable,
|
|
2433
|
+
cwd
|
|
2434
|
+
);
|
|
2435
|
+
if (!executablePath) throw new Error("runtime_executable_unavailable");
|
|
2436
|
+
const argv = resource ? [executablePath, "start", "--attach", "--interactive", resource.id] : [executablePath, ...args];
|
|
2437
|
+
const result = await budgetOperation(backend.runDirectory, {
|
|
2438
|
+
action: "start",
|
|
2439
|
+
spec: {
|
|
2440
|
+
runtime,
|
|
2441
|
+
argv,
|
|
2442
|
+
cwd,
|
|
2443
|
+
memoryMb,
|
|
2444
|
+
groupId: group.id,
|
|
2445
|
+
generation: group.generation,
|
|
2446
|
+
role,
|
|
2447
|
+
...resource ? { resource } : {},
|
|
2448
|
+
...dockerPreparation ? { dockerPreparation } : {}
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
const session = result.session;
|
|
2452
|
+
group.sessions.push({ id: session.id, role, stopped: false });
|
|
2453
|
+
store.save();
|
|
2454
|
+
authorizeSession(context, session.id);
|
|
2455
|
+
return { session, role, generation: group.generation };
|
|
2456
|
+
}
|
|
2457
|
+
async function stopSession(context, sessionId) {
|
|
2458
|
+
const { store, backend } = context;
|
|
2459
|
+
store.session(sessionId);
|
|
2460
|
+
assertOwnedRecords(backend);
|
|
2461
|
+
const reference = store.state.group.sessions.find(
|
|
2462
|
+
(session) => session.id === sessionId
|
|
2463
|
+
);
|
|
2464
|
+
if (!reference.stopped) {
|
|
2465
|
+
await budgetOperation(backend.runDirectory, {
|
|
2466
|
+
action: "stop",
|
|
2467
|
+
id: sessionId
|
|
2468
|
+
});
|
|
2469
|
+
reference.stopped = true;
|
|
2470
|
+
store.state.leases = store.state.leases.filter(
|
|
2471
|
+
(lease) => lease.sessionId !== sessionId
|
|
2472
|
+
);
|
|
2473
|
+
store.save();
|
|
2474
|
+
}
|
|
2475
|
+
return { sessionId, stopped: true };
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
// src/harness/recovery/service.ts
|
|
2479
|
+
function inspectRecovery(context, includeStarts = false) {
|
|
2480
|
+
const pendingStarts = includeStarts ? inspectPendingStarts(context) : [];
|
|
2481
|
+
return {
|
|
2482
|
+
...listSessions(context),
|
|
2483
|
+
control: context.store.state.leases.map(({ token, ...lease }) => ({
|
|
2484
|
+
...lease,
|
|
2485
|
+
expired: Date.parse(lease.expiresAt) <= Date.now()
|
|
2486
|
+
})),
|
|
2487
|
+
deliveries: context.store.state.deliveries,
|
|
2488
|
+
unregisteredOwnedSessions: context.backend.records().filter(
|
|
2489
|
+
(record) => !record.stoppedAt && !context.store.state.group.sessions.some(
|
|
2490
|
+
(session) => session.id === record.id
|
|
2491
|
+
)
|
|
2492
|
+
).map((record) => ({ id: record.id, runtime: record.runtime })),
|
|
2493
|
+
inputReplayed: false,
|
|
2494
|
+
...includeStarts ? { pendingStarts } : {}
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
function abortStart(context, sessionId) {
|
|
2498
|
+
const records2 = context.backend.recordStore;
|
|
2499
|
+
const journal = new LaunchJournal(records2, sessionId);
|
|
2500
|
+
if (!pendingLaunches(records2).some((entry) => entry.id === sessionId) && !journal.abortRecord())
|
|
2501
|
+
throw new Error("start_already_committed");
|
|
2502
|
+
const result = abortLaunch(new Connection(records2), journal);
|
|
2503
|
+
const reference = context.store.state.group.sessions.find(
|
|
2504
|
+
(session) => session.id === sessionId
|
|
2505
|
+
);
|
|
2506
|
+
if (reference) reference.stopped = true;
|
|
2507
|
+
context.store.state.leases = context.store.state.leases.filter(
|
|
2508
|
+
(lease) => lease.sessionId !== sessionId
|
|
2509
|
+
);
|
|
2510
|
+
context.store.save();
|
|
2511
|
+
return { ...result, sessionId };
|
|
2512
|
+
}
|
|
2513
|
+
function reconcile(context) {
|
|
2514
|
+
const { store, backend } = context;
|
|
2515
|
+
for (const delivery of store.state.deliveries) {
|
|
2516
|
+
if (delivery.state === "intent") delivery.state = "delivery_uncertain";
|
|
2517
|
+
}
|
|
2518
|
+
store.save();
|
|
2519
|
+
assertOwnedRecords(backend);
|
|
2520
|
+
for (const session of store.state.group.sessions) {
|
|
2521
|
+
if (!session.stopped) recoverInputLock(backend, session.id);
|
|
2522
|
+
}
|
|
2523
|
+
const retained = [];
|
|
2524
|
+
for (const lease of store.state.leases) {
|
|
2525
|
+
if (Date.parse(lease.expiresAt) > Date.now()) {
|
|
2526
|
+
retained.push(lease);
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
const session = backend.inspect(lease.sessionId);
|
|
2530
|
+
if (writableClients(backend, session.tmuxSessionId) > 0) {
|
|
2531
|
+
retained.push(lease);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
store.state.leases = retained;
|
|
2535
|
+
store.save();
|
|
2536
|
+
return inspectRecovery(context);
|
|
2537
|
+
}
|
|
2538
|
+
async function stopOrphan(context, sessionId) {
|
|
2539
|
+
if (context.store.state.group.sessions.some(
|
|
2540
|
+
(session) => session.id === sessionId
|
|
2541
|
+
))
|
|
2542
|
+
throw new Error("session_is_registered");
|
|
2543
|
+
assertOwnedRecords(context.backend);
|
|
2544
|
+
context.backend.inspect(sessionId);
|
|
2545
|
+
await budgetOperation(context.backend.runDirectory, {
|
|
2546
|
+
action: "stop",
|
|
2547
|
+
id: sessionId
|
|
2548
|
+
});
|
|
2549
|
+
return { sessionId, stopped: true, adopted: false };
|
|
2550
|
+
}
|
|
2551
|
+
async function replaceMain(context, input) {
|
|
2552
|
+
const { store, backend } = context;
|
|
2553
|
+
const previous = store.state.group.sessions.find(
|
|
2554
|
+
(session) => session.role === "main" && !session.stopped
|
|
2555
|
+
);
|
|
2556
|
+
if (previous) {
|
|
2557
|
+
const observation = backend.inspect(previous.id);
|
|
2558
|
+
if (observation.alive) throw new Error("main_still_alive");
|
|
2559
|
+
previous.stopped = true;
|
|
2560
|
+
}
|
|
2561
|
+
store.state.group.generation = randomUUID7();
|
|
2562
|
+
store.state.leases = [];
|
|
2563
|
+
store.save();
|
|
2564
|
+
const result = await startSession(context, { ...input, role: "main" });
|
|
2565
|
+
return {
|
|
2566
|
+
...result,
|
|
2567
|
+
previousMain: previous?.id ?? null,
|
|
2568
|
+
inputReplayed: false
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// src/harness/application/context.ts
|
|
2573
|
+
import { dirname as dirname2 } from "path";
|
|
2574
|
+
function openGroup(path) {
|
|
2575
|
+
const store = new GroupStore(path);
|
|
2576
|
+
const backend = openBackend(dirname2(store.path));
|
|
2577
|
+
if (store.state.group.owner !== backend.owner)
|
|
2578
|
+
throw new Error("foreign_group");
|
|
2579
|
+
return { store, backend };
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// src/harness/application/effects.ts
|
|
2583
|
+
var observations = /* @__PURE__ */ new Set([
|
|
2584
|
+
"group:inspect",
|
|
2585
|
+
"session:list",
|
|
2586
|
+
"session:inspect",
|
|
2587
|
+
"session:read",
|
|
2588
|
+
"session:watch",
|
|
2589
|
+
"delivery:list",
|
|
2590
|
+
"delivery:inspect",
|
|
2591
|
+
"recovery:inspect",
|
|
2592
|
+
"attach:inspect"
|
|
2593
|
+
]);
|
|
2594
|
+
function isReadOnly(operation) {
|
|
2595
|
+
if (operation.family === "attach" && operation.action === "open")
|
|
2596
|
+
return (operation.mode ?? "observe") === "observe";
|
|
2597
|
+
return observations.has(`${operation.family}:${operation.action}`);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// src/harness/adapters/tmux/detach.ts
|
|
2601
|
+
function detachClients(context, sessionId) {
|
|
2602
|
+
context.store.session(sessionId);
|
|
2603
|
+
const session = context.backend.inspect(sessionId);
|
|
2604
|
+
context.backend.tmux(["detach-client", "-s", session.tmuxSessionId]);
|
|
2605
|
+
return { sessionId, clientsDetached: true, runtimeStopRequested: false };
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
// src/harness/adapters/tmux/clients.ts
|
|
2609
|
+
function attachedClients(backend, sessionId) {
|
|
2610
|
+
const session = backend.inspect(sessionId);
|
|
2611
|
+
if (!session.alive) throw new Error("session_unavailable");
|
|
2612
|
+
const output = backend.tmux([
|
|
2613
|
+
"list-clients",
|
|
2614
|
+
"-F",
|
|
2615
|
+
"#{session_id}|#{client_pid}|#{client_readonly}|#{client_tty}"
|
|
2616
|
+
]).trim();
|
|
2617
|
+
if (!output) return [];
|
|
2618
|
+
return output.split("\n").flatMap((line) => {
|
|
2619
|
+
const [id2, pid, readOnly, tty] = line.split("|");
|
|
2620
|
+
if (id2 !== session.tmuxSessionId) return [];
|
|
2621
|
+
if (!/^\d+$/.test(pid ?? "") || !tty || !["0", "1"].includes(readOnly ?? ""))
|
|
2622
|
+
throw new Error("attachment_observation_unknown");
|
|
2623
|
+
return [{ pid: Number(pid), tty, readOnly: readOnly === "1" }];
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// src/harness/observation/attachments.ts
|
|
2628
|
+
function inspectAttachments(context, sessionId) {
|
|
2629
|
+
context.store.session(sessionId);
|
|
2630
|
+
return { sessionId, clients: attachedClients(context.backend, sessionId) };
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// src/harness/application/dispatch.ts
|
|
2634
|
+
async function runOperation(operation) {
|
|
2635
|
+
const context = openGroup(text(operation.groupFile, "group_file"));
|
|
2636
|
+
if (!isReadOnly(operation) && operation.generation !== context.store.state.group.generation) {
|
|
2637
|
+
throw new Error("stale_group_generation");
|
|
2638
|
+
}
|
|
2639
|
+
const { family, action } = operation;
|
|
2640
|
+
if (family === "group")
|
|
2641
|
+
return groupOperation(context, action, operation.input);
|
|
2642
|
+
if (family === "session") {
|
|
2643
|
+
if (action === "start")
|
|
2644
|
+
return startSession(context, object(operation.input));
|
|
2645
|
+
if (action === "list") return listSessions(context);
|
|
2646
|
+
const sessionId = text(operation.sessionId, "session_id");
|
|
2647
|
+
if (action === "stop") return stopSession(context, sessionId);
|
|
2648
|
+
return observeSession(context, action, sessionId, operation.input);
|
|
2649
|
+
}
|
|
2650
|
+
if (family === "control") {
|
|
2651
|
+
if (action === "acquire") {
|
|
2652
|
+
return acquireControl(
|
|
2653
|
+
context,
|
|
2654
|
+
text(operation.sessionId, "session_id"),
|
|
2655
|
+
text(operation.controller, "controller"),
|
|
2656
|
+
operation.mode ?? "cli"
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
return controlOperation(
|
|
2660
|
+
context,
|
|
2661
|
+
action,
|
|
2662
|
+
text(operation.leaseFile, "lease_file")
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
if (family === "input") {
|
|
2666
|
+
const leaseFile = text(operation.leaseFile, "lease_file");
|
|
2667
|
+
if (action === "interrupt") return interrupt2(context, leaseFile);
|
|
2668
|
+
return deliver(
|
|
2669
|
+
context,
|
|
2670
|
+
action,
|
|
2671
|
+
leaseFile,
|
|
2672
|
+
text(operation.requestId, "request_id"),
|
|
2673
|
+
operation.text ?? ""
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
if (family === "attach") {
|
|
2677
|
+
if (action === "inspect")
|
|
2678
|
+
return inspectAttachments(
|
|
2679
|
+
context,
|
|
2680
|
+
text(operation.sessionId, "session_id")
|
|
2681
|
+
);
|
|
2682
|
+
if (action === "detach")
|
|
2683
|
+
return detachClients(context, text(operation.sessionId, "session_id"));
|
|
2684
|
+
return attachment(
|
|
2685
|
+
context,
|
|
2686
|
+
text(operation.sessionId, "session_id"),
|
|
2687
|
+
operation.mode ?? "observe",
|
|
2688
|
+
operation.leaseFile
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
if (family === "delivery")
|
|
2692
|
+
return inspectDeliveries(context, operation.requestId);
|
|
2693
|
+
if (family === "recovery") {
|
|
2694
|
+
if (action === "abort-start")
|
|
2695
|
+
return abortStart(context, text(operation.sessionId, "session_id"));
|
|
2696
|
+
if (action === "inspect")
|
|
2697
|
+
return inspectRecovery(context, operation.includeStarts === true);
|
|
2698
|
+
if (action === "reconcile") return reconcile(context);
|
|
2699
|
+
if (action === "stop-orphan")
|
|
2700
|
+
return stopOrphan(context, text(operation.sessionId, "session_id"));
|
|
2701
|
+
if (action === "replace-main")
|
|
2702
|
+
return replaceMain(context, object(operation.input));
|
|
2703
|
+
}
|
|
2704
|
+
throw new Error("unsupported_operation");
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// src/harness/application/worker.ts
|
|
2708
|
+
var operationFile = process.argv[2];
|
|
2709
|
+
try {
|
|
2710
|
+
const operation = readPrivateJson(operationFile);
|
|
2711
|
+
const receipt = await runOperation(operation);
|
|
2712
|
+
const group = new GroupStore(text(operation.groupFile, "group_file")).state.group;
|
|
2713
|
+
console.log(
|
|
2714
|
+
JSON.stringify({
|
|
2715
|
+
schemaVersion: 1,
|
|
2716
|
+
ok: true,
|
|
2717
|
+
groupId: group.id,
|
|
2718
|
+
generation: group.generation,
|
|
2719
|
+
effect: `${operation.family}:${operation.action}`,
|
|
2720
|
+
...receipt
|
|
2721
|
+
})
|
|
2722
|
+
);
|
|
2723
|
+
if (receipt.ok === false) process.exitCode = 1;
|
|
2724
|
+
} catch (error) {
|
|
2725
|
+
console.log(
|
|
2726
|
+
JSON.stringify({
|
|
2727
|
+
schemaVersion: 1,
|
|
2728
|
+
ok: false,
|
|
2729
|
+
error: error instanceof Error ? error.message : "operation_failed",
|
|
2730
|
+
resendAllowed: false
|
|
2731
|
+
})
|
|
2732
|
+
);
|
|
2733
|
+
process.exitCode = 1;
|
|
2734
|
+
} finally {
|
|
2735
|
+
unlinkSync4(operationFile);
|
|
2736
|
+
}
|
|
2737
|
+
//# sourceMappingURL=harness-worker.js.map
|