@sellable/install 0.1.359 → 0.1.360-phase111.20260720061815

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-install.mjs +0 -2
  2. package/package.json +2 -11
  3. package/skill-templates/refill-sends-evergreen.md +5 -0
  4. package/skill-templates/refill-sends-v2.md +23 -1
  5. package/skill-templates/refill-sends.md +23 -1
  6. package/bin/sellable-agent-egress-proxy.mjs +0 -31
  7. package/bin/sellable-agent-host-bootstrap.mjs +0 -26
  8. package/bin/sellable-agent-host-worker.mjs +0 -9
  9. package/bin/sellable-agent-runtime-helper.mjs +0 -37
  10. package/bin/sellable-agent-runtime-launcher.mjs +0 -151
  11. package/bin/sellable-agent-runtime-probe.mjs +0 -163
  12. package/bin/sellable-agent-sandbox-init.mjs +0 -93
  13. package/container/Dockerfile +0 -37
  14. package/container/README.md +0 -15
  15. package/container/compose.yaml +0 -22
  16. package/container/entrypoint.sh +0 -13
  17. package/lib/sellable-agent/containment-contract.mjs +0 -472
  18. package/lib/sellable-agent/external-runtime-builder.mjs +0 -272
  19. package/lib/sellable-agent/hermes-bridge.mjs +0 -497
  20. package/lib/sellable-agent/host-bootstrap.mjs +0 -458
  21. package/lib/sellable-agent/host-worker.mjs +0 -2067
  22. package/lib/sellable-agent/profile-materializer.mjs +0 -1410
  23. package/lib/sellable-agent/provisioning-adapter.mjs +0 -992
  24. package/lib/sellable-agent/runtime-boundary.mjs +0 -635
  25. package/lib/sellable-agent/runtime-egress-proxy.mjs +0 -241
  26. package/lib/sellable-agent/runtime-helper.mjs +0 -1428
  27. package/lib/sellable-agent/runtime-reconciler.mjs +0 -683
  28. package/lib/sellable-agent/service-installer.mjs +0 -441
  29. package/services/s6/sellable-agent-egress-proxy/run +0 -15
  30. package/services/s6/sellable-agent-host-worker/run +0 -4
  31. package/services/s6/sellable-agent-runtime/run +0 -19
@@ -401,8 +401,6 @@ function parseArgs(argv) {
401
401
  if (opts.sellableConfigPath && !opts.sellableConfigsDir) {
402
402
  opts.sellableConfigsDir = join(dirname(opts.sellableConfigPath), "configs");
403
403
  }
404
- opts.lockWorkspaceId = opts.workspaceId;
405
- opts.requireWorkspaceLock = Boolean(opts.workspaceId);
406
404
 
407
405
  return opts;
408
406
  }
package/package.json CHANGED
@@ -1,17 +1,10 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.359",
3
+ "version": "0.1.360-phase111.20260720061815",
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",
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"
7
+ "sellable": "bin/sellable-install.mjs"
15
8
  },
16
9
  "scripts": {
17
10
  "sync-skills": "node scripts/sync-skill-templates.mjs",
@@ -20,8 +13,6 @@
20
13
  "files": [
21
14
  "bin",
22
15
  "lib",
23
- "services",
24
- "container",
25
16
  "agents",
26
17
  "skill-templates",
27
18
  "README.md",
@@ -17,3 +17,8 @@ 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,9 +52,24 @@ 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, runId, fence })
55
+ refill_sends_v2({ workspaceId, runHandle:{runId,fence}, targetConfig, reportingContext, messageTemplateRevision? })
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
+
58
73
  If a stale handle loses the lease, the tool reports the holder status and the
59
74
  approximately 10 minute lockout window. Reinvoke with the current handle or wait
60
75
  for lease expiry; never guess a fence.
@@ -85,6 +100,8 @@ The run may execute only packet-named bounded work:
85
100
  when the campaign is in the packet's pinned lane chain;
86
101
  - refresh paid InMail credit facts during bootstrap and once per scheduler-wait
87
102
  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.
88
105
 
89
106
  The loop records planned -> did -> outcome before and after every foreign
90
107
  mutation. It refuses stale packets by fingerprint and validates packet
@@ -168,5 +185,10 @@ reason or resume handle, lane source, lane chain, chosen label with token
168
185
  secondary, plan revision, journal path, blocked continuation packets, and
169
186
  firstFailing checklist when present.
170
187
 
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
+
171
193
  Real-run journals live under `~/.sellable/refill/runs` by default and append an
172
194
  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", runId?: string, fence?: number })
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 })
97
97
  ```
98
98
 
99
99
  That command helper normalizes arguments and returns the execution contract. In
@@ -104,6 +104,23 @@ 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
+ When the user supplied a complete revision except for the current prior digest,
116
+ acquire the exact run first and reread with its unchanged `targetConfig` and
117
+ `runHandle`. Use only `reportingContext.source.templateAuthorityDigest` from
118
+ that fenced reread as `priorTemplateAuthorityDigest`, then immediately continue
119
+ the same run with the complete literal revision—even when the ordinary refill
120
+ queue is empty or the sender is otherwise ineligible. An empty ordinary queue
121
+ must not suppress an explicit revision. If the fenced reread returns no digest,
122
+ stop with `template_authority_missing` or the returned
123
+ `templateAuthorityBlocker`; never guess the digest.
107
124
  Safe primitives are paid-credit refresh,
108
125
  existing-row message preparation, generated-message approval, receipt-proven
109
126
  same-source row copy, one bounded Signal Discovery search derived only from
@@ -567,6 +584,11 @@ scheduler wait loop. Do not finish the refill goal, mark it complete, or mark it
567
584
  blocked only because it is still `awaiting_scheduler_after_ready_buffer`; keep
568
585
  waiting unless Christian stops the run or the host cannot continue.
569
586
 
587
+ Carry the canonical `refill_reporting.v2` DTO through progress, continuation,
588
+ replay, and terminal reporting without recomputing counts or losing live epoch,
589
+ snapshot sequence, reset reason, watermark, blockers, compatibility, or
590
+ terminal outcome. Redact raw copy and prospect fields.
591
+
570
592
  Future scheduled coverage and already sent actions are distinct; only
571
593
  scheduler-owned future scheduled actions count as scheduled coverage. For
572
594
  multiple target dates, finish D1 execution and the full D1 reread before
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { lstatSync, readFileSync } from "node:fs";
4
- import {
5
- createRuntimeEgressProxy,
6
- validateRuntimeEgressProxyConfig,
7
- } from "../lib/sellable-agent/runtime-egress-proxy.mjs";
8
-
9
- const CONFIG_PATH = "/etc/sellable-agent/egress-proxy.json";
10
-
11
- try {
12
- if (process.argv.length !== 2) throw new Error("arguments rejected");
13
- const link = lstatSync(CONFIG_PATH);
14
- if (link.isSymbolicLink() || !link.isFile() || link.uid !== 0 || (link.mode & 0o777) !== 0o444) {
15
- throw new Error("proxy config ownership rejected");
16
- }
17
- const parsed = validateRuntimeEgressProxyConfig(JSON.parse(readFileSync(CONFIG_PATH, "utf8")));
18
- if (!parsed.ok || process.getuid?.() !== parsed.config.uid || process.getgid?.() !== parsed.config.gid) {
19
- throw new Error("proxy identity rejected");
20
- }
21
- const proxy = createRuntimeEgressProxy(parsed.config);
22
- await proxy.listen();
23
- const stop = async () => {
24
- try { await proxy.close(); } finally { process.exit(0); }
25
- };
26
- process.once("SIGTERM", stop);
27
- process.once("SIGINT", stop);
28
- } catch {
29
- process.stderr.write("sellable-agent egress proxy failed closed\n");
30
- process.exitCode = 1;
31
- }
@@ -1,26 +0,0 @@
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
- }
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runHostWorkerMain } from "../lib/sellable-agent/host-worker.mjs";
4
-
5
- const result = await runHostWorkerMain({ argv: process.argv.slice(2) });
6
- if (!result.ok) {
7
- process.stderr.write(`${result.code}\n`);
8
- process.exitCode = 1;
9
- }
@@ -1,37 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
4
- import { runRuntimeHelperRequest } from "../lib/sellable-agent/runtime-helper.mjs";
5
-
6
- const requestIndex = process.argv.indexOf("--request");
7
- const requestPath = requestIndex >= 0 ? process.argv[requestIndex + 1] : null;
8
- if (!requestPath || process.argv.length !== 4) {
9
- process.stderr.write("usage: sellable-agent-runtime-helper --request /absolute/path\n");
10
- process.exitCode = 64;
11
- } else {
12
- try {
13
- const configPath = "/etc/sellable-agent/runtime-helper.json";
14
- const before = lstatSync(configPath);
15
- if (before.isSymbolicLink() || !before.isFile() || before.uid !== 0 || (before.mode & 0o022) !== 0) {
16
- throw new Error("runtime helper config rejected");
17
- }
18
- const descriptor = openSync(configPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
19
- let bytes;
20
- try {
21
- const after = fstatSync(descriptor);
22
- if (!after.isFile() || after.uid !== 0 || after.dev !== before.dev || after.ino !== before.ino || (after.mode & 0o022) !== 0) {
23
- throw new Error("runtime helper config changed during open");
24
- }
25
- bytes = readFileSync(descriptor, "utf8");
26
- } finally {
27
- closeSync(descriptor);
28
- }
29
- const config = JSON.parse(bytes);
30
- const result = await runRuntimeHelperRequest({ requestPath, config });
31
- process.stdout.write(`${JSON.stringify(result)}\n`);
32
- if (!result.ok) process.exitCode = 1;
33
- } catch {
34
- process.stdout.write(`${JSON.stringify({ ok: false, code: "runtime_helper_config_rejected" })}\n`);
35
- process.exitCode = 1;
36
- }
37
- }
@@ -1,151 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn, spawnSync } from "node:child_process";
4
- import {
5
- existsSync,
6
- lstatSync,
7
- mkdirSync,
8
- readFileSync,
9
- writeFileSync,
10
- } from "node:fs";
11
- import { join } from "node:path";
12
- import {
13
- buildRuntimeBoundaryContract,
14
- buildRuntimeBoundaryLaunchSpec,
15
- buildRuntimeBoundaryNftProgram,
16
- expectedRuntimeCgroup,
17
- inspectRuntimeBoundaryInputs,
18
- runtimeBoundaryNftRulesMatch,
19
- } from "../lib/sellable-agent/runtime-boundary.mjs";
20
-
21
- const CONFIG_PATH = "/etc/sellable-agent/runtime-boundary.json";
22
- const SECRET = /xox[bap]-|sat_[A-Za-z0-9_-]+|asec\.v1\.|access[_-]?token|refresh[_-]?token/i;
23
-
24
- function fixedRootConfig() {
25
- const link = lstatSync(CONFIG_PATH);
26
- if (link.isSymbolicLink() || !link.isFile() || link.uid !== 0 || link.gid !== 0 || (link.mode & 0o777) !== 0o400) {
27
- throw new Error("runtime boundary config rejected");
28
- }
29
- const bytes = readFileSync(CONFIG_PATH, "utf8");
30
- if (bytes.length > 64 * 1024 || SECRET.test(bytes)) throw new Error("runtime boundary config rejected");
31
- const built = buildRuntimeBoundaryContract(JSON.parse(bytes));
32
- if (!built.ok) throw new Error("runtime boundary contract rejected");
33
- return built.contract;
34
- }
35
-
36
- function provisionCgroup(contract) {
37
- const parent = contract.paths.cgroupRoot;
38
- const systemRoot = "/sys/fs/cgroup";
39
- const rootInfo = lstatSync(systemRoot);
40
- if (rootInfo.isSymbolicLink() || !rootInfo.isDirectory()) throw new Error("cgroup v2 root rejected");
41
- const controllers = readFileSync(join(systemRoot, "cgroup.controllers"), "utf8").trim().split(/\s+/);
42
- if (!["cpu", "memory", "pids"].every((name) => controllers.includes(name))) {
43
- throw new Error("required cgroup controllers unavailable");
44
- }
45
- const requiredControllers = ["cpu", "memory", "pids"];
46
- const enableControllers = (path) => {
47
- const controlPath = join(path, "cgroup.subtree_control");
48
- const enabled = new Set(readFileSync(controlPath, "utf8").trim().split(/\s+/).filter(Boolean));
49
- const missing = requiredControllers.filter((name) => !enabled.has(name));
50
- if (missing.length) writeFileSync(controlPath, `${missing.map((name) => `+${name}`).join(" ")}\n`);
51
- const observed = new Set(readFileSync(controlPath, "utf8").trim().split(/\s+/).filter(Boolean));
52
- if (requiredControllers.some((name) => !observed.has(name))) throw new Error("cgroup controller enablement failed");
53
- };
54
- enableControllers(systemRoot);
55
- if (!existsSync(parent)) mkdirSync(parent, { mode: 0o755 });
56
- const parentInfo = lstatSync(parent);
57
- if (parentInfo.isSymbolicLink() || !parentInfo.isDirectory() || parentInfo.uid !== 0 || (parentInfo.mode & 0o022) !== 0) {
58
- throw new Error("runtime cgroup parent rejected");
59
- }
60
- enableControllers(parent);
61
- const expected = expectedRuntimeCgroup(contract);
62
- if (!existsSync(expected.path)) mkdirSync(expected.path, { mode: 0o755 });
63
- const groupInfo = lstatSync(expected.path);
64
- if (groupInfo.isSymbolicLink() || !groupInfo.isDirectory() || groupInfo.uid !== 0 || (groupInfo.mode & 0o022) !== 0) {
65
- throw new Error("runtime cgroup rejected");
66
- }
67
- if (readFileSync(join(expected.path, "cgroup.procs"), "utf8").trim()) {
68
- throw new Error("runtime cgroup still has processes");
69
- }
70
- for (const [name, value] of Object.entries(expected.files)) {
71
- writeFileSync(join(expected.path, name), `${value}\n`);
72
- if (readFileSync(join(expected.path, name), "utf8").trim() !== value) throw new Error("runtime cgroup limit drift");
73
- }
74
- writeFileSync(join(expected.path, "cgroup.procs"), `${process.pid}\n`);
75
- }
76
-
77
- function provisionFirewall(contract) {
78
- const policy = buildRuntimeBoundaryNftProgram(contract);
79
- if (!policy.ok) throw new Error("runtime firewall contract rejected");
80
- const listed = spawnSync(contract.paths.nftPath, ["-j", "list", "tables"], {
81
- encoding: "utf8",
82
- env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" },
83
- timeout: 10_000,
84
- });
85
- if (listed.status !== 0) throw new Error("runtime firewall inventory failed");
86
- let inventory;
87
- try { inventory = JSON.parse(listed.stdout); } catch { throw new Error("runtime firewall inventory rejected"); }
88
- const exists = Array.isArray(inventory?.nftables) && inventory.nftables.some((entry) =>
89
- entry?.table?.family === "inet" && entry.table.name === policy.table);
90
- const program = `${exists ? `delete table inet ${policy.table}\n` : ""}${policy.program}`;
91
- const applied = spawnSync(contract.paths.nftPath, policy.applyArgs, {
92
- input: program,
93
- encoding: "utf8",
94
- env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" },
95
- timeout: 10_000,
96
- maxBuffer: 64 * 1024,
97
- });
98
- if (applied.status !== 0 || SECRET.test(`${applied.stdout}${applied.stderr}`)) {
99
- throw new Error("runtime firewall apply failed");
100
- }
101
- const observed = spawnSync(contract.paths.nftPath, ["list", "table", "inet", policy.table], {
102
- encoding: "utf8",
103
- env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" },
104
- timeout: 10_000,
105
- });
106
- if (observed.status !== 0 || !runtimeBoundaryNftRulesMatch(contract, observed.stdout)) {
107
- throw new Error("runtime firewall readback failed");
108
- }
109
- }
110
-
111
- async function run() {
112
- if (process.argv.length !== 2 || process.getuid?.() !== 0 || process.getgid?.() !== 0) {
113
- throw new Error("runtime launcher identity rejected");
114
- }
115
- for (const fd of [8, 9]) {
116
- const link = lstatSync(`/proc/self/fd/${fd}`);
117
- if (!link.isSymbolicLink()) throw new Error("runtime Slack descriptor rejected");
118
- }
119
- const contract = fixedRootConfig();
120
- if (!inspectRuntimeBoundaryInputs(contract).ok) throw new Error("runtime boundary inputs rejected");
121
- provisionCgroup(contract);
122
- provisionFirewall(contract);
123
- const spec = buildRuntimeBoundaryLaunchSpec(contract);
124
- if (!spec.ok) throw new Error("runtime launch spec rejected");
125
- const stdio = Array(10).fill("ignore");
126
- stdio[1] = "inherit";
127
- stdio[2] = "inherit";
128
- stdio[8] = 8;
129
- stdio[9] = 9;
130
- const child = spawn(spec.command, spec.args, {
131
- env: spec.env,
132
- shell: false,
133
- stdio,
134
- });
135
- const forwardTerm = () => { if (!child.killed) child.kill("SIGTERM"); };
136
- const forwardInt = () => { if (!child.killed) child.kill("SIGINT"); };
137
- process.on("SIGTERM", forwardTerm);
138
- process.on("SIGINT", forwardInt);
139
- const exitCode = await new Promise((resolveExit, rejectExit) => {
140
- child.once("error", rejectExit);
141
- child.once("exit", (code, signal) => resolveExit(code ?? (signal ? 1 : 0)));
142
- });
143
- process.exitCode = exitCode;
144
- }
145
-
146
- try {
147
- await run();
148
- } catch {
149
- process.stderr.write("sellable-agent runtime boundary failed closed\n");
150
- process.exitCode = 1;
151
- }
@@ -1,163 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn } from "node:child_process";
4
- import { lstatSync, readFileSync } from "node:fs";
5
- import { createInterface } from "node:readline";
6
-
7
- const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
8
- const profileIndex = process.argv.indexOf("--profile-id");
9
- const profileId = profileIndex >= 0 ? process.argv[profileIndex + 1] : null;
10
-
11
- function requiredEnv(name) {
12
- const value = process.env[name]?.trim();
13
- if (!value) throw new Error(`missing ${name}`);
14
- return value;
15
- }
16
-
17
- async function withClient(lockWorkspaceId, run) {
18
- const command = requiredEnv("SELLABLE_AGENT_MCP_COMMAND");
19
- if (command !== "/usr/local/lib/sellable-agent/external-runtime/mcp/bin/sellable-mcp") {
20
- throw new Error("MCP command pin rejected");
21
- }
22
- const env = {
23
- PATH: "/usr/local/bin:/usr/bin:/bin",
24
- HOME: requiredEnv("HOME"),
25
- NODE_ENV: "production",
26
- SELLABLE_CONFIG_PATH: requiredEnv("SELLABLE_CONFIG_PATH"),
27
- SELLABLE_AGENT_RUNTIME_SECRET_ROOT: requiredEnv("SELLABLE_AGENT_RUNTIME_SECRET_ROOT"),
28
- SELLABLE_AGENT_PROFILE_ID: profileId,
29
- SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE: requiredEnv("SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE"),
30
- SELLABLE_AGENT_WORKSPACE_ID: requiredEnv("SELLABLE_AGENT_WORKSPACE_ID"),
31
- SELLABLE_AGENT_ID: requiredEnv("SELLABLE_AGENT_ID"),
32
- SELLABLE_AGENT_CREDENTIAL_GENERATION: requiredEnv("SELLABLE_AGENT_CREDENTIAL_GENERATION"),
33
- SELLABLE_AGENT_POLICY_HASH: requiredEnv("SELLABLE_AGENT_POLICY_HASH"),
34
- SELLABLE_AGENT_SELECTED_CHANNEL_IDS: requiredEnv("SELLABLE_AGENT_SELECTED_CHANNEL_IDS"),
35
- SELLABLE_LOCK_WORKSPACE_ID: lockWorkspaceId,
36
- SELLABLE_REQUIRE_WORKSPACE_LOCK: "1",
37
- };
38
- const child = spawn(command, [], { env, shell: false, stdio: ["pipe", "pipe", "ignore"] });
39
- const pending = new Map();
40
- let nextId = 1;
41
- let failed = null;
42
- const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
43
- const failAll = (error) => {
44
- failed = error;
45
- for (const waiter of pending.values()) waiter.reject(error);
46
- pending.clear();
47
- };
48
- lines.on("line", (line) => {
49
- try {
50
- const message = JSON.parse(line);
51
- if (!Number.isSafeInteger(message.id) || !pending.has(message.id)) return;
52
- const waiter = pending.get(message.id);
53
- pending.delete(message.id);
54
- if (message.error) waiter.reject(new Error("MCP request rejected"));
55
- else waiter.resolve(message.result);
56
- } catch {
57
- failAll(new Error("MCP response rejected"));
58
- }
59
- });
60
- child.once("error", failAll);
61
- child.once("exit", () => failAll(new Error("MCP process exited")));
62
- const request = (method, params = {}) => new Promise((resolveRequest, rejectRequest) => {
63
- if (failed) return rejectRequest(failed);
64
- const id = nextId++;
65
- const timer = setTimeout(() => {
66
- pending.delete(id);
67
- rejectRequest(new Error("MCP request timed out"));
68
- }, 10_000);
69
- pending.set(id, {
70
- resolve(value) { clearTimeout(timer); resolveRequest(value); },
71
- reject(error) { clearTimeout(timer); rejectRequest(error); },
72
- });
73
- child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
74
- });
75
- const notify = (method, params = {}) => {
76
- child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
77
- };
78
- const initialized = await request("initialize", {
79
- protocolVersion: "2025-06-18",
80
- capabilities: {},
81
- clientInfo: { name: "sellable-agent-runtime-probe", version: "1.0.0" },
82
- });
83
- if (!initialized?.serverInfo) throw new Error("MCP initialization rejected");
84
- notify("notifications/initialized");
85
- const client = {
86
- listTools: () => request("tools/list", {}),
87
- callTool: (params) => request("tools/call", params),
88
- };
89
- try {
90
- return await run(client);
91
- } finally {
92
- lines.close();
93
- child.stdin.end();
94
- child.kill("SIGTERM");
95
- }
96
- }
97
-
98
- async function toolDenied(client, name, channelId, requesterId, providerRequestId) {
99
- try {
100
- const result = await client.callTool({
101
- name,
102
- arguments: {},
103
- _meta: { sellableAgent: { channelId, requesterId, providerRequestId } },
104
- }, undefined, { timeout: 10_000, maxTotalTimeout: 10_000 });
105
- return result?.isError === true;
106
- } catch {
107
- return true;
108
- }
109
- }
110
-
111
- try {
112
- if (!SAFE_ID.test(profileId ?? "") || process.argv.length !== 4) throw new Error("probe arguments rejected");
113
- const configPath = requiredEnv("SELLABLE_CONFIG_PATH");
114
- const link = lstatSync(configPath);
115
- if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o077) !== 0) throw new Error("profile config rejected");
116
- const config = JSON.parse(readFileSync(configPath, "utf8"));
117
- const workspaceId = requiredEnv("SELLABLE_AGENT_WORKSPACE_ID");
118
- const channelIds = config.requestChannelPolicy?.selectedChannelIds;
119
- const requesterIds = config.requestChannelPolicy?.memberAllowlist;
120
- const tools = config.tools?.include;
121
- if (
122
- config.workspaceLockId !== workspaceId || !Array.isArray(channelIds) || channelIds.length === 0 ||
123
- !Array.isArray(requesterIds) || requesterIds.length === 0 || !Array.isArray(tools) ||
124
- !tools.includes("get_company_info")
125
- ) throw new Error("profile MCP contract rejected");
126
- const channelId = channelIds[0];
127
- const requesterId = requesterIds[0];
128
- const providerRequestId = `runtime-probe-${profileId}`;
129
- const main = await withClient(workspaceId, async (client) => {
130
- const listed = (await client.listTools(undefined, { timeout: 10_000, maxTotalTimeout: 10_000 })).tools
131
- .map((tool) => tool.name).sort();
132
- const safe = await client.callTool({
133
- name: "get_company_info",
134
- arguments: {},
135
- _meta: { sellableAgent: { channelId, requesterId, providerRequestId } },
136
- }, undefined, { timeout: 10_000, maxTotalTimeout: 10_000 });
137
- return {
138
- tools: listed,
139
- safeRead: safe?.isError !== true,
140
- unexpectedToolDenied: await toolDenied(client, "__sellable_unapproved_probe__", channelId, requesterId, providerRequestId),
141
- wrongChannelDenied: await toolDenied(client, "get_company_info", "C0000000000", requesterId, providerRequestId),
142
- };
143
- });
144
- const wrongWorkspaceDenied = await withClient(`wrong-${workspaceId}`, (client) =>
145
- toolDenied(client, "get_company_info", channelId, requesterId, providerRequestId));
146
- const ok =
147
- main.safeRead && main.unexpectedToolDenied && main.wrongChannelDenied && wrongWorkspaceDenied &&
148
- JSON.stringify(main.tools) === JSON.stringify([...tools].sort());
149
- process.stdout.write(`${JSON.stringify({
150
- ok,
151
- profileId,
152
- workspaceId,
153
- tools: main.tools,
154
- safeRead: main.safeRead,
155
- unexpectedToolDenied: main.unexpectedToolDenied,
156
- wrongChannelDenied: main.wrongChannelDenied,
157
- wrongWorkspaceDenied,
158
- })}\n`);
159
- if (!ok) process.exitCode = 1;
160
- } catch {
161
- process.stdout.write(`${JSON.stringify({ ok: false, code: "runtime_probe_rejected" })}\n`);
162
- process.exitCode = 1;
163
- }
@@ -1,93 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn } from "node:child_process";
4
- import { closeSync, constants, fstatSync, openSync, unlinkSync } from "node:fs";
5
-
6
- const APP_PATH = "/run/bootstrap/slack-app";
7
- const BOT_PATH = "/run/bootstrap/slack-bot";
8
- const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
9
-
10
- function required(name) {
11
- const value = process.env[name];
12
- if (typeof value !== "string" || !value) throw new Error(`missing ${name}`);
13
- return value;
14
- }
15
-
16
- function identity(name) {
17
- const value = required(name);
18
- if (!/^[1-9][0-9]{0,9}$/.test(value)) throw new Error("runtime identity rejected");
19
- return value;
20
- }
21
-
22
- function inheritedSecret(path) {
23
- const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
24
- const info = fstatSync(fd);
25
- if (!info.isFile() || info.uid !== 0 || info.gid !== 0 || (info.mode & 0o777) !== 0o600 || info.size < 6 || info.size > 4096) {
26
- closeSync(fd);
27
- throw new Error("runtime Slack descriptor source rejected");
28
- }
29
- unlinkSync(path);
30
- return fd;
31
- }
32
-
33
- async function run() {
34
- if (process.argv.length !== 2 || process.getuid?.() !== 0 || process.getgid?.() !== 0) {
35
- throw new Error("sandbox init identity rejected");
36
- }
37
- const uid = identity("SELLABLE_AGENT_RUNTIME_UID");
38
- const gid = identity("SELLABLE_AGENT_RUNTIME_GID");
39
- const profileId = required("SELLABLE_AGENT_PROFILE_ID");
40
- if (!SAFE_ID.test(profileId)) throw new Error("sandbox profile rejected");
41
- const hermes = required("SELLABLE_AGENT_HERMES_EXECUTABLE");
42
- const setpriv = required("SELLABLE_AGENT_SETPRIV_EXECUTABLE");
43
- if (!hermes.startsWith("/") || !setpriv.startsWith("/") || setpriv !== "/usr/bin/setpriv") {
44
- throw new Error("sandbox executable rejected");
45
- }
46
- const appFd = inheritedSecret(APP_PATH);
47
- const botFd = inheritedSecret(BOT_PATH);
48
- try {
49
- const args = [
50
- "--reuid", uid,
51
- "--regid", gid,
52
- "--clear-groups",
53
- "--no-new-privs",
54
- "--bounding-set=-all",
55
- "--inh-caps=-all",
56
- "--ambient-caps=-all",
57
- hermes,
58
- "--profile", profileId,
59
- "gateway", "run",
60
- ];
61
- const stdio = Array(10).fill("ignore");
62
- stdio[1] = "inherit";
63
- stdio[2] = "inherit";
64
- stdio[8] = appFd;
65
- stdio[9] = botFd;
66
- const child = spawn(setpriv, args, {
67
- env: process.env,
68
- shell: false,
69
- stdio,
70
- });
71
- closeSync(appFd);
72
- closeSync(botFd);
73
- const forwardTerm = () => { if (!child.killed) child.kill("SIGTERM"); };
74
- const forwardInt = () => { if (!child.killed) child.kill("SIGINT"); };
75
- process.once("SIGTERM", forwardTerm);
76
- process.once("SIGINT", forwardInt);
77
- process.exitCode = await new Promise((resolveExit, rejectExit) => {
78
- child.once("error", rejectExit);
79
- child.once("exit", (code, signal) => resolveExit(code ?? (signal ? 1 : 0)));
80
- });
81
- } catch (error) {
82
- try { closeSync(appFd); } catch {}
83
- try { closeSync(botFd); } catch {}
84
- throw error;
85
- }
86
- }
87
-
88
- try {
89
- await run();
90
- } catch {
91
- process.stderr.write("sellable-agent sandbox init failed closed\n");
92
- process.exitCode = 1;
93
- }