@sellable/install 0.1.359-phase111.20260720052618 → 0.1.359
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/bin/sellable-agent-egress-proxy.mjs +31 -0
- package/bin/sellable-agent-host-bootstrap.mjs +26 -0
- package/bin/sellable-agent-host-worker.mjs +9 -0
- package/bin/sellable-agent-runtime-helper.mjs +37 -0
- package/bin/sellable-agent-runtime-launcher.mjs +151 -0
- package/bin/sellable-agent-runtime-probe.mjs +163 -0
- package/bin/sellable-agent-sandbox-init.mjs +93 -0
- package/bin/sellable-install.mjs +2 -0
- package/container/Dockerfile +37 -0
- package/container/README.md +15 -0
- package/container/compose.yaml +22 -0
- package/container/entrypoint.sh +13 -0
- package/lib/sellable-agent/containment-contract.mjs +472 -0
- package/lib/sellable-agent/external-runtime-builder.mjs +272 -0
- package/lib/sellable-agent/hermes-bridge.mjs +497 -0
- package/lib/sellable-agent/host-bootstrap.mjs +458 -0
- package/lib/sellable-agent/host-worker.mjs +2067 -0
- package/lib/sellable-agent/profile-materializer.mjs +1410 -0
- package/lib/sellable-agent/provisioning-adapter.mjs +992 -0
- package/lib/sellable-agent/runtime-boundary.mjs +635 -0
- package/lib/sellable-agent/runtime-egress-proxy.mjs +241 -0
- package/lib/sellable-agent/runtime-helper.mjs +1428 -0
- package/lib/sellable-agent/runtime-reconciler.mjs +683 -0
- package/lib/sellable-agent/service-installer.mjs +441 -0
- package/package.json +11 -2
- package/services/s6/sellable-agent-egress-proxy/run +15 -0
- package/services/s6/sellable-agent-host-worker/run +4 -0
- package/services/s6/sellable-agent-runtime/run +19 -0
- package/skill-templates/refill-sends-evergreen.md +0 -5
- package/skill-templates/refill-sends-v2.md +1 -23
- package/skill-templates/refill-sends.md +1 -14
|
@@ -0,0 +1,1428 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync, chownSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync,
|
|
4
|
+
openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync,
|
|
5
|
+
} from "node:fs";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
+
import { createConnection } from "node:net";
|
|
8
|
+
import { runBoundedChildProcess } from "./provisioning-adapter.mjs";
|
|
9
|
+
import {
|
|
10
|
+
HERMES_AGENT_BRIDGE_CONTRACT,
|
|
11
|
+
verifyHermesAgentBridge,
|
|
12
|
+
} from "./hermes-bridge.mjs";
|
|
13
|
+
import { buildRuntimeBoundaryContract, observeRuntimeBoundary } from "./runtime-boundary.mjs";
|
|
14
|
+
import { observeRuntimeEgressProxy, validateRuntimeEgressProxyConfig } from "./runtime-egress-proxy.mjs";
|
|
15
|
+
|
|
16
|
+
const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
17
|
+
const SHA256_16 = /^[a-f0-9]{16}$/;
|
|
18
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
19
|
+
export const EXTERNAL_RUNTIME_CLOSURE_ROOT = "/usr/local/lib/sellable-agent/external-runtime";
|
|
20
|
+
export const EXTERNAL_RUNTIME_MANIFEST_PATH = `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/manifest.json`;
|
|
21
|
+
export const EXTERNAL_MCP_ENTRYPOINT = `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/mcp/bin/sellable-mcp`;
|
|
22
|
+
export const EXTERNAL_HERMES_ENTRYPOINT = `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes/venv/bin/hermes`;
|
|
23
|
+
const EXTERNAL_RUNTIME_MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
|
|
24
|
+
const ACTIONS = new Set([
|
|
25
|
+
"INSTALL_MCP_CREDENTIAL",
|
|
26
|
+
"INSTALL_PROVIDER_CREDENTIAL",
|
|
27
|
+
"INSTALL_RUNTIME_SERVICE_ENV",
|
|
28
|
+
"SET_RUNTIME_GATES",
|
|
29
|
+
"OBSERVE_RUNTIME_GATES",
|
|
30
|
+
"OBSERVE_RUNTIME_LIFECYCLE",
|
|
31
|
+
"CLEAN_PROFILE_SECRETS",
|
|
32
|
+
"CHOWN_PROFILE",
|
|
33
|
+
"REMOVE_PROFILE",
|
|
34
|
+
"OBSERVE_RUNTIME",
|
|
35
|
+
"OBSERVE_RUNTIME_DRIFT",
|
|
36
|
+
"PROBE_CONTAINMENT",
|
|
37
|
+
"INSTALL_HERMES_BRIDGE",
|
|
38
|
+
"VERIFY_HERMES_BRIDGE",
|
|
39
|
+
"CONTROL_RUNTIME",
|
|
40
|
+
]);
|
|
41
|
+
const PROFILE_ENTRIES = Object.freeze([
|
|
42
|
+
[".", "directory"],
|
|
43
|
+
["agent-profile.json", "file"],
|
|
44
|
+
["config.yaml", "file"],
|
|
45
|
+
["sellable", "directory"],
|
|
46
|
+
["sellable/mcp.json", "file"],
|
|
47
|
+
["SOUL.md", "file"],
|
|
48
|
+
["skills", "directory"],
|
|
49
|
+
["skills/sellable", "directory"],
|
|
50
|
+
["skills/sellable/SKILL.md", "file"],
|
|
51
|
+
]);
|
|
52
|
+
const ACTION_KEYS = Object.freeze({
|
|
53
|
+
INSTALL_MCP_CREDENTIAL: ["action", "profileId", "operationId", "fence", "sourceRequestFile", "generation", "fingerprint"],
|
|
54
|
+
INSTALL_PROVIDER_CREDENTIAL: ["action", "profileId", "operationId", "fence", "fingerprint"],
|
|
55
|
+
INSTALL_RUNTIME_SERVICE_ENV: ["action", "profileId", "operationId", "fence", "revisionId", "profileDigest"],
|
|
56
|
+
SET_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
|
|
57
|
+
OBSERVE_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
|
|
58
|
+
OBSERVE_RUNTIME_LIFECYCLE: ["action", "profileId", "operationId", "fence", "lifecycle"],
|
|
59
|
+
CLEAN_PROFILE_SECRETS: ["action", "profileId", "operationId", "fence"],
|
|
60
|
+
CHOWN_PROFILE: ["action", "profileId", "operationId", "fence", "profileRoot", "revisionId", "profileDigest"],
|
|
61
|
+
REMOVE_PROFILE: ["action", "profileId", "operationId", "fence", "slot", "revisionId"],
|
|
62
|
+
OBSERVE_RUNTIME: ["action", "profileId", "operationId", "fence"],
|
|
63
|
+
OBSERVE_RUNTIME_DRIFT: ["action", "profileId", "operationId", "fence"],
|
|
64
|
+
PROBE_CONTAINMENT: ["action", "profileId", "operationId", "fence", "profileRoot", "revisionId"],
|
|
65
|
+
INSTALL_HERMES_BRIDGE: ["action", "profileId", "operationId", "fence"],
|
|
66
|
+
VERIFY_HERMES_BRIDGE: ["action", "profileId", "operationId", "fence"],
|
|
67
|
+
CONTROL_RUNTIME: ["action", "profileId", "operationId", "fence", "control"],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function privateRoot(value) {
|
|
71
|
+
if (!isAbsolute(value) || !existsSync(value)) throw new Error("helper root missing");
|
|
72
|
+
const link = lstatSync(value);
|
|
73
|
+
if (link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o077) !== 0) throw new Error("helper root rejected");
|
|
74
|
+
return realpathSync(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function profileStorageRoot(value) {
|
|
78
|
+
if (!isAbsolute(value) || !existsSync(value)) throw new Error("profile storage root missing");
|
|
79
|
+
const link = lstatSync(value);
|
|
80
|
+
const mode = link.mode & 0o777;
|
|
81
|
+
if (
|
|
82
|
+
link.isSymbolicLink() || !link.isDirectory() ||
|
|
83
|
+
(mode & 0o700) !== 0o700 || (mode & 0o066) !== 0
|
|
84
|
+
) throw new Error("profile storage root rejected");
|
|
85
|
+
return realpathSync(value);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function immutableSourceRoot(value) {
|
|
89
|
+
if (!isAbsolute(value) || !existsSync(value)) throw new Error("source root missing");
|
|
90
|
+
const link = lstatSync(value);
|
|
91
|
+
if (link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o022) !== 0) {
|
|
92
|
+
throw new Error("source root rejected");
|
|
93
|
+
}
|
|
94
|
+
return realpathSync(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fsyncDirectory(value) {
|
|
98
|
+
const descriptor = openSync(value, "r");
|
|
99
|
+
try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function atomicCredential(pathValue, secret, uid, gid) {
|
|
103
|
+
const temp = join(dirname(pathValue), `.${basename(pathValue)}.${randomUUID()}.tmp`);
|
|
104
|
+
writeFileSync(temp, `${secret}\n`, { mode: 0o600, flag: "wx" });
|
|
105
|
+
chmodSync(temp, 0o600);
|
|
106
|
+
const tempOwner = lstatSync(temp);
|
|
107
|
+
if (tempOwner.uid !== uid || tempOwner.gid !== gid) chownSync(temp, uid, gid);
|
|
108
|
+
const descriptor = openSync(temp, "r");
|
|
109
|
+
try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
|
|
110
|
+
renameSync(temp, pathValue);
|
|
111
|
+
fsyncDirectory(dirname(pathValue));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function providerCredentialPath(profile, profileId) {
|
|
115
|
+
const runtimeStateRoot = safeRuntimeStateRoot(profile);
|
|
116
|
+
const profilesRoot = join(runtimeStateRoot, "profiles");
|
|
117
|
+
const profileRoot = join(profilesRoot, profileId);
|
|
118
|
+
const runtimeHome = join(profileRoot, "home");
|
|
119
|
+
for (const pathValue of [profilesRoot, profileRoot, runtimeHome]) {
|
|
120
|
+
const link = lstatSync(pathValue);
|
|
121
|
+
if (
|
|
122
|
+
link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o777) !== 0o700 ||
|
|
123
|
+
link.uid !== profile.runtimeUid || link.gid !== profile.runtimeGid
|
|
124
|
+
) throw new Error("provider runtime home rejected");
|
|
125
|
+
}
|
|
126
|
+
const codexRoot = join(runtimeHome, ".codex");
|
|
127
|
+
if (!existsSync(codexRoot)) {
|
|
128
|
+
mkdirSync(codexRoot, { mode: 0o700 });
|
|
129
|
+
chownSync(codexRoot, profile.runtimeUid, profile.runtimeGid);
|
|
130
|
+
}
|
|
131
|
+
const codex = lstatSync(codexRoot);
|
|
132
|
+
if (
|
|
133
|
+
codex.isSymbolicLink() || !codex.isDirectory() || (codex.mode & 0o777) !== 0o700 ||
|
|
134
|
+
codex.uid !== profile.runtimeUid || codex.gid !== profile.runtimeGid
|
|
135
|
+
) throw new Error("provider credential root rejected");
|
|
136
|
+
return join(codexRoot, "auth.json");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function installProviderCredential(profile, request, config) {
|
|
140
|
+
if (
|
|
141
|
+
!SHA256.test(request.fingerprint ?? "") ||
|
|
142
|
+
request.fingerprint !== profile.providerCredentialFingerprint ||
|
|
143
|
+
(
|
|
144
|
+
profile.providerCredentialSourcePath !== "/var/lib/sellable-agent-bootstrap/provider-auth" &&
|
|
145
|
+
config.disposableFixture !== true
|
|
146
|
+
)
|
|
147
|
+
) throw new Error("provider credential identity rejected");
|
|
148
|
+
const sourceRoot = privateRoot(dirname(profile.providerCredentialSourcePath));
|
|
149
|
+
const source = readPrivateDirectFile(sourceRoot, profile.providerCredentialSourcePath).trim();
|
|
150
|
+
if (!source || source.length > 1024 * 1024 || sha256(source) !== request.fingerprint) {
|
|
151
|
+
throw new Error("provider credential source rejected");
|
|
152
|
+
}
|
|
153
|
+
const parsed = JSON.parse(source);
|
|
154
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
155
|
+
throw new Error("provider credential JSON rejected");
|
|
156
|
+
}
|
|
157
|
+
const target = providerCredentialPath(profile, request.profileId);
|
|
158
|
+
atomicCredential(target, source, profile.runtimeUid, profile.runtimeGid);
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
profileId: request.profileId,
|
|
162
|
+
fingerprint: request.fingerprint,
|
|
163
|
+
path: target,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function removeProviderCredential(profile, profileId) {
|
|
168
|
+
const target = providerCredentialPath(profile, profileId);
|
|
169
|
+
if (existsSync(target)) {
|
|
170
|
+
const link = lstatSync(target);
|
|
171
|
+
if (link.isSymbolicLink() || !link.isFile()) throw new Error("provider credential cleanup rejected");
|
|
172
|
+
rmSync(target, { force: true });
|
|
173
|
+
fsyncDirectory(dirname(target));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function observeProviderCredential(profile, profileId) {
|
|
178
|
+
const target = providerCredentialPath(profile, profileId);
|
|
179
|
+
const link = lstatSync(target);
|
|
180
|
+
if (
|
|
181
|
+
link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 ||
|
|
182
|
+
link.uid !== profile.runtimeUid || link.gid !== profile.runtimeGid
|
|
183
|
+
) throw new Error("provider credential readback rejected");
|
|
184
|
+
return sha256(readFileSync(target, "utf8").trim());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function applyRuntimeOwnership(pathValue, uid, gid) {
|
|
188
|
+
const link = lstatSync(pathValue);
|
|
189
|
+
if (link.isSymbolicLink()) throw new Error("profile symlink rejected");
|
|
190
|
+
if (link.isDirectory()) {
|
|
191
|
+
for (const name of readdirSync(pathValue)) applyRuntimeOwnership(join(pathValue, name), uid, gid);
|
|
192
|
+
chownSync(pathValue, uid, gid);
|
|
193
|
+
chmodSync(pathValue, 0o500);
|
|
194
|
+
} else if (link.isFile()) {
|
|
195
|
+
chownSync(pathValue, uid, gid);
|
|
196
|
+
chmodSync(pathValue, 0o400);
|
|
197
|
+
} else {
|
|
198
|
+
throw new Error("profile entry rejected");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function profileTarget(profile, request, allowedSlots) {
|
|
203
|
+
const root = profileStorageRoot(profile.profilesRoot);
|
|
204
|
+
const slot = request.slot;
|
|
205
|
+
if (!allowedSlots.includes(slot)) throw new Error("profile slot rejected");
|
|
206
|
+
let name;
|
|
207
|
+
if (slot === "ACTIVE") name = request.profileId;
|
|
208
|
+
else if (slot === "BACKUP") name = `${request.profileId}.rollback-prior-good`;
|
|
209
|
+
else {
|
|
210
|
+
if (!SAFE_ID.test(request.revisionId ?? "") || !/^[1-9][0-9]{0,19}$/.test(request.fence ?? "")) {
|
|
211
|
+
throw new Error("profile revision binding rejected");
|
|
212
|
+
}
|
|
213
|
+
name = `${request.profileId}.staging-${request.revisionId}-${request.fence}`;
|
|
214
|
+
}
|
|
215
|
+
const target = join(root, name);
|
|
216
|
+
if (dirname(target) !== root) throw new Error("profile target rejected");
|
|
217
|
+
return { root, target };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function resolveContainmentProfileTarget(profile, request) {
|
|
221
|
+
const active = profileTarget(profile, { ...request, slot: "ACTIVE" }, ["ACTIVE"]);
|
|
222
|
+
const staging = profileTarget(profile, { ...request, slot: "STAGING" }, ["STAGING"]);
|
|
223
|
+
const provided = resolve(request.profileRoot ?? "");
|
|
224
|
+
if (!existsSync(provided) || lstatSync(provided).isSymbolicLink()) {
|
|
225
|
+
throw new Error("containment profile rejected");
|
|
226
|
+
}
|
|
227
|
+
const requested = realpathSync(provided);
|
|
228
|
+
if (requested === active.target) return { ...active, slot: "ACTIVE" };
|
|
229
|
+
if (requested === staging.target) return { ...staging, slot: "STAGING" };
|
|
230
|
+
throw new Error("containment profile rejected");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function observeProfileEntries(profileRoot) {
|
|
234
|
+
const entries = [];
|
|
235
|
+
const walk = (pathValue, logicalPath) => {
|
|
236
|
+
const link = lstatSync(pathValue);
|
|
237
|
+
if (link.isSymbolicLink()) throw new Error("profile symlink rejected");
|
|
238
|
+
const type = link.isDirectory() ? "directory" : link.isFile() ? "file" : "other";
|
|
239
|
+
entries.push({ path: logicalPath, type, uid: link.uid, gid: link.gid, mode: link.mode & 0o777 });
|
|
240
|
+
if (type === "directory") {
|
|
241
|
+
for (const name of readdirSync(pathValue).sort()) {
|
|
242
|
+
walk(join(pathValue, name), logicalPath === "." ? name : `${logicalPath}/${name}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
walk(profileRoot, ".");
|
|
247
|
+
return entries;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function confinedRequest(requestPath, config) {
|
|
251
|
+
const root = privateRoot(config.helperRequestRoot);
|
|
252
|
+
return JSON.parse(readPrivateDirectFile(root, requestPath));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function readPrivateDirectFile(root, pathValue) {
|
|
256
|
+
const value = resolve(pathValue ?? "");
|
|
257
|
+
if (realpathSync(dirname(value)) !== root) throw new Error("private file confinement failed");
|
|
258
|
+
const before = lstatSync(value);
|
|
259
|
+
if (before.isSymbolicLink() || !before.isFile() || (before.mode & 0o777) !== 0o600) {
|
|
260
|
+
throw new Error("private file rejected");
|
|
261
|
+
}
|
|
262
|
+
const descriptor = openSync(value, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
263
|
+
try {
|
|
264
|
+
const after = fstatSync(descriptor);
|
|
265
|
+
if (!after.isFile() || after.dev !== before.dev || after.ino !== before.ino || (after.mode & 0o777) !== 0o600) {
|
|
266
|
+
throw new Error("private file changed during open");
|
|
267
|
+
}
|
|
268
|
+
return readFileSync(descriptor, "utf8");
|
|
269
|
+
} finally {
|
|
270
|
+
closeSync(descriptor);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function sha256(value) {
|
|
275
|
+
return createHash("sha256").update(value).digest("hex");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function stableJson(value) {
|
|
279
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
280
|
+
if (value && typeof value === "object") {
|
|
281
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
282
|
+
}
|
|
283
|
+
return JSON.stringify(value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function exactKeys(value, keys) {
|
|
287
|
+
return Boolean(
|
|
288
|
+
value && typeof value === "object" && !Array.isArray(value) &&
|
|
289
|
+
Object.keys(value).sort().join("\0") === [...keys].sort().join("\0")
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function safeRuntimeStateRoot(profile) {
|
|
294
|
+
const root = privateRoot(profile.runtimeStateRoot);
|
|
295
|
+
const owner = lstatSync(root);
|
|
296
|
+
if (owner.uid !== profile.runtimeUid || owner.gid !== profile.runtimeGid) {
|
|
297
|
+
throw new Error("runtime state ownership rejected");
|
|
298
|
+
}
|
|
299
|
+
return root;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function ensureOwnedRuntimeDirectory(pathValue, root, uid, gid) {
|
|
303
|
+
if (dirname(pathValue) !== root || existsSync(pathValue)) {
|
|
304
|
+
if (!existsSync(pathValue)) throw new Error("runtime directory confinement failed");
|
|
305
|
+
const link = lstatSync(pathValue);
|
|
306
|
+
if (
|
|
307
|
+
link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o777) !== 0o700 ||
|
|
308
|
+
link.uid !== uid || link.gid !== gid
|
|
309
|
+
) throw new Error("runtime directory rejected");
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
mkdirSync(pathValue, { mode: 0o700 });
|
|
313
|
+
const owner = lstatSync(pathValue);
|
|
314
|
+
if (owner.uid !== uid || owner.gid !== gid) chownSync(pathValue, uid, gid);
|
|
315
|
+
chmodSync(pathValue, 0o700);
|
|
316
|
+
fsyncDirectory(root);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function validateActiveManifest(target, request, profile, allowFileDrift = false) {
|
|
320
|
+
const entries = observeProfileEntries(target);
|
|
321
|
+
if (
|
|
322
|
+
JSON.stringify(entries.map(({ path, type }) => [path, type]).sort()) !== JSON.stringify([...PROFILE_ENTRIES].sort()) ||
|
|
323
|
+
entries.some((entry) =>
|
|
324
|
+
entry.uid !== profile.runtimeUid || entry.gid !== profile.runtimeGid ||
|
|
325
|
+
entry.mode !== (entry.type === "directory" ? 0o500 : 0o400)
|
|
326
|
+
)
|
|
327
|
+
) {
|
|
328
|
+
throw new Error("active profile inventory rejected");
|
|
329
|
+
}
|
|
330
|
+
const manifest = JSON.parse(readFileSync(join(target, "agent-profile.json"), "utf8"));
|
|
331
|
+
if (
|
|
332
|
+
manifest.profileId !== request.profileId || manifest.revisionId !== request.revisionId ||
|
|
333
|
+
manifest.profileDigest !== request.profileDigest || !SHA256.test(manifest.profileDigest ?? "") ||
|
|
334
|
+
!SAFE_ID.test(manifest.workspaceId ?? "") || !SAFE_ID.test(manifest.agentId ?? "") ||
|
|
335
|
+
!Number.isSafeInteger(manifest.serviceCredentialGeneration) || manifest.serviceCredentialGeneration < 1 ||
|
|
336
|
+
!SHA256.test(manifest.policyHash ?? "") ||
|
|
337
|
+
!Array.isArray(manifest.slackChannelIds) || manifest.slackChannelIds.length === 0 ||
|
|
338
|
+
manifest.slackChannelIds.some((id) => !/^C[A-Z0-9]{8,20}$/.test(id)) ||
|
|
339
|
+
JSON.stringify(manifest.slackChannelIds) !== JSON.stringify([...new Set(manifest.slackChannelIds)].sort()) ||
|
|
340
|
+
!manifest.slackChannelIds.includes(manifest.slackDefaultChannelId) ||
|
|
341
|
+
!Array.isArray(manifest.slackMemberAllowlist) || manifest.slackMemberAllowlist.length === 0 ||
|
|
342
|
+
manifest.slackMemberAllowlist.some((id) => !/^[UW][A-Z0-9]{8,20}$/.test(id)) ||
|
|
343
|
+
JSON.stringify(manifest.slackMemberAllowlist) !== JSON.stringify([...new Set(manifest.slackMemberAllowlist)].sort()) ||
|
|
344
|
+
!exactKeys(manifest.fileHashes, ["config.yaml", "sellable/mcp.json", "SOUL.md", "skills/sellable/SKILL.md"])
|
|
345
|
+
) throw new Error("active profile manifest rejected");
|
|
346
|
+
for (const [logicalPath, digest] of Object.entries(manifest.fileHashes)) {
|
|
347
|
+
if (!SHA256.test(digest) || (!allowFileDrift && sha256(readFileSync(join(target, logicalPath))) !== digest)) {
|
|
348
|
+
throw new Error("active profile file hash rejected");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const { profileDigest: _profileDigest, ...digestBody } = manifest;
|
|
352
|
+
if (sha256(stableJson(digestBody)) !== manifest.profileDigest) throw new Error("active profile digest rejected");
|
|
353
|
+
return manifest;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function observeRuntimeProfileFiles(profileRoot) {
|
|
357
|
+
const target = resolve(profileRoot ?? "");
|
|
358
|
+
if (!isAbsolute(target) || !existsSync(target)) throw new Error("runtime profile drift root missing");
|
|
359
|
+
const root = lstatSync(target);
|
|
360
|
+
if (root.isSymbolicLink() || !root.isDirectory() || (root.mode & 0o022) !== 0) {
|
|
361
|
+
throw new Error("runtime profile drift root rejected");
|
|
362
|
+
}
|
|
363
|
+
const manifestPath = join(target, "agent-profile.json");
|
|
364
|
+
const manifestLink = lstatSync(manifestPath);
|
|
365
|
+
if (manifestLink.isSymbolicLink() || !manifestLink.isFile() || (manifestLink.mode & 0o022) !== 0) {
|
|
366
|
+
throw new Error("runtime profile drift manifest rejected");
|
|
367
|
+
}
|
|
368
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
369
|
+
if (!exactKeys(manifest.fileHashes, ["config.yaml", "sellable/mcp.json", "SOUL.md", "skills/sellable/SKILL.md"])) {
|
|
370
|
+
throw new Error("runtime profile drift inventory rejected");
|
|
371
|
+
}
|
|
372
|
+
const { profileDigest: _profileDigest, ...manifestBody } = manifest;
|
|
373
|
+
if (!SHA256.test(manifest.profileDigest ?? "") || sha256(stableJson(manifestBody)) !== manifest.profileDigest) {
|
|
374
|
+
throw new Error("runtime profile drift manifest digest rejected");
|
|
375
|
+
}
|
|
376
|
+
const actualFileHashes = Object.fromEntries(
|
|
377
|
+
Object.keys(manifest.fileHashes).map((logicalPath) => {
|
|
378
|
+
const pathValue = join(target, logicalPath);
|
|
379
|
+
const link = lstatSync(pathValue);
|
|
380
|
+
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o022) !== 0) {
|
|
381
|
+
throw new Error("runtime profile drift file rejected");
|
|
382
|
+
}
|
|
383
|
+
return [logicalPath, sha256(readFileSync(pathValue))];
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
const runtimeConfig = JSON.parse(readFileSync(join(target, "config.yaml"), "utf8"));
|
|
387
|
+
const runtimeMcp = JSON.parse(readFileSync(join(target, "sellable", "mcp.json"), "utf8"));
|
|
388
|
+
const tools = runtimeConfig?.tools?.include;
|
|
389
|
+
if (!Array.isArray(tools) || tools.length === 0 || tools.some((tool) => typeof tool !== "string")) {
|
|
390
|
+
throw new Error("runtime profile drift tools rejected");
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
manifest,
|
|
394
|
+
runtimeConfig,
|
|
395
|
+
runtimeMcp,
|
|
396
|
+
tools: [...tools].sort(),
|
|
397
|
+
fileHashes: actualFileHashes,
|
|
398
|
+
profileDigest: sha256(stableJson({ ...manifestBody, fileHashes: actualFileHashes })),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function runtimeServiceEnvLocation(profile) {
|
|
403
|
+
const servicePath = resolve(profile.runtimeServicePath ?? "");
|
|
404
|
+
const envRoot = resolve(profile.runtimeServiceEnvRoot ?? "");
|
|
405
|
+
if (
|
|
406
|
+
!isAbsolute(servicePath) || !existsSync(servicePath) || lstatSync(servicePath).isSymbolicLink() ||
|
|
407
|
+
!lstatSync(servicePath).isDirectory() || (lstatSync(servicePath).mode & 0o022) !== 0 ||
|
|
408
|
+
envRoot !== join(servicePath, "env")
|
|
409
|
+
) throw new Error("runtime service environment path rejected");
|
|
410
|
+
const realServicePath = realpathSync(servicePath);
|
|
411
|
+
if (realpathSync(dirname(envRoot)) !== realServicePath) throw new Error("runtime service environment parent rejected");
|
|
412
|
+
return { servicePath: realServicePath, envRoot: join(realServicePath, "env") };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function observeClosedServiceEnvironment({ envRoot, values }) {
|
|
416
|
+
const ownerUid = process.getuid?.() ?? lstatSync(envRoot).uid;
|
|
417
|
+
const ownerGid = process.getgid?.() ?? lstatSync(envRoot).gid;
|
|
418
|
+
const names = readdirSync(envRoot).sort();
|
|
419
|
+
if (JSON.stringify(names) !== JSON.stringify(Object.keys(values).sort())) throw new Error("runtime service environment inventory rejected");
|
|
420
|
+
for (const key of names) {
|
|
421
|
+
const pathValue = join(envRoot, key);
|
|
422
|
+
const link = lstatSync(pathValue);
|
|
423
|
+
if (
|
|
424
|
+
link.isSymbolicLink() || !link.isFile() || link.uid !== ownerUid || link.gid !== ownerGid ||
|
|
425
|
+
(link.mode & 0o777) !== 0o600 || readFileSync(pathValue, "utf8") !== `${values[key]}\n`
|
|
426
|
+
) throw new Error("runtime service environment readback rejected");
|
|
427
|
+
}
|
|
428
|
+
const root = lstatSync(envRoot);
|
|
429
|
+
if (root.isSymbolicLink() || !root.isDirectory() || root.uid !== ownerUid || root.gid !== ownerGid || (root.mode & 0o777) !== 0o700) {
|
|
430
|
+
throw new Error("runtime service environment root rejected");
|
|
431
|
+
}
|
|
432
|
+
return { keys: names, digest: sha256(stableJson(values)), path: envRoot };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function writeClosedServiceEnvironment({ envRoot, servicePath, request, values }) {
|
|
436
|
+
for (const [key, value] of Object.entries(values)) {
|
|
437
|
+
if (!/^[A-Z][A-Z0-9_]{0,127}$/.test(key) || typeof value !== "string" || !value || /[\0\r\n]/.test(value)) {
|
|
438
|
+
throw new Error("runtime service environment value rejected");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const staging = join(servicePath, `.env.staging-${request.operationId}-${request.fence}`);
|
|
442
|
+
const backup = join(servicePath, ".env.rollback-prior-good");
|
|
443
|
+
for (const pathValue of [staging, backup]) {
|
|
444
|
+
if (dirname(pathValue) !== servicePath || (existsSync(pathValue) && lstatSync(pathValue).isSymbolicLink())) {
|
|
445
|
+
throw new Error("runtime service environment staging rejected");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (existsSync(backup) && !existsSync(envRoot)) renameSync(backup, envRoot);
|
|
449
|
+
rmSync(staging, { recursive: true, force: true });
|
|
450
|
+
rmSync(backup, { recursive: true, force: true });
|
|
451
|
+
mkdirSync(staging, { mode: 0o700 });
|
|
452
|
+
chmodSync(staging, 0o700);
|
|
453
|
+
for (const key of Object.keys(values).sort()) {
|
|
454
|
+
const pathValue = join(staging, key);
|
|
455
|
+
writeFileSync(pathValue, `${values[key]}\n`, { mode: 0o600, flag: "wx" });
|
|
456
|
+
chmodSync(pathValue, 0o600);
|
|
457
|
+
const descriptor = openSync(pathValue, "r");
|
|
458
|
+
try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
|
|
459
|
+
}
|
|
460
|
+
fsyncDirectory(staging);
|
|
461
|
+
if (existsSync(envRoot)) renameSync(envRoot, backup);
|
|
462
|
+
renameSync(staging, envRoot);
|
|
463
|
+
fsyncDirectory(servicePath);
|
|
464
|
+
rmSync(backup, { recursive: true, force: true });
|
|
465
|
+
fsyncDirectory(servicePath);
|
|
466
|
+
return observeClosedServiceEnvironment({ envRoot, values });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function runtimeServiceEnvironmentValues({ profile, profileId, manifest, target, runtimeStateRoot }) {
|
|
470
|
+
const slackRoot = privateRoot(profile.workerRuntimeSecretRoot);
|
|
471
|
+
const mcpRoot = privateRoot(profile.mcpCredentialRoot);
|
|
472
|
+
const hermesRoot = runtimeStateRoot;
|
|
473
|
+
const hermesProfileRoot = join(hermesRoot, "profiles", profileId);
|
|
474
|
+
const runtimeHome = join(hermesProfileRoot, "home");
|
|
475
|
+
return {
|
|
476
|
+
paths: { slackRoot, mcpRoot, runtimeHome, hermesRoot, hermesProfileRoot },
|
|
477
|
+
values: {
|
|
478
|
+
HERMES_HOME: hermesRoot,
|
|
479
|
+
HOME: runtimeHome,
|
|
480
|
+
SELLABLE_AGENT_CREDENTIAL_GENERATION: String(manifest.serviceCredentialGeneration),
|
|
481
|
+
SELLABLE_AGENT_DEFAULT_CHANNEL_ID: manifest.slackDefaultChannelId,
|
|
482
|
+
SELLABLE_AGENT_HERMES_PROFILE_ROOT: hermesProfileRoot,
|
|
483
|
+
SELLABLE_AGENT_ID: manifest.agentId,
|
|
484
|
+
SELLABLE_AGENT_MANAGED_PROFILE_ROOT: target,
|
|
485
|
+
SELLABLE_AGENT_MCP_CREDENTIAL_ROOT: mcpRoot,
|
|
486
|
+
SELLABLE_AGENT_MEMBER_ALLOWLIST: manifest.slackMemberAllowlist.join(","),
|
|
487
|
+
SELLABLE_AGENT_POLICY_HASH: manifest.policyHash,
|
|
488
|
+
SELLABLE_AGENT_PROFILE_ID: profileId,
|
|
489
|
+
SELLABLE_AGENT_RUNTIME_ENV_LOADED: "1",
|
|
490
|
+
SELLABLE_AGENT_RUNTIME_GATE_FILE: "/runtime/gates.json",
|
|
491
|
+
SELLABLE_AGENT_RUNTIME_SECRET_ROOT: mcpRoot,
|
|
492
|
+
SELLABLE_AGENT_SELECTED_CHANNEL_IDS: JSON.stringify(manifest.slackChannelIds),
|
|
493
|
+
SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE: join(mcpRoot, profileId, "service-credential"),
|
|
494
|
+
SELLABLE_AGENT_WORKER_SLACK_SECRET_ROOT: slackRoot,
|
|
495
|
+
SELLABLE_AGENT_WORKSPACE_ID: manifest.workspaceId,
|
|
496
|
+
SELLABLE_CONFIG_PATH: `/runtime/profiles/${profileId}/sellable/mcp.json`,
|
|
497
|
+
SELLABLE_LOCK_WORKSPACE_ID: manifest.workspaceId,
|
|
498
|
+
SELLABLE_REQUIRE_WORKSPACE_LOCK: "1",
|
|
499
|
+
SLACK_ALLOWED_CHANNELS: manifest.slackChannelIds.join(","),
|
|
500
|
+
SLACK_ALLOWED_USERS: manifest.slackMemberAllowlist.join(","),
|
|
501
|
+
SLACK_HOME_CHANNEL: manifest.slackDefaultChannelId,
|
|
502
|
+
SLACK_REQUIRE_MENTION: "true",
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function expectedRuntimeGates(lifecycle) {
|
|
508
|
+
if (lifecycle === "ON") return { lifecycle, dispatch: "ENABLED", route: "OPEN", output: "OPEN" };
|
|
509
|
+
if (lifecycle === "OFF") return { lifecycle, dispatch: "FENCED", route: "OPEN", output: "OPEN" };
|
|
510
|
+
if (["ARCHIVING", "ARCHIVED"].includes(lifecycle)) {
|
|
511
|
+
return { lifecycle: "ARCHIVED", dispatch: "FENCED", route: "FENCED", output: "FENCED" };
|
|
512
|
+
}
|
|
513
|
+
throw new Error("runtime lifecycle rejected");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function writeRuntimeGates(profile, lifecycle) {
|
|
517
|
+
const root = safeRuntimeStateRoot(profile);
|
|
518
|
+
const gates = expectedRuntimeGates(lifecycle);
|
|
519
|
+
const pathValue = join(root, "gates.json");
|
|
520
|
+
atomicCredential(pathValue, JSON.stringify(gates), profile.runtimeUid, profile.runtimeGid);
|
|
521
|
+
const parsed = JSON.parse(readFileSync(pathValue, "utf8"));
|
|
522
|
+
if (stableJson(parsed) !== stableJson(gates)) throw new Error("runtime gates readback rejected");
|
|
523
|
+
return { path: pathValue, gates };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function observeRuntimeGates(profile, lifecycle) {
|
|
527
|
+
const root = safeRuntimeStateRoot(profile);
|
|
528
|
+
const pathValue = join(root, "gates.json");
|
|
529
|
+
const link = lstatSync(pathValue);
|
|
530
|
+
const parsed = JSON.parse(readFileSync(pathValue, "utf8"));
|
|
531
|
+
const expected = expectedRuntimeGates(lifecycle);
|
|
532
|
+
if (
|
|
533
|
+
link.isSymbolicLink() || !link.isFile() || link.uid !== profile.runtimeUid ||
|
|
534
|
+
link.gid !== profile.runtimeGid || (link.mode & 0o777) !== 0o600 ||
|
|
535
|
+
stableJson(parsed) !== stableJson(expected)
|
|
536
|
+
) throw new Error("runtime gates observation rejected");
|
|
537
|
+
return { path: pathValue, gates: parsed };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function safePinnedFile(pathValue, label) {
|
|
541
|
+
const resolved = resolve(pathValue ?? "");
|
|
542
|
+
if (!isAbsolute(resolved) || !existsSync(resolved)) throw new Error(`${label} missing`);
|
|
543
|
+
const link = lstatSync(resolved);
|
|
544
|
+
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o022) !== 0) throw new Error(`${label} rejected`);
|
|
545
|
+
return resolved;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function externalTarget(pathRoot, absolutePath) {
|
|
549
|
+
const configuredRoot = resolve(pathRoot ?? "/");
|
|
550
|
+
const root = existsSync(configuredRoot) ? realpathSync(configuredRoot) : configuredRoot;
|
|
551
|
+
const target = root === "/" ? resolve(absolutePath) : join(root, String(absolutePath).replace(/^\/+/, ""));
|
|
552
|
+
const confined = relative(root, target);
|
|
553
|
+
if (!isAbsolute(root) || !isAbsolute(absolutePath) || confined.startsWith("..") || isAbsolute(confined)) {
|
|
554
|
+
throw new Error("external runtime path confinement rejected");
|
|
555
|
+
}
|
|
556
|
+
return target;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function observeImmutableDirectory(pathValue, ownerUid, ownerGid) {
|
|
560
|
+
const link = lstatSync(pathValue);
|
|
561
|
+
if (
|
|
562
|
+
link.isSymbolicLink() || !link.isDirectory() || link.uid !== ownerUid || link.gid !== ownerGid ||
|
|
563
|
+
(link.mode & 0o022) !== 0 || realpathSync(pathValue) !== pathValue
|
|
564
|
+
) throw new Error("external runtime directory rejected");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function walkExternalRoot(root, ownerUid, ownerGid) {
|
|
568
|
+
const files = [];
|
|
569
|
+
const directories = [];
|
|
570
|
+
const visit = (directory, logicalDirectory) => {
|
|
571
|
+
observeImmutableDirectory(directory, ownerUid, ownerGid);
|
|
572
|
+
if (logicalDirectory) directories.push({ path: logicalDirectory, mode: lstatSync(directory).mode & 0o777 });
|
|
573
|
+
for (const name of readdirSync(directory).sort()) {
|
|
574
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
|
|
575
|
+
throw new Error("external runtime inventory name rejected");
|
|
576
|
+
}
|
|
577
|
+
const pathValue = join(directory, name);
|
|
578
|
+
const logicalPath = logicalDirectory ? `${logicalDirectory}/${name}` : name;
|
|
579
|
+
const link = lstatSync(pathValue);
|
|
580
|
+
if (link.isSymbolicLink()) throw new Error("external runtime symlink rejected");
|
|
581
|
+
if (link.isDirectory()) visit(pathValue, logicalPath);
|
|
582
|
+
else if (link.isFile()) files.push({ logicalPath, pathValue, link });
|
|
583
|
+
else throw new Error("external runtime special file rejected");
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
visit(root, "");
|
|
587
|
+
const byPath = (left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
|
|
588
|
+
const byLogicalPath = (left, right) => left.logicalPath < right.logicalPath ? -1 : left.logicalPath > right.logicalPath ? 1 : 0;
|
|
589
|
+
return { directories: directories.sort(byPath), files: files.sort(byLogicalPath) };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function observeExternalRuntimeClosure(input = {}) {
|
|
593
|
+
const pathRoot = resolve(input.pathRoot ?? "/");
|
|
594
|
+
const ownerUid = input.ownerUid ?? 0;
|
|
595
|
+
const ownerGid = input.ownerGid ?? 0;
|
|
596
|
+
const manifestHostPath = resolve(input.manifestPath ?? EXTERNAL_RUNTIME_MANIFEST_PATH);
|
|
597
|
+
const expectedClosureRoot = resolve(input.expectedClosureRoot ?? EXTERNAL_RUNTIME_CLOSURE_ROOT);
|
|
598
|
+
const trustRoot = resolve(input.trustRoot ?? dirname(expectedClosureRoot));
|
|
599
|
+
if (
|
|
600
|
+
!Number.isSafeInteger(ownerUid) || ownerUid < 0 || !Number.isSafeInteger(ownerGid) || ownerGid < 0 ||
|
|
601
|
+
manifestHostPath !== join(expectedClosureRoot, "manifest.json") ||
|
|
602
|
+
relative(trustRoot, expectedClosureRoot).startsWith("..")
|
|
603
|
+
) throw new Error("external runtime closure input rejected");
|
|
604
|
+
const mappedTrustRoot = externalTarget(pathRoot, trustRoot);
|
|
605
|
+
const mappedClosureRoot = externalTarget(pathRoot, expectedClosureRoot);
|
|
606
|
+
const mappedManifestPath = externalTarget(pathRoot, manifestHostPath);
|
|
607
|
+
observeImmutableDirectory(mappedTrustRoot, ownerUid, ownerGid);
|
|
608
|
+
const trustRelative = relative(mappedTrustRoot, mappedClosureRoot);
|
|
609
|
+
let current = mappedTrustRoot;
|
|
610
|
+
for (const component of trustRelative.split(/[/\\]+/).filter(Boolean)) {
|
|
611
|
+
current = join(current, component);
|
|
612
|
+
observeImmutableDirectory(current, ownerUid, ownerGid);
|
|
613
|
+
}
|
|
614
|
+
const manifestLink = lstatSync(mappedManifestPath);
|
|
615
|
+
if (
|
|
616
|
+
manifestLink.isSymbolicLink() || !manifestLink.isFile() || manifestLink.uid !== ownerUid ||
|
|
617
|
+
manifestLink.gid !== ownerGid || (manifestLink.mode & 0o777) !== 0o444
|
|
618
|
+
) throw new Error("external runtime manifest rejected");
|
|
619
|
+
if (
|
|
620
|
+
JSON.stringify(readdirSync(mappedClosureRoot).sort()) !==
|
|
621
|
+
JSON.stringify(["hermes", "manifest.json", "mcp"])
|
|
622
|
+
) throw new Error("external runtime closure root inventory rejected");
|
|
623
|
+
const manifestBytes = readFileSync(mappedManifestPath);
|
|
624
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
625
|
+
if (
|
|
626
|
+
!exactKeys(manifest, ["version", "closureRoot", "roots"]) ||
|
|
627
|
+
manifest.version !== EXTERNAL_RUNTIME_MANIFEST_VERSION ||
|
|
628
|
+
manifest.closureRoot !== expectedClosureRoot || !Array.isArray(manifest.roots) ||
|
|
629
|
+
JSON.stringify(manifest.roots.map((root) => root?.kind)) !== JSON.stringify(["mcp", "hermes"])
|
|
630
|
+
) throw new Error("external runtime manifest schema rejected");
|
|
631
|
+
const observations = [];
|
|
632
|
+
for (const rootRecord of manifest.roots) {
|
|
633
|
+
if (
|
|
634
|
+
!exactKeys(rootRecord, ["kind", "entrypoint", "rootMode", "directories", "files"]) ||
|
|
635
|
+
!["mcp", "hermes"].includes(rootRecord.kind) ||
|
|
636
|
+
rootRecord.rootMode !== 0o555 ||
|
|
637
|
+
typeof rootRecord.entrypoint !== "string" || !Array.isArray(rootRecord.directories) ||
|
|
638
|
+
!Array.isArray(rootRecord.files)
|
|
639
|
+
) throw new Error("external runtime root schema rejected");
|
|
640
|
+
const hostRoot = join(expectedClosureRoot, rootRecord.kind);
|
|
641
|
+
const mappedRoot = externalTarget(pathRoot, hostRoot);
|
|
642
|
+
if ((lstatSync(mappedRoot).mode & 0o777) !== rootRecord.rootMode) {
|
|
643
|
+
throw new Error("external runtime root mode rejected");
|
|
644
|
+
}
|
|
645
|
+
const walked = walkExternalRoot(mappedRoot, ownerUid, ownerGid);
|
|
646
|
+
if (
|
|
647
|
+
JSON.stringify(rootRecord.directories.map((directory) => directory?.path)) !==
|
|
648
|
+
JSON.stringify(rootRecord.directories.map((directory) => directory?.path).sort()) ||
|
|
649
|
+
new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length
|
|
650
|
+
) throw new Error("external runtime directory order rejected");
|
|
651
|
+
if (
|
|
652
|
+
rootRecord.directories.some((directory) =>
|
|
653
|
+
!exactKeys(directory, ["path", "mode"]) ||
|
|
654
|
+
!/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(directory.path ?? "") ||
|
|
655
|
+
directory.mode !== 0o555
|
|
656
|
+
)
|
|
657
|
+
) throw new Error("external runtime directory schema rejected");
|
|
658
|
+
if (JSON.stringify(walked.directories) !== JSON.stringify(rootRecord.directories)) {
|
|
659
|
+
throw new Error("external runtime directory inventory rejected");
|
|
660
|
+
}
|
|
661
|
+
if (
|
|
662
|
+
rootRecord.files.length === 0 ||
|
|
663
|
+
JSON.stringify(rootRecord.files.map((file) => file?.path)) !==
|
|
664
|
+
JSON.stringify(rootRecord.files.map((file) => file?.path).sort()) ||
|
|
665
|
+
new Set(rootRecord.files.map((file) => file?.path)).size !== rootRecord.files.length
|
|
666
|
+
) throw new Error("external runtime file order rejected");
|
|
667
|
+
const actualPaths = walked.files.map((file) => file.logicalPath);
|
|
668
|
+
if (JSON.stringify(actualPaths) !== JSON.stringify(rootRecord.files.map((file) => file.path))) {
|
|
669
|
+
throw new Error("external runtime open inventory rejected");
|
|
670
|
+
}
|
|
671
|
+
const observedFiles = rootRecord.files.map((file, index) => {
|
|
672
|
+
if (
|
|
673
|
+
!exactKeys(file, ["path", "mode", "digest"]) ||
|
|
674
|
+
!/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(file.path ?? "") ||
|
|
675
|
+
![0o444, 0o555].includes(file.mode) || !SHA256.test(file.digest ?? "")
|
|
676
|
+
) throw new Error("external runtime file schema rejected");
|
|
677
|
+
const actual = walked.files[index];
|
|
678
|
+
const digest = sha256(readFileSync(actual.pathValue));
|
|
679
|
+
if (
|
|
680
|
+
actual.link.uid !== ownerUid || actual.link.gid !== ownerGid ||
|
|
681
|
+
(actual.link.mode & 0o777) !== file.mode || digest !== file.digest
|
|
682
|
+
) throw new Error("external runtime file drift rejected");
|
|
683
|
+
return { path: file.path, mode: file.mode, digest };
|
|
684
|
+
});
|
|
685
|
+
if (!rootRecord.files.some((file) => file.path === rootRecord.entrypoint && file.mode === 0o555)) {
|
|
686
|
+
throw new Error("external runtime entrypoint rejected");
|
|
687
|
+
}
|
|
688
|
+
observations.push({
|
|
689
|
+
kind: rootRecord.kind,
|
|
690
|
+
root: hostRoot,
|
|
691
|
+
entrypoint: join(hostRoot, rootRecord.entrypoint),
|
|
692
|
+
rootMode: rootRecord.rootMode,
|
|
693
|
+
directories: [...rootRecord.directories],
|
|
694
|
+
files: observedFiles,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const expectedEntrypoints = input.expectedEntrypoints ?? {
|
|
698
|
+
mcp: EXTERNAL_MCP_ENTRYPOINT,
|
|
699
|
+
hermes: EXTERNAL_HERMES_ENTRYPOINT,
|
|
700
|
+
};
|
|
701
|
+
for (const observed of observations) {
|
|
702
|
+
if (observed.entrypoint !== expectedEntrypoints[observed.kind]) {
|
|
703
|
+
throw new Error("external runtime executed entrypoint rejected");
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
version: EXTERNAL_RUNTIME_MANIFEST_VERSION,
|
|
708
|
+
manifestDigest: sha256(manifestBytes),
|
|
709
|
+
roots: observations,
|
|
710
|
+
closureDigest: sha256(stableJson({ manifest: sha256(manifestBytes), roots: observations })),
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function runtimePackageHash(profile) {
|
|
715
|
+
const manifestPath = safePinnedFile(profile.installedPackageManifestPath, "installed package manifest");
|
|
716
|
+
const manifestBytes = readFileSync(manifestPath);
|
|
717
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
718
|
+
if (manifest?.version !== "sellable-agent-installed-closure/v1" || !Array.isArray(manifest.files)) {
|
|
719
|
+
throw new Error("installed package manifest rejected");
|
|
720
|
+
}
|
|
721
|
+
const external = observeExternalRuntimeClosure({
|
|
722
|
+
manifestPath: profile.externalRuntimeManifestPath,
|
|
723
|
+
pathRoot: profile.externalRuntimePathRoot,
|
|
724
|
+
trustRoot: profile.externalRuntimeTrustRoot,
|
|
725
|
+
expectedClosureRoot: profile.externalRuntimeClosureRoot,
|
|
726
|
+
ownerUid: profile.externalRuntimeOwnerUid,
|
|
727
|
+
ownerGid: profile.externalRuntimeOwnerGid,
|
|
728
|
+
expectedEntrypoints: { mcp: profile.mcpCommand, hermes: profile.hermesExecutablePath },
|
|
729
|
+
});
|
|
730
|
+
return sha256(stableJson({
|
|
731
|
+
installedManifest: sha256(manifestBytes),
|
|
732
|
+
externalRuntimeClosure: external.closureDigest,
|
|
733
|
+
hermesBridge: HERMES_AGENT_BRIDGE_CONTRACT.contractDigest,
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function runtimeBoundaryInput({ profile, profileId, manifest, target, runtimeStateRoot }) {
|
|
738
|
+
const proxyExecutablePath = safePinnedFile(profile.proxyExecutablePath, "proxy executable");
|
|
739
|
+
const proxyConfigPath = safePinnedFile(profile.proxyConfigPath, "proxy config");
|
|
740
|
+
const hermesExecutablePath = safePinnedFile(profile.hermesExecutablePath, "Hermes executable");
|
|
741
|
+
const proxyConfigBytes = readFileSync(proxyConfigPath);
|
|
742
|
+
const proxy = validateRuntimeEgressProxyConfig(JSON.parse(proxyConfigBytes.toString("utf8")));
|
|
743
|
+
if (
|
|
744
|
+
!proxy.ok || proxy.config.uid !== profile.proxyUid || proxy.config.gid !== profile.proxyGid ||
|
|
745
|
+
proxy.config.listenPort !== profile.proxyPort
|
|
746
|
+
) throw new Error("proxy runtime identity rejected");
|
|
747
|
+
const hermesSourceRoot = immutableSourceRoot(profile.hermesSourceRoot);
|
|
748
|
+
return {
|
|
749
|
+
profileId,
|
|
750
|
+
workspaceId: manifest.workspaceId,
|
|
751
|
+
agentId: manifest.agentId,
|
|
752
|
+
runtimeUid: profile.runtimeUid,
|
|
753
|
+
runtimeGid: profile.runtimeGid,
|
|
754
|
+
proxyUid: profile.proxyUid,
|
|
755
|
+
proxyGid: profile.proxyGid,
|
|
756
|
+
proxyPort: profile.proxyPort,
|
|
757
|
+
proxyExecutablePath,
|
|
758
|
+
proxyExecutableDigest: sha256(readFileSync(proxyExecutablePath)),
|
|
759
|
+
proxyConfigPath,
|
|
760
|
+
proxyConfigDigest: sha256(proxyConfigBytes),
|
|
761
|
+
activeProfileRoot: target,
|
|
762
|
+
runtimeStateRoot,
|
|
763
|
+
mcpCredentialRoot: privateRoot(profile.mcpCredentialRoot),
|
|
764
|
+
hermesSourceRoot,
|
|
765
|
+
hermesExecutablePath,
|
|
766
|
+
hermesExecutableDigest: sha256(readFileSync(hermesExecutablePath)),
|
|
767
|
+
mcpExecutablePath: profile.mcpCommand,
|
|
768
|
+
bubblewrapPath: profile.bubblewrapPath,
|
|
769
|
+
setprivPath: profile.setprivPath,
|
|
770
|
+
nftPath: profile.nftPath,
|
|
771
|
+
cgroupRoot: profile.cgroupRoot,
|
|
772
|
+
serviceCredentialGeneration: manifest.serviceCredentialGeneration,
|
|
773
|
+
policyHash: manifest.policyHash,
|
|
774
|
+
selectedChannelIds: [...manifest.slackChannelIds],
|
|
775
|
+
defaultChannelId: manifest.slackDefaultChannelId,
|
|
776
|
+
memberAllowlist: [...manifest.slackMemberAllowlist],
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function writeRuntimeBoundaryConfig(profile, input) {
|
|
781
|
+
const built = buildRuntimeBoundaryContract(input);
|
|
782
|
+
if (!built.ok) throw new Error("runtime boundary contract rejected");
|
|
783
|
+
const pathValue = resolve(profile.runtimeBoundaryConfigPath ?? "");
|
|
784
|
+
if (!isAbsolute(pathValue) || basename(pathValue) !== "runtime-boundary.json") {
|
|
785
|
+
throw new Error("runtime boundary config path rejected");
|
|
786
|
+
}
|
|
787
|
+
if (process.getuid?.() === 0 && pathValue !== "/etc/sellable-agent/runtime-boundary.json") {
|
|
788
|
+
throw new Error("live runtime boundary config path rejected");
|
|
789
|
+
}
|
|
790
|
+
const parent = dirname(pathValue);
|
|
791
|
+
const parentLink = lstatSync(parent);
|
|
792
|
+
const ownerUid = process.getuid?.() === 0 ? 0 : (process.getuid?.() ?? parentLink.uid);
|
|
793
|
+
const ownerGid = process.getuid?.() === 0 ? 0 : (process.getgid?.() ?? parentLink.gid);
|
|
794
|
+
if (
|
|
795
|
+
parentLink.isSymbolicLink() || !parentLink.isDirectory() || (parentLink.mode & 0o022) !== 0 ||
|
|
796
|
+
parentLink.uid !== ownerUid || parentLink.gid !== ownerGid || realpathSync(parent) !== parent
|
|
797
|
+
) throw new Error("runtime boundary config parent rejected");
|
|
798
|
+
if (existsSync(pathValue) && lstatSync(pathValue).isSymbolicLink()) throw new Error("runtime boundary config symlink rejected");
|
|
799
|
+
const bytes = `${JSON.stringify(input, null, 2)}\n`;
|
|
800
|
+
const temp = join(parent, `.runtime-boundary.${randomUUID()}.tmp`);
|
|
801
|
+
writeFileSync(temp, bytes, { mode: 0o400, flag: "wx" });
|
|
802
|
+
chmodSync(temp, 0o400);
|
|
803
|
+
const descriptor = openSync(temp, "r");
|
|
804
|
+
try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
|
|
805
|
+
renameSync(temp, pathValue);
|
|
806
|
+
fsyncDirectory(parent);
|
|
807
|
+
const link = lstatSync(pathValue);
|
|
808
|
+
if (
|
|
809
|
+
link.isSymbolicLink() || !link.isFile() || link.uid !== ownerUid || link.gid !== ownerGid ||
|
|
810
|
+
(link.mode & 0o777) !== 0o400 || readFileSync(pathValue, "utf8") !== bytes
|
|
811
|
+
) throw new Error("runtime boundary config readback rejected");
|
|
812
|
+
const readback = buildRuntimeBoundaryContract(JSON.parse(readFileSync(pathValue, "utf8")));
|
|
813
|
+
if (!readback.ok || readback.contract.contractDigest !== built.contract.contractDigest) {
|
|
814
|
+
throw new Error("runtime boundary contract readback rejected");
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
path: pathValue,
|
|
818
|
+
contractDigest: built.contract.contractDigest,
|
|
819
|
+
fileDigest: sha256(bytes),
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function observeRuntimeBoundaryConfig(profile, input) {
|
|
824
|
+
const pathValue = resolve(profile.runtimeBoundaryConfigPath ?? "");
|
|
825
|
+
const link = lstatSync(pathValue);
|
|
826
|
+
const parent = lstatSync(dirname(pathValue));
|
|
827
|
+
const ownerUid = process.getuid?.() === 0 ? 0 : (process.getuid?.() ?? link.uid);
|
|
828
|
+
const ownerGid = process.getuid?.() === 0 ? 0 : (process.getgid?.() ?? link.gid);
|
|
829
|
+
if (
|
|
830
|
+
parent.isSymbolicLink() || !parent.isDirectory() || parent.uid !== ownerUid || parent.gid !== ownerGid ||
|
|
831
|
+
(parent.mode & 0o022) !== 0 || realpathSync(dirname(pathValue)) !== dirname(pathValue) ||
|
|
832
|
+
link.isSymbolicLink() || !link.isFile() || link.uid !== ownerUid || link.gid !== ownerGid ||
|
|
833
|
+
(link.mode & 0o777) !== 0o400
|
|
834
|
+
) throw new Error("runtime boundary config observation rejected");
|
|
835
|
+
const parsed = JSON.parse(readFileSync(pathValue, "utf8"));
|
|
836
|
+
if (stableJson(parsed) !== stableJson(input)) throw new Error("runtime boundary config drift rejected");
|
|
837
|
+
const built = buildRuntimeBoundaryContract(parsed);
|
|
838
|
+
if (!built.ok) throw new Error("runtime boundary config contract rejected");
|
|
839
|
+
return built.contract;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function runtimePidFromCgroup(contract, procRoot) {
|
|
843
|
+
const procsPath = join(contract.paths.cgroupRoot, contract.profileId, "cgroup.procs");
|
|
844
|
+
const candidates = readFileSync(procsPath, "utf8").split(/\s+/).filter((value) => /^[1-9][0-9]{0,9}$/.test(value));
|
|
845
|
+
const matches = candidates.filter((pidText) => {
|
|
846
|
+
try {
|
|
847
|
+
const status = readFileSync(join(procRoot, pidText, "status"), "utf8");
|
|
848
|
+
const uid = Number(status.match(/^Uid:\s+(\d+)/m)?.[1]);
|
|
849
|
+
const cmdline = readFileSync(join(procRoot, pidText, "cmdline")).toString("utf8").split("\0").filter(Boolean);
|
|
850
|
+
return uid === contract.runtime.uid && cmdline.includes(contract.paths.hermesExecutablePath);
|
|
851
|
+
} catch {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
if (matches.length !== 1) throw new Error("runtime cgroup process rejected");
|
|
856
|
+
return Number(matches[0]);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function readPinnedPid(pathValue) {
|
|
860
|
+
const resolved = resolve(pathValue ?? "");
|
|
861
|
+
const link = lstatSync(resolved);
|
|
862
|
+
const value = readFileSync(resolved, "utf8").trim();
|
|
863
|
+
if (link.isSymbolicLink() || !link.isFile() || !/^[1-9][0-9]{0,9}$/.test(value)) {
|
|
864
|
+
throw new Error("supervised pid rejected");
|
|
865
|
+
}
|
|
866
|
+
return Number(value);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function proxyListens(host, port) {
|
|
870
|
+
return new Promise((resolveListen) => {
|
|
871
|
+
const socket = createConnection({ host, port });
|
|
872
|
+
const finish = (value) => {
|
|
873
|
+
socket.removeAllListeners();
|
|
874
|
+
socket.destroy();
|
|
875
|
+
resolveListen(value);
|
|
876
|
+
};
|
|
877
|
+
socket.setTimeout(2_000, () => finish(false));
|
|
878
|
+
socket.once("error", () => finish(false));
|
|
879
|
+
socket.once("connect", () => finish(true));
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async function observeKernelRuntimeBoundary(profile, contract, procRoot) {
|
|
884
|
+
const runtimePid = runtimePidFromCgroup(contract, procRoot);
|
|
885
|
+
const proxyPid = readPinnedPid(profile.proxyPidPath);
|
|
886
|
+
const nft = await runBoundedChildProcess({
|
|
887
|
+
command: contract.paths.nftPath,
|
|
888
|
+
args: ["list", "table", "inet", contract.firewall.table],
|
|
889
|
+
env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" },
|
|
890
|
+
shell: false,
|
|
891
|
+
timeoutMs: 10_000,
|
|
892
|
+
maxOutputBytes: 128 * 1024,
|
|
893
|
+
});
|
|
894
|
+
if (nft.exitCode !== 0 || nft.timedOut || nft.outputLimitExceeded || nft.stderr.trim()) {
|
|
895
|
+
throw new Error("runtime firewall readback failed");
|
|
896
|
+
}
|
|
897
|
+
const listenObserved = await proxyListens(contract.proxy.host, contract.proxy.port);
|
|
898
|
+
const proxyObservation = observeRuntimeEgressProxy({
|
|
899
|
+
configPath: contract.proxy.configPath,
|
|
900
|
+
executablePath: contract.proxy.executablePath,
|
|
901
|
+
pid: proxyPid,
|
|
902
|
+
procRoot,
|
|
903
|
+
listenObserved,
|
|
904
|
+
});
|
|
905
|
+
if (!proxyObservation.ok) throw new Error("runtime proxy readback failed");
|
|
906
|
+
const boundary = observeRuntimeBoundary({
|
|
907
|
+
contract,
|
|
908
|
+
pid: runtimePid,
|
|
909
|
+
procRoot,
|
|
910
|
+
cgroupRoot: contract.paths.cgroupRoot,
|
|
911
|
+
nftRuleset: nft.stdout,
|
|
912
|
+
proxyObservation,
|
|
913
|
+
});
|
|
914
|
+
if (!boundary.ok) throw new Error("runtime boundary readback failed");
|
|
915
|
+
return { runtimePid, boundary };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function installRuntimeServiceEnvironment(profile, request) {
|
|
919
|
+
const { target } = profileTarget(profile, { ...request, slot: "ACTIVE" }, ["ACTIVE"]);
|
|
920
|
+
if (!existsSync(target)) throw new Error("active profile absent");
|
|
921
|
+
const manifest = validateActiveManifest(target, request, profile);
|
|
922
|
+
const runtimeStateRoot = safeRuntimeStateRoot(profile);
|
|
923
|
+
writeRuntimeGates(profile, "ON");
|
|
924
|
+
const { paths, values } = runtimeServiceEnvironmentValues({
|
|
925
|
+
profile, profileId: request.profileId, manifest, target, runtimeStateRoot,
|
|
926
|
+
});
|
|
927
|
+
const { runtimeHome, hermesRoot } = paths;
|
|
928
|
+
const { servicePath, envRoot } = runtimeServiceEnvLocation(profile);
|
|
929
|
+
const hermesProfilesRoot = join(hermesRoot, "profiles");
|
|
930
|
+
ensureOwnedRuntimeDirectory(hermesProfilesRoot, hermesRoot, profile.runtimeUid, profile.runtimeGid);
|
|
931
|
+
const hermesProfileRoot = join(hermesProfilesRoot, request.profileId);
|
|
932
|
+
ensureOwnedRuntimeDirectory(hermesProfileRoot, hermesProfilesRoot, profile.runtimeUid, profile.runtimeGid);
|
|
933
|
+
ensureOwnedRuntimeDirectory(runtimeHome, hermesProfileRoot, profile.runtimeUid, profile.runtimeGid);
|
|
934
|
+
const installed = writeClosedServiceEnvironment({ envRoot, servicePath, request, values });
|
|
935
|
+
const boundaryInput = runtimeBoundaryInput({
|
|
936
|
+
profile, profileId: request.profileId, manifest, target, runtimeStateRoot,
|
|
937
|
+
});
|
|
938
|
+
const boundary = writeRuntimeBoundaryConfig(profile, boundaryInput);
|
|
939
|
+
return {
|
|
940
|
+
ok: true,
|
|
941
|
+
profileId: request.profileId,
|
|
942
|
+
revisionId: request.revisionId,
|
|
943
|
+
profileDigest: request.profileDigest,
|
|
944
|
+
envDigest: installed.digest,
|
|
945
|
+
envKeys: installed.keys,
|
|
946
|
+
envPath: installed.path,
|
|
947
|
+
boundaryConfigPath: boundary.path,
|
|
948
|
+
boundaryContractDigest: boundary.contractDigest,
|
|
949
|
+
boundaryFileDigest: boundary.fileDigest,
|
|
950
|
+
hermesRoot,
|
|
951
|
+
hermesProfileRoot,
|
|
952
|
+
runtimeHome,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function observedSecret(pathValue, uid, gid) {
|
|
957
|
+
const link = lstatSync(pathValue);
|
|
958
|
+
if (
|
|
959
|
+
link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 ||
|
|
960
|
+
link.uid !== uid || link.gid !== gid
|
|
961
|
+
) throw new Error("runtime secret readback rejected");
|
|
962
|
+
const secret = readFileSync(pathValue, "utf8").trim();
|
|
963
|
+
if (!secret) throw new Error("runtime secret absent");
|
|
964
|
+
return sha256(secret).slice(0, 16);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async function observeRuntime(profile, request) {
|
|
968
|
+
const driftMode = request.action === "OBSERVE_RUNTIME_DRIFT";
|
|
969
|
+
if (
|
|
970
|
+
!Number.isSafeInteger(profile.workerUid) || profile.workerUid < 1 ||
|
|
971
|
+
!Number.isSafeInteger(profile.workerGid) || profile.workerGid < 1 ||
|
|
972
|
+
profile.workerUid === profile.runtimeUid
|
|
973
|
+
) throw new Error("worker profile identity rejected");
|
|
974
|
+
const { target } = profileTarget(profile, { ...request, slot: "ACTIVE" }, ["ACTIVE"]);
|
|
975
|
+
if (!existsSync(target)) throw new Error("active profile absent");
|
|
976
|
+
const entries = observeProfileEntries(target);
|
|
977
|
+
if (
|
|
978
|
+
JSON.stringify(entries.map(({ path, type }) => [path, type]).sort()) !== JSON.stringify([...PROFILE_ENTRIES].sort()) ||
|
|
979
|
+
entries.some((entry) =>
|
|
980
|
+
entry.uid !== profile.runtimeUid || entry.gid !== profile.runtimeGid ||
|
|
981
|
+
entry.mode !== (entry.type === "directory" ? 0o500 : 0o400)
|
|
982
|
+
)
|
|
983
|
+
) throw new Error("active profile readback rejected");
|
|
984
|
+
const manifestCandidate = JSON.parse(readFileSync(join(target, "agent-profile.json"), "utf8"));
|
|
985
|
+
const manifest = validateActiveManifest(target, {
|
|
986
|
+
...request,
|
|
987
|
+
revisionId: manifestCandidate.revisionId,
|
|
988
|
+
profileDigest: manifestCandidate.profileDigest,
|
|
989
|
+
}, profile, driftMode);
|
|
990
|
+
if (manifest.profileId !== request.profileId) throw new Error("active profile identity rejected");
|
|
991
|
+
const runtimeStateRoot = safeRuntimeStateRoot(profile);
|
|
992
|
+
const runtimeService = runtimeServiceEnvironmentValues({
|
|
993
|
+
profile, profileId: request.profileId, manifest, target, runtimeStateRoot,
|
|
994
|
+
});
|
|
995
|
+
for (const pathValue of [
|
|
996
|
+
runtimeService.paths.runtimeHome,
|
|
997
|
+
runtimeService.paths.hermesRoot,
|
|
998
|
+
join(runtimeService.paths.hermesRoot, "profiles"),
|
|
999
|
+
runtimeService.paths.hermesProfileRoot,
|
|
1000
|
+
]) {
|
|
1001
|
+
const link = lstatSync(pathValue);
|
|
1002
|
+
if (
|
|
1003
|
+
link.isSymbolicLink() || !link.isDirectory() || link.uid !== profile.runtimeUid ||
|
|
1004
|
+
link.gid !== profile.runtimeGid || (link.mode & 0o777) !== 0o700
|
|
1005
|
+
) throw new Error("runtime Hermes layout rejected");
|
|
1006
|
+
}
|
|
1007
|
+
let environmentDrift = false;
|
|
1008
|
+
try {
|
|
1009
|
+
observeClosedServiceEnvironment({
|
|
1010
|
+
envRoot: runtimeServiceEnvLocation(profile).envRoot,
|
|
1011
|
+
values: runtimeService.values,
|
|
1012
|
+
});
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
if (!driftMode) throw error;
|
|
1015
|
+
environmentDrift = true;
|
|
1016
|
+
}
|
|
1017
|
+
const boundaryInput = runtimeBoundaryInput({
|
|
1018
|
+
profile, profileId: request.profileId, manifest, target, runtimeStateRoot,
|
|
1019
|
+
});
|
|
1020
|
+
const boundaryContract = observeRuntimeBoundaryConfig(profile, boundaryInput);
|
|
1021
|
+
const procRoot = resolve(profile.procRoot ?? "/proc");
|
|
1022
|
+
const kernel = await observeKernelRuntimeBoundary(profile, boundaryContract, procRoot);
|
|
1023
|
+
const slackRoot = privateRoot(profile.workerRuntimeSecretRoot);
|
|
1024
|
+
const slackProfileRoot = join(slackRoot, request.profileId);
|
|
1025
|
+
const mcpRoot = privateRoot(profile.mcpCredentialRoot);
|
|
1026
|
+
const mcpProfileRoot = join(mcpRoot, request.profileId);
|
|
1027
|
+
const pidText = String(kernel.runtimePid);
|
|
1028
|
+
const statText = readFileSync(join(procRoot, pidText, "stat"), "utf8").trim();
|
|
1029
|
+
const closeParen = statText.lastIndexOf(")");
|
|
1030
|
+
const fields = statText.slice(closeParen + 2).split(/\s+/);
|
|
1031
|
+
const startTicks = fields[19];
|
|
1032
|
+
const status = readFileSync(join(procRoot, pidText, "status"), "utf8");
|
|
1033
|
+
const uid = Number(status.match(/^Uid:\s+(\d+)/m)?.[1]);
|
|
1034
|
+
if (uid !== profile.runtimeUid || !/^[1-9][0-9]{0,31}$/.test(startTicks ?? "")) {
|
|
1035
|
+
throw new Error("runtime process identity rejected");
|
|
1036
|
+
}
|
|
1037
|
+
const bridge = verifyHermesAgentBridge({
|
|
1038
|
+
sourceRoot: immutableSourceRoot(profile.hermesSourceRoot),
|
|
1039
|
+
contract: HERMES_AGENT_BRIDGE_CONTRACT,
|
|
1040
|
+
});
|
|
1041
|
+
const profileFiles = observeRuntimeProfileFiles(target);
|
|
1042
|
+
const { runtimeConfig, runtimeMcp, tools, fileHashes: actualFileHashes } = profileFiles;
|
|
1043
|
+
const actualProfileDigest = profileFiles.profileDigest;
|
|
1044
|
+
let observedProvider;
|
|
1045
|
+
try {
|
|
1046
|
+
observedProvider = observeProviderCredential(profile, request.profileId);
|
|
1047
|
+
if (observedProvider !== runtimeConfig?.provider?.fingerprint) {
|
|
1048
|
+
throw new Error("provider credential fingerprint rejected");
|
|
1049
|
+
}
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
if (!driftMode) throw error;
|
|
1052
|
+
observedProvider = sha256(`provider-drift\0${runtimeConfig?.provider?.fingerprint ?? "absent"}`);
|
|
1053
|
+
}
|
|
1054
|
+
if (environmentDrift) {
|
|
1055
|
+
observedProvider = sha256(`environment-drift\0${observedProvider}`);
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
profileId: manifest.profileId,
|
|
1059
|
+
workspaceId: manifest.workspaceId,
|
|
1060
|
+
agentId: manifest.agentId,
|
|
1061
|
+
revisionId: manifest.revisionId,
|
|
1062
|
+
revisionSequence: manifest.revisionSequence,
|
|
1063
|
+
profileDigest: manifest.profileDigest,
|
|
1064
|
+
tools: [...tools].sort(),
|
|
1065
|
+
hashes: {
|
|
1066
|
+
package: runtimePackageHash(profile),
|
|
1067
|
+
config: actualFileHashes["config.yaml"],
|
|
1068
|
+
mcp: actualFileHashes["sellable/mcp.json"],
|
|
1069
|
+
content: actualProfileDigest,
|
|
1070
|
+
policy: runtimeMcp?.policyHash,
|
|
1071
|
+
slackManifest: runtimeConfig?.slack?.manifestHash,
|
|
1072
|
+
soul: actualFileHashes["SOUL.md"],
|
|
1073
|
+
skills: actualFileHashes["skills/sellable/SKILL.md"],
|
|
1074
|
+
provider: observedProvider,
|
|
1075
|
+
},
|
|
1076
|
+
slack: {
|
|
1077
|
+
app: runtimeConfig?.slack?.appHash,
|
|
1078
|
+
team: runtimeConfig?.slack?.teamHash,
|
|
1079
|
+
bot: runtimeConfig?.slack?.botHash,
|
|
1080
|
+
channels: runtimeConfig?.slack?.channelHash,
|
|
1081
|
+
defaultChannel: runtimeConfig?.slack?.defaultChannelHash,
|
|
1082
|
+
memberAllowlist: runtimeConfig?.slack?.memberAllowlistHash,
|
|
1083
|
+
channelIds: runtimeConfig?.slack?.selectedChannelIds,
|
|
1084
|
+
defaultChannelId: runtimeConfig?.slack?.defaultChannelId,
|
|
1085
|
+
memberIds: runtimeConfig?.slack?.memberAllowlist,
|
|
1086
|
+
generation: runtimeConfig?.slack?.generation,
|
|
1087
|
+
xappPresent: true,
|
|
1088
|
+
xoxbPresent: true,
|
|
1089
|
+
xappFingerprint: observedSecret(join(slackProfileRoot, "slack-app"), profile.workerUid, profile.workerGid),
|
|
1090
|
+
xoxbFingerprint: observedSecret(join(slackProfileRoot, "slack-bot"), profile.workerUid, profile.workerGid),
|
|
1091
|
+
},
|
|
1092
|
+
serviceCredential: {
|
|
1093
|
+
present: true,
|
|
1094
|
+
fingerprint: observedSecret(join(mcpProfileRoot, "service-credential"), profile.runtimeUid, profile.runtimeGid),
|
|
1095
|
+
generation: runtimeMcp?.serviceCredential?.generation,
|
|
1096
|
+
},
|
|
1097
|
+
process: {
|
|
1098
|
+
ready: true,
|
|
1099
|
+
pid: Number(pidText),
|
|
1100
|
+
startTicks,
|
|
1101
|
+
bootId: readFileSync(profile.bootIdPath ?? "/proc/sys/kernel/random/boot_id", "utf8").trim(),
|
|
1102
|
+
serviceUser: "sellable-agent-runtime",
|
|
1103
|
+
uid,
|
|
1104
|
+
},
|
|
1105
|
+
hermesBridge: bridge,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async function readableAs(pathValue, uid, gid) {
|
|
1110
|
+
const result = await runBoundedChildProcess({
|
|
1111
|
+
command: "/usr/bin/test",
|
|
1112
|
+
args: ["-r", pathValue],
|
|
1113
|
+
env: { PATH: "/usr/bin:/bin" },
|
|
1114
|
+
shell: false,
|
|
1115
|
+
uid,
|
|
1116
|
+
gid,
|
|
1117
|
+
timeoutMs: 5_000,
|
|
1118
|
+
maxOutputBytes: 1024,
|
|
1119
|
+
});
|
|
1120
|
+
return result.exitCode === 0 && !result.timedOut && !result.outputLimitExceeded;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async function probeRuntimeContainment(profile, request) {
|
|
1124
|
+
if (
|
|
1125
|
+
!Number.isSafeInteger(profile.workerUid) || profile.workerUid < 1 ||
|
|
1126
|
+
!Number.isSafeInteger(profile.workerGid) || profile.workerGid < 1 ||
|
|
1127
|
+
!Number.isSafeInteger(profile.siblingUid) || profile.siblingUid < 1 ||
|
|
1128
|
+
!Number.isSafeInteger(profile.siblingGid) || profile.siblingGid < 1 ||
|
|
1129
|
+
!Number.isSafeInteger(profile.adminUid) || profile.adminUid < 1 ||
|
|
1130
|
+
!Number.isSafeInteger(profile.adminGid) || profile.adminGid < 1 ||
|
|
1131
|
+
new Set([profile.runtimeUid, profile.workerUid, profile.siblingUid, profile.adminUid]).size !== 4
|
|
1132
|
+
) throw new Error("containment identities rejected");
|
|
1133
|
+
const { root, target } = resolveContainmentProfileTarget(profile, request);
|
|
1134
|
+
if (!existsSync(target)) throw new Error("containment profile rejected");
|
|
1135
|
+
const ownProbe = join(target, "agent-profile.json");
|
|
1136
|
+
const siblingRoot = join(root, `.containment-sibling-${request.profileId}-${randomUUID()}`);
|
|
1137
|
+
const siblingProbe = join(siblingRoot, "probe");
|
|
1138
|
+
mkdirSync(siblingRoot, { mode: 0o500 });
|
|
1139
|
+
chownSync(siblingRoot, profile.siblingUid, profile.siblingGid);
|
|
1140
|
+
writeFileSync(siblingProbe, "sibling\n", { mode: 0o400 });
|
|
1141
|
+
chownSync(siblingProbe, profile.siblingUid, profile.siblingGid);
|
|
1142
|
+
try {
|
|
1143
|
+
const workerSlackRoot = privateRoot(profile.workerRuntimeSecretRoot);
|
|
1144
|
+
const workerSecret = join(workerSlackRoot, request.profileId, "slack-bot");
|
|
1145
|
+
const hostProbe = resolve(profile.hostProbePath ?? "");
|
|
1146
|
+
if (!isAbsolute(hostProbe) || !existsSync(hostProbe) || lstatSync(hostProbe).isSymbolicLink()) {
|
|
1147
|
+
throw new Error("host probe rejected");
|
|
1148
|
+
}
|
|
1149
|
+
const entries = observeProfileEntries(target);
|
|
1150
|
+
const providerBytesDenied = entries
|
|
1151
|
+
.filter((entry) => entry.type === "file")
|
|
1152
|
+
.every((entry) => !/(?:xox[bap]-|oauth-provider-secret-byte|asec\.v1\.)/i.test(
|
|
1153
|
+
readFileSync(entry.path === "." ? target : join(target, entry.path), "utf8")
|
|
1154
|
+
));
|
|
1155
|
+
if (
|
|
1156
|
+
profile.runtimeProbePath !== "/usr/local/bin/sellable-agent-runtime-probe" ||
|
|
1157
|
+
profile.mcpCommand !== EXTERNAL_MCP_ENTRYPOINT
|
|
1158
|
+
) throw new Error("runtime probe pin rejected");
|
|
1159
|
+
const manifest = JSON.parse(readFileSync(join(target, "agent-profile.json"), "utf8"));
|
|
1160
|
+
const mcpRoot = privateRoot(profile.mcpCredentialRoot);
|
|
1161
|
+
const runtimeStateRoot = privateRoot(profile.runtimeStateRoot);
|
|
1162
|
+
const probe = await runBoundedChildProcess({
|
|
1163
|
+
command: profile.runtimeProbePath,
|
|
1164
|
+
args: ["--profile-id", request.profileId],
|
|
1165
|
+
env: {
|
|
1166
|
+
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
1167
|
+
HOME: runtimeStateRoot,
|
|
1168
|
+
NODE_ENV: "production",
|
|
1169
|
+
SELLABLE_AGENT_MCP_COMMAND: profile.mcpCommand,
|
|
1170
|
+
SELLABLE_CONFIG_PATH: join(target, "sellable", "mcp.json"),
|
|
1171
|
+
SELLABLE_AGENT_RUNTIME_SECRET_ROOT: mcpRoot,
|
|
1172
|
+
SELLABLE_AGENT_PROFILE_ID: request.profileId,
|
|
1173
|
+
SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE: join(mcpRoot, request.profileId, "service-credential"),
|
|
1174
|
+
SELLABLE_AGENT_WORKSPACE_ID: manifest.workspaceId,
|
|
1175
|
+
SELLABLE_AGENT_ID: manifest.agentId,
|
|
1176
|
+
SELLABLE_AGENT_CREDENTIAL_GENERATION: String(manifest.serviceCredentialGeneration),
|
|
1177
|
+
SELLABLE_AGENT_POLICY_HASH: manifest.policyHash,
|
|
1178
|
+
SELLABLE_AGENT_SELECTED_CHANNEL_IDS: JSON.stringify(manifest.slackChannelIds),
|
|
1179
|
+
},
|
|
1180
|
+
shell: false,
|
|
1181
|
+
uid: profile.runtimeUid,
|
|
1182
|
+
gid: profile.runtimeGid,
|
|
1183
|
+
timeoutMs: 45_000,
|
|
1184
|
+
maxOutputBytes: 32 * 1024,
|
|
1185
|
+
});
|
|
1186
|
+
if (probe.exitCode !== 0 || probe.timedOut || probe.outputLimitExceeded || probe.stderr.trim()) {
|
|
1187
|
+
throw new Error("runtime MCP probe failed");
|
|
1188
|
+
}
|
|
1189
|
+
const mcpProof = JSON.parse(probe.stdout);
|
|
1190
|
+
if (
|
|
1191
|
+
mcpProof?.ok !== true || mcpProof.profileId !== request.profileId ||
|
|
1192
|
+
mcpProof.workspaceId !== manifest.workspaceId || mcpProof.safeRead !== true ||
|
|
1193
|
+
mcpProof.unexpectedToolDenied !== true || mcpProof.wrongChannelDenied !== true ||
|
|
1194
|
+
mcpProof.wrongWorkspaceDenied !== true ||
|
|
1195
|
+
JSON.stringify(mcpProof.tools) !== JSON.stringify([...manifest.toolInclude].sort())
|
|
1196
|
+
) throw new Error("runtime MCP proof rejected");
|
|
1197
|
+
return {
|
|
1198
|
+
ok: true,
|
|
1199
|
+
runtimeUid: profile.runtimeUid,
|
|
1200
|
+
runtimeProfileRead: await readableAs(ownProbe, profile.runtimeUid, profile.runtimeGid),
|
|
1201
|
+
siblingReadDenied: !(await readableAs(ownProbe, profile.siblingUid, profile.siblingGid)),
|
|
1202
|
+
workerReadDenied: !(await readableAs(ownProbe, profile.workerUid, profile.workerGid)),
|
|
1203
|
+
adminReadDenied: !(await readableAs(ownProbe, profile.adminUid, profile.adminGid)),
|
|
1204
|
+
runtimeSiblingReadDenied: !(await readableAs(siblingProbe, profile.runtimeUid, profile.runtimeGid)),
|
|
1205
|
+
runtimeSlackPathDenied: !(await readableAs(workerSecret, profile.runtimeUid, profile.runtimeGid)),
|
|
1206
|
+
providerBytesDenied,
|
|
1207
|
+
hostMountDenied: !(await readableAs(hostProbe, profile.runtimeUid, profile.runtimeGid)),
|
|
1208
|
+
dockerDenied: !(await readableAs(profile.dockerSocketPath ?? "/var/run/docker.sock", profile.runtimeUid, profile.runtimeGid)),
|
|
1209
|
+
unexpectedToolDenied: mcpProof.unexpectedToolDenied,
|
|
1210
|
+
wrongWorkspaceDispatchDenied: mcpProof.wrongWorkspaceDenied && mcpProof.wrongChannelDenied,
|
|
1211
|
+
tools: mcpProof.tools,
|
|
1212
|
+
safeRead: mcpProof.safeRead,
|
|
1213
|
+
workspaceId: mcpProof.workspaceId,
|
|
1214
|
+
};
|
|
1215
|
+
} finally {
|
|
1216
|
+
rmSync(siblingRoot, { recursive: true, force: true });
|
|
1217
|
+
fsyncDirectory(root);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
export async function runRuntimeHelperRequest({ requestPath, config }) {
|
|
1222
|
+
try {
|
|
1223
|
+
const request = confinedRequest(requestPath, config);
|
|
1224
|
+
if (!ACTIONS.has(request.action) || !SAFE_ID.test(request.profileId ?? "")) return { ok: false, code: "helper_action_rejected" };
|
|
1225
|
+
if (
|
|
1226
|
+
Object.keys(request).sort().join("\0") !== [...ACTION_KEYS[request.action]].sort().join("\0") ||
|
|
1227
|
+
!SAFE_ID.test(request.operationId ?? "") || !/^[1-9][0-9]{0,19}$/.test(request.fence ?? "")
|
|
1228
|
+
) return { ok: false, code: "helper_request_rejected" };
|
|
1229
|
+
const profile = config.profiles?.[request.profileId];
|
|
1230
|
+
if (
|
|
1231
|
+
!profile || !Number.isSafeInteger(profile.runtimeUid) || profile.runtimeUid < 1 ||
|
|
1232
|
+
!Number.isSafeInteger(profile.runtimeGid) || profile.runtimeGid < 1
|
|
1233
|
+
) return { ok: false, code: "helper_profile_rejected" };
|
|
1234
|
+
if (request.action === "INSTALL_MCP_CREDENTIAL") {
|
|
1235
|
+
const sourceRoot = privateRoot(config.workerRequestRoot);
|
|
1236
|
+
const payload = JSON.parse(readPrivateDirectFile(sourceRoot, request.sourceRequestFile));
|
|
1237
|
+
if (
|
|
1238
|
+
payload.operationId !== request.operationId || payload.fence !== request.fence ||
|
|
1239
|
+
payload.mcpServiceGeneration !== request.generation || typeof payload.serviceCredential !== "string" ||
|
|
1240
|
+
!payload.serviceCredential.startsWith("sat_") || !SHA256_16.test(request.fingerprint ?? "") ||
|
|
1241
|
+
createHash("sha256").update(payload.serviceCredential).digest("hex").slice(0, 16) !== request.fingerprint ||
|
|
1242
|
+
Object.hasOwn(request, "runtimeUid") || Object.hasOwn(request, "runtimeGid")
|
|
1243
|
+
) throw new Error("credential binding rejected");
|
|
1244
|
+
const root = privateRoot(profile.mcpCredentialRoot);
|
|
1245
|
+
const rootOwner = lstatSync(root);
|
|
1246
|
+
if (rootOwner.uid !== profile.runtimeUid || rootOwner.gid !== profile.runtimeGid) {
|
|
1247
|
+
chownSync(root, profile.runtimeUid, profile.runtimeGid);
|
|
1248
|
+
}
|
|
1249
|
+
const profileRoot = join(root, request.profileId);
|
|
1250
|
+
if (dirname(profileRoot) !== root) throw new Error("credential target rejected");
|
|
1251
|
+
if (existsSync(profileRoot)) rmSync(profileRoot, { recursive: true, force: true });
|
|
1252
|
+
mkdirSync(profileRoot, { mode: 0o700 });
|
|
1253
|
+
const profileOwner = lstatSync(profileRoot);
|
|
1254
|
+
if (profileOwner.uid !== profile.runtimeUid || profileOwner.gid !== profile.runtimeGid) {
|
|
1255
|
+
chownSync(profileRoot, profile.runtimeUid, profile.runtimeGid);
|
|
1256
|
+
}
|
|
1257
|
+
const credentialPath = join(profileRoot, "service-credential");
|
|
1258
|
+
atomicCredential(credentialPath, payload.serviceCredential, profile.runtimeUid, profile.runtimeGid);
|
|
1259
|
+
fsyncDirectory(root);
|
|
1260
|
+
return {
|
|
1261
|
+
ok: true,
|
|
1262
|
+
profileId: request.profileId,
|
|
1263
|
+
path: credentialPath,
|
|
1264
|
+
fingerprint: request.fingerprint,
|
|
1265
|
+
generation: request.generation,
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
if (request.action === "INSTALL_PROVIDER_CREDENTIAL") {
|
|
1269
|
+
return installProviderCredential(profile, request, config);
|
|
1270
|
+
}
|
|
1271
|
+
if (request.action === "INSTALL_RUNTIME_SERVICE_ENV") {
|
|
1272
|
+
return installRuntimeServiceEnvironment(profile, request);
|
|
1273
|
+
}
|
|
1274
|
+
if (request.action === "SET_RUNTIME_GATES") {
|
|
1275
|
+
const written = writeRuntimeGates(profile, request.lifecycle);
|
|
1276
|
+
return { ok: true, profileId: request.profileId, lifecycle: request.lifecycle, ...written };
|
|
1277
|
+
}
|
|
1278
|
+
if (request.action === "CLEAN_PROFILE_SECRETS") {
|
|
1279
|
+
for (const configured of [profile.workerRuntimeSecretRoot, profile.mcpCredentialRoot]) {
|
|
1280
|
+
const root = privateRoot(configured);
|
|
1281
|
+
const target = join(root, request.profileId);
|
|
1282
|
+
if (dirname(target) !== root || (existsSync(target) && lstatSync(target).isSymbolicLink())) throw new Error("secret cleanup rejected");
|
|
1283
|
+
rmSync(target, { recursive: true, force: true });
|
|
1284
|
+
fsyncDirectory(root);
|
|
1285
|
+
}
|
|
1286
|
+
removeProviderCredential(profile, request.profileId);
|
|
1287
|
+
return { ok: true, profileId: request.profileId, disposition: "REMOVED" };
|
|
1288
|
+
}
|
|
1289
|
+
if (request.action === "CHOWN_PROFILE") {
|
|
1290
|
+
const { root, target } = profileTarget(profile, { ...request, slot: "STAGING" }, ["STAGING"]);
|
|
1291
|
+
if (resolve(request.profileRoot ?? "") !== target || !existsSync(target) || !SHA256.test(request.profileDigest ?? "")) {
|
|
1292
|
+
throw new Error("profile ownership target rejected");
|
|
1293
|
+
}
|
|
1294
|
+
const before = observeProfileEntries(target);
|
|
1295
|
+
if (
|
|
1296
|
+
JSON.stringify(before.map(({ path, type }) => [path, type]).sort()) !== JSON.stringify([...PROFILE_ENTRIES].sort()) ||
|
|
1297
|
+
before.some((entry) => (entry.mode & 0o022) !== 0)
|
|
1298
|
+
) throw new Error("profile inventory rejected");
|
|
1299
|
+
const manifest = JSON.parse(readFileSync(join(target, "agent-profile.json"), "utf8"));
|
|
1300
|
+
if (
|
|
1301
|
+
manifest.profileId !== request.profileId || manifest.revisionId !== request.revisionId ||
|
|
1302
|
+
manifest.profileDigest !== request.profileDigest
|
|
1303
|
+
) throw new Error("profile digest binding rejected");
|
|
1304
|
+
applyRuntimeOwnership(target, profile.runtimeUid, profile.runtimeGid);
|
|
1305
|
+
fsyncDirectory(root);
|
|
1306
|
+
const entries = observeProfileEntries(target);
|
|
1307
|
+
if (entries.some((entry) =>
|
|
1308
|
+
entry.uid !== profile.runtimeUid || entry.gid !== profile.runtimeGid ||
|
|
1309
|
+
entry.mode !== (entry.type === "directory" ? 0o500 : 0o400)
|
|
1310
|
+
)) throw new Error("profile ownership readback rejected");
|
|
1311
|
+
return {
|
|
1312
|
+
ok: true,
|
|
1313
|
+
profileId: request.profileId,
|
|
1314
|
+
revisionId: request.revisionId,
|
|
1315
|
+
profileDigest: request.profileDigest,
|
|
1316
|
+
entries,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
if (request.action === "REMOVE_PROFILE") {
|
|
1320
|
+
const { root, target } = profileTarget(profile, request, ["ACTIVE", "BACKUP", "STAGING"]);
|
|
1321
|
+
if (existsSync(target)) {
|
|
1322
|
+
if (lstatSync(target).isSymbolicLink()) throw new Error("profile cleanup rejected");
|
|
1323
|
+
rmSync(target, { recursive: true, force: true });
|
|
1324
|
+
fsyncDirectory(root);
|
|
1325
|
+
}
|
|
1326
|
+
return { ok: true, profileId: request.profileId, slot: request.slot, disposition: "REMOVED" };
|
|
1327
|
+
}
|
|
1328
|
+
if (["OBSERVE_RUNTIME", "OBSERVE_RUNTIME_DRIFT"].includes(request.action)) {
|
|
1329
|
+
return { ok: true, observation: await observeRuntime(profile, request) };
|
|
1330
|
+
}
|
|
1331
|
+
if (request.action === "OBSERVE_RUNTIME_GATES") {
|
|
1332
|
+
const observed = observeRuntimeGates(profile, request.lifecycle);
|
|
1333
|
+
return { ok: true, profileId: request.profileId, ...observed };
|
|
1334
|
+
}
|
|
1335
|
+
if (request.action === "OBSERVE_RUNTIME_LIFECYCLE") {
|
|
1336
|
+
const gateObservation = observeRuntimeGates(profile, request.lifecycle);
|
|
1337
|
+
const { target } = profileTarget(profile, { ...request, slot: "ACTIVE" }, ["ACTIVE"]);
|
|
1338
|
+
const manifest = JSON.parse(readFileSync(join(target, "agent-profile.json"), "utf8"));
|
|
1339
|
+
const slackProfileRoot = join(privateRoot(profile.workerRuntimeSecretRoot), request.profileId);
|
|
1340
|
+
const mcpProfileRoot = join(privateRoot(profile.mcpCredentialRoot), request.profileId);
|
|
1341
|
+
const secretsPresent = [
|
|
1342
|
+
join(slackProfileRoot, "slack-app"),
|
|
1343
|
+
join(slackProfileRoot, "slack-bot"),
|
|
1344
|
+
join(mcpProfileRoot, "service-credential"),
|
|
1345
|
+
].every((pathValue) => existsSync(pathValue) && lstatSync(pathValue).isFile());
|
|
1346
|
+
if (request.lifecycle === "ON") {
|
|
1347
|
+
const observation = await observeRuntime(profile, request);
|
|
1348
|
+
if (!secretsPresent) throw new Error("running runtime secrets absent");
|
|
1349
|
+
return {
|
|
1350
|
+
ok: true,
|
|
1351
|
+
lifecycle: "ON",
|
|
1352
|
+
revisionId: observation.revisionId,
|
|
1353
|
+
credentialGeneration: observation.serviceCredential.generation,
|
|
1354
|
+
profileDigest: observation.profileDigest,
|
|
1355
|
+
gates: gateObservation.gates,
|
|
1356
|
+
secretsPresent,
|
|
1357
|
+
process: observation.process,
|
|
1358
|
+
runtimeObservation: observation,
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
const servicePath = resolve(profile.runtimeServicePath);
|
|
1362
|
+
const superviseRoot = join(servicePath, "supervise");
|
|
1363
|
+
const stat = readFileSync(join(superviseRoot, "stat"), "utf8").trim();
|
|
1364
|
+
const pid = readFileSync(join(superviseRoot, "pid"), "utf8").trim();
|
|
1365
|
+
const cgroupProcs = join(profile.cgroupRoot, request.profileId, "cgroup.procs");
|
|
1366
|
+
if (stat !== "down" || !["", "0"].includes(pid) || (existsSync(cgroupProcs) && readFileSync(cgroupProcs, "utf8").trim())) {
|
|
1367
|
+
throw new Error("stopped runtime process still present");
|
|
1368
|
+
}
|
|
1369
|
+
if (["OFF", "ARCHIVING"].includes(request.lifecycle) && !secretsPresent) {
|
|
1370
|
+
throw new Error("stopped runtime secrets absent");
|
|
1371
|
+
}
|
|
1372
|
+
if (request.lifecycle === "ARCHIVED" && secretsPresent) throw new Error("archived runtime secrets present");
|
|
1373
|
+
return {
|
|
1374
|
+
ok: true,
|
|
1375
|
+
lifecycle: request.lifecycle,
|
|
1376
|
+
revisionId: manifest.revisionId,
|
|
1377
|
+
credentialGeneration: manifest.serviceCredentialGeneration,
|
|
1378
|
+
profileDigest: manifest.profileDigest,
|
|
1379
|
+
gates: gateObservation.gates,
|
|
1380
|
+
secretsPresent,
|
|
1381
|
+
process: {
|
|
1382
|
+
ready: false,
|
|
1383
|
+
pid: null,
|
|
1384
|
+
startTicks: null,
|
|
1385
|
+
bootId: readFileSync(profile.bootIdPath ?? "/proc/sys/kernel/random/boot_id", "utf8").trim(),
|
|
1386
|
+
serviceUser: "sellable-agent-runtime",
|
|
1387
|
+
uid: profile.runtimeUid,
|
|
1388
|
+
},
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
if (request.action === "PROBE_CONTAINMENT") {
|
|
1392
|
+
return probeRuntimeContainment(profile, request);
|
|
1393
|
+
}
|
|
1394
|
+
if (request.action === "INSTALL_HERMES_BRIDGE") {
|
|
1395
|
+
return {
|
|
1396
|
+
ok: true,
|
|
1397
|
+
bridge: verifyHermesAgentBridge({
|
|
1398
|
+
sourceRoot: immutableSourceRoot(profile.hermesSourceRoot),
|
|
1399
|
+
contract: HERMES_AGENT_BRIDGE_CONTRACT,
|
|
1400
|
+
}),
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
if (request.action === "VERIFY_HERMES_BRIDGE") {
|
|
1404
|
+
return {
|
|
1405
|
+
ok: true,
|
|
1406
|
+
bridge: verifyHermesAgentBridge({
|
|
1407
|
+
sourceRoot: immutableSourceRoot(profile.hermesSourceRoot),
|
|
1408
|
+
contract: HERMES_AGENT_BRIDGE_CONTRACT,
|
|
1409
|
+
}),
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
const command = config.s6SvcPath ?? "/command/s6-svc";
|
|
1413
|
+
if (!["/command/s6-svc", "/usr/bin/s6-svc"].includes(command) || !["UP", "DOWN", "RESTART"].includes(request.control)) {
|
|
1414
|
+
throw new Error("runtime control rejected");
|
|
1415
|
+
}
|
|
1416
|
+
const servicePath = resolve(profile.runtimeServicePath);
|
|
1417
|
+
if (!isAbsolute(servicePath) || !existsSync(servicePath) || lstatSync(servicePath).isSymbolicLink()) throw new Error("service path rejected");
|
|
1418
|
+
const result = await runBoundedChildProcess({
|
|
1419
|
+
command, args: [{ UP: "-u", DOWN: "-d", RESTART: "-t" }[request.control], servicePath],
|
|
1420
|
+
env: { PATH: "/command:/usr/bin:/bin" }, shell: false, timeoutMs: 15_000, maxOutputBytes: 16 * 1024,
|
|
1421
|
+
});
|
|
1422
|
+
return result.exitCode === 0 && !result.timedOut && !result.outputLimitExceeded
|
|
1423
|
+
? { ok: true, profileId: request.profileId, control: request.control }
|
|
1424
|
+
: { ok: false, code: "runtime_control_failed" };
|
|
1425
|
+
} catch {
|
|
1426
|
+
return { ok: false, code: "runtime_helper_rejected" };
|
|
1427
|
+
}
|
|
1428
|
+
}
|