@sellable/install 0.1.794 → 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.
@@ -0,0 +1,611 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
3
+ import {
4
+ appendFileSync,
5
+ lstatSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
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";
17
+ import { validateOpenerOutputContract } from "./hermes-runs-client.mjs";
18
+ import {
19
+ parseRunsApiConnections,
20
+ RUNS_API_CONNECTIONS_ENV,
21
+ } from "./runs-api-connections.mjs";
22
+
23
+ const MAX_BODY = 320 * 1024;
24
+ const MAX_OUTPUT = 48 * 1024;
25
+ const MAX_CLI_OUTPUT = 512 * 1024;
26
+ const MCP_SESSION = fileURLToPath(
27
+ new URL("./claude-code-mcp-session.mjs", import.meta.url)
28
+ );
29
+ const hash = (s) => createHash("sha256").update(s).digest("hex");
30
+ const record = (v) => v && typeof v === "object" && !Array.isArray(v);
31
+
32
+ export function validateClaudeRunsRequest(body, models) {
33
+ const allowed = [
34
+ "input",
35
+ "instructions",
36
+ "conversation_history",
37
+ "session_id",
38
+ "provider",
39
+ "model",
40
+ "model_options",
41
+ ];
42
+ if (
43
+ !record(body) ||
44
+ Object.keys(body).some((k) => !allowed.includes(k)) ||
45
+ typeof body.input !== "string" ||
46
+ !body.input.length ||
47
+ Buffer.byteLength(body.input) > 128 * 1024 ||
48
+ typeof body.instructions !== "string" ||
49
+ !body.instructions.length ||
50
+ Buffer.byteLength(body.instructions) > 128 * 1024 ||
51
+ !Array.isArray(body.conversation_history) ||
52
+ body.conversation_history.length > 64 ||
53
+ Buffer.byteLength(JSON.stringify(body.conversation_history)) > 128 * 1024 ||
54
+ body.conversation_history.some(
55
+ (item) =>
56
+ !record(item) ||
57
+ Object.keys(item).sort().join(",") !== "content,role" ||
58
+ !["user", "assistant"].includes(item.role) ||
59
+ typeof item.content !== "string"
60
+ ) ||
61
+ !/^sellable_endpoint_[a-f0-9]{32}$/.test(body.session_id ?? "") ||
62
+ body.provider !== "claude-code" ||
63
+ !record(body.model_options) ||
64
+ Object.keys(body.model_options).some(
65
+ (key) =>
66
+ !["reasoning_effort", "sellable_opener_output_contract"].includes(key)
67
+ ) ||
68
+ !models.some(
69
+ (model) =>
70
+ model.provider === body.provider &&
71
+ model.model === body.model &&
72
+ model.reasoningEfforts.includes(body.model_options.reasoning_effort)
73
+ )
74
+ )
75
+ throw new Error("claude_runs_request_rejected");
76
+ const contract = body.model_options.sellable_opener_output_contract;
77
+ const schema =
78
+ contract === undefined
79
+ ? null
80
+ : validateOpenerOutputContract(contract).schema;
81
+ return {
82
+ input: body.conversation_history.length
83
+ ? JSON.stringify({
84
+ conversation_history: body.conversation_history,
85
+ input: body.input,
86
+ })
87
+ : body.input,
88
+ instructions: body.instructions,
89
+ schema,
90
+ model: body.model,
91
+ effort: body.model_options.reasoning_effort,
92
+ sessionId: body.session_id,
93
+ tools: null,
94
+ };
95
+ }
96
+
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) {
133
+ return {
134
+ command: `${root}/cli/node_modules/.bin/claude`,
135
+ args: [
136
+ "-p",
137
+ "--model",
138
+ request.model,
139
+ "--effort",
140
+ request.effort,
141
+ "--output-format",
142
+ "json",
143
+ ...(request.schema
144
+ ? ["--json-schema", JSON.stringify(request.schema)]
145
+ : []),
146
+ "--tools",
147
+ "",
148
+ "--permission-mode",
149
+ "dontAsk",
150
+ "--strict-mcp-config",
151
+ "--mcp-config",
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":{}}']),
170
+ "--setting-sources",
171
+ "",
172
+ "--settings",
173
+ '{"disableAllHooks":true,"autoMemoryEnabled":false}',
174
+ "--disable-slash-commands",
175
+ "--no-session-persistence",
176
+ "--max-turns",
177
+ request.tools ? "16" : "3",
178
+ "--system-prompt",
179
+ request.instructions,
180
+ ],
181
+ // Whitelist instead of inheriting the runtime's provider keys, URLs, hooks,
182
+ // plugin paths, NODE_OPTIONS, or workspace access credentials.
183
+ options: {
184
+ cwd: `${root}/work`,
185
+ detached: true,
186
+ stdio: ["pipe", "pipe", "pipe"],
187
+ env: {
188
+ PATH: "/usr/local/bin:/usr/bin:/bin",
189
+ HOME: `${root}/claude`,
190
+ CLAUDE_CONFIG_DIR: `${root}/claude`,
191
+ LANG: "C.UTF-8",
192
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
193
+ },
194
+ },
195
+ };
196
+ }
197
+
198
+ export function parseClaudeResult(stdout, request) {
199
+ const result = JSON.parse(stdout);
200
+ const models = Object.keys(result.modelUsage ?? {});
201
+ if (
202
+ result.type !== "result" ||
203
+ result.is_error !== false ||
204
+ result.subtype !== "success" ||
205
+ (request.schema
206
+ ? !record(result.structured_output)
207
+ : typeof result.result !== "string") ||
208
+ (result.permission_denials ?? []).length ||
209
+ !models.length ||
210
+ models.some(
211
+ (model) =>
212
+ model !== request.model && !model.startsWith(`${request.model}-`)
213
+ )
214
+ )
215
+ throw new Error("claude_runs_output_rejected");
216
+ const output = request.schema
217
+ ? JSON.stringify(result.structured_output)
218
+ : result.result;
219
+ if (!output.trim()) throw new Error("claude_runs_output_empty");
220
+ if (Buffer.byteLength(output) > MAX_OUTPUT)
221
+ throw new Error("claude_runs_output_too_large");
222
+ return {
223
+ output,
224
+ runtime: { provider: "claude-code", model: models[0] },
225
+ usage: {
226
+ input_tokens: result.usage?.input_tokens ?? 0,
227
+ output_tokens: result.usage?.output_tokens ?? 0,
228
+ cache_read_tokens: result.usage?.cache_read_input_tokens ?? 0,
229
+ cache_write_tokens: result.usage?.cache_creation_input_tokens ?? 0,
230
+ },
231
+ };
232
+ }
233
+
234
+ export function startClaudeRun(
235
+ request,
236
+ { root, spawnImpl = spawn, timeoutMs = 750_000 } = {}
237
+ ) {
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
+ }
260
+ let reason = null;
261
+ let killTimer;
262
+ const stop = (code = "claude_runs_cancelled") => {
263
+ reason ??= code;
264
+ try {
265
+ process.kill(-child.pid, "SIGTERM");
266
+ } catch {
267
+ child.kill("SIGTERM");
268
+ }
269
+ killTimer ??= setTimeout(() => {
270
+ try {
271
+ process.kill(-child.pid, "SIGKILL");
272
+ } catch {
273
+ child.kill("SIGKILL");
274
+ }
275
+ }, 5_000);
276
+ killTimer.unref();
277
+ };
278
+ const promise = new Promise((resolve, reject) => {
279
+ const chunks = [];
280
+ let bytes = 0;
281
+ const timer = setTimeout(() => stop("claude_runs_timeout"), timeoutMs);
282
+ child.stdout.on("data", (chunk) => {
283
+ bytes += chunk.length;
284
+ if (bytes > MAX_CLI_OUTPUT) stop("claude_runs_output_too_large");
285
+ else chunks.push(chunk);
286
+ });
287
+ // Drain diagnostics without logging prompts, credentials, or message bodies.
288
+ child.stderr.on("data", () => {});
289
+ child.once("error", () => {
290
+ clearTimeout(timer);
291
+ removeSpec();
292
+ reject(new Error("claude_runs_cli_unavailable"));
293
+ });
294
+ child.once("close", (code) => {
295
+ clearTimeout(timer);
296
+ clearTimeout(killTimer);
297
+ removeSpec();
298
+ if (reason || code !== 0)
299
+ return reject(new Error(reason ?? "claude_runs_cli_failed"));
300
+ try {
301
+ resolve(
302
+ parseClaudeResult(Buffer.concat(chunks).toString("utf8"), request)
303
+ );
304
+ } catch {
305
+ reject(new Error("claude_runs_output_rejected"));
306
+ }
307
+ });
308
+ child.stdin.on("error", () => {});
309
+ child.stdin.end(request.input);
310
+ });
311
+ return { promise, stop };
312
+ }
313
+
314
+ export function createClaudeRunsApi({
315
+ token,
316
+ connection,
317
+ execute = (request) =>
318
+ startClaudeRun(request, { root: connection.localRoot }),
319
+ audit = () => {},
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,
325
+ } = {}) {
326
+ if (!/^[a-f0-9]{64}$/.test(token ?? ""))
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
+ };
348
+ const recordAudit = (entry) => {
349
+ try {
350
+ audit(entry);
351
+ } catch {
352
+ /* Logging cannot crash the Admin runtime. */
353
+ }
354
+ };
355
+ const runs = new Map();
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;
359
+ const json = (res, status, body) => {
360
+ res.writeHead(status, {
361
+ "content-type": "application/json",
362
+ "cache-control": "no-store",
363
+ "retry-after": "2",
364
+ });
365
+ res.end(JSON.stringify(body));
366
+ };
367
+ const terminal = (run, event, fields) => {
368
+ Object.assign(run, fields);
369
+ run.event = {
370
+ event,
371
+ run_id: run.run_id,
372
+ timestamp: new Date().toISOString(),
373
+ ...(run.status === "completed"
374
+ ? { output: run.output, runtime: run.runtime, usage: run.usage }
375
+ : run.status === "failed"
376
+ ? { error: run.error }
377
+ : {}),
378
+ };
379
+ for (const res of run.listeners)
380
+ res.end(`data: ${JSON.stringify(run.event)}\n\n`);
381
+ run.listeners.clear();
382
+ recordAudit({
383
+ runId: run.run_id,
384
+ status: run.status,
385
+ toolLane: run.pool === "tools",
386
+ servedRuntime: run.runtime ?? null,
387
+ usage: run.usage ?? null,
388
+ durationMs: Date.now() - run.startedAt,
389
+ outputDigest: run.output ? hash(run.output) : null,
390
+ });
391
+ setTimeout(() => runs.delete(run.run_id), 3_600_000).unref();
392
+ };
393
+ const server = createServer(async (req, res) => {
394
+ const provided = Buffer.from(req.headers.authorization ?? "");
395
+ const expected = Buffer.from(`Bearer ${token}`);
396
+ if (
397
+ provided.length !== expected.length ||
398
+ !timingSafeEqual(provided, expected)
399
+ )
400
+ return json(res, 401, { error: "claude_runs_auth_required" });
401
+ if (
402
+ req.headers["x-sellable-workspace-id"] !== connection.workspaceId ||
403
+ req.headers["x-sellable-agent-id"] !== connection.agentId
404
+ )
405
+ return json(res, 403, { error: "claude_runs_scope_rejected" });
406
+ if (req.method === "GET" && req.url === "/health")
407
+ return json(res, 200, {
408
+ ok: true,
409
+ provider: "claude-code",
410
+ active: inFlight.plain + inFlight.tools,
411
+ toolActive: inFlight.tools,
412
+ scope: {
413
+ workspaceId: connection.workspaceId,
414
+ agentId: connection.agentId,
415
+ },
416
+ });
417
+ if (req.method === "POST" && req.url === "/v1/runs") {
418
+ if (full("plain") && (!connection.toolEndpointIds || full("tools")))
419
+ return json(res, 429, { error: "claude_runs_busy" });
420
+ let request;
421
+ try {
422
+ let size = 0;
423
+ const chunks = [];
424
+ for await (const chunk of req) {
425
+ size += chunk.length;
426
+ if (size > MAX_BODY) {
427
+ json(res, 413, { error: "claude_runs_input_too_large" });
428
+ return;
429
+ }
430
+ chunks.push(chunk);
431
+ }
432
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
433
+ request = validateClaudeRunsRequest(body, connection.models);
434
+ request.tools = resolveTools(request);
435
+ } catch (error) {
436
+ recordAudit({
437
+ status: "request-rejected",
438
+ reason: /^claude_runs_[a-z_]+$/.test(error.message ?? "")
439
+ ? error.message
440
+ : "claude_runs_invalid_request",
441
+ });
442
+ return json(res, 400, { error: "claude_runs_request_rejected" });
443
+ }
444
+ // Check again after reading: concurrent uploads cannot over-admit.
445
+ const pool = request.tools ? "tools" : "plain";
446
+ if (full(pool)) return json(res, 429, { error: "claude_runs_busy" });
447
+ const run = {
448
+ run_id: `run_${randomBytes(16).toString("hex")}`,
449
+ status: "running",
450
+ startedAt: Date.now(),
451
+ listeners: new Set(),
452
+ consumed: false,
453
+ pool,
454
+ };
455
+ inFlight[pool]++;
456
+ runs.set(run.run_id, run);
457
+ try {
458
+ run.execution = execute(request);
459
+ run.execution.promise
460
+ .then(
461
+ (result) =>
462
+ terminal(run, "run.completed", {
463
+ status: "completed",
464
+ ...result,
465
+ }),
466
+ (error) =>
467
+ terminal(
468
+ run,
469
+ run.stopping && error.message === "claude_runs_cancelled"
470
+ ? "run.cancelled"
471
+ : "run.failed",
472
+ run.stopping && error.message === "claude_runs_cancelled"
473
+ ? { status: "cancelled" }
474
+ : { status: "failed", error: "claude_runs_generation_failed" }
475
+ )
476
+ )
477
+ .finally(() => inFlight[pool]--);
478
+ } catch {
479
+ inFlight[pool]--;
480
+ terminal(run, "run.failed", {
481
+ status: "failed",
482
+ error: "claude_runs_cli_unavailable",
483
+ });
484
+ }
485
+ return json(res, 202, { run_id: run.run_id, status: "started" });
486
+ }
487
+ const match =
488
+ /^\/v1\/runs\/(run_[a-f0-9]{32})(?:\/(events|stop|approval))?$/.exec(
489
+ req.url ?? ""
490
+ );
491
+ const run = match && runs.get(match[1]);
492
+ if (!run) return json(res, 404, { error: "claude_runs_run_missing" });
493
+ if (req.method === "GET" && !match[2])
494
+ return json(res, 200, {
495
+ run_id: run.run_id,
496
+ status: run.status,
497
+ ...(run.output
498
+ ? { output: run.output, runtime: run.runtime, usage: run.usage }
499
+ : {}),
500
+ ...(run.error ? { error: run.error } : {}),
501
+ });
502
+ if (req.method === "GET" && match[2] === "events") {
503
+ if (run.consumed)
504
+ return json(res, 404, { error: "claude_runs_events_consumed" });
505
+ run.consumed = true;
506
+ res.writeHead(200, {
507
+ "content-type": "text/event-stream",
508
+ "cache-control": "no-store",
509
+ });
510
+ res.flushHeaders();
511
+ if (run.event) return res.end(`data: ${JSON.stringify(run.event)}\n\n`);
512
+ run.listeners.add(res);
513
+ const heartbeat = setInterval(
514
+ () => res.write(": keep-alive\n\n"),
515
+ 15_000
516
+ );
517
+ res.once("close", () => {
518
+ clearInterval(heartbeat);
519
+ run.listeners.delete(res);
520
+ });
521
+ return;
522
+ }
523
+ if (req.method === "POST" && match[2] === "stop") {
524
+ if (run.event)
525
+ return json(res, 200, {
526
+ run_id: run.run_id,
527
+ status: run.status,
528
+ ...(run.output
529
+ ? { output: run.output, runtime: run.runtime, usage: run.usage }
530
+ : {}),
531
+ });
532
+ run.stopping = true;
533
+ run.execution.stop();
534
+ return json(res, 200, { run_id: run.run_id, status: "stopping" });
535
+ }
536
+ return json(res, 405, { error: "claude_runs_method_rejected" });
537
+ });
538
+ server.stopActiveRuns = async () => {
539
+ const activeRuns = [...runs.values()].filter(
540
+ (run) => !run.event && run.execution
541
+ );
542
+ for (const run of activeRuns) {
543
+ run.stopping = true;
544
+ run.execution.stop();
545
+ }
546
+ await Promise.allSettled(activeRuns.map((run) => run.execution.promise));
547
+ };
548
+ return server;
549
+ }
550
+
551
+ // The Admin runtime owns these listeners for its lifetime. Image-owned startup
552
+ // plus the persistent CLI/auth directory makes normal restarts repeatable.
553
+ export async function startConfiguredClaudeRunsApis({
554
+ env = process.env,
555
+ diagnostic = () => {},
556
+ createApi = createClaudeRunsApi,
557
+ readToken = (path) => readFileSync(path, "utf8").trim(),
558
+ toolLanes = null,
559
+ } = {}) {
560
+ const services = [];
561
+ for (const connection of parseRunsApiConnections(
562
+ env[RUNS_API_CONNECTIONS_ENV]
563
+ )) {
564
+ if (
565
+ !connection.localRoot ||
566
+ connection.runtimeId !== env.SELLABLE_RUNTIME_ID
567
+ )
568
+ continue;
569
+ try {
570
+ const token = readToken(connection.apiKeyFile);
571
+ const server = createApi({
572
+ token,
573
+ connection,
574
+ toolLanes,
575
+ audit: (entry) =>
576
+ appendFileSync(
577
+ `${connection.localRoot}/audit.jsonl`,
578
+ `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`,
579
+ { mode: 0o600 }
580
+ ),
581
+ });
582
+ await new Promise((resolve, reject) => {
583
+ server.once("error", reject);
584
+ server.listen(
585
+ Number(new URL(connection.baseUrl).port),
586
+ "127.0.0.1",
587
+ resolve
588
+ );
589
+ });
590
+ services.push(server);
591
+ diagnostic({
592
+ ok: true,
593
+ code: "claude_runs_api_ready",
594
+ baseUrl: connection.baseUrl,
595
+ });
596
+ } catch {
597
+ diagnostic({ ok: false, code: "claude_runs_api_unavailable" });
598
+ }
599
+ }
600
+ return {
601
+ async stop() {
602
+ for (const server of services) {
603
+ await server.stopActiveRuns();
604
+ await new Promise((resolve) => {
605
+ server.close(resolve);
606
+ server.closeAllConnections();
607
+ });
608
+ }
609
+ },
610
+ };
611
+ }
@@ -93,12 +93,12 @@ import {
93
93
  validateEndpointUatCompletionControl,
94
94
  } from "./endpoint-uat-completion-gate.mjs";
95
95
  import {
96
- createHermesRunsClient,
97
96
  createHermesSessionRetention,
98
97
  HERMES_RUNS_FIXTURE_IDENTITY,
99
98
  validateEndpointModelSelection,
100
99
  validateOpenerOutputContract,
101
100
  } from "./hermes-runs-client.mjs";
101
+ import { createConfiguredRunsClient } from "./runs-api-connections.mjs";
102
102
  import {
103
103
  createOpenerStageExecution,
104
104
  usesOpenerDraftStrategy,
@@ -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.794";
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;
@@ -1605,8 +1605,7 @@ export function createCustomerEndpointRelay(options = {}) {
1605
1605
  runtimeGeneration,
1606
1606
  getApiKeyFile = ({ claim }) =>
1607
1607
  join(SECRET_ROOT, claim.runtime.profileId, "api-server-key"),
1608
- createRunsClient = ({ apiKeyFile }) =>
1609
- createHermesRunsClient({ apiKeyFile }),
1608
+ createRunsClient = createConfiguredRunsClient,
1610
1609
  now = () => new Date(),
1611
1610
  sleepImpl = sleep,
1612
1611
  diagnostic = () => {},
@@ -0,0 +1,22 @@
1
+ export const ENDPOINT_MODEL_SELECTION_ALLOWLIST: Readonly<{
2
+ openrouter: readonly [
3
+ "anthropic/claude-opus-5.5",
4
+ "anthropic/claude-sonnet-5.5",
5
+ ];
6
+ }>;
7
+ export const ENDPOINT_MODEL_REASONING_EFFORTS: readonly [
8
+ "low",
9
+ "medium",
10
+ "high",
11
+ "xhigh",
12
+ ];
13
+ export type EndpointModelSelection = Readonly<{
14
+ provider: string;
15
+ model: string;
16
+ reasoningEffort: (typeof ENDPOINT_MODEL_REASONING_EFFORTS)[number];
17
+ runsApiBaseUrl?: string;
18
+ }>;
19
+ export function normalizeRunsApiBaseUrl(value: unknown): string | null;
20
+ export function compileEndpointModelSelection(
21
+ value: unknown
22
+ ): EndpointModelSelection | null | "invalid";