@sellable/install 0.1.802 → 0.1.804
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 +3 -1
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +19 -3
- package/lib/sellable-agent/fly-customer-image/Dockerfile +2 -2
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.d.mts +23 -3
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.mjs +216 -62
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +48 -22
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.d.mts +1 -0
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.mjs +21 -4
- package/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs +61 -5
- package/lib/sellable-agent/fly-customer-image/openai-runs-client.d.mts +3 -1
- package/lib/sellable-agent/fly-customer-image/openai-runs-client.mjs +4 -1
- package/lib/sellable-agent/fly-customer-image/public-endpoint-guard.d.mts +28 -0
- package/lib/sellable-agent/fly-customer-image/public-endpoint-guard.mjs +209 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.d.mts +1 -1
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.mjs +97 -24
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.mjs +11 -42
- package/lib/sellable-agent/fly-customer-image/workspace-connection-client.d.mts +22 -0
- package/lib/sellable-agent/fly-customer-image/workspace-connection-client.mjs +98 -0
- package/lib/sellable-agent/fly-runtime-identity.mjs +8 -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.804
|
|
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.804" \
|
|
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.802" \
|
|
|
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.804' || 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.804', 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.804` 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 fly-customer-image/claude-code-mcp-session.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.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 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/
|
|
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
|
|
@@ -277,6 +277,8 @@ RUN set -eux; \
|
|
|
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
279
|
/usr/local/lib/sellable-agent/fly-customer-image/claude-code-mcp-session.mjs \
|
|
280
|
+
/usr/local/lib/sellable-agent/fly-customer-image/public-endpoint-guard.mjs \
|
|
281
|
+
/usr/local/lib/sellable-agent/fly-customer-image/workspace-connection-client.mjs \
|
|
280
282
|
/usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs \
|
|
281
283
|
/usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs \
|
|
282
284
|
/usr/local/lib/sellable-agent/admin-native-gateway-supervisor.mjs \
|
|
@@ -61,9 +61,12 @@ import {
|
|
|
61
61
|
SELLABLE_BROWSER_TOOLSET_DEFAULT_MARKER,
|
|
62
62
|
sellableSlackToolsetsForMaterialization,
|
|
63
63
|
} from "../fly-capabilities-bridge.mjs";
|
|
64
|
+
import {
|
|
65
|
+
startConfiguredClaudeRunsApis,
|
|
66
|
+
startPublicClaudeRunsApi,
|
|
67
|
+
} from "../fly-customer-image/claude-code-runs-api.mjs";
|
|
64
68
|
import { createCustomerEndpointRelay } from "../fly-customer-image/customer-runtime.mjs";
|
|
65
69
|
import { createHermesSessionRetention } from "../fly-customer-image/hermes-runs-client.mjs";
|
|
66
|
-
import { startConfiguredClaudeRunsApis } from "../fly-customer-image/claude-code-runs-api.mjs";
|
|
67
70
|
import {
|
|
68
71
|
claimWithSoulObservation,
|
|
69
72
|
createFlyCustomerControlClient,
|
|
@@ -3847,7 +3850,7 @@ async function runAdminProduction() {
|
|
|
3847
3850
|
let customRunsServices = null;
|
|
3848
3851
|
try {
|
|
3849
3852
|
customRunsServices = await startConfiguredClaudeRunsApis({
|
|
3850
|
-
diagnostic: value => process.stderr.write(`${JSON.stringify(value)}\n`),
|
|
3853
|
+
diagnostic: (value) => process.stderr.write(`${JSON.stringify(value)}\n`),
|
|
3851
3854
|
toolLanes: {
|
|
3852
3855
|
turnDescriptorPath: join(MCP_TURN_CONTEXT_ROOT, "turn-context.json"),
|
|
3853
3856
|
server: () =>
|
|
@@ -3857,7 +3860,19 @@ async function runAdminProduction() {
|
|
|
3857
3860
|
},
|
|
3858
3861
|
});
|
|
3859
3862
|
} catch {
|
|
3860
|
-
process.stderr.write(
|
|
3863
|
+
process.stderr.write(
|
|
3864
|
+
`${JSON.stringify({ ok: false, code: "custom_runs_api_start_failed" })}\n`
|
|
3865
|
+
);
|
|
3866
|
+
}
|
|
3867
|
+
let publicRunsService = null;
|
|
3868
|
+
try {
|
|
3869
|
+
publicRunsService = await startPublicClaudeRunsApi({
|
|
3870
|
+
diagnostic: (value) => process.stderr.write(`${JSON.stringify(value)}\n`),
|
|
3871
|
+
});
|
|
3872
|
+
} catch {
|
|
3873
|
+
process.stderr.write(
|
|
3874
|
+
`${JSON.stringify({ ok: false, code: "public_runs_api_start_failed" })}\n`
|
|
3875
|
+
);
|
|
3861
3876
|
}
|
|
3862
3877
|
const endpointRelay = createCustomerEndpointRelay({
|
|
3863
3878
|
getControlClient: () => client,
|
|
@@ -4496,6 +4511,7 @@ async function runAdminProduction() {
|
|
|
4496
4511
|
}
|
|
4497
4512
|
await endpointLoop;
|
|
4498
4513
|
await customRunsServices?.stop();
|
|
4514
|
+
await publicRunsService?.stop();
|
|
4499
4515
|
await activeModelHealthReporter?.stop();
|
|
4500
4516
|
await stopNativeGateway(activeGateway);
|
|
4501
4517
|
await clearRuntimeSecrets();
|
|
@@ -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.804
|
|
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
|
|
@@ -98,7 +98,7 @@ COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
|
|
|
98
98
|
COPY hermes-endpoint-cron.mjs /opt/sellable/bin/hermes-endpoint-cron
|
|
99
99
|
COPY fly-customer-image/model-auth-store.mjs /usr/local/lib/sellable-agent/fly-customer-image/model-auth-store.mjs
|
|
100
100
|
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 /usr/local/lib/sellable-agent/fly-customer-image/
|
|
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/
|
|
102
102
|
COPY fly-customer-image/opener-stage-execution.mjs /usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs
|
|
103
103
|
COPY fly-customer-image/endpoint-uat-completion-gate.mjs /usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs
|
|
104
104
|
COPY fly-customer-image/customer-runtime-phase-telemetry.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs
|
|
@@ -10,7 +10,9 @@ export type ClaudeRunsRequest = {
|
|
|
10
10
|
model: string;
|
|
11
11
|
effort: string;
|
|
12
12
|
sessionId: string;
|
|
13
|
-
tools: ClaudeRunsTools | null;
|
|
13
|
+
tools: ClaudeRunsTools | ClaudeRunsRemoteTools | null;
|
|
14
|
+
/** Remote tools: the run's private MCP config file, written by the service. */
|
|
15
|
+
mcpConfigPath?: string;
|
|
14
16
|
};
|
|
15
17
|
export type ClaudeRunsToolServer = {
|
|
16
18
|
command: string;
|
|
@@ -18,6 +20,11 @@ export type ClaudeRunsToolServer = {
|
|
|
18
20
|
env: Record<string, string>;
|
|
19
21
|
};
|
|
20
22
|
export type ClaudeRunsTools = { names: string[]; server: ClaudeRunsToolServer };
|
|
23
|
+
/** The run's own remote MCP server and grant (public mode). */
|
|
24
|
+
export type ClaudeRunsRemoteTools = {
|
|
25
|
+
names: string[];
|
|
26
|
+
remote: { url: string; authorization: string };
|
|
27
|
+
};
|
|
21
28
|
export type ClaudeRunsToolLanes = {
|
|
22
29
|
turnDescriptorPath: string;
|
|
23
30
|
server(): ClaudeRunsToolServer | null;
|
|
@@ -30,7 +37,11 @@ export type ClaudeRunsResult = {
|
|
|
30
37
|
export type ClaudeRunsServer = Server & { stopActiveRuns(): Promise<void> };
|
|
31
38
|
export type ClaudeRunsApiOptions = {
|
|
32
39
|
token: string;
|
|
33
|
-
|
|
40
|
+
/** Required in loopback mode; the public listener has none. */
|
|
41
|
+
connection?: RunsApiConnection;
|
|
42
|
+
mode?: "loopback" | "public";
|
|
43
|
+
/** The CLI and account root; defaults to the connection's localRoot. */
|
|
44
|
+
root?: string;
|
|
34
45
|
execute?:
|
|
35
46
|
| ((request: ClaudeRunsRequest) => {
|
|
36
47
|
promise: Promise<ClaudeRunsResult>;
|
|
@@ -53,7 +64,8 @@ export type ClaudeRunsApiOptions = {
|
|
|
53
64
|
};
|
|
54
65
|
export function validateClaudeRunsRequest(
|
|
55
66
|
body: unknown,
|
|
56
|
-
|
|
67
|
+
/** null: the public rule (claude-* model, low/medium/high effort). */
|
|
68
|
+
models: RunsApiConnection["models"] | null
|
|
57
69
|
): ClaudeRunsRequest;
|
|
58
70
|
export function endpointToolNames(options: {
|
|
59
71
|
turnDescriptorPath: string;
|
|
@@ -96,6 +108,14 @@ export function startConfiguredClaudeRunsApis(options?: {
|
|
|
96
108
|
toolLanes?: ClaudeRunsToolLanes | null;
|
|
97
109
|
}): Promise<{ stop(): Promise<void> }>;
|
|
98
110
|
|
|
111
|
+
export function startPublicClaudeRunsApi(options?: {
|
|
112
|
+
env?: Record<string, string | undefined>;
|
|
113
|
+
diagnostic?: (entry: Record<string, unknown>) => void;
|
|
114
|
+
root?: string;
|
|
115
|
+
createApi?: typeof createClaudeRunsApi;
|
|
116
|
+
readToken?: (path: string) => string;
|
|
117
|
+
}): Promise<{ stop(): Promise<void> } | null>;
|
|
118
|
+
|
|
99
119
|
export function readClaudeAuthentication(options: {
|
|
100
120
|
root: string;
|
|
101
121
|
configDir?: string;
|
|
@@ -2,8 +2,10 @@ import { execFile, spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
appendFileSync,
|
|
5
|
+
chmodSync,
|
|
5
6
|
existsSync,
|
|
6
7
|
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
7
9
|
readdirSync,
|
|
8
10
|
readFileSync,
|
|
9
11
|
rmSync,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
endpointHermesSessionId,
|
|
17
19
|
endpointMcpDescriptorPath,
|
|
18
20
|
} from "../mcp-context-proxy.mjs";
|
|
21
|
+
import { normalizeRunsApiBaseUrl } from "./endpoint-model-selection.mjs";
|
|
19
22
|
import { validateOpenerOutputContract } from "./hermes-runs-client.mjs";
|
|
20
23
|
import {
|
|
21
24
|
parseRunsApiConnections,
|
|
@@ -33,6 +36,42 @@ const MCP_SESSION = fileURLToPath(
|
|
|
33
36
|
);
|
|
34
37
|
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
35
38
|
const record = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
39
|
+
// The public service (models null) has no model list: any Claude model, at
|
|
40
|
+
// the efforts the CLI takes.
|
|
41
|
+
const publicModel = (body) =>
|
|
42
|
+
typeof body.model === "string" &&
|
|
43
|
+
/^claude-[a-z0-9-]{1,100}$/.test(body.model) &&
|
|
44
|
+
["low", "medium", "high"].includes(body.model_options.reasoning_effort);
|
|
45
|
+
|
|
46
|
+
// The run's one remote MCP server, in the shape the Runs client sends it.
|
|
47
|
+
function remoteTools(tools) {
|
|
48
|
+
const entry = Array.isArray(tools) && tools.length === 1 ? tools[0] : null;
|
|
49
|
+
const url = normalizeRunsApiBaseUrl(entry?.server_url);
|
|
50
|
+
const names = entry?.allowed_tools;
|
|
51
|
+
if (
|
|
52
|
+
!record(entry) ||
|
|
53
|
+
Object.keys(entry).sort().join(",") !==
|
|
54
|
+
"allowed_tools,authorization,require_approval,server_label,server_url,type" ||
|
|
55
|
+
entry.type !== "mcp" ||
|
|
56
|
+
entry.server_label !== "sellable" ||
|
|
57
|
+
entry.require_approval !== "never" ||
|
|
58
|
+
!url?.startsWith("https://") ||
|
|
59
|
+
typeof entry.authorization !== "string" ||
|
|
60
|
+
!/^srt_[A-Za-z0-9-]{1,128}_[A-Za-z0-9_-]{43}$/.test(entry.authorization) ||
|
|
61
|
+
!Array.isArray(names) ||
|
|
62
|
+
names.length < 1 ||
|
|
63
|
+
names.length > 32 ||
|
|
64
|
+
new Set(names).size !== names.length ||
|
|
65
|
+
names.some(
|
|
66
|
+
(name) => typeof name !== "string" || !/^[a-z][a-z0-9_]{0,63}$/.test(name)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
throw new Error("claude_runs_tools_rejected");
|
|
70
|
+
return {
|
|
71
|
+
names: [...names],
|
|
72
|
+
remote: { url, authorization: entry.authorization },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
36
75
|
|
|
37
76
|
export function validateClaudeRunsRequest(body, models) {
|
|
38
77
|
const allowed = [
|
|
@@ -43,6 +82,7 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
43
82
|
"provider",
|
|
44
83
|
"model",
|
|
45
84
|
"model_options",
|
|
85
|
+
...(models === null ? ["tools"] : []),
|
|
46
86
|
];
|
|
47
87
|
if (
|
|
48
88
|
!record(body) ||
|
|
@@ -70,12 +110,14 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
70
110
|
(key) =>
|
|
71
111
|
!["reasoning_effort", "sellable_opener_output_contract"].includes(key)
|
|
72
112
|
) ||
|
|
73
|
-
!models
|
|
74
|
-
(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
!(models === null
|
|
114
|
+
? publicModel(body)
|
|
115
|
+
: models.some(
|
|
116
|
+
(model) =>
|
|
117
|
+
model.provider === body.provider &&
|
|
118
|
+
model.model === body.model &&
|
|
119
|
+
model.reasoningEfforts.includes(body.model_options.reasoning_effort)
|
|
120
|
+
))
|
|
79
121
|
)
|
|
80
122
|
throw new Error("claude_runs_request_rejected");
|
|
81
123
|
const contract = body.model_options.sellable_opener_output_contract;
|
|
@@ -95,7 +137,7 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
95
137
|
model: body.model,
|
|
96
138
|
effort: body.model_options.reasoning_effort,
|
|
97
139
|
sessionId: body.session_id,
|
|
98
|
-
tools: null,
|
|
140
|
+
tools: body.tools === undefined ? null : remoteTools(body.tools),
|
|
99
141
|
};
|
|
100
142
|
}
|
|
101
143
|
|
|
@@ -162,18 +204,22 @@ export function claudeInvocation(
|
|
|
162
204
|
"dontAsk",
|
|
163
205
|
"--strict-mcp-config",
|
|
164
206
|
"--mcp-config",
|
|
165
|
-
// A tool lane's only MCP server is the run's own session wrapper
|
|
166
|
-
//
|
|
207
|
+
// A tool lane's only MCP server is the run's own session wrapper, and a
|
|
208
|
+
// public run's is its own remote server, read from its private file.
|
|
209
|
+
// Either way its allowed tools are exactly the run's. Every other run
|
|
210
|
+
// has none.
|
|
167
211
|
...(request.tools
|
|
168
212
|
? [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
213
|
+
request.tools.remote
|
|
214
|
+
? request.mcpConfigPath
|
|
215
|
+
: JSON.stringify({
|
|
216
|
+
mcpServers: {
|
|
217
|
+
sellable: {
|
|
218
|
+
command: process.execPath,
|
|
219
|
+
args: [MCP_SESSION, mcpSpecPath],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
177
223
|
"--allowedTools",
|
|
178
224
|
request.tools.names
|
|
179
225
|
.map((name) => `mcp__sellable__${name}`)
|
|
@@ -332,6 +378,9 @@ export function claudeUsageLimit(stdout, now = Date.now()) {
|
|
|
332
378
|
return { retryAt };
|
|
333
379
|
}
|
|
334
380
|
|
|
381
|
+
// A failure reason the run may report: a fixed code, never raw CLI text.
|
|
382
|
+
const RUN_FAILURE_CODE = /^claude_runs_[a-z_]{1,48}$/;
|
|
383
|
+
|
|
335
384
|
export function parseClaudeResult(stdout, request) {
|
|
336
385
|
const result = JSON.parse(stdout);
|
|
337
386
|
const models = Object.keys(result.modelUsage ?? {});
|
|
@@ -341,15 +390,19 @@ export function parseClaudeResult(stdout, request) {
|
|
|
341
390
|
result.subtype !== "success" ||
|
|
342
391
|
(request.schema
|
|
343
392
|
? !record(result.structured_output)
|
|
344
|
-
: typeof result.result !== "string")
|
|
345
|
-
|
|
393
|
+
: typeof result.result !== "string")
|
|
394
|
+
)
|
|
395
|
+
throw new Error("claude_runs_output_rejected");
|
|
396
|
+
if ((result.permission_denials ?? []).length)
|
|
397
|
+
throw new Error("claude_runs_permission_denied");
|
|
398
|
+
if (
|
|
346
399
|
!models.length ||
|
|
347
400
|
models.some(
|
|
348
401
|
(model) =>
|
|
349
402
|
model !== request.model && !model.startsWith(`${request.model}-`)
|
|
350
403
|
)
|
|
351
404
|
)
|
|
352
|
-
throw new Error("
|
|
405
|
+
throw new Error("claude_runs_model_mismatch");
|
|
353
406
|
const output = request.schema
|
|
354
407
|
? JSON.stringify(result.structured_output)
|
|
355
408
|
: result.result;
|
|
@@ -379,7 +432,7 @@ export function startClaudeRun(
|
|
|
379
432
|
) {
|
|
380
433
|
// The tool server's environment travels in a private per-run file, never
|
|
381
434
|
// in argv, and is removed when the run ends.
|
|
382
|
-
const mcpSpecPath = request.tools
|
|
435
|
+
const mcpSpecPath = request.tools?.server
|
|
383
436
|
? join(root, "work", `mcp-session-${randomBytes(12).toString("hex")}.json`)
|
|
384
437
|
: null;
|
|
385
438
|
if (mcpSpecPath)
|
|
@@ -447,8 +500,12 @@ export function startClaudeRun(
|
|
|
447
500
|
return reject(new Error(reason ?? "claude_runs_cli_failed"));
|
|
448
501
|
try {
|
|
449
502
|
resolve(parseClaudeResult(stdout, request));
|
|
450
|
-
} catch {
|
|
451
|
-
reject(
|
|
503
|
+
} catch (error) {
|
|
504
|
+
reject(
|
|
505
|
+
error instanceof Error && RUN_FAILURE_CODE.test(error.message)
|
|
506
|
+
? error
|
|
507
|
+
: new Error("claude_runs_output_rejected")
|
|
508
|
+
);
|
|
452
509
|
}
|
|
453
510
|
});
|
|
454
511
|
child.stdin.on("error", () => {});
|
|
@@ -457,9 +514,38 @@ export function startClaudeRun(
|
|
|
457
514
|
return { promise, stop };
|
|
458
515
|
}
|
|
459
516
|
|
|
517
|
+
// A public run's remote server and grant, in a 0600 file under a 0700
|
|
518
|
+
// directory of its own: never in argv or the CLI's environment.
|
|
519
|
+
function writeRemoteMcpConfig(dir, remote) {
|
|
520
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
521
|
+
chmodSync(dir, 0o700);
|
|
522
|
+
const stat = lstatSync(dir);
|
|
523
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || (stat.mode & 0o077) !== 0)
|
|
524
|
+
throw new Error("claude_runs_run_directory_rejected");
|
|
525
|
+
const path = join(dir, "mcp.json");
|
|
526
|
+
writeFileSync(
|
|
527
|
+
path,
|
|
528
|
+
JSON.stringify({
|
|
529
|
+
mcpServers: {
|
|
530
|
+
sellable: {
|
|
531
|
+
type: "http",
|
|
532
|
+
url: remote.url,
|
|
533
|
+
headers: { Authorization: `Bearer ${remote.authorization}` },
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
}),
|
|
537
|
+
{ mode: 0o600, flag: "wx" }
|
|
538
|
+
);
|
|
539
|
+
return path;
|
|
540
|
+
}
|
|
541
|
+
|
|
460
542
|
export function createClaudeRunsApi({
|
|
461
543
|
token,
|
|
462
544
|
connection,
|
|
545
|
+
// "public": the Admin's internet-facing listener. The bearer is its only
|
|
546
|
+
// scope, and a run's tools come from its own request, never a local lane.
|
|
547
|
+
mode = "loopback",
|
|
548
|
+
root = connection?.localRoot,
|
|
463
549
|
execute = null,
|
|
464
550
|
audit = () => {},
|
|
465
551
|
maxConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY,
|
|
@@ -468,8 +554,8 @@ export function createClaudeRunsApi({
|
|
|
468
554
|
maxToolConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY,
|
|
469
555
|
toolLanes = null,
|
|
470
556
|
checkAuthentication = (configDir) =>
|
|
471
|
-
readClaudeAuthentication({ root
|
|
472
|
-
accounts = () => claudeAccountDirs(
|
|
557
|
+
readClaudeAuthentication({ root, configDir }),
|
|
558
|
+
accounts = () => claudeAccountDirs(root),
|
|
473
559
|
startRun = startClaudeRun,
|
|
474
560
|
now = Date.now,
|
|
475
561
|
} = {}) {
|
|
@@ -522,10 +608,7 @@ export function createClaudeRunsApi({
|
|
|
522
608
|
failure ??= "claude_runs_sign_in_required";
|
|
523
609
|
continue;
|
|
524
610
|
}
|
|
525
|
-
activeRun = startRun(request, {
|
|
526
|
-
root: connection.localRoot,
|
|
527
|
-
configDir,
|
|
528
|
-
});
|
|
611
|
+
activeRun = startRun(request, { root, configDir });
|
|
529
612
|
try {
|
|
530
613
|
return await activeRun.promise;
|
|
531
614
|
} catch (error) {
|
|
@@ -650,12 +733,21 @@ export function createClaudeRunsApi({
|
|
|
650
733
|
)
|
|
651
734
|
return json(res, 401, { error: "claude_runs_auth_required" });
|
|
652
735
|
if (
|
|
653
|
-
|
|
654
|
-
req.headers["x-sellable-
|
|
736
|
+
mode !== "public" &&
|
|
737
|
+
(req.headers["x-sellable-workspace-id"] !== connection.workspaceId ||
|
|
738
|
+
req.headers["x-sellable-agent-id"] !== connection.agentId)
|
|
655
739
|
)
|
|
656
740
|
return json(res, 403, { error: "claude_runs_scope_rejected" });
|
|
657
741
|
if (req.method === "GET" && req.url === "/health") {
|
|
658
742
|
const { authentication, accounts: accountHealth } = await readHealth();
|
|
743
|
+
// Public health names no accounts, load, or scope.
|
|
744
|
+
if (mode === "public")
|
|
745
|
+
return json(res, 200, {
|
|
746
|
+
ok: authentication === "READY",
|
|
747
|
+
authentication,
|
|
748
|
+
provider: "claude-code",
|
|
749
|
+
tools: ["mcp"],
|
|
750
|
+
});
|
|
659
751
|
return json(res, 200, {
|
|
660
752
|
ok: authentication === "READY",
|
|
661
753
|
authentication,
|
|
@@ -670,7 +762,7 @@ export function createClaudeRunsApi({
|
|
|
670
762
|
});
|
|
671
763
|
}
|
|
672
764
|
if (req.method === "POST" && req.url === "/v1/runs") {
|
|
673
|
-
if (full("plain") && (!toolLanes || full("tools")))
|
|
765
|
+
if (full("plain") && ((mode !== "public" && !toolLanes) || full("tools")))
|
|
674
766
|
return json(res, 429, { error: "claude_runs_busy" });
|
|
675
767
|
let request;
|
|
676
768
|
try {
|
|
@@ -685,8 +777,12 @@ export function createClaudeRunsApi({
|
|
|
685
777
|
chunks.push(chunk);
|
|
686
778
|
}
|
|
687
779
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
688
|
-
request = validateClaudeRunsRequest(
|
|
689
|
-
|
|
780
|
+
request = validateClaudeRunsRequest(
|
|
781
|
+
body,
|
|
782
|
+
mode === "public" ? null : connection.models
|
|
783
|
+
);
|
|
784
|
+
// A public run's tools are its own entry's, never a local lane's.
|
|
785
|
+
if (mode !== "public") request.tools = resolveTools(request);
|
|
690
786
|
} catch (error) {
|
|
691
787
|
recordAudit({
|
|
692
788
|
status: "request-rejected",
|
|
@@ -709,7 +805,18 @@ export function createClaudeRunsApi({
|
|
|
709
805
|
};
|
|
710
806
|
inFlight[pool]++;
|
|
711
807
|
runs.set(run.run_id, run);
|
|
808
|
+
const runDir = request.tools?.remote
|
|
809
|
+
? join(root, "work", "runs", run.run_id)
|
|
810
|
+
: null;
|
|
811
|
+
const removeRunDir = () => {
|
|
812
|
+
if (runDir) rmSync(runDir, { recursive: true, force: true });
|
|
813
|
+
};
|
|
712
814
|
try {
|
|
815
|
+
if (runDir)
|
|
816
|
+
request.mcpConfigPath = writeRemoteMcpConfig(
|
|
817
|
+
runDir,
|
|
818
|
+
request.tools.remote
|
|
819
|
+
);
|
|
713
820
|
run.execution = executeRun(request);
|
|
714
821
|
run.execution.promise
|
|
715
822
|
.then(
|
|
@@ -728,19 +835,21 @@ export function createClaudeRunsApi({
|
|
|
728
835
|
? { status: "cancelled" }
|
|
729
836
|
: {
|
|
730
837
|
status: "failed",
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
"claude_runs_usage_limited",
|
|
735
|
-
].includes(error.message)
|
|
838
|
+
// The reason, so a failure can be told apart later;
|
|
839
|
+
// anything else the CLI threw stays generic.
|
|
840
|
+
error: RUN_FAILURE_CODE.test(error?.message ?? "")
|
|
736
841
|
? error.message
|
|
737
842
|
: "claude_runs_generation_failed",
|
|
738
843
|
}
|
|
739
844
|
)
|
|
740
845
|
)
|
|
741
|
-
.finally(() =>
|
|
846
|
+
.finally(() => {
|
|
847
|
+
inFlight[pool]--;
|
|
848
|
+
removeRunDir();
|
|
849
|
+
});
|
|
742
850
|
} catch {
|
|
743
851
|
inFlight[pool]--;
|
|
852
|
+
removeRunDir();
|
|
744
853
|
terminal(run, "run.failed", {
|
|
745
854
|
status: "failed",
|
|
746
855
|
error: "claude_runs_cli_unavailable",
|
|
@@ -812,13 +921,34 @@ export function createClaudeRunsApi({
|
|
|
812
921
|
return server;
|
|
813
922
|
}
|
|
814
923
|
|
|
924
|
+
const readKey = (path) => readFileSync(path, "utf8").trim();
|
|
925
|
+
// Every listener audits to its own root and stops its runs before closing.
|
|
926
|
+
const auditTo = (root) => (entry) =>
|
|
927
|
+
appendFileSync(
|
|
928
|
+
`${root}/audit.jsonl`,
|
|
929
|
+
`${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`,
|
|
930
|
+
{ mode: 0o600 }
|
|
931
|
+
);
|
|
932
|
+
const listen = (server, port, host) =>
|
|
933
|
+
new Promise((resolve, reject) => {
|
|
934
|
+
server.once("error", reject);
|
|
935
|
+
server.listen(port, host, resolve);
|
|
936
|
+
});
|
|
937
|
+
async function stopServer(server) {
|
|
938
|
+
await server.stopActiveRuns();
|
|
939
|
+
await new Promise((resolve) => {
|
|
940
|
+
server.close(resolve);
|
|
941
|
+
server.closeAllConnections();
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
815
945
|
// The Admin runtime owns these listeners for its lifetime. Image-owned startup
|
|
816
946
|
// plus the persistent CLI/auth directory makes normal restarts repeatable.
|
|
817
947
|
export async function startConfiguredClaudeRunsApis({
|
|
818
948
|
env = process.env,
|
|
819
949
|
diagnostic = () => {},
|
|
820
950
|
createApi = createClaudeRunsApi,
|
|
821
|
-
readToken =
|
|
951
|
+
readToken = readKey,
|
|
822
952
|
toolLanes = null,
|
|
823
953
|
} = {}) {
|
|
824
954
|
const services = [];
|
|
@@ -836,21 +966,13 @@ export async function startConfiguredClaudeRunsApis({
|
|
|
836
966
|
token,
|
|
837
967
|
connection,
|
|
838
968
|
toolLanes,
|
|
839
|
-
audit: (
|
|
840
|
-
appendFileSync(
|
|
841
|
-
`${connection.localRoot}/audit.jsonl`,
|
|
842
|
-
`${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`,
|
|
843
|
-
{ mode: 0o600 }
|
|
844
|
-
),
|
|
845
|
-
});
|
|
846
|
-
await new Promise((resolve, reject) => {
|
|
847
|
-
server.once("error", reject);
|
|
848
|
-
server.listen(
|
|
849
|
-
Number(new URL(connection.baseUrl).port),
|
|
850
|
-
"127.0.0.1",
|
|
851
|
-
resolve
|
|
852
|
-
);
|
|
969
|
+
audit: auditTo(connection.localRoot),
|
|
853
970
|
});
|
|
971
|
+
await listen(
|
|
972
|
+
server,
|
|
973
|
+
Number(new URL(connection.baseUrl).port),
|
|
974
|
+
"127.0.0.1"
|
|
975
|
+
);
|
|
854
976
|
services.push(server);
|
|
855
977
|
diagnostic({
|
|
856
978
|
ok: true,
|
|
@@ -863,13 +985,45 @@ export async function startConfiguredClaudeRunsApis({
|
|
|
863
985
|
}
|
|
864
986
|
return {
|
|
865
987
|
async stop() {
|
|
866
|
-
for (const server of services)
|
|
867
|
-
await server.stopActiveRuns();
|
|
868
|
-
await new Promise((resolve) => {
|
|
869
|
-
server.close(resolve);
|
|
870
|
-
server.closeAllConnections();
|
|
871
|
-
});
|
|
872
|
-
}
|
|
988
|
+
for (const server of services) await stopServer(server);
|
|
873
989
|
},
|
|
874
990
|
};
|
|
875
991
|
}
|
|
992
|
+
|
|
993
|
+
// The Admin's internet-facing listener; Fly terminates TLS at the edge. It
|
|
994
|
+
// starts only on a declared port with the machine's private connection key.
|
|
995
|
+
export async function startPublicClaudeRunsApi({
|
|
996
|
+
env = process.env,
|
|
997
|
+
diagnostic = () => {},
|
|
998
|
+
root = "/opt/data/private-opener",
|
|
999
|
+
createApi = createClaudeRunsApi,
|
|
1000
|
+
readToken = readKey,
|
|
1001
|
+
} = {}) {
|
|
1002
|
+
const value = env.SELLABLE_AGENT_PUBLIC_RUNS_SERVICE_PORT ?? "";
|
|
1003
|
+
const port = Number(value);
|
|
1004
|
+
const keyFile = `${root}/api-key`;
|
|
1005
|
+
let key = null;
|
|
1006
|
+
try {
|
|
1007
|
+
key = lstatSync(keyFile);
|
|
1008
|
+
} catch {
|
|
1009
|
+
/* No key, no public listener. */
|
|
1010
|
+
}
|
|
1011
|
+
if (
|
|
1012
|
+
!/^[0-9]{4,5}$/.test(value) ||
|
|
1013
|
+
port < 1024 ||
|
|
1014
|
+
port > 65535 ||
|
|
1015
|
+
// lstat: a symlink is not a file.
|
|
1016
|
+
!key?.isFile() ||
|
|
1017
|
+
(key.mode & 0o077) !== 0
|
|
1018
|
+
)
|
|
1019
|
+
return null;
|
|
1020
|
+
const server = createApi({
|
|
1021
|
+
mode: "public",
|
|
1022
|
+
token: readToken(keyFile),
|
|
1023
|
+
root,
|
|
1024
|
+
audit: auditTo(root),
|
|
1025
|
+
});
|
|
1026
|
+
await listen(server, port, "0.0.0.0");
|
|
1027
|
+
diagnostic({ ok: true, code: "claude_runs_public_api_ready", port });
|
|
1028
|
+
return { stop: () => stopServer(server) };
|
|
1029
|
+
}
|