@sellable/install 0.1.363 → 0.1.364-phase111.20260720135054

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.
@@ -3712,8 +3712,7 @@ function upsertCodexMcpServerConfig(content, opts) {
3712
3712
  content,
3713
3713
  "mcp_servers.sellable",
3714
3714
  `[mcp_servers.sellable]
3715
- url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}
3716
- default_tools_approval_mode = "approve"`
3715
+ url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}`
3717
3716
  );
3718
3717
  content = removeTomlSection(content, "mcp_servers.sellable.env");
3719
3718
  return content;
@@ -3725,8 +3724,7 @@ default_tools_approval_mode = "approve"`
3725
3724
  "mcp_servers.sellable",
3726
3725
  `[mcp_servers.sellable]
3727
3726
  command = ${quoteToml(command)}
3728
- args = ${tomlArray(args)}
3729
- default_tools_approval_mode = "approve"`
3727
+ args = ${tomlArray(args)}`
3730
3728
  );
3731
3729
  content = upsertTomlTable(
3732
3730
  content,
@@ -3740,9 +3738,6 @@ ${tomlEnvTable("codex", opts)}`
3740
3738
  function codexMcpServerMatches(content, opts) {
3741
3739
  const server = readTomlTable(content, "mcp_servers.sellable");
3742
3740
  if (!server) return false;
3743
- if (!server.includes('default_tools_approval_mode = "approve"')) {
3744
- return false;
3745
- }
3746
3741
  if (opts.server === "hosted") {
3747
3742
  return (
3748
3743
  server.includes(
@@ -285,14 +285,14 @@ export const HERMES_AGENT_BRIDGE_CONTRACT = normalizeContract({
285
285
  {
286
286
  path: "plugins/platforms/slack/adapter.py",
287
287
  patchKind: "slack-adapter",
288
- sourceSha256: "20b12d9bbff0f5b280e0be8e795c7327f4089c3fa8b04d7a6d346dabae591f2a",
289
- patchedSha256: "548ab70f515e22830cbcbf6b0691768272dd708fd0118f7c601e1e1232bb016b",
288
+ sourceSha256: "a84f1f15368d8d93341a1c2449c45ca13ebe92938049e92d80ce5cab0e1a6294",
289
+ patchedSha256: "4ba3b1f5b443abf970b8888de3c1b21ab7f043645aba6304432453c1dc643f55",
290
290
  },
291
291
  {
292
292
  path: "tools/mcp_tool.py",
293
293
  patchKind: "mcp-tool",
294
- sourceSha256: "1bcceccaeb8ec8894c83819d389a64258517f4b48db1c8fea5c486633e56fe8b",
295
- patchedSha256: "44bf5dfeca47bf91fc5e68ecd6455c33f41afd2b04fa16d213f59115ddb38f21",
294
+ sourceSha256: "ee453ede1c72c90b28155e292a352bfba96eb1793c47eb8667d16fe79beb181e",
295
+ patchedSha256: "c0923d8c88d507a766121dab9f0f36e99963f85a9124cbef2908ee7176d56e07",
296
296
  },
297
297
  ],
298
298
  });
@@ -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.363" ||
961
- pinned.mcpPackage !== "@sellable/mcp@0.1.620" ||
960
+ pinned.installerPackage !== "@sellable/install@0.1.357" ||
961
+ pinned.mcpPackage !== "@sellable/mcp@0.1.619" ||
962
962
  !Array.isArray(policy.toolInclude) || policy.toolInclude.length === 0 ||
963
963
  !Number.isSafeInteger(slack.generation) || slack.generation < 1 ||
964
964
  !/^A[A-Z0-9]{8,20}$/.test(slack.appId ?? "") ||
@@ -1423,11 +1423,6 @@ 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
- });
1431
1426
  await helper({ action: "INSTALL_HERMES_BRIDGE", ...helperIdentity() });
1432
1427
  await controlRuntime("UP");
1433
1428
  },
@@ -1496,6 +1491,17 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
1496
1491
  lifecycle: {},
1497
1492
  };
1498
1493
  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
+ };
1499
1505
  const rawRuntimeObservation = async ({ drift = false } = {}) => {
1500
1506
  const observed = (await helper({
1501
1507
  action: drift ? "OBSERVE_RUNTIME_DRIFT" : "OBSERVE_RUNTIME",
@@ -1558,11 +1564,6 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
1558
1564
  revisionId: result.receipt.revisionId,
1559
1565
  profileDigest: result.receipt.profileDigest,
1560
1566
  });
1561
- await helper({
1562
- action: "INSTALL_PROVIDER_CREDENTIAL",
1563
- ...helperIdentity(),
1564
- fingerprint: context.desired.providerFingerprint,
1565
- });
1566
1567
  await helper({ action: "INSTALL_HERMES_BRIDGE", ...helperIdentity() });
1567
1568
  };
1568
1569
  const refreshThen = (effect) => async (input) => {
@@ -1570,8 +1571,8 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
1570
1571
  return effect(input);
1571
1572
  };
1572
1573
  Object.assign(ports.refresh, {
1573
- reloadMcp: refreshThen(restart),
1574
- reloadSkills: refreshThen(restart),
1574
+ reloadMcp: refreshThen(() => reload("/reload-mcp")),
1575
+ reloadSkills: refreshThen(() => reload("/reload-skills")),
1575
1576
  reloadEnvironmentOrRestart: refreshThen(restart),
1576
1577
  restartGateway: refreshThen(restart),
1577
1578
  restartProfileSession: refreshThen(restart),
@@ -1674,8 +1675,11 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
1674
1675
  async fenceDispatch() { await setRuntimeLifecycle("OFF"); },
1675
1676
  async fenceRouteOutput() { await setRuntimeLifecycle("ARCHIVED"); },
1676
1677
  async drain() {
1677
- await controlRuntime("DOWN");
1678
- const observed = { drained: true, remaining: 0 };
1678
+ const base = new URL(config.runtimeControlUrl);
1679
+ const response = await fetch(new URL("/drain", base), { method: "POST", signal: AbortSignal.timeout(15_000) });
1680
+ if (!response.ok) throw new Error("drain rejected");
1681
+ const observed = await response.json();
1682
+ if (!observed?.drained || observed.remaining !== 0) throw new Error("drain readback rejected");
1679
1683
  atomicPrivateJson(join(realpathSync(config.stateRoot), `drain-${context.activeClaim.operation.id}.json`), {
1680
1684
  operationId: context.activeClaim.operation.id,
1681
1685
  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.363" ||
135
- desired.mcpPackage !== "@sellable/mcp@0.1.620" ||
134
+ desired.installerPackage !== "@sellable/install@0.1.357" ||
135
+ desired.mcpPackage !== "@sellable/mcp@0.1.619" ||
136
136
  !Array.isArray(desired.toolInclude) || desired.toolInclude.length === 0 ||
137
137
  desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
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.363";
23
- export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.620";
22
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.357";
23
+ export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.619";
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.363" ||
579
+ manifest.installerVersion !== "0.1.357" ||
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.363",
784
+ installVersion: "0.1.357",
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.363",
824
+ installVersion: "0.1.357",
825
825
  mcpPackage: PINNED_MCP_PACKAGE,
826
826
  command: "hermes profile bootstrap",
827
827
  },
@@ -23,7 +23,6 @@ export const EXTERNAL_HERMES_ENTRYPOINT = `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/herm
23
23
  const EXTERNAL_RUNTIME_MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
24
24
  const ACTIONS = new Set([
25
25
  "INSTALL_MCP_CREDENTIAL",
26
- "INSTALL_PROVIDER_CREDENTIAL",
27
26
  "INSTALL_RUNTIME_SERVICE_ENV",
28
27
  "SET_RUNTIME_GATES",
29
28
  "OBSERVE_RUNTIME_GATES",
@@ -51,7 +50,6 @@ const PROFILE_ENTRIES = Object.freeze([
51
50
  ]);
52
51
  const ACTION_KEYS = Object.freeze({
53
52
  INSTALL_MCP_CREDENTIAL: ["action", "profileId", "operationId", "fence", "sourceRequestFile", "generation", "fingerprint"],
54
- INSTALL_PROVIDER_CREDENTIAL: ["action", "profileId", "operationId", "fence", "fingerprint"],
55
53
  INSTALL_RUNTIME_SERVICE_ENV: ["action", "profileId", "operationId", "fence", "revisionId", "profileDigest"],
56
54
  SET_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
57
55
  OBSERVE_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
@@ -111,79 +109,6 @@ function atomicCredential(pathValue, secret, uid, gid) {
111
109
  fsyncDirectory(dirname(pathValue));
112
110
  }
113
111
 
114
- function providerCredentialPath(profile, profileId) {
115
- const runtimeStateRoot = safeRuntimeStateRoot(profile);
116
- const profilesRoot = join(runtimeStateRoot, "profiles");
117
- const profileRoot = join(profilesRoot, profileId);
118
- const runtimeHome = join(profileRoot, "home");
119
- for (const pathValue of [profilesRoot, profileRoot, runtimeHome]) {
120
- const link = lstatSync(pathValue);
121
- if (
122
- link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o777) !== 0o700 ||
123
- link.uid !== profile.runtimeUid || link.gid !== profile.runtimeGid
124
- ) throw new Error("provider runtime home rejected");
125
- }
126
- const codexRoot = join(runtimeHome, ".codex");
127
- if (!existsSync(codexRoot)) {
128
- mkdirSync(codexRoot, { mode: 0o700 });
129
- chownSync(codexRoot, profile.runtimeUid, profile.runtimeGid);
130
- }
131
- const codex = lstatSync(codexRoot);
132
- if (
133
- codex.isSymbolicLink() || !codex.isDirectory() || (codex.mode & 0o777) !== 0o700 ||
134
- codex.uid !== profile.runtimeUid || codex.gid !== profile.runtimeGid
135
- ) throw new Error("provider credential root rejected");
136
- return join(codexRoot, "auth.json");
137
- }
138
-
139
- function installProviderCredential(profile, request, config) {
140
- if (
141
- !SHA256.test(request.fingerprint ?? "") ||
142
- request.fingerprint !== profile.providerCredentialFingerprint ||
143
- (
144
- profile.providerCredentialSourcePath !== "/var/lib/sellable-agent-bootstrap/provider-auth" &&
145
- config.disposableFixture !== true
146
- )
147
- ) throw new Error("provider credential identity rejected");
148
- const sourceRoot = privateRoot(dirname(profile.providerCredentialSourcePath));
149
- const source = readPrivateDirectFile(sourceRoot, profile.providerCredentialSourcePath).trim();
150
- if (!source || source.length > 1024 * 1024 || sha256(source) !== request.fingerprint) {
151
- throw new Error("provider credential source rejected");
152
- }
153
- const parsed = JSON.parse(source);
154
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
155
- throw new Error("provider credential JSON rejected");
156
- }
157
- const target = providerCredentialPath(profile, request.profileId);
158
- atomicCredential(target, source, profile.runtimeUid, profile.runtimeGid);
159
- return {
160
- ok: true,
161
- profileId: request.profileId,
162
- fingerprint: request.fingerprint,
163
- path: target,
164
- };
165
- }
166
-
167
- function removeProviderCredential(profile, profileId) {
168
- const target = providerCredentialPath(profile, profileId);
169
- if (existsSync(target)) {
170
- const link = lstatSync(target);
171
- if (link.isSymbolicLink() || !link.isFile()) throw new Error("provider credential cleanup rejected");
172
- rmSync(target, { force: true });
173
- fsyncDirectory(dirname(target));
174
- }
175
- }
176
-
177
- function observeProviderCredential(profile, profileId) {
178
- const target = providerCredentialPath(profile, profileId);
179
- const link = lstatSync(target);
180
- if (
181
- link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 ||
182
- link.uid !== profile.runtimeUid || link.gid !== profile.runtimeGid
183
- ) throw new Error("provider credential readback rejected");
184
- return sha256(readFileSync(target, "utf8").trim());
185
- }
186
-
187
112
  function applyRuntimeOwnership(pathValue, uid, gid) {
188
113
  const link = lstatSync(pathValue);
189
114
  if (link.isSymbolicLink()) throw new Error("profile symlink rejected");
@@ -584,9 +509,7 @@ function walkExternalRoot(root, ownerUid, ownerGid) {
584
509
  }
585
510
  };
586
511
  visit(root, "");
587
- const byPath = (left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
588
- const byLogicalPath = (left, right) => left.logicalPath < right.logicalPath ? -1 : left.logicalPath > right.logicalPath ? 1 : 0;
589
- return { directories: directories.sort(byPath), files: files.sort(byLogicalPath) };
512
+ return { directories: directories.sort((left, right) => left.path.localeCompare(right.path)), files };
590
513
  }
591
514
 
592
515
  export function observeExternalRuntimeClosure(input = {}) {
@@ -646,24 +569,18 @@ export function observeExternalRuntimeClosure(input = {}) {
646
569
  if (
647
570
  JSON.stringify(rootRecord.directories.map((directory) => directory?.path)) !==
648
571
  JSON.stringify(rootRecord.directories.map((directory) => directory?.path).sort()) ||
649
- new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length
650
- ) throw new Error("external runtime directory order rejected");
651
- if (
572
+ new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length ||
652
573
  rootRecord.directories.some((directory) =>
653
574
  !exactKeys(directory, ["path", "mode"]) ||
654
- !/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(directory.path ?? "") ||
575
+ !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(directory.path ?? "") ||
655
576
  directory.mode !== 0o555
656
- )
657
- ) throw new Error("external runtime directory schema rejected");
658
- if (JSON.stringify(walked.directories) !== JSON.stringify(rootRecord.directories)) {
659
- throw new Error("external runtime directory inventory rejected");
660
- }
661
- if (
577
+ ) ||
578
+ JSON.stringify(walked.directories) !== JSON.stringify(rootRecord.directories) ||
662
579
  rootRecord.files.length === 0 ||
663
580
  JSON.stringify(rootRecord.files.map((file) => file?.path)) !==
664
581
  JSON.stringify(rootRecord.files.map((file) => file?.path).sort()) ||
665
582
  new Set(rootRecord.files.map((file) => file?.path)).size !== rootRecord.files.length
666
- ) throw new Error("external runtime file order rejected");
583
+ ) throw new Error("external runtime inventory rejected");
667
584
  const actualPaths = walked.files.map((file) => file.logicalPath);
668
585
  if (JSON.stringify(actualPaths) !== JSON.stringify(rootRecord.files.map((file) => file.path))) {
669
586
  throw new Error("external runtime open inventory rejected");
@@ -671,7 +588,7 @@ export function observeExternalRuntimeClosure(input = {}) {
671
588
  const observedFiles = rootRecord.files.map((file, index) => {
672
589
  if (
673
590
  !exactKeys(file, ["path", "mode", "digest"]) ||
674
- !/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(file.path ?? "") ||
591
+ !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(file.path ?? "") ||
675
592
  ![0o444, 0o555].includes(file.mode) || !SHA256.test(file.digest ?? "")
676
593
  ) throw new Error("external runtime file schema rejected");
677
594
  const actual = walked.files[index];
@@ -1041,19 +958,9 @@ async function observeRuntime(profile, request) {
1041
958
  const profileFiles = observeRuntimeProfileFiles(target);
1042
959
  const { runtimeConfig, runtimeMcp, tools, fileHashes: actualFileHashes } = profileFiles;
1043
960
  const actualProfileDigest = profileFiles.profileDigest;
1044
- let observedProvider;
1045
- try {
1046
- observedProvider = observeProviderCredential(profile, request.profileId);
1047
- if (observedProvider !== runtimeConfig?.provider?.fingerprint) {
1048
- throw new Error("provider credential fingerprint rejected");
1049
- }
1050
- } catch (error) {
1051
- if (!driftMode) throw error;
1052
- observedProvider = sha256(`provider-drift\0${runtimeConfig?.provider?.fingerprint ?? "absent"}`);
1053
- }
1054
- if (environmentDrift) {
1055
- observedProvider = sha256(`environment-drift\0${observedProvider}`);
1056
- }
961
+ const observedProvider = environmentDrift
962
+ ? sha256(`environment-drift\0${runtimeConfig?.provider?.fingerprint ?? "absent"}`)
963
+ : runtimeConfig?.provider?.fingerprint;
1057
964
  return {
1058
965
  profileId: manifest.profileId,
1059
966
  workspaceId: manifest.workspaceId,
@@ -1265,9 +1172,6 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
1265
1172
  generation: request.generation,
1266
1173
  };
1267
1174
  }
1268
- if (request.action === "INSTALL_PROVIDER_CREDENTIAL") {
1269
- return installProviderCredential(profile, request, config);
1270
- }
1271
1175
  if (request.action === "INSTALL_RUNTIME_SERVICE_ENV") {
1272
1176
  return installRuntimeServiceEnvironment(profile, request);
1273
1177
  }
@@ -1283,7 +1187,6 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
1283
1187
  rmSync(target, { recursive: true, force: true });
1284
1188
  fsyncDirectory(root);
1285
1189
  }
1286
- removeProviderCredential(profile, request.profileId);
1287
1190
  return { ok: true, profileId: request.profileId, disposition: "REMOVED" };
1288
1191
  }
1289
1192
  if (request.action === "CHOWN_PROFILE") {
@@ -247,8 +247,6 @@ function validateInstallInput(input) {
247
247
  profile.hermesSourceRoot !== `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes` ||
248
248
  profile.proxyConfigPath !== "/etc/sellable-agent/egress-proxy.json" ||
249
249
  profile.runtimeBoundaryConfigPath !== "/etc/sellable-agent/runtime-boundary.json" ||
250
- profile.providerCredentialSourcePath !== "/var/lib/sellable-agent-bootstrap/provider-auth" ||
251
- !SHA256.test(profile.providerCredentialFingerprint ?? "") ||
252
250
  profile.runtimeServicePath !== "/etc/services.d/sellable-agent-runtime" ||
253
251
  profile.runtimeServiceEnvRoot !== "/etc/services.d/sellable-agent-runtime/env" ||
254
252
  new Set([profile.runtimeUid, profile.proxyUid, profile.workerUid]).size !== 3
@@ -305,7 +303,7 @@ function configurationFiles(input, checked) {
305
303
  ].join("\n");
306
304
  return [
307
305
  ["/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`, 0o444],
306
+ ["/etc/sellable-agent/worker.json", `${JSON.stringify(input.workerConfig, null, 2)}\n`, 0o600],
309
307
  ["/etc/sellable-agent/runtime-helper.json", `${JSON.stringify(input.helperConfig, null, 2)}\n`, 0o600],
310
308
  ["/etc/sudoers.d/sellable-agent-runtime-helper", sudoers, 0o440],
311
309
  ].map(([logicalPath, bytes, mode]) => ({ logicalPath, bytes, mode }));
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.363",
3
+ "version": "0.1.364-phase111.20260720135054",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {
7
7
  "sellable": "bin/sellable-install.mjs",
8
- "sellable-agent-host-bootstrap": "bin/sellable-agent-host-bootstrap.mjs",
9
8
  "sellable-agent-egress-proxy": "bin/sellable-agent-egress-proxy.mjs",
10
9
  "sellable-agent-host-worker": "bin/sellable-agent-host-worker.mjs",
11
10
  "sellable-agent-runtime-helper": "bin/sellable-agent-runtime-helper.mjs",
@@ -21,7 +20,6 @@
21
20
  "bin",
22
21
  "lib",
23
22
  "services",
24
- "container",
25
23
  "agents",
26
24
  "skill-templates",
27
25
  "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,35 @@ 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
+
73
+ After a committed and dispatched revision receipt, continue only with the
74
+ planner-provided revision-derived `approve_messages` action. It must preserve
75
+ the exact revision cohort as `rowSelector:{type:"rowIds",rowIds:[...]}` plus
76
+ `approvalMode:"approve"`, the compiled readiness identity, and the canonical
77
+ `reportingContext`, even when the ordinary sender lane is ineligible. Do not
78
+ replace it with `queue_campaign_cells` on Approved cells; that path cannot mint
79
+ the authoritative approval/lane-scope receipt. Require `laneScope` in the
80
+ bounded preparation receipt before the lane is considered approved. A
81
+ disconnected or zero-capacity sender still blocks scheduling and does not
82
+ authorize reconnecting, campaign start, or direct send.
83
+
58
84
  If a stale handle loses the lease, the tool reports the holder status and the
59
85
  approximately 10 minute lockout window. Reinvoke with the current handle or wait
60
86
  for lease expiry; never guess a fence.
@@ -85,6 +111,10 @@ The run may execute only packet-named bounded work:
85
111
  when the campaign is in the packet's pinned lane chain;
86
112
  - refresh paid InMail credit facts during bootstrap and once per scheduler-wait
87
113
  entry when the packet requires it.
114
+ - apply `revise_message_template_and_rerun` only when the packet contains the
115
+ complete explicit user-approved revision envelope and exact fenced target.
116
+ - after that revision commits, apply only its exact-row, readiness-bound
117
+ `approve_messages` continuation and require the returned lane-scope receipt.
88
118
 
89
119
  The loop records planned -> did -> outcome before and after every foreign
90
120
  mutation. It refuses stale packets by fingerprint and validates packet
@@ -168,5 +198,10 @@ reason or resume handle, lane source, lane chain, chosen label with token
168
198
  secondary, plan revision, journal path, blocked continuation packets, and
169
199
  firstFailing checklist when present.
170
200
 
201
+ Preserve `refill_reporting.v2` exactly in progress, continuation, replay, and
202
+ terminal results. Do not recompute its semantic counts or drop live-preparation
203
+ epoch, snapshot sequence, reset reason, watermark, blockers, compatibility, or
204
+ terminal fields. Never surface raw prospect or message-copy fields.
205
+
171
206
  Real-run journals live under `~/.sellable/refill/runs` by default and append an
172
207
  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,49 @@ 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.
124
+ After the revision receipt is committed and dispatched, the next mutation must
125
+ be the planner-provided revision-derived `approve_messages` action. Preserve
126
+ its exact `rowSelector:{type:"rowIds",rowIds:[...]}` cohort,
127
+ `approvalMode:"approve"`, compiled readiness authority, and
128
+ `reportingContext`, even if the ordinary sender lane is ineligible. Never use
129
+ `queue_campaign_cells` to flip Approved cells: that path cannot produce the
130
+ authoritative approval/lane-scope receipt. Require the bounded preparation
131
+ receipt's `laneScope` before considering the selected first-touch lane
132
+ approved. Zero sender capacity remains a scheduler blocker and never authorizes
133
+ reconnection, campaign start, or direct send.
134
+
135
+ Before that approval mutation, perform an exact authoritative copy audit with
136
+ `select_campaign_cells({ columnRole:"generateMessage",
137
+ rowSelector:{type:"rowIds",rowIds:[...]},
138
+ includeGeneratedMessageReview:true })` over the complete revision cohort in
139
+ batches of at most 20. Require every
140
+ `authoritativeGeneratedMessageReview.complete:true`, every row status to be
141
+ completed-like, the exact user-approved rendered subject/body, the expected
142
+ `templateAuthorityDigest`, and `matchesCurrentTemplateAuthority:true`. This
143
+ projection is the only MCP copy-review source of truth. Never use formula
144
+ Message/Subject columns, campaign message previews, table previews, or CSV
145
+ exports to validate revised Generate Message results. If any row is missing,
146
+ non-current, or text/digest mismatched, stop before approval and report
147
+ `regenerated_copy_integrity_failed`; never regenerate again implicitly or
148
+ approve the cohort on counts/revision hashes alone.
149
+
107
150
  Safe primitives are paid-credit refresh,
108
151
  existing-row message preparation, generated-message approval, receipt-proven
109
152
  same-source row copy, one bounded Signal Discovery search derived only from
@@ -567,6 +610,11 @@ scheduler wait loop. Do not finish the refill goal, mark it complete, or mark it
567
610
  blocked only because it is still `awaiting_scheduler_after_ready_buffer`; keep
568
611
  waiting unless Christian stops the run or the host cannot continue.
569
612
 
613
+ Carry the canonical `refill_reporting.v2` DTO through progress, continuation,
614
+ replay, and terminal reporting without recomputing counts or losing live epoch,
615
+ snapshot sequence, reset reason, watermark, blockers, compatibility, or
616
+ terminal outcome. Redact raw copy and prospect fields.
617
+
570
618
  Future scheduled coverage and already sent actions are distinct; only
571
619
  scheduler-owned future scheduled actions count as scheduled coverage. For
572
620
  multiple target dates, finish D1 execution and the full D1 reread before
@@ -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,41 +0,0 @@
1
- ARG HERMES_IMAGE=ghcr.io/hostinger/hvps-hermes-agent@sha256:f048663be9ba564de795df5f2a8184f32b92561a1f053c179dca3b45fa5bc8b9
2
- FROM ${HERMES_IMAGE}
3
-
4
- USER root
5
-
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.363
7
- ARG MCP_PACKAGE=@sellable/mcp@0.1.620
8
- ARG INSTALLER_INTEGRITY
9
- ARG MCP_INTEGRITY=sha512-N+2clnLHWAlvoXCjX10HaeEUMPLqZQHIX+x/2e1CSJbqb56CU1SAtVdwmhXyha7+MQT0DZF7pm2+WnoZKFEzOw==
10
-
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.363" \
12
- && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.620" \
13
- && test -n "${INSTALLER_INTEGRITY}" \
14
- && test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
15
- && test "$(npm view "${MCP_PACKAGE}" dist.integrity)" = "${MCP_INTEGRITY}" \
16
- && apt-get update \
17
- && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
18
- bubblewrap \
19
- ca-certificates \
20
- nftables \
21
- sudo \
22
- && rm -rf /var/lib/apt/lists/* \
23
- && mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
24
- && npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
25
- && npm install --prefix /opt/sellable-agent/mcp-root --omit=dev --ignore-scripts --no-audit --no-fund "${MCP_PACKAGE}" \
26
- && node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.363' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.620') throw new Error('package identity rejected');" \
27
- && node --input-type=module -e "import { buildExternalRuntimeClosure } from '/opt/sellable-agent/install-root/node_modules/@sellable/install/lib/sellable-agent/external-runtime-builder.mjs'; const built = buildExternalRuntimeClosure({ mcpPackageRoot: '/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp', mcpNodeModulesRoot: '/opt/sellable-agent/mcp-root/node_modules', hermesSourceRoot: '/opt/hermes', ownerUid: 0, ownerGid: 0 }); if (!built.closureDigest) throw new Error('runtime closure rejected');" \
28
- && node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.363', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.620', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
29
- && npm cache clean --force
30
-
31
- COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
32
- RUN test -x /usr/local/bin/node \
33
- && test ! -e /usr/bin/node \
34
- && ln -s /usr/local/bin/node /usr/bin/node \
35
- && test "$(readlink -f /usr/bin/node)" = "/usr/local/bin/node" \
36
- && chmod 0555 /usr/local/bin/sellable-agent-container-entrypoint
37
-
38
- HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
39
- CMD /command/s6-svstat /etc/services.d/sellable-agent-host-worker | grep -q '^up' || exit 1
40
-
41
- ENTRYPOINT ["/usr/local/bin/sellable-agent-container-entrypoint"]
@@ -1,15 +0,0 @@
1
- # Sellable Agent host container
2
-
3
- This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
4
-
5
- Build from this directory after `@sellable/install@0.1.363` is published:
6
-
7
- ```sh
8
- docker build --platform linux/amd64 \
9
- --build-arg INSTALLER_INTEGRITY='sha512-…' \
10
- --tag sellable-agent-host:0.1.363 .
11
- ```
12
-
13
- Create a private `bootstrap/` directory owned by root with mode `0700`. It must contain `bootstrap.json`, `provider-auth`, and `slack-app`, each owned by root with mode `0600`. `bootstrap.json` contains only non-secret identities, hashes, package integrities, and the control-plane URL. The other two files contain the provider JSON and global Slack app token whose hashes are pinned by `bootstrap.json`.
14
-
15
- Set `SELLABLE_AGENT_IMAGE` to the exact built image digest and start `compose.yaml`. The container bootstraps idempotently, writes `bootstrap/registration-receipt.json`, and starts the worker and isolated runtime under s6. Do not mount a Docker socket.