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