@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.
- package/container/Dockerfile +7 -7
- package/container/README.md +2 -2
- package/lib/sellable-agent/external-runtime-builder.mjs +1 -1
- package/lib/sellable-agent/fly-admin-image/Dockerfile +13 -0
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +97 -1
- package/lib/sellable-agent/fly-customer-image/Dockerfile +13 -4
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +29 -4
- package/lib/sellable-agent/fly-skills-bridge-exec.mjs +45 -0
- package/lib/sellable-agent/fly-skills-bridge.mjs +977 -0
- package/lib/sellable-agent/hermes-bridge.mjs +154 -19
- package/lib/sellable-agent/host-bootstrap.mjs +2 -2
- package/lib/sellable-agent/host-worker.mjs +12 -5
- package/lib/sellable-agent/profile-materializer.mjs +169 -14
- package/lib/sellable-agent/provisioning-adapter.mjs +5 -8
- package/package.json +1 -1
|
@@ -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
|
-
|
|
380
|
-
|
|
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
|
|
665
|
+
# ${HERMES_AGENT_BRIDGE_MARKER}: cron sessions may use Sellable MCP toolsets.
|
|
638
666
|
def _sellable_agent_strip_cron_mcp(toolsets, cfg):
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
"
|
|
1626
|
+
" disabled = set() if skip_disabled else _get_disabled_skill_names()",
|
|
1575
1627
|
[
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
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
|
-
"
|
|
1648
|
+
" skills = []",
|
|
1649
|
+
" seen_names: set = set()",
|
|
1581
1650
|
].join("\n"),
|
|
1582
|
-
"v0.20
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
43
|
-
const MCP_PACKAGE = "@sellable/mcp@0.1.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
620
|
-
|
|
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
|
-
|
|
649
|
-
|
|
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(
|
|
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) !==
|
|
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
|
-
|
|
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
|
-
!
|
|
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
|
-
|
|
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.
|
|
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.
|
|
876
|
+
installVersion: "0.1.581",
|
|
880
877
|
mcpPackage: PINNED_MCP_PACKAGE,
|
|
881
878
|
command: "hermes profile bootstrap",
|
|
882
879
|
},
|