@sellable/install 0.1.356 → 0.1.357-wip.20260720060000.4
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/bin/sellable-agent-egress-proxy.mjs +31 -0
- package/bin/sellable-agent-host-worker.mjs +9 -0
- package/bin/sellable-agent-runtime-helper.mjs +37 -0
- package/bin/sellable-agent-runtime-launcher.mjs +151 -0
- package/bin/sellable-agent-runtime-probe.mjs +162 -0
- package/bin/sellable-agent-sandbox-init.mjs +93 -0
- package/lib/sellable-agent/containment-contract.mjs +472 -0
- package/lib/sellable-agent/hermes-bridge.mjs +496 -0
- package/lib/sellable-agent/host-worker.mjs +2059 -0
- package/lib/sellable-agent/profile-materializer.mjs +1410 -0
- package/lib/sellable-agent/provisioning-adapter.mjs +987 -0
- package/lib/sellable-agent/runtime-boundary.mjs +635 -0
- package/lib/sellable-agent/runtime-egress-proxy.mjs +241 -0
- package/lib/sellable-agent/runtime-helper.mjs +1331 -0
- package/lib/sellable-agent/runtime-reconciler.mjs +683 -0
- package/lib/sellable-agent/service-installer.mjs +439 -0
- package/package.json +9 -2
- package/services/s6/sellable-agent-egress-proxy/run +15 -0
- package/services/s6/sellable-agent-host-worker/run +4 -0
- package/services/s6/sellable-agent-runtime/run +19 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
closeSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
realpathSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
|
|
21
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.356";
|
|
22
|
+
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.614";
|
|
23
|
+
|
|
24
|
+
export function deriveAgentProfileId(workspaceId, agentId) {
|
|
25
|
+
return `agent-${createHash("sha256")
|
|
26
|
+
.update("sellable-agent-profile\0")
|
|
27
|
+
.update(String(workspaceId))
|
|
28
|
+
.update("\0")
|
|
29
|
+
.update(String(agentId))
|
|
30
|
+
.digest("hex")
|
|
31
|
+
.slice(0, 24)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const INSTALLER_BIN = fileURLToPath(
|
|
35
|
+
new URL("../../bin/sellable-install.mjs", import.meta.url)
|
|
36
|
+
);
|
|
37
|
+
const INSTALLER_PACKAGE_JSON = fileURLToPath(
|
|
38
|
+
new URL("../../package.json", import.meta.url)
|
|
39
|
+
);
|
|
40
|
+
const REQUEST_KEYS = new Set([
|
|
41
|
+
"action",
|
|
42
|
+
"workspaceId",
|
|
43
|
+
"agentId",
|
|
44
|
+
"generation",
|
|
45
|
+
"desiredRevision",
|
|
46
|
+
"idempotencyKey",
|
|
47
|
+
"slackHomeChannel",
|
|
48
|
+
]);
|
|
49
|
+
const SAFE_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
50
|
+
const PRIVATE_REFERENCE = /^secret:\/\/[A-Za-z0-9/_-]+$/;
|
|
51
|
+
|
|
52
|
+
function validateRequest(request) {
|
|
53
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
54
|
+
return { ok: false, code: "invalid_request" };
|
|
55
|
+
}
|
|
56
|
+
const unknown = Object.keys(request).find((key) => !REQUEST_KEYS.has(key));
|
|
57
|
+
if (unknown) return { ok: false, code: "unknown_field", field: unknown };
|
|
58
|
+
if (request.action !== PROVISIONING_ACTION) {
|
|
59
|
+
return { ok: false, code: "unknown_action" };
|
|
60
|
+
}
|
|
61
|
+
for (const key of [
|
|
62
|
+
"workspaceId",
|
|
63
|
+
"agentId",
|
|
64
|
+
"desiredRevision",
|
|
65
|
+
"idempotencyKey",
|
|
66
|
+
]) {
|
|
67
|
+
if (typeof request[key] !== "string" || !SAFE_ID.test(request[key])) {
|
|
68
|
+
return { ok: false, code: "invalid_field", field: key };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!Number.isSafeInteger(request.generation) || request.generation < 1) {
|
|
72
|
+
return { ok: false, code: "invalid_field", field: "generation" };
|
|
73
|
+
}
|
|
74
|
+
if (!/^C[A-Z0-9]{8,20}$/.test(request.slackHomeChannel)) {
|
|
75
|
+
return { ok: false, code: "invalid_field", field: "slackHomeChannel" };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isInside(root, target) {
|
|
81
|
+
const rel = relative(root, target);
|
|
82
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function existingRealDirectory(pathValue) {
|
|
86
|
+
if (!isAbsolute(pathValue) || !existsSync(pathValue)) return null;
|
|
87
|
+
const link = lstatSync(pathValue);
|
|
88
|
+
if (link.isSymbolicLink() || !link.isDirectory()) return null;
|
|
89
|
+
return realpathSync(pathValue);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertDirectConfinedPath(profilesRoot, target, expectedBaseName) {
|
|
93
|
+
const configuredRoot = resolve(profilesRoot);
|
|
94
|
+
const realRoot = existingRealDirectory(configuredRoot);
|
|
95
|
+
if (!realRoot) return false;
|
|
96
|
+
const resolved = resolve(target);
|
|
97
|
+
if (
|
|
98
|
+
dirname(resolved) !== configuredRoot ||
|
|
99
|
+
basename(resolved) !== expectedBaseName ||
|
|
100
|
+
!isInside(configuredRoot, resolved)
|
|
101
|
+
) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (existsSync(resolved)) {
|
|
105
|
+
const link = lstatSync(resolved);
|
|
106
|
+
if (link.isSymbolicLink()) return false;
|
|
107
|
+
const real = realpathSync(resolved);
|
|
108
|
+
if (!isInside(realRoot, real) || dirname(real) !== realRoot) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveServerPaths(request, deps) {
|
|
116
|
+
try {
|
|
117
|
+
const profileId = deriveAgentProfileId(request.workspaceId, request.agentId);
|
|
118
|
+
const configuredProfilesRoot =
|
|
119
|
+
typeof deps?.profilesRoot === "string" ? resolve(deps.profilesRoot) : null;
|
|
120
|
+
const profilesRoot = existingRealDirectory(configuredProfilesRoot);
|
|
121
|
+
const runtimeHome = existingRealDirectory(deps?.runtimeHome);
|
|
122
|
+
if (!profilesRoot || !runtimeHome) {
|
|
123
|
+
return { ok: false, code: "invalid_server_configuration" };
|
|
124
|
+
}
|
|
125
|
+
const configuredProfile = deps?.profileRoots?.[profileId];
|
|
126
|
+
if (
|
|
127
|
+
typeof configuredProfile !== "string" ||
|
|
128
|
+
resolve(configuredProfile) !==
|
|
129
|
+
join(configuredProfilesRoot, profileId) ||
|
|
130
|
+
!assertDirectConfinedPath(
|
|
131
|
+
configuredProfilesRoot,
|
|
132
|
+
configuredProfile,
|
|
133
|
+
profileId
|
|
134
|
+
)
|
|
135
|
+
) {
|
|
136
|
+
return { ok: false, code: "path_confinement_failed" };
|
|
137
|
+
}
|
|
138
|
+
const profileRoot = resolve(configuredProfile);
|
|
139
|
+
const stagingBase = `${profileId}.staging-${request.idempotencyKey}`;
|
|
140
|
+
const backupBase = `${profileId}.rollback-${request.idempotencyKey}`;
|
|
141
|
+
const stagingRoot = join(configuredProfilesRoot, stagingBase);
|
|
142
|
+
const backupRoot = join(configuredProfilesRoot, backupBase);
|
|
143
|
+
if (
|
|
144
|
+
!assertDirectConfinedPath(configuredProfilesRoot, stagingRoot, stagingBase) ||
|
|
145
|
+
!assertDirectConfinedPath(configuredProfilesRoot, backupRoot, backupBase)
|
|
146
|
+
) {
|
|
147
|
+
return { ok: false, code: "path_confinement_failed" };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const credentialRoot = existingRealDirectory(deps?.credentialRoot);
|
|
151
|
+
const credentialFile = deps?.credentialFiles?.[profileId];
|
|
152
|
+
if (
|
|
153
|
+
!credentialRoot ||
|
|
154
|
+
typeof credentialFile !== "string" ||
|
|
155
|
+
!isAbsolute(credentialFile) ||
|
|
156
|
+
!existsSync(credentialFile)
|
|
157
|
+
) {
|
|
158
|
+
return { ok: false, code: "credential_confinement_failed" };
|
|
159
|
+
}
|
|
160
|
+
const credentialLink = lstatSync(credentialFile);
|
|
161
|
+
const credentialReal = credentialLink.isSymbolicLink()
|
|
162
|
+
? null
|
|
163
|
+
: realpathSync(credentialFile);
|
|
164
|
+
if (
|
|
165
|
+
!credentialReal ||
|
|
166
|
+
!credentialLink.isFile() ||
|
|
167
|
+
!isInside(credentialRoot, credentialReal) ||
|
|
168
|
+
(credentialLink.mode & 0o077) !== 0
|
|
169
|
+
) {
|
|
170
|
+
return { ok: false, code: "credential_confinement_failed" };
|
|
171
|
+
}
|
|
172
|
+
const serviceCredentialReference =
|
|
173
|
+
deps?.serviceCredentialReferences?.[profileId];
|
|
174
|
+
if (!PRIVATE_REFERENCE.test(serviceCredentialReference ?? "")) {
|
|
175
|
+
return { ok: false, code: "invalid_server_configuration" };
|
|
176
|
+
}
|
|
177
|
+
const credentialKeyVersion = deps?.credentialKeyVersions?.[profileId];
|
|
178
|
+
if (typeof credentialKeyVersion !== "string" || !SAFE_ID.test(credentialKeyVersion)) {
|
|
179
|
+
return { ok: false, code: "invalid_server_configuration" };
|
|
180
|
+
}
|
|
181
|
+
if (
|
|
182
|
+
!Number.isSafeInteger(deps.runtimeUid) ||
|
|
183
|
+
deps.runtimeUid < 1 ||
|
|
184
|
+
!Number.isSafeInteger(deps.runtimeGid) ||
|
|
185
|
+
deps.runtimeGid < 1
|
|
186
|
+
) {
|
|
187
|
+
return { ok: false, code: "invalid_server_configuration" };
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
profileId,
|
|
192
|
+
profilesRoot: configuredProfilesRoot,
|
|
193
|
+
profileRoot,
|
|
194
|
+
stagingRoot,
|
|
195
|
+
backupRoot,
|
|
196
|
+
credentialFile: resolve(credentialFile),
|
|
197
|
+
serviceCredentialReference,
|
|
198
|
+
credentialKeyVersion,
|
|
199
|
+
runtimeHome,
|
|
200
|
+
};
|
|
201
|
+
} catch {
|
|
202
|
+
return { ok: false, code: "path_confinement_failed" };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function detectPinnedInstallerCapabilities() {
|
|
207
|
+
try {
|
|
208
|
+
const packageJson = JSON.parse(readFileSync(INSTALLER_PACKAGE_JSON, "utf8"));
|
|
209
|
+
const source = readFileSync(INSTALLER_BIN, "utf8");
|
|
210
|
+
const installVersion = packageJson.version;
|
|
211
|
+
const capabilities = {
|
|
212
|
+
hermesProfileBootstrap:
|
|
213
|
+
source.includes('rawArgs[0] === "hermes"') &&
|
|
214
|
+
source.includes('rawArgs[2] === "bootstrap"'),
|
|
215
|
+
tokenFile: source.includes('arg === "--token-file"'),
|
|
216
|
+
profileRoot: source.includes('arg === "--profile-root"'),
|
|
217
|
+
workspaceLock: source.includes('arg === "--require-workspace-lock"'),
|
|
218
|
+
jsonReceipt: source.includes('arg === "--json"'),
|
|
219
|
+
};
|
|
220
|
+
if (
|
|
221
|
+
`${packageJson.name}@${installVersion}` !== PINNED_INSTALL_PACKAGE ||
|
|
222
|
+
Object.values(capabilities).some((value) => value !== true)
|
|
223
|
+
) {
|
|
224
|
+
return { ok: false, code: "installer_capability_mismatch" };
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
installPackage: PINNED_INSTALL_PACKAGE,
|
|
229
|
+
installVersion,
|
|
230
|
+
mcpPackage: PINNED_MCP_PACKAGE,
|
|
231
|
+
capabilities,
|
|
232
|
+
};
|
|
233
|
+
} catch {
|
|
234
|
+
return { ok: false, code: "installer_capability_mismatch" };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function hasExactKeys(value, keys) {
|
|
239
|
+
return (
|
|
240
|
+
value &&
|
|
241
|
+
typeof value === "object" &&
|
|
242
|
+
!Array.isArray(value) &&
|
|
243
|
+
Object.keys(value).sort().join("\0") === [...keys].sort().join("\0")
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function isStringArray(value) {
|
|
248
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function validateInstallerAttestation({ stdout, expected }) {
|
|
252
|
+
const failed = { ok: false, code: "installer_attestation_failed" };
|
|
253
|
+
try {
|
|
254
|
+
const parsed = JSON.parse(stdout);
|
|
255
|
+
const stagingRoot = resolve(expected.stagingRoot);
|
|
256
|
+
const expectedPaths = {
|
|
257
|
+
profileRoot: stagingRoot,
|
|
258
|
+
hermesConfigPath: join(stagingRoot, "config.yaml"),
|
|
259
|
+
envPath: join(stagingRoot, ".env"),
|
|
260
|
+
sellableConfigPath: join(stagingRoot, "sellable", "config.json"),
|
|
261
|
+
sellableConfigsDir: join(stagingRoot, "sellable", "configs"),
|
|
262
|
+
skillsRoot: join(stagingRoot, "skills", "sellable"),
|
|
263
|
+
};
|
|
264
|
+
if (
|
|
265
|
+
!hasExactKeys(parsed, [
|
|
266
|
+
"ok",
|
|
267
|
+
"command",
|
|
268
|
+
"profile",
|
|
269
|
+
"dryRun",
|
|
270
|
+
"paths",
|
|
271
|
+
"auth",
|
|
272
|
+
"slack",
|
|
273
|
+
"created",
|
|
274
|
+
"updated",
|
|
275
|
+
"preserved",
|
|
276
|
+
"skipped",
|
|
277
|
+
"warnings",
|
|
278
|
+
]) ||
|
|
279
|
+
parsed.ok !== true ||
|
|
280
|
+
parsed.command !== "hermes profile bootstrap" ||
|
|
281
|
+
parsed.profile !== expected.profileId ||
|
|
282
|
+
parsed.dryRun !== false ||
|
|
283
|
+
!hasExactKeys(parsed.paths, Object.keys(expectedPaths)) ||
|
|
284
|
+
Object.entries(expectedPaths).some(([key, value]) => parsed.paths[key] !== value) ||
|
|
285
|
+
!hasExactKeys(parsed.auth, [
|
|
286
|
+
"apiUrl",
|
|
287
|
+
"activeWorkspaceId",
|
|
288
|
+
"activeWorkspaceName",
|
|
289
|
+
"workspaceLock",
|
|
290
|
+
"tokenPresent",
|
|
291
|
+
"tokenSource",
|
|
292
|
+
"tokenFingerprint",
|
|
293
|
+
]) ||
|
|
294
|
+
parsed.auth.apiUrl !== "https://app.sellable.dev" ||
|
|
295
|
+
parsed.auth.activeWorkspaceId !== expected.workspaceId ||
|
|
296
|
+
![null, undefined].includes(parsed.auth.activeWorkspaceName) ||
|
|
297
|
+
!hasExactKeys(parsed.auth.workspaceLock, ["enabled", "workspaceId", "required"]) ||
|
|
298
|
+
parsed.auth.workspaceLock.enabled !== true ||
|
|
299
|
+
parsed.auth.workspaceLock.workspaceId !== expected.workspaceId ||
|
|
300
|
+
parsed.auth.workspaceLock.required !== true ||
|
|
301
|
+
parsed.auth.tokenPresent !== true ||
|
|
302
|
+
parsed.auth.tokenSource !== "token-file" ||
|
|
303
|
+
!/^[a-f0-9]{16}$/.test(parsed.auth.tokenFingerprint) ||
|
|
304
|
+
!hasExactKeys(parsed.slack, ["keys", "envKeys", "fingerprints", "scope"]) ||
|
|
305
|
+
!Array.isArray(parsed.slack.keys) ||
|
|
306
|
+
parsed.slack.keys.length !== 0 ||
|
|
307
|
+
JSON.stringify(parsed.slack.envKeys) !==
|
|
308
|
+
JSON.stringify(["SLACK_HOME_CHANNEL", "SLACK_ALLOWED_CHANNELS"]) ||
|
|
309
|
+
!hasExactKeys(parsed.slack.fingerprints, []) ||
|
|
310
|
+
!hasExactKeys(parsed.slack.scope, [
|
|
311
|
+
"homeChannel",
|
|
312
|
+
"homeChannelName",
|
|
313
|
+
"allowedChannels",
|
|
314
|
+
"freeResponseChannels",
|
|
315
|
+
"allowedUsersCount",
|
|
316
|
+
"requireMention",
|
|
317
|
+
]) ||
|
|
318
|
+
parsed.slack.scope.homeChannel !== expected.slackHomeChannel ||
|
|
319
|
+
parsed.slack.scope.homeChannelName !== null ||
|
|
320
|
+
JSON.stringify(parsed.slack.scope.allowedChannels) !==
|
|
321
|
+
JSON.stringify([expected.slackHomeChannel]) ||
|
|
322
|
+
!Array.isArray(parsed.slack.scope.freeResponseChannels) ||
|
|
323
|
+
parsed.slack.scope.freeResponseChannels.length !== 0 ||
|
|
324
|
+
parsed.slack.scope.allowedUsersCount !== 0 ||
|
|
325
|
+
parsed.slack.scope.requireMention !== null ||
|
|
326
|
+
!isStringArray(parsed.created) ||
|
|
327
|
+
!isStringArray(parsed.updated) ||
|
|
328
|
+
!isStringArray(parsed.preserved) ||
|
|
329
|
+
!isStringArray(parsed.skipped) ||
|
|
330
|
+
!isStringArray(parsed.warnings)
|
|
331
|
+
) {
|
|
332
|
+
return failed;
|
|
333
|
+
}
|
|
334
|
+
const allowedMaterializedPaths = new Set(Object.values(expectedPaths).slice(1));
|
|
335
|
+
if (
|
|
336
|
+
[...parsed.created, ...parsed.updated, ...parsed.preserved].some(
|
|
337
|
+
(value) => !allowedMaterializedPaths.has(value)
|
|
338
|
+
) ||
|
|
339
|
+
/xox[bap]-|sat_[^\s"']+|authorization[_-]?code|access[_-]?token|refresh[_-]?token/i.test(
|
|
340
|
+
stdout
|
|
341
|
+
)
|
|
342
|
+
) {
|
|
343
|
+
return failed;
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
attestation: {
|
|
348
|
+
credentialFingerprint: parsed.auth.tokenFingerprint,
|
|
349
|
+
tokenSource: "token-file",
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
} catch {
|
|
353
|
+
return failed;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function buildProvisioningInvocation(request, deps) {
|
|
358
|
+
const valid = validateRequest(request);
|
|
359
|
+
if (!valid.ok) return valid;
|
|
360
|
+
const paths = resolveServerPaths(request, deps);
|
|
361
|
+
if (!paths.ok) return paths;
|
|
362
|
+
return {
|
|
363
|
+
ok: true,
|
|
364
|
+
invocation: {
|
|
365
|
+
package: PINNED_INSTALL_PACKAGE,
|
|
366
|
+
mcpPackage: PINNED_MCP_PACKAGE,
|
|
367
|
+
command: process.execPath,
|
|
368
|
+
args: [
|
|
369
|
+
INSTALLER_BIN,
|
|
370
|
+
"hermes",
|
|
371
|
+
"profile",
|
|
372
|
+
"bootstrap",
|
|
373
|
+
"--profile-root",
|
|
374
|
+
paths.stagingRoot,
|
|
375
|
+
"--profile",
|
|
376
|
+
paths.profileId,
|
|
377
|
+
"--workspace-id",
|
|
378
|
+
request.workspaceId,
|
|
379
|
+
"--token-file",
|
|
380
|
+
paths.credentialFile,
|
|
381
|
+
"--slack-home-channel",
|
|
382
|
+
request.slackHomeChannel,
|
|
383
|
+
"--api-url",
|
|
384
|
+
"https://app.sellable.dev",
|
|
385
|
+
"--server",
|
|
386
|
+
"package",
|
|
387
|
+
"--mcp-package",
|
|
388
|
+
PINNED_MCP_PACKAGE,
|
|
389
|
+
"--require-workspace-lock",
|
|
390
|
+
"--json",
|
|
391
|
+
],
|
|
392
|
+
env: {
|
|
393
|
+
HOME: paths.runtimeHome,
|
|
394
|
+
NODE_ENV: "production",
|
|
395
|
+
PATH: deps.runtimeEnv?.PATH || "/usr/bin:/bin",
|
|
396
|
+
},
|
|
397
|
+
shell: false,
|
|
398
|
+
timeoutMs: 60_000,
|
|
399
|
+
maxOutputBytes: 64 * 1024,
|
|
400
|
+
uid: deps.runtimeUid,
|
|
401
|
+
gid: deps.runtimeGid,
|
|
402
|
+
profileRoot: paths.stagingRoot,
|
|
403
|
+
},
|
|
404
|
+
paths,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function runBoundedChildProcess(invocation) {
|
|
409
|
+
return new Promise((resolveResult) => {
|
|
410
|
+
let stdout = Buffer.alloc(0);
|
|
411
|
+
let stderr = Buffer.alloc(0);
|
|
412
|
+
let outputBytes = 0;
|
|
413
|
+
let timedOut = false;
|
|
414
|
+
let outputLimitExceeded = false;
|
|
415
|
+
let terminationReason = null;
|
|
416
|
+
let settled = false;
|
|
417
|
+
const finish = (exitCode, signal, spawnFailed = false) => {
|
|
418
|
+
if (settled) return;
|
|
419
|
+
settled = true;
|
|
420
|
+
clearTimeout(timer);
|
|
421
|
+
resolveResult({
|
|
422
|
+
exitCode,
|
|
423
|
+
signal,
|
|
424
|
+
stdout: stdout.toString("utf8"),
|
|
425
|
+
stderr: stderr.toString("utf8"),
|
|
426
|
+
outputBytes,
|
|
427
|
+
timedOut,
|
|
428
|
+
outputLimitExceeded,
|
|
429
|
+
terminated: timedOut || outputLimitExceeded,
|
|
430
|
+
terminationReason:
|
|
431
|
+
terminationReason ?? (spawnFailed ? "spawn_failed" : null),
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
let child;
|
|
435
|
+
try {
|
|
436
|
+
child = spawn(invocation.command, invocation.args, {
|
|
437
|
+
env: invocation.env,
|
|
438
|
+
shell: false,
|
|
439
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
440
|
+
...(Number.isSafeInteger(invocation.uid) ? { uid: invocation.uid } : {}),
|
|
441
|
+
...(Number.isSafeInteger(invocation.gid) ? { gid: invocation.gid } : {}),
|
|
442
|
+
});
|
|
443
|
+
} catch {
|
|
444
|
+
resolveResult({
|
|
445
|
+
exitCode: null,
|
|
446
|
+
signal: null,
|
|
447
|
+
stdout: "",
|
|
448
|
+
stderr: "",
|
|
449
|
+
outputBytes: 0,
|
|
450
|
+
timedOut: false,
|
|
451
|
+
outputLimitExceeded: false,
|
|
452
|
+
terminated: false,
|
|
453
|
+
terminationReason: "spawn_failed",
|
|
454
|
+
});
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const capture = (target, chunk) => {
|
|
458
|
+
const remaining = Math.max(0, invocation.maxOutputBytes - outputBytes);
|
|
459
|
+
const kept = chunk.subarray(0, remaining);
|
|
460
|
+
if (target === "stdout") stdout = Buffer.concat([stdout, kept]);
|
|
461
|
+
else stderr = Buffer.concat([stderr, kept]);
|
|
462
|
+
outputBytes += kept.length;
|
|
463
|
+
if (chunk.length > remaining && !outputLimitExceeded) {
|
|
464
|
+
outputLimitExceeded = true;
|
|
465
|
+
terminationReason = "output_limit";
|
|
466
|
+
child.kill("SIGKILL");
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
child.stdout.on("data", (chunk) => capture("stdout", Buffer.from(chunk)));
|
|
470
|
+
child.stderr.on("data", (chunk) => capture("stderr", Buffer.from(chunk)));
|
|
471
|
+
child.once("error", () => finish(null, null, true));
|
|
472
|
+
child.once("close", (code, signal) => finish(code, signal));
|
|
473
|
+
const timer = setTimeout(() => {
|
|
474
|
+
timedOut = true;
|
|
475
|
+
terminationReason = "timeout";
|
|
476
|
+
child.kill("SIGKILL");
|
|
477
|
+
}, invocation.timeoutMs);
|
|
478
|
+
timer.unref?.();
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function matchesReadback(observed, request) {
|
|
483
|
+
const profileId = deriveAgentProfileId(request.workspaceId, request.agentId);
|
|
484
|
+
return (
|
|
485
|
+
observed?.profileId === profileId &&
|
|
486
|
+
observed?.workspaceId === request.workspaceId &&
|
|
487
|
+
observed?.agentId === request.agentId &&
|
|
488
|
+
observed?.generation === request.generation &&
|
|
489
|
+
observed?.revision === request.desiredRevision &&
|
|
490
|
+
observed?.idempotencyKey === request.idempotencyKey
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function sha256(value) {
|
|
495
|
+
return createHash("sha256").update(value).digest("hex");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function stableSerialize(value) {
|
|
499
|
+
if (Array.isArray(value)) {
|
|
500
|
+
return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
|
|
501
|
+
}
|
|
502
|
+
if (value && typeof value === "object") {
|
|
503
|
+
return `{${Object.keys(value)
|
|
504
|
+
.sort()
|
|
505
|
+
.map((key) => `${JSON.stringify(key)}:${stableSerialize(value[key])}`)
|
|
506
|
+
.join(",")}}`;
|
|
507
|
+
}
|
|
508
|
+
return JSON.stringify(value);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function hashProfileTree(root, { excludeManifest = false } = {}) {
|
|
512
|
+
const digest = createHash("sha256");
|
|
513
|
+
const walk = (pathValue, rel) => {
|
|
514
|
+
const link = lstatSync(pathValue);
|
|
515
|
+
if (link.isSymbolicLink()) throw new Error("symlink in profile");
|
|
516
|
+
const mode = (link.mode & 0o777).toString(8).padStart(3, "0");
|
|
517
|
+
if (link.isDirectory()) {
|
|
518
|
+
digest.update(`D\0${rel}\0${mode}\n`);
|
|
519
|
+
for (const name of readdirSync(pathValue).sort()) {
|
|
520
|
+
const childRel = rel ? `${rel}/${name}` : name;
|
|
521
|
+
if (excludeManifest && childRel === "agent-profile.json") continue;
|
|
522
|
+
walk(join(pathValue, name), childRel);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (!link.isFile()) throw new Error("unsupported profile entry");
|
|
527
|
+
digest.update(`F\0${rel}\0${mode}\0${link.size}\n`);
|
|
528
|
+
digest.update(readFileSync(pathValue));
|
|
529
|
+
digest.update("\n");
|
|
530
|
+
};
|
|
531
|
+
walk(root, "");
|
|
532
|
+
return digest.digest("hex");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function inspectProvisionedProfile({
|
|
536
|
+
profileRoot,
|
|
537
|
+
request,
|
|
538
|
+
serviceCredentialReference,
|
|
539
|
+
credentialKeyVersion,
|
|
540
|
+
}) {
|
|
541
|
+
try {
|
|
542
|
+
const manifestPath = join(profileRoot, "agent-profile.json");
|
|
543
|
+
const manifestLink = lstatSync(manifestPath);
|
|
544
|
+
if (
|
|
545
|
+
manifestLink.isSymbolicLink() ||
|
|
546
|
+
!manifestLink.isFile() ||
|
|
547
|
+
(manifestLink.mode & 0o777) !== 0o600
|
|
548
|
+
) {
|
|
549
|
+
return { ok: false, code: "manifest_mode_invalid" };
|
|
550
|
+
}
|
|
551
|
+
const manifestBytes = readFileSync(manifestPath);
|
|
552
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
553
|
+
if (
|
|
554
|
+
!hasExactKeys(manifest, [
|
|
555
|
+
"profileId",
|
|
556
|
+
"workspaceId",
|
|
557
|
+
"agentId",
|
|
558
|
+
"generation",
|
|
559
|
+
"revision",
|
|
560
|
+
"idempotencyKey",
|
|
561
|
+
"serviceCredentialReference",
|
|
562
|
+
"installerPackage",
|
|
563
|
+
"installerVersion",
|
|
564
|
+
"mcpPackage",
|
|
565
|
+
"credentialFingerprint",
|
|
566
|
+
"credentialKeyVersion",
|
|
567
|
+
"profileContentDigest",
|
|
568
|
+
"producer",
|
|
569
|
+
"subject",
|
|
570
|
+
]) ||
|
|
571
|
+
!matchesReadback(manifest, request) ||
|
|
572
|
+
manifest.serviceCredentialReference !== serviceCredentialReference ||
|
|
573
|
+
manifest.installerPackage !== PINNED_INSTALL_PACKAGE ||
|
|
574
|
+
manifest.installerVersion !== "0.1.356" ||
|
|
575
|
+
manifest.mcpPackage !== PINNED_MCP_PACKAGE ||
|
|
576
|
+
manifest.credentialKeyVersion !== credentialKeyVersion ||
|
|
577
|
+
!/^[a-f0-9]{16}$/.test(manifest.credentialFingerprint) ||
|
|
578
|
+
!/^[a-f0-9]{64}$/.test(manifest.profileContentDigest) ||
|
|
579
|
+
manifest.producer !== "sellable-agent-provisioning-adapter" ||
|
|
580
|
+
manifest.subject !== `agent-profile:${manifest.profileId}`
|
|
581
|
+
) {
|
|
582
|
+
return { ok: false, code: "manifest_binding_invalid" };
|
|
583
|
+
}
|
|
584
|
+
const authPath = join(profileRoot, "sellable", "config.json");
|
|
585
|
+
const authLink = lstatSync(authPath);
|
|
586
|
+
if (
|
|
587
|
+
authLink.isSymbolicLink() ||
|
|
588
|
+
!authLink.isFile() ||
|
|
589
|
+
(authLink.mode & 0o777) !== 0o600
|
|
590
|
+
) {
|
|
591
|
+
return { ok: false, code: "auth_mode_invalid" };
|
|
592
|
+
}
|
|
593
|
+
const envPath = join(profileRoot, ".env");
|
|
594
|
+
if (existsSync(envPath)) {
|
|
595
|
+
const envLink = lstatSync(envPath);
|
|
596
|
+
if (
|
|
597
|
+
envLink.isSymbolicLink() ||
|
|
598
|
+
!envLink.isFile() ||
|
|
599
|
+
(envLink.mode & 0o777) !== 0o600
|
|
600
|
+
) {
|
|
601
|
+
return { ok: false, code: "env_mode_invalid" };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
605
|
+
const fingerprint =
|
|
606
|
+
typeof auth.token === "string" ? sha256(auth.token).slice(0, 16) : null;
|
|
607
|
+
if (
|
|
608
|
+
typeof auth.token !== "string" ||
|
|
609
|
+
!auth.token.startsWith("sat_") ||
|
|
610
|
+
auth.activeWorkspaceId !== request.workspaceId ||
|
|
611
|
+
auth.workspaceId !== request.workspaceId ||
|
|
612
|
+
auth.bootstrap?.profile !== manifest.profileId ||
|
|
613
|
+
fingerprint !== manifest.credentialFingerprint
|
|
614
|
+
) {
|
|
615
|
+
return { ok: false, code: "auth_binding_invalid" };
|
|
616
|
+
}
|
|
617
|
+
const contentDigest = hashProfileTree(profileRoot, { excludeManifest: true });
|
|
618
|
+
if (contentDigest !== manifest.profileContentDigest) {
|
|
619
|
+
return { ok: false, code: "profile_content_digest_mismatch" };
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
ok: true,
|
|
623
|
+
observed: manifest,
|
|
624
|
+
credential: { fingerprint, keyVersion: credentialKeyVersion },
|
|
625
|
+
digests: {
|
|
626
|
+
current: hashProfileTree(profileRoot),
|
|
627
|
+
manifest: sha256(manifestBytes),
|
|
628
|
+
content: contentDigest,
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
} catch {
|
|
632
|
+
return { ok: false, code: "profile_inspection_failed" };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function safeRemoveDirect(profilesRoot, target) {
|
|
637
|
+
const targetBase = basename(target);
|
|
638
|
+
if (!assertDirectConfinedPath(profilesRoot, target, targetBase)) {
|
|
639
|
+
throw new Error("confined removal rejected");
|
|
640
|
+
}
|
|
641
|
+
if (existsSync(target)) rmSync(target, { recursive: true, force: true });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function fsyncPath(pathValue) {
|
|
645
|
+
const descriptor = openSync(pathValue, "r");
|
|
646
|
+
try {
|
|
647
|
+
fsyncSync(descriptor);
|
|
648
|
+
} finally {
|
|
649
|
+
closeSync(descriptor);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function fsyncTree(root) {
|
|
654
|
+
const link = lstatSync(root);
|
|
655
|
+
if (link.isSymbolicLink()) throw new Error("symlink in staged profile");
|
|
656
|
+
if (link.isDirectory()) {
|
|
657
|
+
for (const name of readdirSync(root)) fsyncTree(join(root, name));
|
|
658
|
+
}
|
|
659
|
+
fsyncPath(root);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function rollbackPromotedProfile({ profilesRoot, profileRoot, backupRoot }) {
|
|
663
|
+
if (existsSync(profileRoot)) safeRemoveDirect(profilesRoot, profileRoot);
|
|
664
|
+
if (existsSync(backupRoot)) renameSync(backupRoot, profileRoot);
|
|
665
|
+
fsyncPath(profilesRoot);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export async function atomicPromoteProfile({
|
|
669
|
+
profilesRoot,
|
|
670
|
+
profileRoot,
|
|
671
|
+
stagingRoot,
|
|
672
|
+
backupRoot,
|
|
673
|
+
}) {
|
|
674
|
+
const configuredProfilesRoot = resolve(profilesRoot);
|
|
675
|
+
const realProfilesRoot = existingRealDirectory(configuredProfilesRoot);
|
|
676
|
+
if (!realProfilesRoot) throw new Error("profile promotion confinement failed");
|
|
677
|
+
for (const target of [profileRoot, stagingRoot, backupRoot]) {
|
|
678
|
+
if (
|
|
679
|
+
!assertDirectConfinedPath(
|
|
680
|
+
configuredProfilesRoot,
|
|
681
|
+
target,
|
|
682
|
+
basename(target)
|
|
683
|
+
)
|
|
684
|
+
) {
|
|
685
|
+
throw new Error("profile promotion confinement failed");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (existsSync(stagingRoot)) fsyncTree(stagingRoot);
|
|
689
|
+
if (existsSync(backupRoot))
|
|
690
|
+
safeRemoveDirect(configuredProfilesRoot, backupRoot);
|
|
691
|
+
const hadPrevious = existsSync(profileRoot);
|
|
692
|
+
if (hadPrevious) renameSync(profileRoot, backupRoot);
|
|
693
|
+
try {
|
|
694
|
+
renameSync(stagingRoot, profileRoot);
|
|
695
|
+
fsyncPath(configuredProfilesRoot);
|
|
696
|
+
return { hadPrevious };
|
|
697
|
+
} catch {
|
|
698
|
+
if (existsSync(profileRoot))
|
|
699
|
+
safeRemoveDirect(configuredProfilesRoot, profileRoot);
|
|
700
|
+
if (hadPrevious && existsSync(backupRoot)) renameSync(backupRoot, profileRoot);
|
|
701
|
+
fsyncPath(configuredProfilesRoot);
|
|
702
|
+
throw new Error("profile promotion failed; prior profile restored");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export async function promoteProfileWithReadback(input) {
|
|
707
|
+
let promotion;
|
|
708
|
+
try {
|
|
709
|
+
promotion = await atomicPromoteProfile(input);
|
|
710
|
+
} catch {
|
|
711
|
+
return { ok: false, code: "promotion_failed" };
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
const observed = JSON.parse(
|
|
715
|
+
readFileSync(join(input.profileRoot, "agent-profile.json"), "utf8")
|
|
716
|
+
);
|
|
717
|
+
if (!matchesReadback(observed, input.request)) {
|
|
718
|
+
await rollbackPromotedProfile(input);
|
|
719
|
+
return {
|
|
720
|
+
ok: false,
|
|
721
|
+
code: "readback_mismatch",
|
|
722
|
+
rolledBack: true,
|
|
723
|
+
rollback: { outcome: promotion.hadPrevious ? "RESTORED" : "REMOVED" },
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (input.expectedInspection) {
|
|
727
|
+
const inspection = inspectProvisionedProfile({
|
|
728
|
+
profileRoot: input.profileRoot,
|
|
729
|
+
request: input.request,
|
|
730
|
+
serviceCredentialReference: input.expectedInspection.serviceCredentialReference,
|
|
731
|
+
credentialKeyVersion: input.expectedInspection.credentialKeyVersion,
|
|
732
|
+
});
|
|
733
|
+
if (
|
|
734
|
+
!inspection.ok ||
|
|
735
|
+
inspection.digests.current !== input.expectedInspection.currentDigest
|
|
736
|
+
) {
|
|
737
|
+
await rollbackPromotedProfile(input);
|
|
738
|
+
return {
|
|
739
|
+
ok: false,
|
|
740
|
+
code: "readback_mismatch",
|
|
741
|
+
rolledBack: true,
|
|
742
|
+
rollback: { outcome: promotion.hadPrevious ? "RESTORED" : "REMOVED" },
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
return { ok: true, observed, inspection };
|
|
746
|
+
}
|
|
747
|
+
return { ok: true, observed };
|
|
748
|
+
} catch {
|
|
749
|
+
await rollbackPromotedProfile(input);
|
|
750
|
+
return {
|
|
751
|
+
ok: false,
|
|
752
|
+
code: "readback_failed",
|
|
753
|
+
rolledBack: true,
|
|
754
|
+
rollback: { outcome: promotion.hadPrevious ? "RESTORED" : "REMOVED" },
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function finalizeReceipt(receipt) {
|
|
760
|
+
return { ...receipt, receiptDigest: sha256(stableSerialize(receipt)) };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function successReceipt({
|
|
764
|
+
request,
|
|
765
|
+
stages,
|
|
766
|
+
inspection,
|
|
767
|
+
previousDigest,
|
|
768
|
+
outputBytes,
|
|
769
|
+
reused = false,
|
|
770
|
+
}) {
|
|
771
|
+
const observed = inspection.observed;
|
|
772
|
+
return finalizeReceipt({
|
|
773
|
+
ok: true,
|
|
774
|
+
action: PROVISIONING_ACTION,
|
|
775
|
+
producer: "sellable-agent-provisioning-adapter",
|
|
776
|
+
subject: `agent-profile:${observed.profileId}`,
|
|
777
|
+
cli: {
|
|
778
|
+
installPackage: PINNED_INSTALL_PACKAGE,
|
|
779
|
+
installVersion: "0.1.356",
|
|
780
|
+
mcpPackage: PINNED_MCP_PACKAGE,
|
|
781
|
+
command: "hermes profile bootstrap",
|
|
782
|
+
},
|
|
783
|
+
stages,
|
|
784
|
+
observed: {
|
|
785
|
+
profileId: observed.profileId,
|
|
786
|
+
workspaceId: observed.workspaceId,
|
|
787
|
+
agentId: observed.agentId,
|
|
788
|
+
generation: observed.generation,
|
|
789
|
+
revision: observed.revision,
|
|
790
|
+
},
|
|
791
|
+
credential: inspection.credential,
|
|
792
|
+
digests: {
|
|
793
|
+
previous: previousDigest,
|
|
794
|
+
current: inspection.digests.current,
|
|
795
|
+
readback: inspection.digests.current,
|
|
796
|
+
manifest: inspection.digests.manifest,
|
|
797
|
+
content: inspection.digests.content,
|
|
798
|
+
},
|
|
799
|
+
rollback: { outcome: "NOT_REQUIRED" },
|
|
800
|
+
idempotencyKey: request.idempotencyKey,
|
|
801
|
+
outputBytes,
|
|
802
|
+
reused,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function failure(code, stages, extra = {}, request = null) {
|
|
807
|
+
const safeIdentity =
|
|
808
|
+
request && SAFE_ID.test(request.workspaceId ?? "") && SAFE_ID.test(request.agentId ?? "")
|
|
809
|
+
? deriveAgentProfileId(request.workspaceId, request.agentId)
|
|
810
|
+
: null;
|
|
811
|
+
return finalizeReceipt({
|
|
812
|
+
ok: false,
|
|
813
|
+
code,
|
|
814
|
+
action: PROVISIONING_ACTION,
|
|
815
|
+
producer: "sellable-agent-provisioning-adapter",
|
|
816
|
+
subject: safeIdentity ? `agent-profile:${safeIdentity}` : "agent-profile:unbound",
|
|
817
|
+
cli: {
|
|
818
|
+
installPackage: PINNED_INSTALL_PACKAGE,
|
|
819
|
+
installVersion: "0.1.356",
|
|
820
|
+
mcpPackage: PINNED_MCP_PACKAGE,
|
|
821
|
+
command: "hermes profile bootstrap",
|
|
822
|
+
},
|
|
823
|
+
stages,
|
|
824
|
+
rollback: extra.rollback ?? { outcome: "NOT_APPLICABLE" },
|
|
825
|
+
...(extra.terminated === true
|
|
826
|
+
? {
|
|
827
|
+
terminated: true,
|
|
828
|
+
terminationReason: extra.terminationReason,
|
|
829
|
+
}
|
|
830
|
+
: {}),
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export async function runProvisioningTransaction(request, deps) {
|
|
835
|
+
const built = buildProvisioningInvocation(request, deps);
|
|
836
|
+
if (!built.ok) return { ...built, stages: [] };
|
|
837
|
+
const { paths, invocation } = built;
|
|
838
|
+
const stages = [];
|
|
839
|
+
const capability = detectPinnedInstallerCapabilities();
|
|
840
|
+
stages.push("capability");
|
|
841
|
+
if (!capability.ok) return failure(capability.code, stages, {}, request);
|
|
842
|
+
|
|
843
|
+
let previousDigest = null;
|
|
844
|
+
if (existsSync(paths.profileRoot)) {
|
|
845
|
+
try {
|
|
846
|
+
previousDigest = hashProfileTree(paths.profileRoot);
|
|
847
|
+
} catch {
|
|
848
|
+
previousDigest = null;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (existsSync(join(paths.profileRoot, "agent-profile.json"))) {
|
|
853
|
+
const current = inspectProvisionedProfile({
|
|
854
|
+
profileRoot: paths.profileRoot,
|
|
855
|
+
request,
|
|
856
|
+
serviceCredentialReference: paths.serviceCredentialReference,
|
|
857
|
+
credentialKeyVersion: paths.credentialKeyVersion,
|
|
858
|
+
});
|
|
859
|
+
if (current.ok) {
|
|
860
|
+
stages.push("readback");
|
|
861
|
+
return successReceipt({
|
|
862
|
+
request,
|
|
863
|
+
stages,
|
|
864
|
+
inspection: current,
|
|
865
|
+
previousDigest: current.digests.current,
|
|
866
|
+
outputBytes: 0,
|
|
867
|
+
reused: true,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
try {
|
|
873
|
+
safeRemoveDirect(paths.profilesRoot, paths.stagingRoot);
|
|
874
|
+
safeRemoveDirect(paths.profilesRoot, paths.backupRoot);
|
|
875
|
+
stages.push("stage");
|
|
876
|
+
const output = await runBoundedChildProcess(invocation);
|
|
877
|
+
if (output.timedOut) {
|
|
878
|
+
safeRemoveDirect(paths.profilesRoot, paths.stagingRoot);
|
|
879
|
+
return failure("installer_timeout", stages, {
|
|
880
|
+
terminated: output.terminated,
|
|
881
|
+
terminationReason: output.terminationReason,
|
|
882
|
+
}, request);
|
|
883
|
+
}
|
|
884
|
+
if (output.outputLimitExceeded) {
|
|
885
|
+
safeRemoveDirect(paths.profilesRoot, paths.stagingRoot);
|
|
886
|
+
return failure("installer_output_limit", stages, {
|
|
887
|
+
terminated: output.terminated,
|
|
888
|
+
terminationReason: output.terminationReason,
|
|
889
|
+
}, request);
|
|
890
|
+
}
|
|
891
|
+
if (output.exitCode !== 0) {
|
|
892
|
+
safeRemoveDirect(paths.profilesRoot, paths.stagingRoot);
|
|
893
|
+
return failure("installer_failed", stages, {}, request);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
stages.push("attest");
|
|
897
|
+
const attestation = validateInstallerAttestation({
|
|
898
|
+
stdout: output.stdout,
|
|
899
|
+
expected: {
|
|
900
|
+
profileId: paths.profileId,
|
|
901
|
+
stagingRoot: paths.stagingRoot,
|
|
902
|
+
workspaceId: request.workspaceId,
|
|
903
|
+
slackHomeChannel: request.slackHomeChannel,
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
if (!attestation.ok) {
|
|
907
|
+
safeRemoveDirect(paths.profilesRoot, paths.stagingRoot);
|
|
908
|
+
return failure(attestation.code, stages, {}, request);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const profileContentDigest = hashProfileTree(paths.stagingRoot, {
|
|
912
|
+
excludeManifest: true,
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
const manifest = {
|
|
916
|
+
profileId: paths.profileId,
|
|
917
|
+
workspaceId: request.workspaceId,
|
|
918
|
+
agentId: request.agentId,
|
|
919
|
+
generation: request.generation,
|
|
920
|
+
revision: request.desiredRevision,
|
|
921
|
+
idempotencyKey: request.idempotencyKey,
|
|
922
|
+
serviceCredentialReference: paths.serviceCredentialReference,
|
|
923
|
+
installerPackage: PINNED_INSTALL_PACKAGE,
|
|
924
|
+
installerVersion: capability.installVersion,
|
|
925
|
+
mcpPackage: PINNED_MCP_PACKAGE,
|
|
926
|
+
credentialFingerprint: attestation.attestation.credentialFingerprint,
|
|
927
|
+
credentialKeyVersion: paths.credentialKeyVersion,
|
|
928
|
+
profileContentDigest,
|
|
929
|
+
producer: "sellable-agent-provisioning-adapter",
|
|
930
|
+
subject: `agent-profile:${paths.profileId}`,
|
|
931
|
+
};
|
|
932
|
+
writeFileSync(
|
|
933
|
+
join(paths.stagingRoot, "agent-profile.json"),
|
|
934
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
935
|
+
{ mode: 0o600 }
|
|
936
|
+
);
|
|
937
|
+
stages.push("validate");
|
|
938
|
+
const staged = inspectProvisionedProfile({
|
|
939
|
+
profileRoot: paths.stagingRoot,
|
|
940
|
+
request,
|
|
941
|
+
serviceCredentialReference: paths.serviceCredentialReference,
|
|
942
|
+
credentialKeyVersion: paths.credentialKeyVersion,
|
|
943
|
+
});
|
|
944
|
+
if (!staged.ok) {
|
|
945
|
+
safeRemoveDirect(paths.profilesRoot, paths.stagingRoot);
|
|
946
|
+
return failure("staged_validation_failed", stages, {}, request);
|
|
947
|
+
}
|
|
948
|
+
fsyncTree(paths.stagingRoot);
|
|
949
|
+
stages.push("promote");
|
|
950
|
+
const promoted = await promoteProfileWithReadback({
|
|
951
|
+
profilesRoot: paths.profilesRoot,
|
|
952
|
+
profileRoot: paths.profileRoot,
|
|
953
|
+
stagingRoot: paths.stagingRoot,
|
|
954
|
+
backupRoot: paths.backupRoot,
|
|
955
|
+
request,
|
|
956
|
+
expectedInspection: {
|
|
957
|
+
serviceCredentialReference: paths.serviceCredentialReference,
|
|
958
|
+
credentialKeyVersion: paths.credentialKeyVersion,
|
|
959
|
+
currentDigest: staged.digests.current,
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
stages.push("readback");
|
|
963
|
+
if (!promoted.ok) {
|
|
964
|
+
if (promoted.rolledBack) stages.push("rollback");
|
|
965
|
+
return failure(
|
|
966
|
+
promoted.code,
|
|
967
|
+
stages,
|
|
968
|
+
{ rollback: promoted.rollback ?? { outcome: "NOT_APPLICABLE" } },
|
|
969
|
+
request
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
safeRemoveDirect(paths.profilesRoot, paths.backupRoot);
|
|
973
|
+
safeRemoveDirect(paths.profilesRoot, paths.stagingRoot);
|
|
974
|
+
return successReceipt({
|
|
975
|
+
request,
|
|
976
|
+
stages,
|
|
977
|
+
inspection: promoted.inspection,
|
|
978
|
+
previousDigest,
|
|
979
|
+
outputBytes: output.outputBytes,
|
|
980
|
+
});
|
|
981
|
+
} catch {
|
|
982
|
+
try {
|
|
983
|
+
safeRemoveDirect(paths.profilesRoot, paths.stagingRoot);
|
|
984
|
+
} catch {}
|
|
985
|
+
return failure("provisioning_failed", stages, {}, request);
|
|
986
|
+
}
|
|
987
|
+
}
|