@sellable/install 0.1.359 → 0.1.360-phase111.20260720061815

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.
Files changed (31) hide show
  1. package/bin/sellable-install.mjs +0 -2
  2. package/package.json +2 -11
  3. package/skill-templates/refill-sends-evergreen.md +5 -0
  4. package/skill-templates/refill-sends-v2.md +23 -1
  5. package/skill-templates/refill-sends.md +23 -1
  6. package/bin/sellable-agent-egress-proxy.mjs +0 -31
  7. package/bin/sellable-agent-host-bootstrap.mjs +0 -26
  8. package/bin/sellable-agent-host-worker.mjs +0 -9
  9. package/bin/sellable-agent-runtime-helper.mjs +0 -37
  10. package/bin/sellable-agent-runtime-launcher.mjs +0 -151
  11. package/bin/sellable-agent-runtime-probe.mjs +0 -163
  12. package/bin/sellable-agent-sandbox-init.mjs +0 -93
  13. package/container/Dockerfile +0 -37
  14. package/container/README.md +0 -15
  15. package/container/compose.yaml +0 -22
  16. package/container/entrypoint.sh +0 -13
  17. package/lib/sellable-agent/containment-contract.mjs +0 -472
  18. package/lib/sellable-agent/external-runtime-builder.mjs +0 -272
  19. package/lib/sellable-agent/hermes-bridge.mjs +0 -497
  20. package/lib/sellable-agent/host-bootstrap.mjs +0 -458
  21. package/lib/sellable-agent/host-worker.mjs +0 -2067
  22. package/lib/sellable-agent/profile-materializer.mjs +0 -1410
  23. package/lib/sellable-agent/provisioning-adapter.mjs +0 -992
  24. package/lib/sellable-agent/runtime-boundary.mjs +0 -635
  25. package/lib/sellable-agent/runtime-egress-proxy.mjs +0 -241
  26. package/lib/sellable-agent/runtime-helper.mjs +0 -1428
  27. package/lib/sellable-agent/runtime-reconciler.mjs +0 -683
  28. package/lib/sellable-agent/service-installer.mjs +0 -441
  29. package/services/s6/sellable-agent-egress-proxy/run +0 -15
  30. package/services/s6/sellable-agent-host-worker/run +0 -4
  31. package/services/s6/sellable-agent-runtime/run +0 -19
@@ -1,635 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import {
3
- existsSync,
4
- lstatSync,
5
- readFileSync,
6
- readlinkSync,
7
- realpathSync,
8
- } from "node:fs";
9
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
-
11
- export const RUNTIME_BOUNDARY_VERSION = "sellable-agent-runtime-boundary/v1";
12
- export const RUNTIME_SANDBOX_INIT_PATH = "/usr/local/bin/sellable-agent-sandbox-init";
13
- export const RUNTIME_EGRESS_HOSTS = Object.freeze([
14
- "api.openai.com",
15
- "app.sellable.dev",
16
- "slack.com",
17
- ".slack.com",
18
- ]);
19
- export const RUNTIME_SYSTEM_READ_ONLY_PATHS = Object.freeze([
20
- "/usr",
21
- "/etc/ssl",
22
- "/etc/resolv.conf",
23
- "/etc/hosts",
24
- "/etc/nsswitch.conf",
25
- "/etc/passwd",
26
- "/etc/group",
27
- ]);
28
-
29
- const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
30
- const SLACK_ID = /^[CG][A-Z0-9]{8,31}$/;
31
- const SLACK_MEMBER_ID = /^[UW][A-Z0-9]{8,31}$/;
32
- const SHA256 = /^[a-f0-9]{64}$/;
33
- const INPUT_KEYS = new Set([
34
- "profileId",
35
- "workspaceId",
36
- "agentId",
37
- "runtimeUid",
38
- "runtimeGid",
39
- "proxyUid",
40
- "proxyGid",
41
- "proxyPort",
42
- "proxyExecutablePath",
43
- "proxyExecutableDigest",
44
- "proxyConfigPath",
45
- "proxyConfigDigest",
46
- "activeProfileRoot",
47
- "runtimeStateRoot",
48
- "mcpCredentialRoot",
49
- "hermesSourceRoot",
50
- "hermesExecutablePath",
51
- "hermesExecutableDigest",
52
- "mcpExecutablePath",
53
- "bubblewrapPath",
54
- "setprivPath",
55
- "nftPath",
56
- "cgroupRoot",
57
- "serviceCredentialGeneration",
58
- "policyHash",
59
- "selectedChannelIds",
60
- "defaultChannelId",
61
- "memberAllowlist",
62
- ]);
63
-
64
- const sha256 = (value) => createHash("sha256").update(value).digest("hex");
65
-
66
- function canonical(value) {
67
- if (Array.isArray(value)) return value.map(canonical);
68
- if (value && typeof value === "object") {
69
- return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
70
- }
71
- return value;
72
- }
73
-
74
- function stableJson(value) {
75
- return JSON.stringify(canonical(value));
76
- }
77
-
78
- function safeAbsolute(value) {
79
- return (
80
- typeof value === "string" &&
81
- isAbsolute(value) &&
82
- resolve(value) === value &&
83
- !/[\0\n\r,]/.test(value)
84
- );
85
- }
86
-
87
- function inside(root, candidate) {
88
- const rel = relative(root, candidate);
89
- return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
90
- }
91
-
92
- function exactKeys(value, expected) {
93
- return (
94
- value &&
95
- typeof value === "object" &&
96
- !Array.isArray(value) &&
97
- Object.keys(value).length === expected.size &&
98
- Object.keys(value).every((key) => expected.has(key))
99
- );
100
- }
101
-
102
- function runtimeProfilePath(contract) {
103
- return `/runtime/profiles/${contract.profileId}`;
104
- }
105
-
106
- function boundaryTableName(profileId, runtimeUid) {
107
- return `sellable_agent_${sha256(`${profileId}\0${runtimeUid}`).slice(0, 16)}`;
108
- }
109
-
110
- export function buildRuntimeBoundaryContract(input) {
111
- try {
112
- if (!exactKeys(input, INPUT_KEYS)) throw new Error("boundary schema rejected");
113
- const identityValues = [input.runtimeUid, input.proxyUid];
114
- if (
115
- !SAFE_ID.test(input.profileId) ||
116
- !SAFE_ID.test(input.workspaceId) ||
117
- !SAFE_ID.test(input.agentId) ||
118
- !identityValues.every((value) => Number.isSafeInteger(value) && value > 0) ||
119
- ![input.runtimeGid, input.proxyGid].every((value) => Number.isSafeInteger(value) && value > 0) ||
120
- input.runtimeUid === input.proxyUid ||
121
- !Number.isSafeInteger(input.proxyPort) ||
122
- input.proxyPort < 1024 ||
123
- input.proxyPort > 65535 ||
124
- !Number.isSafeInteger(input.serviceCredentialGeneration) ||
125
- input.serviceCredentialGeneration < 1 ||
126
- !SHA256.test(input.policyHash) ||
127
- !SHA256.test(input.proxyExecutableDigest) ||
128
- !SHA256.test(input.proxyConfigDigest) ||
129
- !SHA256.test(input.hermesExecutableDigest)
130
- ) throw new Error("boundary identity rejected");
131
-
132
- const pathKeys = [
133
- "proxyExecutablePath",
134
- "proxyConfigPath",
135
- "activeProfileRoot",
136
- "runtimeStateRoot",
137
- "mcpCredentialRoot",
138
- "hermesSourceRoot",
139
- "hermesExecutablePath",
140
- "mcpExecutablePath",
141
- "bubblewrapPath",
142
- "setprivPath",
143
- "nftPath",
144
- "cgroupRoot",
145
- ];
146
- if (pathKeys.some((key) => !safeAbsolute(input[key]))) throw new Error("boundary path rejected");
147
- if (
148
- input.bubblewrapPath !== "/usr/bin/bwrap" ||
149
- input.setprivPath !== "/usr/bin/setpriv" ||
150
- !["/usr/sbin/nft", "/usr/bin/nft"].includes(input.nftPath) ||
151
- input.cgroupRoot !== "/sys/fs/cgroup/sellable-agent" ||
152
- input.mcpExecutablePath !== "/usr/local/lib/sellable-agent/external-runtime/mcp/bin/sellable-mcp" ||
153
- !inside(input.hermesSourceRoot, input.hermesExecutablePath)
154
- ) throw new Error("boundary executable pin rejected");
155
- if (
156
- !Array.isArray(input.selectedChannelIds) ||
157
- input.selectedChannelIds.length < 1 ||
158
- input.selectedChannelIds.length > 100 ||
159
- input.selectedChannelIds.some((value) => !SLACK_ID.test(value)) ||
160
- new Set(input.selectedChannelIds).size !== input.selectedChannelIds.length ||
161
- !input.selectedChannelIds.includes(input.defaultChannelId)
162
- ) throw new Error("boundary channel policy rejected");
163
- if (
164
- !Array.isArray(input.memberAllowlist) ||
165
- input.memberAllowlist.length < 1 ||
166
- input.memberAllowlist.length > 1000 ||
167
- input.memberAllowlist.some((value) => !SLACK_MEMBER_ID.test(value)) ||
168
- new Set(input.memberAllowlist).size !== input.memberAllowlist.length
169
- ) throw new Error("boundary member policy rejected");
170
-
171
- const body = {
172
- boundaryVersion: RUNTIME_BOUNDARY_VERSION,
173
- profileId: input.profileId,
174
- workspaceId: input.workspaceId,
175
- agentId: input.agentId,
176
- runtime: { uid: input.runtimeUid, gid: input.runtimeGid },
177
- proxy: {
178
- uid: input.proxyUid,
179
- gid: input.proxyGid,
180
- host: "127.0.0.1",
181
- port: input.proxyPort,
182
- executablePath: input.proxyExecutablePath,
183
- executableDigest: input.proxyExecutableDigest,
184
- configPath: input.proxyConfigPath,
185
- configDigest: input.proxyConfigDigest,
186
- allowedHosts: [...RUNTIME_EGRESS_HOSTS],
187
- },
188
- paths: Object.fromEntries(pathKeys.filter((key) => !["proxyExecutablePath", "proxyConfigPath"].includes(key)).map((key) => [key, input[key]])),
189
- hermesExecutableDigest: input.hermesExecutableDigest,
190
- serviceCredentialGeneration: input.serviceCredentialGeneration,
191
- policyHash: input.policyHash,
192
- selectedChannelIds: [...input.selectedChannelIds],
193
- defaultChannelId: input.defaultChannelId,
194
- memberAllowlist: [...input.memberAllowlist],
195
- systemReadOnlyPaths: [...RUNTIME_SYSTEM_READ_ONLY_PATHS],
196
- limits: {
197
- pidsMax: 128,
198
- memoryMax: 1024 * 1024 * 1024,
199
- cpuMax: "100000 100000",
200
- },
201
- firewall: {
202
- family: "inet",
203
- table: boundaryTableName(input.profileId, input.runtimeUid),
204
- runtimeUid: input.runtimeUid,
205
- proxyHost: "127.0.0.1",
206
- proxyPort: input.proxyPort,
207
- defaultRuntimeAction: "reject",
208
- },
209
- };
210
- return { ok: true, contract: Object.freeze({ ...body, contractDigest: sha256(stableJson(body)) }) };
211
- } catch {
212
- return { ok: false, code: "runtime_boundary_contract_rejected" };
213
- }
214
- }
215
-
216
- function addParentDirs(args, targets) {
217
- const seen = new Set();
218
- for (const target of targets) {
219
- let cursor = dirname(target);
220
- const parents = [];
221
- while (cursor !== "/" && cursor !== ".") {
222
- parents.push(cursor);
223
- cursor = dirname(cursor);
224
- }
225
- for (const parent of parents.reverse()) {
226
- if (!seen.has(parent)) {
227
- args.push("--dir", parent);
228
- seen.add(parent);
229
- }
230
- }
231
- }
232
- }
233
-
234
- function runtimeEnvironment(contract) {
235
- const profileRoot = runtimeProfilePath(contract);
236
- return {
237
- PATH: `${dirname(contract.paths.hermesExecutablePath)}:/usr/local/bin:/usr/bin:/bin`,
238
- HOME: `${profileRoot}/home`,
239
- HERMES_HOME: "/runtime",
240
- NODE_ENV: "production",
241
- HTTPS_PROXY: `http://${contract.proxy.host}:${contract.proxy.port}`,
242
- HTTP_PROXY: `http://${contract.proxy.host}:${contract.proxy.port}`,
243
- NO_PROXY: "127.0.0.1,localhost",
244
- SLACK_HOME_CHANNEL: contract.defaultChannelId,
245
- SLACK_ALLOWED_CHANNELS: contract.selectedChannelIds.join(","),
246
- SLACK_ALLOWED_USERS: contract.memberAllowlist.join(","),
247
- SLACK_REQUIRE_MENTION: "true",
248
- SELLABLE_AGENT_RUNTIME: "1",
249
- SELLABLE_AGENT_RUNTIME_GATE_FILE: "/runtime/gates.json",
250
- SELLABLE_AGENT_RUNTIME_UID: String(contract.runtime.uid),
251
- SELLABLE_AGENT_RUNTIME_GID: String(contract.runtime.gid),
252
- SELLABLE_AGENT_HERMES_EXECUTABLE: contract.paths.hermesExecutablePath,
253
- SELLABLE_AGENT_SETPRIV_EXECUTABLE: contract.paths.setprivPath,
254
- SELLABLE_SLACK_APP_TOKEN_FD: "8",
255
- SELLABLE_SLACK_BOT_TOKEN_FD: "9",
256
- SELLABLE_AGENT_PROFILE_ID: contract.profileId,
257
- SELLABLE_AGENT_WORKSPACE_ID: contract.workspaceId,
258
- SELLABLE_AGENT_ID: contract.agentId,
259
- SELLABLE_AGENT_CREDENTIAL_GENERATION: String(contract.serviceCredentialGeneration),
260
- SELLABLE_AGENT_POLICY_HASH: contract.policyHash,
261
- SELLABLE_AGENT_SELECTED_CHANNEL_IDS: JSON.stringify(contract.selectedChannelIds),
262
- SELLABLE_LOCK_WORKSPACE_ID: contract.workspaceId,
263
- SELLABLE_REQUIRE_WORKSPACE_LOCK: "1",
264
- SELLABLE_CONFIG_PATH: `${profileRoot}/sellable/mcp.json`,
265
- SELLABLE_AGENT_RUNTIME_SECRET_ROOT: "/run/sellable-agent",
266
- SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE: "/run/sellable-agent/service-credential",
267
- };
268
- }
269
-
270
- export function buildRuntimeBoundaryLaunchSpec(contract) {
271
- try {
272
- if (!contract || contract.boundaryVersion !== RUNTIME_BOUNDARY_VERSION || !SHA256.test(contract.contractDigest)) {
273
- throw new Error("boundary contract missing");
274
- }
275
- const active = contract.paths.activeProfileRoot;
276
- const state = contract.paths.runtimeStateRoot;
277
- const credential = join(contract.paths.mcpCredentialRoot, contract.profileId);
278
- const profileTarget = runtimeProfilePath(contract);
279
- const readOnlyProfileBinds = [
280
- [join(active, "agent-profile.json"), `${profileTarget}/agent-profile.json`],
281
- [join(active, "config.yaml"), `${profileTarget}/config.yaml`],
282
- [join(active, "SOUL.md"), `${profileTarget}/SOUL.md`],
283
- [join(active, "skills"), `${profileTarget}/skills`],
284
- [join(active, "sellable"), `${profileTarget}/sellable`],
285
- ];
286
- const args = [
287
- "--die-with-parent",
288
- "--new-session",
289
- "--unshare-pid",
290
- "--unshare-ipc",
291
- "--unshare-uts",
292
- "--unshare-cgroup",
293
- "--clearenv",
294
- "--unsetenv",
295
- "PWD",
296
- "--cap-drop",
297
- "ALL",
298
- "--cap-add",
299
- "CAP_SETUID",
300
- "--cap-add",
301
- "CAP_SETGID",
302
- "--cap-add",
303
- "CAP_SETPCAP",
304
- ];
305
- const bindTargets = [
306
- ...contract.systemReadOnlyPaths,
307
- contract.paths.hermesSourceRoot,
308
- "/runtime",
309
- "/run/sellable-agent",
310
- "/run/bootstrap",
311
- ...readOnlyProfileBinds.map(([, target]) => target),
312
- "/proc",
313
- "/dev",
314
- "/tmp",
315
- ];
316
- addParentDirs(args, bindTargets);
317
- for (const path of contract.systemReadOnlyPaths) args.push("--ro-bind", path, path);
318
- args.push(
319
- "--symlink", "usr/bin", "/bin",
320
- "--symlink", "usr/lib", "/lib",
321
- "--symlink", "usr/lib64", "/lib64",
322
- "--symlink", "usr/sbin", "/sbin",
323
- );
324
- args.push("--ro-bind", contract.paths.hermesSourceRoot, contract.paths.hermesSourceRoot);
325
- args.push("--bind", state, "/runtime");
326
- for (const [source, target] of readOnlyProfileBinds) args.push("--ro-bind", source, target);
327
- args.push("--ro-bind", credential, "/run/sellable-agent");
328
- args.push(
329
- "--perms", "0600", "--file", "8", "/run/bootstrap/slack-app",
330
- "--perms", "0600", "--file", "9", "/run/bootstrap/slack-bot",
331
- );
332
- args.push(
333
- "--proc", "/proc",
334
- "--dev", "/dev",
335
- "--tmpfs", "/tmp",
336
- "--remount-ro", "/",
337
- );
338
- const env = runtimeEnvironment(contract);
339
- for (const [name, value] of Object.entries(env)) args.push("--setenv", name, value);
340
- args.push(
341
- "--chdir",
342
- profileTarget,
343
- RUNTIME_SANDBOX_INIT_PATH,
344
- );
345
- return {
346
- ok: true,
347
- command: contract.paths.bubblewrapPath,
348
- args,
349
- env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" },
350
- inheritedFileDescriptors: [8, 9],
351
- runtimeEnvironment: env,
352
- profileTarget,
353
- cgroupPath: join(contract.paths.cgroupRoot, contract.profileId),
354
- shell: false,
355
- };
356
- } catch {
357
- return { ok: false, code: "runtime_boundary_launch_rejected" };
358
- }
359
- }
360
-
361
- export function buildRuntimeBoundaryNftProgram(contract) {
362
- if (!contract?.firewall || contract.firewall.defaultRuntimeAction !== "reject") {
363
- return { ok: false, code: "runtime_boundary_firewall_rejected" };
364
- }
365
- const { table, runtimeUid, proxyHost, proxyPort } = contract.firewall;
366
- if (!/^sellable_agent_[a-f0-9]{16}$/.test(table) || proxyHost !== "127.0.0.1") {
367
- return { ok: false, code: "runtime_boundary_firewall_rejected" };
368
- }
369
- const program = [
370
- `table inet ${table} {`,
371
- " chain output {",
372
- " type filter hook output priority -20; policy accept;",
373
- ` meta skuid ${runtimeUid} ip daddr 127.0.0.1 tcp dport ${proxyPort} accept`,
374
- ` meta skuid ${runtimeUid} ip6 daddr ::1 tcp dport ${proxyPort} accept`,
375
- ` meta skuid ${runtimeUid} reject with icmpx type admin-prohibited`,
376
- " }",
377
- "}",
378
- "",
379
- ].join("\n");
380
- return {
381
- ok: true,
382
- table,
383
- deleteArgs: ["delete", "table", "inet", table],
384
- applyArgs: ["-f", "-"],
385
- program,
386
- programDigest: sha256(program),
387
- };
388
- }
389
-
390
- export function expectedRuntimeCgroup(contract) {
391
- const path = join(contract.paths.cgroupRoot, contract.profileId);
392
- return {
393
- path,
394
- files: {
395
- "pids.max": String(contract.limits.pidsMax),
396
- "memory.max": String(contract.limits.memoryMax),
397
- "cpu.max": contract.limits.cpuMax,
398
- },
399
- };
400
- }
401
-
402
- function parseStatus(source) {
403
- return Object.fromEntries(
404
- source.split(/\r?\n/).filter(Boolean).map((line) => {
405
- const index = line.indexOf(":");
406
- return [line.slice(0, index), line.slice(index + 1).trim()];
407
- }),
408
- );
409
- }
410
-
411
- function parseMountInfo(source) {
412
- const entries = [];
413
- for (const line of source.split(/\r?\n/).filter(Boolean)) {
414
- const separator = line.indexOf(" - ");
415
- if (separator < 0) continue;
416
- const left = line.slice(0, separator).split(" ");
417
- entries.push({ mountPoint: left[4], options: new Set(left[5].split(",")) });
418
- }
419
- return entries;
420
- }
421
-
422
- function requiredMount(entries, mountPoint, option) {
423
- return entries.some((entry) => entry.mountPoint === mountPoint && entry.options.has(option));
424
- }
425
-
426
- function expectedHermesCommand(contract) {
427
- const executable = safeRead(contract.paths.hermesExecutablePath, null);
428
- if (sha256(executable) !== contract.hermesExecutableDigest) throw new Error("Hermes executable drift rejected");
429
- const firstLine = executable.subarray(0, 4096).toString("utf8").split(/\r?\n/, 1)[0];
430
- if (!firstLine.startsWith("#!")) return [contract.paths.hermesExecutablePath];
431
- const interpreter = firstLine.slice(2).trim();
432
- if (!safeAbsolute(interpreter) || /\s/.test(interpreter)) throw new Error("Hermes shebang rejected");
433
- return [interpreter, contract.paths.hermesExecutablePath];
434
- }
435
-
436
- export function runtimeBoundaryNftRulesMatch(contract, source) {
437
- if (typeof source !== "string" || source.length > 128 * 1024) return false;
438
- const expected = buildRuntimeBoundaryNftProgram(contract);
439
- if (!expected.ok) return false;
440
- const tokens = (value) => value
441
- .replace(/#.*$/gm, "")
442
- .match(/[{};]|[^\s{};]+/g) ?? [];
443
- return stableJson(tokens(source)) === stableJson(tokens(expected.program));
444
- }
445
-
446
- function safeRead(path, encoding = "utf8") {
447
- const link = lstatSync(path);
448
- if (link.isSymbolicLink() || !link.isFile()) throw new Error("boundary observation path rejected");
449
- return readFileSync(path, encoding);
450
- }
451
-
452
- export function observeRuntimeBoundary({
453
- contract,
454
- pid,
455
- procRoot = "/proc",
456
- cgroupRoot = contract?.paths?.cgroupRoot,
457
- nftRuleset,
458
- proxyObservation,
459
- }) {
460
- try {
461
- if (!Number.isSafeInteger(pid) || pid < 2 || !safeAbsolute(procRoot) || !safeAbsolute(cgroupRoot)) {
462
- throw new Error("boundary observation identity rejected");
463
- }
464
- const processRoot = join(procRoot, String(pid));
465
- const status = parseStatus(safeRead(join(processRoot, "status")));
466
- const uid = Number(status.Uid?.split(/\s+/)[0]);
467
- const gid = Number(status.Gid?.split(/\s+/)[0]);
468
- if (
469
- uid !== contract.runtime.uid ||
470
- gid !== contract.runtime.gid ||
471
- status.NoNewPrivs !== "1" ||
472
- !/^0+$/.test(status.CapEff ?? "")
473
- ) throw new Error("runtime privilege observation rejected");
474
-
475
- const namespaceChecks = ["mnt", "pid", "ipc", "uts"].map((name) => {
476
- const runtimeNs = readlinkSync(join(processRoot, "ns", name));
477
- const hostNs = readlinkSync(join(procRoot, "1", "ns", name));
478
- return runtimeNs !== hostNs;
479
- });
480
- if (namespaceChecks.some((value) => !value)) throw new Error("runtime namespace observation rejected");
481
-
482
- const mounts = parseMountInfo(safeRead(join(processRoot, "mountinfo")));
483
- const profileTarget = runtimeProfilePath(contract);
484
- const requiredReadOnlyMounts = [
485
- "/",
486
- contract.paths.hermesSourceRoot,
487
- `${profileTarget}/agent-profile.json`,
488
- `${profileTarget}/config.yaml`,
489
- `${profileTarget}/SOUL.md`,
490
- `${profileTarget}/skills`,
491
- `${profileTarget}/sellable`,
492
- "/run/sellable-agent",
493
- ];
494
- if (
495
- requiredReadOnlyMounts.some((path) => !requiredMount(mounts, path, "ro")) ||
496
- !requiredMount(mounts, "/runtime", "rw")
497
- ) throw new Error("runtime mount observation rejected");
498
-
499
- const cgroupText = safeRead(join(processRoot, "cgroup")).trim();
500
- const expectedGroup = `/sellable-agent/${contract.profileId}`;
501
- if (!cgroupText.split(/\r?\n/).some((line) => line === `0::${expectedGroup}`)) {
502
- throw new Error("runtime cgroup membership rejected");
503
- }
504
- const cgroup = expectedRuntimeCgroup(contract);
505
- const observedCgroupPath = join(cgroupRoot, contract.profileId);
506
- for (const [name, value] of Object.entries(cgroup.files)) {
507
- if (safeRead(join(observedCgroupPath, name)).trim() !== value) throw new Error("runtime cgroup limit rejected");
508
- }
509
-
510
- const environment = Object.fromEntries(
511
- safeRead(join(processRoot, "environ"), null)
512
- .toString("utf8")
513
- .split("\0")
514
- .filter(Boolean)
515
- .map((entry) => [entry.slice(0, entry.indexOf("=")), entry.slice(entry.indexOf("=") + 1)]),
516
- );
517
- const expectedEnv = runtimeEnvironment(contract);
518
- if (
519
- stableJson(environment) !== stableJson(expectedEnv) ||
520
- Object.keys(environment).some((name) => /SLACK_(?:APP|BOT)_TOKEN$|HERMES_SESSION_/.test(name))
521
- ) throw new Error("runtime environment observation rejected");
522
-
523
- const cmdline = safeRead(join(processRoot, "cmdline"), null).toString("utf8").split("\0").filter(Boolean);
524
- const expectedCommand = [
525
- ...expectedHermesCommand(contract),
526
- "--profile",
527
- contract.profileId,
528
- "gateway",
529
- "run",
530
- ];
531
- if (stableJson(cmdline) !== stableJson(expectedCommand)) throw new Error("runtime executable observation rejected");
532
- for (const [fd, name] of [[8, "slack-app"], [9, "slack-bot"]]) {
533
- const descriptor = lstatSync(join(processRoot, "fd", String(fd)));
534
- if (!descriptor.isSymbolicLink()) throw new Error("runtime inherited descriptor rejected");
535
- const target = readlinkSync(join(processRoot, "fd", String(fd)));
536
- if (target !== `/run/bootstrap/${name} (deleted)`) {
537
- throw new Error("runtime inherited descriptor source rejected");
538
- }
539
- }
540
- if (!runtimeBoundaryNftRulesMatch(contract, nftRuleset)) throw new Error("runtime firewall observation rejected");
541
- if (
542
- !proxyObservation?.ok ||
543
- proxyObservation.uid !== contract.proxy.uid ||
544
- proxyObservation.gid !== contract.proxy.gid ||
545
- proxyObservation.host !== contract.proxy.host ||
546
- proxyObservation.port !== contract.proxy.port ||
547
- proxyObservation.executableDigest !== contract.proxy.executableDigest ||
548
- proxyObservation.configDigest !== contract.proxy.configDigest ||
549
- stableJson(proxyObservation.allowedHosts) !== stableJson(contract.proxy.allowedHosts)
550
- ) throw new Error("runtime egress proxy observation rejected");
551
-
552
- return {
553
- ok: true,
554
- boundaryVersion: contract.boundaryVersion,
555
- contractDigest: contract.contractDigest,
556
- pid,
557
- runtimeUid: uid,
558
- cgroup: expectedGroup,
559
- firewallTable: contract.firewall.table,
560
- proxy: {
561
- uid: contract.proxy.uid,
562
- host: contract.proxy.host,
563
- port: contract.proxy.port,
564
- executableDigest: contract.proxy.executableDigest,
565
- configDigest: contract.proxy.configDigest,
566
- allowedHosts: [...contract.proxy.allowedHosts],
567
- },
568
- checks: {
569
- namespacesIsolated: true,
570
- rootReadOnly: true,
571
- revisionReadOnly: true,
572
- runtimeStateWritable: true,
573
- noNewPrivileges: true,
574
- capabilitiesDropped: true,
575
- resourceLimits: true,
576
- directEgressDenied: true,
577
- allowlistedProxyOnly: true,
578
- inheritedSlackDescriptors: true,
579
- },
580
- };
581
- } catch {
582
- return { ok: false, code: "runtime_boundary_observation_rejected" };
583
- }
584
- }
585
-
586
- export function inspectRuntimeBoundaryInputs(contract) {
587
- try {
588
- const regularFiles = [
589
- contract.paths.bubblewrapPath,
590
- contract.paths.setprivPath,
591
- contract.paths.nftPath,
592
- contract.paths.hermesExecutablePath,
593
- contract.paths.mcpExecutablePath,
594
- RUNTIME_SANDBOX_INIT_PATH,
595
- contract.proxy.executablePath,
596
- contract.proxy.configPath,
597
- join(contract.paths.activeProfileRoot, "agent-profile.json"),
598
- join(contract.paths.activeProfileRoot, "config.yaml"),
599
- join(contract.paths.activeProfileRoot, "SOUL.md"),
600
- join(contract.paths.activeProfileRoot, "sellable", "mcp.json"),
601
- join(contract.paths.mcpCredentialRoot, contract.profileId, "service-credential"),
602
- ];
603
- const directories = [
604
- contract.paths.activeProfileRoot,
605
- contract.paths.runtimeStateRoot,
606
- contract.paths.mcpCredentialRoot,
607
- contract.paths.hermesSourceRoot,
608
- join(contract.paths.activeProfileRoot, "skills"),
609
- join(contract.paths.activeProfileRoot, "sellable"),
610
- ];
611
- for (const path of regularFiles) {
612
- const link = lstatSync(path);
613
- if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o022) !== 0) throw new Error("unsafe boundary file");
614
- }
615
- for (const path of directories) {
616
- const link = lstatSync(path);
617
- if (link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o022) !== 0) throw new Error("unsafe boundary directory");
618
- }
619
- if (
620
- sha256(readFileSync(contract.proxy.executablePath)) !== contract.proxy.executableDigest ||
621
- sha256(readFileSync(contract.proxy.configPath)) !== contract.proxy.configDigest ||
622
- sha256(readFileSync(contract.paths.hermesExecutablePath)) !== contract.hermesExecutableDigest ||
623
- realpathSync(contract.paths.hermesSourceRoot) !== contract.paths.hermesSourceRoot ||
624
- !inside(contract.paths.hermesSourceRoot, realpathSync(contract.paths.hermesExecutablePath))
625
- ) throw new Error("boundary byte identity rejected");
626
- return {
627
- ok: true,
628
- contractDigest: contract.contractDigest,
629
- proxyExecutableDigest: contract.proxy.executableDigest,
630
- proxyConfigDigest: contract.proxy.configDigest,
631
- };
632
- } catch {
633
- return { ok: false, code: "runtime_boundary_inputs_rejected" };
634
- }
635
- }