@sellable/install 0.1.794 → 0.1.795-wip.runtime-ux.20261004.1

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,563 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
3
+ import { appendFileSync, readFileSync } from "node:fs";
4
+ import { createServer } from "node:http";
5
+ import { validateOpenerOutputContract } from "./hermes-runs-client.mjs";
6
+ import {
7
+ parseRunsApiConnections,
8
+ RUNS_API_CONNECTIONS_ENV,
9
+ } from "./runs-api-connections.mjs";
10
+
11
+ const MAX_BODY = 320 * 1024;
12
+ const MAX_OUTPUT = 48 * 1024;
13
+ const MAX_CLI_OUTPUT = 512 * 1024;
14
+ const hash = (s) => createHash("sha256").update(s).digest("hex");
15
+ const record = (v) => v && typeof v === "object" && !Array.isArray(v);
16
+
17
+ export function validateClaudeRunsRequest(body, models) {
18
+ const allowed = [
19
+ "input",
20
+ "instructions",
21
+ "conversation_history",
22
+ "session_id",
23
+ "provider",
24
+ "model",
25
+ "model_options",
26
+ ];
27
+ if (
28
+ !record(body) ||
29
+ Object.keys(body).some((k) => !allowed.includes(k)) ||
30
+ typeof body.input !== "string" ||
31
+ !body.input.length ||
32
+ Buffer.byteLength(body.input) > 128 * 1024 ||
33
+ typeof body.instructions !== "string" ||
34
+ !body.instructions.length ||
35
+ Buffer.byteLength(body.instructions) > 128 * 1024 ||
36
+ !Array.isArray(body.conversation_history) ||
37
+ body.conversation_history.length > 64 ||
38
+ Buffer.byteLength(JSON.stringify(body.conversation_history)) > 128 * 1024 ||
39
+ body.conversation_history.some(
40
+ (item) =>
41
+ !record(item) ||
42
+ Object.keys(item).sort().join(",") !== "content,role" ||
43
+ !["user", "assistant"].includes(item.role) ||
44
+ typeof item.content !== "string"
45
+ ) ||
46
+ !/^sellable_endpoint_[a-f0-9]{32}$/.test(body.session_id ?? "") ||
47
+ body.provider !== "claude-code" ||
48
+ !record(body.model_options) ||
49
+ Object.keys(body.model_options).some(
50
+ (key) =>
51
+ !["reasoning_effort", "sellable_opener_output_contract"].includes(key)
52
+ ) ||
53
+ !models.some(
54
+ (model) =>
55
+ model.provider === body.provider &&
56
+ model.model === body.model &&
57
+ model.reasoningEfforts.includes(body.model_options.reasoning_effort)
58
+ )
59
+ )
60
+ throw new Error("claude_runs_request_rejected");
61
+ const contract = body.model_options.sellable_opener_output_contract;
62
+ const schema =
63
+ contract === undefined
64
+ ? null
65
+ : validateOpenerOutputContract(contract).schema;
66
+ return {
67
+ input: body.conversation_history.length
68
+ ? JSON.stringify({
69
+ conversation_history: body.conversation_history,
70
+ input: body.input,
71
+ })
72
+ : body.input,
73
+ instructions: body.instructions,
74
+ schema,
75
+ model: body.model,
76
+ effort: body.model_options.reasoning_effort,
77
+ };
78
+ }
79
+
80
+ export function claudeInvocation(request, root) {
81
+ return {
82
+ command: `${root}/cli/node_modules/.bin/claude`,
83
+ args: [
84
+ "-p",
85
+ "--model",
86
+ request.model,
87
+ "--effort",
88
+ request.effort,
89
+ "--output-format",
90
+ "json",
91
+ ...(request.schema
92
+ ? ["--json-schema", JSON.stringify(request.schema)]
93
+ : []),
94
+ "--tools",
95
+ "",
96
+ "--permission-mode",
97
+ "dontAsk",
98
+ "--strict-mcp-config",
99
+ "--mcp-config",
100
+ '{"mcpServers":{}}',
101
+ "--setting-sources",
102
+ "",
103
+ "--settings",
104
+ '{"disableAllHooks":true,"autoMemoryEnabled":false}',
105
+ "--disable-slash-commands",
106
+ "--no-session-persistence",
107
+ "--max-turns",
108
+ "3",
109
+ "--system-prompt",
110
+ request.instructions,
111
+ ],
112
+ // Whitelist instead of inheriting the runtime's provider keys, URLs, hooks,
113
+ // plugin paths, NODE_OPTIONS, or workspace access credentials.
114
+ options: {
115
+ cwd: `${root}/work`,
116
+ detached: true,
117
+ stdio: ["pipe", "pipe", "pipe"],
118
+ env: {
119
+ PATH: "/usr/local/bin:/usr/bin:/bin",
120
+ HOME: `${root}/claude`,
121
+ CLAUDE_CONFIG_DIR: `${root}/claude`,
122
+ LANG: "C.UTF-8",
123
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
124
+ },
125
+ },
126
+ };
127
+ }
128
+
129
+ // Uses the same isolated environment as generation. Only a typed status leaves
130
+ // this boundary; CLI stdout can contain account identity and is never forwarded.
131
+ export function readClaudeAuthentication({
132
+ root,
133
+ execFileImpl = execFile,
134
+ timeoutMs = 8_000,
135
+ } = {}) {
136
+ const invocation = claudeInvocation(
137
+ { instructions: "", input: "", model: "claude-opus", effort: "high" },
138
+ root
139
+ );
140
+ return new Promise((resolve) => {
141
+ execFileImpl(
142
+ invocation.command,
143
+ ["auth", "status"],
144
+ {
145
+ cwd: invocation.options.cwd,
146
+ env: invocation.options.env,
147
+ timeout: timeoutMs,
148
+ maxBuffer: 16 * 1024,
149
+ encoding: "utf8",
150
+ },
151
+ (error, stdout) => {
152
+ try {
153
+ const value = JSON.parse(stdout);
154
+ if (value.loggedIn === false) return resolve("SIGN_IN_REQUIRED");
155
+ if (!error && value.loggedIn === true) return resolve("READY");
156
+ } catch {
157
+ /* A missing or incompatible CLI is unavailable, never ready. */
158
+ }
159
+ resolve("UNAVAILABLE");
160
+ }
161
+ );
162
+ });
163
+ }
164
+
165
+ export function parseClaudeResult(stdout, request) {
166
+ const result = JSON.parse(stdout);
167
+ const models = Object.keys(result.modelUsage ?? {});
168
+ if (
169
+ result.type !== "result" ||
170
+ result.is_error !== false ||
171
+ result.subtype !== "success" ||
172
+ (request.schema
173
+ ? !record(result.structured_output)
174
+ : typeof result.result !== "string") ||
175
+ (result.permission_denials ?? []).length ||
176
+ !models.length ||
177
+ models.some(
178
+ (model) =>
179
+ model !== request.model && !model.startsWith(`${request.model}-`)
180
+ )
181
+ )
182
+ throw new Error("claude_runs_output_rejected");
183
+ const output = request.schema
184
+ ? JSON.stringify(result.structured_output)
185
+ : result.result;
186
+ if (!output.trim()) throw new Error("claude_runs_output_empty");
187
+ if (Buffer.byteLength(output) > MAX_OUTPUT)
188
+ throw new Error("claude_runs_output_too_large");
189
+ return {
190
+ output,
191
+ runtime: { provider: "claude-code", model: models[0] },
192
+ usage: {
193
+ input_tokens: result.usage?.input_tokens ?? 0,
194
+ output_tokens: result.usage?.output_tokens ?? 0,
195
+ cache_read_tokens: result.usage?.cache_read_input_tokens ?? 0,
196
+ cache_write_tokens: result.usage?.cache_creation_input_tokens ?? 0,
197
+ },
198
+ };
199
+ }
200
+
201
+ export function startClaudeRun(
202
+ request,
203
+ { root, spawnImpl = spawn, timeoutMs = 750_000 } = {}
204
+ ) {
205
+ const invocation = claudeInvocation(request, root);
206
+ const child = spawnImpl(
207
+ invocation.command,
208
+ invocation.args,
209
+ invocation.options
210
+ );
211
+ let reason = null;
212
+ let killTimer;
213
+ const stop = (code = "claude_runs_cancelled") => {
214
+ reason ??= code;
215
+ try {
216
+ process.kill(-child.pid, "SIGTERM");
217
+ } catch {
218
+ child.kill("SIGTERM");
219
+ }
220
+ killTimer ??= setTimeout(() => {
221
+ try {
222
+ process.kill(-child.pid, "SIGKILL");
223
+ } catch {
224
+ child.kill("SIGKILL");
225
+ }
226
+ }, 5_000);
227
+ killTimer.unref();
228
+ };
229
+ const promise = new Promise((resolve, reject) => {
230
+ const chunks = [];
231
+ let bytes = 0;
232
+ const timer = setTimeout(() => stop("claude_runs_timeout"), timeoutMs);
233
+ child.stdout.on("data", (chunk) => {
234
+ bytes += chunk.length;
235
+ if (bytes > MAX_CLI_OUTPUT) stop("claude_runs_output_too_large");
236
+ else chunks.push(chunk);
237
+ });
238
+ // Drain diagnostics without logging prompts, credentials, or message bodies.
239
+ child.stderr.on("data", () => {});
240
+ child.once("error", () => {
241
+ clearTimeout(timer);
242
+ reject(new Error("claude_runs_cli_unavailable"));
243
+ });
244
+ child.once("close", (code) => {
245
+ clearTimeout(timer);
246
+ clearTimeout(killTimer);
247
+ if (reason || code !== 0)
248
+ return reject(new Error(reason ?? "claude_runs_cli_failed"));
249
+ try {
250
+ resolve(
251
+ parseClaudeResult(Buffer.concat(chunks).toString("utf8"), request)
252
+ );
253
+ } catch {
254
+ reject(new Error("claude_runs_output_rejected"));
255
+ }
256
+ });
257
+ child.stdin.on("error", () => {});
258
+ child.stdin.end(request.input);
259
+ });
260
+ return { promise, stop };
261
+ }
262
+
263
+ export function createClaudeRunsApi({
264
+ token,
265
+ connection,
266
+ execute = (request) => {
267
+ let activeRun;
268
+ let stopped = false;
269
+ return {
270
+ promise: (async () => {
271
+ const state = await checkAuthentication();
272
+ if (stopped) throw new Error("claude_runs_cancelled");
273
+ if (state !== "READY")
274
+ throw new Error(
275
+ state === "SIGN_IN_REQUIRED"
276
+ ? "claude_runs_sign_in_required"
277
+ : "claude_runs_auth_check_unavailable"
278
+ );
279
+ activeRun = startClaudeRun(request, { root: connection.localRoot });
280
+ return activeRun.promise;
281
+ })(),
282
+ stop() {
283
+ stopped = true;
284
+ activeRun?.stop();
285
+ },
286
+ };
287
+ },
288
+ audit = () => {},
289
+ maxConcurrency = 2,
290
+ checkAuthentication = () =>
291
+ readClaudeAuthentication({ root: connection.localRoot }),
292
+ } = {}) {
293
+ if (!/^[a-f0-9]{64}$/.test(token ?? ""))
294
+ throw new Error("claude_runs_key_rejected");
295
+ const recordAudit = (entry) => {
296
+ try {
297
+ audit(entry);
298
+ } catch {
299
+ /* Logging cannot crash the Admin runtime. */
300
+ }
301
+ };
302
+ const runs = new Map();
303
+ let active = 0;
304
+ const json = (res, status, body) => {
305
+ res.writeHead(status, {
306
+ "content-type": "application/json",
307
+ "cache-control": "no-store",
308
+ "retry-after": "2",
309
+ });
310
+ res.end(JSON.stringify(body));
311
+ };
312
+ const terminal = (run, event, fields) => {
313
+ Object.assign(run, fields);
314
+ run.event = {
315
+ event,
316
+ run_id: run.run_id,
317
+ timestamp: new Date().toISOString(),
318
+ ...(run.status === "completed"
319
+ ? { output: run.output, runtime: run.runtime, usage: run.usage }
320
+ : run.status === "failed"
321
+ ? { error: run.error }
322
+ : {}),
323
+ };
324
+ for (const res of run.listeners)
325
+ res.end(`data: ${JSON.stringify(run.event)}\n\n`);
326
+ run.listeners.clear();
327
+ recordAudit({
328
+ runId: run.run_id,
329
+ status: run.status,
330
+ servedRuntime: run.runtime ?? null,
331
+ usage: run.usage ?? null,
332
+ durationMs: Date.now() - run.startedAt,
333
+ outputDigest: run.output ? hash(run.output) : null,
334
+ });
335
+ setTimeout(() => runs.delete(run.run_id), 3_600_000).unref();
336
+ };
337
+ const server = createServer(async (req, res) => {
338
+ const provided = Buffer.from(req.headers.authorization ?? "");
339
+ const expected = Buffer.from(`Bearer ${token}`);
340
+ if (
341
+ provided.length !== expected.length ||
342
+ !timingSafeEqual(provided, expected)
343
+ )
344
+ return json(res, 401, { error: "claude_runs_auth_required" });
345
+ if (
346
+ req.headers["x-sellable-workspace-id"] !== connection.workspaceId ||
347
+ req.headers["x-sellable-agent-id"] !== connection.agentId
348
+ )
349
+ return json(res, 403, { error: "claude_runs_scope_rejected" });
350
+ if (req.method === "GET" && req.url === "/health") {
351
+ const authentication = await checkAuthentication().catch(
352
+ () => "UNAVAILABLE"
353
+ );
354
+ return json(res, 200, {
355
+ ok: authentication === "READY",
356
+ authentication,
357
+ provider: "claude-code",
358
+ active,
359
+ scope: {
360
+ workspaceId: connection.workspaceId,
361
+ agentId: connection.agentId,
362
+ },
363
+ });
364
+ }
365
+ if (req.method === "POST" && req.url === "/v1/runs") {
366
+ if (active >= maxConcurrency || runs.size >= 200)
367
+ return json(res, 429, { error: "claude_runs_busy" });
368
+ let request;
369
+ try {
370
+ let size = 0;
371
+ const chunks = [];
372
+ for await (const chunk of req) {
373
+ size += chunk.length;
374
+ if (size > MAX_BODY) {
375
+ json(res, 413, { error: "claude_runs_input_too_large" });
376
+ return;
377
+ }
378
+ chunks.push(chunk);
379
+ }
380
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
381
+ request = validateClaudeRunsRequest(body, connection.models);
382
+ } catch (error) {
383
+ recordAudit({
384
+ status: "request-rejected",
385
+ reason: /^claude_runs_[a-z_]+$/.test(error.message ?? "")
386
+ ? error.message
387
+ : "claude_runs_invalid_request",
388
+ });
389
+ return json(res, 400, { error: "claude_runs_request_rejected" });
390
+ }
391
+ // Check again after reading: concurrent uploads cannot over-admit.
392
+ if (active >= maxConcurrency || runs.size >= 200)
393
+ return json(res, 429, { error: "claude_runs_busy" });
394
+ const run = {
395
+ run_id: `run_${randomBytes(16).toString("hex")}`,
396
+ status: "running",
397
+ startedAt: Date.now(),
398
+ listeners: new Set(),
399
+ consumed: false,
400
+ };
401
+ active++;
402
+ runs.set(run.run_id, run);
403
+ try {
404
+ run.execution = execute(request);
405
+ run.execution.promise
406
+ .then(
407
+ (result) =>
408
+ terminal(run, "run.completed", {
409
+ status: "completed",
410
+ ...result,
411
+ }),
412
+ (error) =>
413
+ terminal(
414
+ run,
415
+ run.stopping && error.message === "claude_runs_cancelled"
416
+ ? "run.cancelled"
417
+ : "run.failed",
418
+ run.stopping && error.message === "claude_runs_cancelled"
419
+ ? { status: "cancelled" }
420
+ : {
421
+ status: "failed",
422
+ error: [
423
+ "claude_runs_sign_in_required",
424
+ "claude_runs_auth_check_unavailable",
425
+ ].includes(error.message)
426
+ ? error.message
427
+ : "claude_runs_generation_failed",
428
+ }
429
+ )
430
+ )
431
+ .finally(() => active--);
432
+ } catch {
433
+ active--;
434
+ terminal(run, "run.failed", {
435
+ status: "failed",
436
+ error: "claude_runs_cli_unavailable",
437
+ });
438
+ }
439
+ return json(res, 202, { run_id: run.run_id, status: "started" });
440
+ }
441
+ const match =
442
+ /^\/v1\/runs\/(run_[a-f0-9]{32})(?:\/(events|stop|approval))?$/.exec(
443
+ req.url ?? ""
444
+ );
445
+ const run = match && runs.get(match[1]);
446
+ if (!run) return json(res, 404, { error: "claude_runs_run_missing" });
447
+ if (req.method === "GET" && !match[2])
448
+ return json(res, 200, {
449
+ run_id: run.run_id,
450
+ status: run.status,
451
+ ...(run.output
452
+ ? { output: run.output, runtime: run.runtime, usage: run.usage }
453
+ : {}),
454
+ ...(run.error ? { error: run.error } : {}),
455
+ });
456
+ if (req.method === "GET" && match[2] === "events") {
457
+ if (run.consumed)
458
+ return json(res, 404, { error: "claude_runs_events_consumed" });
459
+ run.consumed = true;
460
+ res.writeHead(200, {
461
+ "content-type": "text/event-stream",
462
+ "cache-control": "no-store",
463
+ });
464
+ res.flushHeaders();
465
+ if (run.event) return res.end(`data: ${JSON.stringify(run.event)}\n\n`);
466
+ run.listeners.add(res);
467
+ const heartbeat = setInterval(
468
+ () => res.write(": keep-alive\n\n"),
469
+ 15_000
470
+ );
471
+ res.once("close", () => {
472
+ clearInterval(heartbeat);
473
+ run.listeners.delete(res);
474
+ });
475
+ return;
476
+ }
477
+ if (req.method === "POST" && match[2] === "stop") {
478
+ if (run.event)
479
+ return json(res, 200, {
480
+ run_id: run.run_id,
481
+ status: run.status,
482
+ ...(run.output
483
+ ? { output: run.output, runtime: run.runtime, usage: run.usage }
484
+ : {}),
485
+ });
486
+ run.stopping = true;
487
+ run.execution.stop();
488
+ return json(res, 200, { run_id: run.run_id, status: "stopping" });
489
+ }
490
+ return json(res, 405, { error: "claude_runs_method_rejected" });
491
+ });
492
+ server.stopActiveRuns = async () => {
493
+ const activeRuns = [...runs.values()].filter(
494
+ (run) => !run.event && run.execution
495
+ );
496
+ for (const run of activeRuns) {
497
+ run.stopping = true;
498
+ run.execution.stop();
499
+ }
500
+ await Promise.allSettled(activeRuns.map((run) => run.execution.promise));
501
+ };
502
+ return server;
503
+ }
504
+
505
+ // The Admin runtime owns these listeners for its lifetime. Image-owned startup
506
+ // plus the persistent CLI/auth directory makes normal restarts repeatable.
507
+ export async function startConfiguredClaudeRunsApis({
508
+ env = process.env,
509
+ diagnostic = () => {},
510
+ createApi = createClaudeRunsApi,
511
+ readToken = (path) => readFileSync(path, "utf8").trim(),
512
+ } = {}) {
513
+ const services = [];
514
+ for (const connection of parseRunsApiConnections(
515
+ env[RUNS_API_CONNECTIONS_ENV]
516
+ )) {
517
+ if (
518
+ !connection.localRoot ||
519
+ connection.runtimeId !== env.SELLABLE_RUNTIME_ID
520
+ )
521
+ continue;
522
+ try {
523
+ const token = readToken(connection.apiKeyFile);
524
+ const server = createApi({
525
+ token,
526
+ connection,
527
+ audit: (entry) =>
528
+ appendFileSync(
529
+ `${connection.localRoot}/audit.jsonl`,
530
+ `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`,
531
+ { mode: 0o600 }
532
+ ),
533
+ });
534
+ await new Promise((resolve, reject) => {
535
+ server.once("error", reject);
536
+ server.listen(
537
+ Number(new URL(connection.baseUrl).port),
538
+ "127.0.0.1",
539
+ resolve
540
+ );
541
+ });
542
+ services.push(server);
543
+ diagnostic({
544
+ ok: true,
545
+ code: "claude_runs_api_ready",
546
+ baseUrl: connection.baseUrl,
547
+ });
548
+ } catch {
549
+ diagnostic({ ok: false, code: "claude_runs_api_unavailable" });
550
+ }
551
+ }
552
+ return {
553
+ async stop() {
554
+ for (const server of services) {
555
+ await server.stopActiveRuns();
556
+ await new Promise((resolve) => {
557
+ server.close(resolve);
558
+ server.closeAllConnections();
559
+ });
560
+ }
561
+ },
562
+ };
563
+ }
@@ -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.795-wip.runtime-ux.20261004.1";
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,26 @@
1
+ export const ENDPOINT_MODEL_SELECTION_ALLOWLIST: Readonly<{
2
+ openrouter: readonly [
3
+ "anthropic/claude-opus-5.5",
4
+ "deepseek/deepseek-v4-flash-0731",
5
+ "openai/gpt-6-luna",
6
+ "anthropic/claude-sonnet-5.5",
7
+ ];
8
+ }>;
9
+ export const ENDPOINT_MODEL_REASONING_EFFORTS: readonly [
10
+ "none",
11
+ "low",
12
+ "medium",
13
+ "high",
14
+ "xhigh",
15
+ ];
16
+ export type EndpointModelSelection = Readonly<{
17
+ provider: string;
18
+ model: string;
19
+ reasoningEffort: (typeof ENDPOINT_MODEL_REASONING_EFFORTS)[number];
20
+ runsApiBaseUrl?: string;
21
+ fallbackPolicy?: "STOP" | "PRODUCT_DEFAULT";
22
+ }>;
23
+ export function normalizeRunsApiBaseUrl(value: unknown): string | null;
24
+ export function compileEndpointModelSelection(
25
+ value: unknown
26
+ ): EndpointModelSelection | null | "invalid";