@sellable/install 0.1.593-wip.runtime-gates.20260811041542 → 0.1.593

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.
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.593-wip.runtime-gates.20260811041542
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.593
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.860
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-jRA1KsCV2d4Zcuo5/UmJiP8P0NaoC3ZDvUFRQOc2AMSNDVmUR6gWvtkyAiqb1lukbFWKzfVlz2/4EWzxvb7hkQ==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.593-wip.runtime-gates.20260811041542" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.593" \
12
12
  && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.860" \
13
13
  && test -n "${INSTALLER_INTEGRITY}" \
14
14
  && test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.593-wip.runtime-gates.2
23
23
  && mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
24
24
  && npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
25
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.593-wip.runtime-gates.20260811041542' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.860') throw new Error('package identity rejected');" \
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.592' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.860') throw new Error('package identity rejected');" \
27
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.593-wip.runtime-gates.20260811041542', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.860', 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}" \
28
+ && node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.593', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.860', 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
29
  && npm cache clean --force
30
30
 
31
31
  COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
@@ -3,13 +3,13 @@
3
3
  This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
4
4
 
5
5
  Build from this directory after
6
- `@sellable/install@0.1.593-wip.runtime-gates.20260811041542` is
6
+ `@sellable/install@0.1.593` is
7
7
  published:
8
8
 
9
9
  ```sh
10
10
  docker build --platform linux/amd64 \
11
11
  --build-arg INSTALLER_INTEGRITY='sha512-…' \
12
- --tag sellable-agent-host:0.1.593-wip.runtime-gates.20260811041542 .
12
+ --tag sellable-agent-host:0.1.592 .
13
13
  ```
14
14
 
15
15
  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`.
@@ -41,6 +41,7 @@ import {
41
41
  buildAdminAgentServiceEnvironment,
42
42
  buildNativeHermesLaunch,
43
43
  } from "../admin-artifact/node_modules/@sellable/install/lib/sellable-agent/fly-customer-worker.mjs";
44
+ import { expectedRuntimeGates } from "../admin-artifact/node_modules/@sellable/install/lib/sellable-agent/runtime-helper.mjs";
44
45
  import sodium from "../admin-artifact/node_modules/libsodium-wrappers/dist/modules-esm/libsodium-wrappers.mjs";
45
46
  import {
46
47
  claimWithSoulObservation,
@@ -1181,6 +1182,28 @@ async function consumeRuntimeSecrets({ claim, exchange, client }) {
1181
1182
  return { mcpService, slackApp, slackBot, modelCodex };
1182
1183
  }
1183
1184
 
1185
+ function writeAdminRuntimeGates(desiredLifecycle) {
1186
+ const gates = expectedRuntimeGates(desiredLifecycle);
1187
+ const path = join(SECRET_ROOT, "gates.json");
1188
+ atomicPrivateFile(path, `${JSON.stringify(gates)}\n`);
1189
+ const stat = lstatSync(path);
1190
+ let observed;
1191
+ try {
1192
+ observed = JSON.parse(readFileSync(path, "utf8"));
1193
+ } catch {
1194
+ throw new Error("admin_runtime_gates_rejected");
1195
+ }
1196
+ if (
1197
+ stat.isSymbolicLink() ||
1198
+ !stat.isFile() ||
1199
+ (stat.mode & 0o777) !== 0o600 ||
1200
+ canonicalJson(observed) !== canonicalJson(gates)
1201
+ ) {
1202
+ throw new Error("admin_runtime_gates_rejected");
1203
+ }
1204
+ return Object.freeze({ ...observed });
1205
+ }
1206
+
1184
1207
  function adminProfileFactoryAuthJson(model) {
1185
1208
  if (model.value.schemaVersion !== "sellable-agent-model-auth-runtime/v3") {
1186
1209
  return model.hermesRootAuthJson;
@@ -1326,6 +1349,7 @@ function installRuntimeSecrets({ claim, secrets, desiredLifecycle }) {
1326
1349
  slackSecretFile,
1327
1350
  `SLACK_BOT_TOKEN=${secrets.slackBot}\nSLACK_APP_TOKEN=${secrets.slackApp}\n`
1328
1351
  );
1352
+ const runtimeGates = writeAdminRuntimeGates(desiredLifecycle);
1329
1353
  const productConfigPath = join(SECRET_ROOT, "sellable-config.json");
1330
1354
  atomicPrivateFile(
1331
1355
  productConfigPath,
@@ -1344,6 +1368,7 @@ function installRuntimeSecrets({ claim, secrets, desiredLifecycle }) {
1344
1368
  modelAuthState,
1345
1369
  modelConfig: modelAuthConfigOverlay(model),
1346
1370
  productConfigPath,
1371
+ runtimeGates,
1347
1372
  slackSecretFile,
1348
1373
  };
1349
1374
  }
@@ -1528,6 +1553,7 @@ function clearRuntimeSecrets() {
1528
1553
  "slack-app",
1529
1554
  "slack-bot",
1530
1555
  "profile-slack.env",
1556
+ "gates.json",
1531
1557
  "sellable-config.json",
1532
1558
  ]) {
1533
1559
  rmSync(join(SECRET_ROOT, name), { force: true });
@@ -2088,6 +2114,7 @@ function completionReceipt({
2088
2114
  observedLifecycle,
2089
2115
  memorySnapshot,
2090
2116
  modelAuth,
2117
+ runtimeGates,
2091
2118
  }) {
2092
2119
  const identity = operationIdentity(claim, exchange);
2093
2120
  const routeGenerationDigest = flyRuntimeDigest(
@@ -2135,7 +2162,9 @@ function completionReceipt({
2135
2162
  package: sha256(JSON.stringify(packageSpecs())),
2136
2163
  config: operation.profileContract.digest,
2137
2164
  content: observedProfileDigest,
2138
- process: sha256(JSON.stringify({ servers: profile.servers, modelAuth })),
2165
+ process: sha256(
2166
+ JSON.stringify({ servers: profile.servers, modelAuth, runtimeGates })
2167
+ ),
2139
2168
  };
2140
2169
  return {
2141
2170
  ...identity,
@@ -2163,6 +2192,7 @@ function completionReceipt({
2163
2192
  : "MODEL_AUTH_DESIRED_PENDING_START",
2164
2193
  status: "TRUE",
2165
2194
  },
2195
+ { code: "RUNTIME_GATES_PROVEN", status: "TRUE" },
2166
2196
  { code: "SLACK_TRAFFIC_DISABLED", status: "TRUE" },
2167
2197
  { code: "NO_BOOT_INSTALL", status: "TRUE" },
2168
2198
  ],
@@ -2533,6 +2563,7 @@ export async function runAdminCanary() {
2533
2563
  },
2534
2564
  };
2535
2565
  const agentServiceEnvironment = adminAgentServiceEnvironment(canaryClaim);
2566
+ const runtimeGates = writeAdminRuntimeGates("ON");
2536
2567
 
2537
2568
  const template = JSON.parse(readFileSync(ADMIN_PROFILE_TEMPLATE_PATH));
2538
2569
  const { contentDigest, ...templateContent } = template;
@@ -2730,6 +2761,7 @@ export async function runAdminCanary() {
2730
2761
  profileFactory: "stageAdminProfile",
2731
2762
  profileDigest: profileContract.digest,
2732
2763
  servers: profile.servers,
2764
+ runtimeGates,
2733
2765
  workspaceAuthority: {
2734
2766
  agentKind: canaryClaim.agentKind,
2735
2767
  workspaceLockPresent:
@@ -3155,6 +3187,7 @@ async function runAdminProduction() {
3155
3187
  observedLifecycle: desiredLifecycle,
3156
3188
  memorySnapshot,
3157
3189
  modelAuth,
3190
+ runtimeGates: installed.runtimeGates,
3158
3191
  });
3159
3192
  operationHeartbeat.assertHealthy();
3160
3193
  const candidateLaunch =
@@ -7,7 +7,7 @@ install -d -o hermes -g hermes -m 0700 \
7
7
  /opt/data/profiles \
8
8
  /run/sellable-agent \
9
9
  /run/sellable-agent/admin-runtime
10
- chown hermes:hermes /opt/data
10
+ chown -R hermes:hermes /opt/data
11
11
  chmod 0700 /opt/data
12
12
 
13
13
  if [ -f /run/secrets/sellable-admin-recovery-private-key.pem ]; then
@@ -6,7 +6,7 @@ USER root
6
6
 
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.860
8
8
  ARG MCP_INTEGRITY=sha512-jRA1KsCV2d4Zcuo5/UmJiP8P0NaoC3ZDvUFRQOc2AMSNDVmUR6gWvtkyAiqb1lukbFWKzfVlz2/4EWzxvb7hkQ==
9
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.593-wip.runtime-gates.20260811041542
9
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.593
10
10
  ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.186-wip.phase158.20260802034727
11
11
  ARG ADMIN_MCP_INTEGRITY=sha512-qYNew5wd2IfgVcp1UYOG9QvpF/8Mx19YJ5HQtMKzvSfUf3evePjtaKsoXLf2C/GUJ5QlLlHJo84VWOI3V01+9A==
12
12
  ARG TUS_JS_CLIENT_VERSION=4.3.1
@@ -95,7 +95,7 @@ const RUNTIME_GENERATION = Number(
95
95
  const BOOT_SESSION_ID = randomUUID();
96
96
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.0";
97
97
  const INSTALLER_PACKAGE =
98
- process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.593-wip.runtime-gates.20260811041542";
98
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.593";
99
99
  const MCP_PACKAGE =
100
100
  process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.860";
101
101
 
@@ -217,6 +217,13 @@ export function customerRuntimeDoctorFailureCode(
217
217
  return null;
218
218
  }
219
219
 
220
+ export function customerRuntimeDoctorRequired(action) {
221
+ // START already proves the binaries, Slack identities, gateway, and private
222
+ // API directly below. The broad interactive diagnostic belongs to REFRESH;
223
+ // running it on every boot caused five-minute timeout/retry storms.
224
+ return action !== "ENABLE_GATEWAY";
225
+ }
226
+
220
227
  function exactArtifact(claim, type) {
221
228
  const artifacts = claim?.revision?.artifacts?.filter(
222
229
  (artifact) =>
@@ -1838,6 +1845,13 @@ function writeRuntimeHermesConfig({
1838
1845
  },
1839
1846
  },
1840
1847
  });
1848
+ const gatesPath = join(secretRoot, "gates.json");
1849
+ atomicJson(gatesPath, {
1850
+ lifecycle: modelLifecycle,
1851
+ dispatch: modelLifecycle === "ON" ? "ENABLED" : "DISABLED",
1852
+ route: modelLifecycle === "ON" ? "OPEN" : "CLOSED",
1853
+ output: modelLifecycle === "ON" ? "OPEN" : "CLOSED",
1854
+ });
1841
1855
  atomicJson(join(runtimeConfigRoot, "runtime-identity.json"), {
1842
1856
  runtimeId: RUNTIME_ID,
1843
1857
  profileId: desired.profileId,
@@ -1858,6 +1872,7 @@ function writeRuntimeHermesConfig({
1858
1872
  runtimeHome: runtimeConfigRoot,
1859
1873
  runtimeConfigPath,
1860
1874
  runtimeMcpPath,
1875
+ gatesPath,
1861
1876
  modelBundlePath,
1862
1877
  hermesRootAuthStorePath,
1863
1878
  hermesRootAuthMarkerPath,
@@ -1958,6 +1973,7 @@ export function applicationRuntimeEnv({ desired, runtimeConfig, secrets }) {
1958
1973
  HERMES_CONFIG_PATH: runtimeConfig.runtimeConfigPath,
1959
1974
  CODEX_HOME: dirname(runtimeConfig.modelBundlePath),
1960
1975
  SELLABLE_AGENT_RUNTIME: "1",
1976
+ SELLABLE_AGENT_RUNTIME_GATE_FILE: runtimeConfig.gatesPath,
1961
1977
  SELLABLE_AGENT_TURN_DESCRIPTOR_PATH: runtimeConfig.turnDescriptorPath,
1962
1978
  SLACK_HOME_CHANNEL: desired.defaultChannelId,
1963
1979
  SLACK_ALLOWED_CHANNELS: desired.selectedChannelIds.join(","),
@@ -2311,6 +2327,7 @@ async function proveProvisionedApplication({
2311
2327
  }
2312
2328
 
2313
2329
  async function proveApplication({
2330
+ action,
2314
2331
  desired,
2315
2332
  runtimeConfig,
2316
2333
  secrets,
@@ -2323,10 +2340,18 @@ async function proveApplication({
2323
2340
  profileDigest,
2324
2341
  });
2325
2342
  const runtimeEnv = applicationRuntimeEnv({ desired, runtimeConfig, secrets });
2326
- const doctor = await run(HERMES, ["doctor"], {
2327
- env: runtimeEnv,
2328
- timeoutMs: 300_000,
2329
- });
2343
+ const doctor = customerRuntimeDoctorRequired(action)
2344
+ ? await run(HERMES, ["doctor"], {
2345
+ env: runtimeEnv,
2346
+ timeoutMs: 300_000,
2347
+ })
2348
+ : {
2349
+ status: 0,
2350
+ stdout: "Hermes START readiness uses direct runtime checks.",
2351
+ stderr: "",
2352
+ timedOut: false,
2353
+ elapsedMs: 0,
2354
+ };
2330
2355
  const doctorFailure = customerRuntimeDoctorFailureCode(doctor);
2331
2356
  if (doctorFailure) throw new Error(doctorFailure);
2332
2357
  const slackResponse = await fetch(`${slackApiOrigin()}/api/auth.test`, {
@@ -2599,6 +2624,7 @@ async function processOperation({ claim, exchange, client, identity }) {
2599
2624
  ? proveProvisionedApplication
2600
2625
  : proveApplication;
2601
2626
  const proof = await prove({
2627
+ action: claim.operation.action,
2602
2628
  desired,
2603
2629
  runtimeConfig,
2604
2630
  secrets,
@@ -388,6 +388,7 @@ export function buildNativeHermesLaunch({
388
388
  HERMES_PROFILE: profileId,
389
389
  HERMES_CONFIG_PATH: configPath,
390
390
  SELLABLE_AGENT_RUNTIME: "1",
391
+ SELLABLE_AGENT_RUNTIME_GATE_FILE: join(resolvedSecretsRoot, "gates.json"),
391
392
  SELLABLE_SLACK_APP_TOKEN_FD: "8",
392
393
  SELLABLE_SLACK_BOT_TOKEN_FD: "9",
393
394
  }),
@@ -1088,7 +1088,8 @@ export function createFlyCustomerControlClient({
1088
1088
  return post(
1089
1089
  "heartbeat",
1090
1090
  "/api/v3/sellable-agent/worker/heartbeat",
1091
- identityBody
1091
+ identityBody,
1092
+ { replaySafe: true }
1092
1093
  );
1093
1094
  },
1094
1095
  async claimEndpoint(leaseSeconds = 60) {
@@ -306,6 +306,17 @@ def _sellable_agent_call_meta():
306
306
  # identity. Send no actor meta; the serving API enforces its own actor
307
307
  # gates, as with installs outside the Sellable runtime.
308
308
  return None
309
+ import json
310
+ gate_path = os.getenv("SELLABLE_AGENT_RUNTIME_GATE_FILE", "")
311
+ if not gate_path.startswith("/"):
312
+ raise RuntimeError("Sellable runtime gate path is not pinned")
313
+ try:
314
+ with open(gate_path, "r", encoding="utf-8") as gate_file:
315
+ gates = json.load(gate_file)
316
+ except (OSError, ValueError) as exc:
317
+ raise RuntimeError("Sellable runtime gates are unavailable") from exc
318
+ if gates != {"lifecycle": "ON", "dispatch": "ENABLED", "route": "OPEN", "output": "OPEN"}:
319
+ raise RuntimeError("Sellable runtime dispatch is fenced")
309
320
  from gateway.session_context import get_session_env, session_context_engaged
310
321
  if not session_context_engaged():
311
322
  raise RuntimeError("Sellable MCP calls require an active Hermes session context")
@@ -594,6 +605,17 @@ def _sellable_agent_call_meta():
594
605
  # identity. Send no actor meta; the serving API enforces its own actor
595
606
  # gates, as with installs outside the Sellable runtime.
596
607
  return None
608
+ import json
609
+ gate_path = os.getenv("SELLABLE_AGENT_RUNTIME_GATE_FILE", "")
610
+ if not gate_path.startswith("/"):
611
+ raise RuntimeError("Sellable runtime gate path is not pinned")
612
+ try:
613
+ with open(gate_path, "r", encoding="utf-8") as gate_file:
614
+ gates = json.load(gate_file)
615
+ except (OSError, ValueError) as exc:
616
+ raise RuntimeError("Sellable runtime gates are unavailable") from exc
617
+ if gates != {"lifecycle": "ON", "dispatch": "ENABLED", "route": "OPEN", "output": "OPEN"}:
618
+ raise RuntimeError("Sellable runtime dispatch is fenced")
597
619
  from gateway.session_context import get_session_env, session_context_engaged
598
620
  if not session_context_engaged():
599
621
  raise RuntimeError("Sellable MCP calls require an active Hermes session context")
@@ -38,8 +38,7 @@ import { installAgentHostServices } from "./service-installer.mjs";
38
38
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
39
39
  const VERSION = "sellable-agent-host-bootstrap/v1";
40
40
  const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
41
- const INSTALLER_PACKAGE =
42
- "@sellable/install@0.1.593-wip.runtime-gates.20260811041542";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.593";
43
42
  const MCP_PACKAGE = "@sellable/mcp@0.1.860";
44
43
  const HERMES_VERSION = "0.18.0";
45
44
  const SHA256 = /^[a-f0-9]{64}$/;
@@ -1546,7 +1546,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1546
1546
  ]) ||
1547
1547
  pinned.hermesCli !== "hermes" ||
1548
1548
  pinned.hermesVersion !== "0.18.0" ||
1549
- pinned.installerPackage !== "@sellable/install@0.1.593-wip.runtime-gates.20260811041542" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.593" ||
1550
1550
  pinned.mcpPackage !== "@sellable/mcp@0.1.860" ||
1551
1551
  !Array.isArray(policy.toolInclude) ||
1552
1552
  policy.toolInclude.length === 0 ||
@@ -2122,6 +2122,12 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2122
2122
  await helper({ action: "VERIFY_HERMES_BRIDGE", ...helperIdentity() });
2123
2123
  return helper({ action: "CONTROL_RUNTIME", ...helperIdentity(), control });
2124
2124
  };
2125
+ const setRuntimeLifecycle = (lifecycle) =>
2126
+ helper({
2127
+ action: "SET_RUNTIME_GATES",
2128
+ ...helperIdentity(),
2129
+ lifecycle,
2130
+ });
2125
2131
  const observeRuntimeLifecycle = (lifecycle) =>
2126
2132
  helper({
2127
2133
  action: "OBSERVE_RUNTIME_LIFECYCLE",
@@ -2562,13 +2568,10 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2562
2568
  });
2563
2569
  Object.assign(ports.lifecycle, {
2564
2570
  async fenceDispatch() {
2565
- // Lifecycle authority is the service itself. Stopping it closes dispatch
2566
- // without introducing a second file-backed switch that can strand an
2567
- // otherwise healthy runtime.
2568
- await controlRuntime("DOWN");
2571
+ await setRuntimeLifecycle("OFF");
2569
2572
  },
2570
2573
  async fenceRouteOutput() {
2571
- await controlRuntime("DOWN");
2574
+ await setRuntimeLifecycle("ARCHIVED");
2572
2575
  },
2573
2576
  async drain() {
2574
2577
  await controlRuntime("DOWN");
@@ -2588,6 +2591,7 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2588
2591
  return observed;
2589
2592
  },
2590
2593
  async startGateway() {
2594
+ await setRuntimeLifecycle("ON");
2591
2595
  await controlRuntime("UP");
2592
2596
  await observeRuntimeLifecycle("ON");
2593
2597
  },
@@ -2597,6 +2601,7 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2597
2601
  ? "ARCHIVING"
2598
2602
  : "OFF";
2599
2603
  await controlRuntime("DOWN");
2604
+ await setRuntimeLifecycle(lifecycle);
2600
2605
  await observeRuntimeLifecycle(lifecycle);
2601
2606
  },
2602
2607
  async proveNoDispatch() {
@@ -2604,7 +2609,9 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2604
2609
  context.activeClaim.operation.action === "ARCHIVE" ? "ARCHIVED" : "OFF";
2605
2610
  const observed = await observeRuntimeLifecycle(lifecycle);
2606
2611
  return {
2607
- noDispatch: observed.process.ready === false,
2612
+ noDispatch:
2613
+ observed.gates.dispatch === "FENCED" &&
2614
+ observed.process.ready === false,
2608
2615
  };
2609
2616
  },
2610
2617
  async observeGateway(input) {
@@ -2621,6 +2628,7 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2621
2628
  revisionId: observed.revisionId,
2622
2629
  credentialGeneration: observed.credentialGeneration,
2623
2630
  profileDigest: observed.profileDigest,
2631
+ gates: observed.gates,
2624
2632
  secretsPresent: observed.secretsPresent,
2625
2633
  process: observed.process,
2626
2634
  runtimeObservation: observed.runtimeObservation,
@@ -2671,6 +2679,7 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2671
2679
  };
2672
2680
  },
2673
2681
  async rollback({ priorState }) {
2682
+ await setRuntimeLifecycle(priorState.observedLifecycle);
2674
2683
  if (priorState.observedLifecycle === "ON") await controlRuntime("UP");
2675
2684
  else await controlRuntime("DOWN");
2676
2685
  await observeRuntimeLifecycle(priorState.observedLifecycle);
@@ -2679,35 +2688,21 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2679
2688
  try {
2680
2689
  const evidence = {};
2681
2690
  const confirmedSteps = [];
2682
- // Older progress receipts name this step FENCE_ROUTE_OUTPUT. Its
2683
- // concrete proof is now the stopped service, not a gates.json file.
2684
- // Preserve the step identity so an in-flight archive can resume across
2685
- // this runtime upgrade.
2686
- let stopped;
2691
+ let gates;
2687
2692
  try {
2688
- stopped = await observeRuntimeLifecycle("ARCHIVING");
2693
+ gates = await helper({
2694
+ action: "OBSERVE_RUNTIME_GATES",
2695
+ ...helperIdentity(),
2696
+ lifecycle: "ARCHIVED",
2697
+ });
2689
2698
  } catch {
2690
- try {
2691
- stopped = await observeRuntimeLifecycle("ARCHIVED");
2692
- } catch {
2693
- return {
2694
- ok: true,
2695
- observationId: `archive_${canonicalHostWorkerDigest({ service: "not_stopped" })}`,
2696
- confirmedSteps,
2697
- };
2698
- }
2699
- }
2700
- evidence.service = {
2701
- lifecycle: stopped.lifecycle,
2702
- processReady: stopped.process?.ready,
2703
- };
2704
- if (stopped.process?.ready !== false) {
2705
2699
  return {
2706
2700
  ok: true,
2707
- observationId: `archive_${canonicalHostWorkerDigest(evidence)}`,
2701
+ observationId: `archive_${canonicalHostWorkerDigest({ gates: "not_confirmed" })}`,
2708
2702
  confirmedSteps,
2709
2703
  };
2710
2704
  }
2705
+ evidence.gates = gates.gates;
2711
2706
  confirmedSteps.push("FENCE_ROUTE_OUTPUT");
2712
2707
  let drain;
2713
2708
  try {
@@ -277,7 +277,7 @@ function validateDesired(desired) {
277
277
  desired.serviceCredentialGeneration < 1 ||
278
278
  desired.hermesCli !== "hermes" ||
279
279
  desired.hermesVersion !== "0.18.0" ||
280
- desired.installerPackage !== "@sellable/install@0.1.593-wip.runtime-gates.20260811041542" ||
280
+ desired.installerPackage !== "@sellable/install@0.1.593" ||
281
281
  desired.mcpPackage !== "@sellable/mcp@0.1.860" ||
282
282
  !Array.isArray(desired.toolInclude) ||
283
283
  desired.toolInclude.length === 0 ||
@@ -25,8 +25,11 @@ import { fileURLToPath } from "node:url";
25
25
  import { deriveContainedProfileId } from "./profile-materializer.mjs";
26
26
 
27
27
  export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
28
- export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.593-wip.runtime-gates.20260811041542";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.593";
29
29
  export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.860";
30
+ const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
31
+ PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
32
+ );
30
33
 
31
34
  export function deriveAgentProfileId(workspaceId, agentId, hostId) {
32
35
  return deriveContainedProfileId(workspaceId, agentId, hostId);
@@ -611,7 +614,7 @@ function inspectProvisionedProfile({
611
614
  !matchesReadback(manifest, request) ||
612
615
  manifest.serviceCredentialReference !== serviceCredentialReference ||
613
616
  manifest.installerPackage !== PINNED_INSTALL_PACKAGE ||
614
- manifest.installerVersion !== "0.1.593-wip.runtime-gates.20260811041542" ||
617
+ manifest.installerVersion !== PINNED_INSTALL_VERSION ||
615
618
  manifest.mcpPackage !== PINNED_MCP_PACKAGE ||
616
619
  manifest.credentialKeyVersion !== credentialKeyVersion ||
617
620
  !/^[a-f0-9]{16}$/.test(manifest.credentialFingerprint) ||
@@ -825,7 +828,7 @@ function successReceipt({
825
828
  subject: `agent-profile:${observed.profileId}`,
826
829
  cli: {
827
830
  installPackage: PINNED_INSTALL_PACKAGE,
828
- installVersion: "0.1.593-wip.runtime-gates.20260811041542",
831
+ installVersion: PINNED_INSTALL_VERSION,
829
832
  mcpPackage: PINNED_MCP_PACKAGE,
830
833
  command: "hermes profile bootstrap",
831
834
  },
@@ -873,7 +876,7 @@ function failure(code, stages, extra = {}, request = null) {
873
876
  : "agent-profile:unbound",
874
877
  cli: {
875
878
  installPackage: PINNED_INSTALL_PACKAGE,
876
- installVersion: "0.1.593-wip.runtime-gates.20260811041542",
879
+ installVersion: PINNED_INSTALL_VERSION,
877
880
  mcpPackage: PINNED_MCP_PACKAGE,
878
881
  command: "hermes profile bootstrap",
879
882
  },
@@ -276,6 +276,7 @@ function runtimeEnvironment(contract) {
276
276
  SLACK_ALLOWED_USERS: contract.memberAllowlist.join(","),
277
277
  SLACK_REQUIRE_MENTION: "true",
278
278
  SELLABLE_AGENT_RUNTIME: "1",
279
+ SELLABLE_AGENT_RUNTIME_GATE_FILE: "/runtime/gates.json",
279
280
  SELLABLE_AGENT_RUNTIME_UID: String(contract.runtime.uid),
280
281
  SELLABLE_AGENT_RUNTIME_GID: String(contract.runtime.gid),
281
282
  SELLABLE_AGENT_HERMES_EXECUTABLE: contract.paths.hermesExecutablePath,
@@ -54,6 +54,8 @@ const ACTIONS = new Set([
54
54
  "INSTALL_MCP_CREDENTIAL",
55
55
  "INSTALL_PROVIDER_CREDENTIAL",
56
56
  "INSTALL_RUNTIME_SERVICE_ENV",
57
+ "SET_RUNTIME_GATES",
58
+ "OBSERVE_RUNTIME_GATES",
57
59
  "OBSERVE_RUNTIME_LIFECYCLE",
58
60
  "CLEAN_PROFILE_SECRETS",
59
61
  "CHOWN_PROFILE",
@@ -101,6 +103,20 @@ const ACTION_KEYS = Object.freeze({
101
103
  "revisionId",
102
104
  "profileDigest",
103
105
  ],
106
+ SET_RUNTIME_GATES: [
107
+ "action",
108
+ "profileId",
109
+ "operationId",
110
+ "fence",
111
+ "lifecycle",
112
+ ],
113
+ OBSERVE_RUNTIME_GATES: [
114
+ "action",
115
+ "profileId",
116
+ "operationId",
117
+ "fence",
118
+ "lifecycle",
119
+ ],
104
120
  OBSERVE_RUNTIME_LIFECYCLE: [
105
121
  "action",
106
122
  "profileId",
@@ -765,6 +781,7 @@ function runtimeServiceEnvironmentValues({
765
781
  SELLABLE_AGENT_POLICY_HASH: manifest.policyHash,
766
782
  SELLABLE_AGENT_PROFILE_ID: profileId,
767
783
  SELLABLE_AGENT_RUNTIME_ENV_LOADED: "1",
784
+ SELLABLE_AGENT_RUNTIME_GATE_FILE: "/runtime/gates.json",
768
785
  SELLABLE_AGENT_RUNTIME_SECRET_ROOT: mcpRoot,
769
786
  SELLABLE_AGENT_SELECTED_CHANNEL_IDS: JSON.stringify(
770
787
  manifest.slackChannelIds
@@ -787,6 +804,56 @@ function runtimeServiceEnvironmentValues({
787
804
  };
788
805
  }
789
806
 
807
+ export function expectedRuntimeGates(lifecycle) {
808
+ if (lifecycle === "ON")
809
+ return { lifecycle, dispatch: "ENABLED", route: "OPEN", output: "OPEN" };
810
+ if (lifecycle === "OFF")
811
+ return { lifecycle, dispatch: "FENCED", route: "OPEN", output: "OPEN" };
812
+ if (["ARCHIVING", "ARCHIVED"].includes(lifecycle)) {
813
+ return {
814
+ lifecycle: "ARCHIVED",
815
+ dispatch: "FENCED",
816
+ route: "FENCED",
817
+ output: "FENCED",
818
+ };
819
+ }
820
+ throw new Error("runtime lifecycle rejected");
821
+ }
822
+
823
+ function writeRuntimeGates(profile, lifecycle) {
824
+ const root = safeRuntimeStateRoot(profile);
825
+ const gates = expectedRuntimeGates(lifecycle);
826
+ const pathValue = join(root, "gates.json");
827
+ atomicCredential(
828
+ pathValue,
829
+ JSON.stringify(gates),
830
+ profile.runtimeUid,
831
+ profile.runtimeGid
832
+ );
833
+ const parsed = JSON.parse(readFileSync(pathValue, "utf8"));
834
+ if (stableJson(parsed) !== stableJson(gates))
835
+ throw new Error("runtime gates readback rejected");
836
+ return { path: pathValue, gates };
837
+ }
838
+
839
+ function observeRuntimeGates(profile, lifecycle) {
840
+ const root = safeRuntimeStateRoot(profile);
841
+ const pathValue = join(root, "gates.json");
842
+ const link = lstatSync(pathValue);
843
+ const parsed = JSON.parse(readFileSync(pathValue, "utf8"));
844
+ const expected = expectedRuntimeGates(lifecycle);
845
+ if (
846
+ link.isSymbolicLink() ||
847
+ !link.isFile() ||
848
+ link.uid !== profile.runtimeUid ||
849
+ link.gid !== profile.runtimeGid ||
850
+ (link.mode & 0o777) !== 0o600 ||
851
+ stableJson(parsed) !== stableJson(expected)
852
+ )
853
+ throw new Error("runtime gates observation rejected");
854
+ return { path: pathValue, gates: parsed };
855
+ }
856
+
790
857
  function safePinnedFile(pathValue, label) {
791
858
  const resolved = resolve(pathValue ?? "");
792
859
  if (!isAbsolute(resolved) || !existsSync(resolved))
@@ -1358,6 +1425,7 @@ function installRuntimeServiceEnvironment(profile, request) {
1358
1425
  if (!existsSync(target)) throw new Error("active profile absent");
1359
1426
  const manifest = validateActiveManifest(target, request, profile);
1360
1427
  const runtimeStateRoot = safeRuntimeStateRoot(profile);
1428
+ writeRuntimeGates(profile, "ON");
1361
1429
  const { paths, values } = runtimeServiceEnvironmentValues({
1362
1430
  profile,
1363
1431
  profileId: request.profileId,
@@ -1912,6 +1980,15 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
1912
1980
  if (request.action === "INSTALL_RUNTIME_SERVICE_ENV") {
1913
1981
  return installRuntimeServiceEnvironment(profile, request);
1914
1982
  }
1983
+ if (request.action === "SET_RUNTIME_GATES") {
1984
+ const written = writeRuntimeGates(profile, request.lifecycle);
1985
+ return {
1986
+ ok: true,
1987
+ profileId: request.profileId,
1988
+ lifecycle: request.lifecycle,
1989
+ ...written,
1990
+ };
1991
+ }
1915
1992
  if (request.action === "CLEAN_PROFILE_SECRETS") {
1916
1993
  for (const configured of [
1917
1994
  profile.workerRuntimeSecretRoot,
@@ -2001,7 +2078,12 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
2001
2078
  if (["OBSERVE_RUNTIME", "OBSERVE_RUNTIME_DRIFT"].includes(request.action)) {
2002
2079
  return { ok: true, observation: await observeRuntime(profile, request) };
2003
2080
  }
2081
+ if (request.action === "OBSERVE_RUNTIME_GATES") {
2082
+ const observed = observeRuntimeGates(profile, request.lifecycle);
2083
+ return { ok: true, profileId: request.profileId, ...observed };
2084
+ }
2004
2085
  if (request.action === "OBSERVE_RUNTIME_LIFECYCLE") {
2086
+ const gateObservation = observeRuntimeGates(profile, request.lifecycle);
2005
2087
  const { target } = profileTarget(
2006
2088
  profile,
2007
2089
  { ...request, slot: "ACTIVE" },
@@ -2034,6 +2116,7 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
2034
2116
  revisionId: observation.revisionId,
2035
2117
  credentialGeneration: observation.serviceCredential.generation,
2036
2118
  profileDigest: observation.profileDigest,
2119
+ gates: gateObservation.gates,
2037
2120
  secretsPresent,
2038
2121
  process: observation.process,
2039
2122
  runtimeObservation: observation,
@@ -2066,6 +2149,7 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
2066
2149
  revisionId: manifest.revisionId,
2067
2150
  credentialGeneration: manifest.serviceCredentialGeneration,
2068
2151
  profileDigest: manifest.profileDigest,
2152
+ gates: gateObservation.gates,
2069
2153
  secretsPresent,
2070
2154
  process: {
2071
2155
  ready: false,
@@ -502,12 +502,29 @@ function finalizeSuccess(state, operation, status) {
502
502
  }
503
503
 
504
504
  function observationMatches(observation, operation, lifecycle) {
505
+ const expectedGates =
506
+ lifecycle === "ON"
507
+ ? { lifecycle: "ON", dispatch: "ENABLED", route: "OPEN", output: "OPEN" }
508
+ : lifecycle === "OFF"
509
+ ? {
510
+ lifecycle: "OFF",
511
+ dispatch: "FENCED",
512
+ route: "OPEN",
513
+ output: "OPEN",
514
+ }
515
+ : {
516
+ lifecycle: "ARCHIVED",
517
+ dispatch: "FENCED",
518
+ route: "FENCED",
519
+ output: "FENCED",
520
+ };
505
521
  return Boolean(
506
522
  observation &&
507
523
  observation.lifecycle === lifecycle &&
508
524
  observation.revisionId === operation.targetRevisionId &&
509
525
  observation.credentialGeneration === operation.credentialGeneration &&
510
526
  observation.profileDigest === operation.profileDigest &&
527
+ stableJson(observation.gates) === stableJson(expectedGates) &&
511
528
  observation.secretsPresent === (lifecycle !== "ARCHIVED") &&
512
529
  observation.process &&
513
530
  observation.process.ready === (lifecycle === "ON") &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.593-wip.runtime-gates.20260811041542",
3
+ "version": "0.1.593",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {