@sellable/install 0.1.650 → 0.1.652

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.650
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.652
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.880
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-+Fo8aQlYduLaQMQff+ZA25LeUhWZ7I7UH5DECpND84LQ+rHSjKeKFj4Zs93bVHXwQCIy2OLbr75y4f4+CyT57w==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.650" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.652" \
12
12
  && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.880" \
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.650" \
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.650' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.880') 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.652' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.880') 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.650', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.880', 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.652', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.880', 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.650` is
6
+ `@sellable/install@0.1.652` is
7
7
  published:
8
8
 
9
9
  ```sh
@@ -0,0 +1,24 @@
1
+ export const ADMIN_NATIVE_GATEWAY_MAX_LOCAL_RESTARTS = 3;
2
+
3
+ export function nativeGatewayRunning(gateway) {
4
+ const child = gateway?.child;
5
+ return Boolean(
6
+ child &&
7
+ child.exitCode === null &&
8
+ child.signalCode === null &&
9
+ child.killed !== true
10
+ );
11
+ }
12
+
13
+ export function nextAdminNativeGatewayRecovery({ attempts }) {
14
+ const completedAttempts = Number.isInteger(attempts)
15
+ ? Math.max(0, attempts)
16
+ : 0;
17
+ if (completedAttempts >= ADMIN_NATIVE_GATEWAY_MAX_LOCAL_RESTARTS) {
18
+ return {
19
+ action: "EXHAUSTED",
20
+ attempt: ADMIN_NATIVE_GATEWAY_MAX_LOCAL_RESTARTS,
21
+ };
22
+ }
23
+ return { action: "RESTART", attempt: completedAttempts + 1 };
24
+ }
@@ -150,6 +150,7 @@ RUN --mount=type=secret,id=mcp_trust_root,required=true \
150
150
  COPY fly-admin-image/admin-runtime.mjs /usr/local/lib/sellable-agent/admin-runtime/admin-runtime.mjs
151
151
  COPY fly-admin-image/admin-state-generation.mjs /usr/local/lib/sellable-agent/admin-runtime/admin-state-generation.mjs
152
152
  COPY fly-admin-image/start-admin-runtime /usr/local/lib/sellable-agent/admin-runtime/start-admin-runtime
153
+ COPY admin-native-gateway-supervisor.mjs /usr/local/lib/sellable-agent/admin-native-gateway-supervisor.mjs
153
154
  COPY abandoned-runtime-scratch.mjs /usr/local/lib/sellable-agent/abandoned-runtime-scratch.mjs
154
155
  COPY mcp-context-proxy.mjs /usr/local/lib/sellable-agent/mcp-context-proxy.mjs
155
156
  COPY mcp-runtime-binding.mjs /usr/local/lib/sellable-agent/mcp-runtime-binding.mjs
@@ -237,6 +238,7 @@ RUN set -eux; \
237
238
  /usr/local/lib/sellable-agent/admin-runtime/admin-runtime.mjs \
238
239
  /usr/local/lib/sellable-agent/admin-runtime/admin-state-generation.mjs \
239
240
  /usr/local/lib/sellable-agent/admin-runtime/start-admin-runtime \
241
+ /usr/local/lib/sellable-agent/admin-native-gateway-supervisor.mjs \
240
242
  /usr/local/lib/sellable-agent/abandoned-runtime-scratch.mjs \
241
243
  /usr/local/lib/sellable-agent/mcp-context-proxy.mjs \
242
244
  /usr/local/lib/sellable-agent/mcp-runtime-binding.mjs \
@@ -28,6 +28,7 @@ import { createRequire } from "node:module";
28
28
  import { basename, dirname, join } from "node:path";
29
29
  import { createInterface } from "node:readline";
30
30
 
31
+ import { pruneAbandonedRuntimeScratch } from "../abandoned-runtime-scratch.mjs";
31
32
  import {
32
33
  buildAdminSettingsManifest,
33
34
  encodeAdminSettingsBundle,
@@ -45,6 +46,15 @@ import {
45
46
  isOperationBoundBackupClaim,
46
47
  } from "../admin-artifact/node_modules/@sellable/install/lib/sellable-agent/fly-customer-worker.mjs";
47
48
  import sodium from "../admin-artifact/node_modules/libsodium-wrappers/dist/modules-esm/libsodium-wrappers.mjs";
49
+ import {
50
+ nativeGatewayRunning,
51
+ nextAdminNativeGatewayRecovery,
52
+ } from "../admin-native-gateway-supervisor.mjs";
53
+ import {
54
+ bindDefaultProfileProbeCron,
55
+ buildDefaultProfileProjection,
56
+ reconcileDefaultProfileBundle,
57
+ } from "../default-profile-reconciler.mjs";
48
58
  import {
49
59
  SELLABLE_BROWSER_TOOLSET_DEFAULT_MARKER,
50
60
  sellableSlackToolsetsForMaterialization,
@@ -63,18 +73,13 @@ import {
63
73
  renewFlyAdminControlSession,
64
74
  signedWorkerAuthorization,
65
75
  } from "../fly-runtime-identity.mjs";
76
+ import { proveDefaultProfileNativeSkills } from "../fly-skills-bridge.mjs";
66
77
  import {
67
78
  executeExternalSoulBridgeOperation,
68
79
  observeProfileSoul,
69
80
  prepareSoulLockRoot,
70
81
  withProfileSoulLock,
71
82
  } from "../fly-soul-bridge.mjs";
72
- import { proveDefaultProfileNativeSkills } from "../fly-skills-bridge.mjs";
73
- import {
74
- bindDefaultProfileProbeCron,
75
- buildDefaultProfileProjection,
76
- reconcileDefaultProfileBundle,
77
- } from "../default-profile-reconciler.mjs";
78
83
  import {
79
84
  HERMES_SNAPSHOT_HOOKS,
80
85
  HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES,
@@ -103,7 +108,6 @@ import {
103
108
  prepareAdminStateGeneration,
104
109
  recoverAdminStateReceiptMismatch,
105
110
  } from "./admin-state-generation.mjs";
106
- import { pruneAbandonedRuntimeScratch } from "../abandoned-runtime-scratch.mjs";
107
111
 
108
112
  export const ADMIN_RUNTIME_SETTINGS_DIRECTORY =
109
113
  "/run/sellable-agent/admin-settings";
@@ -268,7 +272,10 @@ function adminMaterializerReceipt(profileId, profileRoot) {
268
272
  fileHashes: Object.fromEntries(
269
273
  paths
270
274
  .filter((relative) => existsSync(join(profileRoot, relative)))
271
- .map((relative) => [relative, sha256(readFileSync(join(profileRoot, relative)))])
275
+ .map((relative) => [
276
+ relative,
277
+ sha256(readFileSync(join(profileRoot, relative))),
278
+ ])
272
279
  ),
273
280
  };
274
281
  }
@@ -1453,8 +1460,7 @@ function operationSnapshotIdentity(claim, exchange) {
1453
1460
  function adminServingObservation(activeLaunch, activeGateway, exchange) {
1454
1461
  try {
1455
1462
  const authority = activeLaunch?.claim?.runtimeAuthority;
1456
- const running =
1457
- activeGateway?.child?.exitCode === null && !activeGateway.child.killed;
1463
+ const running = nativeGatewayRunning(activeGateway);
1458
1464
  const productMcp = activeLaunch?.profile?.servers?.find(
1459
1465
  (server) => server.alias === "sellable"
1460
1466
  );
@@ -2813,7 +2819,7 @@ async function startNativeGateway({ profile, claim }) {
2813
2819
  }
2814
2820
 
2815
2821
  async function stopNativeGateway(gateway) {
2816
- if (!gateway?.child || gateway.child.exitCode !== null) return;
2822
+ if (!nativeGatewayRunning(gateway)) return;
2817
2823
  gateway.child.kill("SIGTERM");
2818
2824
  const stopped = await Promise.race([
2819
2825
  gateway.closed.then(() => true),
@@ -2825,6 +2831,100 @@ async function stopNativeGateway(gateway) {
2825
2831
  }
2826
2832
  }
2827
2833
 
2834
+ async function recoverUnexpectedNativeGateway({
2835
+ activeLaunch,
2836
+ activeGateway,
2837
+ activeModelHealthReporter,
2838
+ identity,
2839
+ exchange,
2840
+ attempts,
2841
+ }) {
2842
+ const recovery = nextAdminNativeGatewayRecovery({ attempts });
2843
+ await activeModelHealthReporter?.stop();
2844
+ await stopNativeGateway(activeGateway);
2845
+ if (recovery.action === "EXHAUSTED") {
2846
+ process.stderr.write(
2847
+ `${JSON.stringify({
2848
+ ok: false,
2849
+ code: "admin_native_gateway_restart_exhausted",
2850
+ attempts: recovery.attempt,
2851
+ manualRetryAvailable: true,
2852
+ })}\n`
2853
+ );
2854
+ return {
2855
+ activeGateway: null,
2856
+ activeModelHealthReporter: null,
2857
+ attempts: recovery.attempt,
2858
+ exhausted: true,
2859
+ };
2860
+ }
2861
+ let candidateGateway = null;
2862
+ try {
2863
+ process.stderr.write(
2864
+ `${JSON.stringify({
2865
+ ok: false,
2866
+ code: "admin_native_gateway_exit_detected",
2867
+ attempt: recovery.attempt,
2868
+ })}\n`
2869
+ );
2870
+ candidateGateway = await startNativeGateway(activeLaunch);
2871
+ if (!nativeGatewayRunning(candidateGateway)) {
2872
+ throw new Error("admin_native_gateway_local_restart_failed");
2873
+ }
2874
+ const nextModelHealthReporter = adminModelHealthReporter({
2875
+ identity,
2876
+ exchange,
2877
+ installed: activeLaunch.installed,
2878
+ claim: activeLaunch.claim,
2879
+ });
2880
+ try {
2881
+ await nextModelHealthReporter.start("gateway-recovered");
2882
+ } catch (error) {
2883
+ process.stderr.write(
2884
+ `${JSON.stringify({
2885
+ ok: false,
2886
+ code:
2887
+ error instanceof Error
2888
+ ? error.message
2889
+ : "model_health_report_failed",
2890
+ })}\n`
2891
+ );
2892
+ }
2893
+ process.stderr.write(
2894
+ `${JSON.stringify({
2895
+ ok: true,
2896
+ code: "admin_native_gateway_local_restart",
2897
+ attempt: recovery.attempt,
2898
+ processIdentity: `gateway-${candidateGateway.child.pid}-${activeLaunch.claim.operation.fence}`,
2899
+ })}\n`
2900
+ );
2901
+ return {
2902
+ activeGateway: candidateGateway,
2903
+ activeModelHealthReporter: nextModelHealthReporter,
2904
+ attempts: recovery.attempt,
2905
+ exhausted: false,
2906
+ };
2907
+ } catch (error) {
2908
+ await stopNativeGateway(candidateGateway);
2909
+ process.stderr.write(
2910
+ `${JSON.stringify({
2911
+ ok: false,
2912
+ code:
2913
+ error instanceof Error
2914
+ ? error.message
2915
+ : "admin_native_gateway_local_restart_failed",
2916
+ attempt: recovery.attempt,
2917
+ })}\n`
2918
+ );
2919
+ return {
2920
+ activeGateway: null,
2921
+ activeModelHealthReporter: null,
2922
+ attempts: recovery.attempt,
2923
+ exhausted: false,
2924
+ };
2925
+ }
2926
+ }
2927
+
2828
2928
  export async function runAdminCanary() {
2829
2929
  const step = process.env.SELLABLE_AGENT_ADMIN_CANARY_STEP;
2830
2930
  const recoverySecretPath =
@@ -3379,6 +3479,8 @@ async function runAdminProduction() {
3379
3479
  process.once("SIGTERM", stop);
3380
3480
  let operationHeartbeat = null;
3381
3481
  let soulClaimTracker = null;
3482
+ let gatewayRestartAttempts = 0;
3483
+ let gatewayRestartExhaustionReported = false;
3382
3484
  let failures = 0;
3383
3485
  while (!controller.signal.aborted) {
3384
3486
  let claimedOperation = null;
@@ -3454,6 +3556,23 @@ async function runAdminProduction() {
3454
3556
  if (controller.signal.aborted) break;
3455
3557
  if (!claim) {
3456
3558
  if (activeLaunch) {
3559
+ if (
3560
+ !nativeGatewayRunning(activeGateway) &&
3561
+ !gatewayRestartExhaustionReported
3562
+ ) {
3563
+ const recovery = await recoverUnexpectedNativeGateway({
3564
+ activeLaunch,
3565
+ activeGateway,
3566
+ activeModelHealthReporter,
3567
+ identity,
3568
+ exchange,
3569
+ attempts: gatewayRestartAttempts,
3570
+ });
3571
+ activeGateway = recovery.activeGateway;
3572
+ activeModelHealthReporter = recovery.activeModelHealthReporter;
3573
+ gatewayRestartAttempts = recovery.attempts;
3574
+ gatewayRestartExhaustionReported = recovery.exhausted;
3575
+ }
3457
3576
  await reconcileHermesSnapshot({
3458
3577
  profileId: activeLaunch.profile.profile,
3459
3578
  profileRoot: activeLaunch.profile.profileHome,
@@ -3619,6 +3738,7 @@ async function runAdminProduction() {
3619
3738
  fetchLatest: () => client.getMemorySnapshot(),
3620
3739
  });
3621
3740
  if (!memoryRestore.ok) throw new Error(memoryRestore.code);
3741
+ await operationHeartbeat.reattest();
3622
3742
  const legacyLightfieldVenv = quarantineLegacyLightfieldVenv({
3623
3743
  profileId: basename(profileContract.profileHome),
3624
3744
  profileRoot: profileContract.profileHome,
@@ -3647,6 +3767,7 @@ async function runAdminProduction() {
3647
3767
  slack: nativeSlackProfileInput(claim, installed),
3648
3768
  agentServiceEnv: adminAgentServiceEnvironment(claim),
3649
3769
  });
3770
+ await operationHeartbeat.reattest();
3650
3771
  const materializerReceipt = adminMaterializerReceipt(
3651
3772
  profile.profile,
3652
3773
  profile.profileHome
@@ -3697,6 +3818,7 @@ async function runAdminProduction() {
3697
3818
  profileId: profile.profile,
3698
3819
  profileRoot: profile.profileHome,
3699
3820
  });
3821
+ await operationHeartbeat.reattest();
3700
3822
  const settings = settingsProof(
3701
3823
  imported.receipt.generationRoot,
3702
3824
  imported.receipt.settingsDigest
@@ -3723,6 +3845,7 @@ async function runAdminProduction() {
3723
3845
  desiredLifecycle === "ON"
3724
3846
  ? await proveAdminSlackServing(claim, secrets)
3725
3847
  : false;
3848
+ await operationHeartbeat.reattest();
3726
3849
  const candidateLaunch =
3727
3850
  desiredLifecycle === "ON"
3728
3851
  ? { profile, claim, installed, slackProof }
@@ -3731,6 +3854,7 @@ async function runAdminProduction() {
3731
3854
  try {
3732
3855
  if (candidateLaunch) {
3733
3856
  candidateGateway = await startNativeGateway(candidateLaunch);
3857
+ await operationHeartbeat.reattest();
3734
3858
  }
3735
3859
  const nativeDefaultProfileProof = candidateGateway
3736
3860
  ? proveDefaultProfileNativeSkills({
@@ -3818,6 +3942,8 @@ async function runAdminProduction() {
3818
3942
  }
3819
3943
  await operationHeartbeat.stop();
3820
3944
  operationHeartbeat = null;
3945
+ gatewayRestartAttempts = 0;
3946
+ gatewayRestartExhaustionReported = false;
3821
3947
  failures = 0;
3822
3948
  } catch (error) {
3823
3949
  try {
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
8
8
 
9
9
  ARG MCP_PACKAGE=@sellable/mcp@0.1.880
10
10
  ARG MCP_INTEGRITY=sha512-+Fo8aQlYduLaQMQff+ZA25LeUhWZ7I7UH5DECpND84LQ+rHSjKeKFj4Zs93bVHXwQCIy2OLbr75y4f4+CyT57w==
11
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.650
11
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.652
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
@@ -105,7 +105,7 @@ const BOOT_SESSION_ID = randomUUID();
105
105
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.0";
106
106
  const INSTALLER_PACKAGE =
107
107
  process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ??
108
- "@sellable/install@0.1.650";
108
+ "@sellable/install@0.1.652";
109
109
  const MCP_PACKAGE =
110
110
  process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.880";
111
111
  const DEFAULT_PROFILE_BUNDLE_ROOT =
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
2
  import {
3
3
  closeSync,
4
4
  constants,
@@ -18,11 +18,169 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
18
18
 
19
19
  export const HERMES_AGENT_BRIDGE_ID = "sellable-agent-hermes-bridge/v1";
20
20
  export const HERMES_AGENT_BRIDGE_MARKER = "SELLABLE_AGENT_HERMES_BRIDGE_V1";
21
+ export const HERMES_DEVELOPER_SLACK_METADATA_CONTRACT =
22
+ "hermes-developer-native-slack-metadata/v1";
23
+
24
+ const HERMES_DEVELOPER_NATIVE_SOURCE = "hermes-developer-node-socket-mode/v1";
25
+ const HERMES_DEVELOPER_EVENT_TYPES = new Set([
26
+ "app_mention",
27
+ "message",
28
+ "interactive",
29
+ ]);
30
+ const HERMES_DEVELOPER_EVENT_SUBTYPES = new Set([
31
+ "",
32
+ "message_changed",
33
+ "message_deleted",
34
+ "unsupported",
35
+ ]);
36
+ const HERMES_DEVELOPER_SLACK_ID = /^[A-Z][A-Z0-9]{8,31}$/;
37
+ const HERMES_DEVELOPER_SLACK_TS = /^\d{1,16}\.\d{6}$/;
38
+ const HERMES_DEVELOPER_EVENT_ID = /^[A-Za-z0-9_.:-]{1,128}$/;
21
39
 
22
40
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
23
41
  const isSha256 = (value) =>
24
42
  typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
25
43
 
44
+ function canonicalHermesDeveloperMetadata(value) {
45
+ if (Array.isArray(value)) return value.map(canonicalHermesDeveloperMetadata);
46
+ if (value && typeof value === "object") {
47
+ return Object.fromEntries(
48
+ Object.keys(value)
49
+ .sort()
50
+ .map((key) => [key, canonicalHermesDeveloperMetadata(value[key])])
51
+ );
52
+ }
53
+ return value;
54
+ }
55
+
56
+ function exactHermesDeveloperMetadataKeys(value, expected) {
57
+ return (
58
+ value &&
59
+ typeof value === "object" &&
60
+ !Array.isArray(value) &&
61
+ Object.keys(value).sort().join("\0") === [...expected].sort().join("\0")
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Seal identity copied by the repo-owned Socket Mode receiver before any
67
+ * planner or worker execution. This deliberately has no text or tool
68
+ * arguments: downstream code cannot select a Slack principal, route, event or
69
+ * acknowledgement contract.
70
+ */
71
+ export function createHermesDeveloperTrustedSlackMetadata(
72
+ input,
73
+ authenticationKey
74
+ ) {
75
+ if (!Buffer.isBuffer(authenticationKey) || authenticationKey.length < 32) {
76
+ throw new HermesAgentBridgeError(
77
+ "HERMES_DEVELOPER_SLACK_METADATA_KEY_REQUIRED",
78
+ "Hermes developer Slack metadata requires a gateway-private authentication key"
79
+ );
80
+ }
81
+ const keys = [
82
+ "source",
83
+ "teamId",
84
+ "channelId",
85
+ "actorId",
86
+ "eventId",
87
+ "eventType",
88
+ "eventSubtype",
89
+ "messageTs",
90
+ "threadTs",
91
+ "botId",
92
+ "ackCapability",
93
+ "adapterGeneration",
94
+ ];
95
+ if (
96
+ !exactHermesDeveloperMetadataKeys(input, keys) ||
97
+ input.source !== HERMES_DEVELOPER_NATIVE_SOURCE ||
98
+ !HERMES_DEVELOPER_SLACK_ID.test(input.teamId ?? "") ||
99
+ !HERMES_DEVELOPER_SLACK_ID.test(input.channelId ?? "") ||
100
+ !HERMES_DEVELOPER_SLACK_ID.test(input.actorId ?? "") ||
101
+ !HERMES_DEVELOPER_EVENT_ID.test(input.eventId ?? "") ||
102
+ !HERMES_DEVELOPER_EVENT_TYPES.has(input.eventType) ||
103
+ !HERMES_DEVELOPER_EVENT_SUBTYPES.has(input.eventSubtype ?? "") ||
104
+ !HERMES_DEVELOPER_SLACK_TS.test(input.messageTs ?? "") ||
105
+ (input.threadTs !== null &&
106
+ !HERMES_DEVELOPER_SLACK_TS.test(input.threadTs ?? "")) ||
107
+ (input.botId !== null &&
108
+ !HERMES_DEVELOPER_SLACK_ID.test(input.botId ?? "")) ||
109
+ input.ackCapability !== "defer_until_durable_intake" ||
110
+ !Number.isSafeInteger(input.adapterGeneration) ||
111
+ input.adapterGeneration < 1
112
+ ) {
113
+ throw new HermesAgentBridgeError(
114
+ "HERMES_DEVELOPER_SLACK_METADATA_REJECTED",
115
+ "Hermes developer Slack metadata must come from the deferred-ack repo-owned receiver"
116
+ );
117
+ }
118
+ const body = canonicalHermesDeveloperMetadata({
119
+ contract: HERMES_DEVELOPER_SLACK_METADATA_CONTRACT,
120
+ ...input,
121
+ });
122
+ const metadataDigest = sha256(JSON.stringify(body));
123
+ return Object.freeze({
124
+ ...body,
125
+ metadataDigest,
126
+ authenticationTag: createHmac("sha256", authenticationKey)
127
+ .update(metadataDigest)
128
+ .digest("hex"),
129
+ });
130
+ }
131
+
132
+ export function verifyHermesDeveloperTrustedSlackMetadata(
133
+ value,
134
+ authenticationKey
135
+ ) {
136
+ try {
137
+ if (!Buffer.isBuffer(authenticationKey) || authenticationKey.length < 32) {
138
+ return false;
139
+ }
140
+ if (
141
+ !exactHermesDeveloperMetadataKeys(value, [
142
+ "contract",
143
+ "source",
144
+ "teamId",
145
+ "channelId",
146
+ "actorId",
147
+ "eventId",
148
+ "eventType",
149
+ "eventSubtype",
150
+ "messageTs",
151
+ "threadTs",
152
+ "botId",
153
+ "ackCapability",
154
+ "adapterGeneration",
155
+ "metadataDigest",
156
+ "authenticationTag",
157
+ ]) ||
158
+ value.contract !== HERMES_DEVELOPER_SLACK_METADATA_CONTRACT
159
+ ) {
160
+ return false;
161
+ }
162
+ const {
163
+ contract: _contract,
164
+ metadataDigest,
165
+ authenticationTag,
166
+ ...input
167
+ } = value;
168
+ const rebuilt = createHermesDeveloperTrustedSlackMetadata(
169
+ input,
170
+ authenticationKey
171
+ );
172
+ const supplied = Buffer.from(authenticationTag ?? "", "hex");
173
+ const expected = Buffer.from(rebuilt.authenticationTag, "hex");
174
+ return (
175
+ rebuilt.metadataDigest === metadataDigest &&
176
+ supplied.length === expected.length &&
177
+ timingSafeEqual(supplied, expected)
178
+ );
179
+ } catch {
180
+ return false;
181
+ }
182
+ }
183
+
26
184
  export class HermesAgentBridgeError extends Error {
27
185
  constructor(code, message, details = {}) {
28
186
  super(message);
@@ -38,7 +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 = "@sellable/install@0.1.650";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.652";
42
42
  const MCP_PACKAGE = "@sellable/mcp@0.1.880";
43
43
  const HERMES_VERSION = "0.18.0";
44
44
  const SHA256 = /^[a-f0-9]{64}$/;
@@ -1547,7 +1547,7 @@ 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.650" ||
1550
+ "@sellable/install@0.1.652" ||
1551
1551
  pinned.mcpPackage !== "@sellable/mcp@0.1.880" ||
1552
1552
  !Array.isArray(policy.toolInclude) ||
1553
1553
  policy.toolInclude.length === 0 ||
@@ -283,7 +283,7 @@ function validateDesired(desired) {
283
283
  desired.hermesCli !== "hermes" ||
284
284
  desired.hermesVersion !== "0.18.0" ||
285
285
  desired.installerPackage !==
286
- "@sellable/install@0.1.650" ||
286
+ "@sellable/install@0.1.652" ||
287
287
  desired.mcpPackage !== "@sellable/mcp@0.1.880" ||
288
288
  !Array.isArray(desired.toolInclude) ||
289
289
  desired.toolInclude.length === 0 ||
@@ -26,7 +26,7 @@ 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.650";
29
+ "@sellable/install@0.1.652";
30
30
  export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.880";
31
31
  const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
32
32
  PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.650",
3
+ "version": "0.1.652",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {