@sellable/install 0.1.713 → 0.1.715

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.713
7
- ARG MCP_PACKAGE=@sellable/mcp@0.1.914-wip.first-campaign-onboarding.20260831.4
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.715
7
+ ARG MCP_PACKAGE=@sellable/mcp@0.1.915
8
8
  ARG INSTALLER_INTEGRITY
9
- ARG MCP_INTEGRITY=sha512-rvGDnZy/RxmL/+LZSyevZVBUa7ROeWHw+ltgFNAKvQDq3yIFdccu0ENfU4aYpI+GdNOoahK981UQ5nSjS9CTRQ==
9
+ ARG MCP_INTEGRITY=sha512-+WdyO3WxQ9U7fHMvxBojPRDIZun/INy9mKOz/A1IcBFEWXFkpPooT0yMUzY1qCYcewTM1AUxP3p2QOtsxRIYQA==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.713" \
12
- && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.914-wip.first-campaign-onboarding.20260831.4" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.715" \
12
+ && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.915" \
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.713" \
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.713' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.914-wip.first-campaign-onboarding.20260831.4') 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.715' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.915') 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.713', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.914-wip.first-campaign-onboarding.20260831.4', 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.715', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.915', 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.713` is
6
+ `@sellable/install@0.1.715` 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.914-wip.first-campaign-onboarding.20260831.4";
31
+ const MCP_VERSION = "0.1.915";
32
32
  const SAFE_COMPONENT = /^[A-Za-z0-9@._+-]+$/;
33
33
  const HERMES_EXCLUDES = new Set([".git", ".playwright", "node_modules"]);
34
34
 
@@ -134,6 +134,13 @@ const PROFILES_ROOT = join(DATA_ROOT, "profiles");
134
134
  const RUNTIME_ROOT = join(DATA_ROOT, ".sellable-agent", "admin-runtime");
135
135
  const DELIVERY_ROOT = join(RUNTIME_ROOT, "delivery");
136
136
  const SECRET_ROOT = "/run/sellable-agent/admin-runtime";
137
+ // Keep the Product MCP's Agent credential outside its mutable JSON config.
138
+ // The MCP re-reads that config on every request and ordinary login tooling can
139
+ // update it, so the credential file is the durable service-authority source.
140
+ const PRODUCT_MCP_CREDENTIAL_ROOT = join(
141
+ SECRET_ROOT,
142
+ "product-mcp-credentials"
143
+ );
137
144
  // Turn descriptor + consumed roots for the Product MCP context proxy. They are
138
145
  // normally empty and receive one signed descriptor only while the fenced
139
146
  // endpoint relay owns a turn; the proxy fails closed if either root is absent.
@@ -1878,14 +1885,29 @@ function installRuntimeSecrets({ claim, secrets, desiredLifecycle }) {
1878
1885
  slackSecretFile,
1879
1886
  `SLACK_BOT_TOKEN=${secrets.slackBot}\nSLACK_APP_TOKEN=${secrets.slackApp}\n`
1880
1887
  );
1888
+ const productCredentialDirectory = join(
1889
+ PRODUCT_MCP_CREDENTIAL_ROOT,
1890
+ claim.operation.profileId
1891
+ );
1892
+ const productCredentialPath = join(
1893
+ productCredentialDirectory,
1894
+ "service-credential"
1895
+ );
1896
+ privateDirectory(PRODUCT_MCP_CREDENTIAL_ROOT);
1897
+ privateDirectory(productCredentialDirectory);
1898
+ atomicPrivateFile(productCredentialPath, `${secrets.mcpService}\n`);
1881
1899
  const productConfigPath = join(SECRET_ROOT, "sellable-config.json");
1882
1900
  atomicPrivateFile(
1883
1901
  productConfigPath,
1884
1902
  `${JSON.stringify({
1885
1903
  apiUrl: ORIGIN,
1886
- token: secrets.mcpService,
1887
1904
  activeWorkspaceId: claim.operation.workspaceId,
1888
1905
  workspaceId: claim.operation.workspaceId,
1906
+ serviceCredential: {
1907
+ reference: `secret://agent/service/g${serviceAuthority.credentialGeneration}`,
1908
+ fingerprint: serviceAuthority.credentialFingerprint,
1909
+ generation: serviceAuthority.credentialGeneration,
1910
+ },
1889
1911
  })}\n`
1890
1912
  );
1891
1913
  return {
@@ -2085,6 +2107,7 @@ function clearRuntimeSecrets() {
2085
2107
  ]) {
2086
2108
  rmSync(join(SECRET_ROOT, name), { force: true });
2087
2109
  }
2110
+ rmSync(PRODUCT_MCP_CREDENTIAL_ROOT, { recursive: true, force: true });
2088
2111
  }
2089
2112
 
2090
2113
  function adminAgentServiceEnvironment(claim) {
@@ -2106,6 +2129,7 @@ function adminAgentServiceEnvironment(claim) {
2106
2129
  !Number.isSafeInteger(RUNTIME_GENERATION) ||
2107
2130
  RUNTIME_GENERATION < 1 ||
2108
2131
  !operation ||
2132
+ !/^[A-Za-z0-9_-]{1,128}$/.test(operation.profileId ?? "") ||
2109
2133
  !service ||
2110
2134
  !/^[A-Za-z0-9_-]{1,128}$/.test(service.principalId ?? "") ||
2111
2135
  !Array.isArray(toolInclude) ||
@@ -2139,7 +2163,15 @@ function adminAgentServiceEnvironment(claim) {
2139
2163
  // customer profile while the signed Agent service context remains the home
2140
2164
  // workspace authority.
2141
2165
  SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND: "ADMIN",
2166
+ SELLABLE_AGENT_RUNTIME: "1",
2142
2167
  SELLABLE_AGENT_RUNTIME_GENERATION: String(RUNTIME_GENERATION),
2168
+ SELLABLE_AGENT_PROFILE_ID: operation.profileId,
2169
+ SELLABLE_AGENT_RUNTIME_SECRET_ROOT: PRODUCT_MCP_CREDENTIAL_ROOT,
2170
+ SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE: join(
2171
+ PRODUCT_MCP_CREDENTIAL_ROOT,
2172
+ operation.profileId,
2173
+ "service-credential"
2174
+ ),
2143
2175
  SELLABLE_AGENT_ADMIN_GENERATION_ID: claim.adminRuntimeGeneration.id,
2144
2176
  SELLABLE_AGENT_ADMIN_GENERATION_FENCE: String(
2145
2177
  claim.adminRuntimeGeneration.fence
@@ -3261,6 +3293,7 @@ export async function runAdminCanary() {
3261
3293
  operation: {
3262
3294
  workspaceId: fixtureWorkspaceId,
3263
3295
  agentId: fixtureAgentId,
3296
+ profileId: "phase158-admin-canary",
3264
3297
  fence: "phase158-canary-operation-fence-a",
3265
3298
  },
3266
3299
  runtimeContract: {
@@ -6,9 +6,9 @@ USER root
6
6
 
7
7
  ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
8
8
 
9
- ARG MCP_PACKAGE=@sellable/mcp@0.1.914-wip.first-campaign-onboarding.20260831.4
10
- ARG MCP_INTEGRITY=sha512-rvGDnZy/RxmL/+LZSyevZVBUa7ROeWHw+ltgFNAKvQDq3yIFdccu0ENfU4aYpI+GdNOoahK981UQ5nSjS9CTRQ==
11
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.713
9
+ ARG MCP_PACKAGE=@sellable/mcp@0.1.915
10
+ ARG MCP_INTEGRITY=sha512-+WdyO3WxQ9U7fHMvxBojPRDIZun/INy9mKOz/A1IcBFEWXFkpPooT0yMUzY1qCYcewTM1AUxP3p2QOtsxRIYQA==
11
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.715
12
12
  ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.186-wip.phase158.20260802034727
13
13
  ARG ADMIN_MCP_INTEGRITY=sha512-qYNew5wd2IfgVcp1UYOG9QvpF/8Mx19YJ5HQtMKzvSfUf3evePjtaKsoXLf2C/GUJ5QlLlHJo84VWOI3V01+9A==
14
14
  ARG TUS_JS_CLIENT_VERSION=4.3.1
@@ -58,6 +58,8 @@ import {
58
58
  restoreLatestHermesSnapshot,
59
59
  } from "../hermes-memory-snapshot.mjs";
60
60
  import {
61
+ endpointHermesSessionId,
62
+ endpointMcpDescriptorPath,
61
63
  installMcpTurnDescriptor,
62
64
  replaceMcpTurnDescriptorAtomically,
63
65
  } from "../mcp-context-proxy.mjs";
@@ -105,9 +107,10 @@ const RUNTIME_GENERATION = Number(
105
107
  const BOOT_SESSION_ID = randomUUID();
106
108
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.4";
107
109
  const INSTALLER_PACKAGE =
108
- process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.713";
110
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.715";
109
111
  const MCP_PACKAGE =
110
- process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.914-wip.first-campaign-onboarding.20260831.4";
112
+ process.env.SELLABLE_AGENT_MCP_PACKAGE ??
113
+ "@sellable/mcp@0.1.915";
111
114
  const DEFAULT_PROFILE_BUNDLE_ROOT =
112
115
  "/usr/local/share/sellable-agent/default-profile-bundle";
113
116
  const DEFAULT_PROFILE_CRON_PYTHON = [
@@ -1191,7 +1194,8 @@ function endpointProductProjection(productExecution, output) {
1191
1194
  !action ||
1192
1195
  action.actionKind !== "SEND_MESSAGE" ||
1193
1196
  action.status !== "PROPOSED" ||
1194
- action.argumentHash !== endpointCanonicalOutputDigest(projectedArguments) ||
1197
+ action.argumentHash !==
1198
+ endpointCanonicalOutputDigest(projectedArguments) ||
1195
1199
  /\[SUPPRESS\]|\bSUPPRESS\b|\b(?:no|nothing)\s+truthful\b|\b(?:do|should)\s+not\s+send\b/i.test(
1196
1200
  `${subject ?? ""}\n${candidate}`
1197
1201
  )
@@ -1388,33 +1392,51 @@ export function createCustomerEndpointRelay(options = {}) {
1388
1392
  ) {
1389
1393
  throw new Error("customer_endpoint_relay_configuration_rejected");
1390
1394
  }
1391
- let activeAttempt = null;
1395
+ const activeAttempts = new Set();
1392
1396
  let failures = 0;
1397
+ const maximumConcurrentRuns = 4;
1393
1398
 
1394
- const append = async (claim, identity, event, ordinal) => {
1399
+ // Each redacted event is bounded to 8 KiB and the signed request is bounded
1400
+ // to 64 KiB. Six events leave room for identity/signature envelope overhead
1401
+ // while replacing the previous one-HTTP-request-per-event handoff.
1402
+ const endpointEventBatchSize = 6;
1403
+
1404
+ const appendMany = async (claim, identity, indexedEvents) => {
1395
1405
  if (!endpointRuntimeCurrent(claim, getActiveRuntime(), runtimeGeneration)) {
1396
1406
  throw new Error("customer_endpoint_generation_fenced");
1397
1407
  }
1398
- const envelope = endpointEventEnvelope(event, ordinal);
1408
+ const envelopes = indexedEvents.map(({ event, ordinal }) =>
1409
+ endpointEventEnvelope(event, ordinal)
1410
+ );
1399
1411
  const value = await getControlClient().appendEndpointEvents({
1400
1412
  ...identity,
1401
1413
  schemaVersion: "sellable-agent-endpoint-events/v1",
1402
- events: [envelope],
1414
+ events: envelopes,
1403
1415
  });
1404
1416
  if (
1405
1417
  value?.accepted !== true ||
1406
1418
  !Array.isArray(value.events) ||
1407
- value.events.length !== 1 ||
1408
- value.events[0]?.nativeEventId !== envelope.nativeEventId ||
1409
- typeof value.events[0]?.sequence !== "string"
1419
+ value.events.length !== envelopes.length ||
1420
+ value.events.some(
1421
+ (persisted, index) =>
1422
+ persisted?.nativeEventId !== envelopes[index].nativeEventId ||
1423
+ typeof persisted?.sequence !== "string"
1424
+ )
1410
1425
  ) {
1411
1426
  throw new Error(
1412
- event.kind === "LATE_COMPLETION_NOTICE"
1427
+ indexedEvents.some(
1428
+ ({ event }) => event.kind === "LATE_COMPLETION_NOTICE"
1429
+ )
1413
1430
  ? "customer_endpoint_late_notice_rejected"
1414
1431
  : "customer_endpoint_event_persist_rejected"
1415
1432
  );
1416
1433
  }
1417
- return value.events[0].sequence;
1434
+ return value.events.map((event) => event.sequence);
1435
+ };
1436
+
1437
+ const append = async (claim, identity, event, ordinal) => {
1438
+ const sequences = await appendMany(claim, identity, [{ event, ordinal }]);
1439
+ return sequences[0];
1418
1440
  };
1419
1441
 
1420
1442
  const complete = async (claim, identity, completion) => {
@@ -1666,7 +1688,16 @@ export function createCustomerEndpointRelay(options = {}) {
1666
1688
  claim,
1667
1689
  activeRuntime
1668
1690
  );
1669
- const descriptorPath = activeRuntime.runtimeConfig.turnDescriptorPath;
1691
+ const hermesSessionId = endpointHermesSessionId(
1692
+ descriptorBinding.descriptor.body
1693
+ );
1694
+ const descriptorPath = endpointMcpDescriptorPath(
1695
+ activeRuntime.runtimeConfig.turnDescriptorPath,
1696
+ hermesSessionId
1697
+ );
1698
+ if (!descriptorPath) {
1699
+ throw new Error("customer_endpoint_hermes_session_rejected");
1700
+ }
1670
1701
  const installedDescriptor = installDescriptor({
1671
1702
  descriptorPath,
1672
1703
  descriptorRoot: dirname(descriptorPath),
@@ -1732,7 +1763,7 @@ export function createCustomerEndpointRelay(options = {}) {
1732
1763
  });
1733
1764
  await held.controlChain;
1734
1765
  };
1735
- activeAttempt = held;
1766
+ activeAttempts.add(held);
1736
1767
  const heartbeat = (async () => {
1737
1768
  while (!controller.signal.aborted && !heartbeatStopped) {
1738
1769
  if (deadlineExpired || now().getTime() >= deadlineMs) {
@@ -1783,6 +1814,14 @@ export function createCustomerEndpointRelay(options = {}) {
1783
1814
  })();
1784
1815
  let terminalEvent = null;
1785
1816
  let ordinal = 0;
1817
+ const pendingStreamEvents = [];
1818
+ const flushStreamEvents = async () => {
1819
+ if (pendingStreamEvents.length === 0) return;
1820
+ const batch = pendingStreamEvents.slice(0, endpointEventBatchSize);
1821
+ const sequences = await appendMany(claim, identity, batch);
1822
+ pendingStreamEvents.splice(0, batch.length);
1823
+ held.committedSequence = sequences.at(-1);
1824
+ };
1786
1825
  try {
1787
1826
  let submitted;
1788
1827
  try {
@@ -1817,20 +1856,30 @@ export function createCustomerEndpointRelay(options = {}) {
1817
1856
  ordinal++
1818
1857
  );
1819
1858
  if (held.pendingControl) await queueControl(held.pendingControl);
1820
- const streamed = await runs.streamEvents(submitted.runId, {
1821
- signal: controller.signal,
1822
- onEvent: async (event) => {
1823
- const observedEvent = await holdEndpointUatNativeCompletion({
1824
- control: uatCompletionControl,
1825
- claim,
1826
- event,
1827
- now,
1828
- sleep: sleepImpl,
1829
- signal: controller.signal,
1830
- });
1831
- const late = deadlineExpired || now().getTime() >= deadlineMs;
1832
- const durableEvent =
1833
- late && observedEvent.kind === "RUN_COMPLETED"
1859
+ let streamed;
1860
+ try {
1861
+ streamed = await runs.streamEvents(submitted.runId, {
1862
+ signal: controller.signal,
1863
+ onEvent: async (event) => {
1864
+ const observedEvent = await holdEndpointUatNativeCompletion({
1865
+ control: uatCompletionControl,
1866
+ claim,
1867
+ event,
1868
+ now,
1869
+ sleep: sleepImpl,
1870
+ signal: controller.signal,
1871
+ });
1872
+ const late = deadlineExpired || now().getTime() >= deadlineMs;
1873
+ const lateCompletion =
1874
+ late && observedEvent.kind === "RUN_COMPLETED";
1875
+ // Late notices use a distinct post-deadline authorization path and
1876
+ // cannot share a signed batch with ordinary pre-deadline events.
1877
+ if (lateCompletion) {
1878
+ while (pendingStreamEvents.length > 0) {
1879
+ await flushStreamEvents();
1880
+ }
1881
+ }
1882
+ const durableEvent = lateCompletion
1834
1883
  ? {
1835
1884
  kind: "LATE_COMPLETION_NOTICE",
1836
1885
  outputDigest: observedEvent.outputDigest,
@@ -1838,19 +1887,30 @@ export function createCustomerEndpointRelay(options = {}) {
1838
1887
  nativeEventIdentity: observedEvent.nativeEventIdentity,
1839
1888
  }
1840
1889
  : observedEvent;
1841
- // Persist first. Only the returned committed sequence becomes the
1842
- // local/public cursor witness, and terminal state is assigned after.
1843
- const committedSequence = await append(
1844
- claim,
1845
- identity,
1846
- durableEvent,
1847
- ordinal++
1848
- );
1849
- held.committedSequence = committedSequence;
1850
- if (observedEvent.terminal)
1851
- terminalEvent = { ...observedEvent, late };
1852
- },
1853
- });
1890
+ pendingStreamEvents.push({
1891
+ event: durableEvent,
1892
+ ordinal: ordinal++,
1893
+ });
1894
+ const approvalBoundary =
1895
+ observedEvent.classification === "AWAITING_APPROVAL" ||
1896
+ String(observedEvent.kind ?? "").includes("APPROVAL");
1897
+ if (
1898
+ pendingStreamEvents.length >= endpointEventBatchSize ||
1899
+ observedEvent.terminal ||
1900
+ approvalBoundary
1901
+ ) {
1902
+ await flushStreamEvents();
1903
+ }
1904
+ // Terminal state is assigned only after its durable event batch.
1905
+ if (observedEvent.terminal)
1906
+ terminalEvent = { ...observedEvent, late };
1907
+ },
1908
+ });
1909
+ } finally {
1910
+ while (pendingStreamEvents.length > 0) {
1911
+ await flushStreamEvents();
1912
+ }
1913
+ }
1854
1914
  if (!terminalEvent && !controller.signal.aborted) {
1855
1915
  const status = await runs.status(submitted.runId, {
1856
1916
  signal: controller.signal,
@@ -1967,7 +2027,7 @@ export function createCustomerEndpointRelay(options = {}) {
1967
2027
  await heartbeat;
1968
2028
  await held.controlChain;
1969
2029
  if (existsSync(descriptorPath)) unlinkSync(descriptorPath);
1970
- if (activeAttempt === held) activeAttempt = null;
2030
+ activeAttempts.delete(held);
1971
2031
  }
1972
2032
  };
1973
2033
 
@@ -1986,49 +2046,96 @@ export function createCustomerEndpointRelay(options = {}) {
1986
2046
  });
1987
2047
  },
1988
2048
  async fenceActive(reason = "GATEWAY_RESTART") {
1989
- const held = activeAttempt;
1990
- if (!held || held.stopRequested)
2049
+ const heldAttempts = [...activeAttempts].filter(
2050
+ (held) => !held.stopRequested
2051
+ );
2052
+ if (heldAttempts.length === 0)
1991
2053
  return { ok: true, code: "NO_ACTIVE_ATTEMPT" };
1992
- held.stopRequested = true;
1993
- let stopResult = {
1994
- ok: false,
1995
- classification: "RESTART_LOST",
1996
- certainty: "UNCERTAIN",
1997
- };
1998
- if (held.nativeRunId) {
1999
- stopResult = await held.runs.stop(held.nativeRunId, {
2000
- signal: AbortSignal.timeout(5_000),
2001
- });
2002
- }
2003
- try {
2004
- await append(
2005
- held.claim,
2006
- held.identity,
2007
- {
2008
- kind: "GATEWAY_FENCE_REQUESTED",
2009
- classification: stopResult.classification ?? "STOP_UNCERTAIN",
2010
- requestDigest: sha256(reason),
2011
- },
2012
- Number.MAX_SAFE_INTEGER
2013
- );
2014
- } catch {
2015
- // A newer runtime generation is expected to reject this old fence.
2016
- }
2017
- held.controller.abort(new Error("customer_endpoint_gateway_fenced"));
2054
+ const results = await Promise.all(
2055
+ heldAttempts.map(async (held) => {
2056
+ held.stopRequested = true;
2057
+ let stopResult = {
2058
+ ok: false,
2059
+ classification: "RESTART_LOST",
2060
+ certainty: "UNCERTAIN",
2061
+ };
2062
+ if (held.nativeRunId) {
2063
+ try {
2064
+ stopResult = await held.runs.stop(held.nativeRunId, {
2065
+ signal: AbortSignal.timeout(5_000),
2066
+ });
2067
+ } catch {
2068
+ stopResult = {
2069
+ ok: false,
2070
+ classification: "STOP_UNCERTAIN",
2071
+ certainty: "UNCERTAIN",
2072
+ };
2073
+ }
2074
+ }
2075
+ try {
2076
+ await append(
2077
+ held.claim,
2078
+ held.identity,
2079
+ {
2080
+ kind: "GATEWAY_FENCE_REQUESTED",
2081
+ classification: stopResult.classification ?? "STOP_UNCERTAIN",
2082
+ requestDigest: sha256(reason),
2083
+ },
2084
+ Number.MAX_SAFE_INTEGER
2085
+ );
2086
+ } catch {
2087
+ // A newer runtime generation is expected to reject this old fence.
2088
+ }
2089
+ held.controller.abort(new Error("customer_endpoint_gateway_fenced"));
2090
+ return stopResult;
2091
+ })
2092
+ );
2093
+ const uncertain = results.find(
2094
+ (result) => result.positiveCancellation !== true
2095
+ );
2018
2096
  return {
2019
- ok: stopResult.positiveCancellation === true,
2020
- code:
2021
- stopResult.positiveCancellation === true
2022
- ? "CANCELLED"
2023
- : (stopResult.classification ?? "STOP_UNCERTAIN"),
2097
+ ok: !uncertain,
2098
+ code: uncertain
2099
+ ? (uncertain.classification ?? "STOP_UNCERTAIN")
2100
+ : "CANCELLED",
2024
2101
  };
2025
2102
  },
2026
2103
  async run(signal) {
2027
2104
  if (!(signal instanceof AbortSignal)) {
2028
2105
  throw new Error("customer_endpoint_relay_signal_rejected");
2029
2106
  }
2107
+ const inFlight = new Set();
2108
+ const launch = (claim) => {
2109
+ let task;
2110
+ task = (async () => {
2111
+ try {
2112
+ const result = await runClaim(claim, signal);
2113
+ diagnostic({
2114
+ ok: result.ok,
2115
+ code: result.code,
2116
+ attemptIdDigest: sha256(claim.attempt?.id ?? ""),
2117
+ });
2118
+ } catch (error) {
2119
+ diagnostic({
2120
+ ok: false,
2121
+ code:
2122
+ error instanceof Error
2123
+ ? redactRuntimeDiagnostic(error.message)
2124
+ : "customer_endpoint_run_failed",
2125
+ attemptIdDigest: sha256(claim.attempt?.id ?? ""),
2126
+ });
2127
+ } finally {
2128
+ inFlight.delete(task);
2129
+ }
2130
+ })();
2131
+ inFlight.add(task);
2132
+ };
2030
2133
  while (!signal.aborted) {
2031
2134
  try {
2135
+ if (inFlight.size >= maximumConcurrentRuns) {
2136
+ await Promise.race(inFlight);
2137
+ continue;
2138
+ }
2032
2139
  const activeRuntime = getActiveRuntime();
2033
2140
  if (!activeRuntime?.gateway?.readback?.().running) {
2034
2141
  await abortablePause(500, signal, sleepImpl);
@@ -2040,12 +2147,7 @@ export function createCustomerEndpointRelay(options = {}) {
2040
2147
  await abortablePause(1_000, signal, sleepImpl);
2041
2148
  continue;
2042
2149
  }
2043
- const result = await runClaim(claim, signal);
2044
- diagnostic({
2045
- ok: result.ok,
2046
- code: result.code,
2047
- attemptIdDigest: sha256(claim.attempt?.id ?? ""),
2048
- });
2150
+ launch(claim);
2049
2151
  failures = 0;
2050
2152
  } catch (error) {
2051
2153
  failures = Math.min(failures + 1, 6);
@@ -2063,6 +2165,7 @@ export function createCustomerEndpointRelay(options = {}) {
2063
2165
  );
2064
2166
  }
2065
2167
  }
2168
+ await Promise.allSettled(inFlight);
2066
2169
  },
2067
2170
  });
2068
2171
  }
@@ -744,17 +744,8 @@ def _sellable_agent_call_meta():
744
744
  if os.getenv("SELLABLE_AGENT_RUNTIME", "") != "1":
745
745
  return None
746
746
  from gateway.session_context import get_session_env, session_context_engaged
747
- if (
748
- not session_context_engaged()
749
- or get_session_env("HERMES_SESSION_PLATFORM", "").strip().lower() != "slack"
750
- ):
751
- # Non-Slack runtime sessions (console/TUI, cron) carry no Slack actor
752
- # identity. Send no actor meta; the serving API enforces its own actor
753
- # gates, as with installs outside the Sellable runtime.
754
- return None
755
- from gateway.session_context import get_session_env, session_context_engaged
756
747
  if not session_context_engaged():
757
- raise RuntimeError("Sellable MCP calls require an active Hermes session context")
748
+ return None
758
749
  legacy_names = (
759
750
  "HERMES_SESSION_PLATFORM",
760
751
  "HERMES_SESSION_CHAT_ID",
@@ -764,6 +755,11 @@ def _sellable_agent_call_meta():
764
755
  if any(os.getenv(name) for name in legacy_names):
765
756
  raise RuntimeError("Sellable MCP identity must not come from process-global session variables")
766
757
  platform = get_session_env("HERMES_SESSION_PLATFORM", "").strip().lower()
758
+ session_id = get_session_env("HERMES_SESSION_ID", "").strip()
759
+ if platform == "api_server":
760
+ if not session_id:
761
+ raise RuntimeError("Sellable endpoint MCP calls require a bound Hermes session")
762
+ return {"sellableEndpoint": {"sessionId": session_id}}
767
763
  if platform != "slack":
768
764
  descriptor_path = os.getenv("SELLABLE_AGENT_TURN_DESCRIPTOR_PATH", "")
769
765
  if descriptor_path.startswith("/"):
@@ -778,7 +774,6 @@ def _sellable_agent_call_meta():
778
774
  channel_id = get_session_env("HERMES_SESSION_CHAT_ID", "").strip()
779
775
  requester_id = get_session_env("HERMES_SESSION_USER_ID", "").strip()
780
776
  message_id = get_session_env("HERMES_SESSION_MESSAGE_ID", "").strip()
781
- session_id = get_session_env("HERMES_SESSION_ID", "").strip()
782
777
  if not channel_id or not requester_id or not message_id or not session_id:
783
778
  raise RuntimeError("Sellable MCP calls require a bound Slack requester, channel, message, and session")
784
779
  return {
@@ -2021,7 +2016,7 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V020_CONTRACT = normalizeContract({
2021
2016
  sourceSha256:
2022
2017
  "8ce75e7f2d999edf6554a8f5a037289c885bc9427263da1cd6e3448e608fe20f",
2023
2018
  patchedSha256:
2024
- "0fa1fbd502f8e60940dd839b7a0a065c75ef411baf1a1fb419fd05303e3390f0",
2019
+ "2a32c22e499bdb16f31ddd77f143b8f4907df9f4972553b07129fc2a9607b174",
2025
2020
  },
2026
2021
  {
2027
2022
  path: "gateway/run.py",
@@ -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.713";
42
- const MCP_PACKAGE = "@sellable/mcp@0.1.914-wip.first-campaign-onboarding.20260831.4";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.715";
42
+ const MCP_PACKAGE = "@sellable/mcp@0.1.915";
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}$/;
@@ -1547,8 +1547,8 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1547
1547
  pinned.hermesCli !== "hermes" ||
1548
1548
  pinned.hermesVersion !== "0.18.0" ||
1549
1549
  pinned.installerPackage !==
1550
- "@sellable/install@0.1.713" ||
1551
- pinned.mcpPackage !== "@sellable/mcp@0.1.914-wip.first-campaign-onboarding.20260831.4" ||
1550
+ "@sellable/install@0.1.715" ||
1551
+ pinned.mcpPackage !== "@sellable/mcp@0.1.915" ||
1552
1552
  !Array.isArray(policy.toolInclude) ||
1553
1553
  policy.toolInclude.length === 0 ||
1554
1554
  !Number.isSafeInteger(slack.generation) ||
@@ -947,13 +947,38 @@ function isHermesSlackToolCall(request) {
947
947
  );
948
948
  }
949
949
 
950
- function endpointHermesSessionId(body) {
950
+ export function endpointHermesSessionId(body) {
951
951
  if (body?.schemaVersion !== "sellable-agent-mcp-endpoint-run/v1") return null;
952
952
  return `sellable_endpoint_${sha256(
953
953
  `${body.conversationId}:${body.runId}:${body.attemptId}`
954
954
  ).slice(0, 32)}`;
955
955
  }
956
956
 
957
+ export function endpointMcpDescriptorPath(descriptorPath, sessionId) {
958
+ if (
959
+ !isAbsolute(descriptorPath ?? "") ||
960
+ typeof sessionId !== "string" ||
961
+ !SAFE_ID.test(sessionId)
962
+ ) {
963
+ return null;
964
+ }
965
+ return join(dirname(descriptorPath), `endpoint-${sha256(sessionId)}.json`);
966
+ }
967
+
968
+ function requestEndpointHermesSessionId(request) {
969
+ if (request?.method !== "tools/call" && request?.method !== "tools/list") {
970
+ return null;
971
+ }
972
+ const meta = request?.params?._meta;
973
+ if (!exactKeys(meta, ["sellableEndpoint"])) return null;
974
+ const endpoint = meta.sellableEndpoint;
975
+ if (!exactKeys(endpoint, ["sessionId"])) return null;
976
+ return typeof endpoint.sessionId === "string" &&
977
+ SAFE_ID.test(endpoint.sessionId)
978
+ ? endpoint.sessionId
979
+ : null;
980
+ }
981
+
957
982
  function isExactEndpointHermesSessionToolCall(request, body) {
958
983
  if (!isHermesSlackToolCall(request)) return false;
959
984
  const expectedSessionId = endpointHermesSessionId(body);
@@ -1079,13 +1104,45 @@ export async function runMcpContextProxyStdio({
1079
1104
  }
1080
1105
  return response;
1081
1106
  };
1082
- const endpointSession = createMcpEndpointSession();
1107
+ const legacyEndpointSession = createMcpEndpointSession();
1108
+ const endpointSessionsByHermesId = new Map();
1083
1109
 
1084
1110
  for await (const line of inputLines) {
1085
1111
  const request = parseJsonRpcLine(line);
1086
1112
  if (!request) throw new Error("mcp_upstream_protocol_rejected");
1087
- const candidate = existsSync(descriptorPath)
1088
- ? descriptorCandidate(descriptorPath, descriptorRoot)
1113
+ const requestNow = Date.now();
1114
+ for (const [sessionId, session] of endpointSessionsByHermesId) {
1115
+ const body = endpointSessionBody(session);
1116
+ if (!body || requestNow >= Date.parse(body.expiresAt)) {
1117
+ endpointSessionsByHermesId.delete(sessionId);
1118
+ }
1119
+ }
1120
+ const endpointHermesId = requestEndpointHermesSessionId(request);
1121
+ const requestDescriptorPath = endpointHermesId
1122
+ ? endpointMcpDescriptorPath(descriptorPath, endpointHermesId)
1123
+ : descriptorPath;
1124
+ if (!requestDescriptorPath) {
1125
+ await writeJsonLine(output, {
1126
+ jsonrpc: "2.0",
1127
+ id: request.id ?? null,
1128
+ error: {
1129
+ code: -32001,
1130
+ message: "endpoint_session_rejected",
1131
+ data: { code: "endpoint_session_rejected" },
1132
+ },
1133
+ });
1134
+ continue;
1135
+ }
1136
+ let endpointSession = legacyEndpointSession;
1137
+ if (endpointHermesId) {
1138
+ endpointSession = endpointSessionsByHermesId.get(endpointHermesId);
1139
+ if (!endpointSession) {
1140
+ endpointSession = createMcpEndpointSession();
1141
+ endpointSessionsByHermesId.set(endpointHermesId, endpointSession);
1142
+ }
1143
+ }
1144
+ const candidate = existsSync(requestDescriptorPath)
1145
+ ? descriptorCandidate(requestDescriptorPath, descriptorRoot)
1089
1146
  : null;
1090
1147
  const activeBody = endpointSessionBody(endpointSession);
1091
1148
  const endpointBody =
@@ -1097,16 +1154,38 @@ export async function runMcpContextProxyStdio({
1097
1154
  const endpointMode = Boolean(
1098
1155
  mcpEndpointSessionActivated(endpointSession) || endpointBody
1099
1156
  );
1100
- // The patched Hermes bridge adds this exact request-scoped shape from its
1101
- // live Slack session ContextVar; the model controls only `arguments` and
1102
- // cannot author MCP `_meta`. Route genuine Slack calls directly even when
1103
- // this shared stdio transport previously served an endpoint run. Hermes
1104
- // also reuses its Slack-shaped ContextVar for endpoint calls, but gives
1105
- // those calls the exact deterministic endpoint session id. Recover that
1106
- // reserved shape through the signed endpoint authorization below so stale
1107
- // Slack context cannot bypass the endpoint receipt path. Any unverified
1108
- // descriptor can only select that fail-closed path; it is still validated
1109
- // before its metadata reaches the downstream process.
1157
+ if (
1158
+ endpointHermesId &&
1159
+ endpointBody &&
1160
+ endpointHermesSessionId(endpointBody) !== endpointHermesId
1161
+ ) {
1162
+ await writeJsonLine(output, {
1163
+ jsonrpc: "2.0",
1164
+ id: request.id ?? null,
1165
+ error: {
1166
+ code: -32001,
1167
+ message: "endpoint_session_binding_mismatch",
1168
+ data: { code: "endpoint_session_binding_mismatch" },
1169
+ },
1170
+ });
1171
+ continue;
1172
+ }
1173
+ if (endpointHermesId && !endpointMode) {
1174
+ await writeJsonLine(output, {
1175
+ jsonrpc: "2.0",
1176
+ id: request.id ?? null,
1177
+ error: {
1178
+ code: -32001,
1179
+ message: "descriptor_missing",
1180
+ data: { code: "descriptor_missing" },
1181
+ },
1182
+ });
1183
+ continue;
1184
+ }
1185
+ // The patched Hermes bridge adds request-scoped metadata from task-local
1186
+ // ContextVars; the model controls only `arguments` and cannot author MCP
1187
+ // `_meta`. Genuine Slack calls pass through with their actor identity.
1188
+ // API endpoint calls select their own signed descriptor/session above.
1110
1189
  if (
1111
1190
  isHermesSlackToolCall(request) &&
1112
1191
  !(
@@ -1118,16 +1197,16 @@ export async function runMcpContextProxyStdio({
1118
1197
  continue;
1119
1198
  }
1120
1199
  if (request.method === "tools/list" && endpointMode) {
1121
- const expected = existsSync(descriptorPath)
1200
+ const expected = existsSync(requestDescriptorPath)
1122
1201
  ? deriveExpectedMcpTurnBinding({
1123
- descriptorPath,
1202
+ descriptorPath: requestDescriptorPath,
1124
1203
  descriptorRoot,
1125
1204
  runtimeBindingPath,
1126
1205
  })
1127
1206
  : { ok: true, expected: null };
1128
1207
  const authorized = expected.ok
1129
1208
  ? await authorizeMcpToolList({
1130
- descriptorPath,
1209
+ descriptorPath: requestDescriptorPath,
1131
1210
  descriptorRoot,
1132
1211
  consumedRoot,
1133
1212
  trustRootPublicKeyPem,
@@ -1160,20 +1239,20 @@ export async function runMcpContextProxyStdio({
1160
1239
  // The existing Slack bridge remains the task-local authority when no
1161
1240
  // server-minted endpoint descriptor is installed. Installing this proxy
1162
1241
  // for endpoint runs must not weaken or fabricate that Slack path.
1163
- if (!existsSync(descriptorPath) && !endpointMode) {
1242
+ if (!existsSync(requestDescriptorPath) && !endpointMode) {
1164
1243
  await writeJsonLine(child.stdin, request);
1165
1244
  continue;
1166
1245
  }
1167
- const expected = existsSync(descriptorPath)
1246
+ const expected = existsSync(requestDescriptorPath)
1168
1247
  ? deriveExpectedMcpTurnBinding({
1169
- descriptorPath,
1248
+ descriptorPath: requestDescriptorPath,
1170
1249
  descriptorRoot,
1171
1250
  runtimeBindingPath,
1172
1251
  })
1173
1252
  : { ok: true, expected: null };
1174
1253
  const authorized = expected.ok
1175
1254
  ? await authorizeMcpToolCall({
1176
- descriptorPath,
1255
+ descriptorPath: requestDescriptorPath,
1177
1256
  descriptorRoot,
1178
1257
  consumedRoot,
1179
1258
  trustRootPublicKeyPem,
@@ -283,8 +283,8 @@ function validateDesired(desired) {
283
283
  desired.hermesCli !== "hermes" ||
284
284
  desired.hermesVersion !== "0.18.0" ||
285
285
  desired.installerPackage !==
286
- "@sellable/install@0.1.713" ||
287
- desired.mcpPackage !== "@sellable/mcp@0.1.914-wip.first-campaign-onboarding.20260831.4" ||
286
+ "@sellable/install@0.1.715" ||
287
+ desired.mcpPackage !== "@sellable/mcp@0.1.915" ||
288
288
  !Array.isArray(desired.toolInclude) ||
289
289
  desired.toolInclude.length === 0 ||
290
290
  desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
@@ -26,8 +26,8 @@ import { deriveContainedProfileId } from "./profile-materializer.mjs";
26
26
 
27
27
  export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
28
28
  export const PINNED_INSTALL_PACKAGE =
29
- "@sellable/install@0.1.713";
30
- export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.914-wip.first-campaign-onboarding.20260831.4";
29
+ "@sellable/install@0.1.715";
30
+ export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.915";
31
31
  const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
32
32
  PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
33
33
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.713",
3
+ "version": "0.1.715",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {