@sellable/install 0.1.794 → 0.1.795-wip.runtime-ux.20261004.1
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-admin-image/Dockerfile +4 -0
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +10 -0
- package/lib/sellable-agent/fly-customer-image/Dockerfile +2 -1
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.d.mts +57 -0
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.mjs +563 -0
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +3 -4
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.d.mts +26 -0
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.mjs +98 -0
- package/lib/sellable-agent/fly-customer-image/hermes-runs-client.mjs +24 -33
- package/lib/sellable-agent/fly-customer-image/openai-runs-client.d.mts +7 -0
- package/lib/sellable-agent/fly-customer-image/openai-runs-client.mjs +271 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.d.mts +30 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-connections.mjs +245 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.d.mts +9 -0
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.mjs +261 -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/package.json +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
compileEndpointModelSelection,
|
|
4
|
+
normalizeRunsApiBaseUrl,
|
|
5
|
+
} from "./endpoint-model-selection.mjs";
|
|
6
|
+
import { createHermesRunsClient } from "./hermes-runs-client.mjs";
|
|
7
|
+
import { createOpenAiRunsClient } from "./openai-runs-client.mjs";
|
|
8
|
+
|
|
9
|
+
export const RUNS_API_CONNECTIONS_ENV =
|
|
10
|
+
"SELLABLE_AGENT_RUNS_API_CONNECTIONS_JSON";
|
|
11
|
+
const id = (value) =>
|
|
12
|
+
typeof value === "string" && /^[A-Za-z0-9_.:-]{1,128}$/.test(value);
|
|
13
|
+
const record = (value) =>
|
|
14
|
+
value && typeof value === "object" && !Array.isArray(value);
|
|
15
|
+
const stable = (value) =>
|
|
16
|
+
Array.isArray(value)
|
|
17
|
+
? `[${value.map(stable).join(",")}]`
|
|
18
|
+
: record(value)
|
|
19
|
+
? `{${Object.keys(value)
|
|
20
|
+
.sort()
|
|
21
|
+
.map((key) => `${JSON.stringify(key)}:${stable(value[key])}`)
|
|
22
|
+
.join(",")}}`
|
|
23
|
+
: JSON.stringify(value);
|
|
24
|
+
const fail = () => {
|
|
25
|
+
throw new Error("runs_api_connection_configuration_rejected");
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Only the control plane's machine configuration supplies this registry. It
|
|
29
|
+
// contains credential file references, never credentials or request input.
|
|
30
|
+
export function parseRunsApiConnections(raw) {
|
|
31
|
+
if (raw === undefined || raw === "") return [];
|
|
32
|
+
if (typeof raw !== "string" || Buffer.byteLength(raw) > 64 * 1024) fail();
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = JSON.parse(raw);
|
|
36
|
+
} catch {
|
|
37
|
+
fail();
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(entries) || entries.length > 32) fail();
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
return entries.map((entry) => {
|
|
42
|
+
if (
|
|
43
|
+
!record(entry) ||
|
|
44
|
+
Object.keys(entry).some(
|
|
45
|
+
(key) =>
|
|
46
|
+
![
|
|
47
|
+
"runtimeId",
|
|
48
|
+
"workspaceId",
|
|
49
|
+
"agentId",
|
|
50
|
+
"baseUrl",
|
|
51
|
+
"apiKeyFile",
|
|
52
|
+
"endpointIds",
|
|
53
|
+
"workKinds",
|
|
54
|
+
"models",
|
|
55
|
+
"localRoot",
|
|
56
|
+
"name",
|
|
57
|
+
"protocol",
|
|
58
|
+
"structuredOutputs",
|
|
59
|
+
].includes(key)
|
|
60
|
+
) ||
|
|
61
|
+
![entry.runtimeId, entry.workspaceId, entry.agentId].every(id) ||
|
|
62
|
+
(entry.protocol !== undefined &&
|
|
63
|
+
!["runs-api", "openai-chat", "openai-responses"].includes(
|
|
64
|
+
entry.protocol
|
|
65
|
+
)) ||
|
|
66
|
+
(entry.structuredOutputs !== undefined &&
|
|
67
|
+
typeof entry.structuredOutputs !== "boolean") ||
|
|
68
|
+
(entry.name !== undefined &&
|
|
69
|
+
(typeof entry.name !== "string" ||
|
|
70
|
+
!/^[\p{L}\p{N} .()_-]{1,80}$/u.test(entry.name))) ||
|
|
71
|
+
typeof entry.apiKeyFile !== "string" ||
|
|
72
|
+
!/^\/(?:opt\/data|run\/secrets)\/[A-Za-z0-9_./-]{1,250}$/.test(
|
|
73
|
+
entry.apiKeyFile
|
|
74
|
+
) ||
|
|
75
|
+
entry.apiKeyFile
|
|
76
|
+
.split("/")
|
|
77
|
+
.some((part) => part === "." || part === "..") ||
|
|
78
|
+
!Array.isArray(entry.endpointIds) ||
|
|
79
|
+
!entry.endpointIds.length ||
|
|
80
|
+
entry.endpointIds.length > 64 ||
|
|
81
|
+
!entry.endpointIds.every(id) ||
|
|
82
|
+
!Array.isArray(entry.workKinds) ||
|
|
83
|
+
!entry.workKinds.length ||
|
|
84
|
+
!entry.workKinds.every((kind) =>
|
|
85
|
+
["STANDARD", "OPENER", "REPLY", "FOLLOW_UP"].includes(kind)
|
|
86
|
+
) ||
|
|
87
|
+
!Array.isArray(entry.models) ||
|
|
88
|
+
!entry.models.length ||
|
|
89
|
+
entry.models.length > 16
|
|
90
|
+
)
|
|
91
|
+
fail();
|
|
92
|
+
const baseUrl = normalizeRunsApiBaseUrl(entry.baseUrl);
|
|
93
|
+
if (!baseUrl) fail();
|
|
94
|
+
const identity = `${entry.runtimeId}:${baseUrl}`;
|
|
95
|
+
if (seen.has(identity)) fail();
|
|
96
|
+
seen.add(identity);
|
|
97
|
+
const models = entry.models.map((model) => {
|
|
98
|
+
if (
|
|
99
|
+
!record(model) ||
|
|
100
|
+
Object.keys(model).sort().join(",") !==
|
|
101
|
+
"model,provider,reasoningEfforts" ||
|
|
102
|
+
!Array.isArray(model.reasoningEfforts) ||
|
|
103
|
+
!model.reasoningEfforts.length ||
|
|
104
|
+
model.reasoningEfforts.some(
|
|
105
|
+
(reasoningEffort) =>
|
|
106
|
+
compileEndpointModelSelection({
|
|
107
|
+
provider: model.provider,
|
|
108
|
+
model: model.model,
|
|
109
|
+
reasoningEffort,
|
|
110
|
+
runsApiBaseUrl: baseUrl,
|
|
111
|
+
}) === "invalid"
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
fail();
|
|
115
|
+
return { ...model, reasoningEfforts: [...model.reasoningEfforts] };
|
|
116
|
+
});
|
|
117
|
+
if (entry.localRoot !== undefined) {
|
|
118
|
+
const url = new URL(baseUrl);
|
|
119
|
+
if (
|
|
120
|
+
(entry.protocol !== undefined && entry.protocol !== "runs-api") ||
|
|
121
|
+
typeof entry.localRoot !== "string" ||
|
|
122
|
+
!/^\/opt\/data\/[A-Za-z0-9_-]{1,80}$/.test(entry.localRoot) ||
|
|
123
|
+
entry.apiKeyFile !== `${entry.localRoot}/api-key` ||
|
|
124
|
+
url.hostname !== "127.0.0.1" ||
|
|
125
|
+
url.protocol !== "http:" ||
|
|
126
|
+
url.pathname !== "/" ||
|
|
127
|
+
!url.port ||
|
|
128
|
+
Number(url.port) < 1024 ||
|
|
129
|
+
models.some(
|
|
130
|
+
(model) =>
|
|
131
|
+
model.provider !== "claude-code" ||
|
|
132
|
+
!/^claude-[a-z0-9-]{1,100}$/.test(model.model) ||
|
|
133
|
+
model.reasoningEfforts.some(
|
|
134
|
+
(effort) => !["low", "medium", "high"].includes(effort)
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
fail();
|
|
139
|
+
}
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
...entry,
|
|
142
|
+
baseUrl,
|
|
143
|
+
models,
|
|
144
|
+
endpointIds: [...entry.endpointIds],
|
|
145
|
+
workKinds: [...entry.workKinds],
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function runsApiConnectionId(connection) {
|
|
151
|
+
return `runs_${createHash("sha256")
|
|
152
|
+
.update(
|
|
153
|
+
JSON.stringify([
|
|
154
|
+
connection.runtimeId,
|
|
155
|
+
connection.workspaceId,
|
|
156
|
+
connection.agentId,
|
|
157
|
+
connection.baseUrl,
|
|
158
|
+
])
|
|
159
|
+
)
|
|
160
|
+
.digest("hex")
|
|
161
|
+
.slice(0, 24)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function runsApiConnectionsForRuntime(raw, runtime) {
|
|
165
|
+
return parseRunsApiConnections(raw).filter(
|
|
166
|
+
(connection) =>
|
|
167
|
+
connection.runtimeId === runtime.runtimeId &&
|
|
168
|
+
connection.workspaceId === runtime.workspaceId &&
|
|
169
|
+
connection.agentId === runtime.agentId
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** @param {any} options */
|
|
174
|
+
export function createConfiguredRunsClient({
|
|
175
|
+
apiKeyFile,
|
|
176
|
+
claim,
|
|
177
|
+
env = process.env,
|
|
178
|
+
clientFactory = createHermesRunsClient,
|
|
179
|
+
}) {
|
|
180
|
+
const selection = claim?.run?.modelSelection;
|
|
181
|
+
if (!selection?.runsApiBaseUrl) return clientFactory({ apiKeyFile });
|
|
182
|
+
const model = compileEndpointModelSelection(selection);
|
|
183
|
+
if (!model || model === "invalid") throw new Error("runs_api_model_rejected");
|
|
184
|
+
const connection = runsApiConnectionsForRuntime(
|
|
185
|
+
env[RUNS_API_CONNECTIONS_ENV],
|
|
186
|
+
{
|
|
187
|
+
runtimeId: claim.runtime?.runtimeId,
|
|
188
|
+
workspaceId: claim.attempt?.workspaceId,
|
|
189
|
+
agentId: claim.attempt?.agentId,
|
|
190
|
+
}
|
|
191
|
+
).find((value) => value.baseUrl === model.runsApiBaseUrl);
|
|
192
|
+
const workKind = claim.run.productExecution?.workKind ?? "STANDARD";
|
|
193
|
+
if (
|
|
194
|
+
!connection ||
|
|
195
|
+
env.SELLABLE_RUNTIME_ID !== connection.runtimeId ||
|
|
196
|
+
!connection.endpointIds.includes(claim.attempt.endpointId) ||
|
|
197
|
+
!connection.workKinds.includes(workKind) ||
|
|
198
|
+
!connection.models.some(
|
|
199
|
+
(allowed) =>
|
|
200
|
+
allowed.provider === model.provider &&
|
|
201
|
+
allowed.model === model.model &&
|
|
202
|
+
allowed.reasoningEfforts.includes(model.reasoningEffort)
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
throw new Error("runs_api_connection_not_authorized");
|
|
206
|
+
// This client instance retains one destination and one credential for the
|
|
207
|
+
// entire attempt, including stream/status/stop, even if settings later change.
|
|
208
|
+
const client = connection.protocol?.startsWith("openai-")
|
|
209
|
+
? createOpenAiRunsClient({
|
|
210
|
+
connection,
|
|
211
|
+
responseSchema: claim.revision?.outputSchema,
|
|
212
|
+
modelSelection: model,
|
|
213
|
+
})
|
|
214
|
+
: clientFactory({
|
|
215
|
+
baseUrl: connection.baseUrl,
|
|
216
|
+
apiKeyFile: connection.apiKeyFile,
|
|
217
|
+
scopeHeaders: {
|
|
218
|
+
"x-sellable-workspace-id": connection.workspaceId,
|
|
219
|
+
"x-sellable-agent-id": connection.agentId,
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
...client,
|
|
224
|
+
submit(compiled, options) {
|
|
225
|
+
// Public semantic endpoints have a published schema but no product tuple.
|
|
226
|
+
// Preserve that existing contract when calling a custom Runs service.
|
|
227
|
+
const schema = claim.revision?.outputSchema;
|
|
228
|
+
if (compiled.outputContract || !schema?.properties?.proposal)
|
|
229
|
+
return client.submit(compiled, options);
|
|
230
|
+
return client.submit(
|
|
231
|
+
{
|
|
232
|
+
...compiled,
|
|
233
|
+
outputContract: {
|
|
234
|
+
schemaVersion: "sellable-opener-output-contract/v1",
|
|
235
|
+
schema,
|
|
236
|
+
nativeProposalSchemaDigest: createHash("sha256")
|
|
237
|
+
.update(stable(schema.properties.proposal))
|
|
238
|
+
.digest("hex"),
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
options
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { compileEndpointModelSelection } from "./endpoint-model-selection.mjs";
|
|
5
|
+
import { createHermesRunsClient } from "./hermes-runs-client.mjs";
|
|
6
|
+
import {
|
|
7
|
+
createConfiguredRunsClient,
|
|
8
|
+
RUNS_API_CONNECTIONS_ENV,
|
|
9
|
+
runsApiConnectionId,
|
|
10
|
+
runsApiConnectionsForRuntime,
|
|
11
|
+
} from "./runs-api-connections.mjs";
|
|
12
|
+
|
|
13
|
+
// Fixed read-only machine command. Callers supply identities, never a URL,
|
|
14
|
+
// credential path, shell command, or prompt. The installed registry authorizes
|
|
15
|
+
// the destination again on the machine before a credential is read.
|
|
16
|
+
export async function checkRunsApiConnection(
|
|
17
|
+
request,
|
|
18
|
+
{
|
|
19
|
+
env = process.env,
|
|
20
|
+
fetchImpl = fetch,
|
|
21
|
+
readToken = (path) => readFile(path, "utf8"),
|
|
22
|
+
} = {}
|
|
23
|
+
) {
|
|
24
|
+
const checkedAt = new Date().toISOString();
|
|
25
|
+
const result = (state) => ({
|
|
26
|
+
connectionId: request.connectionId,
|
|
27
|
+
state,
|
|
28
|
+
checkedAt,
|
|
29
|
+
});
|
|
30
|
+
if (request.runtimeId !== env.SELLABLE_RUNTIME_ID)
|
|
31
|
+
return result("NOT_CONFIGURED");
|
|
32
|
+
const connection = runsApiConnectionsForRuntime(
|
|
33
|
+
env[RUNS_API_CONNECTIONS_ENV],
|
|
34
|
+
request
|
|
35
|
+
).find(
|
|
36
|
+
(candidate) =>
|
|
37
|
+
runsApiConnectionId(candidate) === request.connectionId &&
|
|
38
|
+
candidate.endpointIds.includes(request.endpointId)
|
|
39
|
+
);
|
|
40
|
+
if (!connection) return result("NOT_CONFIGURED");
|
|
41
|
+
try {
|
|
42
|
+
const token = (await readToken(connection.apiKeyFile)).trim();
|
|
43
|
+
if (!token || token.length > 8192 || /[\r\n]/.test(token))
|
|
44
|
+
return result("UNAVAILABLE");
|
|
45
|
+
const response = await fetchImpl(
|
|
46
|
+
`${connection.baseUrl}/${connection.protocol?.startsWith("openai-") ? "models" : "health"}`,
|
|
47
|
+
{
|
|
48
|
+
headers: {
|
|
49
|
+
authorization: `Bearer ${token}`,
|
|
50
|
+
...(connection.protocol?.startsWith("openai-") ? {} : {
|
|
51
|
+
"x-sellable-workspace-id": connection.workspaceId,
|
|
52
|
+
"x-sellable-agent-id": connection.agentId,
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
redirect: "error",
|
|
56
|
+
signal: AbortSignal.timeout(12_000),
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
if (response.status === 401 || response.status === 403)
|
|
60
|
+
return result("ACCESS_DENIED");
|
|
61
|
+
if (!response.ok) return result("UNAVAILABLE");
|
|
62
|
+
const reader = response.body?.getReader();
|
|
63
|
+
if (!reader) return result("UNAVAILABLE");
|
|
64
|
+
let text = "";
|
|
65
|
+
let size = 0;
|
|
66
|
+
try {
|
|
67
|
+
const decoder = new TextDecoder();
|
|
68
|
+
while (true) {
|
|
69
|
+
const chunk = await reader.read();
|
|
70
|
+
if (chunk.done) break;
|
|
71
|
+
size += chunk.value.byteLength;
|
|
72
|
+
if (size > 16_384) {
|
|
73
|
+
await reader.cancel();
|
|
74
|
+
return result("UNAVAILABLE");
|
|
75
|
+
}
|
|
76
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
77
|
+
}
|
|
78
|
+
text += decoder.decode();
|
|
79
|
+
} finally {
|
|
80
|
+
reader.releaseLock();
|
|
81
|
+
}
|
|
82
|
+
const health = JSON.parse(text);
|
|
83
|
+
if (connection.localRoot) {
|
|
84
|
+
if (
|
|
85
|
+
health.provider !== "claude-code" ||
|
|
86
|
+
health.scope?.workspaceId !== connection.workspaceId ||
|
|
87
|
+
health.scope?.agentId !== connection.agentId
|
|
88
|
+
)
|
|
89
|
+
return result("ACCESS_DENIED");
|
|
90
|
+
if (health.authentication === "SIGN_IN_REQUIRED")
|
|
91
|
+
return result("SIGN_IN_REQUIRED");
|
|
92
|
+
return result(
|
|
93
|
+
health.authentication === "READY" && health.ok === true
|
|
94
|
+
? "READY"
|
|
95
|
+
: "UNAVAILABLE"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return result(
|
|
99
|
+
(
|
|
100
|
+
connection.protocol?.startsWith("openai-")
|
|
101
|
+
? Array.isArray(health.data)
|
|
102
|
+
: health.ok === true
|
|
103
|
+
)
|
|
104
|
+
? "REACHABLE"
|
|
105
|
+
: "UNAVAILABLE"
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
return result("UNAVAILABLE");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function verifyRunsApiSelection(
|
|
113
|
+
request,
|
|
114
|
+
{
|
|
115
|
+
env = process.env,
|
|
116
|
+
makeDefaultClient = createHermesRunsClient,
|
|
117
|
+
makeConfiguredClient = createConfiguredRunsClient,
|
|
118
|
+
health = checkRunsApiConnection,
|
|
119
|
+
} = {}
|
|
120
|
+
) {
|
|
121
|
+
const result = (state, servedRuntime) => ({
|
|
122
|
+
connectionId: request.connectionId,
|
|
123
|
+
state,
|
|
124
|
+
checkedAt: new Date().toISOString(),
|
|
125
|
+
...(servedRuntime ? { servedRuntime } : {}),
|
|
126
|
+
});
|
|
127
|
+
if (
|
|
128
|
+
request.runtimeId !== env.SELLABLE_RUNTIME_ID ||
|
|
129
|
+
!/^[A-Za-z0-9_-]{1,128}$/.test(request.profileId ?? "")
|
|
130
|
+
)
|
|
131
|
+
return result("NOT_CONFIGURED");
|
|
132
|
+
const selection = compileEndpointModelSelection(request.modelSelection);
|
|
133
|
+
if (selection === "invalid") return result("NOT_CONFIGURED");
|
|
134
|
+
let verificationWorkKind = "STANDARD";
|
|
135
|
+
if (selection?.runsApiBaseUrl) {
|
|
136
|
+
const connection = runsApiConnectionsForRuntime(
|
|
137
|
+
env[RUNS_API_CONNECTIONS_ENV],
|
|
138
|
+
request
|
|
139
|
+
).find(
|
|
140
|
+
(item) =>
|
|
141
|
+
runsApiConnectionId(item) === request.connectionId &&
|
|
142
|
+
item.baseUrl === selection.runsApiBaseUrl
|
|
143
|
+
);
|
|
144
|
+
if (!connection?.endpointIds.includes(request.endpointId))
|
|
145
|
+
return result("NOT_CONFIGURED");
|
|
146
|
+
verificationWorkKind = connection.workKinds.includes("STANDARD")
|
|
147
|
+
? "STANDARD"
|
|
148
|
+
: connection.workKinds[0];
|
|
149
|
+
if (connection.localRoot) {
|
|
150
|
+
const readiness = await health(request, { env });
|
|
151
|
+
if (readiness.state !== "READY") return readiness;
|
|
152
|
+
}
|
|
153
|
+
} else if (request.connectionId !== (selection ? "openrouter" : "default"))
|
|
154
|
+
return result("NOT_CONFIGURED");
|
|
155
|
+
const apiKeyFile =
|
|
156
|
+
env.SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND === "ADMIN"
|
|
157
|
+
? "/run/sellable-agent/admin-runtime/api-server-key"
|
|
158
|
+
: `/run/sellable-agent/${request.profileId}/api-server-key`;
|
|
159
|
+
let client;
|
|
160
|
+
let run;
|
|
161
|
+
try {
|
|
162
|
+
client = selection?.runsApiBaseUrl
|
|
163
|
+
? makeConfiguredClient({
|
|
164
|
+
apiKeyFile,
|
|
165
|
+
env,
|
|
166
|
+
claim: {
|
|
167
|
+
runtime: { runtimeId: request.runtimeId },
|
|
168
|
+
attempt: {
|
|
169
|
+
workspaceId: request.workspaceId,
|
|
170
|
+
agentId: request.agentId,
|
|
171
|
+
endpointId: request.endpointId,
|
|
172
|
+
},
|
|
173
|
+
run: {
|
|
174
|
+
modelSelection: selection,
|
|
175
|
+
productExecution: { workKind: verificationWorkKind },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
: makeDefaultClient({ apiKeyFile });
|
|
180
|
+
const compiled = {
|
|
181
|
+
input: "Connection verification. Reply with SELLABLE_CONNECTION_OK.",
|
|
182
|
+
instructions:
|
|
183
|
+
"This is a connection check. Do not use tools or take actions. Return exactly SELLABLE_CONNECTION_OK and nothing else.",
|
|
184
|
+
conversationHistory: [],
|
|
185
|
+
correlation: `connection-check-${randomUUID()}`,
|
|
186
|
+
...(selection ? { modelSelection: selection } : {}),
|
|
187
|
+
};
|
|
188
|
+
const signal = AbortSignal.timeout(70_000);
|
|
189
|
+
// Default Hermes gets the tool-free verification prefix. A Runs service
|
|
190
|
+
// owns its own tool policy; the Claude and OpenAI adapters are tool-free.
|
|
191
|
+
run = await (
|
|
192
|
+
selection?.runsApiBaseUrl ? client.submit : client.submitVerification
|
|
193
|
+
)(compiled, { signal });
|
|
194
|
+
await client.streamEvents(run.runId, { signal, onEvent: async () => {} });
|
|
195
|
+
const status = await client.status(run.runId, { signal });
|
|
196
|
+
const served = status.servedRuntime;
|
|
197
|
+
if (
|
|
198
|
+
status.status !== "completed" ||
|
|
199
|
+
typeof status.output !== "string" ||
|
|
200
|
+
status.output.trim() !== "SELLABLE_CONNECTION_OK" ||
|
|
201
|
+
!served
|
|
202
|
+
)
|
|
203
|
+
return result("UNAVAILABLE");
|
|
204
|
+
if (
|
|
205
|
+
selection &&
|
|
206
|
+
(served.provider !== selection.provider ||
|
|
207
|
+
(served.model !== selection.model &&
|
|
208
|
+
!served.model.startsWith(`${selection.model}-`)))
|
|
209
|
+
)
|
|
210
|
+
return result("UNAVAILABLE");
|
|
211
|
+
return result("READY", served);
|
|
212
|
+
} catch {
|
|
213
|
+
if (client && run)
|
|
214
|
+
await client
|
|
215
|
+
.stop(run.runId, { signal: AbortSignal.timeout(3_000) })
|
|
216
|
+
.catch(() => {});
|
|
217
|
+
return result("UNAVAILABLE");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
process.argv[1] &&
|
|
223
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
224
|
+
) {
|
|
225
|
+
try {
|
|
226
|
+
const raw = process.argv[2];
|
|
227
|
+
if (!raw || raw.length > 4096) throw new Error();
|
|
228
|
+
const request = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
229
|
+
const verify = request.action === "VERIFY";
|
|
230
|
+
if (
|
|
231
|
+
Object.keys(request)
|
|
232
|
+
.filter(
|
|
233
|
+
(key) => !["action", "profileId", "modelSelection"].includes(key)
|
|
234
|
+
)
|
|
235
|
+
.sort()
|
|
236
|
+
.join(",") !==
|
|
237
|
+
"agentId,connectionId,endpointId,runtimeId,workspaceId" ||
|
|
238
|
+
[
|
|
239
|
+
request.agentId,
|
|
240
|
+
request.connectionId,
|
|
241
|
+
request.endpointId,
|
|
242
|
+
request.runtimeId,
|
|
243
|
+
request.workspaceId,
|
|
244
|
+
].some(
|
|
245
|
+
(value) =>
|
|
246
|
+
typeof value !== "string" || !/^[A-Za-z0-9_.:-]{1,128}$/.test(value)
|
|
247
|
+
) ||
|
|
248
|
+
(request.action !== undefined && !verify)
|
|
249
|
+
)
|
|
250
|
+
throw new Error();
|
|
251
|
+
process.stdout.write(
|
|
252
|
+
JSON.stringify(
|
|
253
|
+
await (verify
|
|
254
|
+
? verifyRunsApiSelection(request)
|
|
255
|
+
: checkRunsApiConnection(request))
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
} catch {
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -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.795-wip.runtime-ux.20261004.1";
|
|
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.795-wip.runtime-ux.20261004.1" ||
|
|
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.795-wip.runtime-ux.20261004.1" ||
|
|
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.795-wip.runtime-ux.20261004.1";
|
|
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
|