@sellable/install 0.1.810 → 0.1.813
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 +7 -7
- package/container/README.md +1 -1
- package/lib/sellable-agent/external-runtime-builder.mjs +1 -1
- package/lib/sellable-agent/fly-admin-image/Dockerfile +1 -2
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +1 -48
- package/lib/sellable-agent/fly-customer-image/Dockerfile +3 -3
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.d.mts +16 -39
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.mjs +75 -240
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +70 -22
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.d.mts +1 -0
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.mjs +11 -15
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.d.mts +1 -25
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.mjs +9 -237
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.mjs +12 -153
- package/lib/sellable-agent/fly-runtime-identity.mjs +5 -2
- package/lib/sellable-agent/host-bootstrap.mjs +2 -2
- package/lib/sellable-agent/host-worker.mjs +2 -2
- package/lib/sellable-agent/profile-materializer.mjs +2 -2
- package/lib/sellable-agent/provisioning-adapter.mjs +2 -2
- package/package.json +1 -1
- package/lib/sellable-agent/fly-customer-image/claude-code-mcp-session.d.mts +0 -1
- package/lib/sellable-agent/fly-customer-image/claude-code-mcp-session.mjs +0 -53
|
@@ -13,17 +13,8 @@ import {
|
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { createServer } from "node:http";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
-
import {
|
|
18
|
-
endpointHermesSessionId,
|
|
19
|
-
endpointMcpDescriptorPath,
|
|
20
|
-
} from "../mcp-context-proxy.mjs";
|
|
21
16
|
import { normalizeRunsApiBaseUrl } from "./endpoint-model-selection.mjs";
|
|
22
17
|
import { validateOpenerOutputContract } from "./hermes-runs-client.mjs";
|
|
23
|
-
import {
|
|
24
|
-
parseRunsApiConnections,
|
|
25
|
-
RUNS_API_CONNECTIONS_ENV,
|
|
26
|
-
} from "./runs-api-connections.mjs";
|
|
27
18
|
|
|
28
19
|
// Each pool runs as many generations at once as one Hermes endpoint may
|
|
29
20
|
// (ENDPOINT_USER_MAX_CONCURRENCY); the runtime still caps the machine at 10.
|
|
@@ -31,13 +22,12 @@ export const CLAUDE_RUNS_MAX_CONCURRENCY = 8;
|
|
|
31
22
|
const MAX_BODY = 320 * 1024;
|
|
32
23
|
const MAX_OUTPUT = 48 * 1024;
|
|
33
24
|
const MAX_CLI_OUTPUT = 512 * 1024;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
25
|
+
// The CLI, its sign-ins and the audit log live here on the Admin machine.
|
|
26
|
+
const SERVICE_ROOT = "/opt/data/private-opener";
|
|
37
27
|
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
38
28
|
const record = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
39
|
-
// The
|
|
40
|
-
//
|
|
29
|
+
// The service has no model list: any Claude model, at the efforts the CLI
|
|
30
|
+
// takes.
|
|
41
31
|
const publicModel = (body) =>
|
|
42
32
|
typeof body.model === "string" &&
|
|
43
33
|
/^claude-[a-z0-9-]{1,100}$/.test(body.model) &&
|
|
@@ -73,7 +63,7 @@ function remoteTools(tools) {
|
|
|
73
63
|
};
|
|
74
64
|
}
|
|
75
65
|
|
|
76
|
-
export function validateClaudeRunsRequest(body
|
|
66
|
+
export function validateClaudeRunsRequest(body) {
|
|
77
67
|
const allowed = [
|
|
78
68
|
"input",
|
|
79
69
|
"instructions",
|
|
@@ -82,7 +72,7 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
82
72
|
"provider",
|
|
83
73
|
"model",
|
|
84
74
|
"model_options",
|
|
85
|
-
|
|
75
|
+
"tools",
|
|
86
76
|
];
|
|
87
77
|
if (
|
|
88
78
|
!record(body) ||
|
|
@@ -110,14 +100,7 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
110
100
|
(key) =>
|
|
111
101
|
!["reasoning_effort", "sellable_opener_output_contract"].includes(key)
|
|
112
102
|
) ||
|
|
113
|
-
!(
|
|
114
|
-
? publicModel(body)
|
|
115
|
-
: models.some(
|
|
116
|
-
(model) =>
|
|
117
|
-
model.provider === body.provider &&
|
|
118
|
-
model.model === body.model &&
|
|
119
|
-
model.reasoningEfforts.includes(body.model_options.reasoning_effort)
|
|
120
|
-
))
|
|
103
|
+
!publicModel(body)
|
|
121
104
|
)
|
|
122
105
|
throw new Error("claude_runs_request_rejected");
|
|
123
106
|
const contract = body.model_options.sellable_opener_output_contract;
|
|
@@ -141,50 +124,7 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
141
124
|
};
|
|
142
125
|
}
|
|
143
126
|
|
|
144
|
-
|
|
145
|
-
* A run's own allowed tools, read from the signed turn descriptor the runtime
|
|
146
|
-
* installed for its session before submitting. With a tool list, only runs on
|
|
147
|
-
* those endpoints get tools (and must have some); without one, every run gets
|
|
148
|
-
* exactly the tools its descriptor allows, and a run allowed none is tool-free
|
|
149
|
-
* (null). This only narrows: the context proxy verifies the descriptor itself
|
|
150
|
-
* before it lists or calls any tool.
|
|
151
|
-
*/
|
|
152
|
-
export function endpointToolNames({
|
|
153
|
-
turnDescriptorPath,
|
|
154
|
-
sessionId,
|
|
155
|
-
toolEndpointIds = undefined,
|
|
156
|
-
}) {
|
|
157
|
-
const path = endpointMcpDescriptorPath(turnDescriptorPath, sessionId);
|
|
158
|
-
if (!path) return null;
|
|
159
|
-
try {
|
|
160
|
-
if (!lstatSync(path).isFile())
|
|
161
|
-
throw new Error("claude_runs_tool_descriptor_rejected");
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (error.code === "ENOENT") return null;
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
166
|
-
const body = JSON.parse(readFileSync(path, "utf8"))?.body;
|
|
167
|
-
if (toolEndpointIds && !toolEndpointIds.includes(body?.endpointId))
|
|
168
|
-
return null;
|
|
169
|
-
const names = body?.allowedToolNames;
|
|
170
|
-
if (
|
|
171
|
-
endpointHermesSessionId(body) !== sessionId ||
|
|
172
|
-
!Array.isArray(names) ||
|
|
173
|
-
(toolEndpointIds && !names.length) ||
|
|
174
|
-
names.some(
|
|
175
|
-
(name) => typeof name !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(name)
|
|
176
|
-
)
|
|
177
|
-
)
|
|
178
|
-
throw new Error("claude_runs_tool_descriptor_rejected");
|
|
179
|
-
return names.length ? [...names] : null;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function claudeInvocation(
|
|
183
|
-
request,
|
|
184
|
-
root,
|
|
185
|
-
mcpSpecPath = null,
|
|
186
|
-
configDir = `${root}/claude`
|
|
187
|
-
) {
|
|
127
|
+
export function claudeInvocation(request, root, configDir = `${root}/claude`) {
|
|
188
128
|
return {
|
|
189
129
|
command: `${root}/cli/node_modules/.bin/claude`,
|
|
190
130
|
args: [
|
|
@@ -204,22 +144,12 @@ export function claudeInvocation(
|
|
|
204
144
|
"dontAsk",
|
|
205
145
|
"--strict-mcp-config",
|
|
206
146
|
"--mcp-config",
|
|
207
|
-
// A
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// has none.
|
|
147
|
+
// A run's only MCP server is its own remote server, read from its
|
|
148
|
+
// private file, and its allowed tools are exactly the run's. Every other
|
|
149
|
+
// run has none.
|
|
211
150
|
...(request.tools
|
|
212
151
|
? [
|
|
213
|
-
request.
|
|
214
|
-
? request.mcpConfigPath
|
|
215
|
-
: JSON.stringify({
|
|
216
|
-
mcpServers: {
|
|
217
|
-
sellable: {
|
|
218
|
-
command: process.execPath,
|
|
219
|
-
args: [MCP_SESSION, mcpSpecPath],
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
}),
|
|
152
|
+
request.mcpConfigPath,
|
|
223
153
|
"--allowedTools",
|
|
224
154
|
request.tools.names
|
|
225
155
|
.map((name) => `mcp__sellable__${name}`)
|
|
@@ -265,7 +195,6 @@ export function readClaudeAuthentication({
|
|
|
265
195
|
const invocation = claudeInvocation(
|
|
266
196
|
{ instructions: "", input: "", model: "claude-opus", effort: "high" },
|
|
267
197
|
root,
|
|
268
|
-
null,
|
|
269
198
|
configDir
|
|
270
199
|
);
|
|
271
200
|
return new Promise((resolve) => {
|
|
@@ -378,6 +307,31 @@ export function claudeUsageLimit(stdout, now = Date.now()) {
|
|
|
378
307
|
return { retryAt };
|
|
379
308
|
}
|
|
380
309
|
|
|
310
|
+
// Why a CLI run failed, for the audit only: sanitized facts from its JSON
|
|
311
|
+
// result, never its text, result or stderr.
|
|
312
|
+
export function claudeFailureFacts(stdout, exitCode) {
|
|
313
|
+
let value = null;
|
|
314
|
+
try {
|
|
315
|
+
value = JSON.parse(stdout);
|
|
316
|
+
} catch {
|
|
317
|
+
/* No JSON result. */
|
|
318
|
+
}
|
|
319
|
+
const fact = (item, pattern) =>
|
|
320
|
+
typeof item === "string" && pattern.test(item) ? item : null;
|
|
321
|
+
const status = record(value) ? value.api_error_status : null;
|
|
322
|
+
return {
|
|
323
|
+
exitCode: Number.isInteger(exitCode) ? exitCode : null,
|
|
324
|
+
apiErrorStatus:
|
|
325
|
+
Number.isInteger(status) && status >= 100 && status <= 599
|
|
326
|
+
? status
|
|
327
|
+
: null,
|
|
328
|
+
terminalReason: record(value)
|
|
329
|
+
? fact(value.terminal_reason, /^[a-z_]{1,32}$/)
|
|
330
|
+
: null,
|
|
331
|
+
subtype: record(value) ? fact(value.subtype, /^[a-z_]{1,48}$/) : null,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
381
335
|
// A failure reason the run may report: a fixed code, never raw CLI text.
|
|
382
336
|
const RUN_FAILURE_CODE = /^claude_runs_[a-z_]{1,48}$/;
|
|
383
337
|
|
|
@@ -430,28 +384,12 @@ export function startClaudeRun(
|
|
|
430
384
|
timeoutMs = 750_000,
|
|
431
385
|
} = {}
|
|
432
386
|
) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
writeFileSync(
|
|
440
|
-
mcpSpecPath,
|
|
441
|
-
JSON.stringify({ sessionId: request.sessionId, ...request.tools.server }),
|
|
442
|
-
{ mode: 0o600, flag: "wx" }
|
|
443
|
-
);
|
|
444
|
-
const removeSpec = () => {
|
|
445
|
-
if (mcpSpecPath) rmSync(mcpSpecPath, { force: true });
|
|
446
|
-
};
|
|
447
|
-
const invocation = claudeInvocation(request, root, mcpSpecPath, configDir);
|
|
448
|
-
let child;
|
|
449
|
-
try {
|
|
450
|
-
child = spawnImpl(invocation.command, invocation.args, invocation.options);
|
|
451
|
-
} catch (error) {
|
|
452
|
-
removeSpec();
|
|
453
|
-
throw error;
|
|
454
|
-
}
|
|
387
|
+
const invocation = claudeInvocation(request, root, configDir);
|
|
388
|
+
const child = spawnImpl(
|
|
389
|
+
invocation.command,
|
|
390
|
+
invocation.args,
|
|
391
|
+
invocation.options
|
|
392
|
+
);
|
|
455
393
|
let reason = null;
|
|
456
394
|
let killTimer;
|
|
457
395
|
const stop = (code = "claude_runs_cancelled") => {
|
|
@@ -483,28 +421,32 @@ export function startClaudeRun(
|
|
|
483
421
|
child.stderr.on("data", () => {});
|
|
484
422
|
child.once("error", () => {
|
|
485
423
|
clearTimeout(timer);
|
|
486
|
-
removeSpec();
|
|
487
424
|
reject(new Error("claude_runs_cli_unavailable"));
|
|
488
425
|
});
|
|
489
426
|
child.once("close", (code) => {
|
|
490
427
|
clearTimeout(timer);
|
|
491
428
|
clearTimeout(killTimer);
|
|
492
|
-
removeSpec();
|
|
493
429
|
const stdout = Buffer.concat(chunks).toString("utf8");
|
|
494
430
|
const limit = reason ? null : claudeUsageLimit(stdout);
|
|
495
431
|
if (limit)
|
|
496
432
|
return reject(
|
|
497
433
|
Object.assign(new Error("claude_runs_usage_limited"), limit)
|
|
498
434
|
);
|
|
499
|
-
|
|
500
|
-
|
|
435
|
+
const failed = (message) =>
|
|
436
|
+
Object.assign(new Error(message), {
|
|
437
|
+
failure: claudeFailureFacts(stdout, code),
|
|
438
|
+
});
|
|
439
|
+
if (reason) return reject(new Error(reason));
|
|
440
|
+
if (code !== 0) return reject(failed("claude_runs_cli_failed"));
|
|
501
441
|
try {
|
|
502
442
|
resolve(parseClaudeResult(stdout, request));
|
|
503
443
|
} catch (error) {
|
|
504
|
-
|
|
444
|
+
const message =
|
|
505
445
|
error instanceof Error && RUN_FAILURE_CODE.test(error.message)
|
|
506
|
-
? error
|
|
507
|
-
:
|
|
446
|
+
? error.message
|
|
447
|
+
: "claude_runs_output_rejected";
|
|
448
|
+
reject(
|
|
449
|
+
message === "claude_runs_output_rejected" ? failed(message) : error
|
|
508
450
|
);
|
|
509
451
|
}
|
|
510
452
|
});
|
|
@@ -539,20 +481,17 @@ function writeRemoteMcpConfig(dir, remote) {
|
|
|
539
481
|
return path;
|
|
540
482
|
}
|
|
541
483
|
|
|
484
|
+
// The Admin's internet-facing service. The bearer is its only scope, and a
|
|
485
|
+
// run's tools come from its own request.
|
|
542
486
|
export function createClaudeRunsApi({
|
|
543
487
|
token,
|
|
544
|
-
|
|
545
|
-
// "public": the Admin's internet-facing listener. The bearer is its only
|
|
546
|
-
// scope, and a run's tools come from its own request, never a local lane.
|
|
547
|
-
mode = "loopback",
|
|
548
|
-
root = connection?.localRoot,
|
|
488
|
+
root = SERVICE_ROOT,
|
|
549
489
|
execute = null,
|
|
550
490
|
audit = () => {},
|
|
551
491
|
maxConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY,
|
|
552
|
-
//
|
|
492
|
+
// Runs with tools (replies, follow-ups) have their own slots so a batch of
|
|
553
493
|
// openers can never crowd them out.
|
|
554
494
|
maxToolConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY,
|
|
555
|
-
toolLanes = null,
|
|
556
495
|
checkAuthentication = (configDir) =>
|
|
557
496
|
readClaudeAuthentication({ root, configDir }),
|
|
558
497
|
accounts = () => claudeAccountDirs(root),
|
|
@@ -629,55 +568,21 @@ export function createClaudeRunsApi({
|
|
|
629
568
|
};
|
|
630
569
|
};
|
|
631
570
|
const executeRun = execute ?? runOnAccounts;
|
|
632
|
-
const
|
|
571
|
+
const readAuthentication = async () => {
|
|
633
572
|
const dirs = accounts();
|
|
634
573
|
let ready = 0;
|
|
635
574
|
let signedOut = 0;
|
|
636
|
-
const retryAts = [];
|
|
637
575
|
for (const configDir of dirs) {
|
|
638
|
-
if (resting(configDir))
|
|
639
|
-
retryAts.push(usageLimitedUntil.get(configDir));
|
|
640
|
-
continue;
|
|
641
|
-
}
|
|
576
|
+
if (resting(configDir)) continue;
|
|
642
577
|
const state = await signInState(configDir);
|
|
643
578
|
if (state === "READY") ready++;
|
|
644
579
|
if (state === "SIGN_IN_REQUIRED") signedOut++;
|
|
645
580
|
}
|
|
646
|
-
return
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
: "UNAVAILABLE",
|
|
652
|
-
accounts: {
|
|
653
|
-
total: dirs.length,
|
|
654
|
-
ready,
|
|
655
|
-
usageLimited: retryAts.length,
|
|
656
|
-
retryAt: retryAts.length
|
|
657
|
-
? new Date(Math.min(...retryAts)).toISOString()
|
|
658
|
-
: null,
|
|
659
|
-
},
|
|
660
|
-
};
|
|
661
|
-
};
|
|
662
|
-
// The run's own tools and the profile's MCP server, or null for a tool-free run.
|
|
663
|
-
const resolveTools = (request) => {
|
|
664
|
-
if (!toolLanes) return null;
|
|
665
|
-
const names = endpointToolNames({
|
|
666
|
-
turnDescriptorPath: toolLanes.turnDescriptorPath,
|
|
667
|
-
sessionId: request.sessionId,
|
|
668
|
-
toolEndpointIds: connection.toolEndpointIds,
|
|
669
|
-
});
|
|
670
|
-
if (!names) return null;
|
|
671
|
-
const server = toolLanes.server();
|
|
672
|
-
if (!server) throw new Error("claude_runs_tool_server_unavailable");
|
|
673
|
-
return {
|
|
674
|
-
names,
|
|
675
|
-
server: {
|
|
676
|
-
command: server.command,
|
|
677
|
-
args: [...server.args],
|
|
678
|
-
env: { PATH: "/usr/local/bin:/usr/bin:/bin", ...server.env },
|
|
679
|
-
},
|
|
680
|
-
};
|
|
581
|
+
return ready
|
|
582
|
+
? "READY"
|
|
583
|
+
: signedOut === dirs.length
|
|
584
|
+
? "SIGN_IN_REQUIRED"
|
|
585
|
+
: "UNAVAILABLE";
|
|
681
586
|
};
|
|
682
587
|
const recordAudit = (entry) => {
|
|
683
588
|
try {
|
|
@@ -721,6 +626,7 @@ export function createClaudeRunsApi({
|
|
|
721
626
|
usage: run.usage ?? null,
|
|
722
627
|
durationMs: Date.now() - run.startedAt,
|
|
723
628
|
outputDigest: run.output ? hash(run.output) : null,
|
|
629
|
+
...(run.status === "failed" ? { failure: run.failure ?? null } : {}),
|
|
724
630
|
});
|
|
725
631
|
setTimeout(() => runs.delete(run.run_id), 3_600_000).unref();
|
|
726
632
|
};
|
|
@@ -732,37 +638,18 @@ export function createClaudeRunsApi({
|
|
|
732
638
|
!timingSafeEqual(provided, expected)
|
|
733
639
|
)
|
|
734
640
|
return json(res, 401, { error: "claude_runs_auth_required" });
|
|
735
|
-
if (
|
|
736
|
-
mode !== "public" &&
|
|
737
|
-
(req.headers["x-sellable-workspace-id"] !== connection.workspaceId ||
|
|
738
|
-
req.headers["x-sellable-agent-id"] !== connection.agentId)
|
|
739
|
-
)
|
|
740
|
-
return json(res, 403, { error: "claude_runs_scope_rejected" });
|
|
741
641
|
if (req.method === "GET" && req.url === "/health") {
|
|
742
|
-
const
|
|
743
|
-
//
|
|
744
|
-
if (mode === "public")
|
|
745
|
-
return json(res, 200, {
|
|
746
|
-
ok: authentication === "READY",
|
|
747
|
-
authentication,
|
|
748
|
-
provider: "claude-code",
|
|
749
|
-
tools: ["mcp"],
|
|
750
|
-
});
|
|
642
|
+
const authentication = await readAuthentication();
|
|
643
|
+
// Health names no accounts, load, or scope.
|
|
751
644
|
return json(res, 200, {
|
|
752
645
|
ok: authentication === "READY",
|
|
753
646
|
authentication,
|
|
754
|
-
accounts: accountHealth,
|
|
755
647
|
provider: "claude-code",
|
|
756
|
-
|
|
757
|
-
toolActive: inFlight.tools,
|
|
758
|
-
scope: {
|
|
759
|
-
workspaceId: connection.workspaceId,
|
|
760
|
-
agentId: connection.agentId,
|
|
761
|
-
},
|
|
648
|
+
tools: ["mcp"],
|
|
762
649
|
});
|
|
763
650
|
}
|
|
764
651
|
if (req.method === "POST" && req.url === "/v1/runs") {
|
|
765
|
-
if (full("plain") &&
|
|
652
|
+
if (full("plain") && full("tools"))
|
|
766
653
|
return json(res, 429, { error: "claude_runs_busy" });
|
|
767
654
|
let request;
|
|
768
655
|
try {
|
|
@@ -777,12 +664,7 @@ export function createClaudeRunsApi({
|
|
|
777
664
|
chunks.push(chunk);
|
|
778
665
|
}
|
|
779
666
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
780
|
-
request = validateClaudeRunsRequest(
|
|
781
|
-
body,
|
|
782
|
-
mode === "public" ? null : connection.models
|
|
783
|
-
);
|
|
784
|
-
// A public run's tools are its own entry's, never a local lane's.
|
|
785
|
-
if (mode !== "public") request.tools = resolveTools(request);
|
|
667
|
+
request = validateClaudeRunsRequest(body);
|
|
786
668
|
} catch (error) {
|
|
787
669
|
recordAudit({
|
|
788
670
|
status: "request-rejected",
|
|
@@ -805,7 +687,7 @@ export function createClaudeRunsApi({
|
|
|
805
687
|
};
|
|
806
688
|
inFlight[pool]++;
|
|
807
689
|
runs.set(run.run_id, run);
|
|
808
|
-
const runDir = request.tools
|
|
690
|
+
const runDir = request.tools
|
|
809
691
|
? join(root, "work", "runs", run.run_id)
|
|
810
692
|
: null;
|
|
811
693
|
const removeRunDir = () => {
|
|
@@ -840,6 +722,8 @@ export function createClaudeRunsApi({
|
|
|
840
722
|
error: RUN_FAILURE_CODE.test(error?.message ?? "")
|
|
841
723
|
? error.message
|
|
842
724
|
: "claude_runs_generation_failed",
|
|
725
|
+
// Why the CLI failed, for the audit only.
|
|
726
|
+
failure: error?.failure ?? null,
|
|
843
727
|
}
|
|
844
728
|
)
|
|
845
729
|
)
|
|
@@ -942,60 +826,12 @@ async function stopServer(server) {
|
|
|
942
826
|
});
|
|
943
827
|
}
|
|
944
828
|
|
|
945
|
-
// The Admin runtime owns these listeners for its lifetime. Image-owned startup
|
|
946
|
-
// plus the persistent CLI/auth directory makes normal restarts repeatable.
|
|
947
|
-
export async function startConfiguredClaudeRunsApis({
|
|
948
|
-
env = process.env,
|
|
949
|
-
diagnostic = () => {},
|
|
950
|
-
createApi = createClaudeRunsApi,
|
|
951
|
-
readToken = readKey,
|
|
952
|
-
toolLanes = null,
|
|
953
|
-
} = {}) {
|
|
954
|
-
const services = [];
|
|
955
|
-
for (const connection of parseRunsApiConnections(
|
|
956
|
-
env[RUNS_API_CONNECTIONS_ENV]
|
|
957
|
-
)) {
|
|
958
|
-
if (
|
|
959
|
-
!connection.localRoot ||
|
|
960
|
-
connection.runtimeId !== env.SELLABLE_RUNTIME_ID
|
|
961
|
-
)
|
|
962
|
-
continue;
|
|
963
|
-
try {
|
|
964
|
-
const token = readToken(connection.apiKeyFile);
|
|
965
|
-
const server = createApi({
|
|
966
|
-
token,
|
|
967
|
-
connection,
|
|
968
|
-
toolLanes,
|
|
969
|
-
audit: auditTo(connection.localRoot),
|
|
970
|
-
});
|
|
971
|
-
await listen(
|
|
972
|
-
server,
|
|
973
|
-
Number(new URL(connection.baseUrl).port),
|
|
974
|
-
"127.0.0.1"
|
|
975
|
-
);
|
|
976
|
-
services.push(server);
|
|
977
|
-
diagnostic({
|
|
978
|
-
ok: true,
|
|
979
|
-
code: "claude_runs_api_ready",
|
|
980
|
-
baseUrl: connection.baseUrl,
|
|
981
|
-
});
|
|
982
|
-
} catch {
|
|
983
|
-
diagnostic({ ok: false, code: "claude_runs_api_unavailable" });
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
return {
|
|
987
|
-
async stop() {
|
|
988
|
-
for (const server of services) await stopServer(server);
|
|
989
|
-
},
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
|
|
993
829
|
// The Admin's internet-facing listener; Fly terminates TLS at the edge. It
|
|
994
830
|
// starts only on a declared port with the machine's private connection key.
|
|
995
831
|
export async function startPublicClaudeRunsApi({
|
|
996
832
|
env = process.env,
|
|
997
833
|
diagnostic = () => {},
|
|
998
|
-
root =
|
|
834
|
+
root = SERVICE_ROOT,
|
|
999
835
|
createApi = createClaudeRunsApi,
|
|
1000
836
|
readToken = readKey,
|
|
1001
837
|
} = {}) {
|
|
@@ -1018,7 +854,6 @@ export async function startPublicClaudeRunsApi({
|
|
|
1018
854
|
)
|
|
1019
855
|
return null;
|
|
1020
856
|
const server = createApi({
|
|
1021
|
-
mode: "public",
|
|
1022
857
|
token: readToken(keyFile),
|
|
1023
858
|
root,
|
|
1024
859
|
audit: auditTo(root),
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
obtainFlyOidcToken,
|
|
45
45
|
renewFlyCustomerControlSession,
|
|
46
46
|
signedWorkerAuthorization,
|
|
47
|
+
WorkerControlResponseError,
|
|
47
48
|
} from "../fly-runtime-identity.mjs";
|
|
48
49
|
import {
|
|
49
50
|
executeExternalSoulBridgeOperation,
|
|
@@ -131,9 +132,9 @@ const RUNTIME_GENERATION = Number(
|
|
|
131
132
|
const BOOT_SESSION_ID = randomUUID();
|
|
132
133
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.5";
|
|
133
134
|
const INSTALLER_PACKAGE =
|
|
134
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
135
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.813";
|
|
135
136
|
const MCP_PACKAGE =
|
|
136
|
-
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.
|
|
137
|
+
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.959";
|
|
137
138
|
export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
|
|
138
139
|
const DEFAULT_PROFILE_BUNDLE_ROOT =
|
|
139
140
|
"/usr/local/share/sellable-agent/default-profile-bundle";
|
|
@@ -1181,6 +1182,29 @@ function endpointCompletionBody({
|
|
|
1181
1182
|
};
|
|
1182
1183
|
}
|
|
1183
1184
|
|
|
1185
|
+
// Completion refusals the server returns before it records anything.
|
|
1186
|
+
const ENDPOINT_COMPLETION_REFUSALS = new Map([
|
|
1187
|
+
["product_completion_contract_invalid", "server_contract_invalid"],
|
|
1188
|
+
["completion_output_invalid", "server_contract_invalid"],
|
|
1189
|
+
["endpoint_worker_secret_material", "server_secret_material"],
|
|
1190
|
+
]);
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Why the server refused a product draft's answer, or null when the outcome is
|
|
1194
|
+
* unknown (lease, fence, 5xx, network). Matched on the code: a stale fence is
|
|
1195
|
+
* also a 409.
|
|
1196
|
+
*/
|
|
1197
|
+
function endpointCompletionRefusal(claim, error) {
|
|
1198
|
+
if (
|
|
1199
|
+
claim?.run?.productExecution == null ||
|
|
1200
|
+
!(error instanceof WorkerControlResponseError) ||
|
|
1201
|
+
error.retryable !== false ||
|
|
1202
|
+
![409, 413, 422].includes(error.status)
|
|
1203
|
+
)
|
|
1204
|
+
return null;
|
|
1205
|
+
return ENDPOINT_COMPLETION_REFUSALS.get(error.message) ?? null;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1184
1208
|
/**
|
|
1185
1209
|
* Why endpointProductProjection refused an answer, as one short code for the
|
|
1186
1210
|
* run's events: the answer is never stored, so this is all that explains it.
|
|
@@ -2363,33 +2387,42 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2363
2387
|
// a second completion or expose the late output as canonical.
|
|
2364
2388
|
return done({ ok: true, code: "LATE_NOTICE_ONLY" });
|
|
2365
2389
|
}
|
|
2390
|
+
// The output was never used, so another model may still write.
|
|
2391
|
+
const refused = (reason) => ({
|
|
2392
|
+
failure: "OUTPUT_CONTRACT_FAILED",
|
|
2393
|
+
reason,
|
|
2394
|
+
finish: async () => {
|
|
2395
|
+
await complete(claim, identity, {
|
|
2396
|
+
attemptStatus: "SUCCEEDED",
|
|
2397
|
+
status: "OUTPUT_CONTRACT_FAILED",
|
|
2398
|
+
});
|
|
2399
|
+
return { ok: false, code: "OUTPUT_CONTRACT_FAILED" };
|
|
2400
|
+
},
|
|
2401
|
+
});
|
|
2366
2402
|
const productProjection = endpointProductProjection(
|
|
2367
2403
|
claim.run.productExecution,
|
|
2368
2404
|
terminalEvent.output
|
|
2369
2405
|
);
|
|
2370
2406
|
if (!productProjection.ok) {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
failure: "OUTPUT_CONTRACT_FAILED",
|
|
2374
|
-
reason: endpointProductProjectionReason(
|
|
2407
|
+
return refused(
|
|
2408
|
+
endpointProductProjectionReason(
|
|
2375
2409
|
claim.run.productExecution,
|
|
2376
2410
|
terminalEvent.output
|
|
2377
|
-
)
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2411
|
+
)
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2414
|
+
try {
|
|
2415
|
+
await complete(claim, identity, {
|
|
2416
|
+
attemptStatus: "SUCCEEDED",
|
|
2417
|
+
status: "SUCCEEDED",
|
|
2418
|
+
output: terminalEvent.output,
|
|
2419
|
+
...productProjection.completion,
|
|
2420
|
+
});
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
const reason = endpointCompletionRefusal(claim, error);
|
|
2423
|
+
if (!reason) throw error;
|
|
2424
|
+
return refused(reason);
|
|
2386
2425
|
}
|
|
2387
|
-
await complete(claim, identity, {
|
|
2388
|
-
attemptStatus: "SUCCEEDED",
|
|
2389
|
-
status: "SUCCEEDED",
|
|
2390
|
-
output: terminalEvent.output,
|
|
2391
|
-
...productProjection.completion,
|
|
2392
|
-
});
|
|
2393
2426
|
return done({ ok: true, code: "SUCCEEDED" });
|
|
2394
2427
|
}
|
|
2395
2428
|
if (
|
|
@@ -2556,7 +2589,22 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2556
2589
|
outcome.failure,
|
|
2557
2590
|
deadlineMs - now().getTime()
|
|
2558
2591
|
);
|
|
2559
|
-
if (!decision.advance)
|
|
2592
|
+
if (!decision.advance) {
|
|
2593
|
+
// A refused answer that ends the run says why, once.
|
|
2594
|
+
if (outcome.reason)
|
|
2595
|
+
await append(
|
|
2596
|
+
claim,
|
|
2597
|
+
identity,
|
|
2598
|
+
{
|
|
2599
|
+
kind: "OUTPUT_REFUSED",
|
|
2600
|
+
classification: outcome.failure,
|
|
2601
|
+
chainStep: stepIndex,
|
|
2602
|
+
reason: outcome.reason,
|
|
2603
|
+
},
|
|
2604
|
+
ordinal++
|
|
2605
|
+
);
|
|
2606
|
+
return await outcome.finish();
|
|
2607
|
+
}
|
|
2560
2608
|
await append(
|
|
2561
2609
|
claim,
|
|
2562
2610
|
identity,
|
|
@@ -19,6 +19,7 @@ export type EndpointModelStep = Readonly<{
|
|
|
19
19
|
provider: string;
|
|
20
20
|
model: string;
|
|
21
21
|
reasoningEffort: (typeof ENDPOINT_MODEL_REASONING_EFFORTS)[number];
|
|
22
|
+
/** Both or neither: a custom Runs URL is served by a workspace connection. */
|
|
22
23
|
runsApiBaseUrl?: string;
|
|
23
24
|
modelConnectionId?: string;
|
|
24
25
|
}>;
|