@sellable/install 0.1.803 → 0.1.805
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/agents/post-find-leads-message-scout.md +4 -4
- package/agents/registry.json +1 -1
- package/bin/sellable-install.mjs +36 -20
- package/container/Dockerfile +4 -4
- package/container/README.md +1 -1
- package/lib/sellable-agent/default-profile-bundles/shared/manifest.fragment.json +1 -1
- package/lib/sellable-agent/default-profile-bundles/shared/skills/sellable/sellable-create-campaign/SKILL.md +3 -3
- package/lib/sellable-agent/fly-admin-image/Dockerfile +4 -2
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +19 -3
- package/lib/sellable-agent/fly-customer-image/Dockerfile +3 -3
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.d.mts +23 -3
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.mjs +197 -52
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +48 -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 +21 -4
- package/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs +61 -5
- package/lib/sellable-agent/fly-customer-image/openai-runs-client.d.mts +3 -1
- package/lib/sellable-agent/fly-customer-image/openai-runs-client.mjs +4 -1
- package/lib/sellable-agent/fly-customer-image/public-endpoint-guard.d.mts +28 -0
- package/lib/sellable-agent/fly-customer-image/public-endpoint-guard.mjs +209 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.d.mts +1 -1
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.mjs +97 -24
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.mjs +11 -42
- package/lib/sellable-agent/fly-customer-image/workspace-connection-client.d.mts +22 -0
- package/lib/sellable-agent/fly-customer-image/workspace-connection-client.mjs +98 -0
- package/lib/sellable-agent/fly-runtime-identity.mjs +8 -0
- 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/lib/sellable-agent/public-skills.json +4 -4
- package/lib/sellable-agent/public-skills.mjs +35 -0
- package/package.json +1 -1
- package/skill-templates/create-campaign.md +3 -3
- package/skill-templates/{conversation-chess.md → draft-message.md} +7 -6
- package/skill-templates/first-campaign-onboarding.md +8 -7
|
@@ -61,9 +61,12 @@ import {
|
|
|
61
61
|
SELLABLE_BROWSER_TOOLSET_DEFAULT_MARKER,
|
|
62
62
|
sellableSlackToolsetsForMaterialization,
|
|
63
63
|
} from "../fly-capabilities-bridge.mjs";
|
|
64
|
+
import {
|
|
65
|
+
startConfiguredClaudeRunsApis,
|
|
66
|
+
startPublicClaudeRunsApi,
|
|
67
|
+
} from "../fly-customer-image/claude-code-runs-api.mjs";
|
|
64
68
|
import { createCustomerEndpointRelay } from "../fly-customer-image/customer-runtime.mjs";
|
|
65
69
|
import { createHermesSessionRetention } from "../fly-customer-image/hermes-runs-client.mjs";
|
|
66
|
-
import { startConfiguredClaudeRunsApis } from "../fly-customer-image/claude-code-runs-api.mjs";
|
|
67
70
|
import {
|
|
68
71
|
claimWithSoulObservation,
|
|
69
72
|
createFlyCustomerControlClient,
|
|
@@ -3847,7 +3850,7 @@ async function runAdminProduction() {
|
|
|
3847
3850
|
let customRunsServices = null;
|
|
3848
3851
|
try {
|
|
3849
3852
|
customRunsServices = await startConfiguredClaudeRunsApis({
|
|
3850
|
-
diagnostic: value => process.stderr.write(`${JSON.stringify(value)}\n`),
|
|
3853
|
+
diagnostic: (value) => process.stderr.write(`${JSON.stringify(value)}\n`),
|
|
3851
3854
|
toolLanes: {
|
|
3852
3855
|
turnDescriptorPath: join(MCP_TURN_CONTEXT_ROOT, "turn-context.json"),
|
|
3853
3856
|
server: () =>
|
|
@@ -3857,7 +3860,19 @@ async function runAdminProduction() {
|
|
|
3857
3860
|
},
|
|
3858
3861
|
});
|
|
3859
3862
|
} catch {
|
|
3860
|
-
process.stderr.write(
|
|
3863
|
+
process.stderr.write(
|
|
3864
|
+
`${JSON.stringify({ ok: false, code: "custom_runs_api_start_failed" })}\n`
|
|
3865
|
+
);
|
|
3866
|
+
}
|
|
3867
|
+
let publicRunsService = null;
|
|
3868
|
+
try {
|
|
3869
|
+
publicRunsService = await startPublicClaudeRunsApi({
|
|
3870
|
+
diagnostic: (value) => process.stderr.write(`${JSON.stringify(value)}\n`),
|
|
3871
|
+
});
|
|
3872
|
+
} catch {
|
|
3873
|
+
process.stderr.write(
|
|
3874
|
+
`${JSON.stringify({ ok: false, code: "public_runs_api_start_failed" })}\n`
|
|
3875
|
+
);
|
|
3861
3876
|
}
|
|
3862
3877
|
const endpointRelay = createCustomerEndpointRelay({
|
|
3863
3878
|
getControlClient: () => client,
|
|
@@ -4496,6 +4511,7 @@ async function runAdminProduction() {
|
|
|
4496
4511
|
}
|
|
4497
4512
|
await endpointLoop;
|
|
4498
4513
|
await customRunsServices?.stop();
|
|
4514
|
+
await publicRunsService?.stop();
|
|
4499
4515
|
await activeModelHealthReporter?.stop();
|
|
4500
4516
|
await stopNativeGateway(activeGateway);
|
|
4501
4517
|
await clearRuntimeSecrets();
|
|
@@ -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.805
|
|
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
|
|
@@ -22,7 +22,7 @@ ARG PLAYWRIGHT_BROWSER_REVISION=1243
|
|
|
22
22
|
ARG TIRITH_ARCHIVE_SHA256=6cdbe35e8f9ccf42e70ad95b501c93cd218ac18201c3df958d54f6ba0d995ce2
|
|
23
23
|
ARG DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION=1
|
|
24
24
|
ARG DEFAULT_PROFILE_BUNDLE_VERSION=4
|
|
25
|
-
ARG DEFAULT_PROFILE_BUNDLE_DIGEST=
|
|
25
|
+
ARG DEFAULT_PROFILE_BUNDLE_DIGEST=fe00b8da3617af34c4edef6b0f40b4f48b45a1da11332137fad400deefd92352
|
|
26
26
|
|
|
27
27
|
RUN --mount=type=secret,id=mcp_trust_root,required=true \
|
|
28
28
|
test -s /run/secrets/mcp_trust_root \
|
|
@@ -98,7 +98,7 @@ COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
|
|
|
98
98
|
COPY hermes-endpoint-cron.mjs /opt/sellable/bin/hermes-endpoint-cron
|
|
99
99
|
COPY fly-customer-image/model-auth-store.mjs /usr/local/lib/sellable-agent/fly-customer-image/model-auth-store.mjs
|
|
100
100
|
COPY fly-customer-image/hermes-runs-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs
|
|
101
|
-
COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs /usr/local/lib/sellable-agent/fly-customer-image/
|
|
101
|
+
COPY fly-customer-image/endpoint-model-selection.mjs fly-customer-image/runs-api-connections.mjs fly-customer-image/openai-runs-client.mjs fly-customer-image/runs-api-health-check.mjs fly-customer-image/public-endpoint-guard.mjs fly-customer-image/workspace-connection-client.mjs /usr/local/lib/sellable-agent/fly-customer-image/
|
|
102
102
|
COPY fly-customer-image/opener-stage-execution.mjs /usr/local/lib/sellable-agent/fly-customer-image/opener-stage-execution.mjs
|
|
103
103
|
COPY fly-customer-image/endpoint-uat-completion-gate.mjs /usr/local/lib/sellable-agent/fly-customer-image/endpoint-uat-completion-gate.mjs
|
|
104
104
|
COPY fly-customer-image/customer-runtime-phase-telemetry.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime-phase-telemetry.mjs
|
|
@@ -10,7 +10,9 @@ export type ClaudeRunsRequest = {
|
|
|
10
10
|
model: string;
|
|
11
11
|
effort: string;
|
|
12
12
|
sessionId: string;
|
|
13
|
-
tools: ClaudeRunsTools | null;
|
|
13
|
+
tools: ClaudeRunsTools | ClaudeRunsRemoteTools | null;
|
|
14
|
+
/** Remote tools: the run's private MCP config file, written by the service. */
|
|
15
|
+
mcpConfigPath?: string;
|
|
14
16
|
};
|
|
15
17
|
export type ClaudeRunsToolServer = {
|
|
16
18
|
command: string;
|
|
@@ -18,6 +20,11 @@ export type ClaudeRunsToolServer = {
|
|
|
18
20
|
env: Record<string, string>;
|
|
19
21
|
};
|
|
20
22
|
export type ClaudeRunsTools = { names: string[]; server: ClaudeRunsToolServer };
|
|
23
|
+
/** The run's own remote MCP server and grant (public mode). */
|
|
24
|
+
export type ClaudeRunsRemoteTools = {
|
|
25
|
+
names: string[];
|
|
26
|
+
remote: { url: string; authorization: string };
|
|
27
|
+
};
|
|
21
28
|
export type ClaudeRunsToolLanes = {
|
|
22
29
|
turnDescriptorPath: string;
|
|
23
30
|
server(): ClaudeRunsToolServer | null;
|
|
@@ -30,7 +37,11 @@ export type ClaudeRunsResult = {
|
|
|
30
37
|
export type ClaudeRunsServer = Server & { stopActiveRuns(): Promise<void> };
|
|
31
38
|
export type ClaudeRunsApiOptions = {
|
|
32
39
|
token: string;
|
|
33
|
-
|
|
40
|
+
/** Required in loopback mode; the public listener has none. */
|
|
41
|
+
connection?: RunsApiConnection;
|
|
42
|
+
mode?: "loopback" | "public";
|
|
43
|
+
/** The CLI and account root; defaults to the connection's localRoot. */
|
|
44
|
+
root?: string;
|
|
34
45
|
execute?:
|
|
35
46
|
| ((request: ClaudeRunsRequest) => {
|
|
36
47
|
promise: Promise<ClaudeRunsResult>;
|
|
@@ -53,7 +64,8 @@ export type ClaudeRunsApiOptions = {
|
|
|
53
64
|
};
|
|
54
65
|
export function validateClaudeRunsRequest(
|
|
55
66
|
body: unknown,
|
|
56
|
-
|
|
67
|
+
/** null: the public rule (claude-* model, low/medium/high effort). */
|
|
68
|
+
models: RunsApiConnection["models"] | null
|
|
57
69
|
): ClaudeRunsRequest;
|
|
58
70
|
export function endpointToolNames(options: {
|
|
59
71
|
turnDescriptorPath: string;
|
|
@@ -96,6 +108,14 @@ export function startConfiguredClaudeRunsApis(options?: {
|
|
|
96
108
|
toolLanes?: ClaudeRunsToolLanes | null;
|
|
97
109
|
}): Promise<{ stop(): Promise<void> }>;
|
|
98
110
|
|
|
111
|
+
export function startPublicClaudeRunsApi(options?: {
|
|
112
|
+
env?: Record<string, string | undefined>;
|
|
113
|
+
diagnostic?: (entry: Record<string, unknown>) => void;
|
|
114
|
+
root?: string;
|
|
115
|
+
createApi?: typeof createClaudeRunsApi;
|
|
116
|
+
readToken?: (path: string) => string;
|
|
117
|
+
}): Promise<{ stop(): Promise<void> } | null>;
|
|
118
|
+
|
|
99
119
|
export function readClaudeAuthentication(options: {
|
|
100
120
|
root: string;
|
|
101
121
|
configDir?: string;
|
|
@@ -2,8 +2,10 @@ import { execFile, spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
appendFileSync,
|
|
5
|
+
chmodSync,
|
|
5
6
|
existsSync,
|
|
6
7
|
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
7
9
|
readdirSync,
|
|
8
10
|
readFileSync,
|
|
9
11
|
rmSync,
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
endpointHermesSessionId,
|
|
17
19
|
endpointMcpDescriptorPath,
|
|
18
20
|
} from "../mcp-context-proxy.mjs";
|
|
21
|
+
import { normalizeRunsApiBaseUrl } from "./endpoint-model-selection.mjs";
|
|
19
22
|
import { validateOpenerOutputContract } from "./hermes-runs-client.mjs";
|
|
20
23
|
import {
|
|
21
24
|
parseRunsApiConnections,
|
|
@@ -33,6 +36,42 @@ const MCP_SESSION = fileURLToPath(
|
|
|
33
36
|
);
|
|
34
37
|
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
35
38
|
const record = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
39
|
+
// The public service (models null) has no model list: any Claude model, at
|
|
40
|
+
// the efforts the CLI takes.
|
|
41
|
+
const publicModel = (body) =>
|
|
42
|
+
typeof body.model === "string" &&
|
|
43
|
+
/^claude-[a-z0-9-]{1,100}$/.test(body.model) &&
|
|
44
|
+
["low", "medium", "high"].includes(body.model_options.reasoning_effort);
|
|
45
|
+
|
|
46
|
+
// The run's one remote MCP server, in the shape the Runs client sends it.
|
|
47
|
+
function remoteTools(tools) {
|
|
48
|
+
const entry = Array.isArray(tools) && tools.length === 1 ? tools[0] : null;
|
|
49
|
+
const url = normalizeRunsApiBaseUrl(entry?.server_url);
|
|
50
|
+
const names = entry?.allowed_tools;
|
|
51
|
+
if (
|
|
52
|
+
!record(entry) ||
|
|
53
|
+
Object.keys(entry).sort().join(",") !==
|
|
54
|
+
"allowed_tools,authorization,require_approval,server_label,server_url,type" ||
|
|
55
|
+
entry.type !== "mcp" ||
|
|
56
|
+
entry.server_label !== "sellable" ||
|
|
57
|
+
entry.require_approval !== "never" ||
|
|
58
|
+
!url?.startsWith("https://") ||
|
|
59
|
+
typeof entry.authorization !== "string" ||
|
|
60
|
+
!/^srt_[A-Za-z0-9-]{1,128}_[A-Za-z0-9_-]{43}$/.test(entry.authorization) ||
|
|
61
|
+
!Array.isArray(names) ||
|
|
62
|
+
names.length < 1 ||
|
|
63
|
+
names.length > 32 ||
|
|
64
|
+
new Set(names).size !== names.length ||
|
|
65
|
+
names.some(
|
|
66
|
+
(name) => typeof name !== "string" || !/^[a-z][a-z0-9_]{0,63}$/.test(name)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
throw new Error("claude_runs_tools_rejected");
|
|
70
|
+
return {
|
|
71
|
+
names: [...names],
|
|
72
|
+
remote: { url, authorization: entry.authorization },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
36
75
|
|
|
37
76
|
export function validateClaudeRunsRequest(body, models) {
|
|
38
77
|
const allowed = [
|
|
@@ -43,6 +82,7 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
43
82
|
"provider",
|
|
44
83
|
"model",
|
|
45
84
|
"model_options",
|
|
85
|
+
...(models === null ? ["tools"] : []),
|
|
46
86
|
];
|
|
47
87
|
if (
|
|
48
88
|
!record(body) ||
|
|
@@ -70,12 +110,14 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
70
110
|
(key) =>
|
|
71
111
|
!["reasoning_effort", "sellable_opener_output_contract"].includes(key)
|
|
72
112
|
) ||
|
|
73
|
-
!models
|
|
74
|
-
(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
!(models === null
|
|
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
|
+
))
|
|
79
121
|
)
|
|
80
122
|
throw new Error("claude_runs_request_rejected");
|
|
81
123
|
const contract = body.model_options.sellable_opener_output_contract;
|
|
@@ -95,7 +137,7 @@ export function validateClaudeRunsRequest(body, models) {
|
|
|
95
137
|
model: body.model,
|
|
96
138
|
effort: body.model_options.reasoning_effort,
|
|
97
139
|
sessionId: body.session_id,
|
|
98
|
-
tools: null,
|
|
140
|
+
tools: body.tools === undefined ? null : remoteTools(body.tools),
|
|
99
141
|
};
|
|
100
142
|
}
|
|
101
143
|
|
|
@@ -162,18 +204,22 @@ export function claudeInvocation(
|
|
|
162
204
|
"dontAsk",
|
|
163
205
|
"--strict-mcp-config",
|
|
164
206
|
"--mcp-config",
|
|
165
|
-
// A tool lane's only MCP server is the run's own session wrapper
|
|
166
|
-
//
|
|
207
|
+
// A tool lane's only MCP server is the run's own session wrapper, and a
|
|
208
|
+
// public run's is its own remote server, read from its private file.
|
|
209
|
+
// Either way its allowed tools are exactly the run's. Every other run
|
|
210
|
+
// has none.
|
|
167
211
|
...(request.tools
|
|
168
212
|
? [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
213
|
+
request.tools.remote
|
|
214
|
+
? request.mcpConfigPath
|
|
215
|
+
: JSON.stringify({
|
|
216
|
+
mcpServers: {
|
|
217
|
+
sellable: {
|
|
218
|
+
command: process.execPath,
|
|
219
|
+
args: [MCP_SESSION, mcpSpecPath],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
177
223
|
"--allowedTools",
|
|
178
224
|
request.tools.names
|
|
179
225
|
.map((name) => `mcp__sellable__${name}`)
|
|
@@ -386,7 +432,7 @@ export function startClaudeRun(
|
|
|
386
432
|
) {
|
|
387
433
|
// The tool server's environment travels in a private per-run file, never
|
|
388
434
|
// in argv, and is removed when the run ends.
|
|
389
|
-
const mcpSpecPath = request.tools
|
|
435
|
+
const mcpSpecPath = request.tools?.server
|
|
390
436
|
? join(root, "work", `mcp-session-${randomBytes(12).toString("hex")}.json`)
|
|
391
437
|
: null;
|
|
392
438
|
if (mcpSpecPath)
|
|
@@ -468,9 +514,38 @@ export function startClaudeRun(
|
|
|
468
514
|
return { promise, stop };
|
|
469
515
|
}
|
|
470
516
|
|
|
517
|
+
// A public run's remote server and grant, in a 0600 file under a 0700
|
|
518
|
+
// directory of its own: never in argv or the CLI's environment.
|
|
519
|
+
function writeRemoteMcpConfig(dir, remote) {
|
|
520
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
521
|
+
chmodSync(dir, 0o700);
|
|
522
|
+
const stat = lstatSync(dir);
|
|
523
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || (stat.mode & 0o077) !== 0)
|
|
524
|
+
throw new Error("claude_runs_run_directory_rejected");
|
|
525
|
+
const path = join(dir, "mcp.json");
|
|
526
|
+
writeFileSync(
|
|
527
|
+
path,
|
|
528
|
+
JSON.stringify({
|
|
529
|
+
mcpServers: {
|
|
530
|
+
sellable: {
|
|
531
|
+
type: "http",
|
|
532
|
+
url: remote.url,
|
|
533
|
+
headers: { Authorization: `Bearer ${remote.authorization}` },
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
}),
|
|
537
|
+
{ mode: 0o600, flag: "wx" }
|
|
538
|
+
);
|
|
539
|
+
return path;
|
|
540
|
+
}
|
|
541
|
+
|
|
471
542
|
export function createClaudeRunsApi({
|
|
472
543
|
token,
|
|
473
544
|
connection,
|
|
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,
|
|
474
549
|
execute = null,
|
|
475
550
|
audit = () => {},
|
|
476
551
|
maxConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY,
|
|
@@ -479,8 +554,8 @@ export function createClaudeRunsApi({
|
|
|
479
554
|
maxToolConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY,
|
|
480
555
|
toolLanes = null,
|
|
481
556
|
checkAuthentication = (configDir) =>
|
|
482
|
-
readClaudeAuthentication({ root
|
|
483
|
-
accounts = () => claudeAccountDirs(
|
|
557
|
+
readClaudeAuthentication({ root, configDir }),
|
|
558
|
+
accounts = () => claudeAccountDirs(root),
|
|
484
559
|
startRun = startClaudeRun,
|
|
485
560
|
now = Date.now,
|
|
486
561
|
} = {}) {
|
|
@@ -533,10 +608,7 @@ export function createClaudeRunsApi({
|
|
|
533
608
|
failure ??= "claude_runs_sign_in_required";
|
|
534
609
|
continue;
|
|
535
610
|
}
|
|
536
|
-
activeRun = startRun(request, {
|
|
537
|
-
root: connection.localRoot,
|
|
538
|
-
configDir,
|
|
539
|
-
});
|
|
611
|
+
activeRun = startRun(request, { root, configDir });
|
|
540
612
|
try {
|
|
541
613
|
return await activeRun.promise;
|
|
542
614
|
} catch (error) {
|
|
@@ -661,12 +733,21 @@ export function createClaudeRunsApi({
|
|
|
661
733
|
)
|
|
662
734
|
return json(res, 401, { error: "claude_runs_auth_required" });
|
|
663
735
|
if (
|
|
664
|
-
|
|
665
|
-
req.headers["x-sellable-
|
|
736
|
+
mode !== "public" &&
|
|
737
|
+
(req.headers["x-sellable-workspace-id"] !== connection.workspaceId ||
|
|
738
|
+
req.headers["x-sellable-agent-id"] !== connection.agentId)
|
|
666
739
|
)
|
|
667
740
|
return json(res, 403, { error: "claude_runs_scope_rejected" });
|
|
668
741
|
if (req.method === "GET" && req.url === "/health") {
|
|
669
742
|
const { authentication, accounts: accountHealth } = await readHealth();
|
|
743
|
+
// Public health names no accounts, load, or scope.
|
|
744
|
+
if (mode === "public")
|
|
745
|
+
return json(res, 200, {
|
|
746
|
+
ok: authentication === "READY",
|
|
747
|
+
authentication,
|
|
748
|
+
provider: "claude-code",
|
|
749
|
+
tools: ["mcp"],
|
|
750
|
+
});
|
|
670
751
|
return json(res, 200, {
|
|
671
752
|
ok: authentication === "READY",
|
|
672
753
|
authentication,
|
|
@@ -681,7 +762,7 @@ export function createClaudeRunsApi({
|
|
|
681
762
|
});
|
|
682
763
|
}
|
|
683
764
|
if (req.method === "POST" && req.url === "/v1/runs") {
|
|
684
|
-
if (full("plain") && (!toolLanes || full("tools")))
|
|
765
|
+
if (full("plain") && ((mode !== "public" && !toolLanes) || full("tools")))
|
|
685
766
|
return json(res, 429, { error: "claude_runs_busy" });
|
|
686
767
|
let request;
|
|
687
768
|
try {
|
|
@@ -696,8 +777,12 @@ export function createClaudeRunsApi({
|
|
|
696
777
|
chunks.push(chunk);
|
|
697
778
|
}
|
|
698
779
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
699
|
-
request = validateClaudeRunsRequest(
|
|
700
|
-
|
|
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);
|
|
701
786
|
} catch (error) {
|
|
702
787
|
recordAudit({
|
|
703
788
|
status: "request-rejected",
|
|
@@ -720,7 +805,18 @@ export function createClaudeRunsApi({
|
|
|
720
805
|
};
|
|
721
806
|
inFlight[pool]++;
|
|
722
807
|
runs.set(run.run_id, run);
|
|
808
|
+
const runDir = request.tools?.remote
|
|
809
|
+
? join(root, "work", "runs", run.run_id)
|
|
810
|
+
: null;
|
|
811
|
+
const removeRunDir = () => {
|
|
812
|
+
if (runDir) rmSync(runDir, { recursive: true, force: true });
|
|
813
|
+
};
|
|
723
814
|
try {
|
|
815
|
+
if (runDir)
|
|
816
|
+
request.mcpConfigPath = writeRemoteMcpConfig(
|
|
817
|
+
runDir,
|
|
818
|
+
request.tools.remote
|
|
819
|
+
);
|
|
724
820
|
run.execution = executeRun(request);
|
|
725
821
|
run.execution.promise
|
|
726
822
|
.then(
|
|
@@ -747,9 +843,13 @@ export function createClaudeRunsApi({
|
|
|
747
843
|
}
|
|
748
844
|
)
|
|
749
845
|
)
|
|
750
|
-
.finally(() =>
|
|
846
|
+
.finally(() => {
|
|
847
|
+
inFlight[pool]--;
|
|
848
|
+
removeRunDir();
|
|
849
|
+
});
|
|
751
850
|
} catch {
|
|
752
851
|
inFlight[pool]--;
|
|
852
|
+
removeRunDir();
|
|
753
853
|
terminal(run, "run.failed", {
|
|
754
854
|
status: "failed",
|
|
755
855
|
error: "claude_runs_cli_unavailable",
|
|
@@ -821,13 +921,34 @@ export function createClaudeRunsApi({
|
|
|
821
921
|
return server;
|
|
822
922
|
}
|
|
823
923
|
|
|
924
|
+
const readKey = (path) => readFileSync(path, "utf8").trim();
|
|
925
|
+
// Every listener audits to its own root and stops its runs before closing.
|
|
926
|
+
const auditTo = (root) => (entry) =>
|
|
927
|
+
appendFileSync(
|
|
928
|
+
`${root}/audit.jsonl`,
|
|
929
|
+
`${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`,
|
|
930
|
+
{ mode: 0o600 }
|
|
931
|
+
);
|
|
932
|
+
const listen = (server, port, host) =>
|
|
933
|
+
new Promise((resolve, reject) => {
|
|
934
|
+
server.once("error", reject);
|
|
935
|
+
server.listen(port, host, resolve);
|
|
936
|
+
});
|
|
937
|
+
async function stopServer(server) {
|
|
938
|
+
await server.stopActiveRuns();
|
|
939
|
+
await new Promise((resolve) => {
|
|
940
|
+
server.close(resolve);
|
|
941
|
+
server.closeAllConnections();
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
824
945
|
// The Admin runtime owns these listeners for its lifetime. Image-owned startup
|
|
825
946
|
// plus the persistent CLI/auth directory makes normal restarts repeatable.
|
|
826
947
|
export async function startConfiguredClaudeRunsApis({
|
|
827
948
|
env = process.env,
|
|
828
949
|
diagnostic = () => {},
|
|
829
950
|
createApi = createClaudeRunsApi,
|
|
830
|
-
readToken =
|
|
951
|
+
readToken = readKey,
|
|
831
952
|
toolLanes = null,
|
|
832
953
|
} = {}) {
|
|
833
954
|
const services = [];
|
|
@@ -845,21 +966,13 @@ export async function startConfiguredClaudeRunsApis({
|
|
|
845
966
|
token,
|
|
846
967
|
connection,
|
|
847
968
|
toolLanes,
|
|
848
|
-
audit: (
|
|
849
|
-
appendFileSync(
|
|
850
|
-
`${connection.localRoot}/audit.jsonl`,
|
|
851
|
-
`${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`,
|
|
852
|
-
{ mode: 0o600 }
|
|
853
|
-
),
|
|
854
|
-
});
|
|
855
|
-
await new Promise((resolve, reject) => {
|
|
856
|
-
server.once("error", reject);
|
|
857
|
-
server.listen(
|
|
858
|
-
Number(new URL(connection.baseUrl).port),
|
|
859
|
-
"127.0.0.1",
|
|
860
|
-
resolve
|
|
861
|
-
);
|
|
969
|
+
audit: auditTo(connection.localRoot),
|
|
862
970
|
});
|
|
971
|
+
await listen(
|
|
972
|
+
server,
|
|
973
|
+
Number(new URL(connection.baseUrl).port),
|
|
974
|
+
"127.0.0.1"
|
|
975
|
+
);
|
|
863
976
|
services.push(server);
|
|
864
977
|
diagnostic({
|
|
865
978
|
ok: true,
|
|
@@ -872,13 +985,45 @@ export async function startConfiguredClaudeRunsApis({
|
|
|
872
985
|
}
|
|
873
986
|
return {
|
|
874
987
|
async stop() {
|
|
875
|
-
for (const server of services)
|
|
876
|
-
await server.stopActiveRuns();
|
|
877
|
-
await new Promise((resolve) => {
|
|
878
|
-
server.close(resolve);
|
|
879
|
-
server.closeAllConnections();
|
|
880
|
-
});
|
|
881
|
-
}
|
|
988
|
+
for (const server of services) await stopServer(server);
|
|
882
989
|
},
|
|
883
990
|
};
|
|
884
991
|
}
|
|
992
|
+
|
|
993
|
+
// The Admin's internet-facing listener; Fly terminates TLS at the edge. It
|
|
994
|
+
// starts only on a declared port with the machine's private connection key.
|
|
995
|
+
export async function startPublicClaudeRunsApi({
|
|
996
|
+
env = process.env,
|
|
997
|
+
diagnostic = () => {},
|
|
998
|
+
root = "/opt/data/private-opener",
|
|
999
|
+
createApi = createClaudeRunsApi,
|
|
1000
|
+
readToken = readKey,
|
|
1001
|
+
} = {}) {
|
|
1002
|
+
const value = env.SELLABLE_AGENT_PUBLIC_RUNS_SERVICE_PORT ?? "";
|
|
1003
|
+
const port = Number(value);
|
|
1004
|
+
const keyFile = `${root}/api-key`;
|
|
1005
|
+
let key = null;
|
|
1006
|
+
try {
|
|
1007
|
+
key = lstatSync(keyFile);
|
|
1008
|
+
} catch {
|
|
1009
|
+
/* No key, no public listener. */
|
|
1010
|
+
}
|
|
1011
|
+
if (
|
|
1012
|
+
!/^[0-9]{4,5}$/.test(value) ||
|
|
1013
|
+
port < 1024 ||
|
|
1014
|
+
port > 65535 ||
|
|
1015
|
+
// lstat: a symlink is not a file.
|
|
1016
|
+
!key?.isFile() ||
|
|
1017
|
+
(key.mode & 0o077) !== 0
|
|
1018
|
+
)
|
|
1019
|
+
return null;
|
|
1020
|
+
const server = createApi({
|
|
1021
|
+
mode: "public",
|
|
1022
|
+
token: readToken(keyFile),
|
|
1023
|
+
root,
|
|
1024
|
+
audit: auditTo(root),
|
|
1025
|
+
});
|
|
1026
|
+
await listen(server, port, "0.0.0.0");
|
|
1027
|
+
diagnostic({ ok: true, code: "claude_runs_public_api_ready", port });
|
|
1028
|
+
return { stop: () => stopServer(server) };
|
|
1029
|
+
}
|
|
@@ -131,7 +131,7 @@ const RUNTIME_GENERATION = Number(
|
|
|
131
131
|
const BOOT_SESSION_ID = randomUUID();
|
|
132
132
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.5";
|
|
133
133
|
const INSTALLER_PACKAGE =
|
|
134
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
134
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.805";
|
|
135
135
|
const MCP_PACKAGE =
|
|
136
136
|
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.957";
|
|
137
137
|
export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
|
|
@@ -1979,15 +1979,44 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
1979
1979
|
const chained = chain.length > 1;
|
|
1980
1980
|
// A one-model run is a chain of one; no selection is the Agent default.
|
|
1981
1981
|
const steps = chain.length ? chain : [null];
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1982
|
+
// No selection is the Agent default, built here as before (the opener
|
|
1983
|
+
// stages use it). A chain opens each step inside the run loop instead.
|
|
1984
|
+
const runs = chain.length
|
|
1985
|
+
? null
|
|
1986
|
+
: await createRunsClient({ apiKeyFile, claim, activeRuntime });
|
|
1987
|
+
// A workspace connection step's key, released to this attempt only. Its
|
|
1988
|
+
// literal values are scrubbed from this attempt's error code: the
|
|
1989
|
+
// diagnostic patterns miss a hex key and an srt_ grant.
|
|
1990
|
+
const released = [];
|
|
1991
|
+
const fetchConnectionKey = async (connectionId, chainStep) => {
|
|
1992
|
+
const value = await getControlClient().fetchEndpointConnectionKey({
|
|
1993
|
+
...identity,
|
|
1994
|
+
schemaVersion: "sellable-agent-endpoint-connection-key/v1",
|
|
1995
|
+
connectionId,
|
|
1996
|
+
chainStep,
|
|
1997
|
+
});
|
|
1998
|
+
for (const secret of [value?.apiKey, value?.tools?.authorization])
|
|
1999
|
+
if (typeof secret === "string" && secret) released.push(secret);
|
|
2000
|
+
return value;
|
|
2001
|
+
};
|
|
2002
|
+
// A step whose client cannot be opened sent nothing for this step; it
|
|
2003
|
+
// counts as a clean failure (STEP_UNAVAILABLE).
|
|
2004
|
+
const openStep = async (index) => {
|
|
2005
|
+
try {
|
|
2006
|
+
return await createRunsClient({
|
|
2007
|
+
apiKeyFile,
|
|
2008
|
+
claim: {
|
|
2009
|
+
...claim,
|
|
2010
|
+
run: { ...claim.run, modelSelection: chain[index] },
|
|
2011
|
+
},
|
|
2012
|
+
activeRuntime,
|
|
2013
|
+
chainStep: index,
|
|
2014
|
+
fetchConnectionKey,
|
|
2015
|
+
});
|
|
2016
|
+
} catch {
|
|
2017
|
+
return null;
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
1991
2020
|
const controller = new AbortController();
|
|
1992
2021
|
const abortFromOuter = () => controller.abort(outerSignal.reason);
|
|
1993
2022
|
outerSignal.addEventListener("abort", abortFromOuter, { once: true });
|
|
@@ -2460,7 +2489,7 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2460
2489
|
(descriptorBinding.expected.allowedToolNames ?? []).length === 0;
|
|
2461
2490
|
const history = [];
|
|
2462
2491
|
let stepIndex = 0;
|
|
2463
|
-
let stepRuns = runs;
|
|
2492
|
+
let stepRuns = chain.length ? await openStep(0) : runs;
|
|
2464
2493
|
for (;;) {
|
|
2465
2494
|
const outcome = await runNativeStep(stepIndex, stepRuns);
|
|
2466
2495
|
if (outcome.done) return outcome.result;
|
|
@@ -2492,16 +2521,7 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2492
2521
|
);
|
|
2493
2522
|
history.push(outcome.failure);
|
|
2494
2523
|
stepIndex = decision.stepIndex;
|
|
2495
|
-
|
|
2496
|
-
stepRuns = createRunsClient({
|
|
2497
|
-
apiKeyFile,
|
|
2498
|
-
claim: stepClaim(stepIndex),
|
|
2499
|
-
activeRuntime,
|
|
2500
|
-
});
|
|
2501
|
-
} catch {
|
|
2502
|
-
// Nothing was sent for this step; it counts as a clean failure.
|
|
2503
|
-
stepRuns = null;
|
|
2504
|
-
}
|
|
2524
|
+
stepRuns = await openStep(stepIndex);
|
|
2505
2525
|
}
|
|
2506
2526
|
} catch (error) {
|
|
2507
2527
|
if (heartbeatFailure) throw heartbeatFailure;
|
|
@@ -2554,10 +2574,16 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2554
2574
|
ok: false,
|
|
2555
2575
|
code:
|
|
2556
2576
|
error instanceof Error
|
|
2557
|
-
? redactRuntimeDiagnostic(
|
|
2577
|
+
? redactRuntimeDiagnostic(
|
|
2578
|
+
released.reduce(
|
|
2579
|
+
(text, secret) => text.replaceAll(secret, "[redacted]"),
|
|
2580
|
+
error.message
|
|
2581
|
+
)
|
|
2582
|
+
)
|
|
2558
2583
|
: "customer_endpoint_relay_failed",
|
|
2559
2584
|
};
|
|
2560
2585
|
} finally {
|
|
2586
|
+
released.length = 0;
|
|
2561
2587
|
clearTimeout(deadlineTimer);
|
|
2562
2588
|
outerSignal.removeEventListener("abort", abortFromOuter);
|
|
2563
2589
|
heartbeatStopped = true;
|
|
@@ -20,6 +20,7 @@ export type EndpointModelStep = Readonly<{
|
|
|
20
20
|
model: string;
|
|
21
21
|
reasoningEffort: (typeof ENDPOINT_MODEL_REASONING_EFFORTS)[number];
|
|
22
22
|
runsApiBaseUrl?: string;
|
|
23
|
+
modelConnectionId?: string;
|
|
23
24
|
}>;
|
|
24
25
|
export type EndpointModelSelection = Readonly<
|
|
25
26
|
EndpointModelStep & {
|