@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.
- 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 +168 -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);
|
|
@@ -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.
|
|
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
|