@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.
- package/bin/sellable-agent-host-bootstrap.mjs +26 -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/external-runtime-builder.mjs +272 -0
- package/lib/sellable-agent/hermes-bridge.mjs +4 -4
- package/lib/sellable-agent/host-bootstrap.mjs +458 -0
- package/lib/sellable-agent/host-worker.mjs +15 -19
- package/lib/sellable-agent/profile-materializer.mjs +1 -1
- package/lib/sellable-agent/provisioning-adapter.mjs +4 -4
- package/lib/sellable-agent/runtime-helper.mjs +107 -10
- package/lib/sellable-agent/service-installer.mjs +2 -0
- package/package.json +3 -1
|
@@ -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: "
|
|
289
|
-
patchedSha256: "
|
|
288
|
+
sourceSha256: "20b12d9bbff0f5b280e0be8e795c7327f4089c3fa8b04d7a6d346dabae591f2a",
|
|
289
|
+
patchedSha256: "548ab70f515e22830cbcbf6b0691768272dd708fd0118f7c601e1e1232bb016b",
|
|
290
290
|
},
|
|
291
291
|
{
|
|
292
292
|
path: "tools/mcp_tool.py",
|
|
293
293
|
patchKind: "mcp-tool",
|
|
294
|
-
sourceSha256: "
|
|
295
|
-
patchedSha256: "
|
|
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.
|
|
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(
|
|
1575
|
-
reloadSkills: refreshThen(
|
|
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
|
-
|
|
1679
|
-
const
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
654
|
+
!/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(directory.path ?? "") ||
|
|
576
655
|
directory.mode !== 0o555
|
|
577
|
-
)
|
|
578
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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.
|
|
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",
|