@githolon/testing 0.15.0 → 0.16.0
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/README.md +3 -3
- package/build.mjs +14 -6
- package/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/index.mjs +4 -4
- package/vendor/engine/custody-transport.mjs +167 -0
- package/vendor/engine/engine.mjs +751 -291
- package/vendor/engine/git-fs.mjs +5 -1
- package/vendor/engine/tree.mjs +15 -8
package/vendor/engine/engine.mjs
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
|
-
// AUTO-SYNCED from cloud/
|
|
2
|
-
// Nomos
|
|
1
|
+
// AUTO-SYNCED from cloud/cloudflare-shell/src/engine.mjs by testing/build.mjs — DO NOT EDIT HERE.
|
|
2
|
+
// Nomos — THE CLOUDFLARE SHELL's ENGINE PLANE (the adapter logic that boots + drives the offer-kernel).
|
|
3
3
|
//
|
|
4
|
-
// Everything that
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// Everything that boots the offer-kernel wasm + drives its /work tree lives HERE (the adapter the
|
|
5
|
+
// Cloudflare shell wraps; "host" is out — OFFER_KERNEL_ARCHITECTURE.dot). THE OFFER-KERNEL RUNS IN THE
|
|
6
|
+
// 12GiB CONTAINER — that is its only runtime now (we no longer run the kernel on the edge DO). The
|
|
7
|
+
// container (container/server.mjs) is a thin Node wrapper around this file: wasm compiled from disk, replica
|
|
8
|
+
// from the container instance base, ledgers ARRIVE on each op (containers have no Workers bindings — the
|
|
9
|
+
// control-plane DO mints the Artifacts token and sends it).
|
|
8
10
|
//
|
|
9
|
-
//
|
|
10
|
-
// replica from the DO id, ledgers minted from env.ARTIFACTS, runs in workerd;
|
|
11
|
-
// * the HEAVY host (container/holon-worker.mjs): wasm compiled from disk, replica
|
|
12
|
-
// from the container instance base, ledgers ARRIVE on each op (containers have no
|
|
13
|
-
// Workers bindings — the control-plane DO mints tokens and sends them), runs in Node.
|
|
14
|
-
//
|
|
15
|
-
// CONTROL PLANE STAYS IN THE DO: ownership secrets, the DLQ store,
|
|
11
|
+
// CONTROL PLANE STAYS IN THE DO (CloudflareShellDO — it does NOT run the kernel): ownership secrets, the DLQ store,
|
|
16
12
|
// quotas/rates, alarms. The engine RETURNS data (e.g. dead-letter entries with intent
|
|
17
|
-
// bytes) and the
|
|
13
|
+
// bytes) and the adapter persists. Runtime-neutral: workerd + Node 22 (globalThis.crypto,
|
|
18
14
|
// fetch, TextEncoder are the only ambient dependencies).
|
|
19
15
|
import { WASI, File, Directory, OpenFile, ConsoleStdout, PreopenDirectory } from "@bjorn3/browser_wasi_shim";
|
|
20
|
-
import git from "isomorphic-git";
|
|
21
|
-
import http from "isomorphic-git/http/web";
|
|
22
16
|
import { makeGitFs } from "./git-fs.mjs";
|
|
23
17
|
import { serializeTree } from "./tree.mjs";
|
|
18
|
+
// THE DUMB-PIPE CUSTODY TRANSPORT (raw git smart-HTTP over fetch, zero isomorphic-git). Same-dir module,
|
|
19
|
+
// synced into the container context by deploy.sh alongside engine.mjs.
|
|
20
|
+
import { lsRefs, fetchPack, pushPack, ZERO_OID } from "./custody-transport.mjs";
|
|
24
21
|
|
|
25
22
|
const enc = new TextEncoder(), dec = new TextDecoder();
|
|
26
23
|
const BRANCH = "main";
|
|
27
24
|
export const repoArgOf = (ws) => `/work/ws/${ws}`;
|
|
28
25
|
export const gitdirOf = (ws) => `/work/ws/${ws}/nomos.git`;
|
|
26
|
+
function b64Bytes(bytes) {
|
|
27
|
+
let s = "";
|
|
28
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
29
|
+
s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
30
|
+
}
|
|
31
|
+
return btoa(s);
|
|
32
|
+
}
|
|
33
|
+
function b64Json(o) {
|
|
34
|
+
return b64Bytes(enc.encode(JSON.stringify(o)));
|
|
35
|
+
}
|
|
29
36
|
|
|
30
37
|
const unpack = (p) => { const v = typeof p === "bigint" ? p : BigInt(p); return { ptr: Number(v >> 32n), len: Number(v & 0xffffffffn) }; };
|
|
31
38
|
export async function sha256hex(t) { const b = await crypto.subtle.digest("SHA-256", enc.encode(t)); return [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, "0")).join(""); }
|
|
@@ -55,23 +62,62 @@ function call(ex, mode, fields, STDERR) {
|
|
|
55
62
|
} finally { ex.git_holon_dealloc(reqPtr, req.length); }
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
// ── KERNEL GIT PLUMBING (kgit) — the cloud shell speaks NO git; the kernel (gix) owns the odb. ──
|
|
66
|
+
// These wrap the kernel's local-object RPCs (the adapter is a byte pipe, never touches objects/refs).
|
|
67
|
+
// They mirror the isomorphic-git call shapes they replace so the migration is a 1:1 substitution.
|
|
68
|
+
// The repo dir the wasm opens is `${repoArg}/nomos.git`; the adapter names it as `gitdir` everywhere, so
|
|
69
|
+
// derive repoArg back from it (gitdir === gitdirOf(ws) === `${repoArgOf(ws)}/nomos.git`).
|
|
70
|
+
const repoArgOfGitdir = (gitdir) => gitdir.replace(/\/nomos\.git$/, "");
|
|
71
|
+
const bytesFromB64 = (b64) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
72
|
+
const kgit = {
|
|
73
|
+
// resolveRef → tip oid hex, or null if the ref is absent (replaces git.resolveRef(...).catch(()=>null)).
|
|
74
|
+
resolveRef: (eng, gitdir, ref) =>
|
|
75
|
+
JSON.parse(call(eng.ex, "resolve_ref", { repoArg: repoArgOfGitdir(gitdir), ref }, eng.STDERR)).oid,
|
|
76
|
+
// readBlob → the blob bytes at `path` inside commit `oid`, or null if not found (no throw).
|
|
77
|
+
readBlob: (eng, gitdir, oid, path) => {
|
|
78
|
+
const r = JSON.parse(call(eng.ex, "read_blob", { repoArg: repoArgOfGitdir(gitdir), oid, path }, eng.STDERR));
|
|
79
|
+
return r.ok ? bytesFromB64(r.blobB64) : null;
|
|
80
|
+
},
|
|
81
|
+
// intentsAbove → [{ oid, intentB64 }] oldest→newest over (base, head] (replaces log + readBlob intent.json).
|
|
82
|
+
// `limit` (0 = all) caps to the most-recent N intent commits (replaces git.log depth).
|
|
83
|
+
intentsAbove: (eng, gitdir, head, base = "", limit = 0) =>
|
|
84
|
+
JSON.parse(call(eng.ex, "intents_above", { repoArg: repoArgOfGitdir(gitdir), head, base, limit }, eng.STDERR)).intents,
|
|
85
|
+
// listRefs → [{ ref, oid }] under prefix (replaces git.listRefs for LOCAL refs).
|
|
86
|
+
listRefs: (eng, gitdir, prefix) =>
|
|
87
|
+
JSON.parse(call(eng.ex, "list_refs", { repoArg: repoArgOfGitdir(gitdir), prefix }, eng.STDERR)).refs,
|
|
88
|
+
// writeRef → force-point a ref (plain oid or `ref:`-symbolic) (replaces git.writeRef).
|
|
89
|
+
writeRef: (eng, gitdir, ref, value) =>
|
|
90
|
+
JSON.parse(call(eng.ex, "write_ref", { repoArg: repoArgOfGitdir(gitdir), ref, value }, eng.STDERR)),
|
|
91
|
+
// deleteRef → remove a local ref, idempotent (replaces git.deleteRef).
|
|
92
|
+
deleteRef: (eng, gitdir, ref) =>
|
|
93
|
+
JSON.parse(call(eng.ex, "delete_ref", { repoArg: repoArgOfGitdir(gitdir), ref }, eng.STDERR)),
|
|
94
|
+
// writeMetaCommit → blobs→tree→commit→ref for a CUSTODY-META fact (checkpoint/refused/outbox/shape).
|
|
95
|
+
// `files`: [{ path, bytes:Uint8Array }]; returns the commit oid (replaces writeBlob+writeTree+writeCommit[+writeRef]).
|
|
96
|
+
writeMetaCommit: (eng, gitdir, { parents = [], files, message = "nomos custody meta", timeSecs = 0, ref = "" }) =>
|
|
97
|
+
JSON.parse(call(eng.ex, "write_meta_commit", {
|
|
98
|
+
repoArg: repoArgOfGitdir(gitdir), parents,
|
|
99
|
+
files: files.map((f) => ({ path: f.path, blobB64: b64Bytes(f.bytes) })),
|
|
100
|
+
message, timeSecs, ref,
|
|
101
|
+
}, eng.STDERR)).oid,
|
|
102
|
+
};
|
|
103
|
+
|
|
58
104
|
function seedManifests(_m, _strings) {
|
|
59
105
|
// Runtime manifests are committed inside USDA packages and derived by the holon
|
|
60
106
|
// from active installed law. Hosts must not seed admission/projection sidecars.
|
|
61
107
|
}
|
|
62
108
|
|
|
63
|
-
// (#39 BAILIFF: no
|
|
109
|
+
// (#39 BAILIFF: no adapter-chosen `authorityScope` literal. It was a dead, hardcoded "workspace/root" stamped
|
|
64
110
|
// onto EVERY install — incl. non-root tenants — that NOTHING in the kernel ever reads for a decision
|
|
65
|
-
// (domain_lifecycle.rs folds it to an Option<String> and never branches on it). The
|
|
111
|
+
// (domain_lifecycle.rs folds it to an Option<String> and never branches on it). The adapter carries the
|
|
66
112
|
// package bytes; it does not author an authority-scoping fact the law never declared. Absent ⇒ folded None.)
|
|
67
113
|
export const installPayload = (hash, usda, installedBy, dispositions) => ({ domainHash: hash, packageUsda: usda, installedBy, dependencies: [], finalizers: [], ...(dispositions ? { dispositions } : {}) });
|
|
68
114
|
|
|
69
115
|
/**
|
|
70
116
|
* Boot the resident wasm + /work tree. `wasmModule` is a PRECOMPILED WebAssembly.Module
|
|
71
|
-
* (the
|
|
117
|
+
* (the adapter compiles its way); `replica` = the adapter's unique 63-bit id.
|
|
72
118
|
*
|
|
73
119
|
* Runtime read/admission metadata is sovereign holon state: committed USDA packages
|
|
74
|
-
* are folded by the githolon, and the
|
|
120
|
+
* are folded by the githolon, and the adapter never stages manifest sidecars.
|
|
75
121
|
*/
|
|
76
122
|
export async function createEngine({ wasmModule, bootstrapPkg, nomosPkg, replica, installedBy = "nomos-cloud" }) {
|
|
77
123
|
const hashes = { bootstrap: await sha256hex(bootstrapPkg), nomos: await sha256hex(nomosPkg) };
|
|
@@ -96,11 +142,11 @@ export async function createEngine({ wasmModule, bootstrapPkg, nomosPkg, replica
|
|
|
96
142
|
return eng;
|
|
97
143
|
}
|
|
98
144
|
|
|
99
|
-
// There is NO
|
|
145
|
+
// There is NO adapter→holon manifest channel. The wasm derives every read/identity
|
|
100
146
|
// manifest from the FOLDED ledger (installed_read_manifest_bytes_from_state) and
|
|
101
|
-
// refuses a package lacking nomosReadProjection. A
|
|
147
|
+
// refuses a package lacking nomosReadProjection. A adapter cannot provide a manifest:
|
|
102
148
|
// the op does not exist (Read Confinement — the kernel's transition function, not a
|
|
103
|
-
//
|
|
149
|
+
// adapter convention). `setManifests` is gone, not no-op'd, so the lie is unrepresentable.
|
|
104
150
|
|
|
105
151
|
// ── TRANSPARENT SHARDING: the WRONG-HOME GATE (the edge bailiff — sharding slice 2) ──
|
|
106
152
|
//
|
|
@@ -117,8 +163,8 @@ export async function createEngine({ wasmModule, bootstrapPkg, nomosPkg, replica
|
|
|
117
163
|
/** directive routes off the effective identity manifests: `${domain} ${directiveId}` → route. */
|
|
118
164
|
export function directiveRoutes(eng) {
|
|
119
165
|
void eng;
|
|
120
|
-
//
|
|
121
|
-
// taxonomy API before it can be re-enabled without leaking law into the
|
|
166
|
+
// Adapter-side manifest taxonomy is retired. Shard routing needs a holon-owned
|
|
167
|
+
// taxonomy API before it can be re-enabled without leaking law into the adapter.
|
|
122
168
|
return new Map();
|
|
123
169
|
}
|
|
124
170
|
|
|
@@ -133,7 +179,7 @@ function routeTagHexOfMintedId(id) {
|
|
|
133
179
|
}
|
|
134
180
|
|
|
135
181
|
/**
|
|
136
|
-
* Judge ONE routed intent against the shard map. `shardRouting` is
|
|
182
|
+
* Judge ONE routed intent against the shard map. `shardRouting` is ADAPTER-INJECTED:
|
|
137
183
|
* `{ selfLabel, coordinatorWs, assignments() }` — `selfLabel` is this workspace's shard
|
|
138
184
|
* label (`s<k>`) or null when it IS the coordinator; `assignments()` resolves the
|
|
139
185
|
* ACTIVE `NomosShardAssignment` rows from the coordinator's law-state. Returns null to
|
|
@@ -187,7 +233,7 @@ export async function wrongHomeVerdict(shardRouting, route, payload) {
|
|
|
187
233
|
|
|
188
234
|
// ── §5.2 — THE DELTA LANE (sharding slice 3): the shard-side EMITTER machinery ──
|
|
189
235
|
//
|
|
190
|
-
// After a shard's edge admission folds a batch, the
|
|
236
|
+
// After a shard's edge admission folds a batch, the ADAPTER authors one coalesced
|
|
191
237
|
// `nomosPropagateSummary` Order into the COORDINATOR's chain: per touched
|
|
192
238
|
// (scoped read, group) the ABSOLUTE subtotal + the covered range's per-intent
|
|
193
239
|
// event deltas as content-addressed evidence. The coordinator's ONE gate
|
|
@@ -209,7 +255,7 @@ export const NOMOS_SHARD_ROWS_READ = "nomosShardRows";
|
|
|
209
255
|
const FRAMEWORK_AGG_TYPES = new Set([
|
|
210
256
|
"NomosShardAssignment", "NomosShardIdentity", "NomosHomeReceipt", "NomosShardRegistry",
|
|
211
257
|
"NomosShardPolicy", "NomosSummarySubtotal", "NomosSummaryFrontier", "NomosDeepVerify",
|
|
212
|
-
"NomosCheckpointSeal",
|
|
258
|
+
"NomosCheckpointSeal", "NomosMoveObligation", // P6 — the move-leg obligation (coordinator law-state)
|
|
213
259
|
// CONTROL-PLANE law-state (every chain carries them via installDomain) — never tenant data:
|
|
214
260
|
"PolicyBundle", "DomainInstallation",
|
|
215
261
|
]);
|
|
@@ -233,7 +279,7 @@ export function placementDirectives(eng) {
|
|
|
233
279
|
/** The ESTATE-SCOPED maintained reads off the effective identity manifests.
|
|
234
280
|
* SLICE 4: `where` (the canonical predicate, hash-bearing in the manifest) rides
|
|
235
281
|
* along — the LIVE capture's oracle is the projection's maintained tally (already
|
|
236
|
-
* predicate-aware in the kernel); the SUFFIX re-derivation evaluates it
|
|
282
|
+
* predicate-aware in the kernel); the SUFFIX re-derivation evaluates it adapter-side
|
|
237
283
|
* ({@link evalCanonicalPred}) so both lanes agree byte-for-byte. */
|
|
238
284
|
export function scopedReads(eng) {
|
|
239
285
|
void eng;
|
|
@@ -242,7 +288,7 @@ export function scopedReads(eng) {
|
|
|
242
288
|
|
|
243
289
|
/**
|
|
244
290
|
* Evaluate a canonical count/sum predicate (`predicate.ts` CanonicalPred — eq/ne
|
|
245
|
-
* clauses + and/or trees) over a
|
|
291
|
+
* clauses + and/or trees) over a adapter-folded field map. EXACTLY the kernel's
|
|
246
292
|
* declared semantics: values canonicalize to strings (int fields compare their
|
|
247
293
|
* decimal form); `eq` over an ABSENT field is FALSE, `ne` over an absent field is
|
|
248
294
|
* TRUE (unset ≠ any literal — the §7-D symmetry rule). Used ONLY by the suffix
|
|
@@ -365,7 +411,7 @@ function finishSummaryCapture(eng, ws, ctx, cap, oid, intentId) {
|
|
|
365
411
|
ctx.values.set(key, { read, group, value: after });
|
|
366
412
|
}
|
|
367
413
|
// The synthetic row-count read: existence flips, with the projection as oracle.
|
|
368
|
-
// The ABSOLUTE rides relative to the coordinator-held value (the
|
|
414
|
+
// The ABSOLUTE rides relative to the coordinator-held value (the ADAPTER closes it
|
|
369
415
|
// at emit time — a shard never enumerates its own whole projection per batch).
|
|
370
416
|
// SLICE 8 — EXACT PER-HOME SUBTOTALS: each flip is also attributed to its HOME
|
|
371
417
|
// (the row id's route tag, or the row id itself for an axis root) so the split
|
|
@@ -396,7 +442,7 @@ function finishSummaryCapture(eng, ws, ctx, cap, oid, intentId) {
|
|
|
396
442
|
|
|
397
443
|
/**
|
|
398
444
|
* The coalesced delta of one admission batch (null when nothing summarizable moved).
|
|
399
|
-
* `rowsDelta` rides separately: the
|
|
445
|
+
* `rowsDelta` rides separately: the ADAPTER closes the `nomosShardRows` absolute against
|
|
400
446
|
* the coordinator-held value at emit time (held + Σ deltas — the gate re-checks both
|
|
401
447
|
* halves). The other subtotals' `prev` is the batch-start maintained tally, which
|
|
402
448
|
* equals the coordinator-held value exactly when the frontier is contiguous — and the
|
|
@@ -431,7 +477,7 @@ function summaryOfCapture(ctx, fromOid, toOid) {
|
|
|
431
477
|
subtotals: subtotals.sort((a, b) => (summaryKey(a.readId, a.group) < summaryKey(b.readId, b.group) ? -1 : 1)),
|
|
432
478
|
events: ctx.events,
|
|
433
479
|
rowsDelta: ctx.rowsDelta,
|
|
434
|
-
// SLICE 8: the exact per-home row deltas — the
|
|
480
|
+
// SLICE 8: the exact per-home row deltas — the adapter closes each against the
|
|
435
481
|
// coordinator-held per-home subtotal exactly like the "" grand total.
|
|
436
482
|
rowsByHome: Object.fromEntries([...ctx.rowsByHome.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))),
|
|
437
483
|
};
|
|
@@ -439,7 +485,7 @@ function summaryOfCapture(ctx, fromOid, toOid) {
|
|
|
439
485
|
|
|
440
486
|
/**
|
|
441
487
|
* THE SUFFIX RE-DERIVATION (the frontier-gap self-heal + /summaries/resync): derive
|
|
442
|
-
* the delta payload for (fromOid → main head] from THE CHAIN ITSELF — a
|
|
488
|
+
* the delta payload for (fromOid → main head] from THE CHAIN ITSELF — a adapter-side
|
|
443
489
|
* pure fold of the scoped reads' group/sum fields over every commit since genesis
|
|
444
490
|
* (Lww in chain order; scoped reads are predicate-free by the compile gate). Used
|
|
445
491
|
* when the coordinator's recorded frontier does not match a live capture (a lost
|
|
@@ -448,10 +494,9 @@ function summaryOfCapture(ctx, fromOid, toOid) {
|
|
|
448
494
|
export async function deriveSummarySuffix(eng, ws, fromOid) {
|
|
449
495
|
const reads = scopedReads(eng);
|
|
450
496
|
const gitdir = gitdirOf(ws);
|
|
451
|
-
const head =
|
|
497
|
+
const head = kgit.resolveRef(eng, gitdir, BRANCH);
|
|
452
498
|
if (!head) return { ok: false, error: "no local main to derive from" };
|
|
453
|
-
const commits =
|
|
454
|
-
commits.reverse(); // genesis → head
|
|
499
|
+
const commits = kgit.intentsAbove(eng, gitdir, head); // [{oid, intentB64}] genesis → head
|
|
455
500
|
const state = new Map(); // aggregateId -> { type, fields: Map }
|
|
456
501
|
const running = new Map(); // summaryKey -> value (ADDITIVE kinds)
|
|
457
502
|
const bump = (key, d) => running.set(key, (running.get(key) ?? 0) + d);
|
|
@@ -481,7 +526,7 @@ export async function deriveSummarySuffix(eng, ws, fromOid) {
|
|
|
481
526
|
let sawFrom = inSuffix;
|
|
482
527
|
for (const c of commits) {
|
|
483
528
|
let blob = null;
|
|
484
|
-
try { blob = (
|
|
529
|
+
try { blob = bytesFromB64(c.intentB64); } catch {}
|
|
485
530
|
const deltas = [];
|
|
486
531
|
let intentId = "";
|
|
487
532
|
if (blob) {
|
|
@@ -628,7 +673,7 @@ export async function deriveSummarySuffix(eng, ws, fromOid) {
|
|
|
628
673
|
};
|
|
629
674
|
}
|
|
630
675
|
|
|
631
|
-
/** One read's contribution shape over a
|
|
676
|
+
/** One read's contribution shape over a adapter-folded aggregate (suffix derivation only).
|
|
632
677
|
* SLICE 4: predicate-aware — a `where`-bearing scoped read contributes only while the
|
|
633
678
|
* folded fields satisfy the canonical predicate (same semantics as the kernel's
|
|
634
679
|
* maintained tally, so the suffix lane and the live capture agree byte-for-byte). */
|
|
@@ -658,7 +703,7 @@ function contributionDiff(read, before, after) {
|
|
|
658
703
|
|
|
659
704
|
// ── §6 — THE SHARD LIFECYCLE (slice 5): migration = re-admission, never trust ──
|
|
660
705
|
//
|
|
661
|
-
// The
|
|
706
|
+
// The adapter legs of a split: the TARGET pulls the moving homes' intents off the
|
|
662
707
|
// SOURCE chain and re-admits EACH through its own gate (`apply_intent` — the same
|
|
663
708
|
// primitive the upload birth and edge admission run: plan re-run, byte-compared,
|
|
664
709
|
// invariants judged); the SOURCE then seals the move with ONE law intent that
|
|
@@ -710,14 +755,14 @@ export async function collectHomedIntents(eng, ws, homeKeys) {
|
|
|
710
755
|
for (const hk of homeKeys) tagOf.set(await routeTagHexOfHomeKey(hk), hk);
|
|
711
756
|
const keySet = new Set(homeKeys);
|
|
712
757
|
const gitdir = gitdirOf(ws);
|
|
713
|
-
const
|
|
714
|
-
commits.
|
|
758
|
+
const head = kgit.resolveRef(eng, gitdir, BRANCH);
|
|
759
|
+
const commits = head ? kgit.intentsAbove(eng, gitdir, head) : []; // [{oid, intentB64}] genesis → head
|
|
715
760
|
const docs = [];
|
|
716
761
|
const strikeParity = new Map(); // intentId -> strike count
|
|
717
762
|
for (const c of commits) {
|
|
718
763
|
let doc = null;
|
|
719
764
|
try {
|
|
720
|
-
const
|
|
765
|
+
const blob = bytesFromB64(c.intentB64);
|
|
721
766
|
doc = JSON.parse(dec.decode(blob));
|
|
722
767
|
docs.push({ oid: c.oid, blob, doc });
|
|
723
768
|
} catch { continue; }
|
|
@@ -761,7 +806,7 @@ export async function migrateHomes(eng, ws, ledger, sourceWs, homeKeys, { onlyId
|
|
|
761
806
|
for (const e of homed) {
|
|
762
807
|
if (wanted && !wanted.has(e.id)) continue;
|
|
763
808
|
if (main.ids.has(e.id)) { skipped.push(e.id); continue; }
|
|
764
|
-
const res =
|
|
809
|
+
const res = offerAdmit(eng, ws, e.blob);
|
|
765
810
|
if (res.ok) {
|
|
766
811
|
main.ids.add(e.id); main.oids.add(res.head); main.head = res.head;
|
|
767
812
|
migrated.push(e.id);
|
|
@@ -811,7 +856,7 @@ export async function sealHomes(eng, ws, ledger, { domain, lawHash, homeKeys, ta
|
|
|
811
856
|
* chain head carrying the diffs (deep-verify recomputes from the same oracle).
|
|
812
857
|
*/
|
|
813
858
|
export async function summaryCloseAgainstHeld(eng, ws, heldRows, heldFrontier) {
|
|
814
|
-
const head =
|
|
859
|
+
const head = kgit.resolveRef(eng, gitdirOf(ws), BRANCH);
|
|
815
860
|
if (!head) return { ok: false, error: "no local main to close against" };
|
|
816
861
|
if (head === heldFrontier) return { ok: true, upToDate: true, head };
|
|
817
862
|
const subtotals = [];
|
|
@@ -880,15 +925,15 @@ export async function collectGlobalIntents(eng, ws) {
|
|
|
880
925
|
const globals = globalAggregates(eng);
|
|
881
926
|
if (globals.size === 0) return [];
|
|
882
927
|
const gitdir = gitdirOf(ws);
|
|
883
|
-
const
|
|
884
|
-
commits.
|
|
928
|
+
const head = kgit.resolveRef(eng, gitdir, BRANCH);
|
|
929
|
+
const commits = head ? kgit.intentsAbove(eng, gitdir, head) : []; // [{oid, intentB64}] genesis → head
|
|
885
930
|
const typeOf = new Map(); // aggregateId -> __type (set once at creation — types never change)
|
|
886
931
|
const strikeParity = new Map();
|
|
887
932
|
const docs = [];
|
|
888
933
|
for (const c of commits) {
|
|
889
934
|
let blob = null, doc = null;
|
|
890
935
|
try {
|
|
891
|
-
blob = (
|
|
936
|
+
blob = bytesFromB64(c.intentB64);
|
|
892
937
|
doc = JSON.parse(dec.decode(blob));
|
|
893
938
|
} catch { continue; }
|
|
894
939
|
docs.push({ oid: c.oid, blob, doc });
|
|
@@ -924,7 +969,7 @@ export async function replicateIntents(eng, ws, ledger, intents, { push = true }
|
|
|
924
969
|
const replicated = [], skipped = [], refused = [];
|
|
925
970
|
for (const e of intents) {
|
|
926
971
|
if (e.id && main.ids.has(e.id)) { skipped.push(e.id); continue; }
|
|
927
|
-
const res =
|
|
972
|
+
const res = offerAdmit(eng, ws, e.blob);
|
|
928
973
|
if (res.ok) {
|
|
929
974
|
if (e.id) main.ids.add(e.id);
|
|
930
975
|
main.oids.add(res.head); main.head = res.head;
|
|
@@ -934,7 +979,7 @@ export async function replicateIntents(eng, ws, ledger, intents, { push = true }
|
|
|
934
979
|
}
|
|
935
980
|
}
|
|
936
981
|
// push=false is the OFFLINE proof lane only (bench harness — no ledger remote);
|
|
937
|
-
// every
|
|
982
|
+
// every adapter leg keeps durability-before-ack.
|
|
938
983
|
if (replicated.length && push) await commitAndPush(eng, ws, ledger);
|
|
939
984
|
log("replicate-globals", { ws, replicated: replicated.length, skipped: skipped.length, refused: refused.length });
|
|
940
985
|
return { replicated, skipped, refused };
|
|
@@ -942,18 +987,37 @@ export async function replicateIntents(eng, ws, ledger, intents, { push = true }
|
|
|
942
987
|
|
|
943
988
|
/**
|
|
944
989
|
* Open browser/session lane without a client-side Artifacts receive-pack: the
|
|
945
|
-
* client offers the exact intent bytes it authored locally; the
|
|
990
|
+
* client offers the exact intent bytes it authored locally; the adapter applies the
|
|
946
991
|
* same structural fences as session admission, folds accepted bytes, and seals
|
|
947
992
|
* main once. This is still untrusted work: owner-only/control-plane/receipt
|
|
948
993
|
* writes are refused or dead-lettered exactly like session refs.
|
|
949
994
|
*/
|
|
950
|
-
export async function admitIntentOffers(eng, ws, ledger, offers, { refuseDomains = [], receiptFence = null } = {}) {
|
|
995
|
+
export async function admitIntentOffers(eng, ws, ledger, offers, { refuseDomains = [], receiptFence = null, shardRouting = null, clientBase = null, defer = false } = {}) {
|
|
951
996
|
const refuse = new Set(refuseDomains);
|
|
952
997
|
const main = await mainIntentIds(eng, ws);
|
|
953
998
|
const sessions = new Map();
|
|
954
999
|
const deadLetters = [];
|
|
955
1000
|
let admittedTotal = 0;
|
|
956
1001
|
const timings = { spans: [], intents: 0, sessions: 0 };
|
|
1002
|
+
// THE §5.2 SUMMARY CAPTURE on the OFFER LANE (offers are the only source of intents — this lane is the one
|
|
1003
|
+
// admit, routed workspaces included). A shard (selfLabel set) with scoped reads captures the per-batch
|
|
1004
|
+
// subtotal delta the same way the session-branch lane does; the adapter then closes it onto the coordinator
|
|
1005
|
+
// (`_postAdmissionHooks` → `_emitSummary`). Null on an unsharded/coordinator/read-free batch (zero cost).
|
|
1006
|
+
const summaryCtx = shardRouting && shardRouting.selfLabel && scopedReads(eng).length > 0
|
|
1007
|
+
? { reads: scopedReads(eng), prevs: new Map(), values: new Map(), events: [], rowsDelta: 0, rederived: new Set(), rowsByHome: new Map(), homeTags: new Map(), homeKeySet: new Set() }
|
|
1008
|
+
: null;
|
|
1009
|
+
if (summaryCtx) {
|
|
1010
|
+
try {
|
|
1011
|
+
for (const r of query(eng, ws, "aggregatesByType", JSON.stringify({ __type: "NomosHomeReceipt" }))) {
|
|
1012
|
+
const hk = r.data && typeof r.data.homeKey === "string" ? r.data.homeKey : null;
|
|
1013
|
+
if (!hk) continue;
|
|
1014
|
+
summaryCtx.homeKeySet.add(hk);
|
|
1015
|
+
summaryCtx.homeTags.set(await routeTagHexOfHomeKey(hk), hk);
|
|
1016
|
+
}
|
|
1017
|
+
} catch (e) { log("home-tag-table-error", { ws, error: String(e).slice(0, 200) }); }
|
|
1018
|
+
}
|
|
1019
|
+
const gitdir0 = gitdirOf(ws);
|
|
1020
|
+
const headBefore = summaryCtx ? kgit.resolveRef(eng, gitdir0, BRANCH) : null;
|
|
957
1021
|
const t0 = Date.now(); let seq = 0;
|
|
958
1022
|
const span = (name, kind, depth = 1, extra = {}) => {
|
|
959
1023
|
const s0 = Date.now();
|
|
@@ -977,8 +1041,15 @@ export async function admitIntentOffers(eng, ws, ledger, offers, { refuseDomains
|
|
|
977
1041
|
const dom = typeof doc?.payload?.domain === "string" ? doc.payload.domain : null;
|
|
978
1042
|
const dirId = typeof doc?.payload?.directiveId === "string" ? doc.payload.directiveId : null;
|
|
979
1043
|
const oid = String(offer?.oid || intentId || "").slice(0, 10);
|
|
980
|
-
|
|
981
|
-
|
|
1044
|
+
// THE GATE DECIDES (offer-kernel doctrine — OFFER_KERNEL_ARCHITECTURE.dot): the adapter does NOT
|
|
1045
|
+
// pre-filter control-plane (`nomos`/`bootstrap`) intents by domain string — that was the adapter
|
|
1046
|
+
// "choosing admission", which the doctrine forbids. An install/governance offer rides the SAME lane;
|
|
1047
|
+
// the kernel gate refuses an author who lacks the declared relation (installDomain.requiresRelationGate
|
|
1048
|
+
// = {relation:"installDomain", object:"workspace:self"}), so an unauthorized control-plane write is a
|
|
1049
|
+
// TYPED GATE refusal, not a adapter verdict. Only `!dom` (malformed) and the explicit per-workspace
|
|
1050
|
+
// `refuse` fence (root's `workspaces` governance lane) stay — those are structural, not authority.
|
|
1051
|
+
if (!dom || refuse.has(dom)) {
|
|
1052
|
+
rec.rejected.push({ oid, error: `${dom ? `'${dom}' is adapter-fenced on this workspace` : "non-authored intent"} — governance enters its own admin lane` });
|
|
982
1053
|
continue;
|
|
983
1054
|
}
|
|
984
1055
|
if (receiptFence && dirId && Array.isArray(receiptFence[dom]) && receiptFence[dom].includes(dirId)) {
|
|
@@ -991,12 +1062,22 @@ export async function admitIntentOffers(eng, ws, ledger, offers, { refuseDomains
|
|
|
991
1062
|
rec.skipped.push({ oid, intent: intentId.slice(0, 12), alreadyOnMain: true });
|
|
992
1063
|
continue;
|
|
993
1064
|
}
|
|
994
|
-
const
|
|
1065
|
+
const cap = summaryCtx ? beginSummaryCapture(eng, ws, summaryCtx, blob) : null;
|
|
1066
|
+
// DEFERRED-PROJECTION ACK on the OFFER lane (#47). The per-intent read-projection refresh
|
|
1067
|
+
// (~220ms on a fat ledger) is the warm-sync hot-path waste: the fold IS the correctness step,
|
|
1068
|
+
// the projection only feeds READS, and every read op self-heals to head. So we ACK on the fold
|
|
1069
|
+
// and let the post-ack warm lane (`flushDeferredProjections`, marked below) catch the projection
|
|
1070
|
+
// up — exactly as the session lane (admitAll) already does. A summaryCtx (§5.2) batch keeps the
|
|
1071
|
+
// synchronous refresh: its scoped-read capture re-derives off committed projection state.
|
|
1072
|
+
const _tOffer = Date.now();
|
|
1073
|
+
const res = offerAdmit(eng, ws, blob, { defer: !summaryCtx });
|
|
1074
|
+
log("offer-admit-ms", { ws, ms: Date.now() - _tOffer, deferred: !summaryCtx, ok: res.ok });
|
|
995
1075
|
if (res.ok) {
|
|
996
1076
|
if (intentId) main.ids.add(intentId);
|
|
997
1077
|
main.oids.add(res.head); main.head = res.head;
|
|
998
1078
|
rec.admitted.push({ oid, head: res.head });
|
|
999
1079
|
admittedTotal += 1;
|
|
1080
|
+
if (cap) finishSummaryCapture(eng, ws, summaryCtx, cap, res.head, intentId);
|
|
1000
1081
|
} else {
|
|
1001
1082
|
const error = `${String(res.error || "").slice(0, 300)} — dead-lettered; fix the law (or the payload), then POST /v1/workspaces/${ws}/dead-letters/retry`;
|
|
1002
1083
|
rec.rejected.push({ oid, error, deadLettered: true });
|
|
@@ -1010,22 +1091,41 @@ export async function admitIntentOffers(eng, ws, ledger, offers, { refuseDomains
|
|
|
1010
1091
|
}
|
|
1011
1092
|
timings.sessions = sessions.size;
|
|
1012
1093
|
timings.intents = admittedTotal;
|
|
1013
|
-
|
|
1094
|
+
// The deferred projection (above) is flushed by the post-ack warm lane — register this ws so
|
|
1095
|
+
// `flushDeferredProjections` catches it up after we've ACKed. (summaryCtx never deferred.)
|
|
1096
|
+
if (admittedTotal && !summaryCtx) (eng.pendingProjection ??= new Set()).add(ws);
|
|
1097
|
+
// OFFER-KERNEL P1.2b: refusals on the offer lane become git-custody facts too (pushed inline,
|
|
1098
|
+
// before the seal that unmounts on failure). Runs even when nothing admitted — a cycle can be
|
|
1099
|
+
// pure refusals.
|
|
1100
|
+
if (deadLetters.length) { try { await materializeRefused(eng, ws, ledger, deadLetters); } catch (e) { log("refused-materialize-failed", { ws, error: String(e).slice(0, 200) }); } }
|
|
1101
|
+
// GIT OFF THE HOT PATH. The fold above is the whole correctness step — the client said "btw I did
|
|
1102
|
+
// this", we re-executed it, resident main is now truth. We ACK on that. Custody (Artifacts) is a
|
|
1103
|
+
// write-behind MIRROR, never on the hot path: when `defer` is set the adapter backs the captured head
|
|
1104
|
+
// up off-lane (server.mjs custodyWriteBehind → backupHead), coalesced to one push in flight. The
|
|
1105
|
+
// CONTAINER IS THE SOLE WRITER of main, so the mirror needs no pull-back — it just FF-pushes the
|
|
1106
|
+
// snapshot. A summaryCtx (§5.2) batch still seals durable-before-ack (its follow-ups depend on it).
|
|
1107
|
+
const deferDurability = defer && !summaryCtx;
|
|
1108
|
+
if (admittedTotal && !deferDurability) {
|
|
1014
1109
|
const sealDone = span("seal offered intents", "cseal", 1, { admitted: admittedTotal });
|
|
1015
1110
|
await commitAndPush(eng, ws, ledger);
|
|
1016
1111
|
sealDone();
|
|
1017
1112
|
}
|
|
1018
1113
|
const gitdir = gitdirOf(ws);
|
|
1019
|
-
const head =
|
|
1114
|
+
const head = kgit.resolveRef(eng, gitdir, BRANCH) ?? main.head;
|
|
1115
|
+
// The coalesced §5.2 delta of this batch (null when no scoped read moved) — the adapter closes it onto the
|
|
1116
|
+
// coordinator (`_postAdmissionHooks` → `_emitSummary`), exactly as the session-branch lane does.
|
|
1117
|
+
const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore, head) : null;
|
|
1118
|
+
// (The legacy inline `mainDelta` perf hack is GONE: the client pulls the delta itself via GET /pack +
|
|
1119
|
+
// the kernel's gix apply_pack — one byte-pipe lane, no adapter-side loose-object walk.)
|
|
1020
1120
|
log("admit-intent-offers", { ws, sessions: sessions.size, admitted: admittedTotal, rejected: [...sessions.values()].reduce((n, s) => n + s.rejected.length, 0) });
|
|
1021
|
-
return { sessions: [...sessions.values()].map((s) => ({ ...s, ...(s.skipped.length ? {} : { skipped: undefined }) })), main: head, deadLetters, timings };
|
|
1121
|
+
return { sessions: [...sessions.values()].map((s) => ({ ...s, ...(s.skipped.length ? {} : { skipped: undefined }) })), main: head, deadLetters, timings, ...(summary ? { summary } : {}), ...(deferDurability && admittedTotal && head ? { deferred: true, head } : {}) };
|
|
1022
1122
|
}
|
|
1023
1123
|
|
|
1024
1124
|
/**
|
|
1025
1125
|
* §5.5 (slice 4) — the CONTENT HASH of the coordinator's committed subtotal state:
|
|
1026
1126
|
* sha256 over the canonical (row-id-sorted) projection of every
|
|
1027
1127
|
* `NomosSummarySubtotal` row. The ONE definition every checkpoint party shares —
|
|
1028
|
-
* the sealing
|
|
1128
|
+
* the sealing adapter claims it, an auditor (deep-verify anchoring, the e2e, any
|
|
1029
1129
|
* peer that mounts the chain) recomputes it from law-state and byte-compares.
|
|
1030
1130
|
*/
|
|
1031
1131
|
export async function checkpointStateHash(eng, ws) {
|
|
@@ -1064,7 +1164,7 @@ export async function authorOn(eng, ws, ledger, domain, directiveId, payload, la
|
|
|
1064
1164
|
* the SHARD's whole chain from genesis (the same wasm `verify_chain` the upload
|
|
1065
1165
|
* birth runs), then recompute every coordinator-held subtotal for that shard from
|
|
1066
1166
|
* the shard's OWN maintained tallies and byte-compare. `coordinatorRows` is the
|
|
1067
|
-
* coordinator's law-state (`NomosSummarySubtotal` rows for this shard) — the
|
|
1167
|
+
* coordinator's law-state (`NomosSummarySubtotal` rows for this shard) — the ADAPTER
|
|
1068
1168
|
* resolves and passes them (the engine has no cross-workspace reach). Auditors pay
|
|
1069
1169
|
* for verification; ordinary reads stay O(1).
|
|
1070
1170
|
*/
|
|
@@ -1111,7 +1211,7 @@ export async function deepVerifyShard(eng, shardWs, shardLedger, coordinatorRows
|
|
|
1111
1211
|
if (!match) allMatch = false;
|
|
1112
1212
|
checks.push({ readId: d.readId, group, coordinator: d.value, shard: recomputed, match });
|
|
1113
1213
|
}
|
|
1114
|
-
const head =
|
|
1214
|
+
const head = kgit.resolveRef(eng, gitdirOf(shardWs), BRANCH);
|
|
1115
1215
|
return {
|
|
1116
1216
|
verdict: allMatch ? "verified" : "mismatch",
|
|
1117
1217
|
head,
|
|
@@ -1131,34 +1231,22 @@ export async function mountWorkspace(eng, ws, ledger) {
|
|
|
1131
1231
|
if (eng.mounted.has(ws)) return eng.mounted.get(ws);
|
|
1132
1232
|
stageWorkspaceDir(eng, ws);
|
|
1133
1233
|
const gitdir = gitdirOf(ws);
|
|
1134
|
-
let restored = false, remoteMain = null, restoreError = null
|
|
1234
|
+
let restored = false, remoteMain = null, restoreError = null;
|
|
1135
1235
|
try {
|
|
1136
|
-
|
|
1137
|
-
|
|
1236
|
+
// COLD MOUNT, kernel-native: write the bare skeleton (plain fs), then GET /pack the whole `main` history
|
|
1237
|
+
// via raw smart-HTTP and let the kernel's gix apply_pack ingest it (kept PACKED) — zero isomorphic-git.
|
|
1238
|
+
// NO CHECKPOINT. A derived projection snapshot is NEVER trusted — it can corrupt and wedge the holon (it
|
|
1239
|
+
// did), which violates the law: truth is the replayable ledger, nothing derived is load-bearing. The
|
|
1240
|
+
// resident holon folds `main` ONCE on its first read and stays WARM in RAM thereafter — no cold-fold in
|
|
1241
|
+
// the hot path because the bailiff keeps it resident.
|
|
1242
|
+
await custodySkeleton(eng, gitdir);
|
|
1243
|
+
remoteMain = await custodyFetch(eng, ws, ledger, { remoteRef: "refs/heads/main" });
|
|
1138
1244
|
if (remoteMain) {
|
|
1139
|
-
|
|
1140
|
-
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1141
|
-
const ckpt = await discoverCheckpointFromCustody(ledger).catch(() => null);
|
|
1142
|
-
let shallowOk = false;
|
|
1143
|
-
if (ckpt?.cursor && ckpt.frontier) {
|
|
1144
|
-
const steps = [32, 96, 256, 1024];
|
|
1145
|
-
for (let i = 0; i < steps.length; i++) {
|
|
1146
|
-
const depth = steps[i], relative = i > 0;
|
|
1147
|
-
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: "main", singleBranch: true, depth, relative, headers: ledger.headers });
|
|
1148
|
-
if (await git.readCommit({ fs: eng.fs, gitdir, oid: ckpt.cursor }).then(() => true, () => false)) { shallowOk = true; break; }
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
if (!shallowOk) await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: "main", singleBranch: true, headers: ledger.headers });
|
|
1152
|
-
await git.writeRef({ fs: eng.fs, gitdir, ref: "refs/heads/main", value: remoteMain, force: true });
|
|
1153
|
-
await git.writeRef({ fs: eng.fs, gitdir, ref: "HEAD", value: "ref: refs/heads/main", force: true, symbolic: true });
|
|
1154
|
-
if (shallowOk && ckpt?.blob) {
|
|
1155
|
-
const r = importCheckpoint(eng, ws, ckpt.blob, ckpt.frontier);
|
|
1156
|
-
checkpoint = { restored: !!(r && r.restored), frontierSeeded: !!(r && r.frontierSeeded), cursor: r && r.cursor ? r.cursor : ckpt.cursor, ref: ckpt.ref };
|
|
1157
|
-
}
|
|
1245
|
+
kgit.writeRef(eng, gitdir, "HEAD", "ref: refs/heads/main");
|
|
1158
1246
|
restored = true;
|
|
1159
1247
|
}
|
|
1160
1248
|
} catch (e) { restoreError = String((e && e.stack) || e).split("\n").slice(0, 5).join(" | "); }
|
|
1161
|
-
const m = { restored, remoteMain, restoreError
|
|
1249
|
+
const m = { restored, remoteMain, restoreError };
|
|
1162
1250
|
eng.mounted.set(ws, m);
|
|
1163
1251
|
return m;
|
|
1164
1252
|
}
|
|
@@ -1169,9 +1257,9 @@ function assertAuthored(res, what) {
|
|
|
1169
1257
|
}
|
|
1170
1258
|
|
|
1171
1259
|
/**
|
|
1172
|
-
* Mount a
|
|
1173
|
-
* `/work/ws/<name>`; the
|
|
1174
|
-
* Artifacts push, and the whole chain dies with the resident
|
|
1260
|
+
* Mount a adapter-ephemeral workspace. It is still a normal WASI GitHolon under
|
|
1261
|
+
* `/work/ws/<name>`; the adapter behavior is what differs: no custody restore, no
|
|
1262
|
+
* Artifacts push, and the whole chain dies with the resident adapter.
|
|
1175
1263
|
*/
|
|
1176
1264
|
export async function mountEphemeralWorkspace(eng, ws, { domainHash = "", packageUsda = "" } = {}) {
|
|
1177
1265
|
let m = eng.mounted.get(ws);
|
|
@@ -1195,13 +1283,13 @@ export async function mountEphemeralWorkspace(eng, ws, { domainHash = "", packag
|
|
|
1195
1283
|
return m;
|
|
1196
1284
|
}
|
|
1197
1285
|
|
|
1198
|
-
/** Author into a
|
|
1286
|
+
/** Author into a adapter-ephemeral workspace and leave the resulting head in memory only. */
|
|
1199
1287
|
export async function authorEphemeral(eng, ws, mountOpts, domain, directiveId, payload, lawHash, opts = {}) {
|
|
1200
1288
|
await mountEphemeralWorkspace(eng, ws, mountOpts);
|
|
1201
1289
|
return author(eng, ws, domain, directiveId, payload, lawHash, { deferProjection: false, ...opts });
|
|
1202
1290
|
}
|
|
1203
1291
|
|
|
1204
|
-
/** Query a
|
|
1292
|
+
/** Query a adapter-ephemeral workspace, mounting/installing its package first if needed. */
|
|
1205
1293
|
export async function queryEphemeral(eng, ws, mountOpts, queryId, paramsJson, principal = "", keys = []) {
|
|
1206
1294
|
await mountEphemeralWorkspace(eng, ws, mountOpts);
|
|
1207
1295
|
return query(eng, ws, queryId, paramsJson, principal, keys);
|
|
@@ -1220,18 +1308,44 @@ function writeWork(eng, name, bytes) { eng.preopen.dir.contents.set(name, new Fi
|
|
|
1220
1308
|
/** THE CURRENT LAW PER DOMAIN KEY — the holon's OWN resolution (`refresh_key_map`,
|
|
1221
1309
|
* the map the verify walk and every dispatch use: latest Active install per key
|
|
1222
1310
|
* by install stamp). Returns `{ "<domainKey>": "<currentHash>", … }`. status uses
|
|
1223
|
-
* THIS instead of re-deriving recency
|
|
1311
|
+
* THIS instead of re-deriving recency adapter-side from deploy order. */
|
|
1224
1312
|
export function currentLaw(eng, ws) {
|
|
1225
|
-
const out = JSON.parse(call(eng.ex, "
|
|
1313
|
+
const out = JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "currentLaw" }), branch: BRANCH }, eng.STDERR));
|
|
1226
1314
|
return out.keyMap || {};
|
|
1227
1315
|
}
|
|
1228
1316
|
|
|
1317
|
+
/** THE CAPABILITY DECLARATION READ (Full-A — capability_marketplace.md §4): the holon
|
|
1318
|
+
* surfaces every CURRENTLY-INSTALLED domain's `nomosCapabilities` law key (the impure-
|
|
1319
|
+
* capability quartets sealed into the law). The deploy lane relays these as a "declared —
|
|
1320
|
+
* bind it or obligations wait" notice WITHOUT reaching into root law-state. Returns
|
|
1321
|
+
* `[{capability, domain, aggregateId, obligation, complete, fail, block, deadLetter, tasksQueryId}]`. */
|
|
1322
|
+
export function declaredCapabilities(eng, ws) {
|
|
1323
|
+
const out = JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "declaredCapabilities" }), branch: BRANCH }, eng.STDERR));
|
|
1324
|
+
return out.capabilities || [];
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/** THE REACTOR SCAN (the kernel's existing "orders to emit"): every impure-capability
|
|
1328
|
+
* obligation at status:'requested', as `[{taskType, requestId, scheduledAt, deadlineAt, params}]`.
|
|
1329
|
+
* The bailiff outbox loop reads this + joins each taskType to its declared endpoint. */
|
|
1330
|
+
export function reactorScan(eng, ws) {
|
|
1331
|
+
// THE OUTBOX VERB (one of the seven): the holon's obligations-to-emit. The adapter reads its emit-queue
|
|
1332
|
+
// through `outbox`, never the pre-doctrine `reactor_scan` (which survives behind legacy-adapter-api for tools).
|
|
1333
|
+
return JSON.parse(call(eng.ex, "outbox", { repoArg: repoArgOf(ws), branch: BRANCH }, eng.STDERR));
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1229
1336
|
/** THE EVOLVE DRY-RUN (managed_rollout.md slice 1 — "would this upgrade admit?"):
|
|
1230
1337
|
* the evolve gate's pure judgment over the mounted workspace's folded state and a
|
|
1231
1338
|
* CANDIDATE package, without installing. Returns {admits} or {admits:false,
|
|
1232
1339
|
* domain, detail} — byte-identical to the refusal the real install would meet. */
|
|
1233
1340
|
export function evolveDryRun(eng, ws, usda, domainHash, dispositions = null) {
|
|
1234
|
-
|
|
1341
|
+
// "WOULD THIS UPGRADE ADMIT?" is `offer` with the commit withheld (the offer-kernel dry-run): the SAME
|
|
1342
|
+
// `author(nomos/installDomain, <candidate package + dispositions>)` a real deploy runs, with dryRun:true.
|
|
1343
|
+
// The candidate rides the payload (packageUsda) exactly as a live install carries it — no inline-queryBytes
|
|
1344
|
+
// bundle, no separate verb. The full admission gate runs (incl. the #58 evolve gate, which reads the bundle
|
|
1345
|
+
// from the intent's own events); the commit never happens. The verdict normalizes to the {admits} shape the
|
|
1346
|
+
// evolve-dryrun callers consume — `Ok` ⇒ admits; a refusal carries the gate's own reason (the evolve detail).
|
|
1347
|
+
const r = author(eng, ws, "nomos", "installDomain", installPayload(domainHash, usda, eng.installedBy, dispositions), eng.hashes.nomos, { dryRun: true, deferProjection: false });
|
|
1348
|
+
return r.ok ? { admits: true } : { admits: false, detail: r.error };
|
|
1235
1349
|
}
|
|
1236
1350
|
|
|
1237
1351
|
/** THE FRONT-DOOR MINT (engine plane): reserve a kernel-minted typed id
|
|
@@ -1255,26 +1369,39 @@ export function author(eng, ws, domain, directiveId, payload, controllerHash, op
|
|
|
1255
1369
|
// THE DEFERRED-PROJECTION ACK (#47, sharding slice 7): the read-projection
|
|
1256
1370
|
// catch-up (incl. the derive/combine materialize) moves OFF the author ack path
|
|
1257
1371
|
// by default — every read op self-heals to head, and the post-ack warm lane
|
|
1258
|
-
// (`warmEngine`, which every
|
|
1372
|
+
// (`warmEngine`, which every adapter already calls) flushes it eagerly. Pass
|
|
1259
1373
|
// `opts.deferProjection: false` to keep the pre-#47 synchronous refresh.
|
|
1260
1374
|
const defer = opts.deferProjection !== false;
|
|
1261
1375
|
// authorSecret (warrant-flip): the author's client-held ed25519 device secret (b64), injected for THIS
|
|
1262
1376
|
// local author ONLY so the wasm author door mints the signed-author attestation + self-verifies. Absent
|
|
1263
1377
|
// on a legacy/unwarranted author (the door's legacy fast path); a warranted workspace REQUIRES it.
|
|
1264
|
-
|
|
1378
|
+
// THE ONE EFFECTFUL VERB: authoring is `offer` of a PAYLOAD intent (a `directiveId` present ⇒ the holon
|
|
1379
|
+
// plans it, drawing fresh captured ports from the envelope). The structured verdict normalizes back to the
|
|
1380
|
+
// {ok, head, intentOut} shape every caller consumes — `author` is no longer a adapter verb (it dissolved into
|
|
1381
|
+
// offer; the wasm `author` arm survives for the bench/CLI behind legacy-adapter-api until P8).
|
|
1382
|
+
// DRY-RUN (offer-kernel): `offer` with the commit withheld — run the plan + full gate, get the verdict,
|
|
1383
|
+
// commit nothing. This is the home of the old `evolve_dryrun` verb (see `evolveDryRun`). Never defers
|
|
1384
|
+
// projection (no commit to flush).
|
|
1385
|
+
const v = JSON.parse(call(eng.ex, "offer", { repoArg: repoArgOf(ws), workspace: ws, domain, directiveId, payloadFile: `/work/payload-${seq}.json`, envelopeFile: `/work/envelope-${seq}.json`, seq, actor: opts.actor ?? "", domainFile: genesis ? "/work/domain.package.usda" : "", domainHash: genesis ? "" : controllerHash, branch: BRANCH, ...(opts.authorSecret ? { authorSecret: opts.authorSecret } : {}), ...(opts.dryRun ? { dryRun: true } : (defer ? { deferProjection: true } : {})) }, eng.STDERR));
|
|
1386
|
+
const res = v.outcome === "admitted" ? { ok: true, head: v.head, intentOut: v.intentOut }
|
|
1387
|
+
: v.outcome === "refused" ? { ok: false, error: v.verdict?.reason ?? v.error }
|
|
1388
|
+
: v; // defensive: a non-verdict shape passes through unchanged
|
|
1265
1389
|
if (defer && res.ok) (eng.pendingProjection ??= new Set()).add(ws);
|
|
1266
1390
|
return res;
|
|
1267
1391
|
}
|
|
1268
1392
|
|
|
1269
|
-
/** Catch one workspace's read projection up to head NOW (the deferred-ack flush).
|
|
1393
|
+
/** Catch one workspace's read projection up to head NOW (the deferred-ack flush). P3: rides the
|
|
1394
|
+
* `query` verb (op:"refresh") — a non-effectful maintenance read, not a top-level adapter verb. */
|
|
1270
1395
|
export function refreshProjection(eng, ws) {
|
|
1271
|
-
return JSON.parse(call(eng.ex, "
|
|
1396
|
+
return JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "refresh" }), branch: BRANCH }, eng.STDERR));
|
|
1272
1397
|
}
|
|
1273
1398
|
|
|
1274
1399
|
/** Flush every deferred projection refresh (rides the post-ack warm lane). */
|
|
1275
1400
|
export function flushDeferredProjections(eng) {
|
|
1276
1401
|
for (const ws of eng.pendingProjection ?? []) {
|
|
1402
|
+
const _t = Date.now();
|
|
1277
1403
|
try { refreshProjection(eng, ws); } catch {}
|
|
1404
|
+
log("flush-projection-ms", { ws, ms: Date.now() - _t });
|
|
1278
1405
|
}
|
|
1279
1406
|
if (eng.pendingProjection) eng.pendingProjection.clear();
|
|
1280
1407
|
}
|
|
@@ -1285,24 +1412,24 @@ export function flushDeferredProjections(eng) {
|
|
|
1285
1412
|
// the DO under load). The checkpoint persists the folded projection (cursor + every table) so a
|
|
1286
1413
|
// cold mount RESTORES it and the first read folds only tip−cursor (O(delta)).
|
|
1287
1414
|
|
|
1288
|
-
/** Export `ws`'s projection-at-head to a base64 blob the
|
|
1415
|
+
/** Export `ws`'s projection-at-head to a base64 blob the ADAPTER persists. Returns
|
|
1289
1416
|
* `{cursor, blob, bytes}` (or `{empty:true}` for an unborn branch). */
|
|
1290
1417
|
export function exportCheckpoint(eng, ws) {
|
|
1291
1418
|
return JSON.parse(call(eng.ex, "checkpoint_export", { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH }, eng.STDERR));
|
|
1292
1419
|
}
|
|
1293
1420
|
|
|
1294
1421
|
/** Restore a checkpoint blob into the pool BEFORE the first read on a cold mount (no-op if the pool
|
|
1295
|
-
* is already warm). The
|
|
1422
|
+
* is already warm). The ADAPTER restores ONLY a blob whose stored lawHash matches the workspace's
|
|
1296
1423
|
* CURRENT law (schema parity); a mismatched/absent blob costs at most a cold fold, never a wrong read. */
|
|
1297
1424
|
export function importCheckpoint(eng, ws, blobB64, frontierB64) {
|
|
1298
1425
|
return JSON.parse(call(eng.ex, "checkpoint_import", { repoArg: repoArgOf(ws), workspace: ws, branch: BRANCH, blob: blobB64, ...(frontierB64 ? { frontier: frontierB64 } : {}) }, eng.STDERR));
|
|
1299
1426
|
}
|
|
1300
1427
|
|
|
1301
|
-
// ── CHECKPOINT IN CUSTODY (the
|
|
1428
|
+
// ── CHECKPOINT IN CUSTODY (the adapter-agnostic cold-mount; compute placement is beneath the contract) ──
|
|
1302
1429
|
// The folded projection is a fact ABOUT the chain — so carry it IN custody (a git ref), not in some
|
|
1303
|
-
//
|
|
1430
|
+
// adapter's private storage the NEXT adapter can't reach. The ephemeral heavy container has no ctx.storage;
|
|
1304
1431
|
// a DO checkpoint is invisible to it; so root (heavy) re-folds O(chain) on every cold start. Put the
|
|
1305
|
-
// checkpoint where every peer already looks — the ledger — and ANY
|
|
1432
|
+
// checkpoint where every peer already looks — the ledger — and ANY adapter restores it O(delta): the git
|
|
1306
1433
|
// carries its own folded state. Keyed by the CURRENT law hash (schema parity): a law upgrade orphans
|
|
1307
1434
|
// the old checkpoint (the next reader cold-folds once, re-publishes under the new hash). Best-effort &
|
|
1308
1435
|
// post-ack — a missing/stale checkpoint costs at most one cold fold, never a wrong read; main is truth.
|
|
@@ -1310,7 +1437,7 @@ const CKPT_REF = (lawHash) => `refs/meta/checkpoint/${lawHash}`;
|
|
|
1310
1437
|
const CKPT_AUTHOR = { name: "nomos", email: "nomos@holon", timestamp: 0, timezoneOffset: 0 };
|
|
1311
1438
|
|
|
1312
1439
|
// A serialized SQLite projection is mostly FREE PAGES (the fold DELETEs+reinserts per commit) — it
|
|
1313
|
-
// gzips ~30×. We compress the b64 export
|
|
1440
|
+
// gzips ~30×. We compress the b64 export adapter-side before it enters custody: the ledger meters pushed
|
|
1314
1441
|
// bytes against the workspace quota, and a cold peer fetches less. Runtime-neutral (CompressionStream
|
|
1315
1442
|
// is in Node 18+ and workerd). The `meta` blob records the codec so a future format stays readable.
|
|
1316
1443
|
async function streamAll(stream) {
|
|
@@ -1329,36 +1456,6 @@ async function gunzip(bytes) {
|
|
|
1329
1456
|
return streamAll(new Response(bytes).body.pipeThrough(ds));
|
|
1330
1457
|
}
|
|
1331
1458
|
|
|
1332
|
-
async function discoverCheckpointFromCustody(ledger) {
|
|
1333
|
-
const freshHeaders = () => ({ ...(ledger.headers || {}) });
|
|
1334
|
-
const refs = ((await git.listServerRefs({ http, url: ledger.remote, headers: freshHeaders(), prefix: "refs/meta/checkpoint/", protocolVersion: 2 })) || [])
|
|
1335
|
-
.filter((r) => r.ref && r.ref.startsWith("refs/meta/checkpoint/") && r.oid);
|
|
1336
|
-
if (!refs.length) return null;
|
|
1337
|
-
const root = new Map();
|
|
1338
|
-
root.set("ckpt", new Directory(new Map()));
|
|
1339
|
-
const preopen = new PreopenDirectory("/work", root);
|
|
1340
|
-
const fs = makeGitFs(preopen, "/work", { File, Directory });
|
|
1341
|
-
const gitdir = "/work/ckpt/nomos.git";
|
|
1342
|
-
await git.init({ fs, gitdir, bare: true, defaultBranch: BRANCH });
|
|
1343
|
-
await git.addRemote({ fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1344
|
-
const readFile = async (oid, filepath) => { try { return (await git.readBlob({ fs, gitdir, oid, filepath })).blob; } catch { return null; } };
|
|
1345
|
-
for (let i = refs.length - 1; i >= 0 && i >= refs.length - 3; i--) {
|
|
1346
|
-
const { ref, oid } = refs[i];
|
|
1347
|
-
try {
|
|
1348
|
-
await git.fetch({ fs, http, gitdir, remote: "origin", ref, singleBranch: true, depth: 1, headers: freshHeaders() });
|
|
1349
|
-
const gz = await readFile(oid, "checkpoint");
|
|
1350
|
-
if (!gz) continue;
|
|
1351
|
-
let meta = {}; try { const mb = await readFile(oid, "meta"); if (mb) meta = JSON.parse(dec.decode(mb)); } catch {}
|
|
1352
|
-
const codec = meta.codec ?? "gzip";
|
|
1353
|
-
const blob = dec.decode(codec === "gzip" ? await gunzip(gz) : gz);
|
|
1354
|
-
const fgz = await readFile(oid, "frontier");
|
|
1355
|
-
const frontier = fgz ? dec.decode(codec === "gzip" ? await gunzip(fgz) : fgz) : null;
|
|
1356
|
-
return { ref, oid, cursor: meta.cursor || null, blob, frontier };
|
|
1357
|
-
} catch { /* try an older checkpoint */ }
|
|
1358
|
-
}
|
|
1359
|
-
return null;
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
1459
|
/** Wrap an exported checkpoint into git objects under `refs/meta/checkpoint/<lawHash>` (LOCAL only —
|
|
1363
1460
|
* no network). The ref points at a parentless commit whose tree carries `checkpoint` (the b64 blob)
|
|
1364
1461
|
* and `meta` ({cursor,bytes,lawHash}). Returns {ref, commitOid}. Separated from the push so the
|
|
@@ -1366,26 +1463,21 @@ async function discoverCheckpointFromCustody(ledger) {
|
|
|
1366
1463
|
export async function writeCheckpointObjects(eng, ws, ex, lawHash) {
|
|
1367
1464
|
const gitdir = gitdirOf(ws);
|
|
1368
1465
|
const gz = await gzip(enc.encode(ex.blob));
|
|
1369
|
-
const blobOid = await git.writeBlob({ fs: eng.fs, gitdir, blob: gz });
|
|
1370
1466
|
// THE FOLD FRONTIER (the gate's Workspace state @cursor) rides ALONGSIDE the projection so a cold mount
|
|
1371
1467
|
// can AUTHOR on a shallow clone (the projection alone is a read-collapsed view the gate can't fold from).
|
|
1372
1468
|
// Era-safe: pre-this-era exports carry no `ex.frontier`, so the `frontier` file is simply absent and a
|
|
1373
1469
|
// reader falls back to a full clone (writes need full history). Gzipped like the projection blob.
|
|
1374
|
-
const
|
|
1375
|
-
{ mode: "100644", path: "checkpoint", oid: blobOid, type: "blob" },
|
|
1376
|
-
];
|
|
1470
|
+
const files = [{ path: "checkpoint", bytes: gz }];
|
|
1377
1471
|
let fgzLen = 0;
|
|
1378
1472
|
if (ex.frontier) {
|
|
1379
1473
|
const fgz = await gzip(enc.encode(ex.frontier));
|
|
1380
1474
|
fgzLen = fgz.length;
|
|
1381
|
-
|
|
1475
|
+
files.push({ path: "frontier", bytes: fgz });
|
|
1382
1476
|
}
|
|
1383
|
-
|
|
1384
|
-
tree.push({ mode: "100644", path: "meta", oid: metaOid, type: "blob" });
|
|
1385
|
-
const tree_ = await git.writeTree({ fs: eng.fs, gitdir, tree });
|
|
1386
|
-
const commitOid = await git.writeCommit({ fs: eng.fs, gitdir, commit: { tree: tree_, parent: [], author: CKPT_AUTHOR, committer: CKPT_AUTHOR, message: `checkpoint ${ws} @${String(ex.cursor).slice(0, 16)}` } });
|
|
1477
|
+
files.push({ path: "meta", bytes: enc.encode(JSON.stringify({ cursor: ex.cursor, bytes: ex.bytes ?? 0, lawHash, codec: "gzip", raw: ex.blob.length, stored: gz.length, frontierStored: fgzLen })) });
|
|
1387
1478
|
const ref = CKPT_REF(lawHash);
|
|
1388
|
-
|
|
1479
|
+
// ONE kernel call: blobs→tree→parentless commit→force ref. Fixed timeSecs ⇒ deterministic (idempotent).
|
|
1480
|
+
const commitOid = kgit.writeMetaCommit(eng, gitdir, { parents: [], files, message: `checkpoint ${ws} @${String(ex.cursor).slice(0, 16)}`, timeSecs: 0, ref });
|
|
1389
1481
|
return { ref, commitOid };
|
|
1390
1482
|
}
|
|
1391
1483
|
|
|
@@ -1395,17 +1487,17 @@ export async function writeCheckpointObjects(eng, ws, ex, lawHash) {
|
|
|
1395
1487
|
* pre-this-era checkpoint (no `frontier` file). */
|
|
1396
1488
|
export async function readCheckpointBlob(eng, ws, commitOid) {
|
|
1397
1489
|
const gitdir = gitdirOf(ws);
|
|
1398
|
-
const
|
|
1490
|
+
const blob = kgit.readBlob(eng, gitdir, commitOid, "checkpoint");
|
|
1399
1491
|
let codec = "gzip";
|
|
1400
|
-
try { codec = JSON.parse(dec.decode(
|
|
1492
|
+
try { codec = JSON.parse(dec.decode(kgit.readBlob(eng, gitdir, commitOid, "meta"))).codec ?? "gzip"; } catch {}
|
|
1401
1493
|
const dezip = async (b) => dec.decode(codec === "gzip" ? await gunzip(b) : b);
|
|
1402
1494
|
let frontier = null;
|
|
1403
|
-
try {
|
|
1495
|
+
try { const f = kgit.readBlob(eng, gitdir, commitOid, "frontier"); if (f) frontier = await dezip(f); } catch { /* pre-this-era: no frontier */ }
|
|
1404
1496
|
return { blob: await dezip(blob), frontier };
|
|
1405
1497
|
}
|
|
1406
1498
|
|
|
1407
1499
|
/** The checkpoint's schema-parity key: a hash of the workspace's CURRENT law map (every installed
|
|
1408
|
-
* domain). The
|
|
1500
|
+
* domain). The ADAPTER passes nothing — the engine derives it from the chain (bailiff). A law upgrade
|
|
1409
1501
|
* changes the map → a new key → the old checkpoint is orphaned (the next reader cold-folds once and
|
|
1410
1502
|
* re-publishes). An explicit `lawHash` (the DO's existing govHash callers, tests) overrides. */
|
|
1411
1503
|
export async function lawParity(eng, ws, lawHash) {
|
|
@@ -1423,7 +1515,7 @@ export async function pushCheckpoint(eng, ws, ledger, lawHash) {
|
|
|
1423
1515
|
lawHash = await lawParity(eng, ws, lawHash);
|
|
1424
1516
|
if (!lawHash) return { pushed: false, reason: "no-law" };
|
|
1425
1517
|
const gitdir = gitdirOf(ws);
|
|
1426
|
-
const head =
|
|
1518
|
+
const head = kgit.resolveRef(eng, gitdir, "refs/heads/main");
|
|
1427
1519
|
eng._ckptPushed ??= new Map();
|
|
1428
1520
|
if (head && eng._ckptPushed.get(ws) === `${lawHash}:${head}`) {
|
|
1429
1521
|
return { pushed: false, reason: "unchanged" };
|
|
@@ -1433,12 +1525,66 @@ export async function pushCheckpoint(eng, ws, ledger, lawHash) {
|
|
|
1433
1525
|
const tag = `${lawHash}:${ex.cursor}`;
|
|
1434
1526
|
if (eng._ckptPushed.get(ws) === tag) return { pushed: false, reason: "unchanged" };
|
|
1435
1527
|
const { ref } = await writeCheckpointObjects(eng, ws, ex, lawHash);
|
|
1436
|
-
await
|
|
1528
|
+
await custodyPush(eng, ws, ledger, { remoteRef: ref }); // new checkpoint commit → ships its objects
|
|
1437
1529
|
eng._ckptPushed.set(ws, tag);
|
|
1438
1530
|
return { pushed: true, cursor: ex.cursor, bytes: ex.bytes ?? 0 };
|
|
1439
1531
|
} catch (e) { return { pushed: false, reason: String((e && e.message) || e).slice(0, 160) }; }
|
|
1440
1532
|
}
|
|
1441
1533
|
|
|
1534
|
+
// ── WRITE-BEHIND CHECKPOINT (the cold-fold-on-remount fix) ─────────────────────────────────────────
|
|
1535
|
+
// THE BUG this kills: with write-behind custody on, we mirrored only the LEDGER (the commits), never the
|
|
1536
|
+
// CHECKPOINT (the folded projection). So a re-mounted/evicted holon found no checkpoint and RE-FOLDED the
|
|
1537
|
+
// whole chain on its first admit — O(chain), ~5.7ms/intent, ~9s on a 1635-intent ledger. "Warm" was a lie:
|
|
1538
|
+
// mount restored the git bytes but not the folded truth. These two halves let the write-behind lane persist
|
|
1539
|
+
// the checkpoint the SAME way it persists the ledger — export under the wasm lock (brief, O(delta) on a warm
|
|
1540
|
+
// holon), push off the lock — so a cold mount restores O(1) and admits one intent in O(delta). Cold ≈ warm.
|
|
1541
|
+
|
|
1542
|
+
/** STAGE a checkpoint LOCALLY (wasm export + git object write). The caller MUST hold the wasm serialize
|
|
1543
|
+
* lock (exportCheckpoint re-enters the engine). Returns {ref, tag, cursor, bytes} ready to push, or null
|
|
1544
|
+
* when there is nothing to persist (no law / empty / unchanged since the last push). */
|
|
1545
|
+
export async function stageCheckpoint(eng, ws, lawHash) {
|
|
1546
|
+
try {
|
|
1547
|
+
lawHash = await lawParity(eng, ws, lawHash);
|
|
1548
|
+
if (!lawHash) return null;
|
|
1549
|
+
eng._ckptPushed ??= new Map();
|
|
1550
|
+
const ex = exportCheckpoint(eng, ws);
|
|
1551
|
+
if (!ex || ex.empty || !ex.ok || !ex.blob) return null;
|
|
1552
|
+
const tag = `${lawHash}:${ex.cursor}`;
|
|
1553
|
+
if (eng._ckptPushed.get(ws) === tag) return null; // already mirrored this cursor
|
|
1554
|
+
const { ref } = await writeCheckpointObjects(eng, ws, ex, lawHash);
|
|
1555
|
+
return { ref, tag, cursor: ex.cursor, bytes: ex.bytes ?? 0 };
|
|
1556
|
+
} catch { return null; }
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/** PUSH a staged checkpoint to custody. Network only — the caller must NOT hold the wasm lock (the staged
|
|
1560
|
+
* git objects are immutable once written, so the push is race-free against concurrent admits). */
|
|
1561
|
+
export async function pushStagedCheckpoint(eng, ws, ledger, staged) {
|
|
1562
|
+
if (!staged || !staged.ref) return { pushed: false };
|
|
1563
|
+
await custodyPush(eng, ws, ledger, { remoteRef: staged.ref });
|
|
1564
|
+
(eng._ckptPushed ??= new Map()).set(ws, staged.tag);
|
|
1565
|
+
return { pushed: true, cursor: staged.cursor, bytes: staged.bytes };
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/** RECOVERY: delete EVERY `refs/meta/checkpoint/*` on the ledger. A corrupt checkpoint wedges cold
|
|
1569
|
+
* mount (the restore traps before the holon can serve); deleting it makes the next mount re-fold
|
|
1570
|
+
* from `main` (content-addressed, intact) and write a fresh checkpoint on the next write. Pushes the
|
|
1571
|
+
* ref deletion to the RAW Artifacts remote with the ledger's own write token (no per-ref gate there
|
|
1572
|
+
* — the gate is the worker's git PROXY, not custody). Returns the deleted ref names. */
|
|
1573
|
+
export async function clearCheckpointRefs(ledger) {
|
|
1574
|
+
const headers = ledger.headers || {};
|
|
1575
|
+
// A ref deletion ships NO objects + needs NO local repo: the receive-pack advertisement gives each ref's
|
|
1576
|
+
// current oid (the deletion command's old-oid), and pushPack sends the empty pack — pure custody transport.
|
|
1577
|
+
const refs = await lsRefs(ledger.remote, headers, { service: "git-receive-pack", prefix: "refs/meta/checkpoint/" });
|
|
1578
|
+
const deleted = [];
|
|
1579
|
+
for (const ref of Object.keys(refs)) {
|
|
1580
|
+
try {
|
|
1581
|
+
await pushVia(ledger, () => pushPack(ledger.remote, headers, { updates: [{ ref, oldOid: refs[ref], newOid: ZERO_OID }] }));
|
|
1582
|
+
deleted.push(ref);
|
|
1583
|
+
} catch { /* best-effort cleanup */ }
|
|
1584
|
+
}
|
|
1585
|
+
return deleted;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1442
1588
|
/** Restore ws's projection from custody BEFORE the first read on a cold mount: ls-remote the law's
|
|
1443
1589
|
* checkpoint ref, shallow-fetch ONLY that commit (O(checkpoint), never O(chain)), import it. No-op if
|
|
1444
1590
|
* absent or the law drifted. Returns {restored, cursor?, reason?}. */
|
|
@@ -1451,13 +1597,12 @@ export async function restoreCheckpointFromCustody(eng, ws, ledger, lawHash) {
|
|
|
1451
1597
|
lawHash = await lawParity(eng, ws, lawHash);
|
|
1452
1598
|
if (!lawHash) return { restored: false, reason: "no-law" };
|
|
1453
1599
|
const ref = CKPT_REF(lawHash);
|
|
1454
|
-
const
|
|
1455
|
-
const
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
const { blob, frontier } = await readCheckpointBlob(eng, ws, found.oid);
|
|
1600
|
+
const serverRefs = await custodyLsRefs(ledger, ref);
|
|
1601
|
+
const foundOid = serverRefs[ref];
|
|
1602
|
+
if (!foundOid) return { restored: false, reason: "absent" };
|
|
1603
|
+
// Fetch ONLY the checkpoint commit (parentless → O(checkpoint), never O(chain)) into ws's repo + apply_pack.
|
|
1604
|
+
await custodyFetch(eng, ws, ledger, { remoteRef: ref });
|
|
1605
|
+
const { blob, frontier } = await readCheckpointBlob(eng, ws, foundOid);
|
|
1461
1606
|
const r = importCheckpoint(eng, ws, blob, frontier);
|
|
1462
1607
|
if (r && r.restored === true && r.cursor) {
|
|
1463
1608
|
eng._ckptPushed ??= new Map();
|
|
@@ -1467,17 +1612,72 @@ export async function restoreCheckpointFromCustody(eng, ws, ledger, lawHash) {
|
|
|
1467
1612
|
} catch (e) { return { restored: false, reason: String((e && e.message) || e).slice(0, 160) }; }
|
|
1468
1613
|
}
|
|
1469
1614
|
|
|
1470
|
-
// Serialize a push through the
|
|
1615
|
+
// Serialize a push through the adapter's per-repo lane when present (the DO threads `ledger.pushLane`); the
|
|
1471
1616
|
// container has none and serializes on its own chain. The point: ONE receive-pack to a given Artifacts repo
|
|
1472
1617
|
// at a time — a detached checkpoint/shape push can never race the admit's main push (concurrent receive-packs
|
|
1473
1618
|
// were corrupting/500ing the custody store). All same-repo pushes below funnel through here.
|
|
1474
1619
|
const pushVia = (ledger, fn) => (ledger && ledger.pushLane ? ledger.pushLane(fn) : fn());
|
|
1475
1620
|
|
|
1621
|
+
// ── DUMB-PIPE CUSTODY (raw smart-HTTP via custody-transport; the KERNEL builds/ingests every pack). ──
|
|
1622
|
+
// These replace isomorphic-git's getRemoteInfo/listServerRefs/init/addRemote/fetch/push. The adapter only
|
|
1623
|
+
// shuttles bytes: ls-refs to learn a tip, fetchPack→apply_pack to ingest, packDelta→pushPack to emit.
|
|
1624
|
+
const fullRef = (r) => (r === "HEAD" || r.startsWith("refs/") ? r : `refs/heads/${r}`);
|
|
1625
|
+
const applyPackInto = (eng, ws, pack) => {
|
|
1626
|
+
if (!pack || !pack.length) return;
|
|
1627
|
+
const r = JSON.parse(call(eng.ex, "apply_pack", { repoArg: repoArgOf(ws), packB64: b64Bytes(pack) }, eng.STDERR));
|
|
1628
|
+
if (!r || r.ok !== true) throw new Error(`apply_pack: ${r && r.error}`);
|
|
1629
|
+
};
|
|
1630
|
+
// Build the bare-repo skeleton (plain fs) so gix can open the repo BEFORE any objects land — mirrors the
|
|
1631
|
+
// client: gix init_bare trips wasi getcwd, so write HEAD/objects/refs directly. Idempotent.
|
|
1632
|
+
async function custodySkeleton(eng, gitdir) {
|
|
1633
|
+
const p = eng.fs.promises;
|
|
1634
|
+
for (const d of ["", "/objects", "/objects/pack", "/objects/info", "/refs", "/refs/heads"]) {
|
|
1635
|
+
try { await p.mkdir(`${gitdir}${d}`); } catch (e) { if (e && e.code !== "EEXIST") throw e; }
|
|
1636
|
+
}
|
|
1637
|
+
try { await p.writeFile(`${gitdir}/HEAD`, `ref: refs/heads/${BRANCH}\n`); } catch {}
|
|
1638
|
+
try { await p.writeFile(`${gitdir}/config`, "[core]\n\trepositoryformatversion = 0\n\tbare = true\n"); } catch {}
|
|
1639
|
+
}
|
|
1640
|
+
// ls-remote (upload-pack) → { fullRefName: oid }.
|
|
1641
|
+
const custodyLsRefs = (ledger, prefix = null) => lsRefs(ledger.remote, ledger.headers || {}, { service: "git-upload-pack", prefix });
|
|
1642
|
+
// Fetch `remoteRef`'s history into ws's repo (have=what we hold), apply_pack it, point `localRef` at the tip.
|
|
1643
|
+
// Returns the tip oid (null if the remote lacks the ref). `intoWs` lets a caller ingest into a different repo.
|
|
1644
|
+
async function custodyFetch(eng, ws, ledger, { remoteRef = "refs/heads/main", localRef = null, have = null } = {}) {
|
|
1645
|
+
const remoteRefFull = fullRef(remoteRef);
|
|
1646
|
+
const refs = await custodyLsRefs(ledger, remoteRefFull);
|
|
1647
|
+
const tip = refs[remoteRefFull] || null;
|
|
1648
|
+
if (!tip) return null;
|
|
1649
|
+
if (tip !== have) applyPackInto(eng, ws, await fetchPack(ledger.remote, ledger.headers || {}, { wants: [tip], haves: have ? [have] : [] }));
|
|
1650
|
+
kgit.writeRef(eng, gitdirOf(ws), fullRef(localRef || remoteRef), tip);
|
|
1651
|
+
return tip;
|
|
1652
|
+
}
|
|
1653
|
+
// Push a local ref to custody (advance / force / delete). The kernel builds the pack base(remoteOld)..new;
|
|
1654
|
+
// a delete or no-op sends the empty pack. Funnels through the per-repo push lane (serialised receive-packs).
|
|
1655
|
+
// `pointer`: the target oid is ALREADY in custody (a ref that points at an existing commit — shape/rename),
|
|
1656
|
+
// so send the EMPTY pack (a pure ref update, no objects). `newOid` overrides resolving a local ref.
|
|
1657
|
+
async function custodyPush(eng, ws, ledger, { localRef = null, remoteRef, del = false, pointer = false, newOid = null } = {}) {
|
|
1658
|
+
const headers = ledger.headers || {};
|
|
1659
|
+
const remoteRefFull = fullRef(remoteRef);
|
|
1660
|
+
const remote = await lsRefs(ledger.remote, headers, { service: "git-receive-pack", prefix: remoteRefFull });
|
|
1661
|
+
const oldOid = remote[remoteRefFull] || ZERO_OID;
|
|
1662
|
+
if (del) {
|
|
1663
|
+
if (oldOid === ZERO_OID) return; // already gone
|
|
1664
|
+
return pushVia(ledger, () => pushPack(ledger.remote, headers, { updates: [{ ref: remoteRefFull, oldOid, newOid: ZERO_OID }] }));
|
|
1665
|
+
}
|
|
1666
|
+
const tgt = newOid || kgit.resolveRef(eng, gitdirOf(ws), fullRef(localRef || remoteRef));
|
|
1667
|
+
if (!tgt) throw new Error(`custodyPush: local ref ${localRef || remoteRef} unresolved`);
|
|
1668
|
+
if (tgt === oldOid) return; // up to date
|
|
1669
|
+
// pointer ⇒ no objects (already in custody). Else: FF advance ⇒ delta base..new (minimal); non-ancestor
|
|
1670
|
+
// (force) ⇒ pack_delta returns null → full pack.
|
|
1671
|
+
let pack = null;
|
|
1672
|
+
if (!pointer) { pack = oldOid !== ZERO_OID ? packDelta(eng, ws, oldOid, tgt) : null; if (!pack) pack = packDelta(eng, ws, "", tgt); }
|
|
1673
|
+
return pushVia(ledger, () => pushPack(ledger.remote, headers, { updates: [{ ref: remoteRefFull, oldOid, newOid: tgt }], pack }));
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1476
1676
|
// DURABILITY-BEFORE-ACK: only acked once the write LANDED in the ledger; a failed push
|
|
1477
1677
|
// unmounts (drops local state) so the next access re-restores from custody.
|
|
1478
1678
|
export async function commitAndPush(eng, ws, ledger) {
|
|
1479
1679
|
try {
|
|
1480
|
-
await
|
|
1680
|
+
await custodyPush(eng, ws, ledger, { remoteRef: "refs/heads/main" });
|
|
1481
1681
|
} catch (e) {
|
|
1482
1682
|
unmount(eng, ws);
|
|
1483
1683
|
throw new Error(`ledger push failed — write NOT durable, rolled back to ledger: ${e}`);
|
|
@@ -1498,15 +1698,15 @@ export async function pushShape(eng, ws, ledger) {
|
|
|
1498
1698
|
const bytes = st && (st.sqliteBytes ?? st.bytes);
|
|
1499
1699
|
if (bytes == null) return;
|
|
1500
1700
|
const gitdir = gitdirOf(ws);
|
|
1501
|
-
const head =
|
|
1701
|
+
const head = kgit.resolveRef(eng, gitdir, "refs/heads/main");
|
|
1502
1702
|
if (!head) return;
|
|
1503
1703
|
const newRef = `refs/meta/shape/b${bytes}`;
|
|
1504
1704
|
eng._lastShapeRef ??= new Map();
|
|
1505
1705
|
if (eng._lastShapeRef.get(ws) === newRef) return;
|
|
1506
|
-
|
|
1507
|
-
await
|
|
1706
|
+
kgit.writeRef(eng, gitdir, newRef, head);
|
|
1707
|
+
await custodyPush(eng, ws, ledger, { remoteRef: newRef, pointer: true }); // points at head (already in custody)
|
|
1508
1708
|
const prev = eng._lastShapeRef.get(ws);
|
|
1509
|
-
if (prev && prev !== newRef) { try { await
|
|
1709
|
+
if (prev && prev !== newRef) { try { await custodyPush(eng, ws, ledger, { remoteRef: prev, del: true }); } catch { /* leftover refs are harmless: the reader takes max(bytes) */ } }
|
|
1510
1710
|
eng._lastShapeRef.set(ws, newRef);
|
|
1511
1711
|
} catch { /* best-effort; placement self-heals */ }
|
|
1512
1712
|
}
|
|
@@ -1515,30 +1715,33 @@ export async function pushShape(eng, ws, ledger) {
|
|
|
1515
1715
|
// Returns {bytes} (the max, in case a restart left a stale ref) or null (no shape yet).
|
|
1516
1716
|
export async function readShape(ledger) {
|
|
1517
1717
|
try {
|
|
1518
|
-
const refs = await
|
|
1718
|
+
const refs = await custodyLsRefs(ledger, "refs/meta/shape/");
|
|
1519
1719
|
let best = null;
|
|
1520
|
-
for (const
|
|
1521
|
-
const m = /refs\/meta\/shape\/b(\d+)$/.exec(
|
|
1720
|
+
for (const ref of Object.keys(refs)) {
|
|
1721
|
+
const m = /refs\/meta\/shape\/b(\d+)$/.exec(ref);
|
|
1522
1722
|
if (m) { const bytes = +m[1]; if (!best || bytes > best.bytes) best = { bytes }; }
|
|
1523
1723
|
}
|
|
1524
1724
|
return best;
|
|
1525
1725
|
} catch { return null; }
|
|
1526
1726
|
}
|
|
1527
1727
|
|
|
1528
|
-
// READ VISIBILITY (slice 5.1): the
|
|
1529
|
-
// read RPC (an impure identity read — never a
|
|
1728
|
+
// READ VISIBILITY (slice 5.1): the adapter STAMPS the presented `principal` onto the
|
|
1729
|
+
// read RPC (an impure identity read — never a adapter DECISION); the githolon gates the
|
|
1530
1730
|
// read against its OWN folded role-bindings and returns `{ok:false, error:"read-
|
|
1531
1731
|
// forbidden:..."}` for a private type the principal may not read. Absent/"" principal
|
|
1532
1732
|
// is the pre-5.1 wire — public reads are byte-identical.
|
|
1533
1733
|
// `keys` (slice 6) = the reader's injected scope keys [{scope, epoch, key(b64)}], resolved
|
|
1534
|
-
//
|
|
1535
|
-
|
|
1536
|
-
|
|
1734
|
+
// adapter-side (X25519 unwrap). Threaded into the read RPC; the wasm AEAD-opens envelope fields.
|
|
1735
|
+
// EVERY adapter read rides the opaque `query` verb (offer-kernel P2): the kernel's rpc_query_bytes dispatches
|
|
1736
|
+
// by `op` to the read handler + decides the gate; the adapter never calls a typed read RPC. (The typed arms
|
|
1737
|
+
// survive behind `legacy-adapter-api` until P8 deletes them; no caller reaches them after this.)
|
|
1738
|
+
export const qById = (eng, ws, id, principal = "", keys = []) => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "byId", aggregateId: id }), principal, keys, branch: BRANCH }, eng.STDERR));
|
|
1739
|
+
export const query = (eng, ws, queryId, paramsJson, principal = "", keys = []) => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ kind: "query", queryId, paramsJson }), principal, keys, branch: BRANCH }, eng.STDERR));
|
|
1537
1740
|
// THE ZANZIBAR CHECK/EXPAND (governance authz spine): a pure, replay-deterministic authority probe
|
|
1538
1741
|
// over the projected relations table. `check` → { ok, allowed:bool }; `expand` → { ok, subjects:[...] }.
|
|
1539
1742
|
// No principal/keys: relation facts are public authority state (admin/creator/platformCreator/capability).
|
|
1540
|
-
export const check = (eng, ws, object, relation, subject) => JSON.parse(call(eng.ex, "
|
|
1541
|
-
export const expand = (eng, ws, object, relation) => JSON.parse(call(eng.ex, "
|
|
1743
|
+
export const check = (eng, ws, object, relation, subject) => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "check", object, relation, subject }), branch: BRANCH }, eng.STDERR));
|
|
1744
|
+
export const expand = (eng, ws, object, relation) => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "expand", object, relation }), branch: BRANCH }, eng.STDERR));
|
|
1542
1745
|
// THE AUTHOR-SIDE SEAL (slice 6): encrypt a plaintext field value into the on-chain envelope
|
|
1543
1746
|
// (XChaCha isn't in WebCrypto, so the client calls the wasm; the scope key is injected, never on chain).
|
|
1544
1747
|
// The SLOT IDENTITY is the #58 STABLE SID (rename-safety): the caller resolves (type,field)→sid
|
|
@@ -1547,11 +1750,11 @@ export const expand = (eng, ws, object, relation) => JSON.parse(call(eng.ex, "ex
|
|
|
1547
1750
|
export const cryptoSealField = (eng, { scopeKey, scope, epoch, aggSid, fieldSid, plaintext }) =>
|
|
1548
1751
|
JSON.parse(call(eng.ex, "crypto_seal_field", { scopeKey, scope, epoch, aggSid, fieldSid, plaintext }, eng.STDERR)).envelope;
|
|
1549
1752
|
// The client X25519 crypto (slice 6): keygen (device identity), wrap (committer → recipient pubkey),
|
|
1550
|
-
// unwrap (reader's device secret → scope key). The secret stays client/
|
|
1753
|
+
// unwrap (reader's device secret → scope key). The secret stays client/adapter-side, never on chain.
|
|
1551
1754
|
export const cryptoKeygen = (eng) => JSON.parse(call(eng.ex, "crypto_keygen", {}, eng.STDERR));
|
|
1552
1755
|
|
|
1553
1756
|
// AUTHOR DEVICE KEY (warrant-flip, identity_keys.md) — mint an ed25519 signing keypair IN the holon
|
|
1554
|
-
// (`author_keygen`); the secret stays client-side (here: the test/
|
|
1757
|
+
// (`author_keygen`); the secret stays client-side (here: the test/adapter injects it back as `authorSecret`
|
|
1555
1758
|
// on the author call). Returns { ok, secret, public } (both base64). `author_sign` is the lower-level
|
|
1556
1759
|
// standalone signer; the author DOOR signs internally from the injected secret, so callers use `author`.
|
|
1557
1760
|
export const authorKeygen = (eng) => JSON.parse(call(eng.ex, "author_keygen", {}, eng.STDERR));
|
|
@@ -1564,39 +1767,95 @@ export const attestationSign = (eng, { secret, asserterWorkspace, object, relati
|
|
|
1564
1767
|
export const cryptoWrapKey = (eng, { recipientPub, scopeKey }) => JSON.parse(call(eng.ex, "crypto_wrap_key", { recipientPub, scopeKey }, eng.STDERR));
|
|
1565
1768
|
export const cryptoUnwrapKey = (eng, { secret, hpkeEpk, ct }) => JSON.parse(call(eng.ex, "crypto_unwrap_key", { secret, hpkeEpk, ct }, eng.STDERR)).scopeKey;
|
|
1566
1769
|
// THE READ-POSTURE PROBE (slice 5.1): the githolon's own verdict on whether the
|
|
1567
|
-
// workspace is private + whether `principal` may read ALL of it — the
|
|
1568
|
-
// to gate the
|
|
1569
|
-
export const readPosture = (eng, ws, principal = "") => JSON.parse(call(eng.ex, "
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
export const
|
|
1770
|
+
// workspace is private + whether `principal` may read ALL of it — the adapter relays it
|
|
1771
|
+
// to gate the WHOLE-LEDGER reads (snapshot, git clone) it cannot serve partially.
|
|
1772
|
+
export const readPosture = (eng, ws, principal = "") => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "readPosture" }), principal, branch: BRANCH }, eng.STDERR));
|
|
1773
|
+
// count/sum/extremum are GATED IN THE KERNEL (it knows each tally's `of` type + the read-visibility): the
|
|
1774
|
+
// adapter STAMPS the verified `principal` and relays the holon's verdict — it runs NO gate of its own. They ride
|
|
1775
|
+
// the opaque `query` verb (P2): `op` selects the handler; extremum's own `kind` stays its min/max selector.
|
|
1776
|
+
export const count = (eng, ws, countId, groupKey, principal = "") => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "count", countId, groupKey }), principal, branch: BRANCH }, eng.STDERR));
|
|
1777
|
+
export const sum = (eng, ws, sumId, groupKey, principal = "") => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "sum", sumId, groupKey }), principal, branch: BRANCH }, eng.STDERR));
|
|
1778
|
+
/** The maintained O(1) min/max read (#47). `kind` = "min"|"max"; an absent group answers `value: null`
|
|
1779
|
+
* (never a fabricated 0). Kernel-gated; rides the `query` verb (`op:"extremum"`, `kind` = min/max). */
|
|
1780
|
+
export const extremum = (eng, ws, extremumId, kind, groupKey, principal = "") => JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "extremum", extremumId, kind, groupKey: groupKey ?? "" }), principal, branch: BRANCH }, eng.STDERR));
|
|
1781
|
+
// GIT OFF THE HOT PATH: the KERNEL builds the conformant delta packfile (gix-pack); the adapter is dumb
|
|
1782
|
+
// plumbing (frames pkt-lines + POSTs). Returns the pack bytes (Uint8Array), or null if the kernel
|
|
1783
|
+
// couldn't produce a delta (base not an ancestor / head==base) — the adapter then falls back to a standard
|
|
1784
|
+
// main-ref push (backupToCustody). NO isomorphic-git, NO negotiation: the sole writer knows its delta.
|
|
1785
|
+
export function packDelta(eng, ws, base, head) {
|
|
1786
|
+
try {
|
|
1787
|
+
const r = JSON.parse(call(eng.ex, "pack_delta", { repoArg: repoArgOf(ws), base, head }, eng.STDERR));
|
|
1788
|
+
if (!r || r.ok !== true || typeof r.packB64 !== "string") return null;
|
|
1789
|
+
return Uint8Array.from(atob(r.packB64), (c) => c.charCodeAt(0));
|
|
1790
|
+
} catch { return null; }
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// THE SERVING-LANE PACK (kernel-native clone/pull, replacing isomorphic-git fetch). `base` empty ⇒ a FULL
|
|
1794
|
+
// pack of all reachable from main (fresh clone); `base` = the client's known head ⇒ the incremental delta.
|
|
1795
|
+
// The kernel resolves main itself. Returns `{ packB64, bytes }` (base64 so it rides the container JSON op).
|
|
1796
|
+
export function servePack(eng, ws, base = "") {
|
|
1797
|
+
let r;
|
|
1798
|
+
try {
|
|
1799
|
+
r = JSON.parse(call(eng.ex, "pack_delta", { repoArg: repoArgOf(ws), workspace: ws, base, branch: BRANCH }, eng.STDERR));
|
|
1800
|
+
} catch (e) {
|
|
1801
|
+
// UP TO DATE: the client's `have` already IS main → nothing to send. A clean empty response, not an error.
|
|
1802
|
+
if (/nothing to pack|head == base/.test(String(e))) return { packB64: "", bytes: 0, upToDate: true };
|
|
1803
|
+
throw e;
|
|
1804
|
+
}
|
|
1805
|
+
if (!r || r.ok !== true || typeof r.packB64 !== "string") throw new Error("servePack: kernel produced no pack");
|
|
1806
|
+
return { packB64: r.packB64, bytes: r.bytes };
|
|
1807
|
+
}
|
|
1575
1808
|
export const nomosActive = (eng, ws) => qById(eng, ws, `domain-installation:${eng.hashes.nomos}`).length > 0;
|
|
1576
1809
|
// E2E crypto linkage probe (the encryption arc): run the wrap→unwrap + field seal→open
|
|
1577
1810
|
// INSIDE the deployable wasm. Pure — no workspace, no chain. Returns {wrapHandoff, fieldAead}.
|
|
1578
|
-
export const cryptoSelftest = (eng) => JSON.parse(call(eng.ex, "crypto_selftest", { repoArg: "/work/none" }, eng.STDERR));
|
|
1579
1811
|
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1812
|
+
// THE OFFER ADMISSION VERB (offer-kernel migration P1): admit a SEALED intent into `branch`
|
|
1813
|
+
// through the one gate and return the STRUCTURED VERDICT — `{ok:true, outcome:"admitted", head}`
|
|
1814
|
+
// or `{ok:true, outcome:"refused", offerId, verdict:{reason}}`. The adapter's admit lane migrates
|
|
1815
|
+
// onto this off `apply_intent` over the P1 slices; `defer` carries the batch-projection-defer.
|
|
1816
|
+
export function offerIntent(eng, ws, bytes, opts) {
|
|
1817
|
+
const name = `offer-in-${eng.seq++}.json`;
|
|
1583
1818
|
writeWork(eng, name, bytes);
|
|
1584
|
-
|
|
1819
|
+
const deferProjection = !!(opts && opts.defer);
|
|
1820
|
+
const branch = (opts && opts.branch) || BRANCH;
|
|
1821
|
+
return JSON.parse(call(eng.ex, "offer", { repoArg: repoArgOf(ws), workspace: ws, intentFile: `/work/${name}`, branch, deferProjection }, eng.STDERR));
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/**
|
|
1825
|
+
* RE-ADMIT sealed bytes through the OFFER verb (offer-kernel P1.2 — the one effectful move), normalized to
|
|
1826
|
+
* the `{ok, head, error}` shape every adapter admit lane consumes. The adapter carries the bytes; the HOLON
|
|
1827
|
+
* adjudicates — `admitted` → the new head; `refused` → the gate's own verdict reason. This is how EVERY adapter
|
|
1828
|
+
* re-admission speaks `offer` (session admit, shard migrate, global replicate, DLQ retry); `apply_intent` is
|
|
1829
|
+
* retired from the adapter (the wasm arm survives behind `legacy-adapter-api` only until P8 deletes it).
|
|
1830
|
+
*/
|
|
1831
|
+
export function offerAdmit(eng, ws, bytes, opts) {
|
|
1832
|
+
const v = offerIntent(eng, ws, bytes, opts);
|
|
1833
|
+
return v.outcome === "admitted"
|
|
1834
|
+
? { ok: true, head: v.head }
|
|
1835
|
+
: { ok: false, error: v.verdict?.reason ?? v.error ?? "the offer was refused" };
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
/** Re-admit ONE carried intent (bytes) on local main — through the ONE effectful verb `offer` (no
|
|
1839
|
+
* apply_intent back door). Normalized to the legacy {ok, head, error} shape every caller (bench, CLI
|
|
1840
|
+
* replay, examples, DLQ retry) consumes. `defer` rides through to offer's batch-projection-defer. */
|
|
1841
|
+
export function applyIntentBytes(eng, ws, bytes, opts) {
|
|
1842
|
+
return offerAdmit(eng, ws, bytes, opts);
|
|
1585
1843
|
}
|
|
1586
1844
|
|
|
1587
1845
|
// Main's recent intent ids + oids, cached by head (admission idempotence + walk boundary).
|
|
1588
1846
|
async function mainIntentIds(eng, ws) {
|
|
1589
1847
|
const gitdir = gitdirOf(ws);
|
|
1590
|
-
const head =
|
|
1848
|
+
const head = kgit.resolveRef(eng, gitdir, BRANCH);
|
|
1591
1849
|
if (!head) return { head: null, ids: new Set(), oids: new Set() };
|
|
1592
1850
|
const cached = eng.mainIntents.get(ws);
|
|
1593
1851
|
if (cached && cached.head === head) return cached;
|
|
1594
1852
|
const ids = new Set(), oids = new Set();
|
|
1595
|
-
|
|
1853
|
+
// Main is a linear chain of intent commits (the container appends, never merges), so the recent intent
|
|
1854
|
+
// commits ARE the recent main commits — the bounded idempotence window + walk boundary (was git.log depth 500).
|
|
1855
|
+
for (const c of kgit.intentsAbove(eng, gitdir, head, "", 500)) {
|
|
1596
1856
|
oids.add(c.oid);
|
|
1597
1857
|
try {
|
|
1598
|
-
const
|
|
1599
|
-
const id = JSON.parse(dec.decode(blob)).id;
|
|
1858
|
+
const id = JSON.parse(dec.decode(bytesFromB64(c.intentB64))).id;
|
|
1600
1859
|
if (typeof id === "string" && id) ids.add(id);
|
|
1601
1860
|
} catch {}
|
|
1602
1861
|
}
|
|
@@ -1608,14 +1867,21 @@ async function mainIntentIds(eng, ws) {
|
|
|
1608
1867
|
/**
|
|
1609
1868
|
* EDGE ADMISSION (the one machinery): judge every session/<cid> ref. Structural lane
|
|
1610
1869
|
* refusals (control-plane intents, undeployed domains) and domain rejections route per
|
|
1611
|
-
* Jack's law: legitimate work DEAD-LETTERS (returned to the
|
|
1870
|
+
* Jack's law: legitimate work DEAD-LETTERS (returned to the adapter to persist — the
|
|
1612
1871
|
* engine has no durable store), obvious attacks drop. Admitted intents merge to main,
|
|
1613
1872
|
* durably push, the branch is consumed.
|
|
1614
1873
|
*/
|
|
1615
|
-
export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFence = null, shardRouting = null,
|
|
1874
|
+
export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFence = null, shardRouting = null, defer = false } = {}) {
|
|
1616
1875
|
const refuse = new Set(refuseDomains);
|
|
1617
1876
|
const routes = shardRouting ? directiveRoutes(eng) : null;
|
|
1618
1877
|
const gitdir = gitdirOf(ws);
|
|
1878
|
+
// FRESH HEADER COPY PER GIT OP. isomorphic-git's `discover` MUTATES the headers object it's given
|
|
1879
|
+
// (a v2 op stamps `Git-Protocol: version=2` in). Sharing one object across a v2 listServerRefs and a
|
|
1880
|
+
// v1 git.fetch made the fetch send `version=2`, the server answered v2, isomorphic-git parsed it as
|
|
1881
|
+
// v1 → undefined refs → `TypeError: ...reading 'size'` on EVERY session fetch. A fresh copy per call
|
|
1882
|
+
// makes that leak impossible no matter what mutated `ledger.headers` upstream. (Same pattern as the
|
|
1883
|
+
// checkpoint/shape scans' `freshHeaders()`.)
|
|
1884
|
+
const H = () => ({ ...(ledger.headers || {}) });
|
|
1619
1885
|
// FLAME-GRAPH EDGE TIMINGS — coarse per-stage wall-clock the client stitches into the cross-tier trace.
|
|
1620
1886
|
const _t0 = Date.now(); let _fetchMs = 0, _sealMs = 0, _intents = 0;
|
|
1621
1887
|
const _spans = []; let _traceSeq = 0;
|
|
@@ -1628,29 +1894,16 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1628
1894
|
};
|
|
1629
1895
|
const sessionRefPrefix = "refs/heads/session/";
|
|
1630
1896
|
const sessions = {};
|
|
1631
|
-
const
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
const refs = await git.listServerRefs({
|
|
1642
|
-
http,
|
|
1643
|
-
url: ledger.remote,
|
|
1644
|
-
headers: ledger.headers,
|
|
1645
|
-
prefix: sessionRefPrefix,
|
|
1646
|
-
protocolVersion: 2,
|
|
1647
|
-
});
|
|
1648
|
-
for (const r of refs || []) {
|
|
1649
|
-
const ref = typeof r?.ref === "string" ? r.ref : "";
|
|
1650
|
-
const oid = typeof r?.oid === "string" ? r.oid : "";
|
|
1651
|
-
if (!ref.startsWith(sessionRefPrefix) || !oid) continue;
|
|
1652
|
-
sessions[ref.slice(sessionRefPrefix.length)] = oid;
|
|
1653
|
-
}
|
|
1897
|
+
const _scanDone = _span("scan session refs", "cscan", 1);
|
|
1898
|
+
// CUSTODY IS THE ONLY SOURCE. The adapter keeps NO session index — there is no adapter-held head to
|
|
1899
|
+
// diverge from the ledger. Every admit lists the workspace's own session refs (listServerRefs)
|
|
1900
|
+
// and admits exactly what is durably there. (The earlier adapter "session-offer index" let a
|
|
1901
|
+
// client's CLAIMED head outlive the push that never durably landed; admit then chased an oid
|
|
1902
|
+
// custody never had → NotFoundError on every tick → the client's work stranded forever. The
|
|
1903
|
+
// cure is not a better cache — it is no cache: the adapter holds no authority, the ledger does.)
|
|
1904
|
+
//
|
|
1905
|
+
for (const [ref, oid] of Object.entries(await custodyLsRefs(ledger, sessionRefPrefix))) {
|
|
1906
|
+
if (ref.startsWith(sessionRefPrefix) && oid) sessions[ref.slice(sessionRefPrefix.length)] = oid;
|
|
1654
1907
|
}
|
|
1655
1908
|
_scanDone({ sessions: Object.keys(sessions).length });
|
|
1656
1909
|
const _scanMs = Date.now() - _t0;
|
|
@@ -1661,16 +1914,22 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1661
1914
|
const consumed = []; let totalAdmitted = 0;
|
|
1662
1915
|
// §5.4 (slice 4): on the COORDINATOR of a taxonomy that declares `.global(...)`
|
|
1663
1916
|
// reference data, every admitted intent touching a global aggregate is surfaced
|
|
1664
|
-
// to the
|
|
1917
|
+
// to the ADAPTER, which replicates the SAME sealed bytes into each shard chain.
|
|
1665
1918
|
const globalsDeclared = shardRouting && !shardRouting.selfLabel ? globalAggregates(eng) : null;
|
|
1666
1919
|
// ── §5.2 SUMMARY CAPTURE (slice 3): on a SHARD (selfLabel) whose law declares
|
|
1667
1920
|
// estate-scoped reads, derive each admitted intent's per-(read, group) deltas with
|
|
1668
1921
|
// the shard's own projection as the oracle (tallies sandwiched around the fold —
|
|
1669
|
-
// O(touched)); the
|
|
1922
|
+
// O(touched)); the ADAPTER authors the coalesced `nomosPropagateSummary` Order into
|
|
1670
1923
|
// the coordinator afterwards. The coordinator's gate re-verifies everything.
|
|
1671
1924
|
const summaryCtx = shardRouting && shardRouting.selfLabel && scopedReads(eng).length > 0
|
|
1672
1925
|
? { reads: scopedReads(eng), prevs: new Map(), values: new Map(), events: [], rowsDelta: 0, rederived: new Set(), rowsByHome: new Map(), homeTags: new Map(), homeKeySet: new Set() }
|
|
1673
1926
|
: null;
|
|
1927
|
+
// BATCH ADMIT (the flame fix): defer the per-intent projection catch-up + frontier write
|
|
1928
|
+
// to ONE flush after the whole cycle — UNLESS a per-intent projection read is required
|
|
1929
|
+
// mid-loop: §5.2 summary capture (reads pre/post tallies around each fold) or §5.4 global
|
|
1930
|
+
// surfacing (qById per admitted intent). Those keep the synchronous per-intent path. The
|
|
1931
|
+
// common case (no sharding) gets the batch: N×(SQLite catch-up + checkpoint write) → 1.
|
|
1932
|
+
const canDeferProjection = !summaryCtx && !(globalsDeclared && globalsDeclared.size > 0);
|
|
1674
1933
|
if (summaryCtx) {
|
|
1675
1934
|
// SLICE 8 — the per-home attribution table, off the shard's OWN receipts
|
|
1676
1935
|
// (law-state: which homes this chain owns). A row id whose route-tag slot
|
|
@@ -1685,50 +1944,54 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1685
1944
|
}
|
|
1686
1945
|
} catch (e) { log("home-tag-table-error", { ws, error: String(e).slice(0, 200) }); }
|
|
1687
1946
|
}
|
|
1688
|
-
const headBefore = summaryCtx ?
|
|
1947
|
+
const headBefore = summaryCtx ? kgit.resolveRef(eng, gitdir, BRANCH) : null;
|
|
1689
1948
|
for (const [cid, head] of Object.entries(sessions)) {
|
|
1690
1949
|
const ref = `refs/heads/session/${cid}`;
|
|
1691
1950
|
const shortCid = cid.length > 14 ? `${cid.slice(0, 14)}...` : cid;
|
|
1692
1951
|
const _sessionDone = _span(`session ${shortCid}`, "csession", 1, { client: cid });
|
|
1693
1952
|
try {
|
|
1694
|
-
await git.addRemote({ fs: eng.fs, gitdir, remote: "origin", url: ledger.remote, force: true });
|
|
1695
1953
|
const _fetchDone = _span(`fetch ${shortCid}`, "cfetch", 2, { client: cid });
|
|
1696
1954
|
const _f = Date.now();
|
|
1697
1955
|
try {
|
|
1698
|
-
// THIN-FETCH (the 40s→0.5s fix):
|
|
1699
|
-
//
|
|
1700
|
-
//
|
|
1701
|
-
//
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1956
|
+
// THIN-FETCH (the 40s→0.5s fix): pass our main head as the `have` so the server sends ONLY the delta
|
|
1957
|
+
// above the common ancestor (the kernel's apply_pack ingests it). main is append-only, so the client's
|
|
1958
|
+
// fork point is always an ancestor of our head — the thin pack is sound. A failed thin fetch is an
|
|
1959
|
+
// optimization miss, never load-bearing: fall back to a clean full fetch so work is never stranded.
|
|
1960
|
+
const localMain = kgit.resolveRef(eng, gitdir, BRANCH);
|
|
1961
|
+
try {
|
|
1962
|
+
await custodyFetch(eng, ws, ledger, { remoteRef: ref, have: localMain });
|
|
1963
|
+
} catch (thinErr) {
|
|
1964
|
+
log("thin-fetch-fallback", { ws, cid, error: String(thinErr && thinErr.message || thinErr).slice(0, 160) });
|
|
1965
|
+
await custodyFetch(eng, ws, ledger, { remoteRef: ref, have: null });
|
|
1966
|
+
}
|
|
1707
1967
|
} finally {
|
|
1708
1968
|
_fetchMs += Date.now() - _f;
|
|
1709
1969
|
_fetchDone();
|
|
1710
1970
|
}
|
|
1711
1971
|
const _walkDone = _span(`walk ${shortCid}`, "cwalk", 2, { client: cid });
|
|
1712
1972
|
const main = await mainIntentIds(eng, ws);
|
|
1973
|
+
// The session's NEW intents: walk the session tip newest→oldest, stop at the first commit already on
|
|
1974
|
+
// main (the fork point), reverse to oldest→newest. intentsAbove returns {oid, intentB64} so the gate
|
|
1975
|
+
// loop reads the intent straight off the walk (no per-oid readBlob).
|
|
1976
|
+
const recent = kgit.intentsAbove(eng, gitdir, head, "", 100);
|
|
1713
1977
|
const chain = [];
|
|
1714
|
-
for (
|
|
1715
|
-
if (main.oids.has(c.oid)) break;
|
|
1716
|
-
chain.push(c.oid);
|
|
1717
|
-
}
|
|
1978
|
+
for (let i = recent.length - 1; i >= 0; i--) { if (main.oids.has(recent[i].oid)) break; chain.push(recent[i]); }
|
|
1718
1979
|
chain.reverse(); // oldest → newest
|
|
1719
1980
|
_walkDone({ intents: chain.length });
|
|
1720
1981
|
const admitted = [], rejected = [], skipped = [];
|
|
1721
1982
|
const _gateDone = _span(`gate+fold ${shortCid}`, "cgate", 2, { client: cid, intents: chain.length });
|
|
1722
|
-
for (const
|
|
1983
|
+
for (const c of chain) {
|
|
1984
|
+
const oid = c.oid;
|
|
1723
1985
|
try {
|
|
1724
|
-
const
|
|
1986
|
+
const blob = bytesFromB64(c.intentB64);
|
|
1725
1987
|
let dom = null, intentId = null, dirId = null;
|
|
1726
1988
|
try { const doc = JSON.parse(dec.decode(blob)); intentId = typeof doc.id === "string" ? doc.id : null; dom = doc && doc.payload && typeof doc.payload.domain === "string" ? doc.payload.domain : null; dirId = doc && doc.payload && typeof doc.payload.directiveId === "string" ? doc.payload.directiveId : null; } catch {}
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
|
|
1731
|
-
|
|
1989
|
+
// THE GATE DECIDES (offer-kernel doctrine): no adapter domain-string filter for control-plane —
|
|
1990
|
+
// an install/governance offer rides the SAME lane and the kernel gate refuses an author lacking
|
|
1991
|
+
// the declared relation (a TYPED gate refusal, not a adapter verdict). Only `!dom` (malformed) and
|
|
1992
|
+
// the explicit `refuse` fence (root's `workspaces` governance lane) stay — structural, not authority.
|
|
1993
|
+
if (!dom || refuse.has(dom)) {
|
|
1994
|
+
rejected.push({ oid: oid.slice(0, 10), error: `${dom ? `'${dom}' is adapter-fenced on this workspace` : "non-authored intent"} — governance enters its own admin lane` });
|
|
1732
1995
|
continue;
|
|
1733
1996
|
}
|
|
1734
1997
|
// THE RECEIPT FENCE (capability_marketplace.md slice 1.5 — the ratified
|
|
@@ -1752,10 +2015,10 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1752
2015
|
if (routes && dom && dirId) {
|
|
1753
2016
|
// THE SEAL PEN IS THE BAILIFF'S (slice 5): `nomosSealHome` (strikes the
|
|
1754
2017
|
// migrated history out of a source's fold) and `nomosSealHandoff` (flips
|
|
1755
|
-
// the map) are
|
|
2018
|
+
// the map) are ADAPTER-orchestrated legs of the §6 move — a session-lane
|
|
1756
2019
|
// offer of either is an attack on the move lane, refused and DROPPED.
|
|
1757
2020
|
if (dirId === "nomosSealHome" || dirId === "nomosSealHandoff") {
|
|
1758
|
-
rejected.push({ oid: oid.slice(0, 10), error: `session lane refuses '${dirId}' — the §6 handoff seals are authored by the
|
|
2021
|
+
rejected.push({ oid: oid.slice(0, 10), error: `session lane refuses '${dirId}' — the §6 handoff seals are authored by the adapter after migration verifies (begin a move with nomosSplitShard, or POST /v1/workspaces/<coordinator>/split)` });
|
|
1759
2022
|
continue;
|
|
1760
2023
|
}
|
|
1761
2024
|
const route = routes.get(`${dom} ${dirId}`);
|
|
@@ -1801,13 +2064,21 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1801
2064
|
continue;
|
|
1802
2065
|
}
|
|
1803
2066
|
const cap = summaryCtx ? beginSummaryCapture(eng, ws, summaryCtx, blob) : null;
|
|
1804
|
-
|
|
2067
|
+
// OFFER-KERNEL P1.2: edge re-admission runs through the `offer` verb (the doctrine's one
|
|
2068
|
+
// effectful move) over the SAME admit_core gate. The structured verdict is normalized
|
|
2069
|
+
// back to the {ok, head, error} the outcome handling below consumes; the refused/parked
|
|
2070
|
+
// ref-custody (replacing the JS DLQ) is the next sub-slice — for now refusals still
|
|
2071
|
+
// dead-letter exactly as before (the verdict reason IS the gate's refusal reason).
|
|
2072
|
+
const verdict = offerIntent(eng, ws, blob, { defer: canDeferProjection });
|
|
2073
|
+
const res = verdict.outcome === "admitted"
|
|
2074
|
+
? { ok: true, head: verdict.head }
|
|
2075
|
+
: { ok: false, error: verdict.verdict?.reason ?? verdict.error ?? "the offer was refused" };
|
|
1805
2076
|
if (res.ok) {
|
|
1806
2077
|
main.ids.add(intentId); main.oids.add(res.head); main.head = res.head;
|
|
1807
2078
|
admitted.push({ oid: oid.slice(0, 10), head: res.head });
|
|
1808
2079
|
if (cap) finishSummaryCapture(eng, ws, summaryCtx, cap, res.head, intentId);
|
|
1809
2080
|
// AN ADMITTED PLACEMENT (birth<Type> on the coordinator): surfaced to the
|
|
1810
|
-
//
|
|
2081
|
+
// ADAPTER so it can author the assignment RECEIPT into the shard's own chain
|
|
1811
2082
|
// (sharding §3 — the receipt leg).
|
|
1812
2083
|
if (shardRouting && dirId) {
|
|
1813
2084
|
const placement = placementDirectives(eng).get(dirId);
|
|
@@ -1820,9 +2091,9 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1820
2091
|
}
|
|
1821
2092
|
}
|
|
1822
2093
|
// AN ADMITTED SPLIT (nomosSplitShard on the coordinator, slice 5):
|
|
1823
|
-
// surfaced to the
|
|
2094
|
+
// surfaced to the ADAPTER so it can run the §6 migration legs (re-admit
|
|
1824
2095
|
// on the target, seal the source, close/open the subtotals, flip the
|
|
1825
|
-
// map) — a session-authored split COMPLETES like a
|
|
2096
|
+
// map) — a session-authored split COMPLETES like a adapter-authored one.
|
|
1826
2097
|
if (dirId === "nomosSplitShard" && !shardRouting.selfLabel) {
|
|
1827
2098
|
let p = null;
|
|
1828
2099
|
try { p = JSON.parse(dec.decode(blob)).payload?.payload ?? null; } catch {}
|
|
@@ -1832,7 +2103,7 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1832
2103
|
}
|
|
1833
2104
|
}
|
|
1834
2105
|
// AN ADMITTED GLOBAL WRITE (§5.4, slice 4 — coordinator only): the sealed
|
|
1835
|
-
// bytes are surfaced to the
|
|
2106
|
+
// bytes are surfaced to the ADAPTER, which re-admits them into every shard
|
|
1836
2107
|
// chain (the reverse lane — replicated reference data). O(events) per
|
|
1837
2108
|
// admitted intent, and only when the law declares `.global(...)`.
|
|
1838
2109
|
if (globalsDeclared && globalsDeclared.size > 0) {
|
|
@@ -1880,13 +2151,26 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1880
2151
|
_sessionDone();
|
|
1881
2152
|
}
|
|
1882
2153
|
}
|
|
2154
|
+
// THE DEFERRED-PROJECTION FLUSH (batch admit): the per-intent applies skipped the SQLite
|
|
2155
|
+
// catch-up + frontier write; flush ONCE here — refresh_projection folds every new commit in
|
|
2156
|
+
// a single incremental catch-up and walks the deferred heads to advance the verified frontier.
|
|
2157
|
+
if (canDeferProjection && totalAdmitted) {
|
|
2158
|
+
const _projDone = _span("projection flush (batch)", "cproject", 1, { admitted: totalAdmitted });
|
|
2159
|
+
try { refreshProjection(eng, ws); } catch (e) { log("admit-projection-flush-failed", { ws, error: String(e).slice(0, 200) }); }
|
|
2160
|
+
finally { _projDone(); }
|
|
2161
|
+
}
|
|
2162
|
+
// OFFER-KERNEL P1.2b: materialize this cycle's refusals as git-custody facts (refs/heads/refused/*),
|
|
2163
|
+
// pushed inline — BEFORE the main seal, which unmounts on failure (a refusal fact must not be lost to
|
|
2164
|
+
// an unrelated main-push failure). The holon's verdict recorded as a HOLON FACT that syncs + survives
|
|
2165
|
+
// eviction; the adapter's ctx.storage DLQ is retired in favour of these.
|
|
2166
|
+
if (deadLetters.length) { try { await materializeRefused(eng, ws, ledger, deadLetters); } catch (e) { log("refused-materialize-failed", { ws, error: String(e).slice(0, 200) }); } }
|
|
1883
2167
|
// THE BATCH SEAL — one receive-pack for the whole admit cycle. A failed push unmounts (engine.unmount drops
|
|
1884
2168
|
// the ws fs); we then SKIP the branch deletes so every offered intent re-admits next cycle (idempotent by
|
|
1885
2169
|
// id). On success, delete the consumed session branches (best-effort cleanup; a stale branch is a harmless
|
|
1886
2170
|
// re-fetch). Single-client workspaces were already one push/cycle; this collapses collaborative bursts.
|
|
1887
|
-
// THE BAILIFF SEAM: when the
|
|
2171
|
+
// THE BAILIFF SEAM: when the ADAPTER asked us to `defer` (the open session admit lane on a
|
|
1888
2172
|
// non-coordinator workspace), we DO NOT push or delete consumed branches inline — the fold
|
|
1889
|
-
// is already on resident main (truth), and the
|
|
2173
|
+
// is already on resident main (truth), and the adapter backs it up to custody off the critical
|
|
1890
2174
|
// path (engine.backupToCustody, debounced + coalesced). This is sound because the SESSION
|
|
1891
2175
|
// BRANCHES are themselves durable in custody (the durable inbox) and admit is idempotent by
|
|
1892
2176
|
// intent id: a crash before backup just re-derives the SAME sealed main from the same offers.
|
|
@@ -1909,12 +2193,12 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1909
2193
|
}
|
|
1910
2194
|
if (_sealed && !deferDurability) {
|
|
1911
2195
|
for (const { ref } of consumed) {
|
|
1912
|
-
try { await
|
|
2196
|
+
try { await custodyPush(eng, ws, ledger, { remoteRef: ref, del: true }); } catch {}
|
|
1913
2197
|
}
|
|
1914
2198
|
}
|
|
1915
|
-
const newMain =
|
|
2199
|
+
const newMain = kgit.resolveRef(eng, gitdir, BRANCH);
|
|
1916
2200
|
log("admit", { ws, sessions: results.length, main: newMain });
|
|
1917
|
-
// The coalesced §5.2 delta of this batch (null when no scoped read moved): the
|
|
2201
|
+
// The coalesced §5.2 delta of this batch (null when no scoped read moved): the ADAPTER
|
|
1918
2202
|
// closes it against the coordinator's recorded frontier and authors the Order.
|
|
1919
2203
|
const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore, newMain) : null;
|
|
1920
2204
|
const _total = Date.now() - _t0;
|
|
@@ -1926,29 +2210,48 @@ export async function admitAll(eng, ws, ledger, { refuseDomains = [], receiptFen
|
|
|
1926
2210
|
...(placements.length ? { placements } : {}),
|
|
1927
2211
|
...(splits.length ? { splits } : {}),
|
|
1928
2212
|
...(globalsOut.length ? { globals: globalsOut } : {}),
|
|
1929
|
-
// The
|
|
2213
|
+
// The adapter's backup lane consumes these: `deferred` ⇒ resident main carries totalAdmitted
|
|
1930
2214
|
// un-backed-up commits and `consumed` are the session branches to delete AFTER the push lands.
|
|
1931
2215
|
...(deferDurability && totalAdmitted ? { deferred: true, consumed, admitted: totalAdmitted } : {}),
|
|
1932
2216
|
};
|
|
1933
2217
|
}
|
|
1934
2218
|
|
|
2219
|
+
// WRITE-BEHIND CUSTODY MIRROR (git off the hot path). Push a CAPTURED head snapshot to custody main —
|
|
2220
|
+
// NOT the live `main` ref (re-resolving it would chase a moving target under concurrent folds, the bug
|
|
2221
|
+
// that stormed the old debounced backup). The container is the SOLE writer of main, so custody is a
|
|
2222
|
+
// pure FF mirror: no pull-back, no reconciliation. Read-only on eng.fs except a transient backup ref
|
|
2223
|
+
// (a different key from anything a fold touches; the head's objects are immutable). NEVER unmounts on
|
|
2224
|
+
// failure — the resident holon stays truth and the client can re-offer (worst case: the mirror lags).
|
|
2225
|
+
export async function backupHead(eng, ws, ledger, head) {
|
|
2226
|
+
if (!eng.mounted.has(ws)) return { ok: false, error: "not mounted" };
|
|
2227
|
+
if (!head) return { ok: false, error: "no head" };
|
|
2228
|
+
try {
|
|
2229
|
+
// Push EXACTLY the captured `head` (not live main, which moves under concurrent folds) to custody main —
|
|
2230
|
+
// the kernel builds the delta remoteOld..head; no local tmp ref needed (custodyPush names the oid).
|
|
2231
|
+
await custodyPush(eng, ws, ledger, { remoteRef: "refs/heads/main", newOid: head });
|
|
2232
|
+
return { ok: true, head };
|
|
2233
|
+
} catch (e) {
|
|
2234
|
+
return { ok: false, error: String((e && e.message) || e).slice(0, 200), head };
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
1935
2238
|
// THE BAILIFF'S BACKUP — push resident main to custody when quiet, NEVER on a write's critical
|
|
1936
2239
|
// path and NEVER destructive. Unlike commitAndPush (durability-before-ack: unmounts on failure),
|
|
1937
|
-
// a failed backup leaves the resident holon mounted and the session branches intact, so the
|
|
2240
|
+
// a failed backup leaves the resident holon mounted and the session branches intact, so the adapter
|
|
1938
2241
|
// just retries on the next quiet tick. Append-only main means an off-lock push of an older head
|
|
1939
2242
|
// concurrent with a fresh fold is always a sound fast-forward subset (the newer head backs up next).
|
|
1940
2243
|
export async function backupToCustody(eng, ws, ledger, { consumed = [] } = {}) {
|
|
1941
2244
|
if (!eng.mounted.has(ws)) return { ok: false, error: "not mounted" };
|
|
1942
2245
|
const gitdir = gitdirOf(ws);
|
|
1943
2246
|
let head = null;
|
|
1944
|
-
|
|
2247
|
+
head = kgit.resolveRef(eng, gitdir, BRANCH); if (!head) return { ok: false, error: "no local main" };
|
|
1945
2248
|
try {
|
|
1946
|
-
await
|
|
2249
|
+
await custodyPush(eng, ws, ledger, { remoteRef: "refs/heads/main" });
|
|
1947
2250
|
} catch (e) {
|
|
1948
2251
|
return { ok: false, error: String(e).slice(0, 200), head };
|
|
1949
2252
|
}
|
|
1950
2253
|
for (const { ref } of consumed) {
|
|
1951
|
-
try { await
|
|
2254
|
+
try { await custodyPush(eng, ws, ledger, { remoteRef: ref, del: true }); } catch {}
|
|
1952
2255
|
}
|
|
1953
2256
|
return { ok: true, head };
|
|
1954
2257
|
}
|
|
@@ -1960,7 +2263,7 @@ export async function backupToCustody(eng, ws, ledger, { consumed = [] } = {}) {
|
|
|
1960
2263
|
* genesis (contiguity → per-intent re-admission with the engine re-run; byte-preserving,
|
|
1961
2264
|
* read-only — see wasm_git_holon.rs `verify_chain`). Returns the wasm's verdict object:
|
|
1962
2265
|
* `{valid:true, head, intents, plansRerun, controllerHash, installedDomains}` or
|
|
1963
|
-
* `{valid:false, check, error, atIndex?, atCommit?}`. The
|
|
2266
|
+
* `{valid:false, check, error, atIndex?, atCommit?}`. The ADAPTER decides what a refusal
|
|
1964
2267
|
* does (archive + refuse to serve); this op never mutates the ledger.
|
|
1965
2268
|
*/
|
|
1966
2269
|
export async function verifyChain(eng, ws, ledger) {
|
|
@@ -1970,10 +2273,12 @@ export async function verifyChain(eng, ws, ledger) {
|
|
|
1970
2273
|
return { valid: false, check: "mount", error: m.restoreError || "no refs/heads/main on the ledger — nothing to verify (push refs/heads/main to deliver a chain first)" };
|
|
1971
2274
|
}
|
|
1972
2275
|
try {
|
|
1973
|
-
|
|
2276
|
+
// P4: chain verification rides the `query` verb (op:"verifyChain") — a read-only verdict, not a
|
|
2277
|
+
// top-level adapter verb. The adapter still relays the verdict (archive-on-refuse, bless-on-valid) = custody.
|
|
2278
|
+
return JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "verifyChain" }), branch: BRANCH }, eng.STDERR));
|
|
1974
2279
|
} finally {
|
|
1975
2280
|
// The verified (or refused) chain must never linger as a servable mount: serving
|
|
1976
|
-
// re-restores from custody AFTER the
|
|
2281
|
+
// re-restores from custody AFTER the adapter blesses the verdict.
|
|
1977
2282
|
unmount(eng, ws);
|
|
1978
2283
|
}
|
|
1979
2284
|
}
|
|
@@ -1986,7 +2291,8 @@ export async function verifyChain(eng, ws, ledger) {
|
|
|
1986
2291
|
* the LEDGER's bytes, never a hot mount.)
|
|
1987
2292
|
*/
|
|
1988
2293
|
export function verifyChainLocal(eng, ws) {
|
|
1989
|
-
|
|
2294
|
+
// P4: rides the `query` verb (op:"verifyChain") — a read-only verdict.
|
|
2295
|
+
return JSON.parse(call(eng.ex, "query", { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "verifyChain" }), branch: BRANCH }, eng.STDERR));
|
|
1990
2296
|
}
|
|
1991
2297
|
|
|
1992
2298
|
/**
|
|
@@ -2002,14 +2308,169 @@ export async function archiveRefusedMain(eng, ws, ledger, label) {
|
|
|
2002
2308
|
if (!m.restored) { unmount(eng, ws); return { archived: false, error: "no main to archive" }; }
|
|
2003
2309
|
const gitdir = gitdirOf(ws);
|
|
2004
2310
|
try {
|
|
2005
|
-
|
|
2006
|
-
await
|
|
2311
|
+
// main's objects are already in custody → a pure pointer (empty pack) to refused/<label>, then drop main.
|
|
2312
|
+
await custodyPush(eng, ws, ledger, { localRef: "refs/heads/main", remoteRef: `refs/heads/refused/${label}`, pointer: true });
|
|
2313
|
+
await custodyPush(eng, ws, ledger, { remoteRef: "refs/heads/main", del: true });
|
|
2007
2314
|
return { archived: true, ref: `refs/heads/refused/${label}` };
|
|
2008
2315
|
} finally {
|
|
2009
2316
|
unmount(eng, ws);
|
|
2010
2317
|
}
|
|
2011
2318
|
}
|
|
2012
2319
|
|
|
2320
|
+
/** A refused-offer id sanitized to a git ref path component (the offerId is the intent id). */
|
|
2321
|
+
const refusedRefName = (offerId) => `refs/heads/refused/${String(offerId || "unknown").replace(/[^A-Za-z0-9._-]/g, "_")}`;
|
|
2322
|
+
|
|
2323
|
+
/**
|
|
2324
|
+
* OFFER-KERNEL P1.2b: materialize a REFUSED offer as a HOLON FACT in git custody — a commit on
|
|
2325
|
+
* `refs/heads/refused/<offerId>` carrying { intent.json, verdict.json } (the holon's verdict, NOT
|
|
2326
|
+
* adapter mail). A quarantine branch is OUTSIDE the verified ledger (commit-closure doesn't bind it),
|
|
2327
|
+
* so the adapter writes it as CUSTODY — the DECISION was the holon's (`offer` → `refused`), the adapter
|
|
2328
|
+
* only records it. The ref SYNCS (a client pulls refs/heads/refused/* to see its dead-lettered
|
|
2329
|
+
* work) and survives DO eviction. Deterministic (CKPT_AUTHOR timestamp 0) ⇒ idempotent: re-refusing
|
|
2330
|
+
* the same intent rewrites the same commit. `dl` is the admitAll dead-letter
|
|
2331
|
+
* `{ intentId, oid, error, blob, domain, directiveId }`.
|
|
2332
|
+
*/
|
|
2333
|
+
export async function writeRefusedRef(eng, ws, dl) {
|
|
2334
|
+
const gitdir = gitdirOf(ws);
|
|
2335
|
+
const blob = dl.blob instanceof Uint8Array ? dl.blob : enc.encode(String(dl.blob || ""));
|
|
2336
|
+
const verdict = enc.encode(JSON.stringify({
|
|
2337
|
+
offerId: dl.intentId || null, oid: dl.oid || null,
|
|
2338
|
+
domain: dl.domain || null, directiveId: dl.directiveId || null,
|
|
2339
|
+
verdict: { reason: String(dl.error || "refused") },
|
|
2340
|
+
}));
|
|
2341
|
+
const ref = refusedRefName(dl.intentId || dl.oid);
|
|
2342
|
+
kgit.writeMetaCommit(eng, gitdir, {
|
|
2343
|
+
parents: [], files: [{ path: "intent.json", bytes: blob }, { path: "verdict.json", bytes: verdict }],
|
|
2344
|
+
message: `refused: ${dl.domain || "?"}/${dl.directiveId || "?"} — ${String(dl.error || "").slice(0, 120)}`,
|
|
2345
|
+
timeSecs: 0, ref,
|
|
2346
|
+
});
|
|
2347
|
+
return ref;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
/** Read the refused-offer facts back: refs/heads/refused/* → [{ ref, offerId, domain, directiveId,
|
|
2351
|
+
* verdict, intentB64 }]. The git-custody source for GET /dead-letters + the retry lane (P1.2b.2). */
|
|
2352
|
+
export async function listRefusedRefs(eng, ws) {
|
|
2353
|
+
const gitdir = gitdirOf(ws);
|
|
2354
|
+
const refs = kgit.listRefs(eng, gitdir, "refs/heads/refused/"); // [{ ref, oid }] — oid in hand, no resolveRef
|
|
2355
|
+
const out = [];
|
|
2356
|
+
for (const { ref, oid } of refs) {
|
|
2357
|
+
try {
|
|
2358
|
+
const vb = kgit.readBlob(eng, gitdir, oid, "verdict.json");
|
|
2359
|
+
const ib = kgit.readBlob(eng, gitdir, oid, "intent.json");
|
|
2360
|
+
const v = JSON.parse(dec.decode(vb));
|
|
2361
|
+
out.push({ ref, ...v, intentB64: b64Bytes(ib) });
|
|
2362
|
+
} catch { /* a partial/corrupt refused ref is skipped (the reader is best-effort) */ }
|
|
2363
|
+
}
|
|
2364
|
+
return out;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
/**
|
|
2368
|
+
* OFFER-KERNEL P1.2b: turn an admit cycle's refusals into git-custody facts. Each dead-letter is
|
|
2369
|
+
* written as `refs/heads/refused/<offerId>` ({intent.json, verdict.json}) and PUSHED to custody so
|
|
2370
|
+
* it SYNCS to clients + survives DO eviction — replacing the adapter-mail DLQ (`ctx.storage`). Called
|
|
2371
|
+
* inline from the admit lanes: refusals are EXCEPTIONAL (off the admit hot path), so the inline push
|
|
2372
|
+
* is cheap and makes the fact durable even on the deferred-seal lane (where main's push is debounced).
|
|
2373
|
+
* Best-effort per ref (a failed write/push is logged, never aborts the cycle — the session branch is
|
|
2374
|
+
* still durable, so the refusal re-materializes next cycle, idempotently). Returns the pushed refs.
|
|
2375
|
+
*/
|
|
2376
|
+
export async function materializeRefused(eng, ws, ledger, deadLetters) {
|
|
2377
|
+
const gitdir = gitdirOf(ws);
|
|
2378
|
+
const refs = [];
|
|
2379
|
+
for (const dl of deadLetters || []) {
|
|
2380
|
+
try {
|
|
2381
|
+
const ref = await writeRefusedRef(eng, ws, dl);
|
|
2382
|
+
await custodyPush(eng, ws, ledger, { remoteRef: ref }); // new refused commit → ships its objects
|
|
2383
|
+
refs.push(ref);
|
|
2384
|
+
} catch (e) {
|
|
2385
|
+
log("refused-ref-materialize-failed", { ws, intentId: dl?.intentId, error: String(e).slice(0, 200) });
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
return refs;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
/** Drop a refused-offer fact (retry succeeded, or an explicit discard): delete the local ref + push
|
|
2392
|
+
* the delete to custody so the quarantine branch is gone everywhere. Best-effort. */
|
|
2393
|
+
export async function deleteRefusedRef(eng, ws, ledger, offerId) {
|
|
2394
|
+
const gitdir = gitdirOf(ws);
|
|
2395
|
+
const ref = refusedRefName(offerId);
|
|
2396
|
+
try { kgit.deleteRef(eng, gitdir, ref); } catch {}
|
|
2397
|
+
try { await custodyPush(eng, ws, ledger, { remoteRef: ref, del: true }); }
|
|
2398
|
+
catch (e) { log("refused-ref-delete-push-failed", { ws, ref, error: String(e).slice(0, 200) }); }
|
|
2399
|
+
return ref;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// ── THE OUTBOX (capability_offers_https.md — offer-kernel P6, slice 1) ─────────────────────
|
|
2403
|
+
// A capability is a holon at a URL; an obligation is offered to it as an HTTPS POST. The kernel
|
|
2404
|
+
// already surfaces the orders (reactorScan = obligations at status:'requested'); the capability's
|
|
2405
|
+
// endpoint rides the law (declaredCapabilities). The BAILIFF joins them and POSTs — pure transport,
|
|
2406
|
+
// it decides nothing (the holon decided by admitting the obligation; the law named the endpoint).
|
|
2407
|
+
// A delivered order is recorded as refs/heads/outbox/<requestId> (the delivery marker + custody;
|
|
2408
|
+
// idempotent — a delivered order is never re-POSTed). The receipt lane (slice 2) closes the
|
|
2409
|
+
// obligation; the credential lane (slice 3) forwards the caller's packet.
|
|
2410
|
+
|
|
2411
|
+
/** A requestId sanitized to a git ref path component. */
|
|
2412
|
+
const outboxRefName = (requestId) => `refs/heads/outbox/${String(requestId || "unknown").replace(/[^A-Za-z0-9._-]/g, "_")}`;
|
|
2413
|
+
|
|
2414
|
+
/** Has this order already been delivered? (the outbox ref is the delivered marker — idempotency). */
|
|
2415
|
+
export async function outboxRefExists(eng, ws, requestId) {
|
|
2416
|
+
return !!kgit.resolveRef(eng, gitdirOf(ws), outboxRefName(requestId));
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/** Record a delivered order as a git-custody fact: a commit on refs/heads/outbox/<requestId>
|
|
2420
|
+
* carrying { order.json, delivery.json }. Deterministic CKPT_AUTHOR ⇒ idempotent. */
|
|
2421
|
+
export async function writeOutboxRef(eng, ws, rec) {
|
|
2422
|
+
const gitdir = gitdirOf(ws);
|
|
2423
|
+
const orderBlob = enc.encode(JSON.stringify(rec.order ?? {}));
|
|
2424
|
+
const delivery = enc.encode(JSON.stringify({
|
|
2425
|
+
requestId: rec.requestId || null, capability: rec.capability || null, url: rec.url || null,
|
|
2426
|
+
status: rec.status || "delivered", httpStatus: rec.httpStatus ?? null,
|
|
2427
|
+
}));
|
|
2428
|
+
const ref = outboxRefName(rec.requestId);
|
|
2429
|
+
kgit.writeMetaCommit(eng, gitdir, {
|
|
2430
|
+
parents: [], files: [{ path: "order.json", bytes: orderBlob }, { path: "delivery.json", bytes: delivery }],
|
|
2431
|
+
message: `outbox: ${rec.capability || "?"} → ${String(rec.url || "?").slice(0, 80)}`,
|
|
2432
|
+
timeSecs: 0, ref,
|
|
2433
|
+
});
|
|
2434
|
+
return ref;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
/**
|
|
2438
|
+
* THE BAILIFF OUTBOX LOOP: scan the kernel's requested orders, join each to its declared
|
|
2439
|
+
* capability endpoint, and POST the obligation as an HTTPS offer. Best-effort + idempotent
|
|
2440
|
+
* (a delivered order has an outbox ref and is skipped). Runs on the engine plane (DO edge OR
|
|
2441
|
+
* heavy container — both have `fetch` + the ledger). Returns `{delivered:[requestId], skipped, errors}`.
|
|
2442
|
+
* The adapter decides nothing: the obligation (a holon admission) is the emit decision; the endpoint
|
|
2443
|
+
* is law. NO receipt handling yet (slice 2) — POSTing is delivery, the obligation stays 'requested'.
|
|
2444
|
+
*/
|
|
2445
|
+
export async function emitOutbox(eng, ws, ledger) {
|
|
2446
|
+
const orders = (() => { try { return reactorScan(eng, ws); } catch { return []; } })();
|
|
2447
|
+
if (!Array.isArray(orders) || orders.length === 0) return { delivered: [], skipped: 0, errors: 0 };
|
|
2448
|
+
const byType = new Map();
|
|
2449
|
+
for (const c of declaredCapabilities(eng, ws)) if (c && c.aggregateId) byType.set(c.aggregateId, c);
|
|
2450
|
+
const gitdir = gitdirOf(ws);
|
|
2451
|
+
const delivered = []; let skipped = 0, errors = 0;
|
|
2452
|
+
for (const order of orders) {
|
|
2453
|
+
const cap = byType.get(order.taskType);
|
|
2454
|
+
if (!cap || !cap.endpoint) { skipped++; continue; } // no destination — rests at 'requested'
|
|
2455
|
+
if (await outboxRefExists(eng, ws, order.requestId)) { skipped++; continue; } // already delivered
|
|
2456
|
+
try {
|
|
2457
|
+
const res = await fetch(cap.endpoint, {
|
|
2458
|
+
method: "POST",
|
|
2459
|
+
headers: { "content-type": "application/json", "x-nomos-capability": cap.capability, "x-nomos-workspace": ws },
|
|
2460
|
+
body: JSON.stringify({ capability: cap.capability, requestId: order.requestId, taskType: order.taskType, params: order.params, scheduledAt: order.scheduledAt, deadlineAt: order.deadlineAt }),
|
|
2461
|
+
signal: AbortSignal.timeout(30000),
|
|
2462
|
+
});
|
|
2463
|
+
if (!res.ok) { errors++; log("outbox-deliver-non2xx", { ws, requestId: order.requestId, url: cap.endpoint, status: res.status }); continue; }
|
|
2464
|
+
const ref = await writeOutboxRef(eng, ws, { requestId: order.requestId, capability: cap.capability, url: cap.endpoint, order, status: "delivered", httpStatus: res.status });
|
|
2465
|
+
try { await custodyPush(eng, ws, ledger, { remoteRef: ref }); }
|
|
2466
|
+
catch (e) { log("outbox-ref-push-failed", { ws, ref, error: String(e).slice(0, 200) }); }
|
|
2467
|
+
delivered.push(order.requestId);
|
|
2468
|
+
} catch (e) { errors++; log("outbox-deliver-failed", { ws, requestId: order.requestId, url: cap.endpoint, error: String(e).slice(0, 200) }); }
|
|
2469
|
+
}
|
|
2470
|
+
if (delivered.length || errors) log("outbox", { ws, delivered: delivered.length, skipped, errors });
|
|
2471
|
+
return { delivered, skipped, errors };
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2013
2474
|
/** Genesis boot: bootstrap → the nomos controller; idempotent when already Active. */
|
|
2014
2475
|
export async function boot(eng, ws, ledger) {
|
|
2015
2476
|
const m = await mountWorkspace(eng, ws, ledger);
|
|
@@ -2021,14 +2482,14 @@ export async function boot(eng, ws, ledger) {
|
|
|
2021
2482
|
return { status: "created", head: result.head, controllerHash: eng.hashes.nomos, rows: qById(eng, ws, `domain-installation:${eng.hashes.nomos}`), mount: m };
|
|
2022
2483
|
}
|
|
2023
2484
|
|
|
2024
|
-
/** Install a tenant package under the nomos controller (validation is the
|
|
2485
|
+
/** Install a tenant package under the nomos controller (validation is the ADAPTER's job). */
|
|
2025
2486
|
export async function deploy(eng, ws, ledger, usda, domainHash, dispositions) {
|
|
2026
2487
|
// THE EVOLVE-GATE ANSWER LANE (#58): `dispositions` ({retired, retyped, rebinds,
|
|
2027
2488
|
// ackBy}) ride the committed installDomain payload — IN-HISTORY facts the one gate
|
|
2028
2489
|
// adjudicates the stable-id diff against. Absent for the common (non-destructive)
|
|
2029
2490
|
// upgrade; the gate's typed refusal names the remedy when one is needed.
|
|
2030
2491
|
const result = author(eng, ws, "nomos", "installDomain", installPayload(domainHash, usda, eng.installedBy, dispositions), eng.hashes.nomos);
|
|
2031
|
-
// Shard identity used to be inferred from
|
|
2492
|
+
// Shard identity used to be inferred from adapter-side taxonomy. That is retired:
|
|
2032
2493
|
// if sharding needs declaration, the holon must derive and author it itself.
|
|
2033
2494
|
await commitAndPush(eng, ws, ledger);
|
|
2034
2495
|
return { head: result.head, installation: qById(eng, ws, `domain-installation:${domainHash}`) };
|
|
@@ -2048,18 +2509,15 @@ export function stats(eng) {
|
|
|
2048
2509
|
|
|
2049
2510
|
export async function ledgerStats(eng, ws, ledger = null) {
|
|
2050
2511
|
const gitdir = gitdirOf(ws);
|
|
2051
|
-
let head =
|
|
2512
|
+
let head = kgit.resolveRef(eng, gitdir, BRANCH);
|
|
2052
2513
|
if (!head && ledger?.remote) {
|
|
2053
2514
|
try {
|
|
2054
|
-
|
|
2055
|
-
|
|
2515
|
+
// Cold: build the skeleton + pull main into the KERNEL's odb (custodyFetch → apply_pack), then resolve.
|
|
2516
|
+
await custodySkeleton(eng, gitdir);
|
|
2517
|
+
const remoteMain = await custodyFetch(eng, ws, ledger, { remoteRef: "refs/heads/main" });
|
|
2056
2518
|
if (!remoteMain) return { head: null, commits: 0 };
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
await git.fetch({ fs: eng.fs, http, gitdir, remote: "origin", ref: BRANCH, singleBranch: true, headers: ledger.headers });
|
|
2060
|
-
await git.writeRef({ fs: eng.fs, gitdir, ref: `refs/heads/${BRANCH}`, value: remoteMain, force: true });
|
|
2061
|
-
await git.writeRef({ fs: eng.fs, gitdir, ref: "HEAD", value: `ref: refs/heads/${BRANCH}`, force: true, symbolic: true });
|
|
2062
|
-
head = await git.resolveRef({ fs: eng.fs, gitdir, ref: BRANCH }).catch(() => null);
|
|
2519
|
+
kgit.writeRef(eng, gitdir, "HEAD", `ref: refs/heads/${BRANCH}`);
|
|
2520
|
+
head = kgit.resolveRef(eng, gitdir, BRANCH);
|
|
2063
2521
|
const mounted = eng.mounted.get(ws);
|
|
2064
2522
|
if (mounted) {
|
|
2065
2523
|
mounted.restored = true;
|
|
@@ -2071,32 +2529,32 @@ export async function ledgerStats(eng, ws, ledger = null) {
|
|
|
2071
2529
|
if (!head) return { head: null, commits: 0 };
|
|
2072
2530
|
const mounted = eng.mounted.get(ws);
|
|
2073
2531
|
if (mounted?.ledgerStats?.head === head) return mounted.ledgerStats;
|
|
2074
|
-
|
|
2532
|
+
// The chain is a linear sequence of intent commits, so intent-commit count == commit count.
|
|
2533
|
+
const commits = kgit.intentsAbove(eng, gitdir, head).length;
|
|
2075
2534
|
const out = { head, commits };
|
|
2076
2535
|
if (mounted) mounted.ledgerStats = out;
|
|
2077
2536
|
return out;
|
|
2078
2537
|
}
|
|
2079
2538
|
|
|
2080
|
-
export async function countRemoteCommits(ledger) {
|
|
2081
|
-
const
|
|
2082
|
-
const head = (info.refs && info.refs.heads && info.refs.heads[BRANCH]) || null;
|
|
2539
|
+
export async function countRemoteCommits(eng, ledger) {
|
|
2540
|
+
const head = (await custodyLsRefs(ledger, "refs/heads/main"))["refs/heads/main"] || null;
|
|
2083
2541
|
if (!head) return { head: null, commits: 0 };
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2542
|
+
// Count without mounting the workspace: pull main into a SCRATCH dir under the KERNEL's own odb (the adapter
|
|
2543
|
+
// has no repo — the kernel ingests the pack via apply_pack + walks it), then tear the scratch dir down.
|
|
2544
|
+
const scratch = "__ledgercount__";
|
|
2545
|
+
try {
|
|
2546
|
+
stageWorkspaceDir(eng, scratch);
|
|
2547
|
+
await custodySkeleton(eng, gitdirOf(scratch));
|
|
2548
|
+
await custodyFetch(eng, scratch, ledger, { remoteRef: "refs/heads/main" });
|
|
2549
|
+
const commits = kgit.intentsAbove(eng, gitdirOf(scratch), head).length;
|
|
2550
|
+
return { head, commits };
|
|
2551
|
+
} finally {
|
|
2552
|
+
try { eng.preopen.dir.contents.get("ws").contents.delete(scratch); } catch {}
|
|
2553
|
+
}
|
|
2096
2554
|
}
|
|
2097
2555
|
|
|
2098
2556
|
/** Per-branch projection observables (cursor, projectCalls, sqliteBytes — see #37). */
|
|
2099
|
-
export const projectionStats = (eng, ws) => JSON.parse(call(eng.ex, "
|
|
2557
|
+
export const projectionStats = (eng, ws) => JSON.parse(call(eng.ex, "query", ws ? { repoArg: repoArgOf(ws), workspace: ws, queryBytes: b64Json({ op: "projectionStats" }), branch: BRANCH } : { queryBytes: b64Json({ op: "projectionStats" }) }, eng.STDERR));
|
|
2100
2558
|
|
|
2101
2559
|
/**
|
|
2102
2560
|
* THE POST-ACK WARM LANE (#34): replenish the wasm engine's pristine-sandbox pool
|
|
@@ -2108,5 +2566,7 @@ export const projectionStats = (eng, ws) => JSON.parse(call(eng.ex, "projection_
|
|
|
2108
2566
|
*/
|
|
2109
2567
|
export function warmEngine(eng) {
|
|
2110
2568
|
flushDeferredProjections(eng); // the deferred-ack projection flush (#47) rides the same lane
|
|
2111
|
-
|
|
2569
|
+
// P3: the pristine-pool warm rides the `query` verb (op:"warm") — a non-effectful maintenance read,
|
|
2570
|
+
// not a top-level adapter verb. Still post-ack (the adapter calls warmEngine off the write critical path).
|
|
2571
|
+
return JSON.parse(call(eng.ex, "query", { repoArg: "/work", workspace: "root", queryBytes: b64Json({ op: "warm" }), branch: BRANCH }, eng.STDERR));
|
|
2112
2572
|
}
|