@sellable/install 0.1.359-phase111.20260720052618 → 0.1.359

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/bin/sellable-agent-egress-proxy.mjs +31 -0
  2. package/bin/sellable-agent-host-bootstrap.mjs +26 -0
  3. package/bin/sellable-agent-host-worker.mjs +9 -0
  4. package/bin/sellable-agent-runtime-helper.mjs +37 -0
  5. package/bin/sellable-agent-runtime-launcher.mjs +151 -0
  6. package/bin/sellable-agent-runtime-probe.mjs +163 -0
  7. package/bin/sellable-agent-sandbox-init.mjs +93 -0
  8. package/bin/sellable-install.mjs +2 -0
  9. package/container/Dockerfile +37 -0
  10. package/container/README.md +15 -0
  11. package/container/compose.yaml +22 -0
  12. package/container/entrypoint.sh +13 -0
  13. package/lib/sellable-agent/containment-contract.mjs +472 -0
  14. package/lib/sellable-agent/external-runtime-builder.mjs +272 -0
  15. package/lib/sellable-agent/hermes-bridge.mjs +497 -0
  16. package/lib/sellable-agent/host-bootstrap.mjs +458 -0
  17. package/lib/sellable-agent/host-worker.mjs +2067 -0
  18. package/lib/sellable-agent/profile-materializer.mjs +1410 -0
  19. package/lib/sellable-agent/provisioning-adapter.mjs +992 -0
  20. package/lib/sellable-agent/runtime-boundary.mjs +635 -0
  21. package/lib/sellable-agent/runtime-egress-proxy.mjs +241 -0
  22. package/lib/sellable-agent/runtime-helper.mjs +1428 -0
  23. package/lib/sellable-agent/runtime-reconciler.mjs +683 -0
  24. package/lib/sellable-agent/service-installer.mjs +441 -0
  25. package/package.json +11 -2
  26. package/services/s6/sellable-agent-egress-proxy/run +15 -0
  27. package/services/s6/sellable-agent-host-worker/run +4 -0
  28. package/services/s6/sellable-agent-runtime/run +19 -0
  29. package/skill-templates/refill-sends-evergreen.md +0 -5
  30. package/skill-templates/refill-sends-v2.md +1 -23
  31. package/skill-templates/refill-sends.md +1 -14
@@ -0,0 +1,441 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import {
4
+ chmodSync, chownSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync,
5
+ openSync, readFileSync, realpathSync, renameSync, writeFileSync,
6
+ } from "node:fs";
7
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { validateRuntimeEgressProxyConfig } from "./runtime-egress-proxy.mjs";
10
+ import {
11
+ EXTERNAL_HERMES_ENTRYPOINT,
12
+ EXTERNAL_MCP_ENTRYPOINT,
13
+ EXTERNAL_RUNTIME_CLOSURE_ROOT,
14
+ EXTERNAL_RUNTIME_MANIFEST_PATH,
15
+ observeExternalRuntimeClosure,
16
+ } from "./runtime-helper.mjs";
17
+
18
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
19
+ const IMMUTABLE_PACKAGE_ROOT = "/usr/local/lib/sellable-agent/package";
20
+ const IMMUTABLE_MANIFEST_PATH = `${IMMUTABLE_PACKAGE_ROOT}/manifest.json`;
21
+ const IMMUTABLE_MANIFEST_VERSION = "sellable-agent-installed-closure/v1";
22
+ const AGENT_BIN_FILES = Object.freeze([
23
+ "sellable-agent-egress-proxy.mjs",
24
+ "sellable-agent-host-worker.mjs",
25
+ "sellable-agent-runtime-helper.mjs",
26
+ "sellable-agent-runtime-launcher.mjs",
27
+ "sellable-agent-runtime-probe.mjs",
28
+ "sellable-agent-sandbox-init.mjs",
29
+ ]);
30
+ const AGENT_LIB_FILES = Object.freeze([
31
+ "containment-contract.mjs",
32
+ "hermes-bridge.mjs",
33
+ "host-worker.mjs",
34
+ "profile-materializer.mjs",
35
+ "provisioning-adapter.mjs",
36
+ "runtime-boundary.mjs",
37
+ "runtime-egress-proxy.mjs",
38
+ "runtime-helper.mjs",
39
+ "runtime-reconciler.mjs",
40
+ "service-installer.mjs",
41
+ ]);
42
+ const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
43
+ const SHA256 = /^[a-f0-9]{64}$/;
44
+ const SECRET = /xox[bap]-|sat_[A-Za-z0-9_-]+|asec\.v1\.|access[_-]?token|refresh[_-]?token/i;
45
+
46
+ function sha256(value) {
47
+ return createHash("sha256").update(value).digest("hex");
48
+ }
49
+
50
+ function inside(root, target) {
51
+ const value = relative(root, target);
52
+ return value === "" || (!value.startsWith("..") && !isAbsolute(value));
53
+ }
54
+
55
+ function ensureTrustedParents(root, pathValue, uid, gid) {
56
+ const parent = dirname(pathValue);
57
+ if (!inside(root, parent)) throw new Error("service parent confinement failed");
58
+ const relativeParent = relative(root, parent);
59
+ let current = root;
60
+ for (const component of relativeParent.split(/[/\\]+/).filter(Boolean)) {
61
+ current = join(current, component);
62
+ if (!existsSync(current)) mkdirSync(current, { mode: 0o755 });
63
+ const link = lstatSync(current);
64
+ if (
65
+ link.isSymbolicLink() || !link.isDirectory() || link.uid !== uid || link.gid !== gid ||
66
+ (link.mode & 0o022) !== 0 || realpathSync(current) !== current
67
+ ) throw new Error("service parent rejected");
68
+ }
69
+ if (existsSync(pathValue) && lstatSync(pathValue).isSymbolicLink()) {
70
+ throw new Error("service target symlink rejected");
71
+ }
72
+ }
73
+
74
+ function atomicOwnedFile(root, pathValue, bytes, mode, uid, gid) {
75
+ ensureTrustedParents(root, pathValue, uid, gid);
76
+ const temp = join(dirname(pathValue), `.${randomUUID()}.tmp`);
77
+ writeFileSync(temp, bytes, { flag: "wx", mode });
78
+ chmodSync(temp, mode);
79
+ if (lstatSync(temp).uid !== uid || lstatSync(temp).gid !== gid) chownSync(temp, uid, gid);
80
+ const descriptor = openSync(temp, "r");
81
+ try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
82
+ renameSync(temp, pathValue);
83
+ const parent = openSync(dirname(pathValue), "r");
84
+ try { fsyncSync(parent); } finally { closeSync(parent); }
85
+ }
86
+
87
+ function accountDatabaseState(targetRoot, expected, ownerUid, ownerGid) {
88
+ const passwdPath = target(targetRoot, "/etc/passwd");
89
+ const groupPath = target(targetRoot, "/etc/group");
90
+ const passwdInfo = lstatSync(passwdPath);
91
+ const groupInfo = lstatSync(groupPath);
92
+ if (
93
+ passwdInfo.isSymbolicLink() || !passwdInfo.isFile() || (passwdInfo.mode & 0o022) !== 0 ||
94
+ passwdInfo.uid !== ownerUid || passwdInfo.gid !== ownerGid ||
95
+ groupInfo.isSymbolicLink() || !groupInfo.isFile() || (groupInfo.mode & 0o022) !== 0 ||
96
+ groupInfo.uid !== ownerUid || groupInfo.gid !== ownerGid
97
+ ) throw new Error("service account database rejected");
98
+ const passwd = readFileSync(passwdPath, "utf8").split(/\r?\n/).filter(Boolean).map((line) => line.split(":"));
99
+ const groups = readFileSync(groupPath, "utf8").split(/\r?\n/).filter(Boolean).map((line) => line.split(":"));
100
+ const state = expected.map((account) => {
101
+ const users = passwd.filter((fields) => fields[0] === account.name);
102
+ const groupRows = groups.filter((fields) => fields[0] === account.name);
103
+ const uidRows = passwd.filter((fields) => Number(fields[2]) === account.uid);
104
+ const gidRows = groups.filter((fields) => Number(fields[2]) === account.gid);
105
+ const userPresent = users.length === 1 && users[0].length >= 7 &&
106
+ Number(users[0][2]) === account.uid && Number(users[0][3]) === account.gid &&
107
+ users[0][5] === account.home && users[0][6] === "/usr/sbin/nologin" &&
108
+ uidRows.length === 1 && uidRows[0] === users[0];
109
+ const groupPresent = groupRows.length === 1 && groupRows[0].length >= 4 &&
110
+ Number(groupRows[0][2]) === account.gid && gidRows.length === 1 && gidRows[0] === groupRows[0];
111
+ if ((!userPresent && (users.length > 0 || uidRows.length > 0)) ||
112
+ (!groupPresent && (groupRows.length > 0 || gidRows.length > 0))) {
113
+ throw new Error("service account identity collision");
114
+ }
115
+ return { ...account, userPresent, groupPresent };
116
+ });
117
+ return state;
118
+ }
119
+
120
+ function readAccountDatabase(targetRoot, expected, ownerUid, ownerGid) {
121
+ const state = accountDatabaseState(targetRoot, expected, ownerUid, ownerGid);
122
+ if (state.some((account) => !account.userPresent || !account.groupPresent)) {
123
+ throw new Error("service account identity rejected");
124
+ }
125
+ return state.map(({ name, uid, gid }) => ({ name, uid, gid }));
126
+ }
127
+
128
+ function runAccountCommand(command, args) {
129
+ const result = spawnSync(command, args, {
130
+ encoding: "utf8",
131
+ env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" },
132
+ shell: false,
133
+ stdio: ["ignore", "pipe", "pipe"],
134
+ });
135
+ if (result.error || result.status !== 0 || result.signal || result.stderr?.trim()) {
136
+ throw new Error("service account provisioning failed");
137
+ }
138
+ }
139
+
140
+ export function buildServiceAccountProvisioningCommands(accounts) {
141
+ if (!Array.isArray(accounts) || accounts.some((account) =>
142
+ !SAFE_ID.test(account?.name ?? "") || !Number.isSafeInteger(account?.uid) || account.uid < 1 ||
143
+ !Number.isSafeInteger(account?.gid) || account.gid < 1 ||
144
+ !["/var/lib/sellable-agent-worker", "/var/lib/sellable-agent-runtime", "/var/empty"].includes(account?.home) ||
145
+ typeof account.createUser !== "boolean" || typeof account.createGroup !== "boolean"
146
+ )) throw new Error("service account command input rejected");
147
+ return [
148
+ ...accounts.filter((account) => account.createGroup).map((account) => ({
149
+ command: "/usr/sbin/groupadd",
150
+ args: ["--gid", String(account.gid), account.name],
151
+ })),
152
+ ...accounts.filter((account) => account.createUser).map((account) => ({
153
+ command: "/usr/sbin/useradd",
154
+ args: [
155
+ "--uid", String(account.uid), "--gid", account.name,
156
+ "--home-dir", account.home, "--shell", "/usr/sbin/nologin", "--no-create-home", account.name,
157
+ ],
158
+ })),
159
+ ];
160
+ }
161
+
162
+ function provisionServiceAccounts(input, checked) {
163
+ const initial = accountDatabaseState(
164
+ checked.targetRoot, checked.expectedAccounts, checked.uid, checked.gid,
165
+ );
166
+ const missing = initial.filter((account) => !account.userPresent || !account.groupPresent);
167
+ if (missing.length === 0) {
168
+ return readAccountDatabase(checked.targetRoot, checked.expectedAccounts, checked.uid, checked.gid);
169
+ }
170
+ if (checked.targetRoot !== "/") {
171
+ if (input.disposableFixture !== true || typeof input.accountProvisioner !== "function") {
172
+ throw new Error("fixture service account provisioner missing");
173
+ }
174
+ input.accountProvisioner({
175
+ targetRoot: checked.targetRoot,
176
+ accounts: missing.map(({ name, uid, gid, userPresent, groupPresent, home }) => ({
177
+ name, uid, gid, home, createUser: !userPresent, createGroup: !groupPresent,
178
+ })),
179
+ });
180
+ } else {
181
+ const commands = buildServiceAccountProvisioningCommands(missing.map((account) => ({
182
+ name: account.name,
183
+ uid: account.uid,
184
+ gid: account.gid,
185
+ home: account.home,
186
+ createUser: !account.userPresent,
187
+ createGroup: !account.groupPresent,
188
+ })));
189
+ for (const command of commands) {
190
+ runAccountCommand(command.command, command.args);
191
+ }
192
+ }
193
+ return readAccountDatabase(checked.targetRoot, checked.expectedAccounts, checked.uid, checked.gid);
194
+ }
195
+
196
+ function validateInstallInput(input) {
197
+ const targetRoot = resolve(input.targetRoot ?? "/");
198
+ if (!isAbsolute(targetRoot) || !existsSync(targetRoot) || lstatSync(targetRoot).isSymbolicLink()) {
199
+ throw new Error("service install root rejected");
200
+ }
201
+ if (targetRoot !== "/" && input.disposableFixture !== true) throw new Error("non-host install root rejected");
202
+ const packageRoot = realpathSync(input.packageRoot ?? PACKAGE_ROOT);
203
+ if (
204
+ packageRoot !== realpathSync(PACKAGE_ROOT) &&
205
+ !(input.disposableFixture === true && targetRoot !== "/")
206
+ ) throw new Error("service package root pin rejected");
207
+ const uid = input.ownerUid ?? process.getuid?.();
208
+ const gid = input.ownerGid ?? process.getgid?.();
209
+ if (!Number.isSafeInteger(uid) || uid < 0 || !Number.isSafeInteger(gid) || gid < 0) {
210
+ throw new Error("service install owner rejected");
211
+ }
212
+ if (targetRoot === "/" && (uid !== 0 || gid !== 0 || process.getuid?.() !== 0)) {
213
+ throw new Error("live service install requires root");
214
+ }
215
+ const targetRootInfo = lstatSync(targetRoot);
216
+ if (
217
+ !targetRootInfo.isDirectory() || targetRootInfo.uid !== uid || targetRootInfo.gid !== gid ||
218
+ (targetRootInfo.mode & 0o022) !== 0
219
+ ) throw new Error("service install root ownership rejected");
220
+ const proxy = validateRuntimeEgressProxyConfig(input.egressProxyConfig);
221
+ if (!input.workerConfig || !input.helperConfig || !proxy.ok || SECRET.test(JSON.stringify({
222
+ workerConfig: input.workerConfig,
223
+ helperConfig: input.helperConfig,
224
+ egressProxyConfig: input.egressProxyConfig,
225
+ }))) throw new Error("service config rejected");
226
+ if (
227
+ !SAFE_ID.test(input.workerConfig.workerId ?? "") || !SAFE_ID.test(input.workerConfig.hostId ?? "") ||
228
+ !Number.isSafeInteger(input.workerConfig.workerUid) || input.workerConfig.workerUid < 1 ||
229
+ !Number.isSafeInteger(input.workerConfig.workerGid) || input.workerConfig.workerGid < 1
230
+ ) {
231
+ throw new Error("worker config identity rejected");
232
+ }
233
+ const profiles = Object.entries(input.helperConfig.profiles ?? {});
234
+ if (profiles.length !== 1 || profiles.some(([profileId, profile]) =>
235
+ !SAFE_ID.test(profileId) || !Number.isSafeInteger(profile.runtimeUid) || profile.runtimeUid < 1 ||
236
+ !Number.isSafeInteger(profile.runtimeGid) || profile.runtimeGid < 1 ||
237
+ profile.workerUid !== input.workerConfig.workerUid || profile.workerGid !== input.workerConfig.workerGid ||
238
+ profile.proxyUid !== proxy.config.uid || profile.proxyGid !== proxy.config.gid ||
239
+ profile.proxyExecutablePath !== "/usr/local/lib/sellable-agent/package/bin/sellable-agent-egress-proxy.mjs" ||
240
+ profile.installedPackageManifestPath !== IMMUTABLE_MANIFEST_PATH ||
241
+ profile.externalRuntimeManifestPath !== EXTERNAL_RUNTIME_MANIFEST_PATH ||
242
+ profile.externalRuntimeClosureRoot !== EXTERNAL_RUNTIME_CLOSURE_ROOT ||
243
+ profile.externalRuntimeTrustRoot !== dirname(EXTERNAL_RUNTIME_CLOSURE_ROOT) ||
244
+ profile.externalRuntimeOwnerUid !== uid || profile.externalRuntimeOwnerGid !== gid ||
245
+ profile.mcpCommand !== EXTERNAL_MCP_ENTRYPOINT ||
246
+ profile.hermesExecutablePath !== EXTERNAL_HERMES_ENTRYPOINT ||
247
+ profile.hermesSourceRoot !== `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes` ||
248
+ profile.proxyConfigPath !== "/etc/sellable-agent/egress-proxy.json" ||
249
+ profile.runtimeBoundaryConfigPath !== "/etc/sellable-agent/runtime-boundary.json" ||
250
+ profile.providerCredentialSourcePath !== "/var/lib/sellable-agent-bootstrap/provider-auth" ||
251
+ !SHA256.test(profile.providerCredentialFingerprint ?? "") ||
252
+ profile.runtimeServicePath !== "/etc/services.d/sellable-agent-runtime" ||
253
+ profile.runtimeServiceEnvRoot !== "/etc/services.d/sellable-agent-runtime/env" ||
254
+ new Set([profile.runtimeUid, profile.proxyUid, profile.workerUid]).size !== 3
255
+ )) throw new Error("helper profile map rejected");
256
+ const profile = profiles[0][1];
257
+ const externalRuntime = observeExternalRuntimeClosure({
258
+ manifestPath: profile.externalRuntimeManifestPath,
259
+ pathRoot: targetRoot,
260
+ trustRoot: profile.externalRuntimeTrustRoot,
261
+ expectedClosureRoot: profile.externalRuntimeClosureRoot,
262
+ ownerUid: uid,
263
+ ownerGid: gid,
264
+ expectedEntrypoints: { mcp: profile.mcpCommand, hermes: profile.hermesExecutablePath },
265
+ });
266
+ const expectedAccounts = [
267
+ {
268
+ name: "sellable-agent-worker", uid: input.workerConfig.workerUid,
269
+ gid: input.workerConfig.workerGid, home: "/var/lib/sellable-agent-worker",
270
+ },
271
+ {
272
+ name: "sellable-agent-runtime", uid: profile.runtimeUid,
273
+ gid: profile.runtimeGid, home: "/var/lib/sellable-agent-runtime",
274
+ },
275
+ {
276
+ name: "sellable-agent-egress-proxy", uid: proxy.config.uid,
277
+ gid: proxy.config.gid, home: "/var/empty",
278
+ },
279
+ ];
280
+ return {
281
+ targetRoot: realpathSync(targetRoot), packageRoot, uid, gid, proxyConfig: proxy.config,
282
+ expectedAccounts, externalRuntime,
283
+ };
284
+ }
285
+
286
+ function target(root, absolutePath) {
287
+ const value = join(root, absolutePath.replace(/^\/+/, ""));
288
+ if (!inside(root, value)) throw new Error("service target confinement failed");
289
+ return value;
290
+ }
291
+
292
+ function installedRuntimeLauncher(name) {
293
+ return `#!/bin/sh\nexec /usr/bin/node '${IMMUTABLE_PACKAGE_ROOT}/bin/${name}.mjs' "$@"\n`;
294
+ }
295
+
296
+ function configurationFiles(input, checked) {
297
+ const helperRequestRoot = input.workerConfig.helperRequestRoot;
298
+ if (!isAbsolute(helperRequestRoot) || /[\s,*?\[\]{}]/.test(helperRequestRoot)) {
299
+ throw new Error("helper request sudoers path rejected");
300
+ }
301
+ const sudoers = [
302
+ "Defaults:sellable-agent-worker !setenv,secure_path=/usr/local/bin:/usr/bin:/bin",
303
+ `sellable-agent-worker ALL=(root) NOPASSWD: /usr/local/bin/sellable-agent-runtime-helper --request ${helperRequestRoot}/*`,
304
+ "",
305
+ ].join("\n");
306
+ return [
307
+ ["/etc/sellable-agent/egress-proxy.json", `${JSON.stringify(checked.proxyConfig, null, 2)}\n`, 0o444],
308
+ ["/etc/sellable-agent/worker.json", `${JSON.stringify(input.workerConfig, null, 2)}\n`, 0o600],
309
+ ["/etc/sellable-agent/runtime-helper.json", `${JSON.stringify(input.helperConfig, null, 2)}\n`, 0o600],
310
+ ["/etc/sudoers.d/sellable-agent-runtime-helper", sudoers, 0o440],
311
+ ].map(([logicalPath, bytes, mode]) => ({ logicalPath, bytes, mode }));
312
+ }
313
+
314
+ function installationFiles(input, checked) {
315
+ return [
316
+ ...AGENT_BIN_FILES.map((name) => [
317
+ `${IMMUTABLE_PACKAGE_ROOT}/bin/${name}`, readFileSync(join(checked.packageRoot, "bin", name)), 0o555,
318
+ ]),
319
+ ...AGENT_LIB_FILES.map((name) => [
320
+ `${IMMUTABLE_PACKAGE_ROOT}/lib/sellable-agent/${name}`,
321
+ readFileSync(join(checked.packageRoot, "lib/sellable-agent", name)), 0o555,
322
+ ]),
323
+ [`${IMMUTABLE_PACKAGE_ROOT}/package.json`, readFileSync(join(checked.packageRoot, "package.json")), 0o444],
324
+ ["/usr/local/bin/sellable-agent-egress-proxy", installedRuntimeLauncher("sellable-agent-egress-proxy"), 0o555],
325
+ ["/usr/local/bin/sellable-agent-host-worker", installedRuntimeLauncher("sellable-agent-host-worker"), 0o555],
326
+ ["/usr/local/bin/sellable-agent-runtime-helper", installedRuntimeLauncher("sellable-agent-runtime-helper"), 0o555],
327
+ ["/usr/local/bin/sellable-agent-runtime-launcher", installedRuntimeLauncher("sellable-agent-runtime-launcher"), 0o555],
328
+ ["/usr/local/bin/sellable-agent-runtime-probe", installedRuntimeLauncher("sellable-agent-runtime-probe"), 0o555],
329
+ ["/usr/local/bin/sellable-agent-sandbox-init", installedRuntimeLauncher("sellable-agent-sandbox-init"), 0o555],
330
+ ...configurationFiles(input, checked).map((file) => [file.logicalPath, file.bytes, file.mode]),
331
+ ["/etc/services.d/sellable-agent-runtime/down", "", 0o444],
332
+ ["/etc/services.d/sellable-agent-egress-proxy/run", readFileSync(join(checked.packageRoot, "services/s6/sellable-agent-egress-proxy/run")), 0o555],
333
+ ["/etc/services.d/sellable-agent-runtime/run", readFileSync(join(checked.packageRoot, "services/s6/sellable-agent-runtime/run")), 0o555],
334
+ ["/etc/services.d/sellable-agent-host-worker/run", readFileSync(join(checked.packageRoot, "services/s6/sellable-agent-host-worker/run")), 0o555],
335
+ ].map(([logicalPath, bytes, mode]) => ({ logicalPath, bytes, mode }));
336
+ }
337
+
338
+ function manifestFile(files) {
339
+ const manifest = {
340
+ version: IMMUTABLE_MANIFEST_VERSION,
341
+ files: files.map((file) => ({
342
+ path: file.logicalPath,
343
+ mode: file.mode,
344
+ digest: sha256(Buffer.from(file.bytes)),
345
+ })),
346
+ };
347
+ return {
348
+ logicalPath: IMMUTABLE_MANIFEST_PATH,
349
+ bytes: `${JSON.stringify(manifest, null, 2)}\n`,
350
+ mode: 0o444,
351
+ };
352
+ }
353
+
354
+ function readInstalledManifest(checked) {
355
+ const pathValue = target(checked.targetRoot, IMMUTABLE_MANIFEST_PATH);
356
+ const link = lstatSync(pathValue);
357
+ const expectedPaths = [
358
+ ...AGENT_BIN_FILES.map((name) => `${IMMUTABLE_PACKAGE_ROOT}/bin/${name}`),
359
+ ...AGENT_LIB_FILES.map((name) => `${IMMUTABLE_PACKAGE_ROOT}/lib/sellable-agent/${name}`),
360
+ `${IMMUTABLE_PACKAGE_ROOT}/package.json`,
361
+ ...AGENT_BIN_FILES.map((name) => `/usr/local/bin/${name.replace(/\.mjs$/, "")}`),
362
+ "/etc/sellable-agent/egress-proxy.json",
363
+ "/etc/sellable-agent/worker.json",
364
+ "/etc/sellable-agent/runtime-helper.json",
365
+ "/etc/sudoers.d/sellable-agent-runtime-helper",
366
+ "/etc/services.d/sellable-agent-runtime/down",
367
+ "/etc/services.d/sellable-agent-egress-proxy/run",
368
+ "/etc/services.d/sellable-agent-runtime/run",
369
+ "/etc/services.d/sellable-agent-host-worker/run",
370
+ ].sort();
371
+ if (
372
+ link.isSymbolicLink() || !link.isFile() || link.uid !== checked.uid || link.gid !== checked.gid ||
373
+ (link.mode & 0o777) !== 0o444
374
+ ) throw new Error("installed manifest rejected");
375
+ const bytes = readFileSync(pathValue);
376
+ const parsed = JSON.parse(bytes.toString("utf8"));
377
+ if (
378
+ parsed?.version !== IMMUTABLE_MANIFEST_VERSION || !Array.isArray(parsed.files) ||
379
+ parsed.files.some((file) =>
380
+ typeof file?.path !== "string" || !file.path.startsWith("/") ||
381
+ !Number.isSafeInteger(file.mode) || !SHA256.test(file.digest ?? "")
382
+ ) || new Set(parsed.files.map((file) => file.path)).size !== parsed.files.length ||
383
+ JSON.stringify(parsed.files.map((file) => file.path).sort()) !== JSON.stringify(expectedPaths)
384
+ ) throw new Error("installed manifest schema rejected");
385
+ return { bytes, files: parsed.files };
386
+ }
387
+
388
+ export function observeAgentHostServices(input) {
389
+ try {
390
+ const checked = validateInstallInput(input);
391
+ const accounts = readAccountDatabase(
392
+ checked.targetRoot, checked.expectedAccounts, checked.uid, checked.gid,
393
+ );
394
+ const installedManifest = readInstalledManifest(checked);
395
+ const files = installedManifest.files.map((file) => {
396
+ const pathValue = target(checked.targetRoot, file.path);
397
+ const link = lstatSync(pathValue);
398
+ const bytes = readFileSync(pathValue);
399
+ if (
400
+ link.isSymbolicLink() || !link.isFile() || link.uid !== checked.uid || link.gid !== checked.gid ||
401
+ (link.mode & 0o777) !== file.mode || sha256(bytes) !== file.digest
402
+ ) throw new Error("installed service readback rejected");
403
+ return { path: file.path, mode: file.mode, uid: link.uid, gid: link.gid, digest: sha256(bytes) };
404
+ });
405
+ const configuration = configurationFiles(input, checked);
406
+ for (const file of configuration) {
407
+ const bytes = readFileSync(target(checked.targetRoot, file.logicalPath));
408
+ if (!bytes.equals(Buffer.from(file.bytes))) throw new Error("installed configuration drift rejected");
409
+ }
410
+ return {
411
+ ok: true,
412
+ producer: "sellable-agent-service-installer",
413
+ accounts,
414
+ manifestDigest: sha256(installedManifest.bytes),
415
+ externalRuntimeClosureDigest: checked.externalRuntime.closureDigest,
416
+ files: [
417
+ { path: IMMUTABLE_MANIFEST_PATH, mode: 0o444, uid: checked.uid, gid: checked.gid, digest: sha256(installedManifest.bytes) },
418
+ ...files,
419
+ ],
420
+ };
421
+ } catch {
422
+ return { ok: false, code: "service_readback_rejected" };
423
+ }
424
+ }
425
+
426
+ export function installAgentHostServices(input) {
427
+ try {
428
+ const checked = validateInstallInput(input);
429
+ provisionServiceAccounts(input, checked);
430
+ const files = installationFiles(input, checked);
431
+ for (const file of [...files, manifestFile(files)]) {
432
+ atomicOwnedFile(
433
+ checked.targetRoot, target(checked.targetRoot, file.logicalPath),
434
+ file.bytes, file.mode, checked.uid, checked.gid,
435
+ );
436
+ }
437
+ return observeAgentHostServices(input);
438
+ } catch {
439
+ return { ok: false, code: "service_install_rejected" };
440
+ }
441
+ }
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.359-phase111.20260720052618",
3
+ "version": "0.1.359",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {
7
- "sellable": "bin/sellable-install.mjs"
7
+ "sellable": "bin/sellable-install.mjs",
8
+ "sellable-agent-host-bootstrap": "bin/sellable-agent-host-bootstrap.mjs",
9
+ "sellable-agent-egress-proxy": "bin/sellable-agent-egress-proxy.mjs",
10
+ "sellable-agent-host-worker": "bin/sellable-agent-host-worker.mjs",
11
+ "sellable-agent-runtime-helper": "bin/sellable-agent-runtime-helper.mjs",
12
+ "sellable-agent-runtime-launcher": "bin/sellable-agent-runtime-launcher.mjs",
13
+ "sellable-agent-sandbox-init": "bin/sellable-agent-sandbox-init.mjs",
14
+ "sellable-agent-runtime-probe": "bin/sellable-agent-runtime-probe.mjs"
8
15
  },
9
16
  "scripts": {
10
17
  "sync-skills": "node scripts/sync-skill-templates.mjs",
@@ -13,6 +20,8 @@
13
20
  "files": [
14
21
  "bin",
15
22
  "lib",
23
+ "services",
24
+ "container",
16
25
  "agents",
17
26
  "skill-templates",
18
27
  "README.md",
@@ -0,0 +1,15 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ exec /usr/bin/setpriv \
5
+ --reuid sellable-agent-egress-proxy \
6
+ --regid sellable-agent-egress-proxy \
7
+ --clear-groups \
8
+ --no-new-privs \
9
+ --bounding-set=-all \
10
+ --inh-caps=-all \
11
+ --ambient-caps=-all \
12
+ /usr/bin/env -i \
13
+ PATH=/usr/local/bin:/usr/bin:/bin \
14
+ NODE_ENV=production \
15
+ /usr/bin/node /usr/local/lib/sellable-agent/package/bin/sellable-agent-egress-proxy.mjs
@@ -0,0 +1,4 @@
1
+ #!/command/execlineb -P
2
+ with-contenv
3
+ s6-setuidgid sellable-agent-worker
4
+ /usr/bin/env -i PATH=/usr/local/bin:/usr/bin:/bin HOME=/var/lib/sellable-agent-worker NODE_ENV=production /usr/local/bin/sellable-agent-host-worker --config /etc/sellable-agent/worker.json
@@ -0,0 +1,19 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ if [ "${SELLABLE_AGENT_RUNTIME_ENV_LOADED:-}" != "1" ]; then
5
+ exec /command/s6-envdir /etc/services.d/sellable-agent-runtime/env "$0"
6
+ fi
7
+
8
+ : "${SELLABLE_AGENT_PROFILE_ID:?profile id required}"
9
+ : "${SELLABLE_AGENT_WORKER_SLACK_SECRET_ROOT:?worker Slack secret root required}"
10
+
11
+ SELLABLE_AGENT_SLACK_SECRET_ROOT="${SELLABLE_AGENT_WORKER_SLACK_SECRET_ROOT}/${SELLABLE_AGENT_PROFILE_ID}"
12
+
13
+ exec 8<"${SELLABLE_AGENT_SLACK_SECRET_ROOT}/slack-app"
14
+ exec 9<"${SELLABLE_AGENT_SLACK_SECRET_ROOT}/slack-bot"
15
+
16
+ exec /usr/bin/env -i \
17
+ PATH=/usr/local/bin:/usr/bin:/bin \
18
+ NODE_ENV=production \
19
+ /usr/local/bin/sellable-agent-runtime-launcher
@@ -17,8 +17,3 @@ Use `refill-sends-v2` instead:
17
17
  ```text
18
18
  get_subskill_prompt({ subskillName: "refill-sends-v2" })
19
19
  ```
20
-
21
- The alias must not strip exact `targetConfig`, server `runHandle`,
22
- `reportingContext`, canonical `refill_reporting.v2`, or an explicit
23
- `messageTemplateRevision`. It never infers revision authority from
24
- `forceRerun`.
@@ -52,24 +52,9 @@ refill_sends_v2({ workspaceId, dryRun:true, intent:"auto", senderIds? })
52
52
  To resume an in-progress run, pass the handle back exactly:
53
53
 
54
54
  ```text
55
- refill_sends_v2({ workspaceId, runHandle:{runId,fence}, targetConfig, reportingContext, messageTemplateRevision? })
55
+ refill_sends_v2({ workspaceId, runId, fence })
56
56
  ```
57
57
 
58
- `targetConfig` and the server-issued `runHandle` are separate authorities. Keep
59
- both byte-for-byte through every continuation and takeover; never fold the
60
- rotating fence into target configuration.
61
-
62
- An approved copy change is reachable only through an explicit
63
- `messageTemplateRevision` object with `version:1`, `source:"user_approved"`,
64
- the approved `templateMarkdown`, `messageTemplateRevision` identity,
65
- `priorTemplateAuthorityDigest`, `nextTemplateAuthorityDigest`, 1–500 exact
66
- `cohortRowIds`, and `requestId`, `effectId`, and `revisionOperationId`. Pass it
67
- with the exact `targetConfig` and `reportingContext`; the command obtains or
68
- resumes the server run handle and lets the planner name
69
- `revise_message_template_and_rerun`. Missing or partial input means no revision.
70
- `forceRerun:true` on a queue operation never authorizes or implies a template
71
- change.
72
-
73
58
  If a stale handle loses the lease, the tool reports the holder status and the
74
59
  approximately 10 minute lockout window. Reinvoke with the current handle or wait
75
60
  for lease expiry; never guess a fence.
@@ -100,8 +85,6 @@ The run may execute only packet-named bounded work:
100
85
  when the campaign is in the packet's pinned lane chain;
101
86
  - refresh paid InMail credit facts during bootstrap and once per scheduler-wait
102
87
  entry when the packet requires it.
103
- - apply `revise_message_template_and_rerun` only when the packet contains the
104
- complete explicit user-approved revision envelope and exact fenced target.
105
88
 
106
89
  The loop records planned -> did -> outcome before and after every foreign
107
90
  mutation. It refuses stale packets by fingerprint and validates packet
@@ -185,10 +168,5 @@ reason or resume handle, lane source, lane chain, chosen label with token
185
168
  secondary, plan revision, journal path, blocked continuation packets, and
186
169
  firstFailing checklist when present.
187
170
 
188
- Preserve `refill_reporting.v2` exactly in progress, continuation, replay, and
189
- terminal results. Do not recompute its semantic counts or drop live-preparation
190
- epoch, snapshot sequence, reset reason, watermark, blockers, compatibility, or
191
- terminal fields. Never surface raw prospect or message-copy fields.
192
-
193
171
  Real-run journals live under `~/.sellable/refill/runs` by default and append an
194
172
  index line. Dry runs include the dry marker and do not create run records.
@@ -93,7 +93,7 @@ Accepted invocation flags in the same user request:
93
93
  When the host can call typed MCP tools, start with:
94
94
 
95
95
  ```text
96
- refill_sends({ yolo?: boolean, executionMode?: "manual" | "scheduled" | "yolo", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], actionTypes?: ("send_invite" | "send_inmail_closed")[], horizonSendDays?: number, untilDate?: "YYYY-MM-DD", targetDate?: "YYYY-MM-DD", runHandle?: RefillRunHandleV1, targetConfig?: RefillTargetConfigV1, reportingContext?: RefillReportingContextV2, messageTemplateRevision?: MessageTemplateRevisionV1 })
96
+ refill_sends({ yolo?: boolean, executionMode?: "manual" | "scheduled" | "yolo", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], actionTypes?: ("send_invite" | "send_inmail_closed")[], horizonSendDays?: number, untilDate?: "YYYY-MM-DD", targetDate?: "YYYY-MM-DD", runId?: string, fence?: number })
97
97
  ```
98
98
 
99
99
  That command helper normalizes arguments and returns the execution contract. In
@@ -104,14 +104,6 @@ host must immediately call `refill_sends` again with the original full scope
104
104
  plus the exact returned `runId` and `fence`. Do this automatically inside the
105
105
  same user command until terminal projected coverage or a concrete blocker; do
106
106
  not ask the user to type `continue` and do not start a second run.
107
-
108
- `targetConfig` and the server-issued `runHandle` are separate values and must
109
- survive each continuation unchanged except for a server-issued takeover fence.
110
- Only a literal `messageTemplateRevision` with `source:"user_approved"`, the
111
- approved `templateMarkdown`, prior/new authority digests, 1–500 exact cohort
112
- row IDs, and request/effect/revision-operation IDs can authorize
113
- `revise_message_template_and_rerun`. No envelope means no revision. A
114
- `forceRerun:true` queue call cannot infer or authorize template mutation.
115
107
  Safe primitives are paid-credit refresh,
116
108
  existing-row message preparation, generated-message approval, receipt-proven
117
109
  same-source row copy, one bounded Signal Discovery search derived only from
@@ -575,11 +567,6 @@ scheduler wait loop. Do not finish the refill goal, mark it complete, or mark it
575
567
  blocked only because it is still `awaiting_scheduler_after_ready_buffer`; keep
576
568
  waiting unless Christian stops the run or the host cannot continue.
577
569
 
578
- Carry the canonical `refill_reporting.v2` DTO through progress, continuation,
579
- replay, and terminal reporting without recomputing counts or losing live epoch,
580
- snapshot sequence, reset reason, watermark, blockers, compatibility, or
581
- terminal outcome. Redact raw copy and prospect fields.
582
-
583
570
  Future scheduled coverage and already sent actions are distinct; only
584
571
  scheduler-owned future scheduled actions count as scheduled coverage. For
585
572
  multiple target dates, finish D1 execution and the full D1 reread before