@sellable/install 0.1.642 → 0.1.643

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.
@@ -69,6 +69,12 @@ import {
69
69
  prepareSoulLockRoot,
70
70
  withProfileSoulLock,
71
71
  } from "../fly-soul-bridge.mjs";
72
+ import { proveDefaultProfileNativeSkills } from "../fly-skills-bridge.mjs";
73
+ import {
74
+ bindDefaultProfileProbeCron,
75
+ buildDefaultProfileProjection,
76
+ reconcileDefaultProfileBundle,
77
+ } from "../default-profile-reconciler.mjs";
72
78
  import {
73
79
  HERMES_SNAPSHOT_HOOKS,
74
80
  HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES,
@@ -185,6 +191,17 @@ const RUNTIME_GENERATION = Number(
185
191
  process.env.SELLABLE_AGENT_RUNTIME_GENERATION ?? "0"
186
192
  );
187
193
  const BOOT_SESSION_ID = randomUUID();
194
+ const DEFAULT_PROFILE_BUNDLE_ROOT =
195
+ "/usr/local/share/sellable-agent/default-profile-bundle";
196
+ const DEFAULT_PROFILE_CRON_PYTHON = [
197
+ "import json,sys",
198
+ "from cron.jobs import create_job, update_job",
199
+ "value=json.loads(sys.argv[1])",
200
+ "job_id=value.pop('jobId', None)",
201
+ "enabled=value.pop('enabled')",
202
+ "result=update_job(job_id, {**value, 'enabled': enabled}) if job_id else create_job(**value, enabled=enabled)",
203
+ "print(json.dumps({'id': result.get('id') if result else None}))",
204
+ ].join("\n");
188
205
 
189
206
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
190
207
  const deliveryClaimId = (operationId) =>
@@ -192,6 +209,69 @@ const deliveryClaimId = (operationId) =>
192
209
  const sleep = (milliseconds) =>
193
210
  new Promise((resolve) => setTimeout(resolve, milliseconds));
194
211
 
212
+ function adminDefaultProfileCronAdapter(profileId, profileRoot) {
213
+ const mutate = ({ desired, spec, existing }) => {
214
+ const child = spawnSync(
215
+ HERMES_PYTHON,
216
+ [
217
+ "-c",
218
+ DEFAULT_PROFILE_CRON_PYTHON,
219
+ JSON.stringify({
220
+ name: desired.name,
221
+ schedule: spec.schedule,
222
+ script: desired.script,
223
+ no_agent: true,
224
+ deliver: "local",
225
+ enabled: true,
226
+ ...(existing?.id ? { jobId: existing.id } : {}),
227
+ }),
228
+ ],
229
+ {
230
+ env: {
231
+ ...process.env,
232
+ HOME: DATA_ROOT,
233
+ HERMES_HOME: profileRoot,
234
+ HERMES_PROFILE: profileId,
235
+ NO_COLOR: "1",
236
+ },
237
+ encoding: "utf8",
238
+ timeout: 120_000,
239
+ maxBuffer: 64 * 1024,
240
+ }
241
+ );
242
+ return {
243
+ exitCode: child.status,
244
+ signal: child.signal,
245
+ stdout: child.stdout ?? "",
246
+ stderr: child.stderr ?? "",
247
+ timedOut: Boolean(child.error && child.signal === "SIGTERM"),
248
+ outputLimitExceeded: child.error?.code === "ENOBUFS",
249
+ };
250
+ };
251
+ return {
252
+ create: (input) => mutate(input),
253
+ update: (input) => mutate(input),
254
+ bind: (input) =>
255
+ bindDefaultProfileProbeCron({ ...input, bootId: BOOT_SESSION_ID }),
256
+ };
257
+ }
258
+
259
+ function adminMaterializerReceipt(profileId, profileRoot) {
260
+ const paths = [
261
+ "SOUL.md",
262
+ "skills/sellable/SKILL.md",
263
+ "skills/sellable-admin/SKILL.md",
264
+ ];
265
+ return {
266
+ profileId,
267
+ fileHashes: Object.fromEntries(
268
+ paths
269
+ .filter((relative) => existsSync(join(profileRoot, relative)))
270
+ .map((relative) => [relative, sha256(readFileSync(join(profileRoot, relative)))])
271
+ ),
272
+ };
273
+ }
274
+
195
275
  function canonicalJson(value) {
196
276
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
197
277
  if (value && typeof value === "object") {
@@ -1140,6 +1220,7 @@ async function materializeAdminProfile({
1140
1220
  profile,
1141
1221
  profileHome,
1142
1222
  adminHome,
1223
+ memoryCron,
1143
1224
  opportunitySequencesCron,
1144
1225
  observedSoulRawSha256: promoted.observedSoulRawSha256,
1145
1226
  servers: [serverProofs.sellable, serverProofs["sellable-admin"]].map(
@@ -1410,6 +1491,12 @@ function adminServingObservation(activeLaunch, activeGateway, exchange) {
1410
1491
  serverInfoName: adminMcp?.serverInfoName,
1411
1492
  toolCount: adminMcp?.toolCount,
1412
1493
  },
1494
+ ...(authority.bundleVersion === undefined
1495
+ ? {}
1496
+ : {
1497
+ bundleVersion: authority.bundleVersion,
1498
+ bundleDigest: authority.bundleDigest,
1499
+ }),
1413
1500
  nativeGateway: true,
1414
1501
  customerRouting: false,
1415
1502
  });
@@ -1445,6 +1532,11 @@ function startOperationHeartbeat({ claim, exchange, client }) {
1445
1532
  assertHealthy() {
1446
1533
  if (failure) throw failure;
1447
1534
  },
1535
+ async reattest() {
1536
+ beat();
1537
+ await inFlight;
1538
+ if (failure) throw failure;
1539
+ },
1448
1540
  async stop() {
1449
1541
  stopped = true;
1450
1542
  clearInterval(timer);
@@ -2437,6 +2529,8 @@ function completionReceipt({
2437
2529
  observedLifecycle,
2438
2530
  memorySnapshot,
2439
2531
  modelAuth,
2532
+ activeProjection,
2533
+ nativeDefaultProfileProof,
2440
2534
  }) {
2441
2535
  const identity = operationIdentity(claim, exchange);
2442
2536
  const routeGenerationDigest = flyRuntimeDigest(
@@ -2492,6 +2586,12 @@ function completionReceipt({
2492
2586
  secretDisposition: "INSTALLED",
2493
2587
  observedLifecycle,
2494
2588
  observedRevisionId: claim.operation.targetRevisionId,
2589
+ ...(activeProjection
2590
+ ? {
2591
+ bundleVersion: activeProjection.bundleVersion,
2592
+ bundleDigest: activeProjection.bundleDigest,
2593
+ }
2594
+ : {}),
2495
2595
  signerGeneration: exchange.keyGeneration,
2496
2596
  signedDigest: flyRuntimeDigest({
2497
2597
  ...identity,
@@ -2514,6 +2614,11 @@ function completionReceipt({
2514
2614
  },
2515
2615
  { code: "SLACK_TRAFFIC_DISABLED", status: "TRUE" },
2516
2616
  { code: "NO_BOOT_INSTALL", status: "TRUE" },
2617
+ {
2618
+ code: "DEFAULT_PROFILE_NATIVE_PROOF",
2619
+ status:
2620
+ nativeDefaultProfileProof.status === "PROVEN" ? "TRUE" : "UNKNOWN",
2621
+ },
2517
2622
  ],
2518
2623
  errorClass: "NONE",
2519
2624
  auditIds: [
@@ -2533,9 +2638,10 @@ async function flushHermesSnapshot({
2533
2638
  profileId,
2534
2639
  profileRoot,
2535
2640
  operationIdentity,
2641
+ forceDirty = false,
2536
2642
  }) {
2537
2643
  try {
2538
- markHermesSnapshotDirty(profileRoot);
2644
+ if (forceDirty) markHermesSnapshotDirty(profileRoot);
2539
2645
  return await reconcileHermesSnapshot({
2540
2646
  profileId,
2541
2647
  profileRoot,
@@ -3374,6 +3480,7 @@ async function runAdminProduction() {
3374
3480
  profileId: basename(profile.profileHome),
3375
3481
  profileRoot: profile.profileHome,
3376
3482
  operationIdentity: snapshotIdentity,
3483
+ forceDirty: true,
3377
3484
  });
3378
3485
  if (!backup.ok || backup.status !== "UPLOADED") {
3379
3486
  throw new Error(
@@ -3512,6 +3619,10 @@ async function runAdminProduction() {
3512
3619
  hermesRoot: DATA_ROOT,
3513
3620
  });
3514
3621
  if (!legacyUserState.ok) throw new Error(legacyUserState.code);
3622
+ const priorMaterializerReceipt = adminMaterializerReceipt(
3623
+ basename(profileContract.profileHome),
3624
+ profileContract.profileHome
3625
+ );
3515
3626
  const profile = await materializeAdminProfile({
3516
3627
  profileContract,
3517
3628
  settingsRoot: imported.receipt.generationRoot,
@@ -3522,6 +3633,32 @@ async function runAdminProduction() {
3522
3633
  slack: nativeSlackProfileInput(claim, installed),
3523
3634
  agentServiceEnv: adminAgentServiceEnvironment(claim),
3524
3635
  });
3636
+ const materializerReceipt = adminMaterializerReceipt(
3637
+ profile.profile,
3638
+ profile.profileHome
3639
+ );
3640
+ const activeProjection = buildDefaultProfileProjection({
3641
+ kind: "ADMIN",
3642
+ claim,
3643
+ });
3644
+ const reconciledDefaultProfile = await reconcileDefaultProfileBundle({
3645
+ activeProjection,
3646
+ materializerReceipt,
3647
+ profileRoot: profile.profileHome,
3648
+ bundleRoot: DEFAULT_PROFILE_BUNDLE_ROOT,
3649
+ dataRoot: DATA_ROOT,
3650
+ reattestAuthority: async () => {
3651
+ await operationHeartbeat.reattest();
3652
+ return activeProjection;
3653
+ },
3654
+ cronAdapter: adminDefaultProfileCronAdapter(
3655
+ profile.profile,
3656
+ profile.profileHome
3657
+ ),
3658
+ });
3659
+ if (!reconciledDefaultProfile.ok)
3660
+ throw new Error(reconciledDefaultProfile.code);
3661
+ const defaultProfile = reconciledDefaultProfile;
3525
3662
  const soulProof = observeProfileSoul(profile.profile, {
3526
3663
  dataRoot: DATA_ROOT,
3527
3664
  });
@@ -3534,6 +3671,13 @@ async function runAdminProduction() {
3534
3671
  installed,
3535
3672
  desiredLifecycle
3536
3673
  );
3674
+ const materializerChanged =
3675
+ canonicalJson(priorMaterializerReceipt) !==
3676
+ canonicalJson(materializerReceipt) ||
3677
+ profile.memoryCron.status !== "REUSED" ||
3678
+ profile.opportunitySequencesCron.status !== "REUSED";
3679
+ if (materializerChanged || defaultProfile.changed)
3680
+ markHermesSnapshotDirty(profile.profileHome);
3537
3681
  const memorySnapshot = await flushHermesSnapshot({
3538
3682
  client,
3539
3683
  profileId: profile.profile,
@@ -3560,17 +3704,6 @@ async function runAdminProduction() {
3560
3704
  exchange,
3561
3705
  });
3562
3706
  }
3563
- const receipt = completionReceipt({
3564
- claim,
3565
- exchange,
3566
- operation: proofOperation,
3567
- profile,
3568
- settings,
3569
- recovery,
3570
- observedLifecycle: desiredLifecycle,
3571
- memorySnapshot,
3572
- modelAuth,
3573
- });
3574
3707
  operationHeartbeat.assertHealthy();
3575
3708
  const slackProof =
3576
3709
  desiredLifecycle === "ON"
@@ -3585,6 +3718,26 @@ async function runAdminProduction() {
3585
3718
  if (candidateLaunch) {
3586
3719
  candidateGateway = await startNativeGateway(candidateLaunch);
3587
3720
  }
3721
+ const nativeDefaultProfileProof = candidateGateway
3722
+ ? proveDefaultProfileNativeSkills({
3723
+ actions: defaultProfile.actions,
3724
+ profileId: profile.profile,
3725
+ dataRoot: DATA_ROOT,
3726
+ })
3727
+ : { status: "EMPTY", digest: sha256("[]") };
3728
+ const receipt = completionReceipt({
3729
+ claim,
3730
+ exchange,
3731
+ operation: proofOperation,
3732
+ profile,
3733
+ settings,
3734
+ recovery,
3735
+ observedLifecycle: desiredLifecycle,
3736
+ memorySnapshot,
3737
+ modelAuth,
3738
+ activeProjection,
3739
+ nativeDefaultProfileProof,
3740
+ });
3588
3741
  if (claim.runtimeAuthority && candidateLaunch && candidateGateway) {
3589
3742
  const servingObservation = adminServingObservation(
3590
3743
  candidateLaunch,
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { constants, closeSync, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
9
+ const MANAGED_NAME = /^sellable-default-[a-z0-9][a-z0-9-]{0,63}$/;
10
+ const SHA256 = /^[a-f0-9]{64}$/;
11
+ const PYTHON = "/opt/hermes/.venv/bin/python3";
12
+ const MAX_REQUEST_BYTES = 64 * 1024;
13
+ const DESIRED_FIELDS = Object.freeze([
14
+ "name",
15
+ "enabled",
16
+ "schedule",
17
+ "prompt",
18
+ "skills",
19
+ "script",
20
+ "no_agent",
21
+ "deliver",
22
+ "toolsets",
23
+ "workdir",
24
+ ]);
25
+
26
+ const refused = (code) => ({ ok: false, status: "REFUSED", code });
27
+ const stable = (value) => {
28
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
29
+ if (value && typeof value === "object")
30
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(",")}}`;
31
+ return JSON.stringify(value);
32
+ };
33
+ const same = (left, right) => stable(left) === stable(right);
34
+
35
+ const NATIVE_READ_SCRIPT = String.raw`
36
+ import json, sqlite3, sys
37
+ from datetime import datetime
38
+ from pathlib import Path
39
+ request = json.loads(sys.stdin.read())
40
+ root, name = Path(request["profileRoot"]), request["name"]
41
+ jobs = json.loads((root / "cron" / "jobs.json").read_text()).get("jobs", [])
42
+ matches = [job for job in jobs if job.get("name") == name]
43
+ if len(matches) != 1:
44
+ print(json.dumps({"job": None, "history": []}, separators=(",", ":")))
45
+ raise SystemExit(0)
46
+ job = matches[0]
47
+ history = []
48
+ database = root / "cron" / "executions.db"
49
+ if database.is_file():
50
+ connection = sqlite3.connect(f"file:{database}?mode=ro", uri=True)
51
+ connection.row_factory = sqlite3.Row
52
+ try:
53
+ rows = connection.execute("SELECT id,status,finished_at FROM executions WHERE job_id=? ORDER BY claimed_at DESC,id DESC LIMIT 50", (str(job.get("id") or ""),)).fetchall()
54
+ for row in rows:
55
+ try:
56
+ finished = int(datetime.fromisoformat(row["finished_at"]).timestamp() * 1000) if row["finished_at"] else None
57
+ except (TypeError, ValueError):
58
+ finished = None
59
+ history.append({"id": row["id"], "status": "success" if row["status"] == "completed" else row["status"], "finishedAt": finished})
60
+ finally:
61
+ connection.close()
62
+ print(json.dumps({"job": job, "history": history}, separators=(",", ":")))
63
+ `;
64
+
65
+ function readRegularJson(path) {
66
+ const link = lstatSync(path);
67
+ if (link.isSymbolicLink() || !link.isFile() || link.nlink !== 1 || link.size > 64 * 1024)
68
+ throw new Error("cron_proof_marker_rejected");
69
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
70
+ try {
71
+ const opened = fstatSync(fd);
72
+ if (!opened.isFile() || opened.dev !== link.dev || opened.ino !== link.ino ||
73
+ opened.nlink !== 1 || opened.size !== link.size)
74
+ throw new Error("cron_proof_marker_rejected");
75
+ return JSON.parse(readFileSync(fd, "utf8"));
76
+ } finally {
77
+ closeSync(fd);
78
+ }
79
+ }
80
+
81
+ function desiredProjection(job, desired) {
82
+ if (!desired || typeof desired !== "object" || Array.isArray(desired)) return null;
83
+ const keys = Object.keys(desired);
84
+ if (keys.length === 0 || keys.some((key) => !DESIRED_FIELDS.includes(key))) return null;
85
+ return Object.fromEntries(keys.sort().map((key) => [key, job?.[key]]));
86
+ }
87
+
88
+ export async function proveHermesCronFiring({
89
+ profileId,
90
+ name,
91
+ operationId,
92
+ bootId,
93
+ bundleDigest,
94
+ nonce,
95
+ notBefore,
96
+ desired,
97
+ directExecution = false,
98
+ readNative,
99
+ readMarker,
100
+ } = {}) {
101
+ if (directExecution === true) return refused("cron_proof_direct_execution_invalid");
102
+ if (
103
+ !ID.test(profileId ?? "") ||
104
+ !MANAGED_NAME.test(name ?? "") ||
105
+ !ID.test(operationId ?? "") ||
106
+ !ID.test(bootId ?? "") ||
107
+ !SHA256.test(bundleDigest ?? "") ||
108
+ !ID.test(nonce ?? "") ||
109
+ !Number.isSafeInteger(notBefore) ||
110
+ notBefore < 0 ||
111
+ typeof readNative !== "function" ||
112
+ typeof readMarker !== "function"
113
+ ) {
114
+ return refused("cron_proof_request_rejected");
115
+ }
116
+
117
+ try {
118
+ const native = await readNative({ profileId, name });
119
+ const job = native?.job;
120
+ const projection = desiredProjection(job, desired);
121
+ if (
122
+ !job ||
123
+ typeof job !== "object" ||
124
+ !ID.test(job.id ?? "") ||
125
+ job.name !== name ||
126
+ job.enabled !== true ||
127
+ projection == null ||
128
+ !same(projection, desired) ||
129
+ !Array.isArray(native.history)
130
+ ) {
131
+ return refused("cron_proof_native_projection_rejected");
132
+ }
133
+ const successes = native.history.filter(
134
+ (run) =>
135
+ run &&
136
+ typeof run === "object" &&
137
+ ID.test(run.id ?? "") &&
138
+ run.status === "success" &&
139
+ Number.isSafeInteger(run.finishedAt) &&
140
+ run.finishedAt >= notBefore
141
+ );
142
+ if (successes.length === 0) return refused("cron_proof_history_stale");
143
+
144
+ const marker = await readMarker({ profileId, name, jobId: job.id });
145
+ if (
146
+ !marker ||
147
+ marker.operationId !== operationId ||
148
+ marker.bootId !== bootId ||
149
+ marker.bundleDigest !== bundleDigest ||
150
+ marker.nonce !== nonce ||
151
+ marker.jobId !== job.id ||
152
+ !Number.isSafeInteger(marker.firedAt) ||
153
+ marker.firedAt < notBefore ||
154
+ !successes.some((run) => run.finishedAt >= marker.firedAt)
155
+ ) {
156
+ return refused("cron_proof_marker_rejected");
157
+ }
158
+ return {
159
+ ok: true,
160
+ status: "PROVEN",
161
+ profileId,
162
+ name,
163
+ jobId: job.id,
164
+ projection,
165
+ successfulFirings: successes.length,
166
+ latestFinishedAt: Math.max(...successes.map((run) => run.finishedAt)),
167
+ exactOnceClaimed: false,
168
+ };
169
+ } catch {
170
+ return refused("cron_proof_read_failed");
171
+ }
172
+ }
173
+
174
+ // The image exposes this file as a command, but a caller must supply the
175
+ // gateway-bound native readers. Executing the marker script is never a proof.
176
+ export async function runFlyCronProofExec({
177
+ request,
178
+ dataRoot = "/opt/data",
179
+ python = PYTHON,
180
+ readNative,
181
+ readMarker,
182
+ } = {}) {
183
+ const profileRoot = resolve(dataRoot, "profiles", String(request?.profileId ?? ""));
184
+ const nativeReader = readNative ?? (() => {
185
+ const child = spawnSync(python, ["-c", NATIVE_READ_SCRIPT], {
186
+ input: JSON.stringify({ profileRoot, name: request?.name }),
187
+ encoding: "utf8",
188
+ timeout: 10_000,
189
+ maxBuffer: 1024 * 1024,
190
+ env: { PATH: "/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin" },
191
+ });
192
+ if (child.status !== 0 || child.signal || child.error) throw new Error("native_read");
193
+ return JSON.parse(child.stdout);
194
+ });
195
+ const markerReader = readMarker ?? (() =>
196
+ readRegularJson(resolve(profileRoot, ".sellable-agent", "cron-markers", `${request?.name}.json`))
197
+ );
198
+ return proveHermesCronFiring({ ...request, readNative: nativeReader, readMarker: markerReader });
199
+ }
200
+
201
+ if (resolve(process.argv[1] ?? "") === resolve(fileURLToPath(import.meta.url))) {
202
+ let result;
203
+ try {
204
+ if (process.argv.length !== 3 || process.argv[2].length > MAX_REQUEST_BYTES * 2)
205
+ throw new Error("request");
206
+ const bytes = Buffer.from(process.argv[2], "base64url");
207
+ if (bytes.byteLength > MAX_REQUEST_BYTES || bytes.toString("base64url") !== process.argv[2])
208
+ throw new Error("request");
209
+ result = await runFlyCronProofExec({
210
+ request: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)),
211
+ dataRoot: process.env.SELLABLE_AGENT_DATA_ROOT ?? "/opt/data",
212
+ });
213
+ } catch {
214
+ result = refused("cron_proof_request_rejected");
215
+ }
216
+ process.stdout.write(`${JSON.stringify(result)}\n`);
217
+ process.exitCode = result.ok ? 0 : 2;
218
+ }
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
8
8
 
9
9
  ARG MCP_PACKAGE=@sellable/mcp@0.1.879
10
10
  ARG MCP_INTEGRITY=sha512-dmrjCHuxLqR35HYvwXBQZoNMfCpT/wIWcvkT47cbYcWjL3pvxmxJWJBHFIZetgfQWM0pCAa+zKKAWyuxtAtz+w==
11
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.642
11
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.643
12
12
  ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.186-wip.phase158.20260802034727
13
13
  ARG ADMIN_MCP_INTEGRITY=sha512-qYNew5wd2IfgVcp1UYOG9QvpF/8Mx19YJ5HQtMKzvSfUf3evePjtaKsoXLf2C/GUJ5QlLlHJo84VWOI3V01+9A==
14
14
  ARG TUS_JS_CLIENT_VERSION=4.3.1
@@ -19,6 +19,9 @@ ARG TIRITH_VERSION=0.3.3
19
19
  ARG PLAYWRIGHT_VERSION=1.62.1
20
20
  ARG PLAYWRIGHT_BROWSER_REVISION=1234
21
21
  ARG TIRITH_ARCHIVE_SHA256=6cdbe35e8f9ccf42e70ad95b501c93cd218ac18201c3df958d54f6ba0d995ce2
22
+ ARG DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION=
23
+ ARG DEFAULT_PROFILE_BUNDLE_VERSION=1
24
+ ARG DEFAULT_PROFILE_BUNDLE_DIGEST=a57a043c8788a752c470cdc94eeb921a062315910da9111daa546815415176fc
22
25
 
23
26
  RUN --mount=type=secret,id=mcp_trust_root,required=true \
24
27
  test -s /run/secrets/mcp_trust_root \
@@ -76,8 +79,11 @@ RUN --mount=type=secret,id=mcp_trust_root,required=true \
76
79
  /etc/cont-init.d/02-reconcile-profiles \
77
80
  && npm cache clean --force
78
81
 
79
- COPY mcp-context-proxy.mjs mcp-runtime-binding.mjs fly-customer-model.mjs fly-customer-worker.mjs profile-materializer.mjs hermes-bridge.mjs hermes-memory-snapshot.mjs fly-runtime-identity.mjs fly-soul-bridge.mjs fly-soul-bridge-exec.mjs fly-skills-bridge.mjs fly-skills-bridge-exec.mjs fly-capabilities-bridge.mjs fly-capabilities-bridge-exec.mjs soul-artifact-validator.mjs model-auth-reconciler.mjs model-auth-canary-reseed.mjs model-health-reporter.mjs \
82
+ COPY mcp-context-proxy.mjs mcp-runtime-binding.mjs fly-customer-model.mjs fly-customer-worker.mjs profile-materializer.mjs hermes-bridge.mjs hermes-memory-snapshot.mjs fly-runtime-identity.mjs fly-soul-bridge.mjs fly-soul-bridge-exec.mjs fly-skills-bridge.mjs fly-skills-bridge-exec.mjs fly-capabilities-bridge.mjs fly-capabilities-bridge-exec.mjs soul-artifact-validator.mjs default-profile-bundle.mjs default-profile-reconciler.mjs fly-cron-proof-exec.mjs model-auth-reconciler.mjs model-auth-canary-reseed.mjs model-health-reporter.mjs \
80
83
  /usr/local/lib/sellable-agent/
84
+ COPY default-profile-bundles/shared/ /usr/local/lib/sellable-agent/default-profile-bundles/shared/
85
+ COPY default-profile-bundles/customer/ /usr/local/lib/sellable-agent/default-profile-bundles/customer/
86
+ COPY default-profile-bundles/fixtures/ /usr/local/lib/sellable-agent/default-profile-bundles/fixtures/
81
87
  COPY hermes-memory-dirty.mjs /opt/sellable/bin/hermes-memory-dirty
82
88
  COPY hermes-memory-reconcile.sh /opt/sellable/share/hermes-memory-reconcile.sh
83
89
  COPY hermes-endpoint-cron.mjs /opt/sellable/bin/hermes-endpoint-cron
@@ -87,7 +93,18 @@ COPY fly-customer-image/endpoint-uat-completion-gate.mjs /usr/local/lib/sellable
87
93
  COPY fly-customer-image/customer-runtime.mjs /usr/local/lib/sellable-agent/fly-customer-image/customer-runtime.mjs
88
94
  COPY fly-customer-image/rootfs/ /
89
95
 
90
- RUN node --input-type=module -e \
96
+ RUN set -eux; \
97
+ fixture_args=""; \
98
+ if test -n "${DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION}"; then \
99
+ fixture_args="--fixture-version ${DEFAULT_PROFILE_BUNDLE_FIXTURE_VERSION}"; \
100
+ fi; \
101
+ node /usr/local/lib/sellable-agent/default-profile-bundle.mjs \
102
+ --kind CUSTOMER ${fixture_args} \
103
+ --output /usr/local/share/sellable-agent/default-profile-bundle \
104
+ && node -e \
105
+ '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);' \
106
+ "${DEFAULT_PROFILE_BUNDLE_VERSION}" "${DEFAULT_PROFILE_BUNDLE_DIGEST}" \
107
+ && node --input-type=module -e \
91
108
  '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 });' \
92
109
  && node --input-type=module -e \
93
110
  'await import("/usr/local/lib/sellable-agent/fly-runtime-identity.mjs");' \
@@ -118,11 +135,15 @@ RUN node --input-type=module -e \
118
135
  && sha256sum /usr/local/lib/sellable-agent/fly-skills-bridge.mjs \
119
136
  /usr/local/lib/sellable-agent/fly-skills-bridge-exec.mjs \
120
137
  > /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
138
+ && sha256sum /usr/local/lib/sellable-agent/default-profile-reconciler.mjs \
139
+ /usr/local/lib/sellable-agent/fly-cron-proof-exec.mjs \
140
+ > /usr/local/share/sellable-agent/default-profile-reconcile-source.sha256 \
121
141
  && sha256sum /usr/local/lib/sellable-agent/fly-capabilities-bridge.mjs \
122
142
  /usr/local/lib/sellable-agent/fly-capabilities-bridge-exec.mjs \
123
143
  > /usr/local/share/sellable-agent/capabilities-bridge-source.sha256 \
124
144
  && chmod 0444 /usr/local/share/sellable-agent/soul-bridge-source.sha256 \
125
145
  /usr/local/share/sellable-agent/skills-bridge-source.sha256 \
146
+ /usr/local/share/sellable-agent/default-profile-reconcile-source.sha256 \
126
147
  /usr/local/share/sellable-agent/capabilities-bridge-source.sha256 \
127
148
  && chmod 0755 /etc \
128
149
  && chmod 0555 /usr/local/lib/sellable-agent/*.mjs \
@@ -135,7 +156,9 @@ RUN node --input-type=module -e \
135
156
  LABEL io.sellable.runtime.kind="customer" \
136
157
  io.sellable.runtime.listener="native-dedicated-slack" \
137
158
  io.sellable.runtime.api-server="authenticated-loopback-127.0.0.1:8642" \
138
- io.sellable.runtime.installers="build-only"
159
+ io.sellable.runtime.installers="build-only" \
160
+ io.sellable.runtime.default-profile-bundle-version="${DEFAULT_PROFILE_BUNDLE_VERSION}" \
161
+ io.sellable.runtime.default-profile-bundle-digest="${DEFAULT_PROFILE_BUNDLE_DIGEST}"
139
162
 
140
163
  ENV HOME=/opt/data \
141
164
  HERMES_HOME=/opt/data \
@@ -146,6 +169,8 @@ ENV HOME=/opt/data \
146
169
  SELLABLE_AGENT_HERMES_VERSION=${HERMES_VERSION} \
147
170
  SELLABLE_AGENT_INSTALLER_PACKAGE=${INSTALLER_PACKAGE} \
148
171
  SELLABLE_AGENT_MCP_PACKAGE=${MCP_PACKAGE} \
172
+ SELLABLE_AGENT_DEFAULT_PROFILE_BUNDLE_VERSION=${DEFAULT_PROFILE_BUNDLE_VERSION} \
173
+ SELLABLE_AGENT_DEFAULT_PROFILE_BUNDLE_DIGEST=${DEFAULT_PROFILE_BUNDLE_DIGEST} \
149
174
  SELLABLE_AGENT_PLAYWRIGHT_VERSION=${PLAYWRIGHT_VERSION} \
150
175
  SELLABLE_AGENT_PLAYWRIGHT_BROWSER_REVISION=${PLAYWRIGHT_BROWSER_REVISION} \
151
176
  PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1