@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,245 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ compileEndpointModelSelection,
4
+ normalizeRunsApiBaseUrl,
5
+ } from "./endpoint-model-selection.mjs";
6
+ import { createHermesRunsClient } from "./hermes-runs-client.mjs";
7
+ import { createOpenAiRunsClient } from "./openai-runs-client.mjs";
8
+
9
+ export const RUNS_API_CONNECTIONS_ENV =
10
+ "SELLABLE_AGENT_RUNS_API_CONNECTIONS_JSON";
11
+ const id = (value) =>
12
+ typeof value === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(value);
13
+ const record = (value) =>
14
+ value && typeof value === "object" && !Array.isArray(value);
15
+ const stable = (value) =>
16
+ Array.isArray(value)
17
+ ? `[${value.map(stable).join(",")}]`
18
+ : record(value)
19
+ ? `{${Object.keys(value)
20
+ .sort()
21
+ .map((key) => `${JSON.stringify(key)}:${stable(value[key])}`)
22
+ .join(",")}}`
23
+ : JSON.stringify(value);
24
+ const fail = () => {
25
+ throw new Error("runs_api_connection_configuration_rejected");
26
+ };
27
+
28
+ // Only the control plane's machine configuration supplies this registry. It
29
+ // contains credential file references, never credentials or request input.
30
+ export function parseRunsApiConnections(raw) {
31
+ if (raw === undefined || raw === "") return [];
32
+ if (typeof raw !== "string" || Buffer.byteLength(raw) > 64 * 1024) fail();
33
+ let entries;
34
+ try {
35
+ entries = JSON.parse(raw);
36
+ } catch {
37
+ fail();
38
+ }
39
+ if (!Array.isArray(entries) || entries.length > 32) fail();
40
+ const seen = new Set();
41
+ return entries.map((entry) => {
42
+ if (
43
+ !record(entry) ||
44
+ Object.keys(entry).some(
45
+ (key) =>
46
+ ![
47
+ "runtimeId",
48
+ "workspaceId",
49
+ "agentId",
50
+ "baseUrl",
51
+ "apiKeyFile",
52
+ "endpointIds",
53
+ "workKinds",
54
+ "models",
55
+ "localRoot",
56
+ "name",
57
+ "protocol",
58
+ "structuredOutputs",
59
+ ].includes(key)
60
+ ) ||
61
+ ![entry.runtimeId, entry.workspaceId, entry.agentId].every(id) ||
62
+ (entry.protocol !== undefined &&
63
+ !["runs-api", "openai-chat", "openai-responses"].includes(
64
+ entry.protocol
65
+ )) ||
66
+ (entry.structuredOutputs !== undefined &&
67
+ typeof entry.structuredOutputs !== "boolean") ||
68
+ (entry.name !== undefined &&
69
+ (typeof entry.name !== "string" ||
70
+ !/^[\p{L}\p{N} .()_-]{1,80}$/u.test(entry.name))) ||
71
+ typeof entry.apiKeyFile !== "string" ||
72
+ !/^\/(?:opt\/data|run\/secrets)\/[A-Za-z0-9_./-]{1,250}$/.test(
73
+ entry.apiKeyFile
74
+ ) ||
75
+ entry.apiKeyFile
76
+ .split("/")
77
+ .some((part) => part === "." || part === "..") ||
78
+ !Array.isArray(entry.endpointIds) ||
79
+ !entry.endpointIds.length ||
80
+ entry.endpointIds.length > 64 ||
81
+ !entry.endpointIds.every(id) ||
82
+ !Array.isArray(entry.workKinds) ||
83
+ !entry.workKinds.length ||
84
+ !entry.workKinds.every((kind) =>
85
+ ["STANDARD", "OPENER", "REPLY", "FOLLOW_UP"].includes(kind)
86
+ ) ||
87
+ !Array.isArray(entry.models) ||
88
+ !entry.models.length ||
89
+ entry.models.length > 16
90
+ )
91
+ fail();
92
+ const baseUrl = normalizeRunsApiBaseUrl(entry.baseUrl);
93
+ if (!baseUrl) fail();
94
+ const identity = `${entry.runtimeId}:${baseUrl}`;
95
+ if (seen.has(identity)) fail();
96
+ seen.add(identity);
97
+ const models = entry.models.map((model) => {
98
+ if (
99
+ !record(model) ||
100
+ Object.keys(model).sort().join(",") !==
101
+ "model,provider,reasoningEfforts" ||
102
+ !Array.isArray(model.reasoningEfforts) ||
103
+ !model.reasoningEfforts.length ||
104
+ model.reasoningEfforts.some(
105
+ (reasoningEffort) =>
106
+ compileEndpointModelSelection({
107
+ provider: model.provider,
108
+ model: model.model,
109
+ reasoningEffort,
110
+ runsApiBaseUrl: baseUrl,
111
+ }) === "invalid"
112
+ )
113
+ )
114
+ fail();
115
+ return { ...model, reasoningEfforts: [...model.reasoningEfforts] };
116
+ });
117
+ if (entry.localRoot !== undefined) {
118
+ const url = new URL(baseUrl);
119
+ if (
120
+ (entry.protocol !== undefined && entry.protocol !== "runs-api") ||
121
+ typeof entry.localRoot !== "string" ||
122
+ !/^\/opt\/data\/[A-Za-z0-9_-]{1,80}$/.test(entry.localRoot) ||
123
+ entry.apiKeyFile !== `${entry.localRoot}/api-key` ||
124
+ url.hostname !== "127.0.0.1" ||
125
+ url.protocol !== "http:" ||
126
+ url.pathname !== "/" ||
127
+ !url.port ||
128
+ Number(url.port) < 1024 ||
129
+ models.some(
130
+ (model) =>
131
+ model.provider !== "claude-code" ||
132
+ !/^claude-[a-z0-9-]{1,100}$/.test(model.model) ||
133
+ model.reasoningEfforts.some(
134
+ (effort) => !["low", "medium", "high"].includes(effort)
135
+ )
136
+ )
137
+ )
138
+ fail();
139
+ }
140
+ return Object.freeze({
141
+ ...entry,
142
+ baseUrl,
143
+ models,
144
+ endpointIds: [...entry.endpointIds],
145
+ workKinds: [...entry.workKinds],
146
+ });
147
+ });
148
+ }
149
+
150
+ export function runsApiConnectionId(connection) {
151
+ return `runs_${createHash("sha256")
152
+ .update(
153
+ JSON.stringify([
154
+ connection.runtimeId,
155
+ connection.workspaceId,
156
+ connection.agentId,
157
+ connection.baseUrl,
158
+ ])
159
+ )
160
+ .digest("hex")
161
+ .slice(0, 24)}`;
162
+ }
163
+
164
+ export function runsApiConnectionsForRuntime(raw, runtime) {
165
+ return parseRunsApiConnections(raw).filter(
166
+ (connection) =>
167
+ connection.runtimeId === runtime.runtimeId &&
168
+ connection.workspaceId === runtime.workspaceId &&
169
+ connection.agentId === runtime.agentId
170
+ );
171
+ }
172
+
173
+ /** @param {any} options */
174
+ export function createConfiguredRunsClient({
175
+ apiKeyFile,
176
+ claim,
177
+ env = process.env,
178
+ clientFactory = createHermesRunsClient,
179
+ }) {
180
+ const selection = claim?.run?.modelSelection;
181
+ if (!selection?.runsApiBaseUrl) return clientFactory({ apiKeyFile });
182
+ const model = compileEndpointModelSelection(selection);
183
+ if (!model || model === "invalid") throw new Error("runs_api_model_rejected");
184
+ const connection = runsApiConnectionsForRuntime(
185
+ env[RUNS_API_CONNECTIONS_ENV],
186
+ {
187
+ runtimeId: claim.runtime?.runtimeId,
188
+ workspaceId: claim.attempt?.workspaceId,
189
+ agentId: claim.attempt?.agentId,
190
+ }
191
+ ).find((value) => value.baseUrl === model.runsApiBaseUrl);
192
+ const workKind = claim.run.productExecution?.workKind ?? "STANDARD";
193
+ if (
194
+ !connection ||
195
+ env.SELLABLE_RUNTIME_ID !== connection.runtimeId ||
196
+ !connection.endpointIds.includes(claim.attempt.endpointId) ||
197
+ !connection.workKinds.includes(workKind) ||
198
+ !connection.models.some(
199
+ (allowed) =>
200
+ allowed.provider === model.provider &&
201
+ allowed.model === model.model &&
202
+ allowed.reasoningEfforts.includes(model.reasoningEffort)
203
+ )
204
+ )
205
+ throw new Error("runs_api_connection_not_authorized");
206
+ // This client instance retains one destination and one credential for the
207
+ // entire attempt, including stream/status/stop, even if settings later change.
208
+ const client = connection.protocol?.startsWith("openai-")
209
+ ? createOpenAiRunsClient({
210
+ connection,
211
+ responseSchema: claim.revision?.outputSchema,
212
+ modelSelection: model,
213
+ })
214
+ : clientFactory({
215
+ baseUrl: connection.baseUrl,
216
+ apiKeyFile: connection.apiKeyFile,
217
+ scopeHeaders: {
218
+ "x-sellable-workspace-id": connection.workspaceId,
219
+ "x-sellable-agent-id": connection.agentId,
220
+ },
221
+ });
222
+ return Object.freeze({
223
+ ...client,
224
+ submit(compiled, options) {
225
+ // Public semantic endpoints have a published schema but no product tuple.
226
+ // Preserve that existing contract when calling a custom Runs service.
227
+ const schema = claim.revision?.outputSchema;
228
+ if (compiled.outputContract || !schema?.properties?.proposal)
229
+ return client.submit(compiled, options);
230
+ return client.submit(
231
+ {
232
+ ...compiled,
233
+ outputContract: {
234
+ schemaVersion: "sellable-opener-output-contract/v1",
235
+ schema,
236
+ nativeProposalSchemaDigest: createHash("sha256")
237
+ .update(stable(schema.properties.proposal))
238
+ .digest("hex"),
239
+ },
240
+ },
241
+ options
242
+ );
243
+ },
244
+ });
245
+ }
@@ -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.794";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.795-wip.runtime-ux.20261004.1";
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.794" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.795-wip.runtime-ux.20261004.1" ||
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.794" ||
315
+ desired.installerPackage !== "@sellable/install@0.1.795-wip.runtime-ux.20261004.1" ||
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.794";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.795-wip.runtime-ux.20261004.1";
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.794",
3
+ "version": "0.1.795-wip.runtime-ux.20261004.1",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {