@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.
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.796
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.796" \
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.796' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.957') throw new Error('package identity rejected');" \
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.796', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.957', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
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
@@ -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.796` is
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.796
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
- startClaudeRun(request, { root: connection.localRoot }),
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: true,
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
- : { status: "failed", error: "claude_runs_generation_failed" }
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.796";
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).sort().join(",") !==
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 = clientFactory({
198
- baseUrl: connection.baseUrl,
199
- apiKeyFile: connection.apiKeyFile,
200
- scopeHeaders: {
201
- "x-sellable-workspace-id": connection.workspaceId,
202
- "x-sellable-agent-id": connection.agentId,
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,9 @@
1
+ export function checkRunsApiConnection(
2
+ request: any,
3
+ options?: any
4
+ ): Promise<{ connectionId: string; state: string; checkedAt: string }>;
5
+
6
+ export function verifyRunsApiSelection(
7
+ request: any,
8
+ options?: any
9
+ ): Promise<any>;
@@ -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.796";
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.796" ||
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.796" ||
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.796";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.796",
3
+ "version": "0.1.797-wip.runtime-ux.20261004.2",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {