@sellable/install 0.1.836-wip.disqualify.20261012.1 → 0.1.838
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 +1 -1
- package/lib/sellable-agent/external-runtime-builder.mjs +1 -1
- package/lib/sellable-agent/fly-admin-image/Dockerfile +1 -1
- package/lib/sellable-agent/fly-customer-image/Dockerfile +7 -4
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +273 -27
- package/lib/sellable-agent/fly-customer-image/slack-stand-in.mjs +345 -0
- package/lib/sellable-agent/fly-runtime-identity.mjs +12 -0
- package/lib/sellable-agent/hermes-bridge.mjs +42 -6
- package/lib/sellable-agent/host-bootstrap.mjs +2 -2
- package/lib/sellable-agent/host-worker.mjs +3 -2
- package/lib/sellable-agent/profile-materializer.mjs +2 -3
- package/lib/sellable-agent/provisioning-adapter.mjs +3 -2
- package/package.json +2 -1
package/container/Dockerfile
CHANGED
|
@@ -3,13 +3,13 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
7
|
-
ARG MCP_PACKAGE=@sellable/mcp@0.1.
|
|
6
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.838
|
|
7
|
+
ARG MCP_PACKAGE=@sellable/mcp@0.1.982-wip.durableprep.20261013.1
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
|
-
ARG MCP_INTEGRITY=sha512-
|
|
9
|
+
ARG MCP_INTEGRITY=sha512-8WZ9cVdpKp1vbQY0CWFptXZa4MMiK1VOuGPV7L0O95qYPkYebfr6JIufS5C5sVtSxsSGpBf/v82x04eIG7wufw==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.
|
|
12
|
-
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.838" \
|
|
12
|
+
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.982-wip.durableprep.20261013.1" \
|
|
13
13
|
&& test -n "${INSTALLER_INTEGRITY}" \
|
|
14
14
|
&& test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
|
|
15
15
|
&& test "$(npm view "${MCP_PACKAGE}" dist.integrity)" = "${MCP_INTEGRITY}" \
|
|
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.836-wip.disqualify.2026
|
|
|
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.838' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.982-wip.durableprep.20261013.1') 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.838', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.982-wip.durableprep.20261013.1', 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,7 +3,7 @@
|
|
|
3
3
|
This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
|
|
4
4
|
|
|
5
5
|
Build from this directory after
|
|
6
|
-
`@sellable/install@0.1.
|
|
6
|
+
`@sellable/install@0.1.838` is
|
|
7
7
|
published:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
|
|
29
29
|
const MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
|
|
30
30
|
const MCP_PACKAGE = "@sellable/mcp";
|
|
31
|
-
const MCP_VERSION = "0.1.
|
|
31
|
+
const MCP_VERSION = "0.1.982-wip.durableprep.20261013.1";
|
|
32
32
|
const SAFE_COMPONENT = /^[A-Za-z0-9@._+-]+$/;
|
|
33
33
|
const HERMES_EXCLUDES = new Set([".git", ".playwright", "node_modules"]);
|
|
34
34
|
|
|
@@ -169,7 +169,7 @@ COPY fly-admin-image/start-admin-runtime /usr/local/lib/sellable-agent/admin-run
|
|
|
169
169
|
COPY fly-customer-image/customer-runtime.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime.mjs
|
|
170
170
|
COPY hermes-first-welcome.mjs /usr/local/lib/sellable-agent/hermes-first-welcome.mjs
|
|
171
171
|
COPY fly-customer-image/hermes-runs-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs
|
|
172
|
-
COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/claude-code-runs-api.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs fly-customer-image/public-endpoint-guard.mjs fly-customer-image/workspace-connection-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/
|
|
172
|
+
COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/claude-code-runs-api.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs fly-customer-image/public-endpoint-guard.mjs fly-customer-image/workspace-connection-client.mjs fly-customer-image/slack-stand-in.mjs /usr/local/lib/sellable-agent/fly-customer-image/
|
|
173
173
|
COPY fly-customer-image/opener-stage-execution.mjs /usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs
|
|
174
174
|
COPY fly-customer-image/endpoint-uat-completion-gate.mjs /usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs
|
|
175
175
|
COPY fly-customer-image/customer-runtime-phase-telemetry.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs
|
|
@@ -6,13 +6,14 @@ USER root
|
|
|
6
6
|
|
|
7
7
|
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
|
|
8
8
|
|
|
9
|
-
ARG MCP_PACKAGE=@sellable/mcp@0.1.
|
|
10
|
-
ARG MCP_INTEGRITY=sha512-
|
|
11
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
9
|
+
ARG MCP_PACKAGE=@sellable/mcp@0.1.982-wip.durableprep.20261013.1
|
|
10
|
+
ARG MCP_INTEGRITY=sha512-8WZ9cVdpKp1vbQY0CWFptXZa4MMiK1VOuGPV7L0O95qYPkYebfr6JIufS5C5sVtSxsSGpBf/v82x04eIG7wufw==
|
|
11
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.838
|
|
12
12
|
ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.202
|
|
13
13
|
ARG ADMIN_MCP_INTEGRITY=sha512-ym3aye/0vQ8tfULJlKz9JFOxvIQsl1G+n3np+TTgK/tecwNblBo/j3AkTnRqH9iomKzPbZzP+n4tEQsvsU8ByA==
|
|
14
14
|
ARG TUS_JS_CLIENT_VERSION=4.3.1
|
|
15
15
|
ARG YAML_VERSION=2.9.0
|
|
16
|
+
ARG WS_VERSION=8.18.3
|
|
16
17
|
ARG HERMES_VERSION=0.21.5
|
|
17
18
|
ARG SLACK_SDK_VERSION=3.43.0
|
|
18
19
|
ARG AGENT_BROWSER_VERSION=0.26.0
|
|
@@ -41,9 +42,11 @@ RUN --mount=type=secret,id=mcp_trust_root,required=true \
|
|
|
41
42
|
--omit=dev --ignore-scripts --no-audit --no-fund \
|
|
42
43
|
"agent-browser@${AGENT_BROWSER_VERSION}" \
|
|
43
44
|
"tus-js-client@${TUS_JS_CLIENT_VERSION}" "yaml@${YAML_VERSION}" \
|
|
45
|
+
"ws@${WS_VERSION}" \
|
|
44
46
|
&& test "$(node -p "require('/usr/local/lib/sellable-agent/node_modules/agent-browser/package.json').version")" = "${AGENT_BROWSER_VERSION}" \
|
|
45
47
|
&& test "$(node -p "require('/usr/local/lib/sellable-agent/node_modules/tus-js-client/package.json').version")" = "${TUS_JS_CLIENT_VERSION}" \
|
|
46
48
|
&& test "$(node -p "require('/usr/local/lib/sellable-agent/node_modules/yaml/package.json').version")" = "${YAML_VERSION}" \
|
|
49
|
+
&& test "$(node -p "require('/usr/local/lib/sellable-agent/node_modules/ws/package.json').version")" = "${WS_VERSION}" \
|
|
47
50
|
&& mkdir -p /usr/local/lib/sellable-agent/external-runtime/mcp/bin \
|
|
48
51
|
&& ln -s ../node_modules/.bin/sellable-mcp \
|
|
49
52
|
/usr/local/lib/sellable-agent/external-runtime/mcp/bin/sellable-mcp \
|
|
@@ -98,7 +101,7 @@ COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
|
|
|
98
101
|
COPY hermes-endpoint-cron.mjs /opt/sellable/bin/hermes-endpoint-cron
|
|
99
102
|
COPY fly-customer-image/model-auth-store.mjs /usr/local/lib/sellable-agent/fly-customer-image/model-auth-store.mjs
|
|
100
103
|
COPY fly-customer-image/hermes-runs-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs
|
|
101
|
-
COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs fly-customer-image/public-endpoint-guard.mjs fly-customer-image/workspace-connection-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/
|
|
104
|
+
COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs fly-customer-image/public-endpoint-guard.mjs fly-customer-image/workspace-connection-client.mjs fly-customer-image/slack-stand-in.mjs /usr/local/lib/sellable-agent/fly-customer-image/
|
|
102
105
|
COPY fly-customer-image/opener-stage-execution.mjs /usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs
|
|
103
106
|
COPY fly-customer-image/endpoint-uat-completion-gate.mjs /usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs
|
|
104
107
|
COPY fly-customer-image/customer-runtime-phase-telemetry.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import {
|
|
6
6
|
chmodSync,
|
|
7
7
|
closeSync,
|
|
@@ -109,6 +109,7 @@ import {
|
|
|
109
109
|
usesOpenerDraftStrategy,
|
|
110
110
|
} from "./opener-stage-execution.mjs";
|
|
111
111
|
import { createConfiguredRunsClient } from "./runs-api-connections.mjs";
|
|
112
|
+
import { startSlackStandIn } from "./slack-stand-in.mjs";
|
|
112
113
|
|
|
113
114
|
const DATA_ROOT = "/opt/data";
|
|
114
115
|
const PROFILES_ROOT = join(DATA_ROOT, "profiles");
|
|
@@ -132,9 +133,10 @@ const RUNTIME_GENERATION = Number(
|
|
|
132
133
|
const BOOT_SESSION_ID = randomUUID();
|
|
133
134
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.5";
|
|
134
135
|
const INSTALLER_PACKAGE =
|
|
135
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
136
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.838";
|
|
136
137
|
const MCP_PACKAGE =
|
|
137
|
-
process.env.SELLABLE_AGENT_MCP_PACKAGE ??
|
|
138
|
+
process.env.SELLABLE_AGENT_MCP_PACKAGE ??
|
|
139
|
+
"@sellable/mcp@0.1.982-wip.durableprep.20261013.1";
|
|
138
140
|
export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
|
|
139
141
|
const DEFAULT_PROFILE_BUNDLE_ROOT =
|
|
140
142
|
"/usr/local/share/sellable-agent/default-profile-bundle";
|
|
@@ -422,7 +424,7 @@ function exactArtifact(claim, type) {
|
|
|
422
424
|
.join("\n");
|
|
423
425
|
}
|
|
424
426
|
|
|
425
|
-
function compileDesired(claim, trustRootPem) {
|
|
427
|
+
export function compileDesired(claim, trustRootPem) {
|
|
426
428
|
const operation = claim?.operation;
|
|
427
429
|
const slack = claim?.runtimeContract?.slack;
|
|
428
430
|
const service = claim?.runtimeContract?.service;
|
|
@@ -432,6 +434,7 @@ function compileDesired(claim, trustRootPem) {
|
|
|
432
434
|
const customerProgram = claim?.runtimeContract?.customerProgram;
|
|
433
435
|
const selectedBindings = slack?.selectedBindings;
|
|
434
436
|
const toolInclude = service?.toolInclude;
|
|
437
|
+
const slackTransport = slack?.transport === "shared" ? "shared" : "dedicated";
|
|
435
438
|
if (
|
|
436
439
|
!operation ||
|
|
437
440
|
operation.targetRevisionId !== claim?.revision?.id ||
|
|
@@ -455,7 +458,10 @@ function compileDesired(claim, trustRootPem) {
|
|
|
455
458
|
selectedBindings.length < 1 ||
|
|
456
459
|
!Array.isArray(slack?.memberAllowlist) ||
|
|
457
460
|
!claim?.secretEnvelopes?.mcpService ||
|
|
458
|
-
|
|
461
|
+
(slackTransport === "shared"
|
|
462
|
+
? claim.secretEnvelopes.slackApp !== null ||
|
|
463
|
+
!/^A[A-Z0-9]{8,31}$/.test(slack.appId ?? "")
|
|
464
|
+
: !claim.secretEnvelopes.slackApp) ||
|
|
459
465
|
!claim?.secretEnvelopes?.slackBot ||
|
|
460
466
|
!claim?.secretEnvelopes?.modelCodex ||
|
|
461
467
|
!claim?.secretEnvelopes?.hermesApiServer
|
|
@@ -564,6 +570,8 @@ function compileDesired(claim, trustRootPem) {
|
|
|
564
570
|
soul: exactArtifact(claim, "SOUL"),
|
|
565
571
|
skill: exactArtifact(claim, "SKILL"),
|
|
566
572
|
}),
|
|
573
|
+
slackTransport,
|
|
574
|
+
slackAppId: slack.appId,
|
|
567
575
|
});
|
|
568
576
|
}
|
|
569
577
|
|
|
@@ -2834,7 +2842,92 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2834
2842
|
});
|
|
2835
2843
|
}
|
|
2836
2844
|
|
|
2837
|
-
|
|
2845
|
+
/** @param {any} options */
|
|
2846
|
+
export function createCustomerSlackRelay(options = {}) {
|
|
2847
|
+
const {
|
|
2848
|
+
getControlClient,
|
|
2849
|
+
getActiveRuntime,
|
|
2850
|
+
sleepImpl = sleep,
|
|
2851
|
+
idleMs = 1_000,
|
|
2852
|
+
maxBatch = 10,
|
|
2853
|
+
diagnostic = () => {},
|
|
2854
|
+
} = options;
|
|
2855
|
+
if (
|
|
2856
|
+
typeof getControlClient !== "function" ||
|
|
2857
|
+
typeof getActiveRuntime !== "function" ||
|
|
2858
|
+
typeof sleepImpl !== "function" ||
|
|
2859
|
+
typeof diagnostic !== "function"
|
|
2860
|
+
) {
|
|
2861
|
+
throw new Error("customer_slack_relay_configuration_rejected");
|
|
2862
|
+
}
|
|
2863
|
+
return Object.freeze({
|
|
2864
|
+
async run(signal) {
|
|
2865
|
+
// Outcomes are confirmed on the next claim; the server leases a batch
|
|
2866
|
+
// for 30 s and holds the agent's next batch until it is confirmed.
|
|
2867
|
+
let pending = [];
|
|
2868
|
+
let failures = 0;
|
|
2869
|
+
while (!signal.aborted) {
|
|
2870
|
+
try {
|
|
2871
|
+
const standIn = getActiveRuntime()?.slackStandIn;
|
|
2872
|
+
const ready = standIn?.status().warm === true;
|
|
2873
|
+
if (!ready && pending.length === 0) {
|
|
2874
|
+
await abortablePause(idleMs, signal, sleepImpl);
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
const results = pending.slice(0, 20);
|
|
2878
|
+
const { envelopes } = await getControlClient().claimSlackEnvelopes({
|
|
2879
|
+
max: ready ? maxBatch : 0,
|
|
2880
|
+
results,
|
|
2881
|
+
});
|
|
2882
|
+
pending = pending.slice(results.length);
|
|
2883
|
+
failures = 0;
|
|
2884
|
+
for (const [index, envelope] of envelopes.entries()) {
|
|
2885
|
+
// A malformed envelope can never be delivered: close it as
|
|
2886
|
+
// no_ack so it does not come back at the head of every batch.
|
|
2887
|
+
const outcome = await standIn.deliver(envelope).catch((error) => {
|
|
2888
|
+
if (error?.message !== "slack_envelope_malformed") throw error;
|
|
2889
|
+
diagnostic({ ok: false, code: "slack_envelope_malformed" });
|
|
2890
|
+
return "no_ack";
|
|
2891
|
+
});
|
|
2892
|
+
if (outcome === "released") {
|
|
2893
|
+
// Hermes never got this one: hand it and the rest back.
|
|
2894
|
+
for (const rest of envelopes.slice(index)) {
|
|
2895
|
+
pending.push({ id: rest.id, outcome: "released" });
|
|
2896
|
+
}
|
|
2897
|
+
break;
|
|
2898
|
+
}
|
|
2899
|
+
pending.push({ id: envelope.id, outcome });
|
|
2900
|
+
}
|
|
2901
|
+
if (envelopes.length === 0 && pending.length === 0) {
|
|
2902
|
+
await abortablePause(idleMs, signal, sleepImpl);
|
|
2903
|
+
}
|
|
2904
|
+
} catch (error) {
|
|
2905
|
+
failures = Math.min(failures + 1, 5);
|
|
2906
|
+
diagnostic({
|
|
2907
|
+
ok: false,
|
|
2908
|
+
code:
|
|
2909
|
+
error instanceof Error
|
|
2910
|
+
? redactRuntimeDiagnostic(error.message)
|
|
2911
|
+
: "customer_slack_relay_failed",
|
|
2912
|
+
});
|
|
2913
|
+
await abortablePause(
|
|
2914
|
+
Math.min(5_000, 500 * 2 ** (failures - 1)),
|
|
2915
|
+
signal,
|
|
2916
|
+
sleepImpl
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
},
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
async function consumeSecrets({
|
|
2925
|
+
claim,
|
|
2926
|
+
exchange,
|
|
2927
|
+
client,
|
|
2928
|
+
desired,
|
|
2929
|
+
slackStandIn,
|
|
2930
|
+
}) {
|
|
2838
2931
|
const identity = operationIdentity(claim, exchange);
|
|
2839
2932
|
const consume = async (descriptor) => {
|
|
2840
2933
|
const value = await client.consumeSecret({
|
|
@@ -2856,7 +2949,11 @@ async function consumeSecrets({ claim, exchange, client, desired }) {
|
|
|
2856
2949
|
const [mcpService, slackApp, slackBot, modelCodex, hermesApiServer] =
|
|
2857
2950
|
await Promise.all([
|
|
2858
2951
|
consume(claim.secretEnvelopes.mcpService),
|
|
2859
|
-
|
|
2952
|
+
// Shared: the server sends no app token; the per-boot local token the
|
|
2953
|
+
// stand-in accepts is installed on fd 8 instead.
|
|
2954
|
+
slackStandIn
|
|
2955
|
+
? slackStandIn.localAppToken
|
|
2956
|
+
: consume(claim.secretEnvelopes.slackApp),
|
|
2860
2957
|
consume(claim.secretEnvelopes.slackBot),
|
|
2861
2958
|
consume(claim.secretEnvelopes.modelCodex),
|
|
2862
2959
|
consume(claim.secretEnvelopes.hermesApiServer),
|
|
@@ -3236,9 +3333,21 @@ function slackApiOrigin() {
|
|
|
3236
3333
|
return parsed.origin;
|
|
3237
3334
|
}
|
|
3238
3335
|
|
|
3239
|
-
|
|
3336
|
+
/** @param {any} input */
|
|
3337
|
+
export function applicationRuntimeEnv({
|
|
3338
|
+
desired,
|
|
3339
|
+
runtimeConfig,
|
|
3340
|
+
secrets,
|
|
3341
|
+
slackStandIn,
|
|
3342
|
+
}) {
|
|
3240
3343
|
const env = {
|
|
3241
3344
|
...process.env,
|
|
3345
|
+
...(slackStandIn
|
|
3346
|
+
? {
|
|
3347
|
+
SELLABLE_AGENT_SLACK_TRANSPORT: "shared-local",
|
|
3348
|
+
SELLABLE_AGENT_SLACK_API_ORIGIN: slackStandIn.origin,
|
|
3349
|
+
}
|
|
3350
|
+
: {}),
|
|
3242
3351
|
HOME: DATA_ROOT,
|
|
3243
3352
|
HERMES_HOME: DATA_ROOT,
|
|
3244
3353
|
SELLABLE_HERMES_PROFILE_ID: desired.profileId,
|
|
@@ -3309,13 +3418,93 @@ async function stopNativeGateway(gateway) {
|
|
|
3309
3418
|
}
|
|
3310
3419
|
}
|
|
3311
3420
|
|
|
3312
|
-
|
|
3421
|
+
/**
|
|
3422
|
+
* One loopback Slack stand-in per boot, started for the first shared claim.
|
|
3423
|
+
* @param {any} options
|
|
3424
|
+
*/
|
|
3425
|
+
export function createSlackStandInSlot(options = {}) {
|
|
3426
|
+
const {
|
|
3427
|
+
startStandIn = startSlackStandIn,
|
|
3428
|
+
upstreamOrigin = slackApiOrigin,
|
|
3429
|
+
diagnostic = () => {},
|
|
3430
|
+
} = options;
|
|
3431
|
+
let current = null;
|
|
3432
|
+
return {
|
|
3433
|
+
async ensure({ slackTransport, appId }) {
|
|
3434
|
+
if (slackTransport !== "shared") return null;
|
|
3435
|
+
if (!current) {
|
|
3436
|
+
// The slot is the local app token's only producer; the stand-in
|
|
3437
|
+
// accepts it on apps.connections.open and Hermes reads it from fd 8.
|
|
3438
|
+
const localAppToken = `xapp-1-${randomBytes(24).toString("hex")}`;
|
|
3439
|
+
const instance = await startStandIn({
|
|
3440
|
+
localAppToken,
|
|
3441
|
+
ticket: randomBytes(24).toString("hex"),
|
|
3442
|
+
appId,
|
|
3443
|
+
upstreamOrigin: upstreamOrigin(),
|
|
3444
|
+
diagnostic,
|
|
3445
|
+
});
|
|
3446
|
+
current = { ...instance, localAppToken };
|
|
3447
|
+
}
|
|
3448
|
+
return current;
|
|
3449
|
+
},
|
|
3450
|
+
async close() {
|
|
3451
|
+
const instance = current;
|
|
3452
|
+
current = null;
|
|
3453
|
+
await instance?.close();
|
|
3454
|
+
},
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
/**
|
|
3459
|
+
* The idle loop's crash restart of a stopped gateway.
|
|
3460
|
+
* @param {any} activeRuntime
|
|
3461
|
+
* @param {{ startGateway?: (input: any) => Promise<any> }} [options]
|
|
3462
|
+
*/
|
|
3463
|
+
export async function restartStoppedNativeGateway(
|
|
3464
|
+
activeRuntime,
|
|
3465
|
+
{ startGateway = startNativeGateway } = {}
|
|
3466
|
+
) {
|
|
3467
|
+
if (
|
|
3468
|
+
activeRuntime?.action !== "INSTANTIATE" &&
|
|
3469
|
+
activeRuntime?.gateway &&
|
|
3470
|
+
!activeRuntime.gateway.readback().running
|
|
3471
|
+
) {
|
|
3472
|
+
// The transport comes from activeRuntime, never from its claim: a BACKUP
|
|
3473
|
+
// claim replaces activeRuntime.claim and carries no runtime contract.
|
|
3474
|
+
return await startGateway({
|
|
3475
|
+
desired: activeRuntime.desired,
|
|
3476
|
+
runtimeConfig: activeRuntime.runtimeConfig,
|
|
3477
|
+
secrets: activeRuntime.secrets,
|
|
3478
|
+
slackTransport: activeRuntime.slackTransport,
|
|
3479
|
+
slackStandIn: activeRuntime.slackStandIn,
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
return activeRuntime?.gateway;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
async function startNativeGateway({
|
|
3486
|
+
desired,
|
|
3487
|
+
runtimeConfig,
|
|
3488
|
+
secrets,
|
|
3489
|
+
slackTransport,
|
|
3490
|
+
slackStandIn,
|
|
3491
|
+
}) {
|
|
3492
|
+
// A shared runtime launched with the dedicated env would send the local
|
|
3493
|
+
// app token to Slack and go silent while still looking healthy.
|
|
3494
|
+
if (slackTransport === "shared" && !slackStandIn) {
|
|
3495
|
+
throw new Error("customer_slack_stand_in_required");
|
|
3496
|
+
}
|
|
3313
3497
|
const secretRoot = join(SECRET_ROOT, desired.profileId);
|
|
3314
3498
|
const launch = buildNativeHermesLaunch({
|
|
3315
3499
|
profileRoot: runtimeConfig.runtimeHome,
|
|
3316
3500
|
secretsRoot: secretRoot,
|
|
3317
3501
|
profileId: desired.profileId,
|
|
3318
|
-
env: applicationRuntimeEnv({
|
|
3502
|
+
env: applicationRuntimeEnv({
|
|
3503
|
+
desired,
|
|
3504
|
+
runtimeConfig,
|
|
3505
|
+
secrets,
|
|
3506
|
+
slackStandIn,
|
|
3507
|
+
}),
|
|
3319
3508
|
});
|
|
3320
3509
|
const appDescriptor = openSync(
|
|
3321
3510
|
launch.inheritedFileDescriptors.find((item) => item.fd === 8).path,
|
|
@@ -3571,17 +3760,26 @@ async function proveApiServerReadiness({
|
|
|
3571
3760
|
throw new Error("customer_api_server_readiness_rejected");
|
|
3572
3761
|
}
|
|
3573
3762
|
|
|
3763
|
+
/** @param {any} input */
|
|
3574
3764
|
export async function startProvenNativeGateway({
|
|
3575
3765
|
desired,
|
|
3576
3766
|
runtimeConfig,
|
|
3577
3767
|
secrets,
|
|
3768
|
+
slackTransport,
|
|
3769
|
+
slackStandIn,
|
|
3578
3770
|
startGateway = startNativeGateway,
|
|
3579
3771
|
proveReadiness = proveApiServerReadiness,
|
|
3580
3772
|
stopGateway = stopNativeGateway,
|
|
3581
3773
|
}) {
|
|
3582
3774
|
let gateway = null;
|
|
3583
3775
|
try {
|
|
3584
|
-
gateway = await startGateway({
|
|
3776
|
+
gateway = await startGateway({
|
|
3777
|
+
desired,
|
|
3778
|
+
runtimeConfig,
|
|
3779
|
+
secrets,
|
|
3780
|
+
slackTransport,
|
|
3781
|
+
slackStandIn,
|
|
3782
|
+
});
|
|
3585
3783
|
const gatewayReadback = gateway.readback();
|
|
3586
3784
|
if (!gatewayReadback.running) {
|
|
3587
3785
|
throw new Error("customer_native_gateway_readback_rejected");
|
|
@@ -3618,8 +3816,14 @@ async function proveProvisionedApplication({
|
|
|
3618
3816
|
runtimeConfig,
|
|
3619
3817
|
secrets,
|
|
3620
3818
|
profileDigest,
|
|
3819
|
+
slackStandIn,
|
|
3621
3820
|
}) {
|
|
3622
|
-
const runtimeEnv = applicationRuntimeEnv({
|
|
3821
|
+
const runtimeEnv = applicationRuntimeEnv({
|
|
3822
|
+
desired,
|
|
3823
|
+
runtimeConfig,
|
|
3824
|
+
secrets,
|
|
3825
|
+
slackStandIn,
|
|
3826
|
+
});
|
|
3623
3827
|
const materializedModelAuth = runtimeConfig.modelAuthState.materialized;
|
|
3624
3828
|
const [hermesVersion, mcpVersion, hermesModelAuth] = await Promise.all([
|
|
3625
3829
|
run(HERMES, ["--version"], { env: runtimeEnv, timeoutMs: 180_000 }),
|
|
@@ -3746,14 +3950,22 @@ async function proveApplication({
|
|
|
3746
3950
|
secrets,
|
|
3747
3951
|
profileDigest,
|
|
3748
3952
|
runtimeAuthorityScope,
|
|
3953
|
+
slackTransport,
|
|
3954
|
+
slackStandIn,
|
|
3749
3955
|
}) {
|
|
3750
3956
|
const provisioned = await proveProvisionedApplication({
|
|
3751
3957
|
desired,
|
|
3752
3958
|
runtimeConfig,
|
|
3753
3959
|
secrets,
|
|
3754
3960
|
profileDigest,
|
|
3961
|
+
slackStandIn,
|
|
3962
|
+
});
|
|
3963
|
+
const runtimeEnv = applicationRuntimeEnv({
|
|
3964
|
+
desired,
|
|
3965
|
+
runtimeConfig,
|
|
3966
|
+
secrets,
|
|
3967
|
+
slackStandIn,
|
|
3755
3968
|
});
|
|
3756
|
-
const runtimeEnv = applicationRuntimeEnv({ desired, runtimeConfig, secrets });
|
|
3757
3969
|
const doctor = customerRuntimeDoctorRequired(action, {
|
|
3758
3970
|
runtimeAuthorityScope,
|
|
3759
3971
|
directReadinessProven: true,
|
|
@@ -3789,8 +4001,11 @@ async function proveApplication({
|
|
|
3789
4001
|
) {
|
|
3790
4002
|
throw new Error("customer_slack_identity_rejected");
|
|
3791
4003
|
}
|
|
4004
|
+
// Shared machines hold no Slack app token: Socket Mode is proven against
|
|
4005
|
+
// the loopback stand-in only, never against Slack.
|
|
4006
|
+
const shared = slackTransport === "shared";
|
|
3792
4007
|
const socketResponse = await fetch(
|
|
3793
|
-
`${slackApiOrigin()}/api/apps.connections.open`,
|
|
4008
|
+
`${shared ? slackStandIn.origin : slackApiOrigin()}/api/apps.connections.open`,
|
|
3794
4009
|
{
|
|
3795
4010
|
method: "POST",
|
|
3796
4011
|
headers: {
|
|
@@ -3806,7 +4021,7 @@ async function proveApplication({
|
|
|
3806
4021
|
!socketResponse.ok ||
|
|
3807
4022
|
socket?.ok !== true ||
|
|
3808
4023
|
typeof socket.url !== "string" ||
|
|
3809
|
-
|
|
4024
|
+
!(shared ? /^ws:\/\/127\.0\.0\.1:/ : /^wss?:\/\//).test(socket.url)
|
|
3810
4025
|
) {
|
|
3811
4026
|
throw new Error("customer_slack_socket_identity_rejected");
|
|
3812
4027
|
}
|
|
@@ -3815,7 +4030,20 @@ async function proveApplication({
|
|
|
3815
4030
|
desired,
|
|
3816
4031
|
runtimeConfig,
|
|
3817
4032
|
secrets,
|
|
4033
|
+
slackTransport,
|
|
4034
|
+
slackStandIn,
|
|
3818
4035
|
});
|
|
4036
|
+
if (shared) {
|
|
4037
|
+
let connected = false;
|
|
4038
|
+
for (let attempt = 0; attempt < 240 && !connected; attempt += 1) {
|
|
4039
|
+
connected = slackStandIn.status().hermesConnected;
|
|
4040
|
+
if (!connected) await sleep(500);
|
|
4041
|
+
}
|
|
4042
|
+
if (!connected) {
|
|
4043
|
+
await stopNativeGateway(gateway);
|
|
4044
|
+
throw new Error("customer_slack_stand_in_not_connected");
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
3819
4047
|
return {
|
|
3820
4048
|
...provisioned,
|
|
3821
4049
|
hermesOutput: `${provisioned.hermesOutput}\n${doctor.stdout}`,
|
|
@@ -3994,6 +4222,7 @@ async function processOperation({
|
|
|
3994
4222
|
client,
|
|
3995
4223
|
identity,
|
|
3996
4224
|
reattestAuthority,
|
|
4225
|
+
slackStandInSlot,
|
|
3997
4226
|
}) {
|
|
3998
4227
|
const phaseTelemetry = createCustomerRuntimePhaseTelemetry({
|
|
3999
4228
|
bootSessionId: BOOT_SESSION_ID,
|
|
@@ -4001,15 +4230,25 @@ async function processOperation({
|
|
|
4001
4230
|
});
|
|
4002
4231
|
const runtimeAuthorityScope = claim.runtimeAuthority?.scope ?? null;
|
|
4003
4232
|
const trustRootPem = readFileSync(TRUST_ROOT, "utf8");
|
|
4004
|
-
const { desired, toolInclude } = compileDesired(
|
|
4233
|
+
const { desired, toolInclude, slackTransport, slackAppId } = compileDesired(
|
|
4234
|
+
claim,
|
|
4235
|
+
trustRootPem
|
|
4236
|
+
);
|
|
4005
4237
|
mkdirSync(PROFILES_ROOT, { recursive: true, mode: 0o700 });
|
|
4006
4238
|
chmodSync(PROFILES_ROOT, 0o700);
|
|
4239
|
+
// Shared only: the stand-in listens before consumeSecrets (it produces the
|
|
4240
|
+
// local app token) and before every Hermes run that reads the seam.
|
|
4241
|
+
const slackStandIn = await slackStandInSlot.ensure({
|
|
4242
|
+
slackTransport,
|
|
4243
|
+
appId: slackAppId,
|
|
4244
|
+
});
|
|
4007
4245
|
const secrets = await phaseTelemetry.run("runtime_secrets", async () => {
|
|
4008
4246
|
const consumed = await consumeSecrets({
|
|
4009
4247
|
claim,
|
|
4010
4248
|
exchange,
|
|
4011
4249
|
client,
|
|
4012
4250
|
desired,
|
|
4251
|
+
slackStandIn,
|
|
4013
4252
|
});
|
|
4014
4253
|
await publishManagedAuth({
|
|
4015
4254
|
schemaVersion: "sellable-managed-auth/v1",
|
|
@@ -4175,6 +4414,8 @@ async function processOperation({
|
|
|
4175
4414
|
secrets,
|
|
4176
4415
|
profileDigest: pristineProfile.manifest.profileDigest,
|
|
4177
4416
|
runtimeAuthorityScope,
|
|
4417
|
+
slackTransport,
|
|
4418
|
+
slackStandIn,
|
|
4178
4419
|
})
|
|
4179
4420
|
);
|
|
4180
4421
|
await phaseTelemetry.run("customer_program_crons", async () => {
|
|
@@ -4254,6 +4495,8 @@ async function processOperation({
|
|
|
4254
4495
|
gateway: proof.gateway ?? null,
|
|
4255
4496
|
proof,
|
|
4256
4497
|
modelHealthReporter: null,
|
|
4498
|
+
slackTransport,
|
|
4499
|
+
slackStandIn,
|
|
4257
4500
|
};
|
|
4258
4501
|
if (claim.runtimeAuthority && receipt.observedLifecycle === "ON") {
|
|
4259
4502
|
const servingObservation = customerServingObservation(
|
|
@@ -4431,6 +4674,14 @@ async function main() {
|
|
|
4431
4674
|
diagnostic: (value) => process.stderr.write(`${JSON.stringify(value)}\n`),
|
|
4432
4675
|
});
|
|
4433
4676
|
const endpointLoop = endpointRelay.run(controller.signal);
|
|
4677
|
+
const diagnostic = (value) =>
|
|
4678
|
+
process.stderr.write(`${JSON.stringify(value)}\n`);
|
|
4679
|
+
const slackStandInSlot = createSlackStandInSlot({ diagnostic });
|
|
4680
|
+
const slackLoop = createCustomerSlackRelay({
|
|
4681
|
+
getControlClient: () => client,
|
|
4682
|
+
getActiveRuntime: () => activeRuntime,
|
|
4683
|
+
diagnostic,
|
|
4684
|
+
}).run(controller.signal);
|
|
4434
4685
|
const beginShutdownSnapshot = () => {
|
|
4435
4686
|
if (!activeRuntime || shutdownMemorySnapshot) return;
|
|
4436
4687
|
const runtime = activeRuntime;
|
|
@@ -4537,16 +4788,9 @@ async function main() {
|
|
|
4537
4788
|
),
|
|
4538
4789
|
});
|
|
4539
4790
|
}
|
|
4540
|
-
if (
|
|
4541
|
-
activeRuntime
|
|
4542
|
-
|
|
4543
|
-
!activeRuntime.gateway.readback().running
|
|
4544
|
-
) {
|
|
4545
|
-
activeRuntime.gateway = await startNativeGateway({
|
|
4546
|
-
desired: activeRuntime.desired,
|
|
4547
|
-
runtimeConfig: activeRuntime.runtimeConfig,
|
|
4548
|
-
secrets: activeRuntime.secrets,
|
|
4549
|
-
});
|
|
4791
|
+
if (activeRuntime) {
|
|
4792
|
+
activeRuntime.gateway =
|
|
4793
|
+
await restartStoppedNativeGateway(activeRuntime);
|
|
4550
4794
|
}
|
|
4551
4795
|
await sleep(1_000);
|
|
4552
4796
|
continue;
|
|
@@ -4620,6 +4864,7 @@ async function main() {
|
|
|
4620
4864
|
client,
|
|
4621
4865
|
identity,
|
|
4622
4866
|
reattestAuthority: () => operationHeartbeat.reattest(),
|
|
4867
|
+
slackStandInSlot,
|
|
4623
4868
|
});
|
|
4624
4869
|
soulClaimTracker = createSoulClaimObservationTracker({
|
|
4625
4870
|
runtimeId: RUNTIME_ID,
|
|
@@ -4650,7 +4895,7 @@ async function main() {
|
|
|
4650
4895
|
}
|
|
4651
4896
|
beginShutdownSnapshot();
|
|
4652
4897
|
const finalMemorySnapshot = await shutdownMemorySnapshot;
|
|
4653
|
-
await endpointLoop;
|
|
4898
|
+
await Promise.all([endpointLoop, slackLoop]);
|
|
4654
4899
|
if (finalMemorySnapshot?.ok === false) {
|
|
4655
4900
|
process.stderr.write(
|
|
4656
4901
|
`${JSON.stringify({
|
|
@@ -4660,6 +4905,7 @@ async function main() {
|
|
|
4660
4905
|
);
|
|
4661
4906
|
}
|
|
4662
4907
|
await stopNativeGateway(activeRuntime?.gateway);
|
|
4908
|
+
await slackStandInSlot.close();
|
|
4663
4909
|
await activeRuntime?.modelHealthReporter?.stop();
|
|
4664
4910
|
if (activeRuntime?.desired.profileId) {
|
|
4665
4911
|
await clearManagedAuth(activeRuntime.desired.profileId);
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// The machine-local Slack Socket Mode stand-in for a shared-app runtime.
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
|
|
6
|
+
// The shared app declares /sellable; Hermes registers /hermes. Every other
|
|
7
|
+
// command reaches Hermes under its own name.
|
|
8
|
+
export const SHARED_SLACK_COMMAND_ALIASES = Object.freeze({
|
|
9
|
+
"/sellable": "/hermes",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const record = (value) =>
|
|
13
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
const malformed = () => new Error("slack_envelope_malformed");
|
|
15
|
+
|
|
16
|
+
// One stored Slack inbox row becomes one Socket Mode frame
|
|
17
|
+
// (slack_sdk/socket_mode/request.py). Events keep Slack's exact body bytes;
|
|
18
|
+
// interactive and slash bodies are Slack's form, decoded as Bolt decodes it.
|
|
19
|
+
export function toSocketModeFrame({
|
|
20
|
+
id,
|
|
21
|
+
kind,
|
|
22
|
+
body,
|
|
23
|
+
slackRetryNum,
|
|
24
|
+
slackRetryReason,
|
|
25
|
+
}) {
|
|
26
|
+
if (
|
|
27
|
+
typeof id !== "string" ||
|
|
28
|
+
!id ||
|
|
29
|
+
typeof body !== "string" ||
|
|
30
|
+
!Number.isInteger(slackRetryNum) ||
|
|
31
|
+
slackRetryNum < 0 ||
|
|
32
|
+
typeof slackRetryReason !== "string"
|
|
33
|
+
)
|
|
34
|
+
throw malformed();
|
|
35
|
+
if (kind === "EVENTS_API") {
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(body);
|
|
39
|
+
} catch {
|
|
40
|
+
throw malformed();
|
|
41
|
+
}
|
|
42
|
+
if (!record(parsed)) throw malformed();
|
|
43
|
+
return (
|
|
44
|
+
`{"envelope_id":${JSON.stringify(id)},"type":"events_api",` +
|
|
45
|
+
`"accepts_response_payload":false,"retry_attempt":${slackRetryNum},` +
|
|
46
|
+
`"retry_reason":${JSON.stringify(slackRetryReason)},"payload":${body}}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const frame = (type, payload) =>
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
envelope_id: id,
|
|
52
|
+
type,
|
|
53
|
+
accepts_response_payload: true,
|
|
54
|
+
retry_attempt: slackRetryNum,
|
|
55
|
+
retry_reason: slackRetryReason,
|
|
56
|
+
payload,
|
|
57
|
+
});
|
|
58
|
+
if (kind === "INTERACTIVE") {
|
|
59
|
+
let payload;
|
|
60
|
+
try {
|
|
61
|
+
payload = JSON.parse(new URLSearchParams(body).get("payload"));
|
|
62
|
+
} catch {
|
|
63
|
+
throw malformed();
|
|
64
|
+
}
|
|
65
|
+
if (!record(payload)) throw malformed();
|
|
66
|
+
return frame("interactive", payload);
|
|
67
|
+
}
|
|
68
|
+
if (kind === "SLASH_COMMANDS") {
|
|
69
|
+
const fields = Object.fromEntries(new URLSearchParams(body));
|
|
70
|
+
if (typeof fields.command !== "string" || !fields.command)
|
|
71
|
+
throw malformed();
|
|
72
|
+
fields.command =
|
|
73
|
+
SHARED_SLACK_COMMAND_ALIASES[fields.command] ?? fields.command;
|
|
74
|
+
return frame("slash_commands", fields);
|
|
75
|
+
}
|
|
76
|
+
throw malformed();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const json = (res, status, body) => {
|
|
80
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
81
|
+
res.end(JSON.stringify(body));
|
|
82
|
+
};
|
|
83
|
+
// Length-checked first: timingSafeEqual throws on unequal lengths.
|
|
84
|
+
const same = (provided, expected) => {
|
|
85
|
+
const a = Buffer.from(provided ?? "");
|
|
86
|
+
const b = Buffer.from(expected);
|
|
87
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
88
|
+
};
|
|
89
|
+
const OPEN = 1; // WebSocket.OPEN
|
|
90
|
+
// Node's parser admits request targets URL rejects (for example "http://[").
|
|
91
|
+
// A throw in either handler would take the customer runtime down.
|
|
92
|
+
const targetOf = (raw) => {
|
|
93
|
+
try {
|
|
94
|
+
return new URL(raw, "http://127.0.0.1");
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const API_METHOD = /^\/api\/([A-Za-z0-9_.]+)$/;
|
|
100
|
+
const HOP_BY_HOP = new Set([
|
|
101
|
+
"host",
|
|
102
|
+
"content-length",
|
|
103
|
+
"connection",
|
|
104
|
+
"transfer-encoding",
|
|
105
|
+
"keep-alive",
|
|
106
|
+
"upgrade",
|
|
107
|
+
]);
|
|
108
|
+
const SLACK_CHANNEL_ID = /^[CDG][A-Z0-9]+$/;
|
|
109
|
+
// slack_sdk sends some arguments in the query string, and the status call
|
|
110
|
+
// names the channel channel_id.
|
|
111
|
+
const channelOf = (body, contentType, searchParams) => {
|
|
112
|
+
let fields = {};
|
|
113
|
+
try {
|
|
114
|
+
fields = /json/.test(contentType ?? "")
|
|
115
|
+
? JSON.parse(body)
|
|
116
|
+
: Object.fromEntries(new URLSearchParams(body));
|
|
117
|
+
} catch {}
|
|
118
|
+
const channel =
|
|
119
|
+
fields?.channel ??
|
|
120
|
+
fields?.channel_id ??
|
|
121
|
+
searchParams.get("channel") ??
|
|
122
|
+
searchParams.get("channel_id");
|
|
123
|
+
return typeof channel === "string" && SLACK_CHANNEL_ID.test(channel)
|
|
124
|
+
? channel
|
|
125
|
+
: null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Hermes connects here with the per-boot local app token as if this were
|
|
129
|
+
// Slack (docs.slack.dev/apis/events-api/using-socket-mode). The token and
|
|
130
|
+
// ticket never appear in a diagnostic.
|
|
131
|
+
export async function startSlackStandIn(options = {}) {
|
|
132
|
+
const {
|
|
133
|
+
localAppToken,
|
|
134
|
+
ticket,
|
|
135
|
+
appId,
|
|
136
|
+
upstreamOrigin,
|
|
137
|
+
fetchImpl = fetch,
|
|
138
|
+
ackTimeoutMs = 10_000,
|
|
139
|
+
upstreamTimeoutMs = 30_000,
|
|
140
|
+
warmGraceMs = 8_000,
|
|
141
|
+
now = Date.now,
|
|
142
|
+
diagnostic = () => {},
|
|
143
|
+
} = options;
|
|
144
|
+
if (
|
|
145
|
+
typeof localAppToken !== "string" ||
|
|
146
|
+
!localAppToken ||
|
|
147
|
+
typeof ticket !== "string" ||
|
|
148
|
+
!ticket ||
|
|
149
|
+
typeof appId !== "string" ||
|
|
150
|
+
typeof upstreamOrigin !== "string" ||
|
|
151
|
+
typeof fetchImpl !== "function" ||
|
|
152
|
+
typeof now !== "function" ||
|
|
153
|
+
typeof diagnostic !== "function"
|
|
154
|
+
)
|
|
155
|
+
throw new Error("slack_stand_in_configuration_rejected");
|
|
156
|
+
// Imported here so the Admin image, which loads customer-runtime.mjs but
|
|
157
|
+
// never starts the stand-in, does not need ws.
|
|
158
|
+
const { WebSocketServer } = await import("ws");
|
|
159
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
160
|
+
let current = null;
|
|
161
|
+
let connectedAt = 0;
|
|
162
|
+
let port = 0;
|
|
163
|
+
// One in-flight envelope per deliver() call (the relay is sequential). Each
|
|
164
|
+
// waiter knows the socket it was sent on, so a replaced socket releases its
|
|
165
|
+
// own envelope for redelivery instead of letting it time out as no_ack.
|
|
166
|
+
const waiters = new Map();
|
|
167
|
+
|
|
168
|
+
const server = createServer(async (req, res) => {
|
|
169
|
+
const target = targetOf(req.url);
|
|
170
|
+
if (target?.pathname === "/api/apps.connections.open") {
|
|
171
|
+
const ok = same(req.headers.authorization, `Bearer ${localAppToken}`);
|
|
172
|
+
diagnostic({
|
|
173
|
+
kind: "sellable_slack_stand_in_api",
|
|
174
|
+
method: "apps.connections.open",
|
|
175
|
+
status: 200,
|
|
176
|
+
ok,
|
|
177
|
+
error: ok ? null : "invalid_auth",
|
|
178
|
+
channel: null,
|
|
179
|
+
});
|
|
180
|
+
// The SDK reconnects with its cached URL, so the URL is reusable.
|
|
181
|
+
return json(
|
|
182
|
+
res,
|
|
183
|
+
200,
|
|
184
|
+
ok
|
|
185
|
+
? { ok, url: `ws://127.0.0.1:${port}/link/?ticket=${ticket}` }
|
|
186
|
+
: { ok, error: "invalid_auth" }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const method = target && API_METHOD.exec(target.pathname)?.[1];
|
|
190
|
+
if (!method) return json(res, 404, { ok: false, error: "unknown_method" });
|
|
191
|
+
if (req.method !== "GET" && req.method !== "POST")
|
|
192
|
+
return json(res, 405, { ok: false, error: "method_not_allowed" });
|
|
193
|
+
// Every other call goes to Slack with Hermes' own token, and Slack's
|
|
194
|
+
// answer comes back as is. If Slack resets or hangs, Hermes' connection
|
|
195
|
+
// is dropped so its client retries as it would against Slack. This
|
|
196
|
+
// handler never rejects: the runtime has no unhandledRejection handler.
|
|
197
|
+
const line = {
|
|
198
|
+
kind: "sellable_slack_stand_in_api",
|
|
199
|
+
method,
|
|
200
|
+
status: null,
|
|
201
|
+
ok: null,
|
|
202
|
+
error: null,
|
|
203
|
+
channel: null,
|
|
204
|
+
};
|
|
205
|
+
try {
|
|
206
|
+
const parts = [];
|
|
207
|
+
for await (const part of req) parts.push(part);
|
|
208
|
+
const body = Buffer.concat(parts);
|
|
209
|
+
line.channel = channelOf(
|
|
210
|
+
String(body),
|
|
211
|
+
req.headers["content-type"],
|
|
212
|
+
target.searchParams
|
|
213
|
+
);
|
|
214
|
+
const headers = {};
|
|
215
|
+
for (const [name, value] of Object.entries(req.headers))
|
|
216
|
+
if (!HOP_BY_HOP.has(name)) headers[name] = value;
|
|
217
|
+
const upstream = await fetchImpl(
|
|
218
|
+
`${upstreamOrigin}/api/${method}${target.search}`,
|
|
219
|
+
{
|
|
220
|
+
method: req.method,
|
|
221
|
+
headers,
|
|
222
|
+
body: req.method === "POST" ? body : undefined,
|
|
223
|
+
signal: AbortSignal.timeout(upstreamTimeoutMs),
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
const answer = Buffer.from(await upstream.arrayBuffer());
|
|
227
|
+
line.status = upstream.status;
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(String(answer));
|
|
230
|
+
if (typeof parsed?.ok === "boolean") line.ok = parsed.ok;
|
|
231
|
+
if (typeof parsed?.error === "string") line.error = parsed.error;
|
|
232
|
+
} catch {}
|
|
233
|
+
// fetch has already decoded the body, so content-encoding is not
|
|
234
|
+
// copied; only the headers Hermes' client reads come back.
|
|
235
|
+
const out = {};
|
|
236
|
+
for (const name of ["content-type", "retry-after"]) {
|
|
237
|
+
const value = upstream.headers.get(name);
|
|
238
|
+
if (value !== null) out[name] = value;
|
|
239
|
+
}
|
|
240
|
+
res.writeHead(upstream.status, out);
|
|
241
|
+
res.end(answer);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
line.error =
|
|
244
|
+
error?.name === "TimeoutError"
|
|
245
|
+
? "upstream_timeout"
|
|
246
|
+
: "upstream_unavailable";
|
|
247
|
+
req.socket.destroy();
|
|
248
|
+
}
|
|
249
|
+
diagnostic(line);
|
|
250
|
+
});
|
|
251
|
+
server.on("upgrade", (req, socket, head) => {
|
|
252
|
+
socket.on("error", () => socket.destroy());
|
|
253
|
+
const target = targetOf(req.url);
|
|
254
|
+
if (
|
|
255
|
+
target?.pathname !== "/link/" ||
|
|
256
|
+
!same(target.searchParams.get("ticket"), ticket)
|
|
257
|
+
) {
|
|
258
|
+
socket.once("finish", () => socket.destroy());
|
|
259
|
+
socket.end("HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
263
|
+
// Newest connection wins; Hermes closes its old socket first anyway.
|
|
264
|
+
current?.close(1000);
|
|
265
|
+
current = ws;
|
|
266
|
+
connectedAt = now();
|
|
267
|
+
diagnostic({ kind: "sellable_slack_stand_in_socket", event: "open" });
|
|
268
|
+
ws.on("error", () => {});
|
|
269
|
+
// Acks resolve their waiter; ack payloads are dropped (the inbox already
|
|
270
|
+
// answered Slack). Nothing here may throw.
|
|
271
|
+
ws.on("message", (data, isBinary) => {
|
|
272
|
+
if (isBinary) return;
|
|
273
|
+
let frame;
|
|
274
|
+
try {
|
|
275
|
+
frame = JSON.parse(String(data));
|
|
276
|
+
} catch {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
waiters.get(frame?.envelope_id)?.settle("delivered");
|
|
280
|
+
});
|
|
281
|
+
ws.on("close", () => {
|
|
282
|
+
if (current === ws) current = null;
|
|
283
|
+
for (const waiter of [...waiters.values()])
|
|
284
|
+
if (waiter.ws === ws) waiter.settle("released");
|
|
285
|
+
diagnostic({ kind: "sellable_slack_stand_in_socket", event: "close" });
|
|
286
|
+
});
|
|
287
|
+
ws.send(
|
|
288
|
+
JSON.stringify({
|
|
289
|
+
type: "hello",
|
|
290
|
+
num_connections: 1,
|
|
291
|
+
debug_info: { host: "sellable-local-stand-in" },
|
|
292
|
+
connection_info: { app_id: appId },
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
server.listen(0, "127.0.0.1");
|
|
298
|
+
await once(server, "listening");
|
|
299
|
+
port = server.address().port;
|
|
300
|
+
return {
|
|
301
|
+
origin: `http://127.0.0.1:${port}`,
|
|
302
|
+
port,
|
|
303
|
+
// delivered on Hermes' ack, no_ack past the deadline (Bolt never acks an
|
|
304
|
+
// envelope no listener matches), released when no socket takes it.
|
|
305
|
+
async deliver(envelope) {
|
|
306
|
+
const frame = toSocketModeFrame(envelope);
|
|
307
|
+
const ws = current;
|
|
308
|
+
const outcome = await new Promise((resolve) => {
|
|
309
|
+
if (ws?.readyState !== OPEN) return resolve("released");
|
|
310
|
+
const waiter = {
|
|
311
|
+
ws,
|
|
312
|
+
settle(result) {
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
if (waiters.get(envelope.id) === waiter)
|
|
315
|
+
waiters.delete(envelope.id);
|
|
316
|
+
resolve(result);
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
const timer = setTimeout(() => waiter.settle("no_ack"), ackTimeoutMs);
|
|
320
|
+
waiters.set(envelope.id, waiter);
|
|
321
|
+
ws.send(frame);
|
|
322
|
+
});
|
|
323
|
+
if (outcome !== "delivered")
|
|
324
|
+
diagnostic({
|
|
325
|
+
kind: "sellable_slack_stand_in_delivery",
|
|
326
|
+
envelopeId: envelope.id,
|
|
327
|
+
outcome,
|
|
328
|
+
});
|
|
329
|
+
return outcome;
|
|
330
|
+
},
|
|
331
|
+
status() {
|
|
332
|
+
const hermesConnected = current?.readyState === OPEN;
|
|
333
|
+
return {
|
|
334
|
+
hermesConnected,
|
|
335
|
+
warm: hermesConnected && now() - connectedAt >= warmGraceMs,
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
close: () =>
|
|
339
|
+
new Promise((resolve) => {
|
|
340
|
+
for (const ws of wss.clients) ws.terminate();
|
|
341
|
+
server.close(resolve);
|
|
342
|
+
server.closeAllConnections();
|
|
343
|
+
}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
@@ -1277,6 +1277,18 @@ export function createFlyCustomerControlClient({
|
|
|
1277
1277
|
).claim ?? null
|
|
1278
1278
|
);
|
|
1279
1279
|
},
|
|
1280
|
+
async claimSlackEnvelopes({ max, results }) {
|
|
1281
|
+
return {
|
|
1282
|
+
envelopes:
|
|
1283
|
+
(
|
|
1284
|
+
await post(
|
|
1285
|
+
"slack-claim",
|
|
1286
|
+
"/api/v3/sellable-agent/worker/slack-claim",
|
|
1287
|
+
{ max, results }
|
|
1288
|
+
)
|
|
1289
|
+
).envelopes ?? [],
|
|
1290
|
+
};
|
|
1291
|
+
},
|
|
1280
1292
|
async heartbeatEndpoint(identityBody) {
|
|
1281
1293
|
return post(
|
|
1282
1294
|
"endpoint-heartbeat",
|
|
@@ -301,6 +301,37 @@ def _sellable_agent_slack_api_kwargs():
|
|
|
301
301
|
return {"base_url": origin.rstrip("/") + "/api/"}
|
|
302
302
|
`;
|
|
303
303
|
|
|
304
|
+
// Shared-app runtimes reach Slack through the machine-local stand-in
|
|
305
|
+
// (fly-customer-image/slack-stand-in.mjs). Both keys are required, and the
|
|
306
|
+
// origin must be the stand-in's own loopback listener.
|
|
307
|
+
const SLACK_SHARED_LOOPBACK_API_HELPER = replaceExactlyOnce(
|
|
308
|
+
SLACK_DEDICATED_TEST_API_HELPER,
|
|
309
|
+
"def _sellable_agent_slack_api_kwargs():\n",
|
|
310
|
+
`def _sellable_agent_slack_api_kwargs():
|
|
311
|
+
if (
|
|
312
|
+
os.getenv("SELLABLE_AGENT_RUNTIME", "") == "1"
|
|
313
|
+
and os.getenv("SELLABLE_AGENT_SLACK_TRANSPORT", "") == "shared-local"
|
|
314
|
+
):
|
|
315
|
+
origin = os.getenv("SELLABLE_AGENT_SLACK_API_ORIGIN", "")
|
|
316
|
+
from urllib.parse import urlparse
|
|
317
|
+
parsed = urlparse(origin)
|
|
318
|
+
if (
|
|
319
|
+
parsed.scheme != "http"
|
|
320
|
+
or parsed.hostname != "127.0.0.1"
|
|
321
|
+
or not parsed.port
|
|
322
|
+
or parsed.username
|
|
323
|
+
or parsed.password
|
|
324
|
+
or parsed.path not in {"", "/"}
|
|
325
|
+
or parsed.params
|
|
326
|
+
or parsed.query
|
|
327
|
+
or parsed.fragment
|
|
328
|
+
):
|
|
329
|
+
raise RuntimeError("Sellable shared Slack stand-in origin is invalid")
|
|
330
|
+
return {"base_url": origin.rstrip("/") + "/api/"}
|
|
331
|
+
`,
|
|
332
|
+
"shared Slack stand-in API seam"
|
|
333
|
+
);
|
|
334
|
+
|
|
304
335
|
const SLACK_MESSAGE_UNFURL_FILE_HELPER = `
|
|
305
336
|
# ${HERMES_AGENT_BRIDGE_MARKER}: recover files from bounded allowed-channel Slack message unfurls.
|
|
306
337
|
async def _sellable_agent_recover_slack_message_unfurl_files(
|
|
@@ -1557,15 +1588,20 @@ def getenv(name, default=""):
|
|
|
1557
1588
|
|
|
1558
1589
|
if (
|
|
1559
1590
|
patchKind === "slack-adapter-dedicated-v021" ||
|
|
1560
|
-
patchKind === "slack-adapter-dedicated-v0213"
|
|
1591
|
+
patchKind === "slack-adapter-dedicated-v0213" ||
|
|
1592
|
+
patchKind === "slack-adapter-shared-v0215"
|
|
1561
1593
|
) {
|
|
1562
1594
|
// v0.21.3 moved credential lookup into a profile-scoped helper. Preserve
|
|
1563
1595
|
// that upstream isolation outside the dedicated Sellable runtime.
|
|
1564
|
-
const scopedCredential = patchKind
|
|
1596
|
+
const scopedCredential = patchKind !== "slack-adapter-dedicated-v021";
|
|
1565
1597
|
let patched = replaceExactlyOnce(
|
|
1566
1598
|
source,
|
|
1567
1599
|
"logger = logging.getLogger(__name__)\n",
|
|
1568
|
-
`logger = logging.getLogger(__name__)\n${SECRET_FD_HELPER}${
|
|
1600
|
+
`logger = logging.getLogger(__name__)\n${SECRET_FD_HELPER}${
|
|
1601
|
+
patchKind === "slack-adapter-shared-v0215"
|
|
1602
|
+
? SLACK_SHARED_LOOPBACK_API_HELPER
|
|
1603
|
+
: SLACK_DEDICATED_TEST_API_HELPER
|
|
1604
|
+
}${SLACK_MESSAGE_UNFURL_FILE_HELPER}`,
|
|
1569
1605
|
"v0.21 dedicated Slack helpers"
|
|
1570
1606
|
);
|
|
1571
1607
|
patched = replaceExactlyOnce(
|
|
@@ -2924,11 +2960,11 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V0215_CONTRACT = normalizeContract({
|
|
|
2924
2960
|
},
|
|
2925
2961
|
{
|
|
2926
2962
|
path: "plugins/platforms/slack/adapter.py",
|
|
2927
|
-
patchKind: "slack-adapter-
|
|
2963
|
+
patchKind: "slack-adapter-shared-v0215",
|
|
2928
2964
|
sourceSha256:
|
|
2929
2965
|
"3f2ec0cef97964bf4cb1e9791c0917a03cd1e73bb0bc3293768680cd6302f613",
|
|
2930
2966
|
patchedSha256:
|
|
2931
|
-
"
|
|
2967
|
+
"d4b95fa29bfd980a8e33819d3d918c53a2d3355b91904ba14e6941878d4233f8",
|
|
2932
2968
|
},
|
|
2933
2969
|
{
|
|
2934
2970
|
path: "tools/mcp_tool_handlers.py",
|
|
@@ -3028,7 +3064,7 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V0215_CONTRACT = normalizeContract({
|
|
|
3028
3064
|
},
|
|
3029
3065
|
],
|
|
3030
3066
|
contractDigest:
|
|
3031
|
-
"
|
|
3067
|
+
"3953f2f8c0982eaee52a64c6192115c4ee57f88ec4dd76ffa908043bdce61a8e",
|
|
3032
3068
|
});
|
|
3033
3069
|
|
|
3034
3070
|
function assertRelativeContractPath(path) {
|
|
@@ -38,8 +38,8 @@ 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.
|
|
42
|
-
const MCP_PACKAGE = "@sellable/mcp@0.1.
|
|
41
|
+
const INSTALLER_PACKAGE = "@sellable/install@0.1.838";
|
|
42
|
+
const MCP_PACKAGE = "@sellable/mcp@0.1.982-wip.durableprep.20261013.1";
|
|
43
43
|
const HERMES_VERSION = "0.18.0";
|
|
44
44
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
45
45
|
const SHA256_16 = /^[a-f0-9]{16}$/;
|
|
@@ -1546,8 +1546,9 @@ 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.
|
|
1550
|
-
pinned.mcpPackage !==
|
|
1549
|
+
pinned.installerPackage !== "@sellable/install@0.1.838" ||
|
|
1550
|
+
pinned.mcpPackage !==
|
|
1551
|
+
"@sellable/mcp@0.1.982-wip.durableprep.20261013.1" ||
|
|
1551
1552
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1553
|
policy.toolInclude.length === 0 ||
|
|
1553
1554
|
!Number.isSafeInteger(slack.generation) ||
|
|
@@ -313,9 +313,8 @@ function validateDesired(desired) {
|
|
|
313
313
|
desired.serviceCredentialGeneration < 1 ||
|
|
314
314
|
desired.hermesCli !== "hermes" ||
|
|
315
315
|
desired.hermesVersion !== "0.18.0" ||
|
|
316
|
-
desired.installerPackage !==
|
|
317
|
-
|
|
318
|
-
desired.mcpPackage !== "@sellable/mcp@0.1.981-wip.disqualify.20261012.1" ||
|
|
316
|
+
desired.installerPackage !== "@sellable/install@0.1.838" ||
|
|
317
|
+
desired.mcpPackage !== "@sellable/mcp@0.1.982-wip.durableprep.20261013.1" ||
|
|
319
318
|
!Array.isArray(desired.toolInclude) ||
|
|
320
319
|
desired.toolInclude.length === 0 ||
|
|
321
320
|
desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
|
|
@@ -25,8 +25,9 @@ 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.
|
|
29
|
-
export const PINNED_MCP_PACKAGE =
|
|
28
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.838";
|
|
29
|
+
export const PINNED_MCP_PACKAGE =
|
|
30
|
+
"@sellable/mcp@0.1.982-wip.durableprep.20261013.1";
|
|
30
31
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
32
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|
|
32
33
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sellable/install",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.838",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
41
41
|
"eastasianwidth": "0.2.0",
|
|
42
42
|
"tus-js-client": "^4.3.1",
|
|
43
|
+
"ws": "8.18.3",
|
|
43
44
|
"yaml": "^2.8.1"
|
|
44
45
|
},
|
|
45
46
|
"keywords": [
|