@sellable/install 0.1.836-wip.disqualify.20261012.1 → 0.1.838

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,13 +3,13 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.836-wip.disqualify.20261012.1
7
- ARG MCP_PACKAGE=@sellable/mcp@0.1.981-wip.disqualify.20261012.1
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.838
7
+ ARG MCP_PACKAGE=@sellable/mcp@0.1.982-wip.durableprep.20261013.1
8
8
  ARG INSTALLER_INTEGRITY
9
- ARG MCP_INTEGRITY=sha512-v7K5BYVfD87blq2WTPk8OQukmHOFUIyCjJj7BbQKHu+r326CvcFqpRbgWq1k9X2rfZAYopEKPLTAzBu0rjbjHw==
9
+ ARG MCP_INTEGRITY=sha512-8WZ9cVdpKp1vbQY0CWFptXZa4MMiK1VOuGPV7L0O95qYPkYebfr6JIufS5C5sVtSxsSGpBf/v82x04eIG7wufw==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.836-wip.disqualify.20261012.1" \
12
- && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.981-wip.disqualify.20261012.1" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.838" \
12
+ && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.982-wip.durableprep.20261013.1" \
13
13
  && test -n "${INSTALLER_INTEGRITY}" \
14
14
  && test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
15
15
  && test "$(npm view "${MCP_PACKAGE}" dist.integrity)" = "${MCP_INTEGRITY}" \
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.836-wip.disqualify.2026
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.836-wip.disqualify.20261012.1' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.981-wip.disqualify.20261012.1') 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.838' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.982-wip.durableprep.20261013.1') 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.836-wip.disqualify.20261012.1', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.981-wip.disqualify.20261012.1', 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.838', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.982-wip.durableprep.20261013.1', 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,7 +3,7 @@
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.836-wip.disqualify.20261012.1` is
6
+ `@sellable/install@0.1.838` is
7
7
  published:
8
8
 
9
9
  ```sh
@@ -28,7 +28,7 @@ import {
28
28
 
29
29
  const MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
30
30
  const MCP_PACKAGE = "@sellable/mcp";
31
- const MCP_VERSION = "0.1.981-wip.disqualify.20261012.1";
31
+ const MCP_VERSION = "0.1.982-wip.durableprep.20261013.1";
32
32
  const SAFE_COMPONENT = /^[A-Za-z0-9@._+-]+$/;
33
33
  const HERMES_EXCLUDES = new Set([".git", ".playwright", "node_modules"]);
34
34
 
@@ -169,7 +169,7 @@ COPY fly-admin-image/start-admin-runtime /usr/local/lib/sellable-agent/admin-run
169
169
  COPY fly-customer-image/customer-runtime.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime.mjs
170
170
  COPY hermes-first-welcome.mjs /usr/local/lib/sellable-agent/hermes-first-welcome.mjs
171
171
  COPY fly-customer-image/hermes-runs-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs
172
- COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/claude-code-runs-api.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs fly-customer-image/public-endpoint-guard.mjs fly-customer-image/workspace-connection-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/
172
+ COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/claude-code-runs-api.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs fly-customer-image/public-endpoint-guard.mjs fly-customer-image/workspace-connection-client.mjs fly-customer-image/slack-stand-in.mjs /usr/local/lib/sellable-agent/fly-customer-image/
173
173
  COPY fly-customer-image/opener-stage-execution.mjs /usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs
174
174
  COPY fly-customer-image/endpoint-uat-completion-gate.mjs /usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs
175
175
  COPY fly-customer-image/customer-runtime-phase-telemetry.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs
@@ -6,13 +6,14 @@ USER root
6
6
 
7
7
  ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
8
8
 
9
- ARG MCP_PACKAGE=@sellable/mcp@0.1.981-wip.disqualify.20261012.1
10
- ARG MCP_INTEGRITY=sha512-v7K5BYVfD87blq2WTPk8OQukmHOFUIyCjJj7BbQKHu+r326CvcFqpRbgWq1k9X2rfZAYopEKPLTAzBu0rjbjHw==
11
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.836-wip.disqualify.20261012.1
9
+ ARG MCP_PACKAGE=@sellable/mcp@0.1.982-wip.durableprep.20261013.1
10
+ ARG MCP_INTEGRITY=sha512-8WZ9cVdpKp1vbQY0CWFptXZa4MMiK1VOuGPV7L0O95qYPkYebfr6JIufS5C5sVtSxsSGpBf/v82x04eIG7wufw==
11
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.838
12
12
  ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.202
13
13
  ARG ADMIN_MCP_INTEGRITY=sha512-ym3aye/0vQ8tfULJlKz9JFOxvIQsl1G+n3np+TTgK/tecwNblBo/j3AkTnRqH9iomKzPbZzP+n4tEQsvsU8ByA==
14
14
  ARG TUS_JS_CLIENT_VERSION=4.3.1
15
15
  ARG YAML_VERSION=2.9.0
16
+ ARG WS_VERSION=8.18.3
16
17
  ARG HERMES_VERSION=0.21.5
17
18
  ARG SLACK_SDK_VERSION=3.43.0
18
19
  ARG AGENT_BROWSER_VERSION=0.26.0
@@ -41,9 +42,11 @@ RUN --mount=type=secret,id=mcp_trust_root,required=true \
41
42
  --omit=dev --ignore-scripts --no-audit --no-fund \
42
43
  "agent-browser@${AGENT_BROWSER_VERSION}" \
43
44
  "tus-js-client@${TUS_JS_CLIENT_VERSION}" "yaml@${YAML_VERSION}" \
45
+ "ws@${WS_VERSION}" \
44
46
  && test "$(node -p "require('/usr/local/lib/sellable-agent/node_modules/agent-browser/package.json').version")" = "${AGENT_BROWSER_VERSION}" \
45
47
  && test "$(node -p "require('/usr/local/lib/sellable-agent/node_modules/tus-js-client/package.json').version")" = "${TUS_JS_CLIENT_VERSION}" \
46
48
  && test "$(node -p "require('/usr/local/lib/sellable-agent/node_modules/yaml/package.json').version")" = "${YAML_VERSION}" \
49
+ && test "$(node -p "require('/usr/local/lib/sellable-agent/node_modules/ws/package.json').version")" = "${WS_VERSION}" \
47
50
  && mkdir -p /usr/local/lib/sellable-agent/external-runtime/mcp/bin \
48
51
  && ln -s ../node_modules/.bin/sellable-mcp \
49
52
  /usr/local/lib/sellable-agent/external-runtime/mcp/bin/sellable-mcp \
@@ -98,7 +101,7 @@ COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
98
101
  COPY hermes-endpoint-cron.mjs /opt/sellable/bin/hermes-endpoint-cron
99
102
  COPY fly-customer-image/model-auth-store.mjs /usr/local/lib/sellable-agent/fly-customer-image/model-auth-store.mjs
100
103
  COPY fly-customer-image/hermes-runs-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs
101
- COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs fly-customer-image/public-endpoint-guard.mjs fly-customer-image/workspace-connection-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/
104
+ COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs fly-customer-image/public-endpoint-guard.mjs fly-customer-image/workspace-connection-client.mjs fly-customer-image/slack-stand-in.mjs /usr/local/lib/sellable-agent/fly-customer-image/
102
105
  COPY fly-customer-image/opener-stage-execution.mjs /usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs
103
106
  COPY fly-customer-image/endpoint-uat-completion-gate.mjs /usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs
104
107
  COPY fly-customer-image/customer-runtime-phase-telemetry.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "node:child_process";
4
- import { createHash, randomUUID } from "node:crypto";
4
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
5
5
  import {
6
6
  chmodSync,
7
7
  closeSync,
@@ -109,6 +109,7 @@ import {
109
109
  usesOpenerDraftStrategy,
110
110
  } from "./opener-stage-execution.mjs";
111
111
  import { createConfiguredRunsClient } from "./runs-api-connections.mjs";
112
+ import { startSlackStandIn } from "./slack-stand-in.mjs";
112
113
 
113
114
  const DATA_ROOT = "/opt/data";
114
115
  const PROFILES_ROOT = join(DATA_ROOT, "profiles");
@@ -132,9 +133,10 @@ const RUNTIME_GENERATION = Number(
132
133
  const BOOT_SESSION_ID = randomUUID();
133
134
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.5";
134
135
  const INSTALLER_PACKAGE =
135
- process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.836-wip.disqualify.20261012.1";
136
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.838";
136
137
  const MCP_PACKAGE =
137
- process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.981-wip.disqualify.20261012.1";
138
+ process.env.SELLABLE_AGENT_MCP_PACKAGE ??
139
+ "@sellable/mcp@0.1.982-wip.durableprep.20261013.1";
138
140
  export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
139
141
  const DEFAULT_PROFILE_BUNDLE_ROOT =
140
142
  "/usr/local/share/sellable-agent/default-profile-bundle";
@@ -422,7 +424,7 @@ function exactArtifact(claim, type) {
422
424
  .join("\n");
423
425
  }
424
426
 
425
- function compileDesired(claim, trustRootPem) {
427
+ export function compileDesired(claim, trustRootPem) {
426
428
  const operation = claim?.operation;
427
429
  const slack = claim?.runtimeContract?.slack;
428
430
  const service = claim?.runtimeContract?.service;
@@ -432,6 +434,7 @@ function compileDesired(claim, trustRootPem) {
432
434
  const customerProgram = claim?.runtimeContract?.customerProgram;
433
435
  const selectedBindings = slack?.selectedBindings;
434
436
  const toolInclude = service?.toolInclude;
437
+ const slackTransport = slack?.transport === "shared" ? "shared" : "dedicated";
435
438
  if (
436
439
  !operation ||
437
440
  operation.targetRevisionId !== claim?.revision?.id ||
@@ -455,7 +458,10 @@ function compileDesired(claim, trustRootPem) {
455
458
  selectedBindings.length < 1 ||
456
459
  !Array.isArray(slack?.memberAllowlist) ||
457
460
  !claim?.secretEnvelopes?.mcpService ||
458
- !claim?.secretEnvelopes?.slackApp ||
461
+ (slackTransport === "shared"
462
+ ? claim.secretEnvelopes.slackApp !== null ||
463
+ !/^A[A-Z0-9]{8,31}$/.test(slack.appId ?? "")
464
+ : !claim.secretEnvelopes.slackApp) ||
459
465
  !claim?.secretEnvelopes?.slackBot ||
460
466
  !claim?.secretEnvelopes?.modelCodex ||
461
467
  !claim?.secretEnvelopes?.hermesApiServer
@@ -564,6 +570,8 @@ function compileDesired(claim, trustRootPem) {
564
570
  soul: exactArtifact(claim, "SOUL"),
565
571
  skill: exactArtifact(claim, "SKILL"),
566
572
  }),
573
+ slackTransport,
574
+ slackAppId: slack.appId,
567
575
  });
568
576
  }
569
577
 
@@ -2834,7 +2842,92 @@ export function createCustomerEndpointRelay(options = {}) {
2834
2842
  });
2835
2843
  }
2836
2844
 
2837
- async function consumeSecrets({ claim, exchange, client, desired }) {
2845
+ /** @param {any} options */
2846
+ export function createCustomerSlackRelay(options = {}) {
2847
+ const {
2848
+ getControlClient,
2849
+ getActiveRuntime,
2850
+ sleepImpl = sleep,
2851
+ idleMs = 1_000,
2852
+ maxBatch = 10,
2853
+ diagnostic = () => {},
2854
+ } = options;
2855
+ if (
2856
+ typeof getControlClient !== "function" ||
2857
+ typeof getActiveRuntime !== "function" ||
2858
+ typeof sleepImpl !== "function" ||
2859
+ typeof diagnostic !== "function"
2860
+ ) {
2861
+ throw new Error("customer_slack_relay_configuration_rejected");
2862
+ }
2863
+ return Object.freeze({
2864
+ async run(signal) {
2865
+ // Outcomes are confirmed on the next claim; the server leases a batch
2866
+ // for 30 s and holds the agent's next batch until it is confirmed.
2867
+ let pending = [];
2868
+ let failures = 0;
2869
+ while (!signal.aborted) {
2870
+ try {
2871
+ const standIn = getActiveRuntime()?.slackStandIn;
2872
+ const ready = standIn?.status().warm === true;
2873
+ if (!ready && pending.length === 0) {
2874
+ await abortablePause(idleMs, signal, sleepImpl);
2875
+ continue;
2876
+ }
2877
+ const results = pending.slice(0, 20);
2878
+ const { envelopes } = await getControlClient().claimSlackEnvelopes({
2879
+ max: ready ? maxBatch : 0,
2880
+ results,
2881
+ });
2882
+ pending = pending.slice(results.length);
2883
+ failures = 0;
2884
+ for (const [index, envelope] of envelopes.entries()) {
2885
+ // A malformed envelope can never be delivered: close it as
2886
+ // no_ack so it does not come back at the head of every batch.
2887
+ const outcome = await standIn.deliver(envelope).catch((error) => {
2888
+ if (error?.message !== "slack_envelope_malformed") throw error;
2889
+ diagnostic({ ok: false, code: "slack_envelope_malformed" });
2890
+ return "no_ack";
2891
+ });
2892
+ if (outcome === "released") {
2893
+ // Hermes never got this one: hand it and the rest back.
2894
+ for (const rest of envelopes.slice(index)) {
2895
+ pending.push({ id: rest.id, outcome: "released" });
2896
+ }
2897
+ break;
2898
+ }
2899
+ pending.push({ id: envelope.id, outcome });
2900
+ }
2901
+ if (envelopes.length === 0 && pending.length === 0) {
2902
+ await abortablePause(idleMs, signal, sleepImpl);
2903
+ }
2904
+ } catch (error) {
2905
+ failures = Math.min(failures + 1, 5);
2906
+ diagnostic({
2907
+ ok: false,
2908
+ code:
2909
+ error instanceof Error
2910
+ ? redactRuntimeDiagnostic(error.message)
2911
+ : "customer_slack_relay_failed",
2912
+ });
2913
+ await abortablePause(
2914
+ Math.min(5_000, 500 * 2 ** (failures - 1)),
2915
+ signal,
2916
+ sleepImpl
2917
+ );
2918
+ }
2919
+ }
2920
+ },
2921
+ });
2922
+ }
2923
+
2924
+ async function consumeSecrets({
2925
+ claim,
2926
+ exchange,
2927
+ client,
2928
+ desired,
2929
+ slackStandIn,
2930
+ }) {
2838
2931
  const identity = operationIdentity(claim, exchange);
2839
2932
  const consume = async (descriptor) => {
2840
2933
  const value = await client.consumeSecret({
@@ -2856,7 +2949,11 @@ async function consumeSecrets({ claim, exchange, client, desired }) {
2856
2949
  const [mcpService, slackApp, slackBot, modelCodex, hermesApiServer] =
2857
2950
  await Promise.all([
2858
2951
  consume(claim.secretEnvelopes.mcpService),
2859
- consume(claim.secretEnvelopes.slackApp),
2952
+ // Shared: the server sends no app token; the per-boot local token the
2953
+ // stand-in accepts is installed on fd 8 instead.
2954
+ slackStandIn
2955
+ ? slackStandIn.localAppToken
2956
+ : consume(claim.secretEnvelopes.slackApp),
2860
2957
  consume(claim.secretEnvelopes.slackBot),
2861
2958
  consume(claim.secretEnvelopes.modelCodex),
2862
2959
  consume(claim.secretEnvelopes.hermesApiServer),
@@ -3236,9 +3333,21 @@ function slackApiOrigin() {
3236
3333
  return parsed.origin;
3237
3334
  }
3238
3335
 
3239
- export function applicationRuntimeEnv({ desired, runtimeConfig, secrets }) {
3336
+ /** @param {any} input */
3337
+ export function applicationRuntimeEnv({
3338
+ desired,
3339
+ runtimeConfig,
3340
+ secrets,
3341
+ slackStandIn,
3342
+ }) {
3240
3343
  const env = {
3241
3344
  ...process.env,
3345
+ ...(slackStandIn
3346
+ ? {
3347
+ SELLABLE_AGENT_SLACK_TRANSPORT: "shared-local",
3348
+ SELLABLE_AGENT_SLACK_API_ORIGIN: slackStandIn.origin,
3349
+ }
3350
+ : {}),
3242
3351
  HOME: DATA_ROOT,
3243
3352
  HERMES_HOME: DATA_ROOT,
3244
3353
  SELLABLE_HERMES_PROFILE_ID: desired.profileId,
@@ -3309,13 +3418,93 @@ async function stopNativeGateway(gateway) {
3309
3418
  }
3310
3419
  }
3311
3420
 
3312
- async function startNativeGateway({ desired, runtimeConfig, secrets }) {
3421
+ /**
3422
+ * One loopback Slack stand-in per boot, started for the first shared claim.
3423
+ * @param {any} options
3424
+ */
3425
+ export function createSlackStandInSlot(options = {}) {
3426
+ const {
3427
+ startStandIn = startSlackStandIn,
3428
+ upstreamOrigin = slackApiOrigin,
3429
+ diagnostic = () => {},
3430
+ } = options;
3431
+ let current = null;
3432
+ return {
3433
+ async ensure({ slackTransport, appId }) {
3434
+ if (slackTransport !== "shared") return null;
3435
+ if (!current) {
3436
+ // The slot is the local app token's only producer; the stand-in
3437
+ // accepts it on apps.connections.open and Hermes reads it from fd 8.
3438
+ const localAppToken = `xapp-1-${randomBytes(24).toString("hex")}`;
3439
+ const instance = await startStandIn({
3440
+ localAppToken,
3441
+ ticket: randomBytes(24).toString("hex"),
3442
+ appId,
3443
+ upstreamOrigin: upstreamOrigin(),
3444
+ diagnostic,
3445
+ });
3446
+ current = { ...instance, localAppToken };
3447
+ }
3448
+ return current;
3449
+ },
3450
+ async close() {
3451
+ const instance = current;
3452
+ current = null;
3453
+ await instance?.close();
3454
+ },
3455
+ };
3456
+ }
3457
+
3458
+ /**
3459
+ * The idle loop's crash restart of a stopped gateway.
3460
+ * @param {any} activeRuntime
3461
+ * @param {{ startGateway?: (input: any) => Promise<any> }} [options]
3462
+ */
3463
+ export async function restartStoppedNativeGateway(
3464
+ activeRuntime,
3465
+ { startGateway = startNativeGateway } = {}
3466
+ ) {
3467
+ if (
3468
+ activeRuntime?.action !== "INSTANTIATE" &&
3469
+ activeRuntime?.gateway &&
3470
+ !activeRuntime.gateway.readback().running
3471
+ ) {
3472
+ // The transport comes from activeRuntime, never from its claim: a BACKUP
3473
+ // claim replaces activeRuntime.claim and carries no runtime contract.
3474
+ return await startGateway({
3475
+ desired: activeRuntime.desired,
3476
+ runtimeConfig: activeRuntime.runtimeConfig,
3477
+ secrets: activeRuntime.secrets,
3478
+ slackTransport: activeRuntime.slackTransport,
3479
+ slackStandIn: activeRuntime.slackStandIn,
3480
+ });
3481
+ }
3482
+ return activeRuntime?.gateway;
3483
+ }
3484
+
3485
+ async function startNativeGateway({
3486
+ desired,
3487
+ runtimeConfig,
3488
+ secrets,
3489
+ slackTransport,
3490
+ slackStandIn,
3491
+ }) {
3492
+ // A shared runtime launched with the dedicated env would send the local
3493
+ // app token to Slack and go silent while still looking healthy.
3494
+ if (slackTransport === "shared" && !slackStandIn) {
3495
+ throw new Error("customer_slack_stand_in_required");
3496
+ }
3313
3497
  const secretRoot = join(SECRET_ROOT, desired.profileId);
3314
3498
  const launch = buildNativeHermesLaunch({
3315
3499
  profileRoot: runtimeConfig.runtimeHome,
3316
3500
  secretsRoot: secretRoot,
3317
3501
  profileId: desired.profileId,
3318
- env: applicationRuntimeEnv({ desired, runtimeConfig, secrets }),
3502
+ env: applicationRuntimeEnv({
3503
+ desired,
3504
+ runtimeConfig,
3505
+ secrets,
3506
+ slackStandIn,
3507
+ }),
3319
3508
  });
3320
3509
  const appDescriptor = openSync(
3321
3510
  launch.inheritedFileDescriptors.find((item) => item.fd === 8).path,
@@ -3571,17 +3760,26 @@ async function proveApiServerReadiness({
3571
3760
  throw new Error("customer_api_server_readiness_rejected");
3572
3761
  }
3573
3762
 
3763
+ /** @param {any} input */
3574
3764
  export async function startProvenNativeGateway({
3575
3765
  desired,
3576
3766
  runtimeConfig,
3577
3767
  secrets,
3768
+ slackTransport,
3769
+ slackStandIn,
3578
3770
  startGateway = startNativeGateway,
3579
3771
  proveReadiness = proveApiServerReadiness,
3580
3772
  stopGateway = stopNativeGateway,
3581
3773
  }) {
3582
3774
  let gateway = null;
3583
3775
  try {
3584
- gateway = await startGateway({ desired, runtimeConfig, secrets });
3776
+ gateway = await startGateway({
3777
+ desired,
3778
+ runtimeConfig,
3779
+ secrets,
3780
+ slackTransport,
3781
+ slackStandIn,
3782
+ });
3585
3783
  const gatewayReadback = gateway.readback();
3586
3784
  if (!gatewayReadback.running) {
3587
3785
  throw new Error("customer_native_gateway_readback_rejected");
@@ -3618,8 +3816,14 @@ async function proveProvisionedApplication({
3618
3816
  runtimeConfig,
3619
3817
  secrets,
3620
3818
  profileDigest,
3819
+ slackStandIn,
3621
3820
  }) {
3622
- const runtimeEnv = applicationRuntimeEnv({ desired, runtimeConfig, secrets });
3821
+ const runtimeEnv = applicationRuntimeEnv({
3822
+ desired,
3823
+ runtimeConfig,
3824
+ secrets,
3825
+ slackStandIn,
3826
+ });
3623
3827
  const materializedModelAuth = runtimeConfig.modelAuthState.materialized;
3624
3828
  const [hermesVersion, mcpVersion, hermesModelAuth] = await Promise.all([
3625
3829
  run(HERMES, ["--version"], { env: runtimeEnv, timeoutMs: 180_000 }),
@@ -3746,14 +3950,22 @@ async function proveApplication({
3746
3950
  secrets,
3747
3951
  profileDigest,
3748
3952
  runtimeAuthorityScope,
3953
+ slackTransport,
3954
+ slackStandIn,
3749
3955
  }) {
3750
3956
  const provisioned = await proveProvisionedApplication({
3751
3957
  desired,
3752
3958
  runtimeConfig,
3753
3959
  secrets,
3754
3960
  profileDigest,
3961
+ slackStandIn,
3962
+ });
3963
+ const runtimeEnv = applicationRuntimeEnv({
3964
+ desired,
3965
+ runtimeConfig,
3966
+ secrets,
3967
+ slackStandIn,
3755
3968
  });
3756
- const runtimeEnv = applicationRuntimeEnv({ desired, runtimeConfig, secrets });
3757
3969
  const doctor = customerRuntimeDoctorRequired(action, {
3758
3970
  runtimeAuthorityScope,
3759
3971
  directReadinessProven: true,
@@ -3789,8 +4001,11 @@ async function proveApplication({
3789
4001
  ) {
3790
4002
  throw new Error("customer_slack_identity_rejected");
3791
4003
  }
4004
+ // Shared machines hold no Slack app token: Socket Mode is proven against
4005
+ // the loopback stand-in only, never against Slack.
4006
+ const shared = slackTransport === "shared";
3792
4007
  const socketResponse = await fetch(
3793
- `${slackApiOrigin()}/api/apps.connections.open`,
4008
+ `${shared ? slackStandIn.origin : slackApiOrigin()}/api/apps.connections.open`,
3794
4009
  {
3795
4010
  method: "POST",
3796
4011
  headers: {
@@ -3806,7 +4021,7 @@ async function proveApplication({
3806
4021
  !socketResponse.ok ||
3807
4022
  socket?.ok !== true ||
3808
4023
  typeof socket.url !== "string" ||
3809
- !/^wss?:\/\//.test(socket.url)
4024
+ !(shared ? /^ws:\/\/127\.0\.0\.1:/ : /^wss?:\/\//).test(socket.url)
3810
4025
  ) {
3811
4026
  throw new Error("customer_slack_socket_identity_rejected");
3812
4027
  }
@@ -3815,7 +4030,20 @@ async function proveApplication({
3815
4030
  desired,
3816
4031
  runtimeConfig,
3817
4032
  secrets,
4033
+ slackTransport,
4034
+ slackStandIn,
3818
4035
  });
4036
+ if (shared) {
4037
+ let connected = false;
4038
+ for (let attempt = 0; attempt < 240 && !connected; attempt += 1) {
4039
+ connected = slackStandIn.status().hermesConnected;
4040
+ if (!connected) await sleep(500);
4041
+ }
4042
+ if (!connected) {
4043
+ await stopNativeGateway(gateway);
4044
+ throw new Error("customer_slack_stand_in_not_connected");
4045
+ }
4046
+ }
3819
4047
  return {
3820
4048
  ...provisioned,
3821
4049
  hermesOutput: `${provisioned.hermesOutput}\n${doctor.stdout}`,
@@ -3994,6 +4222,7 @@ async function processOperation({
3994
4222
  client,
3995
4223
  identity,
3996
4224
  reattestAuthority,
4225
+ slackStandInSlot,
3997
4226
  }) {
3998
4227
  const phaseTelemetry = createCustomerRuntimePhaseTelemetry({
3999
4228
  bootSessionId: BOOT_SESSION_ID,
@@ -4001,15 +4230,25 @@ async function processOperation({
4001
4230
  });
4002
4231
  const runtimeAuthorityScope = claim.runtimeAuthority?.scope ?? null;
4003
4232
  const trustRootPem = readFileSync(TRUST_ROOT, "utf8");
4004
- const { desired, toolInclude } = compileDesired(claim, trustRootPem);
4233
+ const { desired, toolInclude, slackTransport, slackAppId } = compileDesired(
4234
+ claim,
4235
+ trustRootPem
4236
+ );
4005
4237
  mkdirSync(PROFILES_ROOT, { recursive: true, mode: 0o700 });
4006
4238
  chmodSync(PROFILES_ROOT, 0o700);
4239
+ // Shared only: the stand-in listens before consumeSecrets (it produces the
4240
+ // local app token) and before every Hermes run that reads the seam.
4241
+ const slackStandIn = await slackStandInSlot.ensure({
4242
+ slackTransport,
4243
+ appId: slackAppId,
4244
+ });
4007
4245
  const secrets = await phaseTelemetry.run("runtime_secrets", async () => {
4008
4246
  const consumed = await consumeSecrets({
4009
4247
  claim,
4010
4248
  exchange,
4011
4249
  client,
4012
4250
  desired,
4251
+ slackStandIn,
4013
4252
  });
4014
4253
  await publishManagedAuth({
4015
4254
  schemaVersion: "sellable-managed-auth/v1",
@@ -4175,6 +4414,8 @@ async function processOperation({
4175
4414
  secrets,
4176
4415
  profileDigest: pristineProfile.manifest.profileDigest,
4177
4416
  runtimeAuthorityScope,
4417
+ slackTransport,
4418
+ slackStandIn,
4178
4419
  })
4179
4420
  );
4180
4421
  await phaseTelemetry.run("customer_program_crons", async () => {
@@ -4254,6 +4495,8 @@ async function processOperation({
4254
4495
  gateway: proof.gateway ?? null,
4255
4496
  proof,
4256
4497
  modelHealthReporter: null,
4498
+ slackTransport,
4499
+ slackStandIn,
4257
4500
  };
4258
4501
  if (claim.runtimeAuthority && receipt.observedLifecycle === "ON") {
4259
4502
  const servingObservation = customerServingObservation(
@@ -4431,6 +4674,14 @@ async function main() {
4431
4674
  diagnostic: (value) => process.stderr.write(`${JSON.stringify(value)}\n`),
4432
4675
  });
4433
4676
  const endpointLoop = endpointRelay.run(controller.signal);
4677
+ const diagnostic = (value) =>
4678
+ process.stderr.write(`${JSON.stringify(value)}\n`);
4679
+ const slackStandInSlot = createSlackStandInSlot({ diagnostic });
4680
+ const slackLoop = createCustomerSlackRelay({
4681
+ getControlClient: () => client,
4682
+ getActiveRuntime: () => activeRuntime,
4683
+ diagnostic,
4684
+ }).run(controller.signal);
4434
4685
  const beginShutdownSnapshot = () => {
4435
4686
  if (!activeRuntime || shutdownMemorySnapshot) return;
4436
4687
  const runtime = activeRuntime;
@@ -4537,16 +4788,9 @@ async function main() {
4537
4788
  ),
4538
4789
  });
4539
4790
  }
4540
- if (
4541
- activeRuntime?.action !== "INSTANTIATE" &&
4542
- activeRuntime?.gateway &&
4543
- !activeRuntime.gateway.readback().running
4544
- ) {
4545
- activeRuntime.gateway = await startNativeGateway({
4546
- desired: activeRuntime.desired,
4547
- runtimeConfig: activeRuntime.runtimeConfig,
4548
- secrets: activeRuntime.secrets,
4549
- });
4791
+ if (activeRuntime) {
4792
+ activeRuntime.gateway =
4793
+ await restartStoppedNativeGateway(activeRuntime);
4550
4794
  }
4551
4795
  await sleep(1_000);
4552
4796
  continue;
@@ -4620,6 +4864,7 @@ async function main() {
4620
4864
  client,
4621
4865
  identity,
4622
4866
  reattestAuthority: () => operationHeartbeat.reattest(),
4867
+ slackStandInSlot,
4623
4868
  });
4624
4869
  soulClaimTracker = createSoulClaimObservationTracker({
4625
4870
  runtimeId: RUNTIME_ID,
@@ -4650,7 +4895,7 @@ async function main() {
4650
4895
  }
4651
4896
  beginShutdownSnapshot();
4652
4897
  const finalMemorySnapshot = await shutdownMemorySnapshot;
4653
- await endpointLoop;
4898
+ await Promise.all([endpointLoop, slackLoop]);
4654
4899
  if (finalMemorySnapshot?.ok === false) {
4655
4900
  process.stderr.write(
4656
4901
  `${JSON.stringify({
@@ -4660,6 +4905,7 @@ async function main() {
4660
4905
  );
4661
4906
  }
4662
4907
  await stopNativeGateway(activeRuntime?.gateway);
4908
+ await slackStandInSlot.close();
4663
4909
  await activeRuntime?.modelHealthReporter?.stop();
4664
4910
  if (activeRuntime?.desired.profileId) {
4665
4911
  await clearManagedAuth(activeRuntime.desired.profileId);
@@ -0,0 +1,345 @@
1
+ // The machine-local Slack Socket Mode stand-in for a shared-app runtime.
2
+ import { timingSafeEqual } from "node:crypto";
3
+ import { once } from "node:events";
4
+ import { createServer } from "node:http";
5
+
6
+ // The shared app declares /sellable; Hermes registers /hermes. Every other
7
+ // command reaches Hermes under its own name.
8
+ export const SHARED_SLACK_COMMAND_ALIASES = Object.freeze({
9
+ "/sellable": "/hermes",
10
+ });
11
+
12
+ const record = (value) =>
13
+ value !== null && typeof value === "object" && !Array.isArray(value);
14
+ const malformed = () => new Error("slack_envelope_malformed");
15
+
16
+ // One stored Slack inbox row becomes one Socket Mode frame
17
+ // (slack_sdk/socket_mode/request.py). Events keep Slack's exact body bytes;
18
+ // interactive and slash bodies are Slack's form, decoded as Bolt decodes it.
19
+ export function toSocketModeFrame({
20
+ id,
21
+ kind,
22
+ body,
23
+ slackRetryNum,
24
+ slackRetryReason,
25
+ }) {
26
+ if (
27
+ typeof id !== "string" ||
28
+ !id ||
29
+ typeof body !== "string" ||
30
+ !Number.isInteger(slackRetryNum) ||
31
+ slackRetryNum < 0 ||
32
+ typeof slackRetryReason !== "string"
33
+ )
34
+ throw malformed();
35
+ if (kind === "EVENTS_API") {
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(body);
39
+ } catch {
40
+ throw malformed();
41
+ }
42
+ if (!record(parsed)) throw malformed();
43
+ return (
44
+ `{"envelope_id":${JSON.stringify(id)},"type":"events_api",` +
45
+ `"accepts_response_payload":false,"retry_attempt":${slackRetryNum},` +
46
+ `"retry_reason":${JSON.stringify(slackRetryReason)},"payload":${body}}`
47
+ );
48
+ }
49
+ const frame = (type, payload) =>
50
+ JSON.stringify({
51
+ envelope_id: id,
52
+ type,
53
+ accepts_response_payload: true,
54
+ retry_attempt: slackRetryNum,
55
+ retry_reason: slackRetryReason,
56
+ payload,
57
+ });
58
+ if (kind === "INTERACTIVE") {
59
+ let payload;
60
+ try {
61
+ payload = JSON.parse(new URLSearchParams(body).get("payload"));
62
+ } catch {
63
+ throw malformed();
64
+ }
65
+ if (!record(payload)) throw malformed();
66
+ return frame("interactive", payload);
67
+ }
68
+ if (kind === "SLASH_COMMANDS") {
69
+ const fields = Object.fromEntries(new URLSearchParams(body));
70
+ if (typeof fields.command !== "string" || !fields.command)
71
+ throw malformed();
72
+ fields.command =
73
+ SHARED_SLACK_COMMAND_ALIASES[fields.command] ?? fields.command;
74
+ return frame("slash_commands", fields);
75
+ }
76
+ throw malformed();
77
+ }
78
+
79
+ const json = (res, status, body) => {
80
+ res.writeHead(status, { "content-type": "application/json" });
81
+ res.end(JSON.stringify(body));
82
+ };
83
+ // Length-checked first: timingSafeEqual throws on unequal lengths.
84
+ const same = (provided, expected) => {
85
+ const a = Buffer.from(provided ?? "");
86
+ const b = Buffer.from(expected);
87
+ return a.length === b.length && timingSafeEqual(a, b);
88
+ };
89
+ const OPEN = 1; // WebSocket.OPEN
90
+ // Node's parser admits request targets URL rejects (for example "http://[").
91
+ // A throw in either handler would take the customer runtime down.
92
+ const targetOf = (raw) => {
93
+ try {
94
+ return new URL(raw, "http://127.0.0.1");
95
+ } catch {
96
+ return null;
97
+ }
98
+ };
99
+ const API_METHOD = /^\/api\/([A-Za-z0-9_.]+)$/;
100
+ const HOP_BY_HOP = new Set([
101
+ "host",
102
+ "content-length",
103
+ "connection",
104
+ "transfer-encoding",
105
+ "keep-alive",
106
+ "upgrade",
107
+ ]);
108
+ const SLACK_CHANNEL_ID = /^[CDG][A-Z0-9]+$/;
109
+ // slack_sdk sends some arguments in the query string, and the status call
110
+ // names the channel channel_id.
111
+ const channelOf = (body, contentType, searchParams) => {
112
+ let fields = {};
113
+ try {
114
+ fields = /json/.test(contentType ?? "")
115
+ ? JSON.parse(body)
116
+ : Object.fromEntries(new URLSearchParams(body));
117
+ } catch {}
118
+ const channel =
119
+ fields?.channel ??
120
+ fields?.channel_id ??
121
+ searchParams.get("channel") ??
122
+ searchParams.get("channel_id");
123
+ return typeof channel === "string" && SLACK_CHANNEL_ID.test(channel)
124
+ ? channel
125
+ : null;
126
+ };
127
+
128
+ // Hermes connects here with the per-boot local app token as if this were
129
+ // Slack (docs.slack.dev/apis/events-api/using-socket-mode). The token and
130
+ // ticket never appear in a diagnostic.
131
+ export async function startSlackStandIn(options = {}) {
132
+ const {
133
+ localAppToken,
134
+ ticket,
135
+ appId,
136
+ upstreamOrigin,
137
+ fetchImpl = fetch,
138
+ ackTimeoutMs = 10_000,
139
+ upstreamTimeoutMs = 30_000,
140
+ warmGraceMs = 8_000,
141
+ now = Date.now,
142
+ diagnostic = () => {},
143
+ } = options;
144
+ if (
145
+ typeof localAppToken !== "string" ||
146
+ !localAppToken ||
147
+ typeof ticket !== "string" ||
148
+ !ticket ||
149
+ typeof appId !== "string" ||
150
+ typeof upstreamOrigin !== "string" ||
151
+ typeof fetchImpl !== "function" ||
152
+ typeof now !== "function" ||
153
+ typeof diagnostic !== "function"
154
+ )
155
+ throw new Error("slack_stand_in_configuration_rejected");
156
+ // Imported here so the Admin image, which loads customer-runtime.mjs but
157
+ // never starts the stand-in, does not need ws.
158
+ const { WebSocketServer } = await import("ws");
159
+ const wss = new WebSocketServer({ noServer: true });
160
+ let current = null;
161
+ let connectedAt = 0;
162
+ let port = 0;
163
+ // One in-flight envelope per deliver() call (the relay is sequential). Each
164
+ // waiter knows the socket it was sent on, so a replaced socket releases its
165
+ // own envelope for redelivery instead of letting it time out as no_ack.
166
+ const waiters = new Map();
167
+
168
+ const server = createServer(async (req, res) => {
169
+ const target = targetOf(req.url);
170
+ if (target?.pathname === "/api/apps.connections.open") {
171
+ const ok = same(req.headers.authorization, `Bearer ${localAppToken}`);
172
+ diagnostic({
173
+ kind: "sellable_slack_stand_in_api",
174
+ method: "apps.connections.open",
175
+ status: 200,
176
+ ok,
177
+ error: ok ? null : "invalid_auth",
178
+ channel: null,
179
+ });
180
+ // The SDK reconnects with its cached URL, so the URL is reusable.
181
+ return json(
182
+ res,
183
+ 200,
184
+ ok
185
+ ? { ok, url: `ws://127.0.0.1:${port}/link/?ticket=${ticket}` }
186
+ : { ok, error: "invalid_auth" }
187
+ );
188
+ }
189
+ const method = target && API_METHOD.exec(target.pathname)?.[1];
190
+ if (!method) return json(res, 404, { ok: false, error: "unknown_method" });
191
+ if (req.method !== "GET" && req.method !== "POST")
192
+ return json(res, 405, { ok: false, error: "method_not_allowed" });
193
+ // Every other call goes to Slack with Hermes' own token, and Slack's
194
+ // answer comes back as is. If Slack resets or hangs, Hermes' connection
195
+ // is dropped so its client retries as it would against Slack. This
196
+ // handler never rejects: the runtime has no unhandledRejection handler.
197
+ const line = {
198
+ kind: "sellable_slack_stand_in_api",
199
+ method,
200
+ status: null,
201
+ ok: null,
202
+ error: null,
203
+ channel: null,
204
+ };
205
+ try {
206
+ const parts = [];
207
+ for await (const part of req) parts.push(part);
208
+ const body = Buffer.concat(parts);
209
+ line.channel = channelOf(
210
+ String(body),
211
+ req.headers["content-type"],
212
+ target.searchParams
213
+ );
214
+ const headers = {};
215
+ for (const [name, value] of Object.entries(req.headers))
216
+ if (!HOP_BY_HOP.has(name)) headers[name] = value;
217
+ const upstream = await fetchImpl(
218
+ `${upstreamOrigin}/api/${method}${target.search}`,
219
+ {
220
+ method: req.method,
221
+ headers,
222
+ body: req.method === "POST" ? body : undefined,
223
+ signal: AbortSignal.timeout(upstreamTimeoutMs),
224
+ }
225
+ );
226
+ const answer = Buffer.from(await upstream.arrayBuffer());
227
+ line.status = upstream.status;
228
+ try {
229
+ const parsed = JSON.parse(String(answer));
230
+ if (typeof parsed?.ok === "boolean") line.ok = parsed.ok;
231
+ if (typeof parsed?.error === "string") line.error = parsed.error;
232
+ } catch {}
233
+ // fetch has already decoded the body, so content-encoding is not
234
+ // copied; only the headers Hermes' client reads come back.
235
+ const out = {};
236
+ for (const name of ["content-type", "retry-after"]) {
237
+ const value = upstream.headers.get(name);
238
+ if (value !== null) out[name] = value;
239
+ }
240
+ res.writeHead(upstream.status, out);
241
+ res.end(answer);
242
+ } catch (error) {
243
+ line.error =
244
+ error?.name === "TimeoutError"
245
+ ? "upstream_timeout"
246
+ : "upstream_unavailable";
247
+ req.socket.destroy();
248
+ }
249
+ diagnostic(line);
250
+ });
251
+ server.on("upgrade", (req, socket, head) => {
252
+ socket.on("error", () => socket.destroy());
253
+ const target = targetOf(req.url);
254
+ if (
255
+ target?.pathname !== "/link/" ||
256
+ !same(target.searchParams.get("ticket"), ticket)
257
+ ) {
258
+ socket.once("finish", () => socket.destroy());
259
+ socket.end("HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n");
260
+ return;
261
+ }
262
+ wss.handleUpgrade(req, socket, head, (ws) => {
263
+ // Newest connection wins; Hermes closes its old socket first anyway.
264
+ current?.close(1000);
265
+ current = ws;
266
+ connectedAt = now();
267
+ diagnostic({ kind: "sellable_slack_stand_in_socket", event: "open" });
268
+ ws.on("error", () => {});
269
+ // Acks resolve their waiter; ack payloads are dropped (the inbox already
270
+ // answered Slack). Nothing here may throw.
271
+ ws.on("message", (data, isBinary) => {
272
+ if (isBinary) return;
273
+ let frame;
274
+ try {
275
+ frame = JSON.parse(String(data));
276
+ } catch {
277
+ return;
278
+ }
279
+ waiters.get(frame?.envelope_id)?.settle("delivered");
280
+ });
281
+ ws.on("close", () => {
282
+ if (current === ws) current = null;
283
+ for (const waiter of [...waiters.values()])
284
+ if (waiter.ws === ws) waiter.settle("released");
285
+ diagnostic({ kind: "sellable_slack_stand_in_socket", event: "close" });
286
+ });
287
+ ws.send(
288
+ JSON.stringify({
289
+ type: "hello",
290
+ num_connections: 1,
291
+ debug_info: { host: "sellable-local-stand-in" },
292
+ connection_info: { app_id: appId },
293
+ })
294
+ );
295
+ });
296
+ });
297
+ server.listen(0, "127.0.0.1");
298
+ await once(server, "listening");
299
+ port = server.address().port;
300
+ return {
301
+ origin: `http://127.0.0.1:${port}`,
302
+ port,
303
+ // delivered on Hermes' ack, no_ack past the deadline (Bolt never acks an
304
+ // envelope no listener matches), released when no socket takes it.
305
+ async deliver(envelope) {
306
+ const frame = toSocketModeFrame(envelope);
307
+ const ws = current;
308
+ const outcome = await new Promise((resolve) => {
309
+ if (ws?.readyState !== OPEN) return resolve("released");
310
+ const waiter = {
311
+ ws,
312
+ settle(result) {
313
+ clearTimeout(timer);
314
+ if (waiters.get(envelope.id) === waiter)
315
+ waiters.delete(envelope.id);
316
+ resolve(result);
317
+ },
318
+ };
319
+ const timer = setTimeout(() => waiter.settle("no_ack"), ackTimeoutMs);
320
+ waiters.set(envelope.id, waiter);
321
+ ws.send(frame);
322
+ });
323
+ if (outcome !== "delivered")
324
+ diagnostic({
325
+ kind: "sellable_slack_stand_in_delivery",
326
+ envelopeId: envelope.id,
327
+ outcome,
328
+ });
329
+ return outcome;
330
+ },
331
+ status() {
332
+ const hermesConnected = current?.readyState === OPEN;
333
+ return {
334
+ hermesConnected,
335
+ warm: hermesConnected && now() - connectedAt >= warmGraceMs,
336
+ };
337
+ },
338
+ close: () =>
339
+ new Promise((resolve) => {
340
+ for (const ws of wss.clients) ws.terminate();
341
+ server.close(resolve);
342
+ server.closeAllConnections();
343
+ }),
344
+ };
345
+ }
@@ -1277,6 +1277,18 @@ export function createFlyCustomerControlClient({
1277
1277
  ).claim ?? null
1278
1278
  );
1279
1279
  },
1280
+ async claimSlackEnvelopes({ max, results }) {
1281
+ return {
1282
+ envelopes:
1283
+ (
1284
+ await post(
1285
+ "slack-claim",
1286
+ "/api/v3/sellable-agent/worker/slack-claim",
1287
+ { max, results }
1288
+ )
1289
+ ).envelopes ?? [],
1290
+ };
1291
+ },
1280
1292
  async heartbeatEndpoint(identityBody) {
1281
1293
  return post(
1282
1294
  "endpoint-heartbeat",
@@ -301,6 +301,37 @@ def _sellable_agent_slack_api_kwargs():
301
301
  return {"base_url": origin.rstrip("/") + "/api/"}
302
302
  `;
303
303
 
304
+ // Shared-app runtimes reach Slack through the machine-local stand-in
305
+ // (fly-customer-image/slack-stand-in.mjs). Both keys are required, and the
306
+ // origin must be the stand-in's own loopback listener.
307
+ const SLACK_SHARED_LOOPBACK_API_HELPER = replaceExactlyOnce(
308
+ SLACK_DEDICATED_TEST_API_HELPER,
309
+ "def _sellable_agent_slack_api_kwargs():\n",
310
+ `def _sellable_agent_slack_api_kwargs():
311
+ if (
312
+ os.getenv("SELLABLE_AGENT_RUNTIME", "") == "1"
313
+ and os.getenv("SELLABLE_AGENT_SLACK_TRANSPORT", "") == "shared-local"
314
+ ):
315
+ origin = os.getenv("SELLABLE_AGENT_SLACK_API_ORIGIN", "")
316
+ from urllib.parse import urlparse
317
+ parsed = urlparse(origin)
318
+ if (
319
+ parsed.scheme != "http"
320
+ or parsed.hostname != "127.0.0.1"
321
+ or not parsed.port
322
+ or parsed.username
323
+ or parsed.password
324
+ or parsed.path not in {"", "/"}
325
+ or parsed.params
326
+ or parsed.query
327
+ or parsed.fragment
328
+ ):
329
+ raise RuntimeError("Sellable shared Slack stand-in origin is invalid")
330
+ return {"base_url": origin.rstrip("/") + "/api/"}
331
+ `,
332
+ "shared Slack stand-in API seam"
333
+ );
334
+
304
335
  const SLACK_MESSAGE_UNFURL_FILE_HELPER = `
305
336
  # ${HERMES_AGENT_BRIDGE_MARKER}: recover files from bounded allowed-channel Slack message unfurls.
306
337
  async def _sellable_agent_recover_slack_message_unfurl_files(
@@ -1557,15 +1588,20 @@ def getenv(name, default=""):
1557
1588
 
1558
1589
  if (
1559
1590
  patchKind === "slack-adapter-dedicated-v021" ||
1560
- patchKind === "slack-adapter-dedicated-v0213"
1591
+ patchKind === "slack-adapter-dedicated-v0213" ||
1592
+ patchKind === "slack-adapter-shared-v0215"
1561
1593
  ) {
1562
1594
  // v0.21.3 moved credential lookup into a profile-scoped helper. Preserve
1563
1595
  // that upstream isolation outside the dedicated Sellable runtime.
1564
- const scopedCredential = patchKind === "slack-adapter-dedicated-v0213";
1596
+ const scopedCredential = patchKind !== "slack-adapter-dedicated-v021";
1565
1597
  let patched = replaceExactlyOnce(
1566
1598
  source,
1567
1599
  "logger = logging.getLogger(__name__)\n",
1568
- `logger = logging.getLogger(__name__)\n${SECRET_FD_HELPER}${SLACK_DEDICATED_TEST_API_HELPER}${SLACK_MESSAGE_UNFURL_FILE_HELPER}`,
1600
+ `logger = logging.getLogger(__name__)\n${SECRET_FD_HELPER}${
1601
+ patchKind === "slack-adapter-shared-v0215"
1602
+ ? SLACK_SHARED_LOOPBACK_API_HELPER
1603
+ : SLACK_DEDICATED_TEST_API_HELPER
1604
+ }${SLACK_MESSAGE_UNFURL_FILE_HELPER}`,
1569
1605
  "v0.21 dedicated Slack helpers"
1570
1606
  );
1571
1607
  patched = replaceExactlyOnce(
@@ -2924,11 +2960,11 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V0215_CONTRACT = normalizeContract({
2924
2960
  },
2925
2961
  {
2926
2962
  path: "plugins/platforms/slack/adapter.py",
2927
- patchKind: "slack-adapter-dedicated-v0213",
2963
+ patchKind: "slack-adapter-shared-v0215",
2928
2964
  sourceSha256:
2929
2965
  "3f2ec0cef97964bf4cb1e9791c0917a03cd1e73bb0bc3293768680cd6302f613",
2930
2966
  patchedSha256:
2931
- "26cedfad352e8dadae82bebcedf8813ef948622a350b392ce99097a8ede2f056",
2967
+ "d4b95fa29bfd980a8e33819d3d918c53a2d3355b91904ba14e6941878d4233f8",
2932
2968
  },
2933
2969
  {
2934
2970
  path: "tools/mcp_tool_handlers.py",
@@ -3028,7 +3064,7 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V0215_CONTRACT = normalizeContract({
3028
3064
  },
3029
3065
  ],
3030
3066
  contractDigest:
3031
- "d6158a72b89062ab158fa28eec4532da331e1325b9d7b66d8cd596973866067d",
3067
+ "3953f2f8c0982eaee52a64c6192115c4ee57f88ec4dd76ffa908043bdce61a8e",
3032
3068
  });
3033
3069
 
3034
3070
  function assertRelativeContractPath(path) {
@@ -38,8 +38,8 @@ 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 = "@sellable/install@0.1.836-wip.disqualify.20261012.1";
42
- const MCP_PACKAGE = "@sellable/mcp@0.1.981-wip.disqualify.20261012.1";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.838";
42
+ const MCP_PACKAGE = "@sellable/mcp@0.1.982-wip.durableprep.20261013.1";
43
43
  const HERMES_VERSION = "0.18.0";
44
44
  const SHA256 = /^[a-f0-9]{64}$/;
45
45
  const SHA256_16 = /^[a-f0-9]{16}$/;
@@ -1546,8 +1546,9 @@ 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.836-wip.disqualify.20261012.1" ||
1550
- pinned.mcpPackage !== "@sellable/mcp@0.1.981-wip.disqualify.20261012.1" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.838" ||
1550
+ pinned.mcpPackage !==
1551
+ "@sellable/mcp@0.1.982-wip.durableprep.20261013.1" ||
1551
1552
  !Array.isArray(policy.toolInclude) ||
1552
1553
  policy.toolInclude.length === 0 ||
1553
1554
  !Number.isSafeInteger(slack.generation) ||
@@ -313,9 +313,8 @@ function validateDesired(desired) {
313
313
  desired.serviceCredentialGeneration < 1 ||
314
314
  desired.hermesCli !== "hermes" ||
315
315
  desired.hermesVersion !== "0.18.0" ||
316
- desired.installerPackage !==
317
- "@sellable/install@0.1.836-wip.disqualify.20261012.1" ||
318
- desired.mcpPackage !== "@sellable/mcp@0.1.981-wip.disqualify.20261012.1" ||
316
+ desired.installerPackage !== "@sellable/install@0.1.838" ||
317
+ desired.mcpPackage !== "@sellable/mcp@0.1.982-wip.durableprep.20261013.1" ||
319
318
  !Array.isArray(desired.toolInclude) ||
320
319
  desired.toolInclude.length === 0 ||
321
320
  desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
@@ -25,8 +25,9 @@ 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.836-wip.disqualify.20261012.1";
29
- export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.981-wip.disqualify.20261012.1";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.838";
29
+ export const PINNED_MCP_PACKAGE =
30
+ "@sellable/mcp@0.1.982-wip.durableprep.20261013.1";
30
31
  const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
31
32
  PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
32
33
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.836-wip.disqualify.20261012.1",
3
+ "version": "0.1.838",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {
@@ -40,6 +40,7 @@
40
40
  "@modelcontextprotocol/sdk": "^1.25.2",
41
41
  "eastasianwidth": "0.2.0",
42
42
  "tus-js-client": "^4.3.1",
43
+ "ws": "8.18.3",
43
44
  "yaml": "^2.8.1"
44
45
  },
45
46
  "keywords": [