@sellable/install 0.1.795-wip.runs-api.20261004.1 → 0.1.796
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 +1 -1
- package/lib/sellable-agent/fly-admin-image/Dockerfile +2 -1
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +33 -0
- package/lib/sellable-agent/fly-customer-image/Dockerfile +1 -1
- package/lib/sellable-agent/fly-customer-image/claude-code-mcp-session.d.mts +1 -0
- package/lib/sellable-agent/fly-customer-image/claude-code-mcp-session.mjs +53 -0
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.d.mts +30 -1
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.mjs +137 -18
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +1 -1
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.d.mts +1 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.mjs +17 -0
- 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 +1 -1
- 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.796
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.957
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
9
|
ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.796" \
|
|
12
12
|
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.957" \
|
|
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.795-wip.runs-api.202610
|
|
|
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.796' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.957') 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.796', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.957', 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.796` is
|
|
7
7
|
published:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
@@ -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 /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/claude-code-mcp-session.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
|
|
@@ -276,6 +276,7 @@ RUN set -eux; \
|
|
|
276
276
|
/usr/local/lib/sellable-agent/fly-customer-image/endpoint-model-selection.mjs \
|
|
277
277
|
/usr/local/lib/sellable-agent/fly-customer-image/runs-api-connections.mjs \
|
|
278
278
|
/usr/local/lib/sellable-agent/fly-customer-image/claude-code-runs-api.mjs \
|
|
279
|
+
/usr/local/lib/sellable-agent/fly-customer-image/claude-code-mcp-session.mjs \
|
|
279
280
|
/usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs \
|
|
280
281
|
/usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs \
|
|
281
282
|
/usr/local/lib/sellable-agent/admin-native-gateway-supervisor.mjs \
|
|
@@ -901,6 +901,32 @@ function installAdminMcpRuntimeBinding(
|
|
|
901
901
|
);
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
+
// The profile's `sellable` MCP server exactly as Hermes runs it, so a Claude
|
|
905
|
+
// Code tool-lane run sees the same tools, through the same context proxy.
|
|
906
|
+
function profileSellableMcpServer(profileRoot) {
|
|
907
|
+
const path = join(profileRoot, "config.yaml");
|
|
908
|
+
const stat = lstatSync(path);
|
|
909
|
+
if (stat.isSymbolicLink() || !stat.isFile()) return null;
|
|
910
|
+
const document = parseDocument(readFileSync(path, "utf8"));
|
|
911
|
+
if (document.errors.length > 0) return null;
|
|
912
|
+
const server = document.toJS().mcp_servers?.sellable;
|
|
913
|
+
if (
|
|
914
|
+
server?.command !== NODE ||
|
|
915
|
+
JSON.stringify(server.args) !== JSON.stringify([MCP_CONTEXT_PROXY]) ||
|
|
916
|
+
!server.env ||
|
|
917
|
+
typeof server.env !== "object"
|
|
918
|
+
) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
command: NODE,
|
|
923
|
+
args: [MCP_CONTEXT_PROXY],
|
|
924
|
+
env: Object.fromEntries(
|
|
925
|
+
Object.entries(server.env).map(([key, value]) => [key, String(value)])
|
|
926
|
+
),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
904
930
|
function installAdminModelConfig(profileRoot, modelConfig) {
|
|
905
931
|
const path = join(profileRoot, "config.yaml");
|
|
906
932
|
const stat = lstatSync(path);
|
|
@@ -3822,6 +3848,13 @@ async function runAdminProduction() {
|
|
|
3822
3848
|
try {
|
|
3823
3849
|
customRunsServices = await startConfiguredClaudeRunsApis({
|
|
3824
3850
|
diagnostic: value => process.stderr.write(`${JSON.stringify(value)}\n`),
|
|
3851
|
+
toolLanes: {
|
|
3852
|
+
turnDescriptorPath: join(MCP_TURN_CONTEXT_ROOT, "turn-context.json"),
|
|
3853
|
+
server: () =>
|
|
3854
|
+
activeLaunch
|
|
3855
|
+
? profileSellableMcpServer(activeLaunch.profile.profileHome)
|
|
3856
|
+
: null,
|
|
3857
|
+
},
|
|
3825
3858
|
});
|
|
3826
3859
|
} catch {
|
|
3827
3860
|
process.stderr.write(`${JSON.stringify({ ok: false, code: "custom_runs_api_start_failed" })}\n`);
|
|
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
|
|
|
8
8
|
|
|
9
9
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.957
|
|
10
10
|
ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
|
|
11
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
11
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.796
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function tagEndpointSession(line: string, sessionId: string): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Claude Code's only MCP server for a tool-lane run. It starts the profile's
|
|
2
|
+
// own `sellable` server (the context proxy, with exactly its configured
|
|
3
|
+
// environment) and adds this run's endpoint session to every tools/list and
|
|
4
|
+
// tools/call. The proxy then loads the run's signed turn descriptor, lists
|
|
5
|
+
// only the run's allowed tools and stamps the run identity on each call; the
|
|
6
|
+
// backend checks it again. Nothing in this file grants anything.
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
export function tagEndpointSession(line, sessionId) {
|
|
13
|
+
let message;
|
|
14
|
+
try {
|
|
15
|
+
message = JSON.parse(line);
|
|
16
|
+
} catch {
|
|
17
|
+
return line;
|
|
18
|
+
}
|
|
19
|
+
if (
|
|
20
|
+
!message ||
|
|
21
|
+
typeof message !== "object" ||
|
|
22
|
+
!["tools/list", "tools/call"].includes(message.method)
|
|
23
|
+
)
|
|
24
|
+
return line;
|
|
25
|
+
const params =
|
|
26
|
+
message.params && typeof message.params === "object" ? message.params : {};
|
|
27
|
+
// The proxy accepts exactly this shape and nothing else in _meta.
|
|
28
|
+
return JSON.stringify({
|
|
29
|
+
...message,
|
|
30
|
+
params: { ...params, _meta: { sellableEndpoint: { sessionId } } },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function main(specPath) {
|
|
35
|
+
const spec = JSON.parse(readFileSync(specPath, "utf8"));
|
|
36
|
+
if (!/^sellable_endpoint_[a-f0-9]{32}$/.test(spec.sessionId ?? ""))
|
|
37
|
+
throw new Error("claude_runs_mcp_session_rejected");
|
|
38
|
+
const child = spawn(spec.command, spec.args, {
|
|
39
|
+
stdio: ["pipe", "inherit", "ignore"],
|
|
40
|
+
env: spec.env,
|
|
41
|
+
});
|
|
42
|
+
child.once("exit", (code) => process.exit(code ?? 1));
|
|
43
|
+
child.stdin.on("error", () => {});
|
|
44
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
45
|
+
lines.on("line", (line) =>
|
|
46
|
+
child.stdin.write(`${tagEndpointSession(line, spec.sessionId)}\n`)
|
|
47
|
+
);
|
|
48
|
+
lines.on("close", () => child.stdin.end());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
52
|
+
main(process.argv[2]);
|
|
53
|
+
}
|
|
@@ -8,6 +8,18 @@ export type ClaudeRunsRequest = {
|
|
|
8
8
|
schema: Record<string, unknown> | null;
|
|
9
9
|
model: string;
|
|
10
10
|
effort: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
tools: ClaudeRunsTools | null;
|
|
13
|
+
};
|
|
14
|
+
export type ClaudeRunsToolServer = {
|
|
15
|
+
command: string;
|
|
16
|
+
args: string[];
|
|
17
|
+
env: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
export type ClaudeRunsTools = { names: string[]; server: ClaudeRunsToolServer };
|
|
20
|
+
export type ClaudeRunsToolLanes = {
|
|
21
|
+
turnDescriptorPath: string;
|
|
22
|
+
server(): ClaudeRunsToolServer | null;
|
|
11
23
|
};
|
|
12
24
|
export type ClaudeRunsResult = {
|
|
13
25
|
output: string;
|
|
@@ -24,19 +36,35 @@ export type ClaudeRunsApiOptions = {
|
|
|
24
36
|
};
|
|
25
37
|
audit?: (entry: Record<string, unknown>) => void;
|
|
26
38
|
maxConcurrency?: number;
|
|
39
|
+
maxToolConcurrency?: number;
|
|
40
|
+
toolLanes?: ClaudeRunsToolLanes | null;
|
|
27
41
|
};
|
|
28
42
|
export function validateClaudeRunsRequest(
|
|
29
43
|
body: unknown,
|
|
30
44
|
models: RunsApiConnection["models"]
|
|
31
45
|
): ClaudeRunsRequest;
|
|
46
|
+
export function endpointToolNames(options: {
|
|
47
|
+
turnDescriptorPath: string;
|
|
48
|
+
sessionId: string;
|
|
49
|
+
toolEndpointIds: string[];
|
|
50
|
+
}): string[] | null;
|
|
32
51
|
export function claudeInvocation(
|
|
33
52
|
request: ClaudeRunsRequest,
|
|
34
|
-
root: string
|
|
53
|
+
root: string,
|
|
54
|
+
mcpSpecPath?: string | null
|
|
35
55
|
): { command: string; args: string[]; options: SpawnOptions };
|
|
36
56
|
export function parseClaudeResult(
|
|
37
57
|
stdout: string,
|
|
38
58
|
request: ClaudeRunsRequest
|
|
39
59
|
): ClaudeRunsResult;
|
|
60
|
+
export function startClaudeRun(
|
|
61
|
+
request: ClaudeRunsRequest,
|
|
62
|
+
options: {
|
|
63
|
+
root: string;
|
|
64
|
+
spawnImpl?: (...args: any[]) => any;
|
|
65
|
+
timeoutMs?: number;
|
|
66
|
+
}
|
|
67
|
+
): { promise: Promise<ClaudeRunsResult>; stop(code?: string): void };
|
|
40
68
|
export function createClaudeRunsApi(
|
|
41
69
|
options: ClaudeRunsApiOptions
|
|
42
70
|
): ClaudeRunsServer;
|
|
@@ -45,4 +73,5 @@ export function startConfiguredClaudeRunsApis(options?: {
|
|
|
45
73
|
diagnostic?: (entry: Record<string, unknown>) => void;
|
|
46
74
|
createApi?: typeof createClaudeRunsApi;
|
|
47
75
|
readToken?: (path: string) => string;
|
|
76
|
+
toolLanes?: ClaudeRunsToolLanes | null;
|
|
48
77
|
}): Promise<{ stop(): Promise<void> }>;
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
appendFileSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
4
10
|
import { createServer } from "node:http";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import {
|
|
14
|
+
endpointHermesSessionId,
|
|
15
|
+
endpointMcpDescriptorPath,
|
|
16
|
+
} from "../mcp-context-proxy.mjs";
|
|
5
17
|
import { validateOpenerOutputContract } from "./hermes-runs-client.mjs";
|
|
6
18
|
import {
|
|
7
19
|
parseRunsApiConnections,
|
|
@@ -11,6 +23,9 @@ import {
|
|
|
11
23
|
const MAX_BODY = 320 * 1024;
|
|
12
24
|
const MAX_OUTPUT = 48 * 1024;
|
|
13
25
|
const MAX_CLI_OUTPUT = 512 * 1024;
|
|
26
|
+
const MCP_SESSION = fileURLToPath(
|
|
27
|
+
new URL("./claude-code-mcp-session.mjs", import.meta.url)
|
|
28
|
+
);
|
|
14
29
|
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
15
30
|
const record = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
16
31
|
|
|
@@ -74,10 +89,47 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
74
89
|
schema,
|
|
75
90
|
model: body.model,
|
|
76
91
|
effort: body.model_options.reasoning_effort,
|
|
92
|
+
sessionId: body.session_id,
|
|
93
|
+
tools: null,
|
|
77
94
|
};
|
|
78
95
|
}
|
|
79
96
|
|
|
80
|
-
|
|
97
|
+
/**
|
|
98
|
+
* A tool-lane run's own allowed tools, read from the signed turn descriptor
|
|
99
|
+
* the runtime installed for its session before submitting; null when the run
|
|
100
|
+
* is not on a tool endpoint. This only narrows: the context proxy verifies the
|
|
101
|
+
* descriptor itself before it lists or calls any tool.
|
|
102
|
+
*/
|
|
103
|
+
export function endpointToolNames({
|
|
104
|
+
turnDescriptorPath,
|
|
105
|
+
sessionId,
|
|
106
|
+
toolEndpointIds,
|
|
107
|
+
}) {
|
|
108
|
+
const path = endpointMcpDescriptorPath(turnDescriptorPath, sessionId);
|
|
109
|
+
if (!path) return null;
|
|
110
|
+
try {
|
|
111
|
+
if (!lstatSync(path).isFile())
|
|
112
|
+
throw new Error("claude_runs_tool_descriptor_rejected");
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error.code === "ENOENT") return null;
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
const body = JSON.parse(readFileSync(path, "utf8"))?.body;
|
|
118
|
+
if (!toolEndpointIds.includes(body?.endpointId)) return null;
|
|
119
|
+
const names = body.allowedToolNames;
|
|
120
|
+
if (
|
|
121
|
+
endpointHermesSessionId(body) !== sessionId ||
|
|
122
|
+
!Array.isArray(names) ||
|
|
123
|
+
!names.length ||
|
|
124
|
+
names.some(
|
|
125
|
+
(name) => typeof name !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(name)
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
throw new Error("claude_runs_tool_descriptor_rejected");
|
|
129
|
+
return [...names];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function claudeInvocation(request, root, mcpSpecPath = null) {
|
|
81
133
|
return {
|
|
82
134
|
command: `${root}/cli/node_modules/.bin/claude`,
|
|
83
135
|
args: [
|
|
@@ -97,7 +149,24 @@ export function claudeInvocation(request, root) {
|
|
|
97
149
|
"dontAsk",
|
|
98
150
|
"--strict-mcp-config",
|
|
99
151
|
"--mcp-config",
|
|
100
|
-
'
|
|
152
|
+
// A tool lane's only MCP server is the run's own session wrapper; its
|
|
153
|
+
// allowed tools are exactly the descriptor's. Every other run has none.
|
|
154
|
+
...(request.tools
|
|
155
|
+
? [
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
mcpServers: {
|
|
158
|
+
sellable: {
|
|
159
|
+
command: process.execPath,
|
|
160
|
+
args: [MCP_SESSION, mcpSpecPath],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
"--allowedTools",
|
|
165
|
+
request.tools.names
|
|
166
|
+
.map((name) => `mcp__sellable__${name}`)
|
|
167
|
+
.join(","),
|
|
168
|
+
]
|
|
169
|
+
: ['{"mcpServers":{}}']),
|
|
101
170
|
"--setting-sources",
|
|
102
171
|
"",
|
|
103
172
|
"--settings",
|
|
@@ -105,7 +174,7 @@ export function claudeInvocation(request, root) {
|
|
|
105
174
|
"--disable-slash-commands",
|
|
106
175
|
"--no-session-persistence",
|
|
107
176
|
"--max-turns",
|
|
108
|
-
"3",
|
|
177
|
+
request.tools ? "16" : "3",
|
|
109
178
|
"--system-prompt",
|
|
110
179
|
request.instructions,
|
|
111
180
|
],
|
|
@@ -166,12 +235,28 @@ export function startClaudeRun(
|
|
|
166
235
|
request,
|
|
167
236
|
{ root, spawnImpl = spawn, timeoutMs = 750_000 } = {}
|
|
168
237
|
) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
238
|
+
// The tool server's environment travels in a private per-run file, never
|
|
239
|
+
// in argv, and is removed when the run ends.
|
|
240
|
+
const mcpSpecPath = request.tools
|
|
241
|
+
? join(root, "work", `mcp-session-${randomBytes(12).toString("hex")}.json`)
|
|
242
|
+
: null;
|
|
243
|
+
if (mcpSpecPath)
|
|
244
|
+
writeFileSync(
|
|
245
|
+
mcpSpecPath,
|
|
246
|
+
JSON.stringify({ sessionId: request.sessionId, ...request.tools.server }),
|
|
247
|
+
{ mode: 0o600, flag: "wx" }
|
|
248
|
+
);
|
|
249
|
+
const removeSpec = () => {
|
|
250
|
+
if (mcpSpecPath) rmSync(mcpSpecPath, { force: true });
|
|
251
|
+
};
|
|
252
|
+
const invocation = claudeInvocation(request, root, mcpSpecPath);
|
|
253
|
+
let child;
|
|
254
|
+
try {
|
|
255
|
+
child = spawnImpl(invocation.command, invocation.args, invocation.options);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
removeSpec();
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
175
260
|
let reason = null;
|
|
176
261
|
let killTimer;
|
|
177
262
|
const stop = (code = "claude_runs_cancelled") => {
|
|
@@ -203,11 +288,13 @@ export function startClaudeRun(
|
|
|
203
288
|
child.stderr.on("data", () => {});
|
|
204
289
|
child.once("error", () => {
|
|
205
290
|
clearTimeout(timer);
|
|
291
|
+
removeSpec();
|
|
206
292
|
reject(new Error("claude_runs_cli_unavailable"));
|
|
207
293
|
});
|
|
208
294
|
child.once("close", (code) => {
|
|
209
295
|
clearTimeout(timer);
|
|
210
296
|
clearTimeout(killTimer);
|
|
297
|
+
removeSpec();
|
|
211
298
|
if (reason || code !== 0)
|
|
212
299
|
return reject(new Error(reason ?? "claude_runs_cli_failed"));
|
|
213
300
|
try {
|
|
@@ -231,9 +318,33 @@ export function createClaudeRunsApi({
|
|
|
231
318
|
startClaudeRun(request, { root: connection.localRoot }),
|
|
232
319
|
audit = () => {},
|
|
233
320
|
maxConcurrency = 2,
|
|
321
|
+
// Tool lanes (replies, follow-ups) have their own slots so a batch of
|
|
322
|
+
// openers can never crowd them out.
|
|
323
|
+
maxToolConcurrency = 2,
|
|
324
|
+
toolLanes = null,
|
|
234
325
|
} = {}) {
|
|
235
326
|
if (!/^[a-f0-9]{64}$/.test(token ?? ""))
|
|
236
327
|
throw new Error("claude_runs_key_rejected");
|
|
328
|
+
// The run's own tools and the profile's MCP server, or null for a tool-free run.
|
|
329
|
+
const resolveTools = (request) => {
|
|
330
|
+
if (!connection.toolEndpointIds || !toolLanes) return null;
|
|
331
|
+
const names = endpointToolNames({
|
|
332
|
+
turnDescriptorPath: toolLanes.turnDescriptorPath,
|
|
333
|
+
sessionId: request.sessionId,
|
|
334
|
+
toolEndpointIds: connection.toolEndpointIds,
|
|
335
|
+
});
|
|
336
|
+
if (!names) return null;
|
|
337
|
+
const server = toolLanes.server();
|
|
338
|
+
if (!server) throw new Error("claude_runs_tool_server_unavailable");
|
|
339
|
+
return {
|
|
340
|
+
names,
|
|
341
|
+
server: {
|
|
342
|
+
command: server.command,
|
|
343
|
+
args: [...server.args],
|
|
344
|
+
env: { PATH: "/usr/local/bin:/usr/bin:/bin", ...server.env },
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
};
|
|
237
348
|
const recordAudit = (entry) => {
|
|
238
349
|
try {
|
|
239
350
|
audit(entry);
|
|
@@ -242,7 +353,9 @@ export function createClaudeRunsApi({
|
|
|
242
353
|
}
|
|
243
354
|
};
|
|
244
355
|
const runs = new Map();
|
|
245
|
-
|
|
356
|
+
const inFlight = { plain: 0, tools: 0 };
|
|
357
|
+
const limits = { plain: maxConcurrency, tools: maxToolConcurrency };
|
|
358
|
+
const full = (pool) => inFlight[pool] >= limits[pool] || runs.size >= 200;
|
|
246
359
|
const json = (res, status, body) => {
|
|
247
360
|
res.writeHead(status, {
|
|
248
361
|
"content-type": "application/json",
|
|
@@ -269,6 +382,7 @@ export function createClaudeRunsApi({
|
|
|
269
382
|
recordAudit({
|
|
270
383
|
runId: run.run_id,
|
|
271
384
|
status: run.status,
|
|
385
|
+
toolLane: run.pool === "tools",
|
|
272
386
|
servedRuntime: run.runtime ?? null,
|
|
273
387
|
usage: run.usage ?? null,
|
|
274
388
|
durationMs: Date.now() - run.startedAt,
|
|
@@ -293,14 +407,15 @@ export function createClaudeRunsApi({
|
|
|
293
407
|
return json(res, 200, {
|
|
294
408
|
ok: true,
|
|
295
409
|
provider: "claude-code",
|
|
296
|
-
active,
|
|
410
|
+
active: inFlight.plain + inFlight.tools,
|
|
411
|
+
toolActive: inFlight.tools,
|
|
297
412
|
scope: {
|
|
298
413
|
workspaceId: connection.workspaceId,
|
|
299
414
|
agentId: connection.agentId,
|
|
300
415
|
},
|
|
301
416
|
});
|
|
302
417
|
if (req.method === "POST" && req.url === "/v1/runs") {
|
|
303
|
-
if (
|
|
418
|
+
if (full("plain") && (!connection.toolEndpointIds || full("tools")))
|
|
304
419
|
return json(res, 429, { error: "claude_runs_busy" });
|
|
305
420
|
let request;
|
|
306
421
|
try {
|
|
@@ -316,6 +431,7 @@ export function createClaudeRunsApi({
|
|
|
316
431
|
}
|
|
317
432
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
318
433
|
request = validateClaudeRunsRequest(body, connection.models);
|
|
434
|
+
request.tools = resolveTools(request);
|
|
319
435
|
} catch (error) {
|
|
320
436
|
recordAudit({
|
|
321
437
|
status: "request-rejected",
|
|
@@ -326,16 +442,17 @@ export function createClaudeRunsApi({
|
|
|
326
442
|
return json(res, 400, { error: "claude_runs_request_rejected" });
|
|
327
443
|
}
|
|
328
444
|
// Check again after reading: concurrent uploads cannot over-admit.
|
|
329
|
-
|
|
330
|
-
|
|
445
|
+
const pool = request.tools ? "tools" : "plain";
|
|
446
|
+
if (full(pool)) return json(res, 429, { error: "claude_runs_busy" });
|
|
331
447
|
const run = {
|
|
332
448
|
run_id: `run_${randomBytes(16).toString("hex")}`,
|
|
333
449
|
status: "running",
|
|
334
450
|
startedAt: Date.now(),
|
|
335
451
|
listeners: new Set(),
|
|
336
452
|
consumed: false,
|
|
453
|
+
pool,
|
|
337
454
|
};
|
|
338
|
-
|
|
455
|
+
inFlight[pool]++;
|
|
339
456
|
runs.set(run.run_id, run);
|
|
340
457
|
try {
|
|
341
458
|
run.execution = execute(request);
|
|
@@ -357,9 +474,9 @@ export function createClaudeRunsApi({
|
|
|
357
474
|
: { status: "failed", error: "claude_runs_generation_failed" }
|
|
358
475
|
)
|
|
359
476
|
)
|
|
360
|
-
.finally(() =>
|
|
477
|
+
.finally(() => inFlight[pool]--);
|
|
361
478
|
} catch {
|
|
362
|
-
|
|
479
|
+
inFlight[pool]--;
|
|
363
480
|
terminal(run, "run.failed", {
|
|
364
481
|
status: "failed",
|
|
365
482
|
error: "claude_runs_cli_unavailable",
|
|
@@ -438,6 +555,7 @@ export async function startConfiguredClaudeRunsApis({
|
|
|
438
555
|
diagnostic = () => {},
|
|
439
556
|
createApi = createClaudeRunsApi,
|
|
440
557
|
readToken = (path) => readFileSync(path, "utf8").trim(),
|
|
558
|
+
toolLanes = null,
|
|
441
559
|
} = {}) {
|
|
442
560
|
const services = [];
|
|
443
561
|
for (const connection of parseRunsApiConnections(
|
|
@@ -453,6 +571,7 @@ export async function startConfiguredClaudeRunsApis({
|
|
|
453
571
|
const server = createApi({
|
|
454
572
|
token,
|
|
455
573
|
connection,
|
|
574
|
+
toolLanes,
|
|
456
575
|
audit: (entry) =>
|
|
457
576
|
appendFileSync(
|
|
458
577
|
`${connection.localRoot}/audit.jsonl`,
|
|
@@ -126,7 +126,7 @@ const RUNTIME_GENERATION = Number(
|
|
|
126
126
|
const BOOT_SESSION_ID = randomUUID();
|
|
127
127
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.5";
|
|
128
128
|
const INSTALLER_PACKAGE =
|
|
129
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
129
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.796";
|
|
130
130
|
const MCP_PACKAGE =
|
|
131
131
|
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.957";
|
|
132
132
|
export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
|
|
@@ -13,6 +13,7 @@ export type RunsApiConnection = Readonly<{
|
|
|
13
13
|
reasoningEfforts: string[];
|
|
14
14
|
}>;
|
|
15
15
|
localRoot?: string;
|
|
16
|
+
toolEndpointIds?: string[];
|
|
16
17
|
}>;
|
|
17
18
|
export function parseRunsApiConnections(raw: unknown): RunsApiConnection[];
|
|
18
19
|
export function runsApiConnectionsForRuntime(
|
|
@@ -52,6 +52,7 @@ export function parseRunsApiConnections(raw) {
|
|
|
52
52
|
"workKinds",
|
|
53
53
|
"models",
|
|
54
54
|
"localRoot",
|
|
55
|
+
"toolEndpointIds",
|
|
55
56
|
].includes(key)
|
|
56
57
|
) ||
|
|
57
58
|
![entry.runtimeId, entry.workspaceId, entry.agentId].every(id) ||
|
|
@@ -123,12 +124,28 @@ export function parseRunsApiConnections(raw) {
|
|
|
123
124
|
)
|
|
124
125
|
fail();
|
|
125
126
|
}
|
|
127
|
+
// Runs on these endpoints get their own allowed tools from the image-owned
|
|
128
|
+
// service; every other run stays tool-free.
|
|
129
|
+
if (
|
|
130
|
+
entry.toolEndpointIds !== undefined &&
|
|
131
|
+
(entry.localRoot === undefined ||
|
|
132
|
+
!Array.isArray(entry.toolEndpointIds) ||
|
|
133
|
+
!entry.toolEndpointIds.length ||
|
|
134
|
+
new Set(entry.toolEndpointIds).size !== entry.toolEndpointIds.length ||
|
|
135
|
+
entry.toolEndpointIds.some(
|
|
136
|
+
(value) => !entry.endpointIds.includes(value)
|
|
137
|
+
))
|
|
138
|
+
)
|
|
139
|
+
fail();
|
|
126
140
|
return Object.freeze({
|
|
127
141
|
...entry,
|
|
128
142
|
baseUrl,
|
|
129
143
|
models,
|
|
130
144
|
endpointIds: [...entry.endpointIds],
|
|
131
145
|
workKinds: [...entry.workKinds],
|
|
146
|
+
...(entry.toolEndpointIds === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { toolEndpointIds: [...entry.toolEndpointIds] }),
|
|
132
149
|
});
|
|
133
150
|
});
|
|
134
151
|
}
|
|
@@ -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.796";
|
|
42
42
|
const MCP_PACKAGE = "@sellable/mcp@0.1.957";
|
|
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.796" ||
|
|
1550
1550
|
pinned.mcpPackage !== "@sellable/mcp@0.1.957" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
@@ -312,7 +312,7 @@ function validateDesired(desired) {
|
|
|
312
312
|
desired.serviceCredentialGeneration < 1 ||
|
|
313
313
|
desired.hermesCli !== "hermes" ||
|
|
314
314
|
desired.hermesVersion !== "0.18.0" ||
|
|
315
|
-
desired.installerPackage !== "@sellable/install@0.1.
|
|
315
|
+
desired.installerPackage !== "@sellable/install@0.1.796" ||
|
|
316
316
|
desired.mcpPackage !== "@sellable/mcp@0.1.957" ||
|
|
317
317
|
!Array.isArray(desired.toolInclude) ||
|
|
318
318
|
desired.toolInclude.length === 0 ||
|
|
@@ -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.796";
|
|
29
29
|
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.957";
|
|
30
30
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
31
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|