@sellable/install 0.1.578 → 0.1.581

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.
@@ -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,17 +1593,112 @@ 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
  );
1596
+ return patched;
1597
+ }
1598
+
1599
+ if (patchKind === "skills-cache-freshness-v020") {
1600
+ let patched = replaceExactlyOnce(
1601
+ source,
1602
+ " sig.append((str(d), m))",
1603
+ [
1604
+ " skill_files = []",
1605
+ " for root, dirs, files in os.walk(d, followlinks=False):",
1606
+ " dirs[:] = sorted(",
1607
+ " name for name in dirs",
1608
+ " if not os.path.islink(os.path.join(root, name))",
1609
+ " )",
1610
+ ' if "SKILL.md" not in files:',
1611
+ " continue",
1612
+ ' skill_path = os.path.join(root, "SKILL.md")',
1613
+ " try:",
1614
+ " skill_stat = os.stat(skill_path, follow_symlinks=False)",
1615
+ " skill_files.append(",
1616
+ " (skill_path, skill_stat.st_mtime_ns, skill_stat.st_size)",
1617
+ " )",
1618
+ " except OSError:",
1619
+ " continue",
1620
+ " sig.append((str(d), m, tuple(skill_files)))",
1621
+ ].join("\n"),
1622
+ "v0.20 live SKILL.md signature"
1623
+ );
1572
1624
  patched = replaceExactlyOnce(
1573
1625
  patched,
1574
- " return None\n\n# Valid delivery platforms",
1626
+ " disabled = set() if skip_disabled else _get_disabled_skill_names()",
1575
1627
  [
1576
- ' if os.getenv("SELLABLE_AGENT_RUNTIME", "") == "1":',
1577
- ' return _sellable_agent_strip_cron_mcp(["safe"], cfg or {})',
1578
- " return None",
1628
+ " disabled = _get_disabled_skill_names()",
1629
+ " filtered_disabled = set() if skip_disabled else disabled",
1630
+ ].join("\n"),
1631
+ "v0.20 shared disabled signature"
1632
+ );
1633
+ patched = replaceExactlyOnce(
1634
+ patched,
1635
+ " if name in disabled:\n continue",
1636
+ " if name in filtered_disabled:\n continue",
1637
+ "v0.20 filtered disabled behavior"
1638
+ );
1639
+ patched = replaceExactlyOnce(
1640
+ patched,
1641
+ " skills = []\n seen_names: set = set()",
1642
+ [
1643
+ " # A signature change invalidates both filtered and unfiltered",
1644
+ " # variants together; neither waits for the 30-second TTL.",
1645
+ " if cached is not None and cached[0] != signature:",
1646
+ " _SKILLS_CACHE.clear()",
1579
1647
  "",
1580
- "# Valid delivery platforms",
1648
+ " skills = []",
1649
+ " seen_names: set = set()",
1581
1650
  ].join("\n"),
1582
- "v0.20 cron MCP fail-closed fallback"
1651
+ "v0.20 both-variant cache invalidation"
1652
+ );
1653
+ return patched;
1654
+ }
1655
+
1656
+ if (patchKind === "skills-prompt-freshness-v020") {
1657
+ let patched = replaceExactlyOnce(
1658
+ source,
1659
+ "_SKILLS_PROMPT_CACHE_LOCK = threading.Lock()",
1660
+ [
1661
+ "_SKILLS_PROMPT_CACHE_LOCK = threading.Lock()",
1662
+ "_SELLABLE_AGENT_SKILLS_PROMPT_SIGNATURE = None",
1663
+ ].join("\n"),
1664
+ "v0.20 prompt signature state"
1665
+ );
1666
+ patched = replaceExactlyOnce(
1667
+ patched,
1668
+ " disabled = get_disabled_skill_names(_platform_hint or None)\n cache_key = (",
1669
+ [
1670
+ " disabled = get_disabled_skill_names(_platform_hint or None)",
1671
+ " signature_roots = [skills_dir, *external_dirs]",
1672
+ " live_signature = tuple(",
1673
+ " (",
1674
+ " str(root),",
1675
+ " tuple(",
1676
+ " (path, tuple(values))",
1677
+ " for path, values in sorted(_build_skills_manifest(root).items())",
1678
+ " ),",
1679
+ " )",
1680
+ " for root in signature_roots",
1681
+ " if root.exists()",
1682
+ " )",
1683
+ " cache_key = (",
1684
+ ].join("\n"),
1685
+ "v0.20 prompt live signature"
1686
+ );
1687
+ patched = replaceExactlyOnce(
1688
+ patched,
1689
+ " tuple(sorted(compact_categories or ())),\n )\n with _SKILLS_PROMPT_CACHE_LOCK:\n cached = _SKILLS_PROMPT_CACHE.get(cache_key)",
1690
+ [
1691
+ " tuple(sorted(compact_categories or ())),",
1692
+ " live_signature,",
1693
+ " )",
1694
+ " global _SELLABLE_AGENT_SKILLS_PROMPT_SIGNATURE",
1695
+ " with _SKILLS_PROMPT_CACHE_LOCK:",
1696
+ " if _SELLABLE_AGENT_SKILLS_PROMPT_SIGNATURE != live_signature:",
1697
+ " _SKILLS_PROMPT_CACHE.clear()",
1698
+ " _SELLABLE_AGENT_SKILLS_PROMPT_SIGNATURE = live_signature",
1699
+ " cached = _SKILLS_PROMPT_CACHE.get(cache_key)",
1700
+ ].join("\n"),
1701
+ "v0.20 prompt cache invalidation"
1583
1702
  );
1584
1703
  return patched;
1585
1704
  }
@@ -1808,7 +1927,7 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V020_CONTRACT = normalizeContract({
1808
1927
  sourceSha256:
1809
1928
  "f9d50eec7b98819fa91b8ce640c8a32eff0726e0f349832763852bd82bbbb958",
1810
1929
  patchedSha256:
1811
- "0622b793e72967f16688b059dbecc22ecd91b78623792be0af37a2cdeb19404b",
1930
+ "4b0bcc47a07a43402958bbd09524483f8adb370bcbfa6ffa99b63b4ca5b35ff1",
1812
1931
  },
1813
1932
  {
1814
1933
  path: "gateway/run.py",
@@ -1824,7 +1943,7 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V020_CONTRACT = normalizeContract({
1824
1943
  sourceSha256:
1825
1944
  "a0e136367b64007d7b49ea006ab0aa7dcc66b12134b512a463a03bd69fb8a90c",
1826
1945
  patchedSha256:
1827
- "670d05a11edf0f9174e84e6b8803ffc1f2522e138898a6f86cbfaff7e0e3e299",
1946
+ "5f19a22c832db2e93b3da0fe9ea62606968233aa964318f8f81b0ae3dc7c8a13",
1828
1947
  },
1829
1948
  {
1830
1949
  path: "hermes_cli/web_routers/tools.py",
@@ -1840,7 +1959,23 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V020_CONTRACT = normalizeContract({
1840
1959
  sourceSha256:
1841
1960
  "aba4c2b9c8691ccc86518cfa10dcd92e55d7b2103c1c6807f54ddf58919f3f48",
1842
1961
  patchedSha256:
1843
- "90c98514a8c6c4078dacdd67a92cf749bd174ec2c074226163c6cb07ca3df59f",
1962
+ "8302998c684a290eae1dd8c34f93c790ced764340d72d52995021b15625f0acb",
1963
+ },
1964
+ {
1965
+ path: "tools/skills_tool.py",
1966
+ patchKind: "skills-cache-freshness-v020",
1967
+ sourceSha256:
1968
+ "6cfe456c8872c91309083694290e5e2cbdb64a1b2f4ac9c2cb38973fab2228ab",
1969
+ patchedSha256:
1970
+ "9a1b1417879d9aac6d5b6e1713237193009e2d67dbc691db35ef5940e94f9397",
1971
+ },
1972
+ {
1973
+ path: "agent/prompt_builder.py",
1974
+ patchKind: "skills-prompt-freshness-v020",
1975
+ sourceSha256:
1976
+ "2a8a8b2b6c0ff830349f68b5c2f26bf2c9871ae03caf516cff3628c00b5769c7",
1977
+ patchedSha256:
1978
+ "4a55eb6dd4e79d35fca9fd02544444576c892ca02dfebb0c97954156cc470fdb",
1844
1979
  },
1845
1980
  ],
1846
1981
  });
@@ -39,8 +39,8 @@ 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.577";
43
- const MCP_PACKAGE = "@sellable/mcp@0.1.858";
42
+ "@sellable/install@0.1.581";
43
+ const MCP_PACKAGE = "@sellable/mcp@0.1.859";
44
44
  const HERMES_VERSION = "0.18.0";
45
45
  const SHA256 = /^[a-f0-9]{64}$/;
46
46
  const SHA256_16 = /^[a-f0-9]{16}$/;
@@ -29,6 +29,7 @@ import {
29
29
  relative,
30
30
  resolve,
31
31
  } from "node:path";
32
+ import { readSkillsProofObservation } from "./fly-skills-bridge.mjs";
32
33
  import {
33
34
  commitRetainedProfilePromotion,
34
35
  computeAgentProfileDigest,
@@ -77,6 +78,11 @@ const SECRET_PATTERN =
77
78
  /sat_[A-Za-z0-9_-]+|xox[bap]-[A-Za-z0-9-]+|asec\.v1\.|authorization[_-]?code|access[_-]?token|refresh[_-]?token/i;
78
79
  const STATE_FILE = "worker-state.json";
79
80
 
81
+ export function attachSkillsProofObservation(observed, profileRoot) {
82
+ const skills = readSkillsProofObservation(profileRoot);
83
+ return skills ? { ...observed, skills } : observed;
84
+ }
85
+
80
86
  function stableJson(value) {
81
87
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
82
88
  if (value && typeof value === "object") {
@@ -1540,10 +1546,8 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1540
1546
  ]) ||
1541
1547
  pinned.hermesCli !== "hermes" ||
1542
1548
  pinned.hermesVersion !== "0.18.0" ||
1543
- pinned.installerPackage !==
1544
- "@sellable/install@0.1.577" ||
1545
- pinned.mcpPackage !==
1546
- "@sellable/mcp@0.1.858" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.581" ||
1550
+ pinned.mcpPackage !== "@sellable/mcp@0.1.859" ||
1547
1551
  !Array.isArray(policy.toolInclude) ||
1548
1552
  policy.toolInclude.length === 0 ||
1549
1553
  !Number.isSafeInteger(slack.generation) ||
@@ -2348,7 +2352,10 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
2348
2352
  observed.process.uid !== config.runtimeUid
2349
2353
  )
2350
2354
  throw new Error("runtime observation identity rejected");
2351
- return observed;
2355
+ return attachSkillsProofObservation(
2356
+ observed,
2357
+ join(realpathSync(config.profilesRoot), context.profileId)
2358
+ );
2352
2359
  };
2353
2360
  const ensureRefreshTarget = async () => {
2354
2361
  if (refreshPrepared) return;
@@ -201,10 +201,8 @@ function validateDesired(desired) {
201
201
  desired.serviceCredentialGeneration < 1 ||
202
202
  desired.hermesCli !== "hermes" ||
203
203
  desired.hermesVersion !== "0.18.0" ||
204
- desired.installerPackage !==
205
- "@sellable/install@0.1.577" ||
206
- desired.mcpPackage !==
207
- "@sellable/mcp@0.1.858" ||
204
+ desired.installerPackage !== "@sellable/install@0.1.581" ||
205
+ desired.mcpPackage !== "@sellable/mcp@0.1.859" ||
208
206
  !Array.isArray(desired.toolInclude) ||
209
207
  desired.toolInclude.length === 0 ||
210
208
  desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
@@ -408,6 +406,113 @@ function configFor(desired, profileId) {
408
406
  };
409
407
  }
410
408
 
409
+ const LIVE_SKILLS_CONFIG_KEYS = Object.freeze([
410
+ "disabled",
411
+ "platform_disabled",
412
+ ]);
413
+
414
+ function profileConfigWithoutLiveSkills(value) {
415
+ const normalized = structuredClone(value);
416
+ if (
417
+ normalized?.skills &&
418
+ typeof normalized.skills === "object" &&
419
+ !Array.isArray(normalized.skills)
420
+ ) {
421
+ for (const key of LIVE_SKILLS_CONFIG_KEYS) delete normalized.skills[key];
422
+ if (Object.keys(normalized.skills).length === 0) delete normalized.skills;
423
+ }
424
+ return normalized;
425
+ }
426
+
427
+ function normalizedProfileConfigBytes(pathValue) {
428
+ const value = JSON.parse(readFileSync(pathValue, "utf8"));
429
+ return Buffer.from(
430
+ `${JSON.stringify(profileConfigWithoutLiveSkills(value), null, 2)}\n`,
431
+ "utf8"
432
+ );
433
+ }
434
+
435
+ function mergeLiveSkillsConfig(sourceRoot, targetRoot) {
436
+ const sourcePath = join(sourceRoot, "config.yaml");
437
+ const targetPath = join(targetRoot, "config.yaml");
438
+ if (!existsSync(sourcePath) || !existsSync(targetPath)) return;
439
+ const source = JSON.parse(readFileSync(sourcePath, "utf8"));
440
+ const target = JSON.parse(readFileSync(targetPath, "utf8"));
441
+ const sourceSkills = source?.skills;
442
+ if (
443
+ !sourceSkills ||
444
+ typeof sourceSkills !== "object" ||
445
+ Array.isArray(sourceSkills)
446
+ ) {
447
+ return;
448
+ }
449
+ const preserved = Object.fromEntries(
450
+ LIVE_SKILLS_CONFIG_KEYS.filter((key) =>
451
+ Object.prototype.hasOwnProperty.call(sourceSkills, key)
452
+ ).map((key) => [key, structuredClone(sourceSkills[key])])
453
+ );
454
+ if (Object.keys(preserved).length === 0) return;
455
+ target.skills = {
456
+ ...(target.skills &&
457
+ typeof target.skills === "object" &&
458
+ !Array.isArray(target.skills)
459
+ ? target.skills
460
+ : {}),
461
+ ...preserved,
462
+ };
463
+ atomicWrite(
464
+ targetPath,
465
+ `${JSON.stringify(target, null, 2)}\n`,
466
+ lstatSync(targetPath).mode & 0o777
467
+ );
468
+ }
469
+
470
+ function mergeLiveSkillsState(sourceRoot, targetRoot) {
471
+ if (!existsSync(sourceRoot)) return;
472
+ const sourceSkills = join(sourceRoot, "skills");
473
+ const targetSkills = join(targetRoot, "skills");
474
+ if (existsSync(sourceSkills)) mergeHermesState(sourceSkills, targetSkills);
475
+ mergeLiveSkillsConfig(sourceRoot, targetRoot);
476
+ }
477
+
478
+ function preservedSkillsConfigIdentity(profileRoot) {
479
+ if (!existsSync(profileRoot)) return null;
480
+ const entries = [];
481
+ const skillsRoot = join(profileRoot, "skills");
482
+ const walk = (directory, relativePath = "") => {
483
+ for (const name of readdirSync(directory).sort()) {
484
+ const nextRelative = relativePath ? `${relativePath}/${name}` : name;
485
+ if (nextRelative === "sellable") continue;
486
+ const pathValue = join(directory, name);
487
+ const link = lstatSync(pathValue);
488
+ if (link.isSymbolicLink())
489
+ throw new Error("live skills symlink rejected");
490
+ if (link.isDirectory()) walk(pathValue, nextRelative);
491
+ else if (link.isFile()) {
492
+ entries.push({
493
+ path: nextRelative,
494
+ sha256: sha256(readFileSync(pathValue)),
495
+ });
496
+ } else throw new Error("live skills entry rejected");
497
+ }
498
+ };
499
+ if (existsSync(skillsRoot)) walk(skillsRoot);
500
+ const configPath = join(profileRoot, "config.yaml");
501
+ let disabled = {};
502
+ if (existsSync(configPath)) {
503
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
504
+ const skills = config?.skills;
505
+ if (skills && typeof skills === "object" && !Array.isArray(skills)) {
506
+ disabled = Object.fromEntries(
507
+ LIVE_SKILLS_CONFIG_KEYS.filter((key) =>
508
+ Object.prototype.hasOwnProperty.call(skills, key)
509
+ ).map((key) => [key, skills[key]])
510
+ );
511
+ }
512
+ }
513
+ return sha256(stableJson({ disabled, entries }));
514
+ }
515
+
411
516
  function mcpFor(desired, profileId) {
412
517
  return {
413
518
  package: desired.mcpPackage,
@@ -616,10 +721,8 @@ function validateRuntimeOwnership(observed, runtimeIdentity) {
616
721
  "skills/sellable",
617
722
  "skills/sellable/SKILL.md",
618
723
  ].sort();
619
- if (
620
- stableJson(observed.entries.map((entry) => entry.path).sort()) !==
621
- stableJson(expectedPaths)
622
- ) {
724
+ const observedPaths = observed.entries.map((entry) => entry.path);
725
+ if (expectedPaths.some((pathValue) => !observedPaths.includes(pathValue))) {
623
726
  return false;
624
727
  }
625
728
  return observed.entries.every(
@@ -644,9 +747,16 @@ export function validateAgentProfileBundle({ profileRoot, desired }) {
644
747
  return { ok: false, code: "profile_path_invalid" };
645
748
  }
646
749
  const foundInventory = inventory(profileRoot);
750
+ const serverOwned = [...AGENT_PROFILE_INVENTORY].sort();
647
751
  if (
648
- JSON.stringify(foundInventory) !==
649
- JSON.stringify([...AGENT_PROFILE_INVENTORY].sort())
752
+ serverOwned.some(
753
+ (logicalPath) => !foundInventory.includes(logicalPath)
754
+ ) ||
755
+ foundInventory.some(
756
+ (logicalPath) =>
757
+ !serverOwned.includes(logicalPath) &&
758
+ !logicalPath.startsWith("skills/")
759
+ )
650
760
  ) {
651
761
  return { ok: false, code: "profile_inventory_open" };
652
762
  }
@@ -667,7 +777,11 @@ export function validateAgentProfileBundle({ profileRoot, desired }) {
667
777
  .sort()
668
778
  .map((logicalPath) => [
669
779
  logicalPath,
670
- sha256(readFileSync(join(profileRoot, logicalPath))),
780
+ sha256(
781
+ logicalPath === "config.yaml"
782
+ ? normalizedProfileConfigBytes(join(profileRoot, logicalPath))
783
+ : readFileSync(join(profileRoot, logicalPath))
784
+ ),
671
785
  ])
672
786
  );
673
787
  if (stableJson(manifest.fileHashes) !== stableJson(expectedHashes)) {
@@ -686,7 +800,8 @@ export function validateAgentProfileBundle({ profileRoot, desired }) {
686
800
  readFileSync(join(profileRoot, "sellable", "mcp.json"), "utf8")
687
801
  );
688
802
  if (
689
- stableJson(config) !== stableJson(configFor(desired, manifest.profileId))
803
+ stableJson(profileConfigWithoutLiveSkills(config)) !==
804
+ stableJson(configFor(desired, manifest.profileId))
690
805
  ) {
691
806
  return { ok: false, code: "config_drift" };
692
807
  }
@@ -1625,7 +1740,7 @@ async function rollback({
1625
1740
  return outcome;
1626
1741
  }
1627
1742
 
1628
- export async function materializeAgentProfile(input = {}) {
1743
+ async function materializeAgentProfileLocked(input = {}) {
1629
1744
  const valid = validateDesired(input.desired);
1630
1745
  if (!valid.ok)
1631
1746
  return { ok: false, status: "DESIRED_REJECTED", code: valid.code };
@@ -1688,7 +1803,15 @@ export async function materializeAgentProfile(input = {}) {
1688
1803
  revisionId: desired.revisionId,
1689
1804
  fence: desired.fence,
1690
1805
  });
1806
+ const liveSkillsIdentity = preservedSkillsConfigIdentity(profileRoot);
1691
1807
  renderStagedProfile(stagingRoot, desired);
1808
+ mergeLiveSkillsState(profileRoot, stagingRoot);
1809
+ if (
1810
+ liveSkillsIdentity !== null &&
1811
+ preservedSkillsConfigIdentity(stagingRoot) !== liveSkillsIdentity
1812
+ ) {
1813
+ throw new Error("live skills identity changed during staging");
1814
+ }
1692
1815
  const staged = validateAgentProfileBundle({
1693
1816
  profileRoot: stagingRoot,
1694
1817
  desired,
@@ -2016,6 +2139,26 @@ export async function materializeAgentProfile(input = {}) {
2016
2139
  }
2017
2140
  }
2018
2141
 
2142
+ export async function materializeAgentProfile(input = {}) {
2143
+ const valid = validateDesired(input.desired);
2144
+ const root = traversableProfilesRoot(input.profilesRoot);
2145
+ if (!valid.ok || !root) return materializeAgentProfileLocked(input);
2146
+ const profileId = deriveContainedProfileId(
2147
+ input.desired.workspaceId,
2148
+ input.desired.agentId,
2149
+ input.desired.hostId
2150
+ );
2151
+ try {
2152
+ return await withProfileSoulLock(
2153
+ profileId,
2154
+ () => materializeAgentProfileLocked(input),
2155
+ { dataRoot: basename(root) === "profiles" ? dirname(root) : root }
2156
+ );
2157
+ } catch {
2158
+ return { ok: false, status: "LOCK_REJECTED", profileId };
2159
+ }
2160
+ }
2161
+
2019
2162
  export const NATIVE_SLACK_CUSTOMER_PROFILE_INVENTORY = Object.freeze([
2020
2163
  "agent-profile.json",
2021
2164
  "config.yaml",
@@ -2326,7 +2469,11 @@ export function validateNativeSlackCustomerProfile({ desired, profileRoot }) {
2326
2469
  link.isSymbolicLink() ||
2327
2470
  !link.isFile() ||
2328
2471
  (link.mode & 0o022) !== 0 ||
2329
- !readFileSync(pathValue).equals(Buffer.from(expected))
2472
+ !(logicalPath === "config.yaml"
2473
+ ? normalizedProfileConfigBytes(pathValue).equals(
2474
+ Buffer.from(expected)
2475
+ )
2476
+ : readFileSync(pathValue).equals(Buffer.from(expected)))
2330
2477
  ) {
2331
2478
  return { ok: false, code: "native_slack_profile_file_drift" };
2332
2479
  }
@@ -2393,6 +2540,7 @@ export function materializeNativeSlackCustomerProfile({
2393
2540
  return { ok: false, code: "native_slack_profile_path_rejected" };
2394
2541
  }
2395
2542
  let soulBytes = Buffer.from(desired.soul, "utf8");
2543
+ const liveSkillsIdentity = preservedSkillsConfigIdentity(activeRoot);
2396
2544
  if (existsSync(activeRoot)) {
2397
2545
  const soulPath = join(activeRoot, "SOUL.md");
2398
2546
  if (existsSync(soulPath)) {
@@ -2409,6 +2557,13 @@ export function materializeNativeSlackCustomerProfile({
2409
2557
  soulBytes
2410
2558
  );
2411
2559
  mergeHermesState(activeRoot, stagingRoot);
2560
+ mergeLiveSkillsConfig(activeRoot, stagingRoot);
2561
+ if (
2562
+ liveSkillsIdentity !== null &&
2563
+ preservedSkillsConfigIdentity(stagingRoot) !== liveSkillsIdentity
2564
+ ) {
2565
+ throw new Error("live skills identity changed during staging");
2566
+ }
2412
2567
  const staged = validateNativeSlackCustomerProfile({
2413
2568
  desired,
2414
2569
  profileRoot: stagingRoot,
@@ -25,10 +25,8 @@ import { fileURLToPath } from "node:url";
25
25
  import { deriveContainedProfileId } from "./profile-materializer.mjs";
26
26
 
27
27
  export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
28
- export const PINNED_INSTALL_PACKAGE =
29
- "@sellable/install@0.1.577";
30
- export const PINNED_MCP_PACKAGE =
31
- "@sellable/mcp@0.1.858";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.581";
29
+ export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.859";
32
30
 
33
31
  export function deriveAgentProfileId(workspaceId, agentId, hostId) {
34
32
  return deriveContainedProfileId(workspaceId, agentId, hostId);
@@ -613,8 +611,7 @@ function inspectProvisionedProfile({
613
611
  !matchesReadback(manifest, request) ||
614
612
  manifest.serviceCredentialReference !== serviceCredentialReference ||
615
613
  manifest.installerPackage !== PINNED_INSTALL_PACKAGE ||
616
- manifest.installerVersion !==
617
- "0.1.577" ||
614
+ manifest.installerVersion !== "0.1.581" ||
618
615
  manifest.mcpPackage !== PINNED_MCP_PACKAGE ||
619
616
  manifest.credentialKeyVersion !== credentialKeyVersion ||
620
617
  !/^[a-f0-9]{16}$/.test(manifest.credentialFingerprint) ||
@@ -828,7 +825,7 @@ function successReceipt({
828
825
  subject: `agent-profile:${observed.profileId}`,
829
826
  cli: {
830
827
  installPackage: PINNED_INSTALL_PACKAGE,
831
- installVersion: "0.1.577",
828
+ installVersion: "0.1.581",
832
829
  mcpPackage: PINNED_MCP_PACKAGE,
833
830
  command: "hermes profile bootstrap",
834
831
  },
@@ -876,7 +873,7 @@ function failure(code, stages, extra = {}, request = null) {
876
873
  : "agent-profile:unbound",
877
874
  cli: {
878
875
  installPackage: PINNED_INSTALL_PACKAGE,
879
- installVersion: "0.1.577",
876
+ installVersion: "0.1.581",
880
877
  mcpPackage: PINNED_MCP_PACKAGE,
881
878
  command: "hermes profile bootstrap",
882
879
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.578",
3
+ "version": "0.1.581",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {