@sellable/install 0.1.641 → 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);
@@ -1852,6 +1944,8 @@ function adminAgentServiceEnvironment(claim) {
1852
1944
  adminGeneration.agentId !== operation?.agentId ||
1853
1945
  !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(adminGeneration.id ?? "") ||
1854
1946
  !/^[1-9][0-9]{0,19}$/.test(String(adminGeneration.fence ?? "")) ||
1947
+ !Number.isSafeInteger(RUNTIME_GENERATION) ||
1948
+ RUNTIME_GENERATION < 1 ||
1855
1949
  !operation ||
1856
1950
  !service ||
1857
1951
  !/^[A-Za-z0-9_-]{1,128}$/.test(service.principalId ?? "") ||
@@ -1886,6 +1980,7 @@ function adminAgentServiceEnvironment(claim) {
1886
1980
  // customer profile while the signed Agent service context remains the home
1887
1981
  // workspace authority.
1888
1982
  SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND: "ADMIN",
1983
+ SELLABLE_AGENT_RUNTIME_GENERATION: String(RUNTIME_GENERATION),
1889
1984
  SELLABLE_AGENT_ADMIN_GENERATION_ID: claim.adminRuntimeGeneration.id,
1890
1985
  SELLABLE_AGENT_ADMIN_GENERATION_FENCE: String(
1891
1986
  claim.adminRuntimeGeneration.fence
@@ -2434,6 +2529,8 @@ function completionReceipt({
2434
2529
  observedLifecycle,
2435
2530
  memorySnapshot,
2436
2531
  modelAuth,
2532
+ activeProjection,
2533
+ nativeDefaultProfileProof,
2437
2534
  }) {
2438
2535
  const identity = operationIdentity(claim, exchange);
2439
2536
  const routeGenerationDigest = flyRuntimeDigest(
@@ -2489,6 +2586,12 @@ function completionReceipt({
2489
2586
  secretDisposition: "INSTALLED",
2490
2587
  observedLifecycle,
2491
2588
  observedRevisionId: claim.operation.targetRevisionId,
2589
+ ...(activeProjection
2590
+ ? {
2591
+ bundleVersion: activeProjection.bundleVersion,
2592
+ bundleDigest: activeProjection.bundleDigest,
2593
+ }
2594
+ : {}),
2492
2595
  signerGeneration: exchange.keyGeneration,
2493
2596
  signedDigest: flyRuntimeDigest({
2494
2597
  ...identity,
@@ -2511,6 +2614,11 @@ function completionReceipt({
2511
2614
  },
2512
2615
  { code: "SLACK_TRAFFIC_DISABLED", status: "TRUE" },
2513
2616
  { code: "NO_BOOT_INSTALL", status: "TRUE" },
2617
+ {
2618
+ code: "DEFAULT_PROFILE_NATIVE_PROOF",
2619
+ status:
2620
+ nativeDefaultProfileProof.status === "PROVEN" ? "TRUE" : "UNKNOWN",
2621
+ },
2514
2622
  ],
2515
2623
  errorClass: "NONE",
2516
2624
  auditIds: [
@@ -2530,9 +2638,10 @@ async function flushHermesSnapshot({
2530
2638
  profileId,
2531
2639
  profileRoot,
2532
2640
  operationIdentity,
2641
+ forceDirty = false,
2533
2642
  }) {
2534
2643
  try {
2535
- markHermesSnapshotDirty(profileRoot);
2644
+ if (forceDirty) markHermesSnapshotDirty(profileRoot);
2536
2645
  return await reconcileHermesSnapshot({
2537
2646
  profileId,
2538
2647
  profileRoot,
@@ -3371,6 +3480,7 @@ async function runAdminProduction() {
3371
3480
  profileId: basename(profile.profileHome),
3372
3481
  profileRoot: profile.profileHome,
3373
3482
  operationIdentity: snapshotIdentity,
3483
+ forceDirty: true,
3374
3484
  });
3375
3485
  if (!backup.ok || backup.status !== "UPLOADED") {
3376
3486
  throw new Error(
@@ -3509,6 +3619,10 @@ async function runAdminProduction() {
3509
3619
  hermesRoot: DATA_ROOT,
3510
3620
  });
3511
3621
  if (!legacyUserState.ok) throw new Error(legacyUserState.code);
3622
+ const priorMaterializerReceipt = adminMaterializerReceipt(
3623
+ basename(profileContract.profileHome),
3624
+ profileContract.profileHome
3625
+ );
3512
3626
  const profile = await materializeAdminProfile({
3513
3627
  profileContract,
3514
3628
  settingsRoot: imported.receipt.generationRoot,
@@ -3519,6 +3633,32 @@ async function runAdminProduction() {
3519
3633
  slack: nativeSlackProfileInput(claim, installed),
3520
3634
  agentServiceEnv: adminAgentServiceEnvironment(claim),
3521
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;
3522
3662
  const soulProof = observeProfileSoul(profile.profile, {
3523
3663
  dataRoot: DATA_ROOT,
3524
3664
  });
@@ -3531,6 +3671,13 @@ async function runAdminProduction() {
3531
3671
  installed,
3532
3672
  desiredLifecycle
3533
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);
3534
3681
  const memorySnapshot = await flushHermesSnapshot({
3535
3682
  client,
3536
3683
  profileId: profile.profile,
@@ -3557,17 +3704,6 @@ async function runAdminProduction() {
3557
3704
  exchange,
3558
3705
  });
3559
3706
  }
3560
- const receipt = completionReceipt({
3561
- claim,
3562
- exchange,
3563
- operation: proofOperation,
3564
- profile,
3565
- settings,
3566
- recovery,
3567
- observedLifecycle: desiredLifecycle,
3568
- memorySnapshot,
3569
- modelAuth,
3570
- });
3571
3707
  operationHeartbeat.assertHealthy();
3572
3708
  const slackProof =
3573
3709
  desiredLifecycle === "ON"
@@ -3582,6 +3718,26 @@ async function runAdminProduction() {
3582
3718
  if (candidateLaunch) {
3583
3719
  candidateGateway = await startNativeGateway(candidateLaunch);
3584
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
+ });
3585
3741
  if (claim.runtimeAuthority && candidateLaunch && candidateGateway) {
3586
3742
  const servingObservation = adminServingObservation(
3587
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.641
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