@sellable/install 0.1.597 → 0.1.599
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 +4 -4
- package/container/README.md +2 -2
- package/lib/sellable-agent/fly-admin-image/Dockerfile +11 -0
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +40 -12
- package/lib/sellable-agent/fly-capabilities-bridge-exec.mjs +78 -0
- package/lib/sellable-agent/fly-capabilities-bridge.mjs +659 -0
- package/lib/sellable-agent/fly-customer-image/Dockerfile +11 -2
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +10 -8
- package/lib/sellable-agent/host-bootstrap.mjs +1 -1
- package/lib/sellable-agent/host-worker.mjs +1 -1
- package/lib/sellable-agent/profile-materializer.mjs +82 -38
- package/lib/sellable-agent/provisioning-adapter.mjs +1 -1
- package/package.json +1 -1
package/container/Dockerfile
CHANGED
|
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
6
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.599
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.860
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
9
|
ARG MCP_INTEGRITY=sha512-jRA1KsCV2d4Zcuo5/UmJiP8P0NaoC3ZDvUFRQOc2AMSNDVmUR6gWvtkyAiqb1lukbFWKzfVlz2/4EWzxvb7hkQ==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.599" \
|
|
12
12
|
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.860" \
|
|
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.597" \
|
|
|
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.
|
|
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.599' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.860') 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.
|
|
28
|
+
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.599', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.860', 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
|
package/container/README.md
CHANGED
|
@@ -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.
|
|
6
|
+
`@sellable/install@0.1.599` 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.
|
|
12
|
+
--tag sellable-agent-host:0.1.599 .
|
|
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`.
|
|
@@ -103,6 +103,8 @@ COPY fly-soul-bridge.mjs /usr/local/lib/sellable-agent/fly-soul-bridge.mjs
|
|
|
103
103
|
COPY fly-soul-bridge-exec.mjs /usr/local/lib/sellable-agent/fly-soul-bridge-exec.mjs
|
|
104
104
|
COPY fly-skills-bridge.mjs /usr/local/lib/sellable-agent/fly-skills-bridge.mjs
|
|
105
105
|
COPY fly-skills-bridge-exec.mjs /usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs
|
|
106
|
+
COPY fly-capabilities-bridge.mjs /usr/local/lib/sellable-agent/fly-capabilities-bridge.mjs
|
|
107
|
+
COPY fly-capabilities-bridge-exec.mjs /usr/local/lib/sellable-agent/fly-capabilities-bridge-exec.mjs
|
|
106
108
|
COPY soul-artifact-validator.mjs /usr/local/lib/sellable-agent/soul-artifact-validator.mjs
|
|
107
109
|
COPY hermes-memory-dirty.mjs /opt/sellable/bin/hermes-memory-dirty
|
|
108
110
|
COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
|
|
@@ -131,13 +133,22 @@ RUN node --input-type=module -e \
|
|
|
131
133
|
'import("file:///usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs").then(({ runFlySkillsBridgeExecCli }) => runFlySkillsBridgeExecCli());' \
|
|
132
134
|
> /usr/local/bin/sellable-skills-bridge-exec \
|
|
133
135
|
&& chmod 0555 /usr/local/bin/sellable-skills-bridge-exec \
|
|
136
|
+
&& printf '%s\n' \
|
|
137
|
+
'#!/usr/bin/env node' \
|
|
138
|
+
'import("file:///usr/local/lib/sellable-agent/fly-capabilities-bridge-exec.mjs").then(({ runFlyCapabilitiesBridgeExecCli }) => runFlyCapabilitiesBridgeExecCli());' \
|
|
139
|
+
> /usr/local/bin/sellable-capabilities-bridge-exec \
|
|
140
|
+
&& chmod 0555 /usr/local/bin/sellable-capabilities-bridge-exec \
|
|
134
141
|
&& sha256sum /usr/local/lib/sellable-agent/fly-soul-bridge.mjs \
|
|
135
142
|
> /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
|
|
136
143
|
&& sha256sum /usr/local/lib/sellable-agent/fly-skills-bridge.mjs \
|
|
137
144
|
/usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs \
|
|
138
145
|
> /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
|
|
146
|
+
&& sha256sum /usr/local/lib/sellable-agent/fly-capabilities-bridge.mjs \
|
|
147
|
+
/usr/local/lib/sellable-agent/fly-capabilities-bridge-exec.mjs \
|
|
148
|
+
> /usr/local/share/sellable-agent/capabilities-bridge-source.sha256 \
|
|
139
149
|
&& chmod 0444 /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
|
|
140
150
|
/usr/local/share/sellable-agent/skills-bridge-source.sha256 \
|
|
151
|
+
/usr/local/share/sellable-agent/capabilities-bridge-source.sha256 \
|
|
141
152
|
&& chmod 0755 /etc \
|
|
142
153
|
&& chmod 0555 \
|
|
143
154
|
/usr/local/lib/sellable-agent/admin-runtime/admin-runtime.mjs \
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
buildNativeHermesLaunch,
|
|
43
43
|
} from "../admin-artifact/node_modules/@sellable/install/lib/sellable-agent/fly-customer-worker.mjs";
|
|
44
44
|
import sodium from "../admin-artifact/node_modules/libsodium-wrappers/dist/modules-esm/libsodium-wrappers.mjs";
|
|
45
|
+
import { SELLABLE_ALLOWED_TOOLSETS } from "../fly-capabilities-bridge.mjs";
|
|
45
46
|
import {
|
|
46
47
|
claimWithSoulObservation,
|
|
47
48
|
createFlyCustomerControlClient,
|
|
@@ -696,6 +697,22 @@ const ADMIN_LIVE_SKILLS_CONFIG_KEYS = Object.freeze([
|
|
|
696
697
|
"platform_disabled",
|
|
697
698
|
]);
|
|
698
699
|
|
|
700
|
+
function adminPreservedSlackToolsets(config) {
|
|
701
|
+
const platformToolsets = config?.platform_toolsets;
|
|
702
|
+
if (
|
|
703
|
+
!platformToolsets ||
|
|
704
|
+
typeof platformToolsets !== "object" ||
|
|
705
|
+
Array.isArray(platformToolsets) ||
|
|
706
|
+
!Object.prototype.hasOwnProperty.call(platformToolsets, "slack")
|
|
707
|
+
)
|
|
708
|
+
return null;
|
|
709
|
+
const slack = Array.isArray(platformToolsets.slack)
|
|
710
|
+
? platformToolsets.slack
|
|
711
|
+
: [];
|
|
712
|
+
const allowed = new Set(SELLABLE_ALLOWED_TOOLSETS);
|
|
713
|
+
return [...new Set(slack.filter((name) => allowed.has(name)))].sort();
|
|
714
|
+
}
|
|
715
|
+
|
|
699
716
|
function adminPreservedSkillsIdentity(profileRoot) {
|
|
700
717
|
if (!existsSync(profileRoot)) return null;
|
|
701
718
|
const entries = [];
|
|
@@ -725,6 +742,7 @@ function adminPreservedSkillsIdentity(profileRoot) {
|
|
|
725
742
|
};
|
|
726
743
|
if (existsSync(skillsRoot)) walk(skillsRoot);
|
|
727
744
|
let disabled = {};
|
|
745
|
+
let slackToolsets = null;
|
|
728
746
|
if (existsSync(configPath)) {
|
|
729
747
|
const document = parseDocument(readFileSync(configPath, "utf8"));
|
|
730
748
|
if (document.errors.length > 0) {
|
|
@@ -738,8 +756,9 @@ function adminPreservedSkillsIdentity(profileRoot) {
|
|
|
738
756
|
).map((key) => [key, skills[key]])
|
|
739
757
|
);
|
|
740
758
|
}
|
|
759
|
+
slackToolsets = adminPreservedSlackToolsets(document.toJS());
|
|
741
760
|
}
|
|
742
|
-
return sha256(canonicalJson({ disabled, entries }));
|
|
761
|
+
return sha256(canonicalJson({ disabled, entries, slackToolsets }));
|
|
743
762
|
}
|
|
744
763
|
|
|
745
764
|
function mergeAdminLiveSkillsConfig(sourceRoot, targetRoot) {
|
|
@@ -751,19 +770,28 @@ function mergeAdminLiveSkillsConfig(sourceRoot, targetRoot) {
|
|
|
751
770
|
if (sourceDocument.errors.length > 0 || targetDocument.errors.length > 0) {
|
|
752
771
|
throw new Error("admin_runtime_live_skills_config_rejected");
|
|
753
772
|
}
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
773
|
+
const sourceConfig = sourceDocument.toJS();
|
|
774
|
+
const sourceSkills = sourceConfig?.skills;
|
|
775
|
+
if (sourceSkills !== undefined) {
|
|
776
|
+
if (
|
|
777
|
+
!sourceSkills ||
|
|
778
|
+
typeof sourceSkills !== "object" ||
|
|
779
|
+
Array.isArray(sourceSkills)
|
|
780
|
+
) {
|
|
781
|
+
throw new Error("admin_runtime_live_skills_config_rejected");
|
|
782
|
+
}
|
|
783
|
+
for (const key of ADMIN_LIVE_SKILLS_CONFIG_KEYS) {
|
|
784
|
+
if (Object.prototype.hasOwnProperty.call(sourceSkills, key)) {
|
|
785
|
+
targetDocument.setIn(
|
|
786
|
+
["skills", key],
|
|
787
|
+
structuredClone(sourceSkills[key])
|
|
788
|
+
);
|
|
789
|
+
}
|
|
765
790
|
}
|
|
766
791
|
}
|
|
792
|
+
const slackToolsets = adminPreservedSlackToolsets(sourceConfig);
|
|
793
|
+
if (slackToolsets !== null)
|
|
794
|
+
targetDocument.setIn(["platform_toolsets", "slack"], slackToolsets);
|
|
767
795
|
atomicPrivateFile(targetPath, targetDocument.toString());
|
|
768
796
|
}
|
|
769
797
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
CAPABILITIES_REQUEST_MAX_BYTES,
|
|
8
|
+
FlyCapabilitiesBridgeError,
|
|
9
|
+
executeExternalCapabilitiesBridgeOperation,
|
|
10
|
+
} from "./fly-capabilities-bridge.mjs";
|
|
11
|
+
|
|
12
|
+
export const FLY_CAPABILITIES_EXEC_ARGUMENT_MAX_BYTES = 96 * 1024;
|
|
13
|
+
export const FLY_CAPABILITIES_EXEC_ARGUMENT_MAX_COUNT = 2;
|
|
14
|
+
|
|
15
|
+
const BASE64URL_CHUNK = /^[A-Za-z0-9_-]+$/;
|
|
16
|
+
const ENCODED_MAX_BYTES = Math.ceil(CAPABILITIES_REQUEST_MAX_BYTES / 3) * 4;
|
|
17
|
+
|
|
18
|
+
export function decodeFlyCapabilitiesExecArguments(args) {
|
|
19
|
+
if (!Array.isArray(args) || args.length === 0)
|
|
20
|
+
throw new FlyCapabilitiesBridgeError("capabilities_request_rejected");
|
|
21
|
+
if (args.length > FLY_CAPABILITIES_EXEC_ARGUMENT_MAX_COUNT)
|
|
22
|
+
throw new FlyCapabilitiesBridgeError("capabilities_request_too_large");
|
|
23
|
+
let encoded = "";
|
|
24
|
+
for (const chunk of args) {
|
|
25
|
+
if (typeof chunk !== "string" || !BASE64URL_CHUNK.test(chunk))
|
|
26
|
+
throw new FlyCapabilitiesBridgeError("capabilities_request_rejected");
|
|
27
|
+
if (
|
|
28
|
+
Buffer.byteLength(chunk, "utf8") >
|
|
29
|
+
FLY_CAPABILITIES_EXEC_ARGUMENT_MAX_BYTES
|
|
30
|
+
)
|
|
31
|
+
throw new FlyCapabilitiesBridgeError("capabilities_request_too_large");
|
|
32
|
+
encoded += chunk;
|
|
33
|
+
if (encoded.length > ENCODED_MAX_BYTES)
|
|
34
|
+
throw new FlyCapabilitiesBridgeError("capabilities_request_too_large");
|
|
35
|
+
}
|
|
36
|
+
let bytes;
|
|
37
|
+
try {
|
|
38
|
+
bytes = Buffer.from(encoded, "base64url");
|
|
39
|
+
} catch {
|
|
40
|
+
throw new FlyCapabilitiesBridgeError("capabilities_request_rejected");
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
bytes.toString("base64url") !== encoded ||
|
|
44
|
+
bytes.byteLength > CAPABILITIES_REQUEST_MAX_BYTES
|
|
45
|
+
)
|
|
46
|
+
throw new FlyCapabilitiesBridgeError("capabilities_request_rejected");
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
49
|
+
} catch {
|
|
50
|
+
throw new FlyCapabilitiesBridgeError("capabilities_request_rejected");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function runFlyCapabilitiesBridgeExecCli() {
|
|
55
|
+
try {
|
|
56
|
+
const request = decodeFlyCapabilitiesExecArguments(process.argv.slice(2));
|
|
57
|
+
const result = executeExternalCapabilitiesBridgeOperation(request);
|
|
58
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
59
|
+
process.exitCode = result.ok ? 0 : 2;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
`${JSON.stringify({
|
|
63
|
+
ok: false,
|
|
64
|
+
code:
|
|
65
|
+
error instanceof FlyCapabilitiesBridgeError
|
|
66
|
+
? error.code
|
|
67
|
+
: "capabilities_bridge_failed",
|
|
68
|
+
})}\n`
|
|
69
|
+
);
|
|
70
|
+
process.exitCode = 2;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
resolve(process.argv[1] ?? "") === resolve(fileURLToPath(import.meta.url))
|
|
76
|
+
) {
|
|
77
|
+
await runFlyCapabilitiesBridgeExecCli();
|
|
78
|
+
}
|
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
closeSync,
|
|
7
|
+
constants,
|
|
8
|
+
existsSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
openSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { isAbsolute, join } from "node:path";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
SOUL_BRIDGE_DATA_ROOT,
|
|
23
|
+
discoverLiveHermesGateway,
|
|
24
|
+
revalidateLiveHermesGateway,
|
|
25
|
+
soulProfilePaths,
|
|
26
|
+
withProfileSoulLock,
|
|
27
|
+
} from "./fly-soul-bridge.mjs";
|
|
28
|
+
import { markHermesSnapshotDirty } from "./hermes-memory-snapshot.mjs";
|
|
29
|
+
|
|
30
|
+
export const CAPABILITIES_BRIDGE_SCHEMA_VERSION =
|
|
31
|
+
"sellable-agent-capabilities-bridge/v1";
|
|
32
|
+
export const CAPABILITIES_PROOF_SCHEMA_VERSION =
|
|
33
|
+
"sellable-agent-capabilities-proof/v1";
|
|
34
|
+
export const CAPABILITIES_REQUEST_MAX_BYTES = 16 * 1024;
|
|
35
|
+
|
|
36
|
+
// Deliberately excludes credential-bearing, external-account, platform-admin,
|
|
37
|
+
// media-generation, and default-off toolsets. These are the already-bundled
|
|
38
|
+
// core capabilities a Sellable Agent operator may narrow or restore for Slack.
|
|
39
|
+
export const SELLABLE_ALLOWED_TOOLSETS = Object.freeze([
|
|
40
|
+
"clarify",
|
|
41
|
+
"code_execution",
|
|
42
|
+
"context_engine",
|
|
43
|
+
"cronjob",
|
|
44
|
+
"delegation",
|
|
45
|
+
"file",
|
|
46
|
+
"memory",
|
|
47
|
+
"session_search",
|
|
48
|
+
"skills",
|
|
49
|
+
"terminal",
|
|
50
|
+
"todo",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const ALLOWED_TOOLSETS = new Set(SELLABLE_ALLOWED_TOOLSETS);
|
|
54
|
+
const PROFILE_ID = /^[a-z0-9][a-z0-9_-]{0,127}$/;
|
|
55
|
+
const BOUND_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
56
|
+
const TOOLSET_NAME = /^[a-z][a-z0-9_]{0,127}$/;
|
|
57
|
+
const TOOL_NAME = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/;
|
|
58
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
59
|
+
const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,127}$/;
|
|
60
|
+
const PYTHON = "/opt/hermes/.venv/bin/python3";
|
|
61
|
+
const EXPECTED_PACKAGES = Object.freeze({
|
|
62
|
+
sellable: "@sellable/mcp",
|
|
63
|
+
"sellable-admin": "@sellable/admin-mcp",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {{
|
|
68
|
+
* enumerate: (input: any) => any,
|
|
69
|
+
* toggle: (input: any) => any,
|
|
70
|
+
* }} CapabilitiesNativeAdapter
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @typedef {{
|
|
75
|
+
* profileId?: string,
|
|
76
|
+
* dataRoot?: string,
|
|
77
|
+
* nativeAdapter?: CapabilitiesNativeAdapter | null,
|
|
78
|
+
* packageSpecs?: Record<string, string> | null,
|
|
79
|
+
* markDirty?: (profileRoot: string) => void,
|
|
80
|
+
* lockHeld?: boolean,
|
|
81
|
+
* }} CapabilitiesBridgeOptions
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
85
|
+
const stableJson = (value) => {
|
|
86
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
87
|
+
if (value && typeof value === "object") {
|
|
88
|
+
return `{${Object.keys(value)
|
|
89
|
+
.sort()
|
|
90
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
91
|
+
.join(",")}}`;
|
|
92
|
+
}
|
|
93
|
+
return JSON.stringify(value);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export class FlyCapabilitiesBridgeError extends Error {
|
|
97
|
+
constructor(code) {
|
|
98
|
+
super(code);
|
|
99
|
+
this.name = "FlyCapabilitiesBridgeError";
|
|
100
|
+
this.code = code;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fail(code) {
|
|
105
|
+
throw new FlyCapabilitiesBridgeError(code);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function regularFile(path, maxBytes = 2 * 1024 * 1024) {
|
|
109
|
+
const link = lstatSync(path);
|
|
110
|
+
if (
|
|
111
|
+
link.isSymbolicLink() ||
|
|
112
|
+
!link.isFile() ||
|
|
113
|
+
link.nlink !== 1 ||
|
|
114
|
+
link.size > maxBytes
|
|
115
|
+
) {
|
|
116
|
+
fail("capabilities_config_rejected");
|
|
117
|
+
}
|
|
118
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
119
|
+
try {
|
|
120
|
+
const opened = fstatSync(fd);
|
|
121
|
+
if (
|
|
122
|
+
!opened.isFile() ||
|
|
123
|
+
opened.dev !== link.dev ||
|
|
124
|
+
opened.ino !== link.ino ||
|
|
125
|
+
opened.nlink !== 1 ||
|
|
126
|
+
opened.size !== link.size
|
|
127
|
+
) {
|
|
128
|
+
fail("capabilities_config_changed");
|
|
129
|
+
}
|
|
130
|
+
return readFileSync(fd);
|
|
131
|
+
} finally {
|
|
132
|
+
closeSync(fd);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertExactKeys(value, allowed) {
|
|
137
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
138
|
+
fail("capabilities_request_rejected");
|
|
139
|
+
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
140
|
+
fail("capabilities_request_rejected");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function expectedAliases(kind) {
|
|
144
|
+
return kind === "ADMIN" ? ["sellable", "sellable-admin"] : ["sellable"];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function assertRequest(request) {
|
|
148
|
+
const common = ["operation", "expectedRuntimeKind", "expectedWorkspaceId"];
|
|
149
|
+
const shapes = {
|
|
150
|
+
list: common,
|
|
151
|
+
toggle_tool: [...common, "toolsetName", "enabled", "configSha256"],
|
|
152
|
+
test_mcp: [...common, "serverName"],
|
|
153
|
+
};
|
|
154
|
+
const keys = shapes[request?.operation];
|
|
155
|
+
if (!keys) fail("capabilities_operation_rejected");
|
|
156
|
+
assertExactKeys(request, new Set(keys));
|
|
157
|
+
if (!["CUSTOMER", "ADMIN"].includes(request.expectedRuntimeKind))
|
|
158
|
+
fail("capabilities_runtime_kind_rejected");
|
|
159
|
+
if (!BOUND_ID.test(request.expectedWorkspaceId ?? ""))
|
|
160
|
+
fail("capabilities_workspace_rejected");
|
|
161
|
+
if (request.operation === "toggle_tool") {
|
|
162
|
+
if (
|
|
163
|
+
!TOOLSET_NAME.test(request.toolsetName ?? "") ||
|
|
164
|
+
!ALLOWED_TOOLSETS.has(request.toolsetName)
|
|
165
|
+
)
|
|
166
|
+
fail("capabilities_toolset_rejected");
|
|
167
|
+
if (
|
|
168
|
+
typeof request.enabled !== "boolean" ||
|
|
169
|
+
!SHA256.test(request.configSha256 ?? "")
|
|
170
|
+
)
|
|
171
|
+
fail("capabilities_cas_rejected");
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
request.operation === "test_mcp" &&
|
|
175
|
+
!expectedAliases(request.expectedRuntimeKind).includes(request.serverName)
|
|
176
|
+
)
|
|
177
|
+
fail("capabilities_mcp_server_rejected");
|
|
178
|
+
return request;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parsePackageSpec(spec, expectedName) {
|
|
182
|
+
if (typeof spec !== "string") fail("capabilities_package_identity_rejected");
|
|
183
|
+
const delimiter = spec.lastIndexOf("@");
|
|
184
|
+
const name = spec.slice(0, delimiter);
|
|
185
|
+
const version = spec.slice(delimiter + 1);
|
|
186
|
+
if (name !== expectedName || !VERSION.test(version))
|
|
187
|
+
fail("capabilities_package_identity_rejected");
|
|
188
|
+
return { packageName: name, version };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function boundedText(value, max) {
|
|
192
|
+
return typeof value === "string" && Buffer.byteLength(value, "utf8") <= max;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeNative(native, request, configSha256, packageSpecs) {
|
|
196
|
+
if (!native || !Array.isArray(native.tools) || !Array.isArray(native.servers))
|
|
197
|
+
fail("capabilities_native_output_rejected");
|
|
198
|
+
const tools = native.tools.map((row) => {
|
|
199
|
+
if (
|
|
200
|
+
!row ||
|
|
201
|
+
!ALLOWED_TOOLSETS.has(row.name) ||
|
|
202
|
+
!boundedText(row.label, 256) ||
|
|
203
|
+
!boundedText(row.description, 2048) ||
|
|
204
|
+
row.platform !== "slack" ||
|
|
205
|
+
typeof row.enabled !== "boolean" ||
|
|
206
|
+
typeof row.configured !== "boolean" ||
|
|
207
|
+
!Array.isArray(row.tools) ||
|
|
208
|
+
row.tools.length > 256 ||
|
|
209
|
+
row.tools.some((name) => !TOOL_NAME.test(name))
|
|
210
|
+
)
|
|
211
|
+
fail("capabilities_tool_inventory_rejected");
|
|
212
|
+
const names = [...new Set(row.tools)].sort();
|
|
213
|
+
if (names.length !== row.tools.length)
|
|
214
|
+
fail("capabilities_tool_inventory_rejected");
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
name: row.name,
|
|
217
|
+
label: row.label,
|
|
218
|
+
description: row.description,
|
|
219
|
+
platform: "slack",
|
|
220
|
+
enabled: row.enabled,
|
|
221
|
+
available: row.enabled && row.configured,
|
|
222
|
+
configured: row.configured,
|
|
223
|
+
policy: "allowed",
|
|
224
|
+
tools: Object.freeze(names),
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
tools.sort((left, right) => left.name.localeCompare(right.name));
|
|
228
|
+
if (
|
|
229
|
+
stableJson(tools.map((tool) => tool.name)) !==
|
|
230
|
+
stableJson([...SELLABLE_ALLOWED_TOOLSETS].sort())
|
|
231
|
+
)
|
|
232
|
+
fail("capabilities_tool_inventory_rejected");
|
|
233
|
+
|
|
234
|
+
const aliases = expectedAliases(request.expectedRuntimeKind);
|
|
235
|
+
const mcpServers = native.servers.map((row) => {
|
|
236
|
+
if (
|
|
237
|
+
!row ||
|
|
238
|
+
!aliases.includes(row.name) ||
|
|
239
|
+
row.enabled !== true ||
|
|
240
|
+
row.transport !== "stdio" ||
|
|
241
|
+
typeof row.workspaceLocked !== "boolean" ||
|
|
242
|
+
typeof row.requiresWorkspaceLock !== "boolean" ||
|
|
243
|
+
!["healthy", "unhealthy", "unknown"].includes(row.health) ||
|
|
244
|
+
!Array.isArray(row.tools) ||
|
|
245
|
+
row.tools.length > 1024 ||
|
|
246
|
+
row.tools.some((name) => !TOOL_NAME.test(name)) ||
|
|
247
|
+
!Number.isSafeInteger(row.prompts) ||
|
|
248
|
+
row.prompts < 0 ||
|
|
249
|
+
!Number.isSafeInteger(row.resources) ||
|
|
250
|
+
row.resources < 0 ||
|
|
251
|
+
![null, "probe_failed", "not_tested"].includes(row.errorCode)
|
|
252
|
+
)
|
|
253
|
+
fail("capabilities_mcp_inventory_rejected");
|
|
254
|
+
if (
|
|
255
|
+
row.name === "sellable" &&
|
|
256
|
+
(!row.workspaceLocked || !row.requiresWorkspaceLock)
|
|
257
|
+
)
|
|
258
|
+
fail("capabilities_workspace_lock_rejected");
|
|
259
|
+
const names = [...new Set(row.tools)].sort();
|
|
260
|
+
if (names.length !== row.tools.length)
|
|
261
|
+
fail("capabilities_mcp_inventory_rejected");
|
|
262
|
+
return Object.freeze({
|
|
263
|
+
name: row.name,
|
|
264
|
+
label: row.name === "sellable" ? "Sellable" : "Sellable Admin",
|
|
265
|
+
...parsePackageSpec(packageSpecs[row.name], EXPECTED_PACKAGES[row.name]),
|
|
266
|
+
enabled: true,
|
|
267
|
+
managed: true,
|
|
268
|
+
readOnly: true,
|
|
269
|
+
transport: "stdio",
|
|
270
|
+
workspaceLocked: row.workspaceLocked,
|
|
271
|
+
requiresWorkspaceLock: row.requiresWorkspaceLock,
|
|
272
|
+
health: row.health,
|
|
273
|
+
tools: Object.freeze(names),
|
|
274
|
+
prompts: row.prompts,
|
|
275
|
+
resources: row.resources,
|
|
276
|
+
errorCode: row.errorCode,
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
mcpServers.sort((left, right) => left.name.localeCompare(right.name));
|
|
280
|
+
if (
|
|
281
|
+
stableJson(mcpServers.map((server) => server.name)) !== stableJson(aliases)
|
|
282
|
+
)
|
|
283
|
+
fail("capabilities_mcp_inventory_rejected");
|
|
284
|
+
const inventory = Object.freeze({
|
|
285
|
+
complete: true,
|
|
286
|
+
configSha256,
|
|
287
|
+
tools: Object.freeze(tools),
|
|
288
|
+
mcpServers: Object.freeze(mcpServers),
|
|
289
|
+
});
|
|
290
|
+
return Object.freeze({
|
|
291
|
+
...inventory,
|
|
292
|
+
inventorySha256: sha256(stableJson(inventory)),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const NATIVE_SCRIPT = String.raw`
|
|
297
|
+
import json, sys
|
|
298
|
+
request = json.loads(sys.stdin.read())
|
|
299
|
+
from hermes_cli.config import load_config
|
|
300
|
+
from hermes_cli.mcp_config import _get_mcp_servers, _probe_single_server
|
|
301
|
+
from hermes_cli.tools_config import CONFIGURABLE_TOOLSETS, _get_platform_tools, _save_platform_tools, _toolset_has_keys
|
|
302
|
+
from toolsets import resolve_toolset
|
|
303
|
+
|
|
304
|
+
def enumerate_capabilities():
|
|
305
|
+
allowed = request["allowedToolsets"]
|
|
306
|
+
config = load_config()
|
|
307
|
+
catalog = {name: (label, description) for name, label, description in CONFIGURABLE_TOOLSETS}
|
|
308
|
+
enabled = _get_platform_tools(config, "slack", include_default_mcp_servers=False)
|
|
309
|
+
tool_rows = []
|
|
310
|
+
for name in allowed:
|
|
311
|
+
if name not in catalog:
|
|
312
|
+
raise RuntimeError("missing_policy_toolset")
|
|
313
|
+
label, description = catalog[name]
|
|
314
|
+
label_parts = str(label).strip().split(None, 1)
|
|
315
|
+
if len(label_parts) == 2 and not any(ch.isascii() and ch.isalnum() for ch in label_parts[0]):
|
|
316
|
+
label = label_parts[1]
|
|
317
|
+
tool_rows.append({
|
|
318
|
+
"name": name,
|
|
319
|
+
"label": str(label),
|
|
320
|
+
"description": str(description),
|
|
321
|
+
"platform": "slack",
|
|
322
|
+
"enabled": name in enabled,
|
|
323
|
+
"configured": bool(_toolset_has_keys(name, config)),
|
|
324
|
+
"tools": sorted(set(resolve_toolset(name, include_registry=False))),
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
servers = _get_mcp_servers(config)
|
|
328
|
+
expected_aliases = request["expectedAliases"]
|
|
329
|
+
if sorted(servers.keys()) != sorted(expected_aliases):
|
|
330
|
+
raise RuntimeError("managed_server_inventory")
|
|
331
|
+
server_rows = []
|
|
332
|
+
for name in expected_aliases:
|
|
333
|
+
server = servers[name]
|
|
334
|
+
env = server.get("env") if isinstance(server.get("env"), dict) else {}
|
|
335
|
+
details = {}
|
|
336
|
+
try:
|
|
337
|
+
found = _probe_single_server(name, server, connect_timeout=12, details=details)
|
|
338
|
+
health = "healthy"
|
|
339
|
+
tools = sorted(set(str(tool_name) for tool_name, _description in found))
|
|
340
|
+
prompts = int(details.get("prompts", 0) or 0)
|
|
341
|
+
resources = int(details.get("resources", 0) or 0)
|
|
342
|
+
error_code = None
|
|
343
|
+
except BaseException:
|
|
344
|
+
health, tools, prompts, resources, error_code = "unhealthy", [], 0, 0, "probe_failed"
|
|
345
|
+
server_rows.append({
|
|
346
|
+
"name": name,
|
|
347
|
+
"enabled": server.get("enabled", True) is not False,
|
|
348
|
+
"transport": "http" if server.get("url") else "stdio" if server.get("command") else "unknown",
|
|
349
|
+
"workspaceLocked": env.get("SELLABLE_LOCK_WORKSPACE_ID") == request["expectedWorkspaceId"],
|
|
350
|
+
"requiresWorkspaceLock": str(env.get("SELLABLE_REQUIRE_WORKSPACE_LOCK", "")) == "1",
|
|
351
|
+
"health": health,
|
|
352
|
+
"tools": tools,
|
|
353
|
+
"prompts": prompts,
|
|
354
|
+
"resources": resources,
|
|
355
|
+
"errorCode": error_code,
|
|
356
|
+
})
|
|
357
|
+
return {"tools": tool_rows, "servers": server_rows}
|
|
358
|
+
|
|
359
|
+
if request["operation"] == "enumerate":
|
|
360
|
+
output = enumerate_capabilities()
|
|
361
|
+
elif request["operation"] == "toggle":
|
|
362
|
+
config = load_config()
|
|
363
|
+
enabled = set(_get_platform_tools(config, "slack", include_default_mcp_servers=False))
|
|
364
|
+
if request["enabled"]: enabled.add(request["name"])
|
|
365
|
+
else: enabled.discard(request["name"])
|
|
366
|
+
_save_platform_tools(config, "slack", enabled)
|
|
367
|
+
output = {"ok": True}
|
|
368
|
+
else:
|
|
369
|
+
raise RuntimeError("operation")
|
|
370
|
+
sys.stdout.write(json.dumps(output, separators=(",", ":")))
|
|
371
|
+
`;
|
|
372
|
+
|
|
373
|
+
function runNative(payload, paths, { python = PYTHON } = {}) {
|
|
374
|
+
const child = spawnSync(python, ["-c", NATIVE_SCRIPT], {
|
|
375
|
+
input: JSON.stringify(payload),
|
|
376
|
+
encoding: "utf8",
|
|
377
|
+
timeout: 30_000,
|
|
378
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
379
|
+
env: {
|
|
380
|
+
HOME: paths.dataRoot,
|
|
381
|
+
HERMES_HOME: paths.profileRoot,
|
|
382
|
+
HERMES_PROFILE: paths.profileId,
|
|
383
|
+
SELLABLE_HERMES_PROFILE_ID: paths.profileId,
|
|
384
|
+
SELLABLE_AGENT_RUNTIME: "1",
|
|
385
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
386
|
+
PATH: "/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin",
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
if (child.status !== 0 || child.signal || child.error)
|
|
390
|
+
fail("capabilities_native_call_failed");
|
|
391
|
+
try {
|
|
392
|
+
return JSON.parse(child.stdout);
|
|
393
|
+
} catch {
|
|
394
|
+
fail("capabilities_native_output_rejected");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function defaultAdapter(paths) {
|
|
399
|
+
return {
|
|
400
|
+
enumerate: ({
|
|
401
|
+
expectedAliases: aliases,
|
|
402
|
+
expectedWorkspaceId,
|
|
403
|
+
probeServerName,
|
|
404
|
+
}) =>
|
|
405
|
+
runNative(
|
|
406
|
+
{
|
|
407
|
+
operation: "enumerate",
|
|
408
|
+
allowedToolsets: SELLABLE_ALLOWED_TOOLSETS,
|
|
409
|
+
expectedAliases: aliases,
|
|
410
|
+
expectedWorkspaceId,
|
|
411
|
+
...(probeServerName ? { probeServerName } : {}),
|
|
412
|
+
},
|
|
413
|
+
paths
|
|
414
|
+
),
|
|
415
|
+
toggle: ({ name, enabled }) =>
|
|
416
|
+
runNative({ operation: "toggle", name, enabled }, paths),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function runtimePackageSpecs(kind, provided) {
|
|
421
|
+
if (provided) return provided;
|
|
422
|
+
return kind === "ADMIN"
|
|
423
|
+
? {
|
|
424
|
+
sellable: process.env.SELLABLE_AGENT_PRODUCT_MCP_PACKAGE,
|
|
425
|
+
"sellable-admin": process.env.SELLABLE_AGENT_ADMIN_MCP_PACKAGE,
|
|
426
|
+
}
|
|
427
|
+
: { sellable: process.env.SELLABLE_AGENT_MCP_PACKAGE };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function atomicProof(paths, inventory) {
|
|
431
|
+
const root = join(paths.profileRoot, ".sellable-agent");
|
|
432
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
433
|
+
const path = join(root, "capabilities-proof.json");
|
|
434
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
435
|
+
writeFileSync(
|
|
436
|
+
temporary,
|
|
437
|
+
`${JSON.stringify({
|
|
438
|
+
schemaVersion: CAPABILITIES_PROOF_SCHEMA_VERSION,
|
|
439
|
+
inventorySha256: inventory.inventorySha256,
|
|
440
|
+
configSha256: inventory.configSha256,
|
|
441
|
+
})}\n`,
|
|
442
|
+
{ mode: 0o600 }
|
|
443
|
+
);
|
|
444
|
+
renameSync(temporary, path);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function readInventory(request, paths, adapter, packageSpecs) {
|
|
448
|
+
const configPath = join(paths.profileRoot, "config.yaml");
|
|
449
|
+
const configSha256 = sha256(regularFile(configPath));
|
|
450
|
+
const aliases = expectedAliases(request.expectedRuntimeKind);
|
|
451
|
+
const native = adapter.enumerate({
|
|
452
|
+
expectedAliases: aliases,
|
|
453
|
+
expectedWorkspaceId: request.expectedWorkspaceId,
|
|
454
|
+
...(request.operation === "test_mcp"
|
|
455
|
+
? { probeServerName: request.serverName }
|
|
456
|
+
: {}),
|
|
457
|
+
});
|
|
458
|
+
if (sha256(regularFile(configPath)) !== configSha256)
|
|
459
|
+
fail("capabilities_config_changed");
|
|
460
|
+
return normalizeNative(native, request, configSha256, packageSpecs);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function executeLocked(request, paths, adapter, options) {
|
|
464
|
+
const packageSpecs = runtimePackageSpecs(
|
|
465
|
+
request.expectedRuntimeKind,
|
|
466
|
+
options.packageSpecs
|
|
467
|
+
);
|
|
468
|
+
const before = readInventory(request, paths, adapter, packageSpecs);
|
|
469
|
+
if (request.operation === "list" || request.operation === "test_mcp") {
|
|
470
|
+
atomicProof(paths, before);
|
|
471
|
+
return {
|
|
472
|
+
ok: true,
|
|
473
|
+
schemaVersion: CAPABILITIES_BRIDGE_SCHEMA_VERSION,
|
|
474
|
+
status: "proven",
|
|
475
|
+
operation: request.operation,
|
|
476
|
+
...(request.operation === "test_mcp"
|
|
477
|
+
? { testedServer: request.serverName }
|
|
478
|
+
: {}),
|
|
479
|
+
...before,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (request.configSha256 !== before.configSha256) {
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
code: "capabilities_config_conflict",
|
|
486
|
+
configSha256: before.configSha256,
|
|
487
|
+
inventorySha256: before.inventorySha256,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const detail = before.tools.find((tool) => tool.name === request.toolsetName);
|
|
491
|
+
if (!detail) fail("capabilities_toolset_rejected");
|
|
492
|
+
if (detail.enabled === request.enabled) {
|
|
493
|
+
atomicProof(paths, before);
|
|
494
|
+
return {
|
|
495
|
+
ok: true,
|
|
496
|
+
schemaVersion: CAPABILITIES_BRIDGE_SCHEMA_VERSION,
|
|
497
|
+
status: "noop",
|
|
498
|
+
operation: "toggle_tool",
|
|
499
|
+
...before,
|
|
500
|
+
detail,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const configPath = join(paths.profileRoot, "config.yaml");
|
|
504
|
+
const configBackup = regularFile(configPath);
|
|
505
|
+
const proofPath = join(
|
|
506
|
+
paths.profileRoot,
|
|
507
|
+
".sellable-agent",
|
|
508
|
+
"capabilities-proof.json"
|
|
509
|
+
);
|
|
510
|
+
const proofBackup = existsSync(proofPath)
|
|
511
|
+
? regularFile(proofPath, 4096)
|
|
512
|
+
: null;
|
|
513
|
+
const rollback = () => {
|
|
514
|
+
writeFileSync(configPath, configBackup, { mode: 0o600 });
|
|
515
|
+
if (proofBackup) writeFileSync(proofPath, proofBackup, { mode: 0o600 });
|
|
516
|
+
else rmSync(proofPath, { force: true });
|
|
517
|
+
};
|
|
518
|
+
try {
|
|
519
|
+
const result = adapter.toggle({
|
|
520
|
+
name: request.toolsetName,
|
|
521
|
+
enabled: request.enabled,
|
|
522
|
+
platform: "slack",
|
|
523
|
+
});
|
|
524
|
+
if (result?.ok !== true) fail("capabilities_native_refused");
|
|
525
|
+
const after = readInventory(request, paths, adapter, packageSpecs);
|
|
526
|
+
const afterDetail = after.tools.find(
|
|
527
|
+
(tool) => tool.name === request.toolsetName
|
|
528
|
+
);
|
|
529
|
+
if (
|
|
530
|
+
!afterDetail ||
|
|
531
|
+
afterDetail.enabled !== request.enabled ||
|
|
532
|
+
after.configSha256 === before.configSha256 ||
|
|
533
|
+
stableJson(after.mcpServers.map((server) => server.name)) !==
|
|
534
|
+
stableJson(before.mcpServers.map((server) => server.name))
|
|
535
|
+
)
|
|
536
|
+
fail("capabilities_readback_failed");
|
|
537
|
+
atomicProof(paths, after);
|
|
538
|
+
options.markDirty(paths.profileRoot);
|
|
539
|
+
return {
|
|
540
|
+
ok: true,
|
|
541
|
+
schemaVersion: CAPABILITIES_BRIDGE_SCHEMA_VERSION,
|
|
542
|
+
status: "applied",
|
|
543
|
+
operation: "toggle_tool",
|
|
544
|
+
effectId: randomUUID(),
|
|
545
|
+
...after,
|
|
546
|
+
detail: afterDetail,
|
|
547
|
+
};
|
|
548
|
+
} catch (error) {
|
|
549
|
+
rollback();
|
|
550
|
+
try {
|
|
551
|
+
const restored = readInventory(request, paths, adapter, packageSpecs);
|
|
552
|
+
if (restored.configSha256 !== before.configSha256)
|
|
553
|
+
return { ok: false, code: "capabilities_rollback_unproven" };
|
|
554
|
+
} catch {
|
|
555
|
+
return { ok: false, code: "capabilities_rollback_unproven" };
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
ok: false,
|
|
559
|
+
code: "capabilities_apply_failed_rolled_back",
|
|
560
|
+
reason:
|
|
561
|
+
error instanceof FlyCapabilitiesBridgeError
|
|
562
|
+
? error.code
|
|
563
|
+
: "capabilities_bridge_failed",
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* @param {any} request
|
|
570
|
+
* @param {CapabilitiesBridgeOptions} [options]
|
|
571
|
+
*/
|
|
572
|
+
export function executeCapabilitiesBridgeOperation(
|
|
573
|
+
request,
|
|
574
|
+
{
|
|
575
|
+
profileId = process.env.SELLABLE_HERMES_PROFILE_ID,
|
|
576
|
+
dataRoot = SOUL_BRIDGE_DATA_ROOT,
|
|
577
|
+
nativeAdapter = null,
|
|
578
|
+
packageSpecs = null,
|
|
579
|
+
markDirty = markHermesSnapshotDirty,
|
|
580
|
+
lockHeld = false,
|
|
581
|
+
} = {}
|
|
582
|
+
) {
|
|
583
|
+
assertRequest(request);
|
|
584
|
+
if (typeof profileId !== "string" || !PROFILE_ID.test(profileId))
|
|
585
|
+
fail("capabilities_profile_id_rejected");
|
|
586
|
+
const paths = soulProfilePaths(profileId, { dataRoot });
|
|
587
|
+
const adapter = nativeAdapter ?? defaultAdapter(paths);
|
|
588
|
+
if (
|
|
589
|
+
!adapter ||
|
|
590
|
+
typeof adapter.enumerate !== "function" ||
|
|
591
|
+
typeof adapter.toggle !== "function"
|
|
592
|
+
)
|
|
593
|
+
fail("capabilities_native_adapter_rejected");
|
|
594
|
+
if (typeof markDirty !== "function")
|
|
595
|
+
fail("capabilities_dirty_marker_rejected");
|
|
596
|
+
const run = () =>
|
|
597
|
+
executeLocked(request, paths, adapter, { packageSpecs, markDirty });
|
|
598
|
+
return lockHeld ? run() : withProfileSoulLock(profileId, run, { dataRoot });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function executeExternalCapabilitiesBridgeOperation(
|
|
602
|
+
request,
|
|
603
|
+
{
|
|
604
|
+
procRoot = "/proc",
|
|
605
|
+
dataRoot = SOUL_BRIDGE_DATA_ROOT,
|
|
606
|
+
nativeAdapter = null,
|
|
607
|
+
packageSpecs = null,
|
|
608
|
+
markDirty = markHermesSnapshotDirty,
|
|
609
|
+
callerProfileId = process.env.SELLABLE_HERMES_PROFILE_ID,
|
|
610
|
+
} = {}
|
|
611
|
+
) {
|
|
612
|
+
const gateway = discoverLiveHermesGateway({ procRoot, dataRoot });
|
|
613
|
+
if (callerProfileId && callerProfileId !== gateway.profileId)
|
|
614
|
+
fail("capabilities_gateway_caller_profile_mismatch");
|
|
615
|
+
revalidateLiveHermesGateway(gateway, { procRoot, dataRoot });
|
|
616
|
+
const result = executeCapabilitiesBridgeOperation(request, {
|
|
617
|
+
profileId: gateway.profileId,
|
|
618
|
+
dataRoot,
|
|
619
|
+
nativeAdapter,
|
|
620
|
+
packageSpecs,
|
|
621
|
+
markDirty,
|
|
622
|
+
});
|
|
623
|
+
revalidateLiveHermesGateway(gateway, { procRoot, dataRoot });
|
|
624
|
+
return Object.freeze({
|
|
625
|
+
...result,
|
|
626
|
+
profileId: gateway.profileId,
|
|
627
|
+
gatewayPid: gateway.pid,
|
|
628
|
+
gatewayStartTicks: gateway.startTicks,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function readCapabilitiesProofObservation(profileRoot) {
|
|
633
|
+
try {
|
|
634
|
+
if (!isAbsolute(profileRoot)) return null;
|
|
635
|
+
const value = JSON.parse(
|
|
636
|
+
regularFile(
|
|
637
|
+
join(
|
|
638
|
+
realpathSync(profileRoot),
|
|
639
|
+
".sellable-agent",
|
|
640
|
+
"capabilities-proof.json"
|
|
641
|
+
),
|
|
642
|
+
4096
|
|
643
|
+
).toString("utf8")
|
|
644
|
+
);
|
|
645
|
+
if (
|
|
646
|
+
value.schemaVersion !== CAPABILITIES_PROOF_SCHEMA_VERSION ||
|
|
647
|
+
!SHA256.test(value.inventorySha256 ?? "") ||
|
|
648
|
+
!SHA256.test(value.configSha256 ?? "") ||
|
|
649
|
+
Object.keys(value).length !== 3
|
|
650
|
+
)
|
|
651
|
+
return null;
|
|
652
|
+
return Object.freeze({
|
|
653
|
+
inventorySha256: value.inventorySha256,
|
|
654
|
+
configSha256: value.configSha256,
|
|
655
|
+
});
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
@@ -6,7 +6,7 @@ USER root
|
|
|
6
6
|
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.860
|
|
8
8
|
ARG MCP_INTEGRITY=sha512-jRA1KsCV2d4Zcuo5/UmJiP8P0NaoC3ZDvUFRQOc2AMSNDVmUR6gWvtkyAiqb1lukbFWKzfVlz2/4EWzxvb7hkQ==
|
|
9
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
9
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.599
|
|
10
10
|
ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.186-wip.phase158.20260802034727
|
|
11
11
|
ARG ADMIN_MCP_INTEGRITY=sha512-qYNew5wd2IfgVcp1UYOG9QvpF/8Mx19YJ5HQtMKzvSfUf3evePjtaKsoXLf2C/GUJ5QlLlHJo84VWOI3V01+9A==
|
|
12
12
|
ARG TUS_JS_CLIENT_VERSION=4.3.1
|
|
@@ -69,7 +69,7 @@ RUN --mount=type=secret,id=mcp_trust_root,required=true \
|
|
|
69
69
|
/etc/cont-init.d/02-reconcile-profiles \
|
|
70
70
|
&& npm cache clean --force
|
|
71
71
|
|
|
72
|
-
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 \
|
|
72
|
+
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 fly-capabilities-bridge.mjs fly-capabilities-bridge-exec.mjs soul-artifact-validator.mjs model-auth-reconciler.mjs model-auth-canary-reseed.mjs model-health-reporter.mjs \
|
|
73
73
|
/usr/local/lib/sellable-agent/
|
|
74
74
|
COPY hermes-memory-dirty.mjs /opt/sellable/bin/hermes-memory-dirty
|
|
75
75
|
COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
|
|
@@ -101,13 +101,22 @@ RUN node --input-type=module -e \
|
|
|
101
101
|
'import("file:///usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs").then(({ runFlySkillsBridgeExecCli }) => runFlySkillsBridgeExecCli());' \
|
|
102
102
|
> /usr/local/bin/sellable-skills-bridge-exec \
|
|
103
103
|
&& chmod 0555 /usr/local/bin/sellable-skills-bridge-exec \
|
|
104
|
+
&& printf '%s\n' \
|
|
105
|
+
'#!/usr/bin/env node' \
|
|
106
|
+
'import("file:///usr/local/lib/sellable-agent/fly-capabilities-bridge-exec.mjs").then(({ runFlyCapabilitiesBridgeExecCli }) => runFlyCapabilitiesBridgeExecCli());' \
|
|
107
|
+
> /usr/local/bin/sellable-capabilities-bridge-exec \
|
|
108
|
+
&& chmod 0555 /usr/local/bin/sellable-capabilities-bridge-exec \
|
|
104
109
|
&& sha256sum /usr/local/lib/sellable-agent/fly-soul-bridge.mjs \
|
|
105
110
|
> /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
|
|
106
111
|
&& sha256sum /usr/local/lib/sellable-agent/fly-skills-bridge.mjs \
|
|
107
112
|
/usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs \
|
|
108
113
|
> /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
|
|
114
|
+
&& sha256sum /usr/local/lib/sellable-agent/fly-capabilities-bridge.mjs \
|
|
115
|
+
/usr/local/lib/sellable-agent/fly-capabilities-bridge-exec.mjs \
|
|
116
|
+
> /usr/local/share/sellable-agent/capabilities-bridge-source.sha256 \
|
|
109
117
|
&& chmod 0444 /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
|
|
110
118
|
/usr/local/share/sellable-agent/skills-bridge-source.sha256 \
|
|
119
|
+
/usr/local/share/sellable-agent/capabilities-bridge-source.sha256 \
|
|
111
120
|
&& chmod 0755 /etc \
|
|
112
121
|
&& chmod 0555 /usr/local/lib/sellable-agent/*.mjs \
|
|
113
122
|
&& chmod 0555 /opt/sellable/bin/hermes-memory-dirty /opt/sellable/bin/hermes-endpoint-cron \
|
|
@@ -95,7 +95,7 @@ const RUNTIME_GENERATION = Number(
|
|
|
95
95
|
const BOOT_SESSION_ID = randomUUID();
|
|
96
96
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.0";
|
|
97
97
|
const INSTALLER_PACKAGE =
|
|
98
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
98
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.599";
|
|
99
99
|
const MCP_PACKAGE =
|
|
100
100
|
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.860";
|
|
101
101
|
|
|
@@ -172,11 +172,11 @@ export function assertCustomerRuntimeInstallerPackage({
|
|
|
172
172
|
) {
|
|
173
173
|
throw new Error("customer_runtime_installer_package_invalid");
|
|
174
174
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
175
|
+
// Runtime releases are selected by immutable image digest. The backend and
|
|
176
|
+
// image are promoted in adjacent steps, so the signed claim can briefly name
|
|
177
|
+
// the next package while this Machine still runs the previously active
|
|
178
|
+
// digest (or vice versa). The image must materialize its own pinned package;
|
|
179
|
+
// rejecting the version skew wedges otherwise compatible Stop/Start actions.
|
|
180
180
|
return actual;
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -282,7 +282,9 @@ function compileDesired(claim, trustRootPem) {
|
|
|
282
282
|
) {
|
|
283
283
|
throw new Error("customer_claim_contract_rejected");
|
|
284
284
|
}
|
|
285
|
-
assertCustomerRuntimeInstallerPackage({
|
|
285
|
+
const imageInstallerPackage = assertCustomerRuntimeInstallerPackage({
|
|
286
|
+
expected: installerPackage,
|
|
287
|
+
});
|
|
286
288
|
const modelEnvelope = claim.secretEnvelopes.modelCodex;
|
|
287
289
|
if (
|
|
288
290
|
model.provider !== "codex" ||
|
|
@@ -375,7 +377,7 @@ function compileDesired(claim, trustRootPem) {
|
|
|
375
377
|
accessPolicyHash,
|
|
376
378
|
runtimeDesiredHash,
|
|
377
379
|
hermesVersion: HERMES_VERSION,
|
|
378
|
-
installerPackage,
|
|
380
|
+
installerPackage: imageInstallerPackage,
|
|
379
381
|
mcpPackage: MCP_PACKAGE,
|
|
380
382
|
policyHash: service.policyHash,
|
|
381
383
|
soul: exactArtifact(claim, "SOUL"),
|
|
@@ -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.
|
|
41
|
+
const INSTALLER_PACKAGE = "@sellable/install@0.1.599";
|
|
42
42
|
const MCP_PACKAGE = "@sellable/mcp@0.1.860";
|
|
43
43
|
const HERMES_VERSION = "0.18.0";
|
|
44
44
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
@@ -1546,7 +1546,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
|
|
|
1546
1546
|
]) ||
|
|
1547
1547
|
pinned.hermesCli !== "hermes" ||
|
|
1548
1548
|
pinned.hermesVersion !== "0.18.0" ||
|
|
1549
|
-
pinned.installerPackage !== "@sellable/install@0.1.
|
|
1549
|
+
pinned.installerPackage !== "@sellable/install@0.1.599" ||
|
|
1550
1550
|
pinned.mcpPackage !== "@sellable/mcp@0.1.860" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
resolve,
|
|
26
26
|
} from "node:path";
|
|
27
27
|
import { parseDocument } from "yaml";
|
|
28
|
+
import { SELLABLE_ALLOWED_TOOLSETS } from "./fly-capabilities-bridge.mjs";
|
|
28
29
|
import {
|
|
29
30
|
FLY_CUSTOMER_MODEL_ID,
|
|
30
31
|
FLY_CUSTOMER_MODEL_PROVIDER,
|
|
@@ -277,7 +278,7 @@ function validateDesired(desired) {
|
|
|
277
278
|
desired.serviceCredentialGeneration < 1 ||
|
|
278
279
|
desired.hermesCli !== "hermes" ||
|
|
279
280
|
desired.hermesVersion !== "0.18.0" ||
|
|
280
|
-
desired.installerPackage !== "@sellable/install@0.1.
|
|
281
|
+
desired.installerPackage !== "@sellable/install@0.1.599" ||
|
|
281
282
|
desired.mcpPackage !== "@sellable/mcp@0.1.860" ||
|
|
282
283
|
!Array.isArray(desired.toolInclude) ||
|
|
283
284
|
desired.toolInclude.length === 0 ||
|
|
@@ -487,7 +488,7 @@ const LIVE_SKILLS_CONFIG_KEYS = Object.freeze([
|
|
|
487
488
|
"platform_disabled",
|
|
488
489
|
]);
|
|
489
490
|
|
|
490
|
-
function
|
|
491
|
+
function profileConfigWithoutLiveState(value) {
|
|
491
492
|
const normalized = structuredClone(value);
|
|
492
493
|
if (
|
|
493
494
|
normalized?.skills &&
|
|
@@ -497,6 +498,15 @@ function profileConfigWithoutLiveSkills(value) {
|
|
|
497
498
|
for (const key of LIVE_SKILLS_CONFIG_KEYS) delete normalized.skills[key];
|
|
498
499
|
if (Object.keys(normalized.skills).length === 0) delete normalized.skills;
|
|
499
500
|
}
|
|
501
|
+
if (
|
|
502
|
+
normalized?.platform_toolsets &&
|
|
503
|
+
typeof normalized.platform_toolsets === "object" &&
|
|
504
|
+
!Array.isArray(normalized.platform_toolsets)
|
|
505
|
+
) {
|
|
506
|
+
delete normalized.platform_toolsets.slack;
|
|
507
|
+
if (Object.keys(normalized.platform_toolsets).length === 0)
|
|
508
|
+
delete normalized.platform_toolsets;
|
|
509
|
+
}
|
|
500
510
|
return normalized;
|
|
501
511
|
}
|
|
502
512
|
|
|
@@ -517,12 +527,28 @@ function readProfileConfig(pathValue) {
|
|
|
517
527
|
function normalizedProfileConfigBytes(pathValue) {
|
|
518
528
|
const value = readProfileConfig(pathValue);
|
|
519
529
|
return Buffer.from(
|
|
520
|
-
`${JSON.stringify(
|
|
530
|
+
`${JSON.stringify(profileConfigWithoutLiveState(value), null, 2)}\n`,
|
|
521
531
|
"utf8"
|
|
522
532
|
);
|
|
523
533
|
}
|
|
524
534
|
|
|
525
|
-
function
|
|
535
|
+
function preservedSlackToolsets(config) {
|
|
536
|
+
const platformToolsets = config?.platform_toolsets;
|
|
537
|
+
if (
|
|
538
|
+
!platformToolsets ||
|
|
539
|
+
typeof platformToolsets !== "object" ||
|
|
540
|
+
Array.isArray(platformToolsets) ||
|
|
541
|
+
!Object.prototype.hasOwnProperty.call(platformToolsets, "slack")
|
|
542
|
+
)
|
|
543
|
+
return null;
|
|
544
|
+
const slack = Array.isArray(platformToolsets.slack)
|
|
545
|
+
? platformToolsets.slack
|
|
546
|
+
: [];
|
|
547
|
+
const allowed = new Set(SELLABLE_ALLOWED_TOOLSETS);
|
|
548
|
+
return [...new Set(slack.filter((name) => allowed.has(name)))].sort();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function mergeLiveMutableConfig(sourceRoot, targetRoot) {
|
|
526
552
|
const sourcePath = join(sourceRoot, "config.yaml");
|
|
527
553
|
const targetPath = join(targetRoot, "config.yaml");
|
|
528
554
|
if (!existsSync(sourcePath) || !existsSync(targetPath)) return;
|
|
@@ -532,27 +558,43 @@ function mergeLiveSkillsConfig(sourceRoot, targetRoot) {
|
|
|
532
558
|
const source = readProfileConfig(sourcePath);
|
|
533
559
|
const target = readProfileConfig(targetPath);
|
|
534
560
|
const sourceSkills = source?.skills;
|
|
561
|
+
let changed = false;
|
|
535
562
|
if (
|
|
536
|
-
|
|
537
|
-
typeof sourceSkills !== "object" ||
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
563
|
+
sourceSkills &&
|
|
564
|
+
(typeof sourceSkills !== "object" || Array.isArray(sourceSkills))
|
|
565
|
+
)
|
|
566
|
+
throw new Error("live skills config rejected");
|
|
567
|
+
if (sourceSkills) {
|
|
568
|
+
const preserved = Object.fromEntries(
|
|
569
|
+
LIVE_SKILLS_CONFIG_KEYS.filter((key) =>
|
|
570
|
+
Object.prototype.hasOwnProperty.call(sourceSkills, key)
|
|
571
|
+
).map((key) => [key, structuredClone(sourceSkills[key])])
|
|
572
|
+
);
|
|
573
|
+
if (Object.keys(preserved).length > 0) {
|
|
574
|
+
target.skills = {
|
|
575
|
+
...(target.skills &&
|
|
576
|
+
typeof target.skills === "object" &&
|
|
577
|
+
!Array.isArray(target.skills)
|
|
578
|
+
? target.skills
|
|
579
|
+
: {}),
|
|
580
|
+
...preserved,
|
|
581
|
+
};
|
|
582
|
+
changed = true;
|
|
583
|
+
}
|
|
541
584
|
}
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
};
|
|
585
|
+
const slackToolsets = preservedSlackToolsets(source);
|
|
586
|
+
if (slackToolsets !== null) {
|
|
587
|
+
target.platform_toolsets = {
|
|
588
|
+
...(target.platform_toolsets &&
|
|
589
|
+
typeof target.platform_toolsets === "object" &&
|
|
590
|
+
!Array.isArray(target.platform_toolsets)
|
|
591
|
+
? target.platform_toolsets
|
|
592
|
+
: {}),
|
|
593
|
+
slack: slackToolsets,
|
|
594
|
+
};
|
|
595
|
+
changed = true;
|
|
596
|
+
}
|
|
597
|
+
if (!changed) return;
|
|
556
598
|
atomicWrite(
|
|
557
599
|
targetPath,
|
|
558
600
|
`${JSON.stringify(target, null, 2)}\n`,
|
|
@@ -560,15 +602,15 @@ function mergeLiveSkillsConfig(sourceRoot, targetRoot) {
|
|
|
560
602
|
);
|
|
561
603
|
}
|
|
562
604
|
|
|
563
|
-
function
|
|
605
|
+
function mergeLiveMutableState(sourceRoot, targetRoot) {
|
|
564
606
|
if (!existsSync(sourceRoot)) return;
|
|
565
607
|
const sourceSkills = join(sourceRoot, "skills");
|
|
566
608
|
const targetSkills = join(targetRoot, "skills");
|
|
567
609
|
if (existsSync(sourceSkills)) mergeHermesState(sourceSkills, targetSkills);
|
|
568
|
-
|
|
610
|
+
mergeLiveMutableConfig(sourceRoot, targetRoot);
|
|
569
611
|
}
|
|
570
612
|
|
|
571
|
-
function
|
|
613
|
+
function preservedLiveStateIdentity(profileRoot) {
|
|
572
614
|
if (!existsSync(profileRoot)) return null;
|
|
573
615
|
const entries = [];
|
|
574
616
|
const skillsRoot = join(profileRoot, "skills");
|
|
@@ -592,6 +634,7 @@ function preservedSkillsConfigIdentity(profileRoot) {
|
|
|
592
634
|
if (existsSync(skillsRoot)) walk(skillsRoot);
|
|
593
635
|
const configPath = join(profileRoot, "config.yaml");
|
|
594
636
|
let disabled = {};
|
|
637
|
+
let slackToolsets = null;
|
|
595
638
|
if (existsSync(configPath)) {
|
|
596
639
|
const config = readProfileConfig(configPath);
|
|
597
640
|
const skills = config?.skills;
|
|
@@ -602,8 +645,9 @@ function preservedSkillsConfigIdentity(profileRoot) {
|
|
|
602
645
|
).map((key) => [key, skills[key]])
|
|
603
646
|
);
|
|
604
647
|
}
|
|
648
|
+
slackToolsets = preservedSlackToolsets(config);
|
|
605
649
|
}
|
|
606
|
-
return sha256(stableJson({ disabled, entries }));
|
|
650
|
+
return sha256(stableJson({ disabled, entries, slackToolsets }));
|
|
607
651
|
}
|
|
608
652
|
|
|
609
653
|
function mcpFor(desired, profileId) {
|
|
@@ -893,7 +937,7 @@ export function validateAgentProfileBundle({ profileRoot, desired }) {
|
|
|
893
937
|
readFileSync(join(profileRoot, "sellable", "mcp.json"), "utf8")
|
|
894
938
|
);
|
|
895
939
|
if (
|
|
896
|
-
stableJson(
|
|
940
|
+
stableJson(profileConfigWithoutLiveState(config)) !==
|
|
897
941
|
stableJson(configFor(desired, manifest.profileId))
|
|
898
942
|
) {
|
|
899
943
|
return { ok: false, code: "config_drift" };
|
|
@@ -1896,14 +1940,14 @@ async function materializeAgentProfileLocked(input = {}) {
|
|
|
1896
1940
|
revisionId: desired.revisionId,
|
|
1897
1941
|
fence: desired.fence,
|
|
1898
1942
|
});
|
|
1899
|
-
const
|
|
1943
|
+
const liveStateIdentity = preservedLiveStateIdentity(profileRoot);
|
|
1900
1944
|
renderStagedProfile(stagingRoot, desired);
|
|
1901
|
-
|
|
1945
|
+
mergeLiveMutableState(profileRoot, stagingRoot);
|
|
1902
1946
|
if (
|
|
1903
|
-
|
|
1904
|
-
|
|
1947
|
+
liveStateIdentity !== null &&
|
|
1948
|
+
preservedLiveStateIdentity(stagingRoot) !== liveStateIdentity
|
|
1905
1949
|
) {
|
|
1906
|
-
throw new Error("live
|
|
1950
|
+
throw new Error("live mutable state identity changed during staging");
|
|
1907
1951
|
}
|
|
1908
1952
|
const staged = validateAgentProfileBundle({
|
|
1909
1953
|
profileRoot: stagingRoot,
|
|
@@ -2721,7 +2765,7 @@ export function materializeNativeSlackCustomerProfile({
|
|
|
2721
2765
|
};
|
|
2722
2766
|
}
|
|
2723
2767
|
let soulBytes = Buffer.from(desired.soul, "utf8");
|
|
2724
|
-
const
|
|
2768
|
+
const liveStateIdentity = preservedLiveStateIdentity(activeRoot);
|
|
2725
2769
|
if (existsSync(activeRoot)) {
|
|
2726
2770
|
const soulPath = join(activeRoot, "SOUL.md");
|
|
2727
2771
|
if (existsSync(soulPath)) {
|
|
@@ -2739,12 +2783,12 @@ export function materializeNativeSlackCustomerProfile({
|
|
|
2739
2783
|
);
|
|
2740
2784
|
mergeHermesState(activeRoot, stagingRoot);
|
|
2741
2785
|
repointInternalCronProfileReferences(desired, stagingRoot);
|
|
2742
|
-
|
|
2786
|
+
mergeLiveMutableConfig(activeRoot, stagingRoot);
|
|
2743
2787
|
if (
|
|
2744
|
-
|
|
2745
|
-
|
|
2788
|
+
liveStateIdentity !== null &&
|
|
2789
|
+
preservedLiveStateIdentity(stagingRoot) !== liveStateIdentity
|
|
2746
2790
|
) {
|
|
2747
|
-
throw new Error("live
|
|
2791
|
+
throw new Error("live mutable state identity changed during staging");
|
|
2748
2792
|
}
|
|
2749
2793
|
const staged = validateNativeSlackCustomerProfile({
|
|
2750
2794
|
desired,
|
|
@@ -25,7 +25,7 @@ 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 = "@sellable/install@0.1.
|
|
28
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.599";
|
|
29
29
|
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.860";
|
|
30
30
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
31
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|