@sellable/install 0.1.796 → 0.1.797-wip.runtime-ux.20261004.2
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 +1 -1
- package/lib/sellable-agent/fly-customer-image/Dockerfile +2 -2
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.d.mts +9 -0
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.mjs +77 -6
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +1 -1
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.d.mts +4 -0
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.mjs +15 -1
- package/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs +6 -4
- package/lib/sellable-agent/fly-customer-image/openai-runs-client.d.mts +7 -0
- package/lib/sellable-agent/fly-customer-image/openai-runs-client.mjs +271 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.d.mts +4 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.mjs +42 -8
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.d.mts +9 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.mjs +261 -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.797-wip.runtime-ux.20261004.2
|
|
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.797-wip.runtime-ux.20261004.2" \
|
|
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.796" \
|
|
|
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.797-wip.runtime-ux.20261004.2' || 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.797-wip.runtime-ux.20261004.2', 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.797-wip.runtime-ux.20261004.2` 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 /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 /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
|
|
@@ -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.797-wip.runtime-ux.20261004.2
|
|
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 /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 /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
|
|
@@ -38,6 +38,9 @@ export type ClaudeRunsApiOptions = {
|
|
|
38
38
|
maxConcurrency?: number;
|
|
39
39
|
maxToolConcurrency?: number;
|
|
40
40
|
toolLanes?: ClaudeRunsToolLanes | null;
|
|
41
|
+
checkAuthentication?: () => Promise<
|
|
42
|
+
"READY" | "SIGN_IN_REQUIRED" | "UNAVAILABLE"
|
|
43
|
+
>;
|
|
41
44
|
};
|
|
42
45
|
export function validateClaudeRunsRequest(
|
|
43
46
|
body: unknown,
|
|
@@ -75,3 +78,9 @@ export function startConfiguredClaudeRunsApis(options?: {
|
|
|
75
78
|
readToken?: (path: string) => string;
|
|
76
79
|
toolLanes?: ClaudeRunsToolLanes | null;
|
|
77
80
|
}): Promise<{ stop(): Promise<void> }>;
|
|
81
|
+
|
|
82
|
+
export function readClaudeAuthentication(options: {
|
|
83
|
+
root: string;
|
|
84
|
+
execFileImpl?: any;
|
|
85
|
+
timeoutMs?: number;
|
|
86
|
+
}): Promise<"READY" | "SIGN_IN_REQUIRED" | "UNAVAILABLE">;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
appendFileSync,
|
|
@@ -195,6 +195,42 @@ export function claudeInvocation(request, root, mcpSpecPath = null) {
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// Uses the same isolated environment as generation. Only a typed status leaves
|
|
199
|
+
// this boundary; CLI stdout can contain account identity and is never forwarded.
|
|
200
|
+
export function readClaudeAuthentication({
|
|
201
|
+
root,
|
|
202
|
+
execFileImpl = execFile,
|
|
203
|
+
timeoutMs = 8_000,
|
|
204
|
+
} = {}) {
|
|
205
|
+
const invocation = claudeInvocation(
|
|
206
|
+
{ instructions: "", input: "", model: "claude-opus", effort: "high" },
|
|
207
|
+
root
|
|
208
|
+
);
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
execFileImpl(
|
|
211
|
+
invocation.command,
|
|
212
|
+
["auth", "status"],
|
|
213
|
+
{
|
|
214
|
+
cwd: invocation.options.cwd,
|
|
215
|
+
env: invocation.options.env,
|
|
216
|
+
timeout: timeoutMs,
|
|
217
|
+
maxBuffer: 16 * 1024,
|
|
218
|
+
encoding: "utf8",
|
|
219
|
+
},
|
|
220
|
+
(error, stdout) => {
|
|
221
|
+
try {
|
|
222
|
+
const value = JSON.parse(stdout);
|
|
223
|
+
if (value.loggedIn === false) return resolve("SIGN_IN_REQUIRED");
|
|
224
|
+
if (!error && value.loggedIn === true) return resolve("READY");
|
|
225
|
+
} catch {
|
|
226
|
+
/* A missing or incompatible CLI is unavailable, never ready. */
|
|
227
|
+
}
|
|
228
|
+
resolve("UNAVAILABLE");
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
198
234
|
export function parseClaudeResult(stdout, request) {
|
|
199
235
|
const result = JSON.parse(stdout);
|
|
200
236
|
const models = Object.keys(result.modelUsage ?? {});
|
|
@@ -314,14 +350,36 @@ export function startClaudeRun(
|
|
|
314
350
|
export function createClaudeRunsApi({
|
|
315
351
|
token,
|
|
316
352
|
connection,
|
|
317
|
-
execute = (request) =>
|
|
318
|
-
|
|
353
|
+
execute = (request) => {
|
|
354
|
+
let activeRun;
|
|
355
|
+
let stopped = false;
|
|
356
|
+
return {
|
|
357
|
+
promise: (async () => {
|
|
358
|
+
const state = await checkAuthentication();
|
|
359
|
+
if (stopped) throw new Error("claude_runs_cancelled");
|
|
360
|
+
if (state !== "READY")
|
|
361
|
+
throw new Error(
|
|
362
|
+
state === "SIGN_IN_REQUIRED"
|
|
363
|
+
? "claude_runs_sign_in_required"
|
|
364
|
+
: "claude_runs_auth_check_unavailable"
|
|
365
|
+
);
|
|
366
|
+
activeRun = startClaudeRun(request, { root: connection.localRoot });
|
|
367
|
+
return activeRun.promise;
|
|
368
|
+
})(),
|
|
369
|
+
stop() {
|
|
370
|
+
stopped = true;
|
|
371
|
+
activeRun?.stop();
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
},
|
|
319
375
|
audit = () => {},
|
|
320
376
|
maxConcurrency = 2,
|
|
321
377
|
// Tool lanes (replies, follow-ups) have their own slots so a batch of
|
|
322
378
|
// openers can never crowd them out.
|
|
323
379
|
maxToolConcurrency = 2,
|
|
324
380
|
toolLanes = null,
|
|
381
|
+
checkAuthentication = () =>
|
|
382
|
+
readClaudeAuthentication({ root: connection.localRoot }),
|
|
325
383
|
} = {}) {
|
|
326
384
|
if (!/^[a-f0-9]{64}$/.test(token ?? ""))
|
|
327
385
|
throw new Error("claude_runs_key_rejected");
|
|
@@ -403,9 +461,13 @@ export function createClaudeRunsApi({
|
|
|
403
461
|
req.headers["x-sellable-agent-id"] !== connection.agentId
|
|
404
462
|
)
|
|
405
463
|
return json(res, 403, { error: "claude_runs_scope_rejected" });
|
|
406
|
-
if (req.method === "GET" && req.url === "/health")
|
|
464
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
465
|
+
const authentication = await checkAuthentication().catch(
|
|
466
|
+
() => "UNAVAILABLE"
|
|
467
|
+
);
|
|
407
468
|
return json(res, 200, {
|
|
408
|
-
ok:
|
|
469
|
+
ok: authentication === "READY",
|
|
470
|
+
authentication,
|
|
409
471
|
provider: "claude-code",
|
|
410
472
|
active: inFlight.plain + inFlight.tools,
|
|
411
473
|
toolActive: inFlight.tools,
|
|
@@ -414,6 +476,7 @@ export function createClaudeRunsApi({
|
|
|
414
476
|
agentId: connection.agentId,
|
|
415
477
|
},
|
|
416
478
|
});
|
|
479
|
+
}
|
|
417
480
|
if (req.method === "POST" && req.url === "/v1/runs") {
|
|
418
481
|
if (full("plain") && (!connection.toolEndpointIds || full("tools")))
|
|
419
482
|
return json(res, 429, { error: "claude_runs_busy" });
|
|
@@ -471,7 +534,15 @@ export function createClaudeRunsApi({
|
|
|
471
534
|
: "run.failed",
|
|
472
535
|
run.stopping && error.message === "claude_runs_cancelled"
|
|
473
536
|
? { status: "cancelled" }
|
|
474
|
-
: {
|
|
537
|
+
: {
|
|
538
|
+
status: "failed",
|
|
539
|
+
error: [
|
|
540
|
+
"claude_runs_sign_in_required",
|
|
541
|
+
"claude_runs_auth_check_unavailable",
|
|
542
|
+
].includes(error.message)
|
|
543
|
+
? error.message
|
|
544
|
+
: "claude_runs_generation_failed",
|
|
545
|
+
}
|
|
475
546
|
)
|
|
476
547
|
)
|
|
477
548
|
.finally(() => inFlight[pool]--);
|
|
@@ -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.797-wip.runtime-ux.20261004.2";
|
|
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;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export const ENDPOINT_MODEL_SELECTION_ALLOWLIST: Readonly<{
|
|
2
2
|
openrouter: readonly [
|
|
3
3
|
"anthropic/claude-opus-5.5",
|
|
4
|
+
"deepseek/deepseek-v4-flash-0731",
|
|
5
|
+
"openai/gpt-6-luna",
|
|
4
6
|
"anthropic/claude-sonnet-5.5",
|
|
5
7
|
];
|
|
6
8
|
}>;
|
|
7
9
|
export const ENDPOINT_MODEL_REASONING_EFFORTS: readonly [
|
|
10
|
+
"none",
|
|
8
11
|
"low",
|
|
9
12
|
"medium",
|
|
10
13
|
"high",
|
|
@@ -15,6 +18,7 @@ export type EndpointModelSelection = Readonly<{
|
|
|
15
18
|
model: string;
|
|
16
19
|
reasoningEffort: (typeof ENDPOINT_MODEL_REASONING_EFFORTS)[number];
|
|
17
20
|
runsApiBaseUrl?: string;
|
|
21
|
+
fallbackPolicy?: "STOP" | "PRODUCT_DEFAULT";
|
|
18
22
|
}>;
|
|
19
23
|
export function normalizeRunsApiBaseUrl(value: unknown): string | null;
|
|
20
24
|
export function compileEndpointModelSelection(
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
// requested destination here; the runtime must also match a trusted connection.
|
|
3
3
|
export const ENDPOINT_MODEL_SELECTION_ALLOWLIST = Object.freeze({
|
|
4
4
|
openrouter: Object.freeze([
|
|
5
|
+
// Retained for existing saved selections; the UI shortlist excludes Opus.
|
|
5
6
|
"anthropic/claude-opus-5.5",
|
|
7
|
+
"deepseek/deepseek-v4-flash-0731",
|
|
8
|
+
"openai/gpt-6-luna",
|
|
6
9
|
"anthropic/claude-sonnet-5.5",
|
|
7
10
|
]),
|
|
8
11
|
});
|
|
9
12
|
export const ENDPOINT_MODEL_REASONING_EFFORTS = Object.freeze([
|
|
13
|
+
"none",
|
|
10
14
|
"low",
|
|
11
15
|
"medium",
|
|
12
16
|
"high",
|
|
@@ -45,13 +49,22 @@ export function compileEndpointModelSelection(value) {
|
|
|
45
49
|
if (value === undefined || value === null) return null;
|
|
46
50
|
if (typeof value !== "object" || Array.isArray(value)) return "invalid";
|
|
47
51
|
const custom = Object.hasOwn(value, "runsApiBaseUrl");
|
|
52
|
+
const hasFallbackPolicy = Object.hasOwn(value, "fallbackPolicy");
|
|
48
53
|
if (
|
|
49
|
-
Object.keys(value)
|
|
54
|
+
Object.keys(value)
|
|
55
|
+
.filter((key) => key !== "fallbackPolicy")
|
|
56
|
+
.sort()
|
|
57
|
+
.join(",") !==
|
|
50
58
|
(custom
|
|
51
59
|
? "model,provider,reasoningEffort,runsApiBaseUrl"
|
|
52
60
|
: "model,provider,reasoningEffort")
|
|
53
61
|
)
|
|
54
62
|
return "invalid";
|
|
63
|
+
if (
|
|
64
|
+
hasFallbackPolicy &&
|
|
65
|
+
!["STOP", "PRODUCT_DEFAULT"].includes(value.fallbackPolicy)
|
|
66
|
+
)
|
|
67
|
+
return "invalid";
|
|
55
68
|
const { provider, model, reasoningEffort } = value;
|
|
56
69
|
if (
|
|
57
70
|
typeof provider !== "string" ||
|
|
@@ -80,5 +93,6 @@ export function compileEndpointModelSelection(value) {
|
|
|
80
93
|
model,
|
|
81
94
|
reasoningEffort,
|
|
82
95
|
...(custom ? { runsApiBaseUrl } : {}),
|
|
96
|
+
...(hasFallbackPolicy ? { fallbackPolicy: value.fallbackPolicy } : {}),
|
|
83
97
|
});
|
|
84
98
|
}
|
|
@@ -244,7 +244,7 @@ export function validateOpenerOutputContract(value) {
|
|
|
244
244
|
return JSON.parse(stableJson(value));
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
function compileCreateBody(input, openerStage = false) {
|
|
247
|
+
function compileCreateBody(input, openerStage = false, verification = false) {
|
|
248
248
|
if (!exactObject(input)) throw new Error("hermes_runs_submit_rejected");
|
|
249
249
|
const allowed = [
|
|
250
250
|
"conversationHistory",
|
|
@@ -252,7 +252,7 @@ function compileCreateBody(input, openerStage = false) {
|
|
|
252
252
|
"instructions",
|
|
253
253
|
"correlation",
|
|
254
254
|
"outputContract",
|
|
255
|
-
...(openerStage ? [] : ["modelSelection"]),
|
|
255
|
+
...(openerStage && !verification ? [] : ["modelSelection"]),
|
|
256
256
|
];
|
|
257
257
|
if (Object.keys(input).some((key) => !allowed.includes(key))) {
|
|
258
258
|
throw new Error("hermes_runs_submit_rejected");
|
|
@@ -624,8 +624,8 @@ export function createHermesRunsClient(options = {}) {
|
|
|
624
624
|
signal: init.signal ?? AbortSignal.timeout(timeoutMs),
|
|
625
625
|
});
|
|
626
626
|
};
|
|
627
|
-
const submit = async (compiled, { signal } = {}, openerStage = false) => {
|
|
628
|
-
const body = compileCreateBody(compiled, openerStage);
|
|
627
|
+
const submit = async (compiled, { signal } = {}, openerStage = false, verification = false) => {
|
|
628
|
+
const body = compileCreateBody(compiled, openerStage, verification);
|
|
629
629
|
let response;
|
|
630
630
|
try {
|
|
631
631
|
response = await request("/v1/runs", {
|
|
@@ -667,6 +667,8 @@ export function createHermesRunsClient(options = {}) {
|
|
|
667
667
|
return Object.freeze({
|
|
668
668
|
fixtureIdentity: HERMES_RUNS_FIXTURE_IDENTITY,
|
|
669
669
|
submit: (compiled, options) => submit(compiled, options),
|
|
670
|
+
// Fixed operator verification uses the existing tool-free session policy.
|
|
671
|
+
submitVerification: (compiled, options) => submit(compiled, options, true, true),
|
|
670
672
|
// Only this reserved session prefix receives the pinned tool-free bridge policy.
|
|
671
673
|
submitOpenerStage: (compiled, options) => submit(compiled, options, true),
|
|
672
674
|
async status(runId, { signal } = {}) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RunsApiConnection } from "./runs-api-connections.mjs";
|
|
2
|
+
export function createOpenAiRunsClient(options: {
|
|
3
|
+
connection: RunsApiConnection;
|
|
4
|
+
responseSchema?: unknown;
|
|
5
|
+
modelSelection?: { provider: string; model: string; reasoningEffort: string };
|
|
6
|
+
fetchImpl?: typeof fetch;
|
|
7
|
+
}): any;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createHermesRunsClient } from "./hermes-runs-client.mjs";
|
|
3
|
+
|
|
4
|
+
async function boundedJson(response) {
|
|
5
|
+
const reader = response.body?.getReader();
|
|
6
|
+
if (!reader) throw new Error("empty_response");
|
|
7
|
+
let size = 0;
|
|
8
|
+
const chunks = [];
|
|
9
|
+
try {
|
|
10
|
+
while (true) {
|
|
11
|
+
const value = await reader.read();
|
|
12
|
+
if (value.done) break;
|
|
13
|
+
size += value.value.byteLength;
|
|
14
|
+
if (size > 512 * 1024) {
|
|
15
|
+
await reader.cancel();
|
|
16
|
+
throw new Error("response_too_large");
|
|
17
|
+
}
|
|
18
|
+
chunks.push(value.value);
|
|
19
|
+
}
|
|
20
|
+
} finally {
|
|
21
|
+
reader.releaseLock();
|
|
22
|
+
}
|
|
23
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Text/structured-output adapter. All calls remain on the approved origin;
|
|
27
|
+
* tools, retries and provider/model fallback are deliberately not synthesized.
|
|
28
|
+
* The existing native client still validates the normalized run lifecycle. */
|
|
29
|
+
export function createOpenAiRunsClient({
|
|
30
|
+
connection,
|
|
31
|
+
responseSchema,
|
|
32
|
+
modelSelection,
|
|
33
|
+
fetchImpl = fetch,
|
|
34
|
+
}) {
|
|
35
|
+
const runs = new Map();
|
|
36
|
+
const json = (value, status = 200) =>
|
|
37
|
+
new Response(JSON.stringify(value), {
|
|
38
|
+
status,
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
});
|
|
41
|
+
const bridge = async (url, init = {}) => {
|
|
42
|
+
const path = String(url).slice(connection.baseUrl.length);
|
|
43
|
+
if (path === "/v1/runs" && init.method === "POST") {
|
|
44
|
+
const request = JSON.parse(init.body);
|
|
45
|
+
if (
|
|
46
|
+
!request.model &&
|
|
47
|
+
modelSelection &&
|
|
48
|
+
request.session_id?.startsWith("sellable_opener_stage_")
|
|
49
|
+
) {
|
|
50
|
+
request.provider = modelSelection.provider;
|
|
51
|
+
request.model = modelSelection.model;
|
|
52
|
+
request.model_options = {
|
|
53
|
+
...request.model_options,
|
|
54
|
+
reasoning_effort: modelSelection.reasoningEffort,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const model = connection.models.find(
|
|
58
|
+
(item) =>
|
|
59
|
+
item.provider === request.provider &&
|
|
60
|
+
item.model === request.model &&
|
|
61
|
+
item.reasoningEfforts.includes(
|
|
62
|
+
request.model_options?.reasoning_effort
|
|
63
|
+
)
|
|
64
|
+
);
|
|
65
|
+
if (!model) return json({ error: "model_not_allowed" }, 403);
|
|
66
|
+
const run = {
|
|
67
|
+
run_id: `run_${randomBytes(16).toString("hex")}`,
|
|
68
|
+
status: "running",
|
|
69
|
+
controller: new AbortController(),
|
|
70
|
+
};
|
|
71
|
+
const schema =
|
|
72
|
+
request.model_options?.sellable_opener_output_contract?.schema ??
|
|
73
|
+
responseSchema;
|
|
74
|
+
const structured = schema && connection.structuredOutputs !== false;
|
|
75
|
+
const instructions =
|
|
76
|
+
structured || !schema
|
|
77
|
+
? request.instructions
|
|
78
|
+
: `${request.instructions}\nReturn only JSON conforming to this schema:\n${JSON.stringify(schema)}`;
|
|
79
|
+
const history = (request.conversation_history ?? []).map(
|
|
80
|
+
({ role, content }) => ({ role, content })
|
|
81
|
+
);
|
|
82
|
+
const effort = request.model_options?.reasoning_effort;
|
|
83
|
+
const responses = connection.protocol === "openai-responses";
|
|
84
|
+
const body = responses
|
|
85
|
+
? {
|
|
86
|
+
model: request.model,
|
|
87
|
+
instructions,
|
|
88
|
+
input: [...history, { role: "user", content: request.input }],
|
|
89
|
+
stream: false,
|
|
90
|
+
store: false,
|
|
91
|
+
max_output_tokens: 4096,
|
|
92
|
+
...(effort && effort !== "none" ? { reasoning: { effort } } : {}),
|
|
93
|
+
...(structured
|
|
94
|
+
? {
|
|
95
|
+
text: {
|
|
96
|
+
format: {
|
|
97
|
+
type: "json_schema",
|
|
98
|
+
name: "endpoint_output",
|
|
99
|
+
strict: true,
|
|
100
|
+
schema,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
: {}),
|
|
105
|
+
}
|
|
106
|
+
: {
|
|
107
|
+
model: request.model,
|
|
108
|
+
messages: [
|
|
109
|
+
{ role: "system", content: instructions },
|
|
110
|
+
...history,
|
|
111
|
+
{ role: "user", content: request.input },
|
|
112
|
+
],
|
|
113
|
+
stream: false,
|
|
114
|
+
max_tokens: 4096,
|
|
115
|
+
...(effort && effort !== "none"
|
|
116
|
+
? { reasoning_effort: effort }
|
|
117
|
+
: {}),
|
|
118
|
+
...(structured
|
|
119
|
+
? {
|
|
120
|
+
response_format: {
|
|
121
|
+
type: "json_schema",
|
|
122
|
+
json_schema: {
|
|
123
|
+
name: "endpoint_output",
|
|
124
|
+
strict: true,
|
|
125
|
+
schema,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
: {}),
|
|
130
|
+
};
|
|
131
|
+
runs.set(run.run_id, run);
|
|
132
|
+
run.promise = (async () => {
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetchImpl(
|
|
135
|
+
`${connection.baseUrl}/${responses ? "responses" : "chat/completions"}`,
|
|
136
|
+
{
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
authorization: init.headers.authorization,
|
|
140
|
+
"content-type": "application/json",
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify(body),
|
|
143
|
+
redirect: "error",
|
|
144
|
+
signal: AbortSignal.any([
|
|
145
|
+
run.controller.signal,
|
|
146
|
+
AbortSignal.timeout(750_000),
|
|
147
|
+
]),
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
if (!response.ok)
|
|
151
|
+
throw new Error(
|
|
152
|
+
response.status === 401 || response.status === 403
|
|
153
|
+
? "access_denied"
|
|
154
|
+
: "provider_unavailable"
|
|
155
|
+
);
|
|
156
|
+
const value = await boundedJson(response);
|
|
157
|
+
const output = responses
|
|
158
|
+
? (value.output ?? [])
|
|
159
|
+
.filter((item) => item.type === "message")
|
|
160
|
+
.flatMap((item) => item.content ?? [])
|
|
161
|
+
.filter((item) => item.type === "output_text")
|
|
162
|
+
.map((item) => item.text)
|
|
163
|
+
.join("")
|
|
164
|
+
: value.choices?.[0]?.message?.content;
|
|
165
|
+
if (
|
|
166
|
+
typeof output !== "string" ||
|
|
167
|
+
!output.trim() ||
|
|
168
|
+
Buffer.byteLength(output) > 48 * 1024 ||
|
|
169
|
+
(responses
|
|
170
|
+
? value.status !== "completed"
|
|
171
|
+
: value.choices?.[0]?.finish_reason !== "stop") ||
|
|
172
|
+
typeof value.model !== "string" ||
|
|
173
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(value.model) ||
|
|
174
|
+
(value.model !== request.model &&
|
|
175
|
+
!new RegExp(
|
|
176
|
+
`^${request.model.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-[0-9]{4}-?[0-9]{2}-?[0-9]{2}$`
|
|
177
|
+
).test(value.model))
|
|
178
|
+
)
|
|
179
|
+
throw new Error("output_rejected");
|
|
180
|
+
Object.assign(run, {
|
|
181
|
+
status: "completed",
|
|
182
|
+
output,
|
|
183
|
+
runtime: { provider: request.provider, model: value.model },
|
|
184
|
+
usage: {
|
|
185
|
+
input_tokens:
|
|
186
|
+
value.usage?.input_tokens ?? value.usage?.prompt_tokens ?? 0,
|
|
187
|
+
output_tokens:
|
|
188
|
+
value.usage?.output_tokens ??
|
|
189
|
+
value.usage?.completion_tokens ??
|
|
190
|
+
0,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
Object.assign(run, {
|
|
195
|
+
status: "failed",
|
|
196
|
+
error:
|
|
197
|
+
error.message === "access_denied"
|
|
198
|
+
? "connection_access_denied"
|
|
199
|
+
: "connection_generation_failed",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
})();
|
|
203
|
+
return json({ run_id: run.run_id, status: "started" }, 202);
|
|
204
|
+
}
|
|
205
|
+
const match = /^\/v1\/runs\/(run_[a-f0-9]{32})(?:\/(events|stop))?$/.exec(
|
|
206
|
+
path
|
|
207
|
+
);
|
|
208
|
+
const run = match && runs.get(match[1]);
|
|
209
|
+
if (!run) return json({ error: "run_missing" }, 404);
|
|
210
|
+
const snapshot = () => ({
|
|
211
|
+
run_id: run.run_id,
|
|
212
|
+
status: run.status,
|
|
213
|
+
...(run.status === "completed"
|
|
214
|
+
? { output: run.output, runtime: run.runtime, usage: run.usage }
|
|
215
|
+
: run.status === "failed"
|
|
216
|
+
? { error: run.error }
|
|
217
|
+
: {}),
|
|
218
|
+
});
|
|
219
|
+
if (match[2] === "stop") {
|
|
220
|
+
run.controller.abort();
|
|
221
|
+
// Aborting HTTP cannot prove cancellation at a remote inference server.
|
|
222
|
+
return json({ error: "stop_uncertain" }, 409);
|
|
223
|
+
}
|
|
224
|
+
if (match[2] === "events") {
|
|
225
|
+
if (init.signal?.aborted) throw new Error("stream_aborted");
|
|
226
|
+
let closed = false;
|
|
227
|
+
let abort;
|
|
228
|
+
const cleanup = () => {
|
|
229
|
+
closed = true;
|
|
230
|
+
init.signal?.removeEventListener("abort", abort);
|
|
231
|
+
};
|
|
232
|
+
return new Response(
|
|
233
|
+
new ReadableStream({
|
|
234
|
+
start(controller) {
|
|
235
|
+
abort = () => {
|
|
236
|
+
if (!closed) {
|
|
237
|
+
cleanup();
|
|
238
|
+
controller.error(new Error("stream_aborted"));
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
init.signal?.addEventListener("abort", abort, { once: true });
|
|
242
|
+
if (init.signal?.aborted) {
|
|
243
|
+
abort();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
void run.promise.then(() => {
|
|
247
|
+
if (closed) return;
|
|
248
|
+
cleanup();
|
|
249
|
+
controller.enqueue(
|
|
250
|
+
new TextEncoder().encode(
|
|
251
|
+
`data: ${JSON.stringify({ event: run.status === "completed" ? "run.completed" : "run.failed", timestamp: new Date().toISOString(), ...snapshot() })}\n\n`
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
controller.close();
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
cancel() {
|
|
258
|
+
cleanup();
|
|
259
|
+
},
|
|
260
|
+
}),
|
|
261
|
+
{ headers: { "content-type": "text/event-stream" } }
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return json(snapshot());
|
|
265
|
+
};
|
|
266
|
+
return createHermesRunsClient({
|
|
267
|
+
baseUrl: connection.baseUrl,
|
|
268
|
+
apiKeyFile: connection.apiKeyFile,
|
|
269
|
+
fetchImpl: bridge,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
@@ -14,7 +14,11 @@ export type RunsApiConnection = Readonly<{
|
|
|
14
14
|
}>;
|
|
15
15
|
localRoot?: string;
|
|
16
16
|
toolEndpointIds?: string[];
|
|
17
|
+
name?: string;
|
|
18
|
+
protocol?: "runs-api" | "openai-chat" | "openai-responses";
|
|
19
|
+
structuredOutputs?: boolean;
|
|
17
20
|
}>;
|
|
21
|
+
export function runsApiConnectionId(connection: RunsApiConnection): string;
|
|
18
22
|
export function parseRunsApiConnections(raw: unknown): RunsApiConnection[];
|
|
19
23
|
export function runsApiConnectionsForRuntime(
|
|
20
24
|
raw: unknown,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
normalizeRunsApiBaseUrl,
|
|
5
5
|
} from "./endpoint-model-selection.mjs";
|
|
6
6
|
import { createHermesRunsClient } from "./hermes-runs-client.mjs";
|
|
7
|
+
import { createOpenAiRunsClient } from "./openai-runs-client.mjs";
|
|
7
8
|
|
|
8
9
|
export const RUNS_API_CONNECTIONS_ENV =
|
|
9
10
|
"SELLABLE_AGENT_RUNS_API_CONNECTIONS_JSON";
|
|
@@ -53,9 +54,21 @@ export function parseRunsApiConnections(raw) {
|
|
|
53
54
|
"models",
|
|
54
55
|
"localRoot",
|
|
55
56
|
"toolEndpointIds",
|
|
57
|
+
"name",
|
|
58
|
+
"protocol",
|
|
59
|
+
"structuredOutputs",
|
|
56
60
|
].includes(key)
|
|
57
61
|
) ||
|
|
58
62
|
![entry.runtimeId, entry.workspaceId, entry.agentId].every(id) ||
|
|
63
|
+
(entry.protocol !== undefined &&
|
|
64
|
+
!["runs-api", "openai-chat", "openai-responses"].includes(
|
|
65
|
+
entry.protocol
|
|
66
|
+
)) ||
|
|
67
|
+
(entry.structuredOutputs !== undefined &&
|
|
68
|
+
typeof entry.structuredOutputs !== "boolean") ||
|
|
69
|
+
(entry.name !== undefined &&
|
|
70
|
+
(typeof entry.name !== "string" ||
|
|
71
|
+
!/^[\p{L}\p{N} .()_-]{1,80}$/u.test(entry.name))) ||
|
|
59
72
|
typeof entry.apiKeyFile !== "string" ||
|
|
60
73
|
!/^\/(?:opt\/data|run\/secrets)\/[A-Za-z0-9_./-]{1,250}$/.test(
|
|
61
74
|
entry.apiKeyFile
|
|
@@ -105,6 +118,7 @@ export function parseRunsApiConnections(raw) {
|
|
|
105
118
|
if (entry.localRoot !== undefined) {
|
|
106
119
|
const url = new URL(baseUrl);
|
|
107
120
|
if (
|
|
121
|
+
(entry.protocol !== undefined && entry.protocol !== "runs-api") ||
|
|
108
122
|
typeof entry.localRoot !== "string" ||
|
|
109
123
|
!/^\/opt\/data\/[A-Za-z0-9_-]{1,80}$/.test(entry.localRoot) ||
|
|
110
124
|
entry.apiKeyFile !== `${entry.localRoot}/api-key` ||
|
|
@@ -150,6 +164,20 @@ export function parseRunsApiConnections(raw) {
|
|
|
150
164
|
});
|
|
151
165
|
}
|
|
152
166
|
|
|
167
|
+
export function runsApiConnectionId(connection) {
|
|
168
|
+
return `runs_${createHash("sha256")
|
|
169
|
+
.update(
|
|
170
|
+
JSON.stringify([
|
|
171
|
+
connection.runtimeId,
|
|
172
|
+
connection.workspaceId,
|
|
173
|
+
connection.agentId,
|
|
174
|
+
connection.baseUrl,
|
|
175
|
+
])
|
|
176
|
+
)
|
|
177
|
+
.digest("hex")
|
|
178
|
+
.slice(0, 24)}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
153
181
|
export function runsApiConnectionsForRuntime(raw, runtime) {
|
|
154
182
|
return parseRunsApiConnections(raw).filter(
|
|
155
183
|
(connection) =>
|
|
@@ -194,14 +222,20 @@ export function createConfiguredRunsClient({
|
|
|
194
222
|
throw new Error("runs_api_connection_not_authorized");
|
|
195
223
|
// This client instance retains one destination and one credential for the
|
|
196
224
|
// entire attempt, including stream/status/stop, even if settings later change.
|
|
197
|
-
const client =
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
225
|
+
const client = connection.protocol?.startsWith("openai-")
|
|
226
|
+
? createOpenAiRunsClient({
|
|
227
|
+
connection,
|
|
228
|
+
responseSchema: claim.revision?.outputSchema,
|
|
229
|
+
modelSelection: model,
|
|
230
|
+
})
|
|
231
|
+
: clientFactory({
|
|
232
|
+
baseUrl: connection.baseUrl,
|
|
233
|
+
apiKeyFile: connection.apiKeyFile,
|
|
234
|
+
scopeHeaders: {
|
|
235
|
+
"x-sellable-workspace-id": connection.workspaceId,
|
|
236
|
+
"x-sellable-agent-id": connection.agentId,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
205
239
|
return Object.freeze({
|
|
206
240
|
...client,
|
|
207
241
|
submit(compiled, options) {
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { compileEndpointModelSelection } from "./endpoint-model-selection.mjs";
|
|
5
|
+
import { createHermesRunsClient } from "./hermes-runs-client.mjs";
|
|
6
|
+
import {
|
|
7
|
+
createConfiguredRunsClient,
|
|
8
|
+
RUNS_API_CONNECTIONS_ENV,
|
|
9
|
+
runsApiConnectionId,
|
|
10
|
+
runsApiConnectionsForRuntime,
|
|
11
|
+
} from "./runs-api-connections.mjs";
|
|
12
|
+
|
|
13
|
+
// Fixed read-only machine command. Callers supply identities, never a URL,
|
|
14
|
+
// credential path, shell command, or prompt. The installed registry authorizes
|
|
15
|
+
// the destination again on the machine before a credential is read.
|
|
16
|
+
export async function checkRunsApiConnection(
|
|
17
|
+
request,
|
|
18
|
+
{
|
|
19
|
+
env = process.env,
|
|
20
|
+
fetchImpl = fetch,
|
|
21
|
+
readToken = (path) => readFile(path, "utf8"),
|
|
22
|
+
} = {}
|
|
23
|
+
) {
|
|
24
|
+
const checkedAt = new Date().toISOString();
|
|
25
|
+
const result = (state) => ({
|
|
26
|
+
connectionId: request.connectionId,
|
|
27
|
+
state,
|
|
28
|
+
checkedAt,
|
|
29
|
+
});
|
|
30
|
+
if (request.runtimeId !== env.SELLABLE_RUNTIME_ID)
|
|
31
|
+
return result("NOT_CONFIGURED");
|
|
32
|
+
const connection = runsApiConnectionsForRuntime(
|
|
33
|
+
env[RUNS_API_CONNECTIONS_ENV],
|
|
34
|
+
request
|
|
35
|
+
).find(
|
|
36
|
+
(candidate) =>
|
|
37
|
+
runsApiConnectionId(candidate) === request.connectionId &&
|
|
38
|
+
candidate.endpointIds.includes(request.endpointId)
|
|
39
|
+
);
|
|
40
|
+
if (!connection) return result("NOT_CONFIGURED");
|
|
41
|
+
try {
|
|
42
|
+
const token = (await readToken(connection.apiKeyFile)).trim();
|
|
43
|
+
if (!token || token.length > 8192 || /[\r\n]/.test(token))
|
|
44
|
+
return result("UNAVAILABLE");
|
|
45
|
+
const response = await fetchImpl(
|
|
46
|
+
`${connection.baseUrl}/${connection.protocol?.startsWith("openai-") ? "models" : "health"}`,
|
|
47
|
+
{
|
|
48
|
+
headers: {
|
|
49
|
+
authorization: `Bearer ${token}`,
|
|
50
|
+
...(connection.protocol?.startsWith("openai-") ? {} : {
|
|
51
|
+
"x-sellable-workspace-id": connection.workspaceId,
|
|
52
|
+
"x-sellable-agent-id": connection.agentId,
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
redirect: "error",
|
|
56
|
+
signal: AbortSignal.timeout(12_000),
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
if (response.status === 401 || response.status === 403)
|
|
60
|
+
return result("ACCESS_DENIED");
|
|
61
|
+
if (!response.ok) return result("UNAVAILABLE");
|
|
62
|
+
const reader = response.body?.getReader();
|
|
63
|
+
if (!reader) return result("UNAVAILABLE");
|
|
64
|
+
let text = "";
|
|
65
|
+
let size = 0;
|
|
66
|
+
try {
|
|
67
|
+
const decoder = new TextDecoder();
|
|
68
|
+
while (true) {
|
|
69
|
+
const chunk = await reader.read();
|
|
70
|
+
if (chunk.done) break;
|
|
71
|
+
size += chunk.value.byteLength;
|
|
72
|
+
if (size > 16_384) {
|
|
73
|
+
await reader.cancel();
|
|
74
|
+
return result("UNAVAILABLE");
|
|
75
|
+
}
|
|
76
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
77
|
+
}
|
|
78
|
+
text += decoder.decode();
|
|
79
|
+
} finally {
|
|
80
|
+
reader.releaseLock();
|
|
81
|
+
}
|
|
82
|
+
const health = JSON.parse(text);
|
|
83
|
+
if (connection.localRoot) {
|
|
84
|
+
if (
|
|
85
|
+
health.provider !== "claude-code" ||
|
|
86
|
+
health.scope?.workspaceId !== connection.workspaceId ||
|
|
87
|
+
health.scope?.agentId !== connection.agentId
|
|
88
|
+
)
|
|
89
|
+
return result("ACCESS_DENIED");
|
|
90
|
+
if (health.authentication === "SIGN_IN_REQUIRED")
|
|
91
|
+
return result("SIGN_IN_REQUIRED");
|
|
92
|
+
return result(
|
|
93
|
+
health.authentication === "READY" && health.ok === true
|
|
94
|
+
? "READY"
|
|
95
|
+
: "UNAVAILABLE"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return result(
|
|
99
|
+
(
|
|
100
|
+
connection.protocol?.startsWith("openai-")
|
|
101
|
+
? Array.isArray(health.data)
|
|
102
|
+
: health.ok === true
|
|
103
|
+
)
|
|
104
|
+
? "REACHABLE"
|
|
105
|
+
: "UNAVAILABLE"
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
return result("UNAVAILABLE");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function verifyRunsApiSelection(
|
|
113
|
+
request,
|
|
114
|
+
{
|
|
115
|
+
env = process.env,
|
|
116
|
+
makeDefaultClient = createHermesRunsClient,
|
|
117
|
+
makeConfiguredClient = createConfiguredRunsClient,
|
|
118
|
+
health = checkRunsApiConnection,
|
|
119
|
+
} = {}
|
|
120
|
+
) {
|
|
121
|
+
const result = (state, servedRuntime) => ({
|
|
122
|
+
connectionId: request.connectionId,
|
|
123
|
+
state,
|
|
124
|
+
checkedAt: new Date().toISOString(),
|
|
125
|
+
...(servedRuntime ? { servedRuntime } : {}),
|
|
126
|
+
});
|
|
127
|
+
if (
|
|
128
|
+
request.runtimeId !== env.SELLABLE_RUNTIME_ID ||
|
|
129
|
+
!/^[A-Za-z0-9_-]{1,128}$/.test(request.profileId ?? "")
|
|
130
|
+
)
|
|
131
|
+
return result("NOT_CONFIGURED");
|
|
132
|
+
const selection = compileEndpointModelSelection(request.modelSelection);
|
|
133
|
+
if (selection === "invalid") return result("NOT_CONFIGURED");
|
|
134
|
+
let verificationWorkKind = "STANDARD";
|
|
135
|
+
if (selection?.runsApiBaseUrl) {
|
|
136
|
+
const connection = runsApiConnectionsForRuntime(
|
|
137
|
+
env[RUNS_API_CONNECTIONS_ENV],
|
|
138
|
+
request
|
|
139
|
+
).find(
|
|
140
|
+
(item) =>
|
|
141
|
+
runsApiConnectionId(item) === request.connectionId &&
|
|
142
|
+
item.baseUrl === selection.runsApiBaseUrl
|
|
143
|
+
);
|
|
144
|
+
if (!connection?.endpointIds.includes(request.endpointId))
|
|
145
|
+
return result("NOT_CONFIGURED");
|
|
146
|
+
verificationWorkKind = connection.workKinds.includes("STANDARD")
|
|
147
|
+
? "STANDARD"
|
|
148
|
+
: connection.workKinds[0];
|
|
149
|
+
if (connection.localRoot) {
|
|
150
|
+
const readiness = await health(request, { env });
|
|
151
|
+
if (readiness.state !== "READY") return readiness;
|
|
152
|
+
}
|
|
153
|
+
} else if (request.connectionId !== (selection ? "openrouter" : "default"))
|
|
154
|
+
return result("NOT_CONFIGURED");
|
|
155
|
+
const apiKeyFile =
|
|
156
|
+
env.SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND === "ADMIN"
|
|
157
|
+
? "/run/sellable-agent/admin-runtime/api-server-key"
|
|
158
|
+
: `/run/sellable-agent/${request.profileId}/api-server-key`;
|
|
159
|
+
let client;
|
|
160
|
+
let run;
|
|
161
|
+
try {
|
|
162
|
+
client = selection?.runsApiBaseUrl
|
|
163
|
+
? makeConfiguredClient({
|
|
164
|
+
apiKeyFile,
|
|
165
|
+
env,
|
|
166
|
+
claim: {
|
|
167
|
+
runtime: { runtimeId: request.runtimeId },
|
|
168
|
+
attempt: {
|
|
169
|
+
workspaceId: request.workspaceId,
|
|
170
|
+
agentId: request.agentId,
|
|
171
|
+
endpointId: request.endpointId,
|
|
172
|
+
},
|
|
173
|
+
run: {
|
|
174
|
+
modelSelection: selection,
|
|
175
|
+
productExecution: { workKind: verificationWorkKind },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
: makeDefaultClient({ apiKeyFile });
|
|
180
|
+
const compiled = {
|
|
181
|
+
input: "Connection verification. Reply with SELLABLE_CONNECTION_OK.",
|
|
182
|
+
instructions:
|
|
183
|
+
"This is a connection check. Do not use tools or take actions. Return exactly SELLABLE_CONNECTION_OK and nothing else.",
|
|
184
|
+
conversationHistory: [],
|
|
185
|
+
correlation: `connection-check-${randomUUID()}`,
|
|
186
|
+
...(selection ? { modelSelection: selection } : {}),
|
|
187
|
+
};
|
|
188
|
+
const signal = AbortSignal.timeout(70_000);
|
|
189
|
+
// Default Hermes gets the tool-free verification prefix. A Runs service
|
|
190
|
+
// owns its own tool policy; the Claude and OpenAI adapters are tool-free.
|
|
191
|
+
run = await (
|
|
192
|
+
selection?.runsApiBaseUrl ? client.submit : client.submitVerification
|
|
193
|
+
)(compiled, { signal });
|
|
194
|
+
await client.streamEvents(run.runId, { signal, onEvent: async () => {} });
|
|
195
|
+
const status = await client.status(run.runId, { signal });
|
|
196
|
+
const served = status.servedRuntime;
|
|
197
|
+
if (
|
|
198
|
+
status.status !== "completed" ||
|
|
199
|
+
typeof status.output !== "string" ||
|
|
200
|
+
status.output.trim() !== "SELLABLE_CONNECTION_OK" ||
|
|
201
|
+
!served
|
|
202
|
+
)
|
|
203
|
+
return result("UNAVAILABLE");
|
|
204
|
+
if (
|
|
205
|
+
selection &&
|
|
206
|
+
(served.provider !== selection.provider ||
|
|
207
|
+
(served.model !== selection.model &&
|
|
208
|
+
!served.model.startsWith(`${selection.model}-`)))
|
|
209
|
+
)
|
|
210
|
+
return result("UNAVAILABLE");
|
|
211
|
+
return result("READY", served);
|
|
212
|
+
} catch {
|
|
213
|
+
if (client && run)
|
|
214
|
+
await client
|
|
215
|
+
.stop(run.runId, { signal: AbortSignal.timeout(3_000) })
|
|
216
|
+
.catch(() => {});
|
|
217
|
+
return result("UNAVAILABLE");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
process.argv[1] &&
|
|
223
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
224
|
+
) {
|
|
225
|
+
try {
|
|
226
|
+
const raw = process.argv[2];
|
|
227
|
+
if (!raw || raw.length > 4096) throw new Error();
|
|
228
|
+
const request = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
229
|
+
const verify = request.action === "VERIFY";
|
|
230
|
+
if (
|
|
231
|
+
Object.keys(request)
|
|
232
|
+
.filter(
|
|
233
|
+
(key) => !["action", "profileId", "modelSelection"].includes(key)
|
|
234
|
+
)
|
|
235
|
+
.sort()
|
|
236
|
+
.join(",") !==
|
|
237
|
+
"agentId,connectionId,endpointId,runtimeId,workspaceId" ||
|
|
238
|
+
[
|
|
239
|
+
request.agentId,
|
|
240
|
+
request.connectionId,
|
|
241
|
+
request.endpointId,
|
|
242
|
+
request.runtimeId,
|
|
243
|
+
request.workspaceId,
|
|
244
|
+
].some(
|
|
245
|
+
(value) =>
|
|
246
|
+
typeof value !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(value)
|
|
247
|
+
) ||
|
|
248
|
+
(request.action !== undefined && !verify)
|
|
249
|
+
)
|
|
250
|
+
throw new Error();
|
|
251
|
+
process.stdout.write(
|
|
252
|
+
JSON.stringify(
|
|
253
|
+
await (verify
|
|
254
|
+
? verifyRunsApiSelection(request)
|
|
255
|
+
: checkRunsApiConnection(request))
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
} catch {
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -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.797-wip.runtime-ux.20261004.2";
|
|
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.797-wip.runtime-ux.20261004.2" ||
|
|
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.797-wip.runtime-ux.20261004.2" ||
|
|
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.797-wip.runtime-ux.20261004.2";
|
|
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
|