@sellable/install 0.1.362-phase111.20260720123904 → 0.1.362

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.
@@ -0,0 +1,458 @@
1
+ import {
2
+ createHash,
3
+ createPrivateKey,
4
+ createPublicKey,
5
+ generateKeyPairSync,
6
+ randomUUID,
7
+ } from "node:crypto";
8
+ import {
9
+ chmodSync,
10
+ chownSync,
11
+ closeSync,
12
+ constants,
13
+ existsSync,
14
+ fstatSync,
15
+ fsyncSync,
16
+ lstatSync,
17
+ mkdirSync,
18
+ openSync,
19
+ readFileSync,
20
+ realpathSync,
21
+ renameSync,
22
+ writeFileSync,
23
+ } from "node:fs";
24
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import { buildExternalRuntimeClosure } from "./external-runtime-builder.mjs";
27
+ import { canonicalHostWorkerDigest } from "./host-worker.mjs";
28
+ import {
29
+ EXTERNAL_HERMES_ENTRYPOINT,
30
+ EXTERNAL_MCP_ENTRYPOINT,
31
+ EXTERNAL_RUNTIME_CLOSURE_ROOT,
32
+ EXTERNAL_RUNTIME_MANIFEST_PATH,
33
+ } from "./runtime-helper.mjs";
34
+ import { RUNTIME_EGRESS_HOSTS } from "./runtime-boundary.mjs";
35
+ import { RUNTIME_EGRESS_PROXY_VERSION } from "./runtime-egress-proxy.mjs";
36
+ import { installAgentHostServices } from "./service-installer.mjs";
37
+
38
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
39
+ const VERSION = "sellable-agent-host-bootstrap/v1";
40
+ const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.362";
42
+ const MCP_PACKAGE = "@sellable/mcp@0.1.620";
43
+ const HERMES_VERSION = "0.18.0";
44
+ const SHA256 = /^[a-f0-9]{64}$/;
45
+ const SHA256_16 = /^[a-f0-9]{16}$/;
46
+ const SHA512_INTEGRITY = /^sha512-[A-Za-z0-9+/]+={0,2}$/;
47
+ const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
48
+ const SAFE_TOOL = /^[a-z][a-z0-9_]{0,63}$/;
49
+ const SECRET = /xox[bap]-|sat_[A-Za-z0-9_-]+|asec\.v1\.|access[_-]?token|refresh[_-]?token|private[_-]?key/i;
50
+
51
+ const PATHS = Object.freeze({
52
+ providerSource: "/var/lib/sellable-agent-bootstrap/provider-auth",
53
+ xappSource: "/var/lib/sellable-agent-bootstrap/slack-app",
54
+ hostProbe: "/var/lib/sellable-agent-bootstrap/host-probe",
55
+ receipt: "/var/lib/sellable-agent-bootstrap/registration-receipt.json",
56
+ releaseIdentity: "/opt/sellable-agent/release.json",
57
+ workerRoot: "/var/lib/sellable-agent-worker",
58
+ workerRequests: "/var/lib/sellable-agent-worker/requests",
59
+ helperRequests: "/var/lib/sellable-agent-worker/helper-requests",
60
+ workerState: "/var/lib/sellable-agent-worker/state",
61
+ workerSlackSecrets: "/var/lib/sellable-agent-worker/runtime-secrets",
62
+ workerHostSecrets: "/var/lib/sellable-agent-worker/host-secrets",
63
+ xappRuntime: "/var/lib/sellable-agent-worker/host-secrets/slack-app",
64
+ signingRoot: "/var/lib/sellable-agent-worker/enrollment",
65
+ signingKey: "/var/lib/sellable-agent-worker/enrollment/private-key.pem",
66
+ runtimeRoot: "/var/lib/sellable-agent-runtime",
67
+ runtimeState: "/var/lib/sellable-agent-runtime/state",
68
+ runtimeMcpSecrets: "/var/lib/sellable-agent-runtime/mcp-secrets",
69
+ profiles: "/var/lib/sellable-agent-profiles",
70
+ });
71
+
72
+ const exactKeys = (value, keys) => Boolean(
73
+ value && typeof value === "object" && !Array.isArray(value) &&
74
+ Object.keys(value).sort().join("\0") === [...keys].sort().join("\0")
75
+ );
76
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
77
+
78
+ function stable(value) {
79
+ if (Array.isArray(value)) return value.map(stable);
80
+ if (value && typeof value === "object") {
81
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
82
+ }
83
+ return value;
84
+ }
85
+
86
+ function targetPath(targetRoot, absolutePath) {
87
+ const root = realpathSync(resolve(targetRoot));
88
+ const value = root === "/" ? resolve(absolutePath) : join(root, absolutePath.replace(/^\/+/, ""));
89
+ const rel = relative(root, value);
90
+ if (!isAbsolute(absolutePath) || rel.startsWith("..") || isAbsolute(rel)) {
91
+ throw new Error("host bootstrap target rejected");
92
+ }
93
+ return value;
94
+ }
95
+
96
+ function atomicFile(pathValue, bytes, mode, { uid, gid, disposableFixture }) {
97
+ const temp = join(dirname(pathValue), `.${randomUUID()}.tmp`);
98
+ writeFileSync(temp, bytes, { flag: "wx", mode });
99
+ chmodSync(temp, mode);
100
+ if (!disposableFixture) chownSync(temp, uid, gid);
101
+ const descriptor = openSync(temp, "r");
102
+ try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
103
+ renameSync(temp, pathValue);
104
+ const parent = openSync(dirname(pathValue), "r");
105
+ try { fsyncSync(parent); } finally { closeSync(parent); }
106
+ }
107
+
108
+ function ensureDirectory(targetRoot, absolutePath, mode, uid, gid, disposableFixture) {
109
+ const value = targetPath(targetRoot, absolutePath);
110
+ if (!existsSync(value)) mkdirSync(value, { recursive: true, mode });
111
+ const link = lstatSync(value);
112
+ if (link.isSymbolicLink() || !link.isDirectory()) throw new Error("host bootstrap directory rejected");
113
+ chmodSync(value, mode);
114
+ if (!disposableFixture) chownSync(value, uid, gid);
115
+ const observed = lstatSync(value);
116
+ if (
117
+ (observed.mode & 0o777) !== mode ||
118
+ (!disposableFixture && (observed.uid !== uid || observed.gid !== gid))
119
+ ) throw new Error("host bootstrap directory readback rejected");
120
+ return value;
121
+ }
122
+
123
+ function readPrivateSource(targetRoot, absolutePath, disposableFixture) {
124
+ const value = targetPath(targetRoot, absolutePath);
125
+ const parent = lstatSync(dirname(value));
126
+ const link = lstatSync(value);
127
+ const expectedUid = disposableFixture ? (process.getuid?.() ?? link.uid) : 0;
128
+ const expectedGid = disposableFixture ? (process.getgid?.() ?? link.gid) : 0;
129
+ if (
130
+ parent.isSymbolicLink() || !parent.isDirectory() || (parent.mode & 0o777) !== 0o700 ||
131
+ parent.uid !== expectedUid || parent.gid !== expectedGid ||
132
+ link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 ||
133
+ link.uid !== expectedUid || link.gid !== expectedGid || realpathSync(dirname(value)) !== dirname(value)
134
+ ) throw new Error("host bootstrap private source rejected");
135
+ const descriptor = openSync(value, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
136
+ try {
137
+ const opened = fstatSync(descriptor);
138
+ if (!opened.isFile() || opened.dev !== link.dev || opened.ino !== link.ino) {
139
+ throw new Error("host bootstrap private source changed");
140
+ }
141
+ return readFileSync(descriptor, "utf8").trim();
142
+ } finally {
143
+ closeSync(descriptor);
144
+ }
145
+ }
146
+
147
+ function readReleaseIdentity(targetRoot, expected, disposableFixture) {
148
+ const value = targetPath(targetRoot, PATHS.releaseIdentity);
149
+ const link = lstatSync(value);
150
+ const expectedUid = disposableFixture ? (process.getuid?.() ?? link.uid) : 0;
151
+ const expectedGid = disposableFixture ? (process.getgid?.() ?? link.gid) : 0;
152
+ if (
153
+ link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o444 ||
154
+ link.uid !== expectedUid || link.gid !== expectedGid
155
+ ) throw new Error("host bootstrap image release rejected");
156
+ const observed = JSON.parse(readFileSync(value, "utf8"));
157
+ if (!exactKeys(observed, ["installerPackage", "installerIntegrity", "mcpPackage", "mcpIntegrity"]) ||
158
+ JSON.stringify(stable(observed)) !== JSON.stringify(stable(expected))) {
159
+ throw new Error("host bootstrap image release mismatch");
160
+ }
161
+ return observed;
162
+ }
163
+
164
+ function validateConfig(config) {
165
+ if (!exactKeys(config, [
166
+ "version", "target", "host", "enrollment", "release", "controlPlaneUrl",
167
+ "runtimeContract", "identities",
168
+ ]) || config.version !== VERSION || SECRET.test(JSON.stringify(config))) {
169
+ throw new Error("host bootstrap config rejected");
170
+ }
171
+ if (
172
+ !exactKeys(config.target, ["vpsId", "hostname", "containerName"]) ||
173
+ Object.values(config.target).some((value) => !SAFE_ID.test(value ?? "")) ||
174
+ !exactKeys(config.host, ["id", "stableKey", "compatibilityHash", "capacity"]) ||
175
+ !SAFE_ID.test(config.host.id ?? "") || !SAFE_ID.test(config.host.stableKey ?? "") ||
176
+ !SHA256.test(config.host.compatibilityHash ?? "") ||
177
+ !Number.isSafeInteger(config.host.capacity) || config.host.capacity < 1 || config.host.capacity > 100 ||
178
+ !exactKeys(config.enrollment, ["id", "workerId", "keyGeneration", "expiresAt"]) ||
179
+ !SAFE_ID.test(config.enrollment.id ?? "") || !SAFE_ID.test(config.enrollment.workerId ?? "") ||
180
+ !Number.isSafeInteger(config.enrollment.keyGeneration) || config.enrollment.keyGeneration < 1 ||
181
+ !Number.isFinite(Date.parse(config.enrollment.expiresAt ?? "")) ||
182
+ Date.parse(config.enrollment.expiresAt) <= Date.now() ||
183
+ !exactKeys(config.release, ["installerPackage", "installerIntegrity", "mcpPackage", "mcpIntegrity"]) ||
184
+ config.release.installerPackage !== INSTALLER_PACKAGE ||
185
+ config.release.mcpPackage !== MCP_PACKAGE ||
186
+ !SHA512_INTEGRITY.test(config.release.installerIntegrity ?? "") ||
187
+ !SHA512_INTEGRITY.test(config.release.mcpIntegrity ?? "") ||
188
+ !/^https:\/\/[A-Za-z0-9.-]+(?::[0-9]+)?$/.test(config.controlPlaneUrl ?? "")
189
+ ) throw new Error("host bootstrap identity rejected");
190
+ const runtime = config.runtimeContract;
191
+ if (
192
+ !exactKeys(runtime, [
193
+ "profileId", "providerReference", "providerFingerprint", "xappReference",
194
+ "xappFingerprint", "policyHash", "toolInclude",
195
+ ]) || !SAFE_ID.test(runtime.profileId ?? "") ||
196
+ typeof runtime.providerReference !== "string" || !runtime.providerReference || runtime.providerReference.length > 256 ||
197
+ !SHA256.test(runtime.providerFingerprint ?? "") ||
198
+ typeof runtime.xappReference !== "string" || !runtime.xappReference || runtime.xappReference.length > 256 ||
199
+ !SHA256_16.test(runtime.xappFingerprint ?? "") ||
200
+ !SHA256.test(runtime.policyHash ?? "") || !Array.isArray(runtime.toolInclude) ||
201
+ runtime.toolInclude.length < 1 || runtime.toolInclude.length > 64 ||
202
+ runtime.toolInclude.some((tool) => !SAFE_TOOL.test(tool)) ||
203
+ JSON.stringify(runtime.toolInclude) !== JSON.stringify([...new Set(runtime.toolInclude)].sort())
204
+ ) throw new Error("host bootstrap runtime contract rejected");
205
+ const identities = config.identities;
206
+ if (
207
+ !exactKeys(identities, ["workerUid", "workerGid", "proxyUid", "proxyGid", "runtimeUid", "runtimeGid", "siblingUid", "siblingGid", "adminUid", "adminGid"]) ||
208
+ Object.values(identities).some((value) => !Number.isSafeInteger(value) || value < 1) ||
209
+ new Set([identities.workerUid, identities.proxyUid, identities.runtimeUid, identities.siblingUid, identities.adminUid]).size !== 5
210
+ ) throw new Error("host bootstrap runtime identities rejected");
211
+ return config;
212
+ }
213
+
214
+ function signingIdentity(targetRoot, config, disposableFixture) {
215
+ const keyPath = targetPath(targetRoot, PATHS.signingKey);
216
+ if (!existsSync(keyPath)) {
217
+ const { privateKey } = generateKeyPairSync("ed25519");
218
+ atomicFile(
219
+ keyPath,
220
+ privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
221
+ 0o600,
222
+ { uid: config.identities.workerUid, gid: config.identities.workerGid, disposableFixture },
223
+ );
224
+ }
225
+ const link = lstatSync(keyPath);
226
+ if (
227
+ link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 ||
228
+ (!disposableFixture && (link.uid !== config.identities.workerUid || link.gid !== config.identities.workerGid))
229
+ ) throw new Error("host bootstrap signing key rejected");
230
+ const privateKey = createPrivateKey(readFileSync(keyPath, "utf8"));
231
+ if (privateKey.asymmetricKeyType !== "ed25519") throw new Error("host bootstrap signing key type rejected");
232
+ const publicKeyPem = createPublicKey(privateKey).export({ type: "spki", format: "pem" }).toString();
233
+ const publicKeyHash = sha256(publicKeyPem);
234
+ const enrollmentFingerprint = canonicalHostWorkerDigest({
235
+ audience: "sellable-agent-worker/v1",
236
+ enrollmentId: config.enrollment.id,
237
+ hostId: config.host.id,
238
+ keyGeneration: config.enrollment.keyGeneration,
239
+ publicKeyHash,
240
+ workerId: config.enrollment.workerId,
241
+ });
242
+ return { publicKeyHash, enrollmentFingerprint };
243
+ }
244
+
245
+ function prepareLayout(targetRoot, config, disposableFixture) {
246
+ const ids = config.identities;
247
+ for (const pathValue of [
248
+ PATHS.workerRoot, PATHS.workerRequests, PATHS.helperRequests, PATHS.workerState,
249
+ PATHS.workerSlackSecrets, PATHS.workerHostSecrets, PATHS.signingRoot,
250
+ ]) ensureDirectory(targetRoot, pathValue, 0o700, ids.workerUid, ids.workerGid, disposableFixture);
251
+ for (const pathValue of [PATHS.runtimeRoot, PATHS.runtimeState, PATHS.runtimeMcpSecrets]) {
252
+ ensureDirectory(targetRoot, pathValue, 0o700, ids.runtimeUid, ids.runtimeGid, disposableFixture);
253
+ }
254
+ ensureDirectory(targetRoot, PATHS.profiles, 0o711, ids.workerUid, ids.workerGid, disposableFixture);
255
+ const probePath = targetPath(targetRoot, PATHS.hostProbe);
256
+ if (!existsSync(probePath)) {
257
+ atomicFile(probePath, "host-only\n", 0o600, { uid: 0, gid: 0, disposableFixture });
258
+ }
259
+ }
260
+
261
+ function installHostAppSecret(targetRoot, config, disposableFixture) {
262
+ const source = readPrivateSource(targetRoot, PATHS.xappSource, disposableFixture);
263
+ if (!source || source.length > 4096 || sha256(source).slice(0, 16) !== config.runtimeContract.xappFingerprint) {
264
+ throw new Error("host bootstrap Slack app source rejected");
265
+ }
266
+ const target = targetPath(targetRoot, PATHS.xappRuntime);
267
+ atomicFile(target, `${source}\n`, 0o600, {
268
+ uid: config.identities.workerUid,
269
+ gid: config.identities.workerGid,
270
+ disposableFixture,
271
+ });
272
+ return target;
273
+ }
274
+
275
+ function configs(config, signing, ownerUid, ownerGid) {
276
+ const ids = config.identities;
277
+ const workerConfig = {
278
+ enrollmentId: config.enrollment.id,
279
+ hostId: config.host.id,
280
+ workerId: config.enrollment.workerId,
281
+ signerGeneration: config.enrollment.keyGeneration,
282
+ enrollmentFingerprint: signing.enrollmentFingerprint,
283
+ publicKeyHash: signing.publicKeyHash,
284
+ privateKeyPath: PATHS.signingKey,
285
+ controlPlaneUrl: config.controlPlaneUrl,
286
+ requestRoot: PATHS.workerRequests,
287
+ helperRequestRoot: PATHS.helperRequests,
288
+ stateRoot: PATHS.workerState,
289
+ profilesRoot: PATHS.profiles,
290
+ runtimeSecretsRoot: PATHS.workerSlackSecrets,
291
+ runtimeMcpSecretsRoot: PATHS.runtimeMcpSecrets,
292
+ workerUid: ids.workerUid,
293
+ workerGid: ids.workerGid,
294
+ runtimeUid: ids.runtimeUid,
295
+ runtimeGid: ids.runtimeGid,
296
+ leaseSeconds: 30,
297
+ poll: { minDelayMs: 1_000, maxDelayMs: 30_000, jitterRatio: 0.2 },
298
+ profileRuntimeContract: {
299
+ hermesCli: "hermes",
300
+ hermesVersion: HERMES_VERSION,
301
+ installerPackage: INSTALLER_PACKAGE,
302
+ mcpPackage: MCP_PACKAGE,
303
+ providerReference: config.runtimeContract.providerReference,
304
+ providerFingerprint: config.runtimeContract.providerFingerprint,
305
+ xappReference: config.runtimeContract.xappReference,
306
+ xappFingerprint: config.runtimeContract.xappFingerprint,
307
+ xappSecretFile: PATHS.xappRuntime,
308
+ policies: {
309
+ [config.runtimeContract.policyHash]: { toolInclude: config.runtimeContract.toolInclude },
310
+ },
311
+ },
312
+ };
313
+ const helperProfile = {
314
+ workerUid: ids.workerUid,
315
+ workerGid: ids.workerGid,
316
+ runtimeUid: ids.runtimeUid,
317
+ runtimeGid: ids.runtimeGid,
318
+ siblingUid: ids.siblingUid,
319
+ siblingGid: ids.siblingGid,
320
+ adminUid: ids.adminUid,
321
+ adminGid: ids.adminGid,
322
+ proxyUid: ids.proxyUid,
323
+ proxyGid: ids.proxyGid,
324
+ profilesRoot: PATHS.profiles,
325
+ workerRuntimeSecretRoot: PATHS.workerSlackSecrets,
326
+ mcpCredentialRoot: PATHS.runtimeMcpSecrets,
327
+ runtimeStateRoot: PATHS.runtimeState,
328
+ proxyExecutablePath: "/usr/local/lib/sellable-agent/package/bin/sellable-agent-egress-proxy.mjs",
329
+ installedPackageManifestPath: "/usr/local/lib/sellable-agent/package/manifest.json",
330
+ externalRuntimeManifestPath: EXTERNAL_RUNTIME_MANIFEST_PATH,
331
+ externalRuntimeClosureRoot: EXTERNAL_RUNTIME_CLOSURE_ROOT,
332
+ externalRuntimeTrustRoot: "/usr/local/lib/sellable-agent",
333
+ externalRuntimePathRoot: "/",
334
+ externalRuntimeOwnerUid: ownerUid,
335
+ externalRuntimeOwnerGid: ownerGid,
336
+ mcpCommand: EXTERNAL_MCP_ENTRYPOINT,
337
+ hermesExecutablePath: EXTERNAL_HERMES_ENTRYPOINT,
338
+ hermesSourceRoot: `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes`,
339
+ proxyConfigPath: "/etc/sellable-agent/egress-proxy.json",
340
+ runtimeBoundaryConfigPath: "/etc/sellable-agent/runtime-boundary.json",
341
+ providerCredentialSourcePath: PATHS.providerSource,
342
+ providerCredentialFingerprint: config.runtimeContract.providerFingerprint,
343
+ runtimeServicePath: "/etc/services.d/sellable-agent-runtime",
344
+ runtimeServiceEnvRoot: "/etc/services.d/sellable-agent-runtime/env",
345
+ runtimeProbePath: "/usr/local/bin/sellable-agent-runtime-probe",
346
+ proxyPidPath: "/etc/services.d/sellable-agent-egress-proxy/supervise/pid",
347
+ proxyPort: 19081,
348
+ procRoot: "/proc",
349
+ bootIdPath: "/proc/sys/kernel/random/boot_id",
350
+ bubblewrapPath: "/usr/bin/bwrap",
351
+ setprivPath: "/usr/bin/setpriv",
352
+ nftPath: "/usr/sbin/nft",
353
+ cgroupRoot: "/sys/fs/cgroup/sellable-agent",
354
+ hostProbePath: PATHS.hostProbe,
355
+ dockerSocketPath: "/var/run/docker.sock",
356
+ };
357
+ const helperConfig = {
358
+ helperRequestRoot: PATHS.helperRequests,
359
+ workerRequestRoot: PATHS.workerRequests,
360
+ s6SvcPath: "/command/s6-svc",
361
+ profiles: { [config.runtimeContract.profileId]: helperProfile },
362
+ };
363
+ const egressProxyConfig = {
364
+ version: RUNTIME_EGRESS_PROXY_VERSION,
365
+ listenHost: "127.0.0.1",
366
+ listenPort: 19081,
367
+ uid: ids.proxyUid,
368
+ gid: ids.proxyGid,
369
+ allowedHosts: [...RUNTIME_EGRESS_HOSTS],
370
+ };
371
+ return { workerConfig, helperConfig, egressProxyConfig };
372
+ }
373
+
374
+ export function bootstrapAgentHost(input) {
375
+ const config = validateConfig(input.config);
376
+ const targetRoot = resolve(input.targetRoot ?? "/");
377
+ const disposableFixture = input.disposableFixture === true;
378
+ if (!isAbsolute(targetRoot) || !existsSync(targetRoot) || (targetRoot !== "/" && !disposableFixture)) {
379
+ throw new Error("host bootstrap root rejected");
380
+ }
381
+ if (targetRoot === "/" && (process.getuid?.() !== 0 || process.getgid?.() !== 0)) {
382
+ throw new Error("host bootstrap requires root");
383
+ }
384
+ readReleaseIdentity(targetRoot, config.release, disposableFixture);
385
+ const provider = readPrivateSource(targetRoot, PATHS.providerSource, disposableFixture);
386
+ if (!provider || provider.length > 1024 * 1024 || sha256(provider) !== config.runtimeContract.providerFingerprint) {
387
+ throw new Error("host bootstrap provider source rejected");
388
+ }
389
+ const providerJson = JSON.parse(provider);
390
+ if (!providerJson || typeof providerJson !== "object" || Array.isArray(providerJson)) {
391
+ throw new Error("host bootstrap provider JSON rejected");
392
+ }
393
+ prepareLayout(targetRoot, config, disposableFixture);
394
+ installHostAppSecret(targetRoot, config, disposableFixture);
395
+ const signing = signingIdentity(targetRoot, config, disposableFixture);
396
+ const sourceRoots = disposableFixture ? input.sourceRoots : {
397
+ mcpPackageRoot: "/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp",
398
+ mcpNodeModulesRoot: "/opt/sellable-agent/mcp-root/node_modules",
399
+ hermesSourceRoot: "/opt/hermes",
400
+ };
401
+ const externalRuntime = buildExternalRuntimeClosure({
402
+ pathRoot: targetRoot,
403
+ ownerUid: disposableFixture ? (process.getuid?.() ?? 0) : 0,
404
+ ownerGid: disposableFixture ? (process.getgid?.() ?? 0) : 0,
405
+ disposableFixture,
406
+ ...(sourceRoots ?? {}),
407
+ ...(disposableFixture && input.hermesBridgeContract ? { hermesBridgeContract: input.hermesBridgeContract } : {}),
408
+ });
409
+ const ownerUid = disposableFixture ? (process.getuid?.() ?? 0) : 0;
410
+ const ownerGid = disposableFixture ? (process.getgid?.() ?? 0) : 0;
411
+ const generated = configs(config, signing, ownerUid, ownerGid);
412
+ const installed = installAgentHostServices({
413
+ targetRoot,
414
+ packageRoot: input.packageRoot ?? PACKAGE_ROOT,
415
+ ownerUid,
416
+ ownerGid,
417
+ disposableFixture,
418
+ ...(disposableFixture && input.accountProvisioner ? { accountProvisioner: input.accountProvisioner } : {}),
419
+ ...generated,
420
+ });
421
+ if (!installed.ok) throw new Error(installed.code);
422
+ const configurationDigest = sha256(JSON.stringify(stable(generated)));
423
+ const receipt = {
424
+ version: RECEIPT_VERSION,
425
+ producer: "sellable-agent-host-bootstrap",
426
+ target: config.target,
427
+ host: config.host,
428
+ enrollment: {
429
+ id: config.enrollment.id,
430
+ workerId: config.enrollment.workerId,
431
+ audience: "sellable-agent-worker/v1",
432
+ keyGeneration: config.enrollment.keyGeneration,
433
+ publicKeyHash: signing.publicKeyHash,
434
+ enrollmentFingerprint: signing.enrollmentFingerprint,
435
+ capabilities: ["claim", "heartbeat", "secret-consume", "complete"],
436
+ expiresAt: new Date(config.enrollment.expiresAt).toISOString(),
437
+ },
438
+ release: {
439
+ ...config.release,
440
+ serviceManifestDigest: installed.manifestDigest,
441
+ externalRuntimeClosureDigest: externalRuntime.closureDigest,
442
+ configurationDigest,
443
+ },
444
+ secretScan: { unexpectedMatches: 0 },
445
+ observedAt: (input.now ?? new Date()).toISOString(),
446
+ };
447
+ if (SECRET.test(JSON.stringify(receipt))) throw new Error("host bootstrap receipt rejected");
448
+ atomicFile(
449
+ targetPath(targetRoot, PATHS.receipt),
450
+ `${JSON.stringify(receipt, null, 2)}\n`,
451
+ 0o600,
452
+ { uid: 0, gid: 0, disposableFixture },
453
+ );
454
+ return { ok: true, receipt, workerConfig: generated.workerConfig, helperConfig: generated.helperConfig };
455
+ }
456
+
457
+ export const SELLABLE_AGENT_HOST_BOOTSTRAP_VERSION = VERSION;
458
+ export const SELLABLE_AGENT_HOST_BOOTSTRAP_PATHS = PATHS;
@@ -957,8 +957,8 @@ export function compileClaimToProfileDesired(activeClaim, config) {
957
957
  ]) ||
958
958
  !exactObject(runtimeService, ["principalId", "policyHash", "credentialGeneration", "credentialFingerprint"]) ||
959
959
  pinned.hermesCli !== "hermes" || pinned.hermesVersion !== "0.18.0" ||
960
- pinned.installerPackage !== "@sellable/install@0.1.357" ||
961
- pinned.mcpPackage !== "@sellable/mcp@0.1.619" ||
960
+ pinned.installerPackage !== "@sellable/install@0.1.362" ||
961
+ pinned.mcpPackage !== "@sellable/mcp@0.1.620" ||
962
962
  !Array.isArray(policy.toolInclude) || policy.toolInclude.length === 0 ||
963
963
  !Number.isSafeInteger(slack.generation) || slack.generation < 1 ||
964
964
  !/^A[A-Z0-9]{8,20}$/.test(slack.appId ?? "") ||
@@ -1423,6 +1423,11 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
1423
1423
  revisionId: request.evidence.revisionId,
1424
1424
  profileDigest: request.evidence.profileDigest,
1425
1425
  });
1426
+ await helper({
1427
+ action: "INSTALL_PROVIDER_CREDENTIAL",
1428
+ ...helperIdentity(),
1429
+ fingerprint: context.desired.providerFingerprint,
1430
+ });
1426
1431
  await helper({ action: "INSTALL_HERMES_BRIDGE", ...helperIdentity() });
1427
1432
  await controlRuntime("UP");
1428
1433
  },
@@ -1491,17 +1496,6 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
1491
1496
  lifecycle: {},
1492
1497
  };
1493
1498
  const restart = () => controlRuntime("RESTART");
1494
- const reload = async (path) => {
1495
- const base = new URL(config.runtimeControlUrl);
1496
- if (base.protocol !== "http:" || !["127.0.0.1", "localhost", "::1"].includes(base.hostname)) {
1497
- throw new Error("runtime control endpoint rejected");
1498
- }
1499
- const response = await (dependencies.fetchImpl ?? fetch)(
1500
- new URL(path, base),
1501
- { method: "POST", signal: AbortSignal.timeout(10_000) },
1502
- );
1503
- if (!response.ok) throw new Error("runtime reload rejected");
1504
- };
1505
1499
  const rawRuntimeObservation = async ({ drift = false } = {}) => {
1506
1500
  const observed = (await helper({
1507
1501
  action: drift ? "OBSERVE_RUNTIME_DRIFT" : "OBSERVE_RUNTIME",
@@ -1564,6 +1558,11 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
1564
1558
  revisionId: result.receipt.revisionId,
1565
1559
  profileDigest: result.receipt.profileDigest,
1566
1560
  });
1561
+ await helper({
1562
+ action: "INSTALL_PROVIDER_CREDENTIAL",
1563
+ ...helperIdentity(),
1564
+ fingerprint: context.desired.providerFingerprint,
1565
+ });
1567
1566
  await helper({ action: "INSTALL_HERMES_BRIDGE", ...helperIdentity() });
1568
1567
  };
1569
1568
  const refreshThen = (effect) => async (input) => {
@@ -1571,8 +1570,8 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
1571
1570
  return effect(input);
1572
1571
  };
1573
1572
  Object.assign(ports.refresh, {
1574
- reloadMcp: refreshThen(() => reload("/reload-mcp")),
1575
- reloadSkills: refreshThen(() => reload("/reload-skills")),
1573
+ reloadMcp: refreshThen(restart),
1574
+ reloadSkills: refreshThen(restart),
1576
1575
  reloadEnvironmentOrRestart: refreshThen(restart),
1577
1576
  restartGateway: refreshThen(restart),
1578
1577
  restartProfileSession: refreshThen(restart),
@@ -1675,11 +1674,8 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
1675
1674
  async fenceDispatch() { await setRuntimeLifecycle("OFF"); },
1676
1675
  async fenceRouteOutput() { await setRuntimeLifecycle("ARCHIVED"); },
1677
1676
  async drain() {
1678
- const base = new URL(config.runtimeControlUrl);
1679
- const response = await fetch(new URL("/drain", base), { method: "POST", signal: AbortSignal.timeout(15_000) });
1680
- if (!response.ok) throw new Error("drain rejected");
1681
- const observed = await response.json();
1682
- if (!observed?.drained || observed.remaining !== 0) throw new Error("drain readback rejected");
1677
+ await controlRuntime("DOWN");
1678
+ const observed = { drained: true, remaining: 0 };
1683
1679
  atomicPrivateJson(join(realpathSync(config.stateRoot), `drain-${context.activeClaim.operation.id}.json`), {
1684
1680
  operationId: context.activeClaim.operation.id,
1685
1681
  fence: context.activeClaim.operation.fence,
@@ -131,8 +131,8 @@ function validateDesired(desired) {
131
131
  !Number.isSafeInteger(desired.slackGeneration) || desired.slackGeneration < 1 ||
132
132
  !Number.isSafeInteger(desired.serviceCredentialGeneration) || desired.serviceCredentialGeneration < 1 ||
133
133
  desired.hermesCli !== "hermes" || desired.hermesVersion !== "0.18.0" ||
134
- desired.installerPackage !== "@sellable/install@0.1.357" ||
135
- desired.mcpPackage !== "@sellable/mcp@0.1.619" ||
134
+ desired.installerPackage !== "@sellable/install@0.1.362" ||
135
+ desired.mcpPackage !== "@sellable/mcp@0.1.620" ||
136
136
  !Array.isArray(desired.toolInclude) || desired.toolInclude.length === 0 ||
137
137
  desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
138
138
  new Set(desired.toolInclude).size !== desired.toolInclude.length ||
@@ -19,8 +19,8 @@ import { fileURLToPath } from "node:url";
19
19
  import { deriveContainedProfileId } from "./profile-materializer.mjs";
20
20
 
21
21
  export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
22
- export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.357";
23
- export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.619";
22
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.362";
23
+ export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.620";
24
24
 
25
25
  export function deriveAgentProfileId(workspaceId, agentId, hostId) {
26
26
  return deriveContainedProfileId(workspaceId, agentId, hostId);
@@ -576,7 +576,7 @@ function inspectProvisionedProfile({
576
576
  !matchesReadback(manifest, request) ||
577
577
  manifest.serviceCredentialReference !== serviceCredentialReference ||
578
578
  manifest.installerPackage !== PINNED_INSTALL_PACKAGE ||
579
- manifest.installerVersion !== "0.1.357" ||
579
+ manifest.installerVersion !== "0.1.362" ||
580
580
  manifest.mcpPackage !== PINNED_MCP_PACKAGE ||
581
581
  manifest.credentialKeyVersion !== credentialKeyVersion ||
582
582
  !/^[a-f0-9]{16}$/.test(manifest.credentialFingerprint) ||
@@ -781,7 +781,7 @@ function successReceipt({
781
781
  subject: `agent-profile:${observed.profileId}`,
782
782
  cli: {
783
783
  installPackage: PINNED_INSTALL_PACKAGE,
784
- installVersion: "0.1.357",
784
+ installVersion: "0.1.362",
785
785
  mcpPackage: PINNED_MCP_PACKAGE,
786
786
  command: "hermes profile bootstrap",
787
787
  },
@@ -821,7 +821,7 @@ function failure(code, stages, extra = {}, request = null) {
821
821
  subject: safeIdentity ? `agent-profile:${safeIdentity}` : "agent-profile:unbound",
822
822
  cli: {
823
823
  installPackage: PINNED_INSTALL_PACKAGE,
824
- installVersion: "0.1.357",
824
+ installVersion: "0.1.362",
825
825
  mcpPackage: PINNED_MCP_PACKAGE,
826
826
  command: "hermes profile bootstrap",
827
827
  },