@sellable/install 0.1.642 → 0.1.644

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.
@@ -0,0 +1,508 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync,
4
+ mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync,
5
+ rmSync, writeFileSync,
6
+ } from "node:fs";
7
+ import { basename, dirname, isAbsolute, join } from "node:path";
8
+
9
+ import { withProfileSoulLock } from "./fly-soul-bridge.mjs";
10
+ import { ensureHermesNamedCron } from "./hermes-memory-snapshot.mjs";
11
+
12
+ export const DEFAULT_PROFILE_RECONCILE_RECEIPT = ".sellable-agent/default-profile-reconcile.json";
13
+ export const DEFAULT_PROFILE_RECONCILE_SCHEMA = "sellable-agent-default-profile-reconcile/v1";
14
+
15
+ const BUNDLE_SCHEMA = "sellable-agent-default-profile-bundle/v1";
16
+ const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
17
+ const MANAGED_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
18
+ const SHA256 = /^[a-f0-9]{64}$/;
19
+ const FENCE = /^[1-9][0-9]{0,19}$/;
20
+ const STAGE_PREFIX = ".sellable-default-stage-";
21
+ const MAX_BYTES = 1024 * 1024;
22
+
23
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
24
+ const canonicalJson = (value) => {
25
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
26
+ if (value && typeof value === "object") {
27
+ return `{${Object.keys(value).sort()
28
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
29
+ .join(",")}}`;
30
+ }
31
+ return JSON.stringify(value);
32
+ };
33
+ const refused = (code) => ({ ok: false, status: "REFUSED", code });
34
+
35
+ export function buildDefaultProfileProjection({
36
+ kind,
37
+ claim,
38
+ env = process.env,
39
+ imageBundle,
40
+ fixtureTarget,
41
+ } = {}) {
42
+ const operation = claim?.operation;
43
+ const authority = claim?.runtimeAuthority;
44
+ if (
45
+ authority?.bundleVersion === undefined &&
46
+ authority?.bundleDigest === undefined
47
+ ) {
48
+ return null;
49
+ }
50
+ const verifiedImage = imageBundle ?? {
51
+ bundleVersion: Number(
52
+ env.SELLABLE_AGENT_DEFAULT_PROFILE_BUNDLE_VERSION ?? ""
53
+ ),
54
+ bundleDigest:
55
+ env.SELLABLE_AGENT_DEFAULT_PROFILE_BUNDLE_DIGEST ?? "",
56
+ };
57
+ if (
58
+ !["CUSTOMER", "ADMIN"].includes(kind) ||
59
+ !Number.isSafeInteger(verifiedImage.bundleVersion) ||
60
+ verifiedImage.bundleVersion < 1 ||
61
+ !SHA256.test(verifiedImage.bundleDigest)
62
+ ) {
63
+ throw new Error("default_profile_image_bundle_rejected");
64
+ }
65
+ if (
66
+ !operation ||
67
+ !Number.isSafeInteger(authority?.authorityGeneration) ||
68
+ authority.authorityGeneration < 1 ||
69
+ authority.bundleVersion !== verifiedImage.bundleVersion ||
70
+ authority.bundleDigest !== verifiedImage.bundleDigest
71
+ ) {
72
+ throw new Error("default_profile_bundle_identity_rejected");
73
+ }
74
+ const verifiedFixture = fixtureTarget ??
75
+ (env.SELLABLE_AGENT_DEFAULT_PROFILE_FIXTURE_TEST_ONLY === "1"
76
+ ? {
77
+ testOnly: true,
78
+ workspaceId:
79
+ env.SELLABLE_AGENT_DEFAULT_PROFILE_FIXTURE_WORKSPACE_ID ?? "",
80
+ agentId:
81
+ env.SELLABLE_AGENT_DEFAULT_PROFILE_FIXTURE_AGENT_ID ?? "",
82
+ }
83
+ : undefined);
84
+ const exactFixture =
85
+ verifiedFixture?.testOnly === true &&
86
+ verifiedFixture.workspaceId === operation.workspaceId &&
87
+ verifiedFixture.agentId === operation.agentId
88
+ ? { ...verifiedFixture }
89
+ : undefined;
90
+ return {
91
+ kind,
92
+ profileId: operation.profileId,
93
+ workspaceId: operation.workspaceId,
94
+ agentId: operation.agentId,
95
+ operationId: operation.id,
96
+ authorityGeneration: authority.authorityGeneration,
97
+ fence: String(operation.fence),
98
+ ...verifiedImage,
99
+ ...(exactFixture ? { fixtureTarget: exactFixture } : {}),
100
+ };
101
+ }
102
+
103
+ export function bindDefaultProfileProbeCron({
104
+ activeProjection,
105
+ profileRoot,
106
+ job,
107
+ bootId,
108
+ } = {}) {
109
+ if (!activeProjection?.fixtureTarget) return { changed: false };
110
+ if (
111
+ !isAbsolute(profileRoot ?? "") ||
112
+ !ID.test(bootId ?? "") ||
113
+ !ID.test(job?.id ?? "") ||
114
+ job.name !== "sellable-default-probe"
115
+ ) {
116
+ throw new Error("default_profile_cron_binding_rejected");
117
+ }
118
+ const scriptPath = join(
119
+ profileRoot,
120
+ "scripts",
121
+ "sellable-default-probe.sh"
122
+ );
123
+ const binding = {
124
+ operationId: activeProjection.operationId,
125
+ bootId,
126
+ bundleDigest: activeProjection.bundleDigest,
127
+ nonce: sha256(
128
+ `${activeProjection.operationId}\0${bootId}\0${activeProjection.bundleDigest}`
129
+ ).slice(0, 32),
130
+ jobId: job.id,
131
+ name: job.name,
132
+ };
133
+ const script = `#!/bin/sh
134
+ set -eu
135
+ exec /opt/hermes/.venv/bin/python3 - ${JSON.stringify(profileRoot)} <<'PY'
136
+ import json, os, sys, time
137
+ root = sys.argv[1]
138
+ binding = json.loads(${JSON.stringify(JSON.stringify(binding))})
139
+ binding["firedAt"] = int(time.time() * 1000)
140
+ marker_root = os.path.join(root, ".sellable-agent", "cron-markers")
141
+ os.makedirs(marker_root, mode=0o700, exist_ok=True)
142
+ target = os.path.join(marker_root, binding["name"] + ".json")
143
+ stage = target + ".tmp"
144
+ with open(stage, "w", encoding="utf-8") as handle:
145
+ json.dump(binding, handle, separators=(",", ":"))
146
+ handle.write("\\n")
147
+ os.chmod(stage, 0o600)
148
+ os.replace(stage, target)
149
+ PY
150
+ `;
151
+ const changed =
152
+ !existsSync(scriptPath) || readFileSync(scriptPath, "utf8") !== script;
153
+ if (changed) atomicWrite(scriptPath, script, 0o700);
154
+ return { changed, binding };
155
+ }
156
+
157
+ function readRegular(path, maxBytes = MAX_BYTES) {
158
+ const link = lstatSync(path);
159
+ if (link.isSymbolicLink() || !link.isFile() || link.nlink !== 1 || link.size > maxBytes)
160
+ throw new Error("default_profile_file_rejected");
161
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
162
+ try {
163
+ const opened = fstatSync(fd);
164
+ if (!opened.isFile() || opened.dev !== link.dev || opened.ino !== link.ino ||
165
+ opened.nlink !== 1 || opened.size !== link.size) {
166
+ throw new Error("default_profile_file_rejected");
167
+ }
168
+ return readFileSync(fd);
169
+ } finally {
170
+ closeSync(fd);
171
+ }
172
+ }
173
+
174
+ function atomicWrite(path, bytes, mode) {
175
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
176
+ const stage = join(dirname(path), `${STAGE_PREFIX}${basename(path)}-${process.pid}-${randomUUID()}`);
177
+ writeFileSync(stage, bytes, { flag: "wx", mode });
178
+ chmodSync(stage, mode);
179
+ const fd = openSync(stage, constants.O_RDONLY);
180
+ try {
181
+ fsyncSync(fd);
182
+ } finally {
183
+ closeSync(fd);
184
+ }
185
+ renameSync(stage, path);
186
+ }
187
+
188
+ function readBundle(bundleRoot) {
189
+ if (!isAbsolute(bundleRoot ?? "") || !existsSync(bundleRoot) ||
190
+ lstatSync(bundleRoot).isSymbolicLink() || !lstatSync(bundleRoot).isDirectory() ||
191
+ realpathSync(bundleRoot) !== bundleRoot) {
192
+ throw new Error("default_profile_bundle_rejected");
193
+ }
194
+ const manifest = JSON.parse(readRegular(join(bundleRoot, "manifest.json")).toString("utf8"));
195
+ if (
196
+ manifest?.schemaVersion !== BUNDLE_SCHEMA ||
197
+ !["CUSTOMER", "ADMIN"].includes(manifest.kind) ||
198
+ !Number.isSafeInteger(manifest.bundleVersion) || manifest.bundleVersion < 1 ||
199
+ !SHA256.test(manifest.bundleDigest ?? "") ||
200
+ !Array.isArray(manifest.materializerReceipts) ||
201
+ !Array.isArray(manifest.files)
202
+ ) {
203
+ throw new Error("default_profile_bundle_rejected");
204
+ }
205
+ const files = manifest.files.map((entry) => {
206
+ const expectedPath = entry?.type === "skill"
207
+ ? `skills/sellable-defaults/${entry.id}/SKILL.md`
208
+ : `crons/${entry?.name}.json`;
209
+ if (
210
+ !MANAGED_ID.test(entry?.id ?? "") ||
211
+ !["skill", "cron"].includes(entry.type) ||
212
+ entry.ownership !== "system_managed" ||
213
+ !["SHARED", "CUSTOMER", "ADMIN", "FIXTURE"].includes(entry.kind) ||
214
+ entry.mode !== "0444" ||
215
+ entry.path !== expectedPath ||
216
+ !SHA256.test(entry.sha256 ?? "") ||
217
+ (entry.type === "cron" && (!MANAGED_ID.test(entry.name ?? "") ||
218
+ !entry.name.startsWith("sellable-default-")))
219
+ ) {
220
+ throw new Error("default_profile_bundle_rejected");
221
+ }
222
+ const content = readRegular(join(bundleRoot, ...entry.path.split("/")), 64 * 1024);
223
+ if (sha256(content) !== entry.sha256)
224
+ throw new Error("default_profile_bundle_rejected");
225
+ return { ...entry, content: content.toString("utf8") };
226
+ });
227
+ const { bundleDigest, ...unsignedManifest } = manifest;
228
+ if (sha256(canonicalJson({ manifest: unsignedManifest, files })) !== bundleDigest)
229
+ throw new Error("default_profile_bundle_rejected");
230
+ return { manifest, files };
231
+ }
232
+
233
+ function activeEntries(bundle, projection) {
234
+ if (bundle.manifest.kind !== projection.kind ||
235
+ bundle.manifest.bundleVersion !== projection.bundleVersion ||
236
+ bundle.manifest.bundleDigest !== projection.bundleDigest) {
237
+ throw new Error("default_profile_projection_mismatch");
238
+ }
239
+ const target = projection.fixtureTarget;
240
+ const fixtureAllowed = target?.testOnly === true &&
241
+ target.workspaceId === projection.workspaceId &&
242
+ target.agentId === projection.agentId;
243
+ return bundle.files
244
+ .filter((entry) => entry.kind !== "FIXTURE" || fixtureAllowed)
245
+ .sort(
246
+ (left, right) => (left.type === "skill" ? 0 : 1) -
247
+ (right.type === "skill" ? 0 : 1) ||
248
+ left.path.localeCompare(right.path)
249
+ );
250
+ }
251
+
252
+ function validateProjection(value) {
253
+ return Boolean(
254
+ value &&
255
+ ["CUSTOMER", "ADMIN"].includes(value.kind) &&
256
+ ["profileId", "workspaceId", "agentId", "operationId"].every(
257
+ (key) => ID.test(value[key] ?? "")
258
+ ) &&
259
+ Number.isSafeInteger(value.authorityGeneration) &&
260
+ value.authorityGeneration > 0 &&
261
+ FENCE.test(value.fence ?? "") &&
262
+ Number.isSafeInteger(value.bundleVersion) &&
263
+ value.bundleVersion > 0 &&
264
+ SHA256.test(value.bundleDigest ?? "")
265
+ );
266
+ }
267
+
268
+ function cronDesired(entry) {
269
+ const spec = JSON.parse(entry.content);
270
+ if (
271
+ !spec ||
272
+ Object.keys(spec).sort().join("\0") !==
273
+ "deliver\0marker\0noAgent\0schedule\0script" ||
274
+ typeof spec.schedule !== "string" ||
275
+ !/^(?:\S+\s+){4}\S+$/.test(spec.schedule) ||
276
+ !/^[a-z0-9][a-z0-9-]{0,63}\.sh$/.test(spec.script ?? "") ||
277
+ spec.noAgent !== true ||
278
+ spec.deliver !== "local" ||
279
+ !MANAGED_ID.test(spec.marker ?? "")
280
+ ) {
281
+ throw new Error("default_profile_cron_spec_rejected");
282
+ }
283
+ const desired = {
284
+ name: entry.name,
285
+ enabled: true,
286
+ no_agent: true,
287
+ schedule: { kind: "cron", expr: spec.schedule },
288
+ script: spec.script,
289
+ deliver: spec.deliver,
290
+ };
291
+ return { spec, desired, sha256: sha256(canonicalJson(desired)) };
292
+ }
293
+
294
+ export async function reconcileDefaultProfileBundle({
295
+ activeProjection,
296
+ materializerReceipt,
297
+ profileRoot,
298
+ bundleRoot,
299
+ dataRoot,
300
+ withProfileLock = withProfileSoulLock,
301
+ reattestAuthority,
302
+ cronAdapter,
303
+ } = {}) {
304
+ if (activeProjection == null)
305
+ return { ok: true, status: "EMPTY", changed: false, actions: [] };
306
+ if (!validateProjection(activeProjection))
307
+ return refused("default_profile_projection_rejected");
308
+ if (!isAbsolute(dataRoot ?? "") || !isAbsolute(profileRoot ?? "") ||
309
+ typeof withProfileLock !== "function" || typeof reattestAuthority !== "function") {
310
+ return refused("default_profile_reconcile_input_rejected");
311
+ }
312
+
313
+ try {
314
+ if (activeEntries(readBundle(bundleRoot), activeProjection).length === 0)
315
+ return { ok: true, status: "EMPTY", changed: false, actions: [] };
316
+ return await withProfileLock(
317
+ activeProjection.profileId,
318
+ async () => {
319
+ const authority = await reattestAuthority(activeProjection);
320
+ if (authority?.operationId !== activeProjection.operationId ||
321
+ authority?.authorityGeneration !== activeProjection.authorityGeneration ||
322
+ authority?.fence !== activeProjection.fence) {
323
+ return refused("default_profile_authority_stale");
324
+ }
325
+ if (!existsSync(profileRoot) || lstatSync(profileRoot).isSymbolicLink() ||
326
+ !lstatSync(profileRoot).isDirectory() || realpathSync(profileRoot) !== profileRoot) {
327
+ throw new Error("default_profile_root_rejected");
328
+ }
329
+
330
+ const bundle = readBundle(bundleRoot);
331
+ const entries = activeEntries(bundle, activeProjection);
332
+ if (materializerReceipt?.profileId !== activeProjection.profileId ||
333
+ !materializerReceipt.fileHashes || Array.isArray(materializerReceipt.fileHashes)) {
334
+ throw new Error("default_profile_materializer_receipt_rejected");
335
+ }
336
+ for (const path of bundle.manifest.materializerReceipts) {
337
+ if (
338
+ !["SOUL.md", "skills/sellable/SKILL.md", "skills/sellable-admin/SKILL.md"]
339
+ .includes(path) ||
340
+ !SHA256.test(materializerReceipt.fileHashes[path] ?? "") ||
341
+ sha256(readRegular(join(profileRoot, ...path.split("/")), 64 * 1024)) !==
342
+ materializerReceipt.fileHashes[path]
343
+ ) {
344
+ throw new Error("default_profile_materializer_receipt_rejected");
345
+ }
346
+ }
347
+ const materializerReceiptSha256 = sha256(canonicalJson(materializerReceipt));
348
+
349
+ const receiptPath = join(profileRoot, DEFAULT_PROFILE_RECONCILE_RECEIPT);
350
+ let prior = null;
351
+ if (existsSync(receiptPath)) {
352
+ prior = JSON.parse(readRegular(receiptPath).toString("utf8"));
353
+ if (prior?.schemaVersion !== DEFAULT_PROFILE_RECONCILE_SCHEMA ||
354
+ prior.profileId !== activeProjection.profileId || !Array.isArray(prior.entries)) {
355
+ throw new Error("default_profile_receipt_rejected");
356
+ }
357
+ }
358
+ const historical = (entry) =>
359
+ prior?.entries.find((value) => value?.type === entry.type && value.id === entry.id &&
360
+ (entry.type !== "cron" || value.name === entry.name) &&
361
+ SHA256.test(value.sha256 ?? "")) ?? null;
362
+
363
+ // Preflight every skill and cron spec before the first mutation.
364
+ const skillPlans = new Map();
365
+ const cronSpecs = new Map();
366
+ for (const entry of entries) {
367
+ if (entry.type === "cron") {
368
+ cronSpecs.set(entry.id, cronDesired(entry));
369
+ continue;
370
+ }
371
+ const directory = join(profileRoot, "skills", "sellable-defaults", entry.id);
372
+ const path = join(directory, "SKILL.md");
373
+ if (existsSync(directory)) {
374
+ const stat = lstatSync(directory);
375
+ if (stat.isSymbolicLink() || !stat.isDirectory())
376
+ throw new Error("default_profile_skill_collision");
377
+ for (const name of readdirSync(directory)) {
378
+ if (name === "SKILL.md") continue;
379
+ const stage = join(directory, name);
380
+ const stageStat = lstatSync(stage);
381
+ if (!name.startsWith(STAGE_PREFIX) || stageStat.isSymbolicLink() ||
382
+ !stageStat.isFile() || stageStat.nlink !== 1) {
383
+ throw new Error("default_profile_skill_collision");
384
+ }
385
+ rmSync(stage);
386
+ }
387
+ }
388
+ let status = "CREATED";
389
+ if (existsSync(path)) {
390
+ const current = sha256(readRegular(path, 64 * 1024));
391
+ if (current === entry.sha256) status = "REUSED";
392
+ else if (historical(entry)?.sha256 === current) status = "UPDATED";
393
+ else throw new Error("default_profile_skill_collision");
394
+ }
395
+ skillPlans.set(entry.id, { directory, path, status });
396
+ }
397
+
398
+ const actions = [];
399
+ const receiptEntries = [];
400
+ for (const entry of entries) {
401
+ if (entry.type === "skill") {
402
+ const plan = skillPlans.get(entry.id);
403
+ if (plan.status !== "REUSED") {
404
+ mkdirSync(plan.directory, { recursive: true, mode: 0o700 });
405
+ atomicWrite(plan.path, entry.content, 0o444);
406
+ if (sha256(readRegular(plan.path, 64 * 1024)) !== entry.sha256)
407
+ throw new Error("default_profile_skill_readback_rejected");
408
+ }
409
+ actions.push({ type: "skill", id: entry.id, status: plan.status });
410
+ receiptEntries.push({ type: "skill", id: entry.id, sha256: entry.sha256 });
411
+ continue;
412
+ }
413
+
414
+ if (typeof cronAdapter?.create !== "function" ||
415
+ typeof cronAdapter?.update !== "function") {
416
+ throw new Error("default_profile_cron_adapter_rejected");
417
+ }
418
+ const { spec, desired, sha256: desiredSha256 } = cronSpecs.get(entry.id);
419
+ const cron = await ensureHermesNamedCron({
420
+ profileRoot,
421
+ name: entry.name,
422
+ isExpected: (job) => Boolean(job?.id && job.name === desired.name &&
423
+ job.enabled === desired.enabled && job.no_agent === desired.no_agent &&
424
+ job.script === desired.script && job.deliver === desired.deliver &&
425
+ job.schedule?.kind === "cron" &&
426
+ job.schedule.expr === desired.schedule.expr),
427
+ isUpdatable: (job) => historical(entry)?.sha256 === sha256(canonicalJson({
428
+ name: job?.name,
429
+ enabled: job?.enabled,
430
+ no_agent: job?.no_agent,
431
+ schedule: job?.schedule,
432
+ script: job?.script,
433
+ deliver: job?.deliver,
434
+ })),
435
+ create: () =>
436
+ cronAdapter.create({
437
+ profileRoot,
438
+ entry,
439
+ spec,
440
+ desired,
441
+ activeProjection,
442
+ }),
443
+ update: (existing) =>
444
+ cronAdapter.update({
445
+ profileRoot,
446
+ entry,
447
+ spec,
448
+ desired,
449
+ existing,
450
+ activeProjection,
451
+ }),
452
+ codePrefix: "default_profile_cron",
453
+ });
454
+ if (!cron.ok) throw new Error(cron.code);
455
+ const bound = typeof cronAdapter.bind === "function"
456
+ ? await cronAdapter.bind({
457
+ profileRoot,
458
+ entry,
459
+ spec,
460
+ desired,
461
+ job: cron.job,
462
+ activeProjection,
463
+ })
464
+ : { changed: false };
465
+ if (!bound || typeof bound.changed !== "boolean")
466
+ throw new Error("default_profile_cron_binding_rejected");
467
+ actions.push({
468
+ type: "cron",
469
+ id: entry.id,
470
+ name: entry.name,
471
+ status: cron.status,
472
+ bindingChanged: bound.changed,
473
+ });
474
+ receiptEntries.push({ type: "cron", id: entry.id, name: entry.name, sha256: desiredSha256 });
475
+ }
476
+
477
+ const receipt = `${canonicalJson({
478
+ schemaVersion: DEFAULT_PROFILE_RECONCILE_SCHEMA,
479
+ profileId: activeProjection.profileId,
480
+ bundleVersion: activeProjection.bundleVersion,
481
+ bundleDigest: activeProjection.bundleDigest,
482
+ materializerReceiptSha256,
483
+ entries: receiptEntries,
484
+ })}\n`;
485
+ const currentReceipt = existsSync(receiptPath)
486
+ ? readRegular(receiptPath).toString("utf8") : null;
487
+ if (currentReceipt !== receipt) atomicWrite(receiptPath, receipt, 0o600);
488
+ const changed = actions.some(
489
+ (action) => action.status !== "REUSED" || action.bindingChanged === true
490
+ );
491
+ return {
492
+ ok: true,
493
+ status: changed ? "APPLIED" : "REUSED",
494
+ changed,
495
+ bundleVersion: activeProjection.bundleVersion,
496
+ bundleDigest: activeProjection.bundleDigest,
497
+ materializerReceiptSha256,
498
+ actions,
499
+ };
500
+ },
501
+ { dataRoot }
502
+ );
503
+ } catch (error) {
504
+ const code = error instanceof Error && /^default_profile_[a-z0-9_]+$/.test(error.message)
505
+ ? error.message : "default_profile_reconcile_failed";
506
+ return refused(code);
507
+ }
508
+ }
@@ -28,7 +28,7 @@ import {
28
28
 
29
29
  const MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
30
30
  const MCP_PACKAGE = "@sellable/mcp";
31
- const MCP_VERSION = "0.1.879";
31
+ const MCP_VERSION = "0.1.880";
32
32
  const SAFE_COMPONENT = /^[A-Za-z0-9@._+-]+$/;
33
33
  const HERMES_EXCLUDES = new Set([".git", ".playwright", "node_modules"]);
34
34
 
@@ -1,3 +1,4 @@
1
+ # syntax=docker/dockerfile:1.7
1
2
  ARG HERMES_IMAGE=nousresearch/hermes-agent@sha256:c0cab4e3711bcb27a312be1b3776254fc06fd50d5f7a6b8017915fc7171cb39e
2
3
  ARG SLACK_PP_GO_IMAGE=golang@sha256:386d475a660466863d9f8c766fec64d7fdad3edac2c6a05020c09534d71edb4b
3
4
 
@@ -26,6 +27,8 @@ ARG ADMIN_MCP_PACKAGE
26
27
  ARG ADMIN_MCP_INTEGRITY
27
28
  ARG ADMIN_INSTALL_PACKAGE
28
29
  ARG ADMIN_INSTALL_INTEGRITY
30
+ ARG ADMIN_INSTALL_SOURCE=registry
31
+ ARG ADMIN_INSTALL_LOCAL_SHA256
29
32
  ARG ADMIN_LOCKED_TREE_DIGEST
30
33
  ARG ADMIN_ARTIFACT_DIGEST
31
34
  ARG ADMIN_PACKAGE_LOCK_B64
@@ -36,16 +39,28 @@ ARG AGENT_BROWSER_VERSION=0.26.0
36
39
  ARG PLAYWRIGHT_BROWSER_REVISION=1234
37
40
  ARG STRIPE_CLI_PACKAGE=@stripe/cli-linux-x64@1.45.0
38
41
  ARG STRIPE_CLI_INTEGRITY=sha512-1ZhoPpoweYfynqsvhCLlTjZ7lPz5IKtxqAMT5MBy6zrwLRTP9x0T6r8h/jLYL8lRH1c7zC/6X6/XUu2V+r9Bog==
42
+ ARG DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION=
43
+ ARG DEFAULT_PROFILE_BUNDLE_VERSION=1
44
+ ARG DEFAULT_PROFILE_BUNDLE_DIGEST=608ab392a8d8477ea0045ac89860af7e8be98740c23b21225fb82e07aad10723
39
45
 
40
46
  COPY --from=slack-pp-cli-builder --chmod=0555 /out/slack-pp-cli /usr/local/bin/slack-pp-cli
41
47
 
42
- RUN set -eux; \
48
+ RUN --mount=type=secret,id=admin_install_tarball,required=false \
49
+ set -eux; \
50
+ test "${ADMIN_INSTALL_SOURCE}" = "registry" -o "${ADMIN_INSTALL_SOURCE}" = "local-tarball"; \
43
51
  test -n "${ADMIN_PACKAGE_LOCK_B64}"; \
44
52
  test -n "${ADMIN_PROFILE_CONTRACT_TEMPLATE_B64}"; \
45
53
  test "$(npm view "${PRODUCT_MCP_PACKAGE}" dist.integrity)" = "${PRODUCT_MCP_INTEGRITY}"; \
46
54
  test "$(npm view "${PRODUCT_INSTALL_PACKAGE}" dist.integrity)" = "${PRODUCT_INSTALL_INTEGRITY}"; \
47
55
  test "$(npm view "${ADMIN_MCP_PACKAGE}" dist.integrity)" = "${ADMIN_MCP_INTEGRITY}"; \
48
- test "$(npm view "${ADMIN_INSTALL_PACKAGE}" dist.integrity)" = "${ADMIN_INSTALL_INTEGRITY}"; \
56
+ if test "${ADMIN_INSTALL_SOURCE}" = "registry"; then \
57
+ test "$(npm view "${ADMIN_INSTALL_PACKAGE}" dist.integrity)" = "${ADMIN_INSTALL_INTEGRITY}"; \
58
+ else \
59
+ test -s /run/secrets/admin_install_tarball; \
60
+ test -n "${ADMIN_INSTALL_LOCAL_SHA256}"; \
61
+ test "$(sha256sum /run/secrets/admin_install_tarball | cut -d ' ' -f 1)" = "${ADMIN_INSTALL_LOCAL_SHA256}"; \
62
+ test "$(node -e 'const c=require("crypto"),f=require("fs"); process.stdout.write(`sha512-${c.createHash("sha512").update(f.readFileSync(process.argv[1])).digest("base64")}`)' /run/secrets/admin_install_tarball)" = "${ADMIN_INSTALL_INTEGRITY}"; \
63
+ fi; \
49
64
  test "$(npm view "${STRIPE_CLI_PACKAGE}" dist.integrity)" = "${STRIPE_CLI_INTEGRITY}"; \
50
65
  mkdir -p /usr/local/lib/sellable-agent/admin-artifact; \
51
66
  node -e 'const fs=require("fs"); const specs=process.argv.slice(1); fs.writeFileSync("/usr/local/lib/sellable-agent/admin-artifact/package.json", JSON.stringify({name:"sellable-admin-runtime-artifact",private:true,version:"1.0.0",dependencies:Object.fromEntries(specs.map((spec)=>{const at=spec.lastIndexOf("@"); return [spec.slice(0,at),spec.slice(at+1)];}))},null,2)+"\n");' \
@@ -58,12 +73,28 @@ RUN set -eux; \
58
73
  printf '%s' "${ADMIN_PROFILE_CONTRACT_TEMPLATE_B64}" | base64 -d \
59
74
  > /usr/local/lib/sellable-agent/admin-artifact/profile-contract-template.json; \
60
75
  test "$(sha256sum /usr/local/lib/sellable-agent/admin-artifact/package-lock.json | cut -d ' ' -f 1)" = "${ADMIN_LOCKED_TREE_DIGEST}"; \
76
+ if test "${ADMIN_INSTALL_SOURCE}" = "local-tarball"; then \
77
+ node -e 'const fs=require("fs"); const packagePath=process.argv[1],lockPath=process.argv[2]; const p=JSON.parse(fs.readFileSync(packagePath)); delete p.dependencies["@sellable/admin-install"]; fs.writeFileSync(packagePath,JSON.stringify(p,null,2)+"\n"); const l=JSON.parse(fs.readFileSync(lockPath)); delete l.packages[""].dependencies["@sellable/admin-install"]; delete l.packages["node_modules/@sellable/admin-install"]; fs.writeFileSync(lockPath,JSON.stringify(l,null,2)+"\n");' \
78
+ /usr/local/lib/sellable-agent/admin-artifact/package.json \
79
+ /usr/local/lib/sellable-agent/admin-artifact/package-lock.json; \
80
+ fi; \
61
81
  npm ci \
62
82
  --prefix /usr/local/lib/sellable-agent/admin-artifact \
63
83
  --ignore-scripts \
64
84
  --omit=dev \
65
85
  --no-audit \
66
86
  --no-fund; \
87
+ if test "${ADMIN_INSTALL_SOURCE}" = "local-tarball"; then \
88
+ npm install \
89
+ --prefix /usr/local/lib/sellable-agent/admin-artifact \
90
+ --ignore-scripts \
91
+ --omit=dev \
92
+ --no-audit \
93
+ --no-fund \
94
+ --no-save \
95
+ --package-lock=false \
96
+ /run/secrets/admin_install_tarball; \
97
+ fi; \
67
98
  npm install \
68
99
  --prefix /usr/local/lib/sellable-agent \
69
100
  --omit=dev \
@@ -94,6 +125,8 @@ RUN set -eux; \
94
125
  test -f /usr/local/lib/sellable-agent/admin-artifact/node_modules/@sellable/admin-mcp/dist/index.js; \
95
126
  node -e 'const p=require("/usr/local/lib/sellable-agent/admin-artifact/node_modules/@sellable/admin-mcp/package.json"); if(p.main!=="dist/index.js" || p.bin?.["sellable-admin-mcp"]!=="dist/index.js") process.exit(1);'; \
96
127
  test -f /usr/local/lib/sellable-agent/admin-artifact/node_modules/@sellable/admin-install/lib/hermes-admin-profile-factory.mjs; \
128
+ test -f /usr/local/lib/sellable-agent/admin-artifact/node_modules/@sellable/admin-install/default-profile-bundles/admin/manifest.fragment.json; \
129
+ test "$(node -p "require('/usr/local/lib/sellable-agent/admin-artifact/node_modules/@sellable/admin-install/package.json').version")" = "${ADMIN_INSTALL_PACKAGE##*@}"; \
97
130
  node -e 'const fs=require("fs"),c=require("crypto"); const p="/usr/local/lib/sellable-agent/admin-artifact/profile-contract-template.json"; const stable=(v)=>Array.isArray(v)?`[${v.map(stable).join(",")}]`:v&&typeof v==="object"?`{${Object.keys(v).sort().map((k)=>`${JSON.stringify(k)}:${stable(v[k])}`).join(",")}}`:JSON.stringify(v); const t=JSON.parse(fs.readFileSync(p)); const {contentDigest,...body}=t; if(t.schemaVersion!=="sellable-admin-profile-contract-template/v1" || c.createHash("sha256").update(stable(body)).digest("hex")!==contentDigest) process.exit(1);'; \
98
131
  rm -f /etc/s6-overlay/s6-rc.d/user/contents.d/dashboard \
99
132
  /etc/s6-overlay/s6-rc.d/user/contents.d/main-hermes \
@@ -132,12 +165,29 @@ COPY fly-skills-bridge-exec.mjs /usr/local/lib/sellable-agent/fly-skills-bridge-
132
165
  COPY fly-capabilities-bridge.mjs /usr/local/lib/sellable-agent/fly-capabilities-bridge.mjs
133
166
  COPY fly-capabilities-bridge-exec.mjs /usr/local/lib/sellable-agent/fly-capabilities-bridge-exec.mjs
134
167
  COPY soul-artifact-validator.mjs /usr/local/lib/sellable-agent/soul-artifact-validator.mjs
168
+ COPY default-profile-bundle.mjs /usr/local/lib/sellable-agent/default-profile-bundle.mjs
169
+ COPY default-profile-reconciler.mjs /usr/local/lib/sellable-agent/default-profile-reconciler.mjs
170
+ COPY fly-cron-proof-exec.mjs /usr/local/lib/sellable-agent/fly-cron-proof-exec.mjs
171
+ COPY default-profile-bundles/shared/ /usr/local/lib/sellable-agent/default-profile-bundles/shared/
172
+ COPY default-profile-bundles/fixtures/ /usr/local/lib/sellable-agent/default-profile-bundles/fixtures/
135
173
  COPY hermes-memory-dirty.mjs /opt/sellable/bin/hermes-memory-dirty
136
174
  COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
137
175
  COPY hermes-endpoint-cron.mjs /opt/sellable/bin/hermes-endpoint-cron
138
176
  COPY fly-admin-image/rootfs/ /
139
177
 
140
- RUN node --input-type=module -e \
178
+ RUN set -eux; \
179
+ fixture_args=""; \
180
+ if test -n "${DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION}"; then \
181
+ fixture_args="--fixture-version ${DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION}"; \
182
+ fi; \
183
+ node /usr/local/lib/sellable-agent/default-profile-bundle.mjs \
184
+ --kind ADMIN ${fixture_args} \
185
+ --admin-fragment /usr/local/lib/sellable-agent/admin-artifact/node_modules/@sellable/admin-install/default-profile-bundles/admin/manifest.fragment.json \
186
+ --output /usr/local/share/sellable-agent/default-profile-bundle \
187
+ && node -e \
188
+ 'const fs=require("fs"); const manifest=JSON.parse(fs.readFileSync("/usr/local/share/sellable-agent/default-profile-bundle/manifest.json")); if(String(manifest.bundleVersion)!==process.argv[1] || manifest.bundleDigest!==process.argv[2]) process.exit(1);' \
189
+ "${DEFAULT_PROFILE_BUNDLE_VERSION}" "${DEFAULT_PROFILE_BUNDLE_DIGEST}" \
190
+ && node --input-type=module -e \
141
191
  'import { installHermesAgentBridge, HERMES_AGENT_BRIDGE_DEDICATED_V020_CONTRACT } from "/usr/local/lib/sellable-agent/hermes-bridge.mjs"; installHermesAgentBridge({ sourceRoot: "/opt/hermes", contract: HERMES_AGENT_BRIDGE_DEDICATED_V020_CONTRACT });' \
142
192
  && node --input-type=module -e \
143
193
  'await import("/usr/local/lib/sellable-agent/fly-runtime-identity.mjs");' \
@@ -169,11 +219,15 @@ RUN node --input-type=module -e \
169
219
  && sha256sum /usr/local/lib/sellable-agent/fly-skills-bridge.mjs \
170
220
  /usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs \
171
221
  > /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
222
+ && sha256sum /usr/local/lib/sellable-agent/default-profile-reconciler.mjs \
223
+ /usr/local/lib/sellable-agent/fly-cron-proof-exec.mjs \
224
+ > /usr/local/share/sellable-agent/default-profile-reconcile-source.sha256 \
172
225
  && sha256sum /usr/local/lib/sellable-agent/fly-capabilities-bridge.mjs \
173
226
  /usr/local/lib/sellable-agent/fly-capabilities-bridge-exec.mjs \
174
227
  > /usr/local/share/sellable-agent/capabilities-bridge-source.sha256 \
175
228
  && chmod 0444 /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
176
229
  /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
230
+ /usr/local/share/sellable-agent/default-profile-reconcile-source.sha256 \
177
231
  /usr/local/share/sellable-agent/capabilities-bridge-source.sha256 \
178
232
  && chmod 0755 /etc \
179
233
  && chmod 0555 \
@@ -192,6 +246,8 @@ RUN node --input-type=module -e \
192
246
  /usr/local/lib/sellable-agent/fly-soul-bridge-exec.mjs \
193
247
  /usr/local/lib/sellable-agent/fly-skills-bridge.mjs \
194
248
  /usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs \
249
+ /usr/local/lib/sellable-agent/default-profile-reconciler.mjs \
250
+ /usr/local/lib/sellable-agent/fly-cron-proof-exec.mjs \
195
251
  /usr/local/lib/sellable-agent/soul-artifact-validator.mjs \
196
252
  /opt/sellable/bin/hermes-memory-dirty \
197
253
  /opt/sellable/bin/hermes-endpoint-cron \
@@ -203,7 +259,9 @@ RUN node --input-type=module -e \
203
259
 
204
260
  LABEL io.sellable.runtime.kind="admin" \
205
261
  io.sellable.runtime.listener="native-dedicated-slack" \
206
- io.sellable.runtime.installers="build-only"
262
+ io.sellable.runtime.installers="build-only" \
263
+ io.sellable.runtime.default-profile-bundle-version="${DEFAULT_PROFILE_BUNDLE_VERSION}" \
264
+ io.sellable.runtime.default-profile-bundle-digest="${DEFAULT_PROFILE_BUNDLE_DIGEST}"
207
265
 
208
266
  ENV SELLABLE_AGENT_ADMIN_RUNTIME=/usr/local/lib/sellable-agent/admin-runtime/admin-runtime.mjs
209
267
  ENV SELLABLE_AGENT_PRODUCT_MCP_PACKAGE=${PRODUCT_MCP_PACKAGE}
@@ -212,6 +270,8 @@ ENV SELLABLE_AGENT_ADMIN_MCP_PACKAGE=${ADMIN_MCP_PACKAGE}
212
270
  ENV SELLABLE_AGENT_ADMIN_INSTALL_PACKAGE=${ADMIN_INSTALL_PACKAGE}
213
271
  ENV SELLABLE_AGENT_ADMIN_LOCKED_TREE_DIGEST=${ADMIN_LOCKED_TREE_DIGEST}
214
272
  ENV SELLABLE_AGENT_ADMIN_ARTIFACT_DIGEST=${ADMIN_ARTIFACT_DIGEST}
273
+ ENV SELLABLE_AGENT_DEFAULT_PROFILE_BUNDLE_VERSION=${DEFAULT_PROFILE_BUNDLE_VERSION}
274
+ ENV SELLABLE_AGENT_DEFAULT_PROFILE_BUNDLE_DIGEST=${DEFAULT_PROFILE_BUNDLE_DIGEST}
215
275
  ENV HOME=/opt/data
216
276
  ENV HERMES_HOME=/opt/data
217
277
  ENV HERMES_WRITE_SAFE_ROOT=/opt/data