@sellable/install 0.1.578-wip.phase170.20260809.2 → 0.1.579

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.578-wip.phase170.20260809.2
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.577
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.858
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-8yiUhwMbBk/KxZAXGXTsEHiN8XeB/qCqNgfApew0rhViJqiLlDN8oS67pZW5iB5gdmeuv4du0jx9Lhq/blr9hQ==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.578-wip.phase170.20260809.2" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.577" \
12
12
  && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.858" \
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.578-wip.phase170.202608
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.578-wip.phase170.20260809.2' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.858') 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.577' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.858') 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.578-wip.phase170.20260809.2', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.858', 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.577', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.858', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
29
29
  && npm cache clean --force
30
30
 
31
31
  COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
@@ -3,13 +3,13 @@
3
3
  This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
4
4
 
5
5
  Build from this directory after
6
- `@sellable/install@0.1.578-wip.phase170.20260809.2` is
6
+ `@sellable/install@0.1.577` is
7
7
  published:
8
8
 
9
9
  ```sh
10
10
  docker build --platform linux/amd64 \
11
11
  --build-arg INSTALLER_INTEGRITY='sha512-…' \
12
- --tag sellable-agent-host:0.1.578-wip.phase170.20260809.2 .
12
+ --tag sellable-agent-host:0.1.577 .
13
13
  ```
14
14
 
15
15
  Create a private `bootstrap/` directory owned by root with mode `0700`. It must contain `bootstrap.json`, `provider-auth`, and `slack-app`, each owned by root with mode `0600`. `bootstrap.json` contains only non-secret identities, hashes, package integrities, and the control-plane URL. The other two files contain the provider JSON and global Slack app token whose hashes are pinned by `bootstrap.json`.
@@ -86,8 +86,6 @@ COPY hermes-bridge.mjs /usr/local/lib/sellable-agent/hermes-bridge.mjs
86
86
  COPY hermes-memory-snapshot.mjs /usr/local/lib/sellable-agent/hermes-memory-snapshot.mjs
87
87
  COPY fly-soul-bridge.mjs /usr/local/lib/sellable-agent/fly-soul-bridge.mjs
88
88
  COPY fly-soul-bridge-exec.mjs /usr/local/lib/sellable-agent/fly-soul-bridge-exec.mjs
89
- COPY fly-skills-bridge.mjs /usr/local/lib/sellable-agent/fly-skills-bridge.mjs
90
- COPY fly-skills-bridge-exec.mjs /usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs
91
89
  COPY soul-artifact-validator.mjs /usr/local/lib/sellable-agent/soul-artifact-validator.mjs
92
90
  COPY hermes-memory-dirty.mjs /opt/sellable/bin/hermes-memory-dirty
93
91
  COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
@@ -109,18 +107,9 @@ RUN node --input-type=module -e \
109
107
  'import("file:///usr/local/lib/sellable-agent/fly-soul-bridge-exec.mjs").then(({ runFlySoulBridgeExecCli }) => runFlySoulBridgeExecCli());' \
110
108
  > /usr/local/bin/sellable-soul-bridge-exec \
111
109
  && chmod 0555 /usr/local/bin/sellable-soul-bridge-exec \
112
- && printf '%s\n' \
113
- '#!/usr/bin/env node' \
114
- 'import("file:///usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs").then(({ runFlySkillsBridgeExecCli }) => runFlySkillsBridgeExecCli());' \
115
- > /usr/local/bin/sellable-skills-bridge-exec \
116
- && chmod 0555 /usr/local/bin/sellable-skills-bridge-exec \
117
110
  && sha256sum /usr/local/lib/sellable-agent/fly-soul-bridge.mjs \
118
111
  > /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
119
- && sha256sum /usr/local/lib/sellable-agent/fly-skills-bridge.mjs \
120
- /usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs \
121
- > /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
122
112
  && chmod 0444 /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
123
- /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
124
113
  && chmod 0755 /etc \
125
114
  && chmod 0555 \
126
115
  /usr/local/lib/sellable-agent/admin-runtime/admin-runtime.mjs \
@@ -133,8 +122,6 @@ RUN node --input-type=module -e \
133
122
  /usr/local/lib/sellable-agent/hermes-memory-snapshot.mjs \
134
123
  /usr/local/lib/sellable-agent/fly-soul-bridge.mjs \
135
124
  /usr/local/lib/sellable-agent/fly-soul-bridge-exec.mjs \
136
- /usr/local/lib/sellable-agent/fly-skills-bridge.mjs \
137
- /usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs \
138
125
  /usr/local/lib/sellable-agent/soul-artifact-validator.mjs \
139
126
  /opt/sellable/bin/hermes-memory-dirty \
140
127
  /opt/sellable/bin/hermes-endpoint-cron \
@@ -606,6 +606,21 @@ function installAdminModelConfig(profileRoot, modelConfig) {
606
606
  modelConfig.credential_pool_strategies
607
607
  );
608
608
  document.set("fallback_providers", modelConfig.fallback_providers);
609
+ if (document.get("provider_routing") === undefined) {
610
+ // OpenRouter provider selection defaults to highest throughput; an
611
+ // explicit admin config value wins.
612
+ document.set("provider_routing", { sort: "throughput" });
613
+ }
614
+ const currentAuxiliary = document.toJS().auxiliary ?? {};
615
+ if (!currentAuxiliary.vision) {
616
+ // Non-vision main models need a working vision auxiliary; the Nous
617
+ // portal has no credentials on these machines, so image input would
618
+ // silently fail without this default.
619
+ document.set("auxiliary", {
620
+ ...currentAuxiliary,
621
+ vision: { provider: "openrouter", model: "google/gemini-3.5-flash" },
622
+ });
623
+ }
609
624
  atomicPrivateFile(path, document.toString());
610
625
  const readback = parseDocument(readFileSync(path, "utf8")).toJS();
611
626
  if (
@@ -622,78 +637,6 @@ function installAdminModelConfig(profileRoot, modelConfig) {
622
637
  }
623
638
  }
624
639
 
625
- const ADMIN_LIVE_SKILLS_CONFIG_KEYS = Object.freeze([
626
- "disabled",
627
- "platform_disabled",
628
- ]);
629
-
630
- function adminPreservedSkillsIdentity(profileRoot) {
631
- if (!existsSync(profileRoot)) return null;
632
- const entries = [];
633
- const skillsRoot = join(profileRoot, "skills");
634
- const walk = (directory, relativePath = "") => {
635
- for (const name of readdirSync(directory).sort()) {
636
- const nextRelative = relativePath ? `${relativePath}/${name}` : name;
637
- if (nextRelative === "sellable") continue;
638
- const path = join(directory, name);
639
- const stat = lstatSync(path);
640
- if (stat.isSymbolicLink()) {
641
- throw new Error("admin_runtime_live_skills_symlink_rejected");
642
- }
643
- if (stat.isDirectory()) walk(path, nextRelative);
644
- else if (stat.isFile()) {
645
- entries.push({
646
- path: nextRelative,
647
- sha256: sha256(readFileSync(path)),
648
- });
649
- } else throw new Error("admin_runtime_live_skills_entry_rejected");
650
- }
651
- };
652
- if (existsSync(skillsRoot)) walk(skillsRoot);
653
- const configPath = join(profileRoot, "config.yaml");
654
- let disabled = {};
655
- if (existsSync(configPath)) {
656
- const document = parseDocument(readFileSync(configPath, "utf8"));
657
- if (document.errors.length > 0) {
658
- throw new Error("admin_runtime_live_skills_config_rejected");
659
- }
660
- const skills = document.toJS()?.skills;
661
- if (skills && typeof skills === "object" && !Array.isArray(skills)) {
662
- disabled = Object.fromEntries(
663
- ADMIN_LIVE_SKILLS_CONFIG_KEYS.filter((key) =>
664
- Object.prototype.hasOwnProperty.call(skills, key)
665
- ).map((key) => [key, skills[key]])
666
- );
667
- }
668
- }
669
- return sha256(canonicalJson({ disabled, entries }));
670
- }
671
-
672
- function mergeAdminLiveSkillsConfig(sourceRoot, targetRoot) {
673
- const sourcePath = join(sourceRoot, "config.yaml");
674
- const targetPath = join(targetRoot, "config.yaml");
675
- if (!existsSync(sourcePath) || !existsSync(targetPath)) return;
676
- const sourceDocument = parseDocument(readFileSync(sourcePath, "utf8"));
677
- const targetDocument = parseDocument(readFileSync(targetPath, "utf8"));
678
- if (sourceDocument.errors.length > 0 || targetDocument.errors.length > 0) {
679
- throw new Error("admin_runtime_live_skills_config_rejected");
680
- }
681
- const sourceSkills = sourceDocument.toJS()?.skills;
682
- if (
683
- !sourceSkills ||
684
- typeof sourceSkills !== "object" ||
685
- Array.isArray(sourceSkills)
686
- ) {
687
- return;
688
- }
689
- for (const key of ADMIN_LIVE_SKILLS_CONFIG_KEYS) {
690
- if (Object.prototype.hasOwnProperty.call(sourceSkills, key)) {
691
- targetDocument.setIn(["skills", key], structuredClone(sourceSkills[key]));
692
- }
693
- }
694
- atomicPrivateFile(targetPath, targetDocument.toString());
695
- }
696
-
697
640
  async function materializeAdminProfile({
698
641
  profileContract,
699
642
  settingsRoot,
@@ -799,6 +742,7 @@ async function materializeAdminProfile({
799
742
  },
800
743
  });
801
744
  installAdminModelConfig(stageRoot, modelConfig);
745
+ mergeHermesState(profileHome, stageRoot);
802
746
  installAdminHermesMemoryHook(stageRoot);
803
747
  stageAdminRuntimeTree(stageRoot);
804
748
  return stage;
@@ -822,16 +766,6 @@ async function materializeAdminProfile({
822
766
  rmSync(stageRoot, { recursive: true, force: true });
823
767
  return { status: "RESTAGE", stagedAuthority: finalAuthority };
824
768
  }
825
- const liveSkillsIdentity = adminPreservedSkillsIdentity(profileHome);
826
- mergeHermesState(profileHome, stageRoot);
827
- mergeAdminLiveSkillsConfig(profileHome, stageRoot);
828
- if (
829
- liveSkillsIdentity !== null &&
830
- adminPreservedSkillsIdentity(stageRoot) !== liveSkillsIdentity
831
- ) {
832
- rmSync(stageRoot, { recursive: true, force: true });
833
- return { status: "RESTAGE", stagedAuthority: finalAuthority };
834
- }
835
769
  const stagedSoulPath = join(stageRoot, "SOUL.md");
836
770
  const stagedSoul = lstatSync(stagedSoulPath);
837
771
  const stagedSoulBytes = readFileSync(stagedSoulPath);
@@ -6,7 +6,7 @@ USER root
6
6
 
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.858
8
8
  ARG MCP_INTEGRITY=sha512-8yiUhwMbBk/KxZAXGXTsEHiN8XeB/qCqNgfApew0rhViJqiLlDN8oS67pZW5iB5gdmeuv4du0jx9Lhq/blr9hQ==
9
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.578-wip.phase170.20260809.2
9
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.577
10
10
  ARG TUS_JS_CLIENT_VERSION=4.3.1
11
11
  ARG HERMES_VERSION=0.20.0
12
12
  ARG TIRITH_VERSION=0.3.3
@@ -59,7 +59,7 @@ RUN --mount=type=secret,id=mcp_trust_root,required=true \
59
59
  /etc/cont-init.d/02-reconcile-profiles \
60
60
  && npm cache clean --force
61
61
 
62
- COPY mcp-context-proxy.mjs mcp-runtime-binding.mjs fly-customer-model.mjs fly-customer-worker.mjs profile-materializer.mjs hermes-bridge.mjs hermes-memory-snapshot.mjs fly-runtime-identity.mjs fly-soul-bridge.mjs fly-soul-bridge-exec.mjs fly-skills-bridge.mjs fly-skills-bridge-exec.mjs soul-artifact-validator.mjs model-auth-reconciler.mjs model-auth-canary-reseed.mjs model-health-reporter.mjs \
62
+ COPY mcp-context-proxy.mjs mcp-runtime-binding.mjs fly-customer-model.mjs fly-customer-worker.mjs profile-materializer.mjs hermes-bridge.mjs hermes-memory-snapshot.mjs fly-runtime-identity.mjs fly-soul-bridge.mjs fly-soul-bridge-exec.mjs soul-artifact-validator.mjs model-auth-reconciler.mjs model-auth-canary-reseed.mjs model-health-reporter.mjs \
63
63
  /usr/local/lib/sellable-agent/
64
64
  COPY hermes-memory-dirty.mjs /opt/sellable/bin/hermes-memory-dirty
65
65
  COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
@@ -86,18 +86,9 @@ RUN node --input-type=module -e \
86
86
  'import("file:///usr/local/lib/sellable-agent/fly-soul-bridge-exec.mjs").then(({ runFlySoulBridgeExecCli }) => runFlySoulBridgeExecCli());' \
87
87
  > /usr/local/bin/sellable-soul-bridge-exec \
88
88
  && chmod 0555 /usr/local/bin/sellable-soul-bridge-exec \
89
- && printf '%s\n' \
90
- '#!/usr/bin/env node' \
91
- 'import("file:///usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs").then(({ runFlySkillsBridgeExecCli }) => runFlySkillsBridgeExecCli());' \
92
- > /usr/local/bin/sellable-skills-bridge-exec \
93
- && chmod 0555 /usr/local/bin/sellable-skills-bridge-exec \
94
89
  && sha256sum /usr/local/lib/sellable-agent/fly-soul-bridge.mjs \
95
90
  > /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
96
- && sha256sum /usr/local/lib/sellable-agent/fly-skills-bridge.mjs \
97
- /usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs \
98
- > /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
99
91
  && chmod 0444 /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
100
- /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
101
92
  && chmod 0755 /etc \
102
93
  && chmod 0555 /usr/local/lib/sellable-agent/*.mjs \
103
94
  && chmod 0555 /opt/sellable/bin/hermes-memory-dirty /opt/sellable/bin/hermes-endpoint-cron \
@@ -93,11 +93,9 @@ const RUNTIME_GENERATION = Number(
93
93
  const BOOT_SESSION_ID = randomUUID();
94
94
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.0";
95
95
  const INSTALLER_PACKAGE =
96
- process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ??
97
- "@sellable/install@0.1.578-wip.phase170.20260809.2";
96
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.577";
98
97
  const MCP_PACKAGE =
99
- process.env.SELLABLE_AGENT_MCP_PACKAGE ??
100
- "@sellable/mcp@0.1.858";
98
+ process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.858";
101
99
 
102
100
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
103
101
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -1639,6 +1637,16 @@ function writeRuntimeHermesConfig({
1639
1637
  );
1640
1638
  const sellable = baseConfig.mcp_servers?.sellable;
1641
1639
  if (!sellable?.env) throw new Error("customer_mcp_config_rejected");
1640
+ const adminServer = baseConfig.mcp_servers?.["sellable-admin"];
1641
+ if (adminServer?.command && existsSync(adminServer.command)) {
1642
+ // The admin MCP installer has shipped this wrapper without an execute
1643
+ // bit; repair it here so a config refresh never revives a dead admin
1644
+ // server entry.
1645
+ const wrapperMode = lstatSync(adminServer.command).mode;
1646
+ if ((wrapperMode & 0o100) === 0) {
1647
+ chmodSync(adminServer.command, wrapperMode | 0o100);
1648
+ }
1649
+ }
1642
1650
  // The named-profile materializer owns the one canonical SOUL file and seeds
1643
1651
  // it only when absent. Runtime config refreshes must never rewrite live SOUL
1644
1652
  // or create a root-level second copy.
@@ -1699,6 +1707,23 @@ function writeRuntimeHermesConfig({
1699
1707
  : {
1700
1708
  ...modelConfig,
1701
1709
  }),
1710
+ provider_routing: {
1711
+ // OpenRouter provider selection defaults to highest throughput; an
1712
+ // explicit profile value always wins.
1713
+ sort: "throughput",
1714
+ ...(baseConfig.provider_routing ?? {}),
1715
+ },
1716
+ auxiliary: {
1717
+ ...(baseConfig.auxiliary ?? {}),
1718
+ vision: {
1719
+ // Non-vision main models (e.g. deepseek) need a working vision
1720
+ // auxiliary; the Nous portal has no credentials on customer
1721
+ // machines, so image input silently fails without this default.
1722
+ provider: "openrouter",
1723
+ model: "google/gemini-3.5-flash",
1724
+ ...(baseConfig.auxiliary?.vision ?? {}),
1725
+ },
1726
+ },
1702
1727
  mcp_servers: {
1703
1728
  ...baseConfig.mcp_servers,
1704
1729
  sellable: {
@@ -297,6 +297,15 @@ const MCP_META_HELPER = `
297
297
  def _sellable_agent_call_meta():
298
298
  if os.getenv("SELLABLE_AGENT_RUNTIME", "") != "1":
299
299
  return None
300
+ from gateway.session_context import get_session_env, session_context_engaged
301
+ if (
302
+ not session_context_engaged()
303
+ or get_session_env("HERMES_SESSION_PLATFORM", "").strip().lower() != "slack"
304
+ ):
305
+ # Non-Slack runtime sessions (console/TUI, cron) carry no Slack actor
306
+ # identity. Send no actor meta; the serving API enforces its own actor
307
+ # gates, as with installs outside the Sellable runtime.
308
+ return None
300
309
  import json
301
310
  gate_path = os.getenv("SELLABLE_AGENT_RUNTIME_GATE_FILE", "")
302
311
  if not gate_path.startswith("/"):
@@ -376,8 +385,18 @@ def _sellable_agent_restore_or_build_proven_soul(
376
385
  expected_home = data_root / "profiles" / profile_id
377
386
  try:
378
387
  observed_home = Path(get_hermes_home())
379
- if observed_home.resolve(strict=True) != expected_home.resolve(strict=True):
380
- raise RuntimeError("Hermes resolved a different SOUL profile")
388
+ observed_resolved = observed_home.resolve(strict=True)
389
+ expected_resolved = expected_home.resolve(strict=True)
390
+ if observed_resolved != expected_resolved:
391
+ # HERMES_HOME may legitimately be the data root (the directory
392
+ # that CONTAINS profiles/<id>) when no per-session override is
393
+ # active yet -- e.g. brand-new sessions in hermes serve. Accept
394
+ # it only when root/profiles/<profile_id> strictly resolves to
395
+ # the canonical profile directory.
396
+ if (
397
+ observed_home / "profiles" / profile_id
398
+ ).resolve(strict=True) != expected_resolved:
399
+ raise RuntimeError("Hermes resolved a different SOUL profile")
381
400
  except OSError as exc:
382
401
  raise RuntimeError("Hermes SOUL profile cannot be resolved") from exc
383
402
 
@@ -574,6 +593,15 @@ const MCP_ENDPOINT_META_HELPER = `
574
593
  def _sellable_agent_call_meta():
575
594
  if os.getenv("SELLABLE_AGENT_RUNTIME", "") != "1":
576
595
  return None
596
+ from gateway.session_context import get_session_env, session_context_engaged
597
+ if (
598
+ not session_context_engaged()
599
+ or get_session_env("HERMES_SESSION_PLATFORM", "").strip().lower() != "slack"
600
+ ):
601
+ # Non-Slack runtime sessions (console/TUI, cron) carry no Slack actor
602
+ # identity. Send no actor meta; the serving API enforces its own actor
603
+ # gates, as with installs outside the Sellable runtime.
604
+ return None
577
605
  import json
578
606
  gate_path = os.getenv("SELLABLE_AGENT_RUNTIME_GATE_FILE", "")
579
607
  if not gate_path.startswith("/"):
@@ -634,15 +662,11 @@ const MCP_ENDPOINT_META_HELPER_OPTIONAL_SESSION =
634
662
  );
635
663
 
636
664
  const CRON_MCP_ISOLATION_HELPER = `
637
- # ${HERMES_AGENT_BRIDGE_MARKER}: cron is never a Sellable MCP principal.
665
+ # ${HERMES_AGENT_BRIDGE_MARKER}: cron sessions may use Sellable MCP toolsets.
638
666
  def _sellable_agent_strip_cron_mcp(toolsets, cfg):
639
- values = list(toolsets or [])
640
- if os.getenv("SELLABLE_AGENT_RUNTIME", "") != "1":
641
- return values
642
- from hermes_cli.tools_config import enabled_mcp_server_names
643
- mcp_names = set(enabled_mcp_server_names(cfg or {}))
644
- mcp_names.add("mcp")
645
- return [name for name in values if name not in mcp_names]
667
+ # Pass-through, kept for injection-site stability: cron sessions carry no
668
+ # Slack actor meta and the serving API enforces its own actor gates.
669
+ return list(toolsets or [])
646
670
  `;
647
671
 
648
672
  const V020_WEB_TOOLSET_DISCOVERY_HELPER = `
@@ -1569,125 +1593,6 @@ export function patchHermesSourceFile(patchKind, source) {
1569
1593
  ' return _sellable_agent_strip_cron_mcp(\n sorted(_get_platform_tools(cfg or {}, "cron")), cfg or {}\n )',
1570
1594
  "v0.20 platform cron MCP isolation"
1571
1595
  );
1572
- patched = replaceExactlyOnce(
1573
- patched,
1574
- " return None\n\n# Valid delivery platforms",
1575
- [
1576
- ' if os.getenv("SELLABLE_AGENT_RUNTIME", "") == "1":',
1577
- ' return _sellable_agent_strip_cron_mcp(["safe"], cfg or {})',
1578
- " return None",
1579
- "",
1580
- "# Valid delivery platforms",
1581
- ].join("\n"),
1582
- "v0.20 cron MCP fail-closed fallback"
1583
- );
1584
- return patched;
1585
- }
1586
-
1587
- if (patchKind === "skills-cache-freshness-v020") {
1588
- let patched = replaceExactlyOnce(
1589
- source,
1590
- " sig.append((str(d), m))",
1591
- [
1592
- " skill_files = []",
1593
- " for root, dirs, files in os.walk(d, followlinks=False):",
1594
- " dirs[:] = sorted(",
1595
- " name for name in dirs",
1596
- " if not os.path.islink(os.path.join(root, name))",
1597
- " )",
1598
- ' if "SKILL.md" not in files:',
1599
- " continue",
1600
- ' skill_path = os.path.join(root, "SKILL.md")',
1601
- " try:",
1602
- " skill_stat = os.stat(skill_path, follow_symlinks=False)",
1603
- " skill_files.append(",
1604
- " (skill_path, skill_stat.st_mtime_ns, skill_stat.st_size)",
1605
- " )",
1606
- " except OSError:",
1607
- " continue",
1608
- " sig.append((str(d), m, tuple(skill_files)))",
1609
- ].join("\n"),
1610
- "v0.20 live SKILL.md signature"
1611
- );
1612
- patched = replaceExactlyOnce(
1613
- patched,
1614
- " disabled = set() if skip_disabled else _get_disabled_skill_names()",
1615
- [
1616
- " disabled = _get_disabled_skill_names()",
1617
- " filtered_disabled = set() if skip_disabled else disabled",
1618
- ].join("\n"),
1619
- "v0.20 shared disabled signature"
1620
- );
1621
- patched = replaceExactlyOnce(
1622
- patched,
1623
- " if name in disabled:\n continue",
1624
- " if name in filtered_disabled:\n continue",
1625
- "v0.20 filtered disabled behavior"
1626
- );
1627
- patched = replaceExactlyOnce(
1628
- patched,
1629
- " skills = []\n seen_names: set = set()",
1630
- [
1631
- " # A signature change invalidates both filtered and unfiltered",
1632
- " # variants together; neither waits for the 30-second TTL.",
1633
- " if cached is not None and cached[0] != signature:",
1634
- " _SKILLS_CACHE.clear()",
1635
- "",
1636
- " skills = []",
1637
- " seen_names: set = set()",
1638
- ].join("\n"),
1639
- "v0.20 both-variant cache invalidation"
1640
- );
1641
- return patched;
1642
- }
1643
-
1644
- if (patchKind === "skills-prompt-freshness-v020") {
1645
- let patched = replaceExactlyOnce(
1646
- source,
1647
- "_SKILLS_PROMPT_CACHE_LOCK = threading.Lock()",
1648
- [
1649
- "_SKILLS_PROMPT_CACHE_LOCK = threading.Lock()",
1650
- "_SELLABLE_AGENT_SKILLS_PROMPT_SIGNATURE = None",
1651
- ].join("\n"),
1652
- "v0.20 prompt signature state"
1653
- );
1654
- patched = replaceExactlyOnce(
1655
- patched,
1656
- " disabled = get_disabled_skill_names(_platform_hint or None)\n cache_key = (",
1657
- [
1658
- " disabled = get_disabled_skill_names(_platform_hint or None)",
1659
- " signature_roots = [skills_dir, *external_dirs]",
1660
- " live_signature = tuple(",
1661
- " (",
1662
- " str(root),",
1663
- " tuple(",
1664
- " (path, tuple(values))",
1665
- " for path, values in sorted(_build_skills_manifest(root).items())",
1666
- " ),",
1667
- " )",
1668
- " for root in signature_roots",
1669
- " if root.exists()",
1670
- " )",
1671
- " cache_key = (",
1672
- ].join("\n"),
1673
- "v0.20 prompt live signature"
1674
- );
1675
- patched = replaceExactlyOnce(
1676
- patched,
1677
- " tuple(sorted(compact_categories or ())),\n )\n with _SKILLS_PROMPT_CACHE_LOCK:\n cached = _SKILLS_PROMPT_CACHE.get(cache_key)",
1678
- [
1679
- " tuple(sorted(compact_categories or ())),",
1680
- " live_signature,",
1681
- " )",
1682
- " global _SELLABLE_AGENT_SKILLS_PROMPT_SIGNATURE",
1683
- " with _SKILLS_PROMPT_CACHE_LOCK:",
1684
- " if _SELLABLE_AGENT_SKILLS_PROMPT_SIGNATURE != live_signature:",
1685
- " _SKILLS_PROMPT_CACHE.clear()",
1686
- " _SELLABLE_AGENT_SKILLS_PROMPT_SIGNATURE = live_signature",
1687
- " cached = _SKILLS_PROMPT_CACHE.get(cache_key)",
1688
- ].join("\n"),
1689
- "v0.20 prompt cache invalidation"
1690
- );
1691
1596
  return patched;
1692
1597
  }
1693
1598
 
@@ -1949,22 +1854,6 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V020_CONTRACT = normalizeContract({
1949
1854
  patchedSha256:
1950
1855
  "90c98514a8c6c4078dacdd67a92cf749bd174ec2c074226163c6cb07ca3df59f",
1951
1856
  },
1952
- {
1953
- path: "tools/skills_tool.py",
1954
- patchKind: "skills-cache-freshness-v020",
1955
- sourceSha256:
1956
- "6cfe456c8872c91309083694290e5e2cbdb64a1b2f4ac9c2cb38973fab2228ab",
1957
- patchedSha256:
1958
- "9a1b1417879d9aac6d5b6e1713237193009e2d67dbc691db35ef5940e94f9397",
1959
- },
1960
- {
1961
- path: "agent/prompt_builder.py",
1962
- patchKind: "skills-prompt-freshness-v020",
1963
- sourceSha256:
1964
- "2a8a8b2b6c0ff830349f68b5c2f26bf2c9871ae03caf516cff3628c00b5769c7",
1965
- patchedSha256:
1966
- "4a55eb6dd4e79d35fca9fd02544444576c892ca02dfebb0c97954156cc470fdb",
1967
- },
1968
1857
  ],
1969
1858
  });
1970
1859
 
@@ -39,7 +39,7 @@ 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
41
  const INSTALLER_PACKAGE =
42
- "@sellable/install@0.1.578-wip.phase170.20260809.2";
42
+ "@sellable/install@0.1.577";
43
43
  const MCP_PACKAGE = "@sellable/mcp@0.1.858";
44
44
  const HERMES_VERSION = "0.18.0";
45
45
  const SHA256 = /^[a-f0-9]{64}$/;
@@ -29,7 +29,6 @@ import {
29
29
  relative,
30
30
  resolve,
31
31
  } from "node:path";
32
- import { readSkillsProofObservation } from "./fly-skills-bridge.mjs";
33
32
  import {
34
33
  commitRetainedProfilePromotion,
35
34
  computeAgentProfileDigest,
@@ -78,11 +77,6 @@ const SECRET_PATTERN =
78
77
  /sat_[A-Za-z0-9_-]+|xox[bap]-[A-Za-z0-9-]+|asec\.v1\.|authorization[_-]?code|access[_-]?token|refresh[_-]?token/i;
79
78
  const STATE_FILE = "worker-state.json";
80
79
 
81
- export function attachSkillsProofObservation(observed, profileRoot) {
82
- const skills = readSkillsProofObservation(profileRoot);
83
- return skills ? { ...observed, skills } : observed;
84
- }
85
-
86
80
  function stableJson(value) {
87
81
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
88
82
  if (value && typeof value === "object") {
@@ -1546,8 +1540,10 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1546
1540
  ]) ||
1547
1541
  pinned.hermesCli !== "hermes" ||
1548
1542
  pinned.hermesVersion !== "0.18.0" ||
1549
- pinned.installerPackage !== "@sellable/install@0.1.578-wip.phase170.20260809.2" ||
1550
- pinned.mcpPackage !== "@sellable/mcp@0.1.858" ||
1543
+ pinned.installerPackage !==
1544
+ "@sellable/install@0.1.577" ||
1545
+ pinned.mcpPackage !==
1546
+ "@sellable/mcp@0.1.858" ||
1551
1547
  !Array.isArray(policy.toolInclude) ||
1552
1548
  policy.toolInclude.length === 0 ||
1553
1549
  !Number.isSafeInteger(slack.generation) ||
@@ -2352,10 +2348,7 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2352
2348
  observed.process.uid !== config.runtimeUid
2353
2349
  )
2354
2350
  throw new Error("runtime observation identity rejected");
2355
- return attachSkillsProofObservation(
2356
- observed,
2357
- join(realpathSync(config.profilesRoot), context.profileId)
2358
- );
2351
+ return observed;
2359
2352
  };
2360
2353
  const ensureRefreshTarget = async () => {
2361
2354
  if (refreshPrepared) return;