@sellable/install 0.1.798 → 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.
- package/container/Dockerfile +4 -4
- package/container/README.md +1 -1
- package/lib/sellable-agent/fly-customer-image/Dockerfile +1 -1
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.d.mts +26 -9
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.mjs +237 -47
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +1 -1
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.d.mts +6 -1
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.mjs +26 -11
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.mjs +12 -6
- package/lib/sellable-agent/host-bootstrap.mjs +1 -1
- package/lib/sellable-agent/host-worker.mjs +1 -1
- package/lib/sellable-agent/profile-materializer.mjs +1 -1
- package/lib/sellable-agent/provisioning-adapter.mjs +1 -1
- package/package.json +1 -1
package/container/Dockerfile
CHANGED
|
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
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.
|
|
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.798" \
|
|
|
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.
|
|
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.
|
|
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
|
package/container/README.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
@@ -31,17 +31,25 @@ export type ClaudeRunsServer = Server & { stopActiveRuns(): Promise<void> };
|
|
|
31
31
|
export type ClaudeRunsApiOptions = {
|
|
32
32
|
token: string;
|
|
33
33
|
connection: RunsApiConnection;
|
|
34
|
-
execute?:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
execute?:
|
|
35
|
+
| ((request: ClaudeRunsRequest) => {
|
|
36
|
+
promise: Promise<ClaudeRunsResult>;
|
|
37
|
+
stop(): void;
|
|
38
|
+
})
|
|
39
|
+
| null;
|
|
38
40
|
audit?: (entry: Record<string, unknown>) => void;
|
|
39
41
|
maxConcurrency?: number;
|
|
40
42
|
maxToolConcurrency?: number;
|
|
41
43
|
toolLanes?: ClaudeRunsToolLanes | null;
|
|
42
|
-
checkAuthentication?: (
|
|
43
|
-
|
|
44
|
-
>;
|
|
44
|
+
checkAuthentication?: (
|
|
45
|
+
configDir: string
|
|
46
|
+
) => Promise<"READY" | "SIGN_IN_REQUIRED" | "UNAVAILABLE">;
|
|
47
|
+
accounts?: () => string[];
|
|
48
|
+
startRun?: (
|
|
49
|
+
request: ClaudeRunsRequest,
|
|
50
|
+
options: { root: string; configDir: string }
|
|
51
|
+
) => { promise: Promise<ClaudeRunsResult>; stop(code?: string): void };
|
|
52
|
+
now?: () => number;
|
|
45
53
|
};
|
|
46
54
|
export function validateClaudeRunsRequest(
|
|
47
55
|
body: unknown,
|
|
@@ -50,13 +58,20 @@ export function validateClaudeRunsRequest(
|
|
|
50
58
|
export function endpointToolNames(options: {
|
|
51
59
|
turnDescriptorPath: string;
|
|
52
60
|
sessionId: string;
|
|
53
|
-
|
|
61
|
+
/** Absent: every run gets the tools its own turn descriptor allows. */
|
|
62
|
+
toolEndpointIds?: string[];
|
|
54
63
|
}): string[] | null;
|
|
55
64
|
export function claudeInvocation(
|
|
56
65
|
request: ClaudeRunsRequest,
|
|
57
66
|
root: string,
|
|
58
|
-
mcpSpecPath?: string | null
|
|
67
|
+
mcpSpecPath?: string | null,
|
|
68
|
+
configDir?: string
|
|
59
69
|
): { command: string; args: string[]; options: SpawnOptions };
|
|
70
|
+
export function claudeAccountDirs(root: string): string[];
|
|
71
|
+
export function claudeUsageLimit(
|
|
72
|
+
stdout: string,
|
|
73
|
+
now?: number
|
|
74
|
+
): { retryAt: number } | null;
|
|
60
75
|
export function parseClaudeResult(
|
|
61
76
|
stdout: string,
|
|
62
77
|
request: ClaudeRunsRequest
|
|
@@ -65,6 +80,7 @@ export function startClaudeRun(
|
|
|
65
80
|
request: ClaudeRunsRequest,
|
|
66
81
|
options: {
|
|
67
82
|
root: string;
|
|
83
|
+
configDir?: string;
|
|
68
84
|
spawnImpl?: (...args: any[]) => any;
|
|
69
85
|
timeoutMs?: number;
|
|
70
86
|
}
|
|
@@ -82,6 +98,7 @@ export function startConfiguredClaudeRunsApis(options?: {
|
|
|
82
98
|
|
|
83
99
|
export function readClaudeAuthentication(options: {
|
|
84
100
|
root: string;
|
|
101
|
+
configDir?: string;
|
|
85
102
|
execFileImpl?: any;
|
|
86
103
|
timeoutMs?: number;
|
|
87
104
|
}): Promise<"READY" | "SIGN_IN_REQUIRED" | "UNAVAILABLE">;
|
|
@@ -2,7 +2,9 @@ import { execFile, spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
appendFileSync,
|
|
5
|
+
existsSync,
|
|
5
6
|
lstatSync,
|
|
7
|
+
readdirSync,
|
|
6
8
|
readFileSync,
|
|
7
9
|
rmSync,
|
|
8
10
|
writeFileSync,
|
|
@@ -98,15 +100,17 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
/**
|
|
101
|
-
* A
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* descriptor
|
|
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.
|
|
105
109
|
*/
|
|
106
110
|
export function endpointToolNames({
|
|
107
111
|
turnDescriptorPath,
|
|
108
112
|
sessionId,
|
|
109
|
-
toolEndpointIds,
|
|
113
|
+
toolEndpointIds = undefined,
|
|
110
114
|
}) {
|
|
111
115
|
const path = endpointMcpDescriptorPath(turnDescriptorPath, sessionId);
|
|
112
116
|
if (!path) return null;
|
|
@@ -118,21 +122,27 @@ export function endpointToolNames({
|
|
|
118
122
|
throw error;
|
|
119
123
|
}
|
|
120
124
|
const body = JSON.parse(readFileSync(path, "utf8"))?.body;
|
|
121
|
-
if (!toolEndpointIds.includes(body?.endpointId))
|
|
122
|
-
|
|
125
|
+
if (toolEndpointIds && !toolEndpointIds.includes(body?.endpointId))
|
|
126
|
+
return null;
|
|
127
|
+
const names = body?.allowedToolNames;
|
|
123
128
|
if (
|
|
124
129
|
endpointHermesSessionId(body) !== sessionId ||
|
|
125
130
|
!Array.isArray(names) ||
|
|
126
|
-
!names.length ||
|
|
131
|
+
(toolEndpointIds && !names.length) ||
|
|
127
132
|
names.some(
|
|
128
133
|
(name) => typeof name !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(name)
|
|
129
134
|
)
|
|
130
135
|
)
|
|
131
136
|
throw new Error("claude_runs_tool_descriptor_rejected");
|
|
132
|
-
return [...names];
|
|
137
|
+
return names.length ? [...names] : null;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
export function claudeInvocation(
|
|
140
|
+
export function claudeInvocation(
|
|
141
|
+
request,
|
|
142
|
+
root,
|
|
143
|
+
mcpSpecPath = null,
|
|
144
|
+
configDir = `${root}/claude`
|
|
145
|
+
) {
|
|
136
146
|
return {
|
|
137
147
|
command: `${root}/cli/node_modules/.bin/claude`,
|
|
138
148
|
args: [
|
|
@@ -189,8 +199,8 @@ export function claudeInvocation(request, root, mcpSpecPath = null) {
|
|
|
189
199
|
stdio: ["pipe", "pipe", "pipe"],
|
|
190
200
|
env: {
|
|
191
201
|
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
192
|
-
HOME:
|
|
193
|
-
CLAUDE_CONFIG_DIR:
|
|
202
|
+
HOME: configDir,
|
|
203
|
+
CLAUDE_CONFIG_DIR: configDir,
|
|
194
204
|
LANG: "C.UTF-8",
|
|
195
205
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
196
206
|
},
|
|
@@ -202,12 +212,15 @@ export function claudeInvocation(request, root, mcpSpecPath = null) {
|
|
|
202
212
|
// this boundary; CLI stdout can contain account identity and is never forwarded.
|
|
203
213
|
export function readClaudeAuthentication({
|
|
204
214
|
root,
|
|
215
|
+
configDir = `${root}/claude`,
|
|
205
216
|
execFileImpl = execFile,
|
|
206
217
|
timeoutMs = 8_000,
|
|
207
218
|
} = {}) {
|
|
208
219
|
const invocation = claudeInvocation(
|
|
209
220
|
{ instructions: "", input: "", model: "claude-opus", effort: "high" },
|
|
210
|
-
root
|
|
221
|
+
root,
|
|
222
|
+
null,
|
|
223
|
+
configDir
|
|
211
224
|
);
|
|
212
225
|
return new Promise((resolve) => {
|
|
213
226
|
execFileImpl(
|
|
@@ -234,6 +247,91 @@ export function readClaudeAuthentication({
|
|
|
234
247
|
});
|
|
235
248
|
}
|
|
236
249
|
|
|
250
|
+
// Each sign-in is its own Claude config folder: the primary `claude`, then
|
|
251
|
+
// `accounts/<name>` in name order. Refresh tokens rotate inside a folder, so a
|
|
252
|
+
// login is never copied between folders.
|
|
253
|
+
export function claudeAccountDirs(root) {
|
|
254
|
+
const dirs = [`${root}/claude`];
|
|
255
|
+
let names = [];
|
|
256
|
+
try {
|
|
257
|
+
names = readdirSync(`${root}/accounts`).sort();
|
|
258
|
+
} catch {
|
|
259
|
+
/* No extra accounts. */
|
|
260
|
+
}
|
|
261
|
+
for (const name of names)
|
|
262
|
+
if (
|
|
263
|
+
/^[a-z0-9][a-z0-9-]{0,62}$/.test(name) &&
|
|
264
|
+
existsSync(`${root}/accounts/${name}/.credentials.json`)
|
|
265
|
+
)
|
|
266
|
+
dirs.push(`${root}/accounts/${name}`);
|
|
267
|
+
return dirs;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const MONTHS = "janfebmaraprmayjunjulaugsepoctnovdec";
|
|
271
|
+
const USAGE_LIMIT_RETRY_MS = 15 * 60_000;
|
|
272
|
+
const SIGN_IN_TRUST_MS = 5 * 60_000;
|
|
273
|
+
|
|
274
|
+
// Claude Code reports a used-up plan as an API 429 result, for example
|
|
275
|
+
// "You've hit your weekly limit · resets Oct 7, 10am (UTC)". Returns when that
|
|
276
|
+
// account can be tried again, or null for any other result.
|
|
277
|
+
export function claudeUsageLimit(stdout, now = Date.now()) {
|
|
278
|
+
let value;
|
|
279
|
+
try {
|
|
280
|
+
value = JSON.parse(stdout);
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
if (
|
|
285
|
+
!record(value) ||
|
|
286
|
+
value.is_error !== true ||
|
|
287
|
+
value.api_error_status !== 429
|
|
288
|
+
)
|
|
289
|
+
return null;
|
|
290
|
+
const reset =
|
|
291
|
+
/resets\s+(?:([a-z]{3})[a-z]*\s+(\d{1,2}),?\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(UTC\)/i.exec(
|
|
292
|
+
String(value.result ?? "")
|
|
293
|
+
);
|
|
294
|
+
let retryAt = null;
|
|
295
|
+
if (reset) {
|
|
296
|
+
const hour =
|
|
297
|
+
(Number(reset[3]) % 12) + (reset[5].toLowerCase() === "pm" ? 12 : 0);
|
|
298
|
+
const minute = Number(reset[4] ?? 0);
|
|
299
|
+
const today = new Date(now);
|
|
300
|
+
if (reset[1]) {
|
|
301
|
+
const month = MONTHS.indexOf(reset[1].toLowerCase()) / 3;
|
|
302
|
+
if (Number.isInteger(month)) {
|
|
303
|
+
retryAt = Date.UTC(
|
|
304
|
+
today.getUTCFullYear(),
|
|
305
|
+
month,
|
|
306
|
+
Number(reset[2]),
|
|
307
|
+
hour,
|
|
308
|
+
minute
|
|
309
|
+
);
|
|
310
|
+
if (retryAt < now - 86_400_000)
|
|
311
|
+
retryAt = Date.UTC(
|
|
312
|
+
today.getUTCFullYear() + 1,
|
|
313
|
+
month,
|
|
314
|
+
Number(reset[2]),
|
|
315
|
+
hour,
|
|
316
|
+
minute
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
retryAt = Date.UTC(
|
|
321
|
+
today.getUTCFullYear(),
|
|
322
|
+
today.getUTCMonth(),
|
|
323
|
+
today.getUTCDate(),
|
|
324
|
+
hour,
|
|
325
|
+
minute
|
|
326
|
+
);
|
|
327
|
+
if (retryAt <= now) retryAt += 86_400_000;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (retryAt === null || retryAt <= now || retryAt > now + 8 * 86_400_000)
|
|
331
|
+
retryAt = now + USAGE_LIMIT_RETRY_MS;
|
|
332
|
+
return { retryAt };
|
|
333
|
+
}
|
|
334
|
+
|
|
237
335
|
export function parseClaudeResult(stdout, request) {
|
|
238
336
|
const result = JSON.parse(stdout);
|
|
239
337
|
const models = Object.keys(result.modelUsage ?? {});
|
|
@@ -272,7 +370,12 @@ export function parseClaudeResult(stdout, request) {
|
|
|
272
370
|
|
|
273
371
|
export function startClaudeRun(
|
|
274
372
|
request,
|
|
275
|
-
{
|
|
373
|
+
{
|
|
374
|
+
root,
|
|
375
|
+
configDir = `${root}/claude`,
|
|
376
|
+
spawnImpl = spawn,
|
|
377
|
+
timeoutMs = 750_000,
|
|
378
|
+
} = {}
|
|
276
379
|
) {
|
|
277
380
|
// The tool server's environment travels in a private per-run file, never
|
|
278
381
|
// in argv, and is removed when the run ends.
|
|
@@ -288,7 +391,7 @@ export function startClaudeRun(
|
|
|
288
391
|
const removeSpec = () => {
|
|
289
392
|
if (mcpSpecPath) rmSync(mcpSpecPath, { force: true });
|
|
290
393
|
};
|
|
291
|
-
const invocation = claudeInvocation(request, root, mcpSpecPath);
|
|
394
|
+
const invocation = claudeInvocation(request, root, mcpSpecPath, configDir);
|
|
292
395
|
let child;
|
|
293
396
|
try {
|
|
294
397
|
child = spawnImpl(invocation.command, invocation.args, invocation.options);
|
|
@@ -334,12 +437,16 @@ export function startClaudeRun(
|
|
|
334
437
|
clearTimeout(timer);
|
|
335
438
|
clearTimeout(killTimer);
|
|
336
439
|
removeSpec();
|
|
440
|
+
const stdout = Buffer.concat(chunks).toString("utf8");
|
|
441
|
+
const limit = reason ? null : claudeUsageLimit(stdout);
|
|
442
|
+
if (limit)
|
|
443
|
+
return reject(
|
|
444
|
+
Object.assign(new Error("claude_runs_usage_limited"), limit)
|
|
445
|
+
);
|
|
337
446
|
if (reason || code !== 0)
|
|
338
447
|
return reject(new Error(reason ?? "claude_runs_cli_failed"));
|
|
339
448
|
try {
|
|
340
|
-
resolve(
|
|
341
|
-
parseClaudeResult(Buffer.concat(chunks).toString("utf8"), request)
|
|
342
|
-
);
|
|
449
|
+
resolve(parseClaudeResult(stdout, request));
|
|
343
450
|
} catch {
|
|
344
451
|
reject(new Error("claude_runs_output_rejected"));
|
|
345
452
|
}
|
|
@@ -353,42 +460,125 @@ export function startClaudeRun(
|
|
|
353
460
|
export function createClaudeRunsApi({
|
|
354
461
|
token,
|
|
355
462
|
connection,
|
|
356
|
-
execute =
|
|
463
|
+
execute = null,
|
|
464
|
+
audit = () => {},
|
|
465
|
+
maxConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY,
|
|
466
|
+
// Tool lanes (replies, follow-ups) have their own slots so a batch of
|
|
467
|
+
// openers can never crowd them out.
|
|
468
|
+
maxToolConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY,
|
|
469
|
+
toolLanes = null,
|
|
470
|
+
checkAuthentication = (configDir) =>
|
|
471
|
+
readClaudeAuthentication({ root: connection.localRoot, configDir }),
|
|
472
|
+
accounts = () => claudeAccountDirs(connection.localRoot),
|
|
473
|
+
startRun = startClaudeRun,
|
|
474
|
+
now = Date.now,
|
|
475
|
+
} = {}) {
|
|
476
|
+
if (!/^[a-f0-9]{64}$/.test(token ?? ""))
|
|
477
|
+
throw new Error("claude_runs_key_rejected");
|
|
478
|
+
// An account whose plan is used up rests until its reset; runs go to the
|
|
479
|
+
// next signed-in account meanwhile, and back to the first once it resets.
|
|
480
|
+
const usageLimitedUntil = new Map();
|
|
481
|
+
const resting = (configDir) =>
|
|
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
|
+
};
|
|
508
|
+
const runOnAccounts = (request) => {
|
|
357
509
|
let activeRun;
|
|
358
510
|
let stopped = false;
|
|
359
511
|
return {
|
|
360
512
|
promise: (async () => {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
|
|
513
|
+
let failure = null;
|
|
514
|
+
for (const configDir of accounts()) {
|
|
515
|
+
if (resting(configDir)) {
|
|
516
|
+
failure = "claude_runs_usage_limited";
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const state = await signInState(configDir);
|
|
520
|
+
if (stopped) throw new Error("claude_runs_cancelled");
|
|
521
|
+
if (state === "SIGN_IN_REQUIRED") {
|
|
522
|
+
failure ??= "claude_runs_sign_in_required";
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
activeRun = startRun(request, {
|
|
526
|
+
root: connection.localRoot,
|
|
527
|
+
configDir,
|
|
528
|
+
});
|
|
529
|
+
try {
|
|
530
|
+
return await activeRun.promise;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (stopped || error.message !== "claude_runs_usage_limited") {
|
|
533
|
+
signedInAt.delete(configDir);
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
usageLimitedUntil.set(configDir, error.retryAt);
|
|
537
|
+
failure = "claude_runs_usage_limited";
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
throw new Error(failure ?? "claude_runs_sign_in_required");
|
|
371
541
|
})(),
|
|
372
542
|
stop() {
|
|
373
543
|
stopped = true;
|
|
374
544
|
activeRun?.stop();
|
|
375
545
|
},
|
|
376
546
|
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
547
|
+
};
|
|
548
|
+
const executeRun = execute ?? runOnAccounts;
|
|
549
|
+
const readHealth = async () => {
|
|
550
|
+
const dirs = accounts();
|
|
551
|
+
let ready = 0;
|
|
552
|
+
let signedOut = 0;
|
|
553
|
+
const retryAts = [];
|
|
554
|
+
for (const configDir of dirs) {
|
|
555
|
+
if (resting(configDir)) {
|
|
556
|
+
retryAts.push(usageLimitedUntil.get(configDir));
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const state = await signInState(configDir);
|
|
560
|
+
if (state === "READY") ready++;
|
|
561
|
+
if (state === "SIGN_IN_REQUIRED") signedOut++;
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
authentication: ready
|
|
565
|
+
? "READY"
|
|
566
|
+
: signedOut === dirs.length
|
|
567
|
+
? "SIGN_IN_REQUIRED"
|
|
568
|
+
: "UNAVAILABLE",
|
|
569
|
+
accounts: {
|
|
570
|
+
total: dirs.length,
|
|
571
|
+
ready,
|
|
572
|
+
usageLimited: retryAts.length,
|
|
573
|
+
retryAt: retryAts.length
|
|
574
|
+
? new Date(Math.min(...retryAts)).toISOString()
|
|
575
|
+
: null,
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
};
|
|
389
579
|
// The run's own tools and the profile's MCP server, or null for a tool-free run.
|
|
390
580
|
const resolveTools = (request) => {
|
|
391
|
-
if (!
|
|
581
|
+
if (!toolLanes) return null;
|
|
392
582
|
const names = endpointToolNames({
|
|
393
583
|
turnDescriptorPath: toolLanes.turnDescriptorPath,
|
|
394
584
|
sessionId: request.sessionId,
|
|
@@ -465,12 +655,11 @@ export function createClaudeRunsApi({
|
|
|
465
655
|
)
|
|
466
656
|
return json(res, 403, { error: "claude_runs_scope_rejected" });
|
|
467
657
|
if (req.method === "GET" && req.url === "/health") {
|
|
468
|
-
const authentication = await
|
|
469
|
-
() => "UNAVAILABLE"
|
|
470
|
-
);
|
|
658
|
+
const { authentication, accounts: accountHealth } = await readHealth();
|
|
471
659
|
return json(res, 200, {
|
|
472
660
|
ok: authentication === "READY",
|
|
473
661
|
authentication,
|
|
662
|
+
accounts: accountHealth,
|
|
474
663
|
provider: "claude-code",
|
|
475
664
|
active: inFlight.plain + inFlight.tools,
|
|
476
665
|
toolActive: inFlight.tools,
|
|
@@ -481,7 +670,7 @@ export function createClaudeRunsApi({
|
|
|
481
670
|
});
|
|
482
671
|
}
|
|
483
672
|
if (req.method === "POST" && req.url === "/v1/runs") {
|
|
484
|
-
if (full("plain") && (!
|
|
673
|
+
if (full("plain") && (!toolLanes || full("tools")))
|
|
485
674
|
return json(res, 429, { error: "claude_runs_busy" });
|
|
486
675
|
let request;
|
|
487
676
|
try {
|
|
@@ -521,7 +710,7 @@ export function createClaudeRunsApi({
|
|
|
521
710
|
inFlight[pool]++;
|
|
522
711
|
runs.set(run.run_id, run);
|
|
523
712
|
try {
|
|
524
|
-
run.execution =
|
|
713
|
+
run.execution = executeRun(request);
|
|
525
714
|
run.execution.promise
|
|
526
715
|
.then(
|
|
527
716
|
(result) =>
|
|
@@ -542,6 +731,7 @@ export function createClaudeRunsApi({
|
|
|
542
731
|
error: [
|
|
543
732
|
"claude_runs_sign_in_required",
|
|
544
733
|
"claude_runs_auth_check_unavailable",
|
|
734
|
+
"claude_runs_usage_limited",
|
|
545
735
|
].includes(error.message)
|
|
546
736
|
? error.message
|
|
547
737
|
: "claude_runs_generation_failed",
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
//
|
|
142
|
-
// service
|
|
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.
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|