@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Shared by the control plane and the runtime. A custom Runs API
|
|
2
|
-
//
|
|
1
|
+
// Shared by the control plane and the runtime. A custom Runs API step always
|
|
2
|
+
// names the workspace model connection that serves it.
|
|
3
3
|
export const ENDPOINT_MODEL_SELECTION_ALLOWLIST = Object.freeze({
|
|
4
4
|
openrouter: Object.freeze([
|
|
5
5
|
// Retained for existing saved selections; the UI shortlist excludes Opus.
|
|
@@ -49,25 +49,22 @@ export function normalizeRunsApiBaseUrl(value) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// One model: provider, model, effort and, for a custom Runs API, its URL
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
52
|
+
// One model: provider, model, effort and, for a custom Runs API, its URL and
|
|
53
|
+
// the workspace's own model connection (modelConnectionId), always https.
|
|
54
|
+
// Skew: an older runtime refuses that unknown key as "invalid", so Verify &
|
|
55
|
+
// save refuses a connection step on a machine that cannot fetch its key per
|
|
56
|
+
// attempt.
|
|
57
57
|
function compileEndpointModelStep(value, extraKeys = []) {
|
|
58
58
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
59
59
|
const custom = Object.hasOwn(value, "runsApiBaseUrl");
|
|
60
|
-
const connection = Object.hasOwn(value, "modelConnectionId");
|
|
61
60
|
if (
|
|
62
61
|
Object.keys(value)
|
|
63
62
|
.filter((key) => !extraKeys.includes(key))
|
|
64
63
|
.sort()
|
|
65
64
|
.join(",") !==
|
|
66
|
-
(
|
|
65
|
+
(custom
|
|
67
66
|
? "model,modelConnectionId,provider,reasoningEffort,runsApiBaseUrl"
|
|
68
|
-
:
|
|
69
|
-
? "model,provider,reasoningEffort,runsApiBaseUrl"
|
|
70
|
-
: "model,provider,reasoningEffort")
|
|
67
|
+
: "model,provider,reasoningEffort")
|
|
71
68
|
)
|
|
72
69
|
return null;
|
|
73
70
|
const { provider, model, reasoningEffort } = value;
|
|
@@ -95,7 +92,7 @@ function compileEndpointModelStep(value, extraKeys = []) {
|
|
|
95
92
|
return null;
|
|
96
93
|
const { modelConnectionId } = value;
|
|
97
94
|
if (
|
|
98
|
-
|
|
95
|
+
custom &&
|
|
99
96
|
(typeof modelConnectionId !== "string" ||
|
|
100
97
|
!/^[A-Za-z0-9_-]{1,128}$/.test(modelConnectionId) ||
|
|
101
98
|
!runsApiBaseUrl.startsWith("https://"))
|
|
@@ -105,8 +102,7 @@ function compileEndpointModelStep(value, extraKeys = []) {
|
|
|
105
102
|
provider,
|
|
106
103
|
model,
|
|
107
104
|
reasoningEffort,
|
|
108
|
-
...(custom ? { runsApiBaseUrl } : {}),
|
|
109
|
-
...(connection ? { modelConnectionId } : {}),
|
|
105
|
+
...(custom ? { runsApiBaseUrl, modelConnectionId } : {}),
|
|
110
106
|
});
|
|
111
107
|
}
|
|
112
108
|
|
|
@@ -1,36 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/** The connection fields the OpenAI-protocol Runs adapter reads. */
|
|
2
2
|
export type RunsApiConnection = Readonly<{
|
|
3
|
-
runtimeId: string;
|
|
4
|
-
workspaceId: string;
|
|
5
|
-
agentId: string;
|
|
6
3
|
baseUrl: string;
|
|
7
|
-
apiKeyFile: string;
|
|
8
|
-
/** Absent: serves every endpoint of its agent. */
|
|
9
|
-
endpointIds?: string[];
|
|
10
|
-
workKinds: Array<"STANDARD" | "OPENER" | "REPLY" | "FOLLOW_UP">;
|
|
11
4
|
models: Array<{
|
|
12
5
|
provider: string;
|
|
13
6
|
model: string;
|
|
14
7
|
reasoningEfforts: string[];
|
|
15
8
|
}>;
|
|
16
|
-
localRoot?: string;
|
|
17
|
-
toolEndpointIds?: string[];
|
|
18
|
-
name?: string;
|
|
19
9
|
protocol?: "runs-api" | "openai-chat" | "openai-responses";
|
|
20
10
|
structuredOutputs?: boolean;
|
|
21
11
|
}>;
|
|
22
|
-
export function runsApiConnectionId(connection: RunsApiConnection): string;
|
|
23
|
-
export function runsApiConnectionServesEndpoint(
|
|
24
|
-
connection: RunsApiConnection,
|
|
25
|
-
endpointId: string
|
|
26
|
-
): boolean;
|
|
27
|
-
export function parseRunsApiConnections(raw: unknown): RunsApiConnection[];
|
|
28
|
-
export function runsApiConnectionsForRuntime(
|
|
29
|
-
raw: unknown,
|
|
30
|
-
runtime: {
|
|
31
|
-
runtimeId: string;
|
|
32
|
-
workspaceId: string;
|
|
33
|
-
agentId: string;
|
|
34
|
-
}
|
|
35
|
-
): RunsApiConnection[];
|
|
36
12
|
export function createConfiguredRunsClient(options: any): Promise<any>;
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
compileEndpointModelSelection,
|
|
4
|
-
normalizeRunsApiBaseUrl,
|
|
5
|
-
} from "./endpoint-model-selection.mjs";
|
|
2
|
+
import { compileEndpointModelSelection } from "./endpoint-model-selection.mjs";
|
|
6
3
|
import { createHermesRunsClient } from "./hermes-runs-client.mjs";
|
|
7
|
-
import { createOpenAiRunsClient } from "./openai-runs-client.mjs";
|
|
8
4
|
import { createPinnedFetch } from "./public-endpoint-guard.mjs";
|
|
9
5
|
import { createWorkspaceConnectionRunsClient } from "./workspace-connection-client.mjs";
|
|
10
6
|
|
|
11
|
-
export const RUNS_API_CONNECTIONS_ENV =
|
|
12
|
-
"SELLABLE_AGENT_RUNS_API_CONNECTIONS_JSON";
|
|
13
|
-
const id = (value) =>
|
|
14
|
-
typeof value === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(value);
|
|
15
7
|
const record = (value) =>
|
|
16
8
|
value && typeof value === "object" && !Array.isArray(value);
|
|
17
9
|
const stable = (value) =>
|
|
@@ -23,187 +15,6 @@ const stable = (value) =>
|
|
|
23
15
|
.map((key) => `${JSON.stringify(key)}:${stable(value[key])}`)
|
|
24
16
|
.join(",")}}`
|
|
25
17
|
: JSON.stringify(value);
|
|
26
|
-
const fail = () => {
|
|
27
|
-
throw new Error("runs_api_connection_configuration_rejected");
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Only the control plane's machine configuration supplies this registry. It
|
|
31
|
-
// contains credential file references, never credentials or request input.
|
|
32
|
-
export function parseRunsApiConnections(raw) {
|
|
33
|
-
if (raw === undefined || raw === "") return [];
|
|
34
|
-
if (typeof raw !== "string" || Buffer.byteLength(raw) > 64 * 1024) fail();
|
|
35
|
-
let entries;
|
|
36
|
-
try {
|
|
37
|
-
entries = JSON.parse(raw);
|
|
38
|
-
} catch {
|
|
39
|
-
fail();
|
|
40
|
-
}
|
|
41
|
-
if (!Array.isArray(entries) || entries.length > 32) fail();
|
|
42
|
-
const seen = new Set();
|
|
43
|
-
return entries.map((entry) => {
|
|
44
|
-
if (
|
|
45
|
-
!record(entry) ||
|
|
46
|
-
Object.keys(entry).some(
|
|
47
|
-
(key) =>
|
|
48
|
-
![
|
|
49
|
-
"runtimeId",
|
|
50
|
-
"workspaceId",
|
|
51
|
-
"agentId",
|
|
52
|
-
"baseUrl",
|
|
53
|
-
"apiKeyFile",
|
|
54
|
-
"endpointIds",
|
|
55
|
-
"workKinds",
|
|
56
|
-
"models",
|
|
57
|
-
"localRoot",
|
|
58
|
-
"toolEndpointIds",
|
|
59
|
-
"name",
|
|
60
|
-
"protocol",
|
|
61
|
-
"structuredOutputs",
|
|
62
|
-
].includes(key)
|
|
63
|
-
) ||
|
|
64
|
-
![entry.runtimeId, entry.workspaceId, entry.agentId].every(id) ||
|
|
65
|
-
(entry.protocol !== undefined &&
|
|
66
|
-
!["runs-api", "openai-chat", "openai-responses"].includes(
|
|
67
|
-
entry.protocol
|
|
68
|
-
)) ||
|
|
69
|
-
(entry.structuredOutputs !== undefined &&
|
|
70
|
-
typeof entry.structuredOutputs !== "boolean") ||
|
|
71
|
-
(entry.name !== undefined &&
|
|
72
|
-
(typeof entry.name !== "string" ||
|
|
73
|
-
!/^[\p{L}\p{N} .()_-]{1,80}$/u.test(entry.name))) ||
|
|
74
|
-
typeof entry.apiKeyFile !== "string" ||
|
|
75
|
-
!/^\/(?:opt\/data|run\/secrets)\/[A-Za-z0-9_./-]{1,250}$/.test(
|
|
76
|
-
entry.apiKeyFile
|
|
77
|
-
) ||
|
|
78
|
-
entry.apiKeyFile
|
|
79
|
-
.split("/")
|
|
80
|
-
.some((part) => part === "." || part === "..") ||
|
|
81
|
-
// Absent: the connection serves every endpoint of its agent.
|
|
82
|
-
(entry.endpointIds !== undefined &&
|
|
83
|
-
(!Array.isArray(entry.endpointIds) ||
|
|
84
|
-
!entry.endpointIds.length ||
|
|
85
|
-
entry.endpointIds.length > 64 ||
|
|
86
|
-
!entry.endpointIds.every(id))) ||
|
|
87
|
-
!Array.isArray(entry.workKinds) ||
|
|
88
|
-
!entry.workKinds.length ||
|
|
89
|
-
!entry.workKinds.every((kind) =>
|
|
90
|
-
["STANDARD", "OPENER", "REPLY", "FOLLOW_UP"].includes(kind)
|
|
91
|
-
) ||
|
|
92
|
-
!Array.isArray(entry.models) ||
|
|
93
|
-
!entry.models.length ||
|
|
94
|
-
entry.models.length > 16
|
|
95
|
-
)
|
|
96
|
-
fail();
|
|
97
|
-
const baseUrl = normalizeRunsApiBaseUrl(entry.baseUrl);
|
|
98
|
-
if (!baseUrl) fail();
|
|
99
|
-
const identity = `${entry.runtimeId}:${baseUrl}`;
|
|
100
|
-
if (seen.has(identity)) fail();
|
|
101
|
-
seen.add(identity);
|
|
102
|
-
const models = entry.models.map((model) => {
|
|
103
|
-
if (
|
|
104
|
-
!record(model) ||
|
|
105
|
-
Object.keys(model).sort().join(",") !==
|
|
106
|
-
"model,provider,reasoningEfforts" ||
|
|
107
|
-
!Array.isArray(model.reasoningEfforts) ||
|
|
108
|
-
!model.reasoningEfforts.length ||
|
|
109
|
-
model.reasoningEfforts.some(
|
|
110
|
-
(reasoningEffort) =>
|
|
111
|
-
compileEndpointModelSelection({
|
|
112
|
-
provider: model.provider,
|
|
113
|
-
model: model.model,
|
|
114
|
-
reasoningEffort,
|
|
115
|
-
runsApiBaseUrl: baseUrl,
|
|
116
|
-
}) === "invalid"
|
|
117
|
-
)
|
|
118
|
-
)
|
|
119
|
-
fail();
|
|
120
|
-
return { ...model, reasoningEfforts: [...model.reasoningEfforts] };
|
|
121
|
-
});
|
|
122
|
-
if (entry.localRoot !== undefined) {
|
|
123
|
-
const url = new URL(baseUrl);
|
|
124
|
-
if (
|
|
125
|
-
(entry.protocol !== undefined && entry.protocol !== "runs-api") ||
|
|
126
|
-
typeof entry.localRoot !== "string" ||
|
|
127
|
-
!/^\/opt\/data\/[A-Za-z0-9_-]{1,80}$/.test(entry.localRoot) ||
|
|
128
|
-
entry.apiKeyFile !== `${entry.localRoot}/api-key` ||
|
|
129
|
-
url.hostname !== "127.0.0.1" ||
|
|
130
|
-
url.protocol !== "http:" ||
|
|
131
|
-
url.pathname !== "/" ||
|
|
132
|
-
!url.port ||
|
|
133
|
-
Number(url.port) < 1024 ||
|
|
134
|
-
models.some(
|
|
135
|
-
(model) =>
|
|
136
|
-
model.provider !== "claude-code" ||
|
|
137
|
-
!/^claude-[a-z0-9-]{1,100}$/.test(model.model) ||
|
|
138
|
-
model.reasoningEfforts.some(
|
|
139
|
-
(effort) => !["low", "medium", "high"].includes(effort)
|
|
140
|
-
)
|
|
141
|
-
)
|
|
142
|
-
)
|
|
143
|
-
fail();
|
|
144
|
-
}
|
|
145
|
-
// When present, only runs on these endpoints get their own allowed tools
|
|
146
|
-
// from the image-owned service. When absent, a run gets the tools its own
|
|
147
|
-
// signed turn descriptor allows, and a run allowed none stays tool-free.
|
|
148
|
-
if (
|
|
149
|
-
entry.toolEndpointIds !== undefined &&
|
|
150
|
-
(entry.localRoot === undefined ||
|
|
151
|
-
!Array.isArray(entry.toolEndpointIds) ||
|
|
152
|
-
!entry.toolEndpointIds.length ||
|
|
153
|
-
!entry.toolEndpointIds.every(id) ||
|
|
154
|
-
new Set(entry.toolEndpointIds).size !== entry.toolEndpointIds.length ||
|
|
155
|
-
(entry.endpointIds !== undefined &&
|
|
156
|
-
entry.toolEndpointIds.some(
|
|
157
|
-
(value) => !entry.endpointIds.includes(value)
|
|
158
|
-
)))
|
|
159
|
-
)
|
|
160
|
-
fail();
|
|
161
|
-
return Object.freeze({
|
|
162
|
-
...entry,
|
|
163
|
-
baseUrl,
|
|
164
|
-
models,
|
|
165
|
-
...(entry.endpointIds === undefined
|
|
166
|
-
? {}
|
|
167
|
-
: { endpointIds: [...entry.endpointIds] }),
|
|
168
|
-
workKinds: [...entry.workKinds],
|
|
169
|
-
...(entry.toolEndpointIds === undefined
|
|
170
|
-
? {}
|
|
171
|
-
: { toolEndpointIds: [...entry.toolEndpointIds] }),
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Whether a trusted connection may serve runs of this endpoint. */
|
|
177
|
-
export function runsApiConnectionServesEndpoint(connection, endpointId) {
|
|
178
|
-
return (
|
|
179
|
-
connection.endpointIds === undefined ||
|
|
180
|
-
connection.endpointIds.includes(endpointId)
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function runsApiConnectionId(connection) {
|
|
185
|
-
return `runs_${createHash("sha256")
|
|
186
|
-
.update(
|
|
187
|
-
JSON.stringify([
|
|
188
|
-
connection.runtimeId,
|
|
189
|
-
connection.workspaceId,
|
|
190
|
-
connection.agentId,
|
|
191
|
-
connection.baseUrl,
|
|
192
|
-
])
|
|
193
|
-
)
|
|
194
|
-
.digest("hex")
|
|
195
|
-
.slice(0, 24)}`;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function runsApiConnectionsForRuntime(raw, runtime) {
|
|
199
|
-
return parseRunsApiConnections(raw).filter(
|
|
200
|
-
(connection) =>
|
|
201
|
-
connection.runtimeId === runtime.runtimeId &&
|
|
202
|
-
connection.workspaceId === runtime.workspaceId &&
|
|
203
|
-
connection.agentId === runtime.agentId
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
18
|
// Exactly the members the endpoint-connection-key verb returns. The key follows
|
|
208
19
|
// the saved key's rule (16-1024 characters), never the key-file rule; the
|
|
209
20
|
// remote tool's values are checked where the create body is built.
|
|
@@ -260,17 +71,15 @@ function customRunsClient(client, claim, remoteTools) {
|
|
|
260
71
|
export async function createConfiguredRunsClient({
|
|
261
72
|
apiKeyFile,
|
|
262
73
|
claim,
|
|
263
|
-
env = process.env,
|
|
264
74
|
clientFactory = createHermesRunsClient,
|
|
265
75
|
fetchConnectionKey,
|
|
266
76
|
chainStep = 0,
|
|
267
77
|
createFetch = createPinnedFetch,
|
|
268
78
|
}) {
|
|
269
79
|
const selection = claim?.run?.modelSelection;
|
|
270
|
-
// A workspace connection
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
// the signed claim, never from the key response.
|
|
80
|
+
// A workspace connection's key is fetched for this attempt and lives only in
|
|
81
|
+
// these closures, and every request goes through the address guard. URL and
|
|
82
|
+
// model come from the signed claim, never from the key response.
|
|
274
83
|
if (selection?.modelConnectionId) {
|
|
275
84
|
const step = compileEndpointModelSelection(selection);
|
|
276
85
|
if (!step || step === "invalid") throw new Error("runs_api_model_rejected");
|
|
@@ -305,46 +114,9 @@ export async function createConfiguredRunsClient({
|
|
|
305
114
|
}
|
|
306
115
|
);
|
|
307
116
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
{
|
|
314
|
-
runtimeId: claim.runtime?.runtimeId,
|
|
315
|
-
workspaceId: claim.attempt?.workspaceId,
|
|
316
|
-
agentId: claim.attempt?.agentId,
|
|
317
|
-
}
|
|
318
|
-
).find((value) => value.baseUrl === model.runsApiBaseUrl);
|
|
319
|
-
const workKind = claim.run.productExecution?.workKind ?? "STANDARD";
|
|
320
|
-
if (
|
|
321
|
-
!connection ||
|
|
322
|
-
env.SELLABLE_RUNTIME_ID !== connection.runtimeId ||
|
|
323
|
-
!runsApiConnectionServesEndpoint(connection, claim.attempt.endpointId) ||
|
|
324
|
-
!connection.workKinds.includes(workKind) ||
|
|
325
|
-
!connection.models.some(
|
|
326
|
-
(allowed) =>
|
|
327
|
-
allowed.provider === model.provider &&
|
|
328
|
-
allowed.model === model.model &&
|
|
329
|
-
allowed.reasoningEfforts.includes(model.reasoningEffort)
|
|
330
|
-
)
|
|
331
|
-
)
|
|
332
|
-
throw new Error("runs_api_connection_not_authorized");
|
|
333
|
-
// This client instance retains one destination and one credential for the
|
|
334
|
-
// entire attempt, including stream/status/stop, even if settings later change.
|
|
335
|
-
const client = connection.protocol?.startsWith("openai-")
|
|
336
|
-
? createOpenAiRunsClient({
|
|
337
|
-
connection,
|
|
338
|
-
responseSchema: claim.revision?.outputSchema,
|
|
339
|
-
modelSelection: model,
|
|
340
|
-
})
|
|
341
|
-
: clientFactory({
|
|
342
|
-
baseUrl: connection.baseUrl,
|
|
343
|
-
apiKeyFile: connection.apiKeyFile,
|
|
344
|
-
scopeHeaders: {
|
|
345
|
-
"x-sellable-workspace-id": connection.workspaceId,
|
|
346
|
-
"x-sellable-agent-id": connection.agentId,
|
|
347
|
-
},
|
|
348
|
-
});
|
|
349
|
-
return customRunsClient(client, claim);
|
|
117
|
+
// A custom URL is served only through a workspace connection: no other
|
|
118
|
+
// client, credential or socket is opened for it.
|
|
119
|
+
if (selection?.runsApiBaseUrl)
|
|
120
|
+
throw new Error("runs_api_connection_unavailable");
|
|
121
|
+
return clientFactory({ apiKeyFile });
|
|
350
122
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
1
|
import { pathToFileURL } from "node:url";
|
|
3
2
|
import {
|
|
4
3
|
compileEndpointModelSelection,
|
|
@@ -6,124 +5,23 @@ import {
|
|
|
6
5
|
endpointModelSelectionSource,
|
|
7
6
|
} from "./endpoint-model-selection.mjs";
|
|
8
7
|
import { createHermesRunsClient } from "./hermes-runs-client.mjs";
|
|
9
|
-
import {
|
|
10
|
-
createConfiguredRunsClient,
|
|
11
|
-
RUNS_API_CONNECTIONS_ENV,
|
|
12
|
-
runsApiConnectionId,
|
|
13
|
-
runsApiConnectionServesEndpoint,
|
|
14
|
-
runsApiConnectionsForRuntime,
|
|
15
|
-
} from "./runs-api-connections.mjs";
|
|
16
8
|
import { probeRunsClient } from "./workspace-connection-client.mjs";
|
|
17
9
|
|
|
18
10
|
// Fixed read-only machine command. Callers supply identities, never a URL,
|
|
19
|
-
// credential path, shell command, or prompt. The
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
env = process.env,
|
|
25
|
-
fetchImpl = fetch,
|
|
26
|
-
readToken = (path) => readFile(path, "utf8"),
|
|
27
|
-
} = {}
|
|
28
|
-
) {
|
|
29
|
-
const checkedAt = new Date().toISOString();
|
|
30
|
-
const result = (state) => ({
|
|
11
|
+
// credential path, shell command, or prompt. The machine holds no connection
|
|
12
|
+
// registry, so no id names a connection it can check: a workspace connection
|
|
13
|
+
// is probed by the server and shape-checked here by VERIFY.
|
|
14
|
+
export async function checkRunsApiConnection(request) {
|
|
15
|
+
return {
|
|
31
16
|
connectionId: request.connectionId,
|
|
32
|
-
state,
|
|
33
|
-
checkedAt,
|
|
34
|
-
}
|
|
35
|
-
if (request.runtimeId !== env.SELLABLE_RUNTIME_ID)
|
|
36
|
-
return result("NOT_CONFIGURED");
|
|
37
|
-
const connection = runsApiConnectionsForRuntime(
|
|
38
|
-
env[RUNS_API_CONNECTIONS_ENV],
|
|
39
|
-
request
|
|
40
|
-
).find(
|
|
41
|
-
(candidate) =>
|
|
42
|
-
runsApiConnectionId(candidate) === request.connectionId &&
|
|
43
|
-
runsApiConnectionServesEndpoint(candidate, request.endpointId)
|
|
44
|
-
);
|
|
45
|
-
if (!connection) return result("NOT_CONFIGURED");
|
|
46
|
-
try {
|
|
47
|
-
const token = (await readToken(connection.apiKeyFile)).trim();
|
|
48
|
-
if (!token || token.length > 8192 || /[\r\n]/.test(token))
|
|
49
|
-
return result("UNAVAILABLE");
|
|
50
|
-
const response = await fetchImpl(
|
|
51
|
-
`${connection.baseUrl}/${connection.protocol?.startsWith("openai-") ? "models" : "health"}`,
|
|
52
|
-
{
|
|
53
|
-
headers: {
|
|
54
|
-
authorization: `Bearer ${token}`,
|
|
55
|
-
...(connection.protocol?.startsWith("openai-")
|
|
56
|
-
? {}
|
|
57
|
-
: {
|
|
58
|
-
"x-sellable-workspace-id": connection.workspaceId,
|
|
59
|
-
"x-sellable-agent-id": connection.agentId,
|
|
60
|
-
}),
|
|
61
|
-
},
|
|
62
|
-
redirect: "error",
|
|
63
|
-
signal: AbortSignal.timeout(12_000),
|
|
64
|
-
}
|
|
65
|
-
);
|
|
66
|
-
if (response.status === 401 || response.status === 403)
|
|
67
|
-
return result("ACCESS_DENIED");
|
|
68
|
-
if (!response.ok) return result("UNAVAILABLE");
|
|
69
|
-
const reader = response.body?.getReader();
|
|
70
|
-
if (!reader) return result("UNAVAILABLE");
|
|
71
|
-
let text = "";
|
|
72
|
-
let size = 0;
|
|
73
|
-
try {
|
|
74
|
-
const decoder = new TextDecoder();
|
|
75
|
-
while (true) {
|
|
76
|
-
const chunk = await reader.read();
|
|
77
|
-
if (chunk.done) break;
|
|
78
|
-
size += chunk.value.byteLength;
|
|
79
|
-
if (size > 16_384) {
|
|
80
|
-
await reader.cancel();
|
|
81
|
-
return result("UNAVAILABLE");
|
|
82
|
-
}
|
|
83
|
-
text += decoder.decode(chunk.value, { stream: true });
|
|
84
|
-
}
|
|
85
|
-
text += decoder.decode();
|
|
86
|
-
} finally {
|
|
87
|
-
reader.releaseLock();
|
|
88
|
-
}
|
|
89
|
-
const health = JSON.parse(text);
|
|
90
|
-
if (connection.localRoot) {
|
|
91
|
-
if (
|
|
92
|
-
health.provider !== "claude-code" ||
|
|
93
|
-
health.scope?.workspaceId !== connection.workspaceId ||
|
|
94
|
-
health.scope?.agentId !== connection.agentId
|
|
95
|
-
)
|
|
96
|
-
return result("ACCESS_DENIED");
|
|
97
|
-
if (health.authentication === "SIGN_IN_REQUIRED")
|
|
98
|
-
return result("SIGN_IN_REQUIRED");
|
|
99
|
-
return result(
|
|
100
|
-
health.authentication === "READY" && health.ok === true
|
|
101
|
-
? "READY"
|
|
102
|
-
: "UNAVAILABLE"
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
return result(
|
|
106
|
-
(
|
|
107
|
-
connection.protocol?.startsWith("openai-")
|
|
108
|
-
? Array.isArray(health.data)
|
|
109
|
-
: health.ok === true
|
|
110
|
-
)
|
|
111
|
-
? "REACHABLE"
|
|
112
|
-
: "UNAVAILABLE"
|
|
113
|
-
);
|
|
114
|
-
} catch {
|
|
115
|
-
return result("UNAVAILABLE");
|
|
116
|
-
}
|
|
17
|
+
state: "NOT_CONFIGURED",
|
|
18
|
+
checkedAt: new Date().toISOString(),
|
|
19
|
+
};
|
|
117
20
|
}
|
|
118
21
|
|
|
119
22
|
export async function verifyRunsApiSelection(
|
|
120
23
|
request,
|
|
121
|
-
{
|
|
122
|
-
env = process.env,
|
|
123
|
-
makeDefaultClient = createHermesRunsClient,
|
|
124
|
-
makeConfiguredClient = createConfiguredRunsClient,
|
|
125
|
-
health = checkRunsApiConnection,
|
|
126
|
-
} = {}
|
|
24
|
+
{ env = process.env, makeDefaultClient = createHermesRunsClient } = {}
|
|
127
25
|
) {
|
|
128
26
|
const result = (state, servedRuntime) => ({
|
|
129
27
|
connectionId: request.connectionId,
|
|
@@ -146,29 +44,7 @@ export async function verifyRunsApiSelection(
|
|
|
146
44
|
// machine holds none to probe with. This is a shape check only; the server
|
|
147
45
|
// performs the connectivity probe (228-05).
|
|
148
46
|
if (selection?.modelConnectionId) return result("REACHABLE");
|
|
149
|
-
|
|
150
|
-
if (selection?.runsApiBaseUrl) {
|
|
151
|
-
const connection = runsApiConnectionsForRuntime(
|
|
152
|
-
env[RUNS_API_CONNECTIONS_ENV],
|
|
153
|
-
request
|
|
154
|
-
).find(
|
|
155
|
-
(item) =>
|
|
156
|
-
runsApiConnectionId(item) === request.connectionId &&
|
|
157
|
-
item.baseUrl === selection.runsApiBaseUrl
|
|
158
|
-
);
|
|
159
|
-
if (
|
|
160
|
-
!connection ||
|
|
161
|
-
!runsApiConnectionServesEndpoint(connection, request.endpointId)
|
|
162
|
-
)
|
|
163
|
-
return result("NOT_CONFIGURED");
|
|
164
|
-
verificationWorkKind = connection.workKinds.includes("STANDARD")
|
|
165
|
-
? "STANDARD"
|
|
166
|
-
: connection.workKinds[0];
|
|
167
|
-
if (connection.localRoot) {
|
|
168
|
-
const readiness = await health(request, { env });
|
|
169
|
-
if (readiness.state !== "READY") return readiness;
|
|
170
|
-
}
|
|
171
|
-
} else if (request.connectionId !== endpointModelSelectionSource(selection))
|
|
47
|
+
if (request.connectionId !== endpointModelSelectionSource(selection))
|
|
172
48
|
return result("NOT_CONFIGURED");
|
|
173
49
|
const apiKeyFile =
|
|
174
50
|
env.SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND === "ADMIN"
|
|
@@ -176,30 +52,13 @@ export async function verifyRunsApiSelection(
|
|
|
176
52
|
: `/run/sellable-agent/${request.profileId}/api-server-key`;
|
|
177
53
|
let client;
|
|
178
54
|
try {
|
|
179
|
-
client =
|
|
180
|
-
? await makeConfiguredClient({
|
|
181
|
-
apiKeyFile,
|
|
182
|
-
env,
|
|
183
|
-
claim: {
|
|
184
|
-
runtime: { runtimeId: request.runtimeId },
|
|
185
|
-
attempt: {
|
|
186
|
-
workspaceId: request.workspaceId,
|
|
187
|
-
agentId: request.agentId,
|
|
188
|
-
endpointId: request.endpointId,
|
|
189
|
-
},
|
|
190
|
-
run: {
|
|
191
|
-
modelSelection: selection,
|
|
192
|
-
productExecution: { workKind: verificationWorkKind },
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
})
|
|
196
|
-
: makeDefaultClient({ apiKeyFile });
|
|
55
|
+
client = makeDefaultClient({ apiKeyFile });
|
|
197
56
|
} catch {
|
|
198
57
|
return result("UNAVAILABLE");
|
|
199
58
|
}
|
|
200
59
|
const probe = await probeRunsClient(client, selection, {
|
|
201
60
|
signal: AbortSignal.timeout(70_000),
|
|
202
|
-
submitVerification:
|
|
61
|
+
submitVerification: true,
|
|
203
62
|
});
|
|
204
63
|
return result(probe.state, probe.servedRuntime);
|
|
205
64
|
}
|
|
@@ -530,8 +530,11 @@ const RETRYABLE_WORKER_CONTROL_STATUSES = new Set([
|
|
|
530
530
|
408, 425, 429, 500, 502, 503, 504,
|
|
531
531
|
]);
|
|
532
532
|
|
|
533
|
-
class WorkerControlResponseError extends Error {
|
|
534
|
-
constructor(
|
|
533
|
+
export class WorkerControlResponseError extends Error {
|
|
534
|
+
constructor(
|
|
535
|
+
code,
|
|
536
|
+
{ retryable = false, status = /** @type {number | null} */ (null) } = {}
|
|
537
|
+
) {
|
|
535
538
|
super(code);
|
|
536
539
|
this.name = "WorkerControlResponseError";
|
|
537
540
|
this.retryable = retryable;
|
|
@@ -38,8 +38,8 @@ 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.
|
|
42
|
-
const MCP_PACKAGE = "@sellable/mcp@0.1.
|
|
41
|
+
const INSTALLER_PACKAGE = "@sellable/install@0.1.813";
|
|
42
|
+
const MCP_PACKAGE = "@sellable/mcp@0.1.959";
|
|
43
43
|
const HERMES_VERSION = "0.18.0";
|
|
44
44
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
45
45
|
const SHA256_16 = /^[a-f0-9]{16}$/;
|
|
@@ -1546,8 +1546,8 @@ 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.
|
|
1550
|
-
pinned.mcpPackage !== "@sellable/mcp@0.1.
|
|
1549
|
+
pinned.installerPackage !== "@sellable/install@0.1.813" ||
|
|
1550
|
+
pinned.mcpPackage !== "@sellable/mcp@0.1.959" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
1553
1553
|
!Number.isSafeInteger(slack.generation) ||
|
|
@@ -312,8 +312,8 @@ 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.
|
|
316
|
-
desired.mcpPackage !== "@sellable/mcp@0.1.
|
|
315
|
+
desired.installerPackage !== "@sellable/install@0.1.813" ||
|
|
316
|
+
desired.mcpPackage !== "@sellable/mcp@0.1.959" ||
|
|
317
317
|
!Array.isArray(desired.toolInclude) ||
|
|
318
318
|
desired.toolInclude.length === 0 ||
|
|
319
319
|
desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
|
|
@@ -25,8 +25,8 @@ 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.
|
|
29
|
-
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.
|
|
28
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.813";
|
|
29
|
+
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.959";
|
|
30
30
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
31
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|
|
32
32
|
);
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export function tagEndpointSession(line: string, sessionId: string): string;
|