@sellable/install 0.1.357 → 0.1.358

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,26 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { lstatSync, readFileSync } from "node:fs";
4
+ import { isAbsolute } from "node:path";
5
+ import { bootstrapAgentHost } from "../lib/sellable-agent/host-bootstrap.mjs";
6
+
7
+ function configPath(argv) {
8
+ if (argv.length !== 2 || argv[0] !== "--config" || !isAbsolute(argv[1])) {
9
+ throw new Error("usage: --config /absolute/path");
10
+ }
11
+ const link = lstatSync(argv[1]);
12
+ if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 || link.uid !== 0 || link.gid !== 0) {
13
+ throw new Error("host bootstrap config rejected");
14
+ }
15
+ return argv[1];
16
+ }
17
+
18
+ try {
19
+ if (process.getuid?.() !== 0 || process.getgid?.() !== 0) throw new Error("host bootstrap requires root");
20
+ const pathValue = configPath(process.argv.slice(2));
21
+ const result = bootstrapAgentHost({ config: JSON.parse(readFileSync(pathValue, "utf8")) });
22
+ process.stdout.write(`${JSON.stringify(result.receipt)}\n`);
23
+ } catch {
24
+ process.stderr.write("sellable-agent host bootstrap failed closed\n");
25
+ process.exitCode = 1;
26
+ }
@@ -0,0 +1,37 @@
1
+ ARG HERMES_IMAGE=ghcr.io/hostinger/hvps-hermes-agent@sha256:f048663be9ba564de795df5f2a8184f32b92561a1f053c179dca3b45fa5bc8b9
2
+ FROM ${HERMES_IMAGE}
3
+
4
+ USER root
5
+
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.358
7
+ ARG MCP_PACKAGE=@sellable/mcp@0.1.619
8
+ ARG INSTALLER_INTEGRITY
9
+ ARG MCP_INTEGRITY=sha512-ZepZ/YDN09SEyBNKfNEHz751z8s4VQZhvWFUnJmw0mc46YUN2OcJ7s3uN90v4ykNTSnlBrUzUCjhlY3B869aEQ==
10
+
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.358" \
12
+ && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.619" \
13
+ && test -n "${INSTALLER_INTEGRITY}" \
14
+ && test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
15
+ && test "$(npm view "${MCP_PACKAGE}" dist.integrity)" = "${MCP_INTEGRITY}" \
16
+ && apt-get update \
17
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
18
+ bubblewrap \
19
+ ca-certificates \
20
+ nftables \
21
+ sudo \
22
+ && rm -rf /var/lib/apt/lists/* \
23
+ && mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
24
+ && npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
25
+ && npm install --prefix /opt/sellable-agent/mcp-root --omit=dev --ignore-scripts --no-audit --no-fund "${MCP_PACKAGE}" \
26
+ && node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.358' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.619') throw new Error('package identity rejected');" \
27
+ && node --input-type=module -e "import { buildExternalRuntimeClosure } from '/opt/sellable-agent/install-root/node_modules/@sellable/install/lib/sellable-agent/external-runtime-builder.mjs'; const built = buildExternalRuntimeClosure({ mcpPackageRoot: '/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp', mcpNodeModulesRoot: '/opt/sellable-agent/mcp-root/node_modules', hermesSourceRoot: '/opt/hermes', ownerUid: 0, ownerGid: 0 }); if (!built.closureDigest) throw new Error('runtime closure rejected');" \
28
+ && node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.358', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.619', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
29
+ && npm cache clean --force
30
+
31
+ COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
32
+ RUN chmod 0555 /usr/local/bin/sellable-agent-container-entrypoint
33
+
34
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
35
+ CMD /command/s6-svstat /etc/services.d/sellable-agent-host-worker | grep -q '^up' || exit 1
36
+
37
+ ENTRYPOINT ["/usr/local/bin/sellable-agent-container-entrypoint"]
@@ -0,0 +1,15 @@
1
+ # Sellable Agent host container
2
+
3
+ This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
4
+
5
+ Build from this directory after `@sellable/install@0.1.358` is published:
6
+
7
+ ```sh
8
+ docker build --platform linux/amd64 \
9
+ --build-arg INSTALLER_INTEGRITY='sha512-…' \
10
+ --tag sellable-agent-host:0.1.358 .
11
+ ```
12
+
13
+ Create a private `bootstrap/` directory owned by root with mode `0700`. It must contain `bootstrap.json`, `provider-auth`, and `slack-app`, each owned by root with mode `0600`. `bootstrap.json` contains only non-secret identities, hashes, package integrities, and the control-plane URL. The other two files contain the provider JSON and global Slack app token whose hashes are pinned by `bootstrap.json`.
14
+
15
+ Set `SELLABLE_AGENT_IMAGE` to the exact built image digest and start `compose.yaml`. The container bootstraps idempotently, writes `bootstrap/registration-receipt.json`, and starts the worker and isolated runtime under s6. Do not mount a Docker socket.
@@ -0,0 +1,22 @@
1
+ services:
2
+ sellable-agent-host:
3
+ image: ${SELLABLE_AGENT_IMAGE:?set exact Sellable Agent image}
4
+ container_name: ${SELLABLE_AGENT_CONTAINER_NAME:-sellable-agent-host-11201}
5
+ hostname: ${SELLABLE_AGENT_HOSTNAME:-hermes-hostinger-1807396}
6
+ restart: unless-stopped
7
+ cap_add:
8
+ - NET_ADMIN
9
+ - SYS_ADMIN
10
+ cgroup: host
11
+ pid: host
12
+ volumes:
13
+ - ./bootstrap:/var/lib/sellable-agent-bootstrap:rw
14
+ - sellable-agent-worker:/var/lib/sellable-agent-worker
15
+ - sellable-agent-runtime:/var/lib/sellable-agent-runtime
16
+ - sellable-agent-profiles:/var/lib/sellable-agent-profiles
17
+ - /sys/fs/cgroup:/sys/fs/cgroup:rw
18
+
19
+ volumes:
20
+ sellable-agent-worker:
21
+ sellable-agent-runtime:
22
+ sellable-agent-profiles:
@@ -0,0 +1,13 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ BOOTSTRAP_CONFIG=/var/lib/sellable-agent-bootstrap/bootstrap.json
5
+ BOOTSTRAP_BIN=/opt/sellable-agent/install-root/node_modules/.bin/sellable-agent-host-bootstrap
6
+
7
+ test "$(id -u)" = "0"
8
+ test "$(id -g)" = "0"
9
+ test -x "${BOOTSTRAP_BIN}"
10
+ test -f "${BOOTSTRAP_CONFIG}"
11
+
12
+ "${BOOTSTRAP_BIN}" --config "${BOOTSTRAP_CONFIG}"
13
+ exec /command/s6-svscan /etc/services.d
@@ -0,0 +1,272 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ chownSync,
5
+ existsSync,
6
+ lstatSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ realpathSync,
11
+ renameSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
16
+ import {
17
+ HERMES_AGENT_BRIDGE_CONTRACT,
18
+ installHermesAgentBridge,
19
+ verifyHermesAgentBridge,
20
+ } from "./hermes-bridge.mjs";
21
+ import {
22
+ EXTERNAL_HERMES_ENTRYPOINT,
23
+ EXTERNAL_MCP_ENTRYPOINT,
24
+ EXTERNAL_RUNTIME_CLOSURE_ROOT,
25
+ EXTERNAL_RUNTIME_MANIFEST_PATH,
26
+ observeExternalRuntimeClosure,
27
+ } from "./runtime-helper.mjs";
28
+
29
+ const MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
30
+ const MCP_PACKAGE = "@sellable/mcp";
31
+ const MCP_VERSION = "0.1.619";
32
+ const SAFE_COMPONENT = /^[A-Za-z0-9@._+-]+$/;
33
+ const HERMES_EXCLUDES = new Set([".git", ".playwright", "node_modules"]);
34
+
35
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
36
+
37
+ function mapped(pathRoot, absolutePath) {
38
+ const root = realpathSync(resolve(pathRoot));
39
+ const target = root === "/"
40
+ ? resolve(absolutePath)
41
+ : join(root, absolutePath.replace(/^\/+/, ""));
42
+ const confined = relative(root, target);
43
+ if (!isAbsolute(absolutePath) || confined.startsWith("..") || isAbsolute(confined)) {
44
+ throw new Error("external runtime build target rejected");
45
+ }
46
+ return target;
47
+ }
48
+
49
+ function trustedDirectory(pathValue) {
50
+ const resolved = realpathSync(resolve(pathValue));
51
+ const link = lstatSync(resolved);
52
+ if (
53
+ link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o022) !== 0
54
+ ) throw new Error("external runtime source rejected");
55
+ return resolved;
56
+ }
57
+
58
+ function copyFile(source, target, transform = (bytes) => bytes) {
59
+ const link = lstatSync(source);
60
+ if (!link.isFile() || link.isSymbolicLink()) throw new Error("external runtime source file rejected");
61
+ const bytes = transform(readFileSync(source));
62
+ const mode = (link.mode & 0o111) === 0 ? 0o644 : 0o755;
63
+ writeFileSync(target, bytes, { flag: "wx", mode });
64
+ chmodSync(target, mode);
65
+ }
66
+
67
+ function hermesScript(bytes) {
68
+ const source = bytes.toString("utf8");
69
+ const newline = source.indexOf("\n");
70
+ const first = newline < 0 ? source : source.slice(0, newline);
71
+ if (!first.startsWith("#!/opt/hermes/.venv/bin/python")) return bytes;
72
+ const rest = newline < 0 ? "" : source.slice(newline);
73
+ return Buffer.from(`#!${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes/venv/bin/python3${rest}`);
74
+ }
75
+
76
+ function copyTree({ sourceRoot, targetRoot, kind, logicalRoot = "" }) {
77
+ for (const name of readdirSync(sourceRoot).sort()) {
78
+ if (!SAFE_COMPONENT.test(name)) throw new Error("external runtime source name rejected");
79
+ if (kind === "hermes" && HERMES_EXCLUDES.has(name)) continue;
80
+ if (kind === "mcp-dependencies" && [".bin", ".package-lock.json"].includes(name)) continue;
81
+ if (kind === "mcp-dependencies" && logicalRoot === "" && name === "@sellable") {
82
+ const scopedSource = join(sourceRoot, name);
83
+ const scopedTarget = join(targetRoot, name);
84
+ mkdirSync(scopedTarget, { mode: 0o755 });
85
+ for (const scopedName of readdirSync(scopedSource).sort()) {
86
+ if (scopedName === "mcp" || scopedName === "install") continue;
87
+ if (!SAFE_COMPONENT.test(scopedName)) throw new Error("external runtime source name rejected");
88
+ const nestedSource = join(scopedSource, scopedName);
89
+ const nestedTarget = join(scopedTarget, scopedName);
90
+ const nested = lstatSync(nestedSource);
91
+ if (!nested.isDirectory() || nested.isSymbolicLink()) {
92
+ throw new Error("external runtime dependency rejected");
93
+ }
94
+ mkdirSync(nestedTarget, { mode: 0o755 });
95
+ copyTree({
96
+ sourceRoot: nestedSource,
97
+ targetRoot: nestedTarget,
98
+ kind,
99
+ logicalRoot: `${name}/${scopedName}`,
100
+ });
101
+ }
102
+ if (readdirSync(scopedTarget).length === 0) rmSync(scopedTarget, { recursive: true });
103
+ continue;
104
+ }
105
+ const outputName = kind === "hermes" && logicalRoot === "" && name === ".venv"
106
+ ? "venv"
107
+ : name;
108
+ const source = join(sourceRoot, name);
109
+ const target = join(targetRoot, outputName);
110
+ const logicalPath = logicalRoot ? `${logicalRoot}/${outputName}` : outputName;
111
+ const link = lstatSync(source);
112
+ if (link.isSymbolicLink()) {
113
+ if (kind !== "hermes") throw new Error("external runtime dependency symlink rejected");
114
+ if (logicalPath === "venv/lib64") continue;
115
+ if (!["venv/bin/python", "venv/bin/python3", "venv/bin/python3.13"].includes(logicalPath)) {
116
+ throw new Error("external Hermes symlink rejected");
117
+ }
118
+ copyFile(realpathSync(source), target);
119
+ continue;
120
+ }
121
+ if (link.isDirectory()) {
122
+ mkdirSync(target, { mode: 0o755 });
123
+ copyTree({ sourceRoot: source, targetRoot: target, kind, logicalRoot: logicalPath });
124
+ continue;
125
+ }
126
+ if (!link.isFile()) throw new Error("external runtime special source rejected");
127
+ copyFile(source, target, kind === "hermes" && logicalPath.startsWith("venv/bin/")
128
+ ? hermesScript
129
+ : undefined);
130
+ }
131
+ }
132
+
133
+ function normalizeTree(root, ownerUid, ownerGid) {
134
+ const directories = [];
135
+ const files = [];
136
+ const visit = (directory, logicalDirectory) => {
137
+ for (const name of readdirSync(directory).sort()) {
138
+ const pathValue = join(directory, name);
139
+ const logicalPath = logicalDirectory ? `${logicalDirectory}/${name}` : name;
140
+ const link = lstatSync(pathValue);
141
+ if (link.isSymbolicLink()) throw new Error("external runtime output symlink rejected");
142
+ if (link.isDirectory()) {
143
+ visit(pathValue, logicalPath);
144
+ chmodSync(pathValue, 0o555);
145
+ chownSync(pathValue, ownerUid, ownerGid);
146
+ directories.push({ path: logicalPath, mode: 0o555 });
147
+ } else if (link.isFile()) {
148
+ const mode = (link.mode & 0o111) === 0 ? 0o444 : 0o555;
149
+ chmodSync(pathValue, mode);
150
+ chownSync(pathValue, ownerUid, ownerGid);
151
+ files.push({ path: logicalPath, mode, digest: sha256(readFileSync(pathValue)) });
152
+ } else {
153
+ throw new Error("external runtime output special file rejected");
154
+ }
155
+ }
156
+ };
157
+ visit(root, "");
158
+ chmodSync(root, 0o555);
159
+ chownSync(root, ownerUid, ownerGid);
160
+ return {
161
+ directories: directories.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0),
162
+ files: files.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0),
163
+ };
164
+ }
165
+
166
+ function packageIdentity(root) {
167
+ const parsed = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
168
+ if (parsed?.name !== MCP_PACKAGE || parsed.version !== MCP_VERSION) {
169
+ throw new Error("external MCP package identity rejected");
170
+ }
171
+ }
172
+
173
+ function bridgeContract(input) {
174
+ return input.disposableFixture === true && input.hermesBridgeContract
175
+ ? input.hermesBridgeContract
176
+ : HERMES_AGENT_BRIDGE_CONTRACT;
177
+ }
178
+
179
+ function currentClosure(input, pathRoot, ownerUid, ownerGid) {
180
+ const observed = observeExternalRuntimeClosure({
181
+ pathRoot,
182
+ manifestPath: EXTERNAL_RUNTIME_MANIFEST_PATH,
183
+ expectedClosureRoot: EXTERNAL_RUNTIME_CLOSURE_ROOT,
184
+ trustRoot: dirname(EXTERNAL_RUNTIME_CLOSURE_ROOT),
185
+ ownerUid,
186
+ ownerGid,
187
+ expectedEntrypoints: { mcp: EXTERNAL_MCP_ENTRYPOINT, hermes: EXTERNAL_HERMES_ENTRYPOINT },
188
+ });
189
+ const root = mapped(pathRoot, EXTERNAL_RUNTIME_CLOSURE_ROOT);
190
+ packageIdentity(join(root, "mcp"));
191
+ verifyHermesAgentBridge({
192
+ sourceRoot: join(root, "hermes"),
193
+ contract: bridgeContract(input),
194
+ });
195
+ return { ...observed, reused: true };
196
+ }
197
+
198
+ export function buildExternalRuntimeClosure(input = {}) {
199
+ const pathRoot = resolve(input.pathRoot ?? "/");
200
+ if (!isAbsolute(pathRoot) || !existsSync(pathRoot)) throw new Error("external runtime path root rejected");
201
+ if (pathRoot !== "/" && input.disposableFixture !== true) {
202
+ throw new Error("external runtime live root rejected");
203
+ }
204
+ const ownerUid = input.ownerUid ?? process.getuid?.() ?? 0;
205
+ const ownerGid = input.ownerGid ?? process.getgid?.() ?? 0;
206
+ if (!Number.isSafeInteger(ownerUid) || ownerUid < 0 || !Number.isSafeInteger(ownerGid) || ownerGid < 0) {
207
+ throw new Error("external runtime owner rejected");
208
+ }
209
+ const mcpPackageRoot = trustedDirectory(input.mcpPackageRoot);
210
+ const mcpNodeModulesRoot = trustedDirectory(input.mcpNodeModulesRoot);
211
+ const hermesSourceRoot = trustedDirectory(input.hermesSourceRoot);
212
+ packageIdentity(mcpPackageRoot);
213
+ const target = mapped(pathRoot, EXTERNAL_RUNTIME_CLOSURE_ROOT);
214
+ if (existsSync(target)) return currentClosure(input, pathRoot, ownerUid, ownerGid);
215
+
216
+ const trustRoot = dirname(target);
217
+ if (!existsSync(trustRoot)) mkdirSync(trustRoot, { recursive: true, mode: 0o755 });
218
+ const staging = join(trustRoot, `.external-runtime-${randomUUID()}`);
219
+ mkdirSync(staging, { mode: 0o755 });
220
+ try {
221
+ const mcpRoot = join(staging, "mcp");
222
+ const dependencyRoot = join(mcpRoot, "node_modules");
223
+ const hermesRoot = join(staging, "hermes");
224
+ mkdirSync(mcpRoot, { mode: 0o755 });
225
+ mkdirSync(dependencyRoot, { mode: 0o755 });
226
+ mkdirSync(hermesRoot, { mode: 0o755 });
227
+ copyTree({ sourceRoot: mcpPackageRoot, targetRoot: mcpRoot, kind: "mcp" });
228
+ copyTree({ sourceRoot: mcpNodeModulesRoot, targetRoot: dependencyRoot, kind: "mcp-dependencies" });
229
+ mkdirSync(join(mcpRoot, "bin"), { mode: 0o755 });
230
+ writeFileSync(
231
+ join(mcpRoot, "bin", "sellable-mcp"),
232
+ `#!/bin/sh\nexec /usr/bin/node '${EXTERNAL_RUNTIME_CLOSURE_ROOT}/mcp/dist/index.js' "$@"\n`,
233
+ { mode: 0o755, flag: "wx" },
234
+ );
235
+ copyTree({ sourceRoot: hermesSourceRoot, targetRoot: hermesRoot, kind: "hermes" });
236
+ installHermesAgentBridge({ sourceRoot: hermesRoot, contract: bridgeContract(input) });
237
+
238
+ const mcp = normalizeTree(mcpRoot, ownerUid, ownerGid);
239
+ const hermes = normalizeTree(hermesRoot, ownerUid, ownerGid);
240
+ const manifest = {
241
+ version: MANIFEST_VERSION,
242
+ closureRoot: EXTERNAL_RUNTIME_CLOSURE_ROOT,
243
+ roots: [
244
+ { kind: "mcp", entrypoint: "bin/sellable-mcp", rootMode: 0o555, ...mcp },
245
+ { kind: "hermes", entrypoint: "venv/bin/hermes", rootMode: 0o555, ...hermes },
246
+ ],
247
+ };
248
+ writeFileSync(join(staging, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, {
249
+ mode: 0o444,
250
+ flag: "wx",
251
+ });
252
+ chmodSync(join(staging, "manifest.json"), 0o444);
253
+ chownSync(join(staging, "manifest.json"), ownerUid, ownerGid);
254
+ chownSync(staging, ownerUid, ownerGid);
255
+ renameSync(staging, target);
256
+ chmodSync(target, 0o555);
257
+ return { ...currentClosure(input, pathRoot, ownerUid, ownerGid), reused: false };
258
+ } catch (error) {
259
+ if (existsSync(staging)) {
260
+ const makeWritable = (pathValue) => {
261
+ const link = lstatSync(pathValue);
262
+ if (link.isDirectory()) {
263
+ chmodSync(pathValue, 0o755);
264
+ for (const name of readdirSync(pathValue)) makeWritable(join(pathValue, name));
265
+ } else if (link.isFile()) chmodSync(pathValue, 0o644);
266
+ };
267
+ makeWritable(staging);
268
+ rmSync(staging, { recursive: true });
269
+ }
270
+ throw error;
271
+ }
272
+ }
@@ -285,14 +285,14 @@ export const HERMES_AGENT_BRIDGE_CONTRACT = normalizeContract({
285
285
  {
286
286
  path: "plugins/platforms/slack/adapter.py",
287
287
  patchKind: "slack-adapter",
288
- sourceSha256: "a84f1f15368d8d93341a1c2449c45ca13ebe92938049e92d80ce5cab0e1a6294",
289
- patchedSha256: "4ba3b1f5b443abf970b8888de3c1b21ab7f043645aba6304432453c1dc643f55",
288
+ sourceSha256: "20b12d9bbff0f5b280e0be8e795c7327f4089c3fa8b04d7a6d346dabae591f2a",
289
+ patchedSha256: "548ab70f515e22830cbcbf6b0691768272dd708fd0118f7c601e1e1232bb016b",
290
290
  },
291
291
  {
292
292
  path: "tools/mcp_tool.py",
293
293
  patchKind: "mcp-tool",
294
- sourceSha256: "ee453ede1c72c90b28155e292a352bfba96eb1793c47eb8667d16fe79beb181e",
295
- patchedSha256: "c0923d8c88d507a766121dab9f0f36e99963f85a9124cbef2908ee7176d56e07",
294
+ sourceSha256: "1bcceccaeb8ec8894c83819d389a64258517f4b48db1c8fea5c486633e56fe8b",
295
+ patchedSha256: "44bf5dfeca47bf91fc5e68ecd6455c33f41afd2b04fa16d213f59115ddb38f21",
296
296
  },
297
297
  ],
298
298
  });
@@ -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.358";
42
+ const MCP_PACKAGE = "@sellable/mcp@0.1.619";
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,7 +957,7 @@ 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" ||
960
+ pinned.installerPackage !== "@sellable/install@0.1.358" ||
961
961
  pinned.mcpPackage !== "@sellable/mcp@0.1.619" ||
962
962
  !Array.isArray(policy.toolInclude) || policy.toolInclude.length === 0 ||
963
963
  !Number.isSafeInteger(slack.generation) || slack.generation < 1 ||
@@ -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,7 +131,7 @@ 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" ||
134
+ desired.installerPackage !== "@sellable/install@0.1.358" ||
135
135
  desired.mcpPackage !== "@sellable/mcp@0.1.619" ||
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)) ||
@@ -19,7 +19,7 @@ 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";
22
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.358";
23
23
  export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.619";
24
24
 
25
25
  export function deriveAgentProfileId(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.358" ||
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.358",
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.358",
825
825
  mcpPackage: PINNED_MCP_PACKAGE,
826
826
  command: "hermes profile bootstrap",
827
827
  },
@@ -23,6 +23,7 @@ export const EXTERNAL_HERMES_ENTRYPOINT = `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/herm
23
23
  const EXTERNAL_RUNTIME_MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
24
24
  const ACTIONS = new Set([
25
25
  "INSTALL_MCP_CREDENTIAL",
26
+ "INSTALL_PROVIDER_CREDENTIAL",
26
27
  "INSTALL_RUNTIME_SERVICE_ENV",
27
28
  "SET_RUNTIME_GATES",
28
29
  "OBSERVE_RUNTIME_GATES",
@@ -50,6 +51,7 @@ const PROFILE_ENTRIES = Object.freeze([
50
51
  ]);
51
52
  const ACTION_KEYS = Object.freeze({
52
53
  INSTALL_MCP_CREDENTIAL: ["action", "profileId", "operationId", "fence", "sourceRequestFile", "generation", "fingerprint"],
54
+ INSTALL_PROVIDER_CREDENTIAL: ["action", "profileId", "operationId", "fence", "fingerprint"],
53
55
  INSTALL_RUNTIME_SERVICE_ENV: ["action", "profileId", "operationId", "fence", "revisionId", "profileDigest"],
54
56
  SET_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
55
57
  OBSERVE_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
@@ -109,6 +111,79 @@ function atomicCredential(pathValue, secret, uid, gid) {
109
111
  fsyncDirectory(dirname(pathValue));
110
112
  }
111
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
+
112
187
  function applyRuntimeOwnership(pathValue, uid, gid) {
113
188
  const link = lstatSync(pathValue);
114
189
  if (link.isSymbolicLink()) throw new Error("profile symlink rejected");
@@ -509,7 +584,9 @@ function walkExternalRoot(root, ownerUid, ownerGid) {
509
584
  }
510
585
  };
511
586
  visit(root, "");
512
- return { directories: directories.sort((left, right) => left.path.localeCompare(right.path)), files };
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) };
513
590
  }
514
591
 
515
592
  export function observeExternalRuntimeClosure(input = {}) {
@@ -569,18 +646,24 @@ export function observeExternalRuntimeClosure(input = {}) {
569
646
  if (
570
647
  JSON.stringify(rootRecord.directories.map((directory) => directory?.path)) !==
571
648
  JSON.stringify(rootRecord.directories.map((directory) => directory?.path).sort()) ||
572
- new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length ||
649
+ new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length
650
+ ) throw new Error("external runtime directory order rejected");
651
+ if (
573
652
  rootRecord.directories.some((directory) =>
574
653
  !exactKeys(directory, ["path", "mode"]) ||
575
- !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(directory.path ?? "") ||
654
+ !/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(directory.path ?? "") ||
576
655
  directory.mode !== 0o555
577
- ) ||
578
- JSON.stringify(walked.directories) !== JSON.stringify(rootRecord.directories) ||
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 (
579
662
  rootRecord.files.length === 0 ||
580
663
  JSON.stringify(rootRecord.files.map((file) => file?.path)) !==
581
664
  JSON.stringify(rootRecord.files.map((file) => file?.path).sort()) ||
582
665
  new Set(rootRecord.files.map((file) => file?.path)).size !== rootRecord.files.length
583
- ) throw new Error("external runtime inventory rejected");
666
+ ) throw new Error("external runtime file order rejected");
584
667
  const actualPaths = walked.files.map((file) => file.logicalPath);
585
668
  if (JSON.stringify(actualPaths) !== JSON.stringify(rootRecord.files.map((file) => file.path))) {
586
669
  throw new Error("external runtime open inventory rejected");
@@ -588,7 +671,7 @@ export function observeExternalRuntimeClosure(input = {}) {
588
671
  const observedFiles = rootRecord.files.map((file, index) => {
589
672
  if (
590
673
  !exactKeys(file, ["path", "mode", "digest"]) ||
591
- !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(file.path ?? "") ||
674
+ !/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(file.path ?? "") ||
592
675
  ![0o444, 0o555].includes(file.mode) || !SHA256.test(file.digest ?? "")
593
676
  ) throw new Error("external runtime file schema rejected");
594
677
  const actual = walked.files[index];
@@ -958,9 +1041,19 @@ async function observeRuntime(profile, request) {
958
1041
  const profileFiles = observeRuntimeProfileFiles(target);
959
1042
  const { runtimeConfig, runtimeMcp, tools, fileHashes: actualFileHashes } = profileFiles;
960
1043
  const actualProfileDigest = profileFiles.profileDigest;
961
- const observedProvider = environmentDrift
962
- ? sha256(`environment-drift\0${runtimeConfig?.provider?.fingerprint ?? "absent"}`)
963
- : runtimeConfig?.provider?.fingerprint;
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
+ }
964
1057
  return {
965
1058
  profileId: manifest.profileId,
966
1059
  workspaceId: manifest.workspaceId,
@@ -1172,6 +1265,9 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
1172
1265
  generation: request.generation,
1173
1266
  };
1174
1267
  }
1268
+ if (request.action === "INSTALL_PROVIDER_CREDENTIAL") {
1269
+ return installProviderCredential(profile, request, config);
1270
+ }
1175
1271
  if (request.action === "INSTALL_RUNTIME_SERVICE_ENV") {
1176
1272
  return installRuntimeServiceEnvironment(profile, request);
1177
1273
  }
@@ -1187,6 +1283,7 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
1187
1283
  rmSync(target, { recursive: true, force: true });
1188
1284
  fsyncDirectory(root);
1189
1285
  }
1286
+ removeProviderCredential(profile, request.profileId);
1190
1287
  return { ok: true, profileId: request.profileId, disposition: "REMOVED" };
1191
1288
  }
1192
1289
  if (request.action === "CHOWN_PROFILE") {
@@ -247,6 +247,8 @@ function validateInstallInput(input) {
247
247
  profile.hermesSourceRoot !== `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes` ||
248
248
  profile.proxyConfigPath !== "/etc/sellable-agent/egress-proxy.json" ||
249
249
  profile.runtimeBoundaryConfigPath !== "/etc/sellable-agent/runtime-boundary.json" ||
250
+ profile.providerCredentialSourcePath !== "/var/lib/sellable-agent-bootstrap/provider-auth" ||
251
+ !SHA256.test(profile.providerCredentialFingerprint ?? "") ||
250
252
  profile.runtimeServicePath !== "/etc/services.d/sellable-agent-runtime" ||
251
253
  profile.runtimeServiceEnvRoot !== "/etc/services.d/sellable-agent-runtime/env" ||
252
254
  new Set([profile.runtimeUid, profile.proxyUid, profile.workerUid]).size !== 3
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.357",
3
+ "version": "0.1.358",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {
7
7
  "sellable": "bin/sellable-install.mjs",
8
+ "sellable-agent-host-bootstrap": "bin/sellable-agent-host-bootstrap.mjs",
8
9
  "sellable-agent-egress-proxy": "bin/sellable-agent-egress-proxy.mjs",
9
10
  "sellable-agent-host-worker": "bin/sellable-agent-host-worker.mjs",
10
11
  "sellable-agent-runtime-helper": "bin/sellable-agent-runtime-helper.mjs",
@@ -20,6 +21,7 @@
20
21
  "bin",
21
22
  "lib",
22
23
  "services",
24
+ "container",
23
25
  "agents",
24
26
  "skill-templates",
25
27
  "README.md",