@sellable/install 0.1.799 → 0.1.800

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.799
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.800
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.799" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.800" \
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.799" \
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.799' || 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.800' || 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.799', 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.800', 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.799` is
6
+ `@sellable/install@0.1.800` is
7
7
  published:
8
8
 
9
9
  ```sh
@@ -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.799
11
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.800
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
@@ -58,7 +58,8 @@ export function validateClaudeRunsRequest(
58
58
  export function endpointToolNames(options: {
59
59
  turnDescriptorPath: string;
60
60
  sessionId: string;
61
- toolEndpointIds: string[];
61
+ /** Absent: every run gets the tools its own turn descriptor allows. */
62
+ toolEndpointIds?: string[];
62
63
  }): string[] | null;
63
64
  export function claudeInvocation(
64
65
  request: ClaudeRunsRequest,
@@ -100,15 +100,17 @@ export function validateClaudeRunsRequest(body, models) {
100
100
  }
101
101
 
102
102
  /**
103
- * A tool-lane run's own allowed tools, read from the signed turn descriptor
104
- * the runtime installed for its session before submitting; null when the run
105
- * is not on a tool endpoint. This only narrows: the context proxy verifies the
106
- * descriptor itself before it lists or calls any tool.
103
+ * A run's own allowed tools, read from the signed turn descriptor the runtime
104
+ * installed for its session before submitting. With a tool list, only runs on
105
+ * those endpoints get tools (and must have some); without one, every run gets
106
+ * exactly the tools its descriptor allows, and a run allowed none is tool-free
107
+ * (null). This only narrows: the context proxy verifies the descriptor itself
108
+ * before it lists or calls any tool.
107
109
  */
108
110
  export function endpointToolNames({
109
111
  turnDescriptorPath,
110
112
  sessionId,
111
- toolEndpointIds,
113
+ toolEndpointIds = undefined,
112
114
  }) {
113
115
  const path = endpointMcpDescriptorPath(turnDescriptorPath, sessionId);
114
116
  if (!path) return null;
@@ -120,18 +122,19 @@ export function endpointToolNames({
120
122
  throw error;
121
123
  }
122
124
  const body = JSON.parse(readFileSync(path, "utf8"))?.body;
123
- if (!toolEndpointIds.includes(body?.endpointId)) return null;
124
- const names = body.allowedToolNames;
125
+ if (toolEndpointIds && !toolEndpointIds.includes(body?.endpointId))
126
+ return null;
127
+ const names = body?.allowedToolNames;
125
128
  if (
126
129
  endpointHermesSessionId(body) !== sessionId ||
127
130
  !Array.isArray(names) ||
128
- !names.length ||
131
+ (toolEndpointIds && !names.length) ||
129
132
  names.some(
130
133
  (name) => typeof name !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(name)
131
134
  )
132
135
  )
133
136
  throw new Error("claude_runs_tool_descriptor_rejected");
134
- return [...names];
137
+ return names.length ? [...names] : null;
135
138
  }
136
139
 
137
140
  export function claudeInvocation(
@@ -266,6 +269,7 @@ export function claudeAccountDirs(root) {
266
269
 
267
270
  const MONTHS = "janfebmaraprmayjunjulaugsepoctnovdec";
268
271
  const USAGE_LIMIT_RETRY_MS = 15 * 60_000;
272
+ const SIGN_IN_TRUST_MS = 5 * 60_000;
269
273
 
270
274
  // Claude Code reports a used-up plan as an API 429 result, for example
271
275
  // "You've hit your weekly limit · resets Oct 7, 10am (UTC)". Returns when that
@@ -476,6 +480,31 @@ export function createClaudeRunsApi({
476
480
  const usageLimitedUntil = new Map();
477
481
  const resting = (configDir) =>
478
482
  (usageLimitedUntil.get(configDir) ?? 0) > now();
483
+ // `claude auth status` starts a whole CLI: on the 4-CPU Admin machine seven
484
+ // at once take longer than its 8s timeout. Runs that arrive together share
485
+ // one check, a good answer is trusted for five minutes, and only a definite
486
+ // SIGN_IN_REQUIRED keeps a run off an account; an unanswered check runs and
487
+ // lets the CLI itself fail the run if the sign-in is really gone.
488
+ const signedInAt = new Map();
489
+ const signInChecks = new Map();
490
+ const signInState = (configDir) => {
491
+ if (now() - (signedInAt.get(configDir) ?? -Infinity) < SIGN_IN_TRUST_MS)
492
+ return Promise.resolve("READY");
493
+ let check = signInChecks.get(configDir);
494
+ if (!check) {
495
+ check = Promise.resolve()
496
+ .then(() => checkAuthentication(configDir))
497
+ .catch(() => "UNAVAILABLE")
498
+ .then((state) => {
499
+ signInChecks.delete(configDir);
500
+ if (state === "READY") signedInAt.set(configDir, now());
501
+ else signedInAt.delete(configDir);
502
+ return state;
503
+ });
504
+ signInChecks.set(configDir, check);
505
+ }
506
+ return check;
507
+ };
479
508
  const runOnAccounts = (request) => {
480
509
  let activeRun;
481
510
  let stopped = false;
@@ -487,13 +516,10 @@ export function createClaudeRunsApi({
487
516
  failure = "claude_runs_usage_limited";
488
517
  continue;
489
518
  }
490
- const state = await checkAuthentication(configDir);
519
+ const state = await signInState(configDir);
491
520
  if (stopped) throw new Error("claude_runs_cancelled");
492
- if (state !== "READY") {
493
- failure ??=
494
- state === "SIGN_IN_REQUIRED"
495
- ? "claude_runs_sign_in_required"
496
- : "claude_runs_auth_check_unavailable";
521
+ if (state === "SIGN_IN_REQUIRED") {
522
+ failure ??= "claude_runs_sign_in_required";
497
523
  continue;
498
524
  }
499
525
  activeRun = startRun(request, {
@@ -503,8 +529,10 @@ export function createClaudeRunsApi({
503
529
  try {
504
530
  return await activeRun.promise;
505
531
  } catch (error) {
506
- if (stopped || error.message !== "claude_runs_usage_limited")
532
+ if (stopped || error.message !== "claude_runs_usage_limited") {
533
+ signedInAt.delete(configDir);
507
534
  throw error;
535
+ }
508
536
  usageLimitedUntil.set(configDir, error.retryAt);
509
537
  failure = "claude_runs_usage_limited";
510
538
  }
@@ -528,9 +556,7 @@ export function createClaudeRunsApi({
528
556
  retryAts.push(usageLimitedUntil.get(configDir));
529
557
  continue;
530
558
  }
531
- const state = await checkAuthentication(configDir).catch(
532
- () => "UNAVAILABLE"
533
- );
559
+ const state = await signInState(configDir);
534
560
  if (state === "READY") ready++;
535
561
  if (state === "SIGN_IN_REQUIRED") signedOut++;
536
562
  }
@@ -552,7 +578,7 @@ export function createClaudeRunsApi({
552
578
  };
553
579
  // The run's own tools and the profile's MCP server, or null for a tool-free run.
554
580
  const resolveTools = (request) => {
555
- if (!connection.toolEndpointIds || !toolLanes) return null;
581
+ if (!toolLanes) return null;
556
582
  const names = endpointToolNames({
557
583
  turnDescriptorPath: toolLanes.turnDescriptorPath,
558
584
  sessionId: request.sessionId,
@@ -644,7 +670,7 @@ export function createClaudeRunsApi({
644
670
  });
645
671
  }
646
672
  if (req.method === "POST" && req.url === "/v1/runs") {
647
- if (full("plain") && (!connection.toolEndpointIds || full("tools")))
673
+ if (full("plain") && (!toolLanes || full("tools")))
648
674
  return json(res, 429, { error: "claude_runs_busy" });
649
675
  let request;
650
676
  try {
@@ -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.799";
129
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.800";
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;
@@ -5,7 +5,8 @@ export type RunsApiConnection = Readonly<{
5
5
  agentId: string;
6
6
  baseUrl: string;
7
7
  apiKeyFile: string;
8
- endpointIds: string[];
8
+ /** Absent: serves every endpoint of its agent. */
9
+ endpointIds?: string[];
9
10
  workKinds: Array<"STANDARD" | "OPENER" | "REPLY" | "FOLLOW_UP">;
10
11
  models: Array<{
11
12
  provider: string;
@@ -19,6 +20,10 @@ export type RunsApiConnection = Readonly<{
19
20
  structuredOutputs?: boolean;
20
21
  }>;
21
22
  export function runsApiConnectionId(connection: RunsApiConnection): string;
23
+ export function runsApiConnectionServesEndpoint(
24
+ connection: RunsApiConnection,
25
+ endpointId: string
26
+ ): boolean;
22
27
  export function parseRunsApiConnections(raw: unknown): RunsApiConnection[];
23
28
  export function runsApiConnectionsForRuntime(
24
29
  raw: unknown,
@@ -76,10 +76,12 @@ export function parseRunsApiConnections(raw) {
76
76
  entry.apiKeyFile
77
77
  .split("/")
78
78
  .some((part) => part === "." || part === "..") ||
79
- !Array.isArray(entry.endpointIds) ||
80
- !entry.endpointIds.length ||
81
- entry.endpointIds.length > 64 ||
82
- !entry.endpointIds.every(id) ||
79
+ // Absent: the connection serves every endpoint of its agent.
80
+ (entry.endpointIds !== undefined &&
81
+ (!Array.isArray(entry.endpointIds) ||
82
+ !entry.endpointIds.length ||
83
+ entry.endpointIds.length > 64 ||
84
+ !entry.endpointIds.every(id))) ||
83
85
  !Array.isArray(entry.workKinds) ||
84
86
  !entry.workKinds.length ||
85
87
  !entry.workKinds.every((kind) =>
@@ -138,24 +140,29 @@ export function parseRunsApiConnections(raw) {
138
140
  )
139
141
  fail();
140
142
  }
141
- // Runs on these endpoints get their own allowed tools from the image-owned
142
- // service; every other run stays tool-free.
143
+ // When present, only runs on these endpoints get their own allowed tools
144
+ // from the image-owned service. When absent, a run gets the tools its own
145
+ // signed turn descriptor allows, and a run allowed none stays tool-free.
143
146
  if (
144
147
  entry.toolEndpointIds !== undefined &&
145
148
  (entry.localRoot === undefined ||
146
149
  !Array.isArray(entry.toolEndpointIds) ||
147
150
  !entry.toolEndpointIds.length ||
151
+ !entry.toolEndpointIds.every(id) ||
148
152
  new Set(entry.toolEndpointIds).size !== entry.toolEndpointIds.length ||
149
- entry.toolEndpointIds.some(
150
- (value) => !entry.endpointIds.includes(value)
151
- ))
153
+ (entry.endpointIds !== undefined &&
154
+ entry.toolEndpointIds.some(
155
+ (value) => !entry.endpointIds.includes(value)
156
+ )))
152
157
  )
153
158
  fail();
154
159
  return Object.freeze({
155
160
  ...entry,
156
161
  baseUrl,
157
162
  models,
158
- endpointIds: [...entry.endpointIds],
163
+ ...(entry.endpointIds === undefined
164
+ ? {}
165
+ : { endpointIds: [...entry.endpointIds] }),
159
166
  workKinds: [...entry.workKinds],
160
167
  ...(entry.toolEndpointIds === undefined
161
168
  ? {}
@@ -164,6 +171,14 @@ export function parseRunsApiConnections(raw) {
164
171
  });
165
172
  }
166
173
 
174
+ /** Whether a trusted connection may serve runs of this endpoint. */
175
+ export function runsApiConnectionServesEndpoint(connection, endpointId) {
176
+ return (
177
+ connection.endpointIds === undefined ||
178
+ connection.endpointIds.includes(endpointId)
179
+ );
180
+ }
181
+
167
182
  export function runsApiConnectionId(connection) {
168
183
  return `runs_${createHash("sha256")
169
184
  .update(
@@ -210,7 +225,7 @@ export function createConfiguredRunsClient({
210
225
  if (
211
226
  !connection ||
212
227
  env.SELLABLE_RUNTIME_ID !== connection.runtimeId ||
213
- !connection.endpointIds.includes(claim.attempt.endpointId) ||
228
+ !runsApiConnectionServesEndpoint(connection, claim.attempt.endpointId) ||
214
229
  !connection.workKinds.includes(workKind) ||
215
230
  !connection.models.some(
216
231
  (allowed) =>
@@ -7,6 +7,7 @@ import {
7
7
  createConfiguredRunsClient,
8
8
  RUNS_API_CONNECTIONS_ENV,
9
9
  runsApiConnectionId,
10
+ runsApiConnectionServesEndpoint,
10
11
  runsApiConnectionsForRuntime,
11
12
  } from "./runs-api-connections.mjs";
12
13
 
@@ -35,7 +36,7 @@ export async function checkRunsApiConnection(
35
36
  ).find(
36
37
  (candidate) =>
37
38
  runsApiConnectionId(candidate) === request.connectionId &&
38
- candidate.endpointIds.includes(request.endpointId)
39
+ runsApiConnectionServesEndpoint(candidate, request.endpointId)
39
40
  );
40
41
  if (!connection) return result("NOT_CONFIGURED");
41
42
  try {
@@ -47,10 +48,12 @@ export async function checkRunsApiConnection(
47
48
  {
48
49
  headers: {
49
50
  authorization: `Bearer ${token}`,
50
- ...(connection.protocol?.startsWith("openai-") ? {} : {
51
- "x-sellable-workspace-id": connection.workspaceId,
52
- "x-sellable-agent-id": connection.agentId,
53
- }),
51
+ ...(connection.protocol?.startsWith("openai-")
52
+ ? {}
53
+ : {
54
+ "x-sellable-workspace-id": connection.workspaceId,
55
+ "x-sellable-agent-id": connection.agentId,
56
+ }),
54
57
  },
55
58
  redirect: "error",
56
59
  signal: AbortSignal.timeout(12_000),
@@ -141,7 +144,10 @@ export async function verifyRunsApiSelection(
141
144
  runsApiConnectionId(item) === request.connectionId &&
142
145
  item.baseUrl === selection.runsApiBaseUrl
143
146
  );
144
- if (!connection?.endpointIds.includes(request.endpointId))
147
+ if (
148
+ !connection ||
149
+ !runsApiConnectionServesEndpoint(connection, request.endpointId)
150
+ )
145
151
  return result("NOT_CONFIGURED");
146
152
  verificationWorkKind = connection.workKinds.includes("STANDARD")
147
153
  ? "STANDARD"
@@ -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.799";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.800";
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.799" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.800" ||
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.799" ||
315
+ desired.installerPackage !== "@sellable/install@0.1.800" ||
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.799";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.800";
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.799",
3
+ "version": "0.1.800",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {