@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.
- package/container/Dockerfile +4 -4
- package/container/README.md +1 -1
- package/lib/sellable-agent/default-profile-bundle.mjs +318 -0
- package/lib/sellable-agent/default-profile-bundles/customer/manifest.fragment.json +8 -0
- package/lib/sellable-agent/default-profile-bundles/fixtures/v1/manifest.fragment.json +26 -0
- package/lib/sellable-agent/default-profile-bundles/fixtures/v2/manifest.fragment.json +34 -0
- package/lib/sellable-agent/default-profile-bundles/shared/manifest.fragment.json +8 -0
- package/lib/sellable-agent/default-profile-reconciler.mjs +508 -0
- package/lib/sellable-agent/fly-admin-image/Dockerfile +64 -4
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +165 -12
- package/lib/sellable-agent/fly-cron-proof-exec.mjs +218 -0
- package/lib/sellable-agent/fly-customer-image/Dockerfile +29 -4
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +131 -3
- package/lib/sellable-agent/fly-customer-worker.mjs +20 -2
- package/lib/sellable-agent/fly-runtime-identity.mjs +41 -2
- package/lib/sellable-agent/fly-skills-bridge.mjs +63 -7
- package/lib/sellable-agent/host-bootstrap.mjs +1 -1
- package/lib/sellable-agent/host-worker.mjs +1 -1
- package/lib/sellable-agent/profile-materializer.mjs +1 -1
- package/lib/sellable-agent/provisioning-adapter.mjs +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
|
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
|