githolon 0.85.0 → 0.86.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.
Files changed (2) hide show
  1. package/dist/dev-server.mjs +279 -1
  2. package/package.json +3 -3
@@ -594,6 +594,126 @@ function summaryOfCapture(ctx, fromOid, toOid) {
594
594
  rowsByHome: Object.fromEntries([...ctx.rowsByHome.entries()].sort((a, b) => a[0] < b[0] ? -1 : 1))
595
595
  };
596
596
  }
597
+ async function admitIntentOffers(eng, ws, ledger, offers, { receiptFence = null, shardRouting = null, clientBase = null, defer = false } = {}) {
598
+ const main = await mainIntentIds(eng, ws);
599
+ const sessions = /* @__PURE__ */ new Map();
600
+ const deadLetters = [];
601
+ let admittedTotal = 0;
602
+ const born = [];
603
+ const birthOutcomes = [];
604
+ const timings = { spans: [], intents: 0, sessions: 0 };
605
+ const summaryCtx = shardRouting && shardRouting.selfLabel && scopedReads(eng).length > 0 ? { reads: scopedReads(eng), prevs: /* @__PURE__ */ new Map(), values: /* @__PURE__ */ new Map(), events: [], rowsDelta: 0, rederived: /* @__PURE__ */ new Set(), rowsByHome: /* @__PURE__ */ new Map(), homeTags: /* @__PURE__ */ new Map(), homeKeySet: /* @__PURE__ */ new Set() } : null;
606
+ if (summaryCtx) {
607
+ try {
608
+ for (const r of query(eng, ws, "aggregatesByType", JSON.stringify({ __type: "NomosHomeReceipt" }))) {
609
+ const hk = r.data && typeof r.data.homeKey === "string" ? r.data.homeKey : null;
610
+ if (!hk) continue;
611
+ summaryCtx.homeKeySet.add(hk);
612
+ summaryCtx.homeTags.set(await routeTagHexOfHomeKey(hk), hk);
613
+ }
614
+ } catch (e) {
615
+ log("home-tag-table-error", { ws, error: String(e).slice(0, 200) });
616
+ }
617
+ }
618
+ const gitdir0 = gitdirOf(ws);
619
+ const headBefore = summaryCtx ? kgit.resolveRef(eng, gitdir0, BRANCH) : null;
620
+ const t0 = Date.now();
621
+ let seq = 0;
622
+ const span = (name, kind, depth = 1, extra = {}) => {
623
+ const s0 = Date.now();
624
+ return (end = {}) => timings.spans.push({ id: `intent-offer-${t0}-${seq++}`, track: "cloud", name, kind, depth, t0: s0, t1: Date.now(), ...extra, ...end });
625
+ };
626
+ for (const offer of Array.isArray(offers) ? offers : []) {
627
+ const cid = typeof offer?.session === "string" && offer.session ? offer.session : "direct";
628
+ const rec = sessions.get(cid) || { session: cid, admitted: [], rejected: [], skipped: [] };
629
+ sessions.set(cid, rec);
630
+ const done = span(`intent offer ${cid.slice(0, 14)}`, "cadmit", 1, { client: cid });
631
+ let blob = offer?.blob instanceof Uint8Array ? offer.blob : null;
632
+ try {
633
+ if (!blob && typeof offer?.intentB64 === "string") blob = Uint8Array.from(atob(offer.intentB64), (c) => c.charCodeAt(0));
634
+ const maxOfferBytes = offer?.staged ? 64 * 1024 * 1024 : 8 * 1024 * 1024;
635
+ if (!blob || blob.byteLength === 0 || blob.byteLength > maxOfferBytes) {
636
+ rec.rejected.push({ oid: String(offer?.oid || "").slice(0, 10), error: `intent offer must carry intentB64 <= ${maxOfferBytes / (1024 * 1024)} MiB` });
637
+ continue;
638
+ }
639
+ let doc = null;
640
+ try {
641
+ doc = JSON.parse(dec3.decode(blob));
642
+ } catch {
643
+ }
644
+ const intentId = typeof doc?.id === "string" ? doc.id : null;
645
+ const dom = typeof doc?.payload?.domain === "string" ? doc.payload.domain : null;
646
+ const dirId = typeof doc?.payload?.directiveId === "string" ? doc.payload.directiveId : null;
647
+ const oid = String(offer?.oid || intentId || "").slice(0, 10);
648
+ if (!dom) {
649
+ rec.rejected.push({ oid, error: "non-authored intent \u2014 governance enters its own admin lane" });
650
+ continue;
651
+ }
652
+ if (receiptFence && dirId && Array.isArray(receiptFence[dom]) && receiptFence[dom].includes(dirId)) {
653
+ const error = `'${dom}/${dirId}' is a capability RECEIPT \u2014 receipts are executor/owner writes, never open-session writes. Author it via POST /v1/workspaces/${ws}/author (owner Bearer), or \u2014 if this parked receipt is sanctioned \u2014 the owner unjams it: POST /v1/workspaces/${ws}/dead-letters/retry`;
654
+ rec.rejected.push({ oid, error, deadLettered: true });
655
+ deadLetters.push({ intentId, oid, session: cid, error, blob, domain: dom, directiveId: dirId });
656
+ continue;
657
+ }
658
+ if (intentId && main.ids.has(intentId)) {
659
+ rec.skipped.push({ oid, intent: intentId.slice(0, 12), alreadyOnMain: true });
660
+ continue;
661
+ }
662
+ const cap = summaryCtx ? beginSummaryCapture(eng, ws, summaryCtx, blob) : null;
663
+ const _tOffer = Date.now();
664
+ const res = offerAdmit(eng, ws, blob, { defer: !summaryCtx });
665
+ log("offer-admit-ms", { ws, ms: Date.now() - _tOffer, deferred: !summaryCtx, ok: res.ok });
666
+ if (res.ok) {
667
+ if (intentId) main.ids.add(intentId);
668
+ main.oids.add(res.head);
669
+ main.head = res.head;
670
+ rec.admitted.push({ oid, head: res.head });
671
+ admittedTotal += 1;
672
+ if (Array.isArray(res.born) && res.born.length) born.push(...res.born);
673
+ if (Array.isArray(res.birthOutcomes) && res.birthOutcomes.length) birthOutcomes.push(...res.birthOutcomes);
674
+ if (cap) finishSummaryCapture(eng, ws, summaryCtx, cap, res.head, intentId);
675
+ } else {
676
+ const error = `${String(res.error || "").slice(0, 300)} \u2014 dead-lettered; fix the law (or the payload), then POST /v1/workspaces/${ws}/dead-letters/retry`;
677
+ rec.rejected.push({ oid, error, deadLettered: true });
678
+ deadLetters.push({ intentId, oid, session: cid, error, blob, domain: dom, directiveId: dirId });
679
+ }
680
+ } catch (e) {
681
+ rec.rejected.push({ oid: String(offer?.oid || "").slice(0, 10), error: String(e).slice(0, 200) });
682
+ } finally {
683
+ done();
684
+ }
685
+ }
686
+ timings.sessions = sessions.size;
687
+ timings.intents = admittedTotal;
688
+ if (admittedTotal && !summaryCtx) (eng.pendingProjection ??= /* @__PURE__ */ new Set()).add(ws);
689
+ if (deadLetters.length) {
690
+ try {
691
+ await materializeRefused(eng, ws, ledger, deadLetters);
692
+ } catch (e) {
693
+ log("refused-materialize-failed", { ws, error: String(e).slice(0, 200) });
694
+ }
695
+ }
696
+ const deferDurability = defer && !summaryCtx;
697
+ if (admittedTotal && !deferDurability) {
698
+ const sealDone = span("seal offered intents", "cseal", 1, { admitted: admittedTotal });
699
+ await commitAndPush(eng, ws, ledger);
700
+ sealDone();
701
+ }
702
+ const gitdir = gitdirOf(ws);
703
+ const head = kgit.resolveRef(eng, gitdir, BRANCH) ?? main.head;
704
+ const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore, head) : null;
705
+ let resultPackB64 = null;
706
+ if (admittedTotal && head && /^[0-9a-f]{40}$/i.test(clientBase || "") && clientBase !== head) {
707
+ try {
708
+ const p = servePack(eng, ws, clientBase);
709
+ if (p && p.packB64 && p.bytes && p.bytes <= RESULT_PACK_INLINE_MAX) resultPackB64 = p.packB64;
710
+ } catch {
711
+ }
712
+ }
713
+ log("admit-intent-offers", { ws, sessions: sessions.size, admitted: admittedTotal, rejected: [...sessions.values()].reduce((n, s) => n + s.rejected.length, 0), resultPack: resultPackB64 ? "inline" : "none" });
714
+ const bornPacked = born.length ? packBornChildren(eng, born) : born;
715
+ return { sessions: [...sessions.values()].map((s) => ({ ...s, ...s.skipped.length ? {} : { skipped: void 0 } })), main: head, deadLetters, timings, ...summary ? { summary } : {}, ...bornPacked.length ? { born: bornPacked } : {}, ...birthOutcomes.length ? { birthOutcomes } : {}, ...resultPackB64 ? { resultPackB64, resultBase: clientBase } : {}, ...deferDurability && admittedTotal && head ? { deferred: true, head } : {} };
716
+ }
597
717
  async function authorOn(eng, ws, ledger, domain, directiveId, payload, lawHash, opts = {}) {
598
718
  await mountWorkspace(eng, ws, ledger);
599
719
  let res;
@@ -1176,6 +1296,35 @@ async function writeRefusedRef(eng, ws, dl) {
1176
1296
  });
1177
1297
  return ref;
1178
1298
  }
1299
+ async function listRefusedRefs(eng, ws) {
1300
+ const gitdir = gitdirOf(ws);
1301
+ const refs = kgit.listRefs(eng, gitdir, "refs/heads/refused/");
1302
+ const out = [];
1303
+ for (const { ref, oid } of refs) {
1304
+ try {
1305
+ const vb = kgit.readBlob(eng, gitdir, oid, "verdict.json");
1306
+ const ib = kgit.readBlob(eng, gitdir, oid, "intent.json");
1307
+ const v = JSON.parse(dec3.decode(vb));
1308
+ out.push({ ref, ...v, intentB64: b64Bytes(ib) });
1309
+ } catch {
1310
+ }
1311
+ }
1312
+ return out;
1313
+ }
1314
+ async function refusedSummary(eng, ws) {
1315
+ const gitdir = gitdirOf(ws);
1316
+ const refs = kgit.listRefs(eng, gitdir, "refs/heads/refused/");
1317
+ let count2 = 0, latestParkedAt = null;
1318
+ for (const { oid } of refs) {
1319
+ try {
1320
+ const v = JSON.parse(dec3.decode(kgit.readBlob(eng, gitdir, oid, "verdict.json")));
1321
+ count2++;
1322
+ if (v && typeof v.parkedAt === "string" && (!latestParkedAt || v.parkedAt > latestParkedAt)) latestParkedAt = v.parkedAt;
1323
+ } catch {
1324
+ }
1325
+ }
1326
+ return { count: count2, latestParkedAt };
1327
+ }
1179
1328
  async function materializeRefused(eng, ws, ledger, deadLetters) {
1180
1329
  const refs = [];
1181
1330
  const unparkedSessions = /* @__PURE__ */ new Set();
@@ -1191,6 +1340,20 @@ async function materializeRefused(eng, ws, ledger, deadLetters) {
1191
1340
  }
1192
1341
  return { refs, unparkedSessions };
1193
1342
  }
1343
+ async function deleteRefusedRef(eng, ws, ledger, offerId) {
1344
+ const gitdir = gitdirOf(ws);
1345
+ const ref = refusedRefName(offerId);
1346
+ try {
1347
+ kgit.deleteRef(eng, gitdir, ref);
1348
+ } catch {
1349
+ }
1350
+ try {
1351
+ await custodyPush(eng, ws, ledger, { remoteRef: ref, del: true });
1352
+ } catch (e) {
1353
+ log("refused-ref-delete-push-failed", { ws, ref, error: String(e).slice(0, 200) });
1354
+ }
1355
+ return ref;
1356
+ }
1194
1357
  async function boot(eng, ws, ledger) {
1195
1358
  const m = await mountWorkspace(eng, ws, ledger);
1196
1359
  if (nomosActive(eng, ws)) {
@@ -1588,6 +1751,34 @@ function receivePackCommands(raw) {
1588
1751
  // ../cloud/dev-server/server.mjs
1589
1752
  var DIR = path.dirname(fileURLToPath(import.meta.url));
1590
1753
  var REPO_ROOT = path.resolve(DIR, "../..");
1754
+ var RECOGNIZED_UNSUPPORTED = /* @__PURE__ */ new Set([
1755
+ "container-status",
1756
+ "recycle-container",
1757
+ "hostnames",
1758
+ "rebirth-custody",
1759
+ "upload-begin",
1760
+ "upload-gate",
1761
+ "upload-verify",
1762
+ "drain-reclaims",
1763
+ "drain-rebirths",
1764
+ "git-read-lease",
1765
+ "poke-admit",
1766
+ "clear-checkpoint",
1767
+ "domain-package",
1768
+ "capabilities"
1769
+ ]);
1770
+ var UNSUPPORTED_WHY = {
1771
+ "container-status": "the heavy container tier is Cloudflare-only (the dev cloud is one in-process engine)",
1772
+ "recycle-container": "the heavy container tier is Cloudflare-only (the dev cloud is one in-process engine)",
1773
+ "hostnames": "platform custom-hostname routing is a multi-tenant edge concern",
1774
+ "rebirth-custody": "the custodian rebirth lane is bound to Cloudflare Artifacts custody",
1775
+ "upload-begin": "the staged-upload birth variant \u2014 push to /v2/workspaces/<ws>/git instead (the upload-birth lane works locally)",
1776
+ "upload-gate": "the staged-upload birth variant \u2014 push to /v2/workspaces/<ws>/git instead",
1777
+ "upload-verify": "the staged-upload birth variant \u2014 push to /v2/workspaces/<ws>/git instead",
1778
+ "drain-reclaims": "an operator lawful-recovery drain (multi-node lifecycle)",
1779
+ "drain-rebirths": "an operator lawful-recovery drain (multi-node lifecycle)",
1780
+ "capabilities": "retired everywhere \u2014 host-side capability storage is gone (capabilities are holon-offered obligations now)"
1781
+ };
1591
1782
  var _realFetch = globalThis.fetch;
1592
1783
  globalThis.fetch = (input, init) => {
1593
1784
  const u = typeof input === "string" ? input : input && input.url || "";
@@ -1903,6 +2094,83 @@ async function startDevCloud(opts = {}) {
1903
2094
  sendJson(res, { ok: true, ws, sessions: out.sessions, main: out.main, ...out.timings ? { timings: out.timings } : {} });
1904
2095
  });
1905
2096
  }
2097
+ if (sub[0] === "intent-offers" && req.method === "POST") {
2098
+ return serialize(async () => {
2099
+ const ledger = ledgerFor(ws);
2100
+ await mountWorkspace(eng, ws, ledger);
2101
+ const body = JSON.parse((await readBody(req)).toString("utf8") || "{}");
2102
+ const offers = Array.isArray(body.offers) ? body.offers.slice(0, 100) : [];
2103
+ if (!offers.length) return sendJson(res, { ok: false, error: "body must be { offers:[{session, oid, intentB64}] }" }, 400);
2104
+ const clientBase = /^[0-9a-f]{40}$/i.test(body.base || "") ? body.base : null;
2105
+ const out = await admitIntentOffers(eng, ws, ledger, offers, { ...clientBase ? { clientBase } : {} });
2106
+ if (Array.isArray(out.born) && out.born.length) await persistBorn(out.born);
2107
+ const bornOut = Array.isArray(out.born) ? out.born.map(({ packB64, packBytes, ...b }) => b) : null;
2108
+ sendJson(res, {
2109
+ ok: true,
2110
+ ws,
2111
+ sessions: out.sessions,
2112
+ main: out.main,
2113
+ ...bornOut ? { born: bornOut } : {},
2114
+ ...out.birthOutcomes ? { birthOutcomes: out.birthOutcomes } : {},
2115
+ ...out.mainDelta ? { mainDelta: out.mainDelta } : {},
2116
+ ...out.resultPackB64 ? { resultPackB64: out.resultPackB64, resultBase: out.resultBase } : {},
2117
+ ...out.deadLetters && out.deadLetters.length ? { deadLetters: out.deadLetters.map(({ blob, ...m }) => m) } : {}
2118
+ });
2119
+ });
2120
+ }
2121
+ if (sub[0] === "dead-letters" && sub.length === 1 && req.method === "GET") {
2122
+ return serialize(async () => {
2123
+ await mountWorkspace(eng, ws, ledgerFor(ws));
2124
+ sendJson(res, { ok: true, ws, deadLetters: await listRefusedRefs(eng, ws) });
2125
+ });
2126
+ }
2127
+ if (sub[0] === "dlq-summary" && req.method === "GET") {
2128
+ return serialize(async () => {
2129
+ await mountWorkspace(eng, ws, ledgerFor(ws));
2130
+ sendJson(res, { ok: true, ws, ...await refusedSummary(eng, ws) });
2131
+ });
2132
+ }
2133
+ if (sub[0] === "dead-letters" && sub[1] === "retry" && req.method === "POST") {
2134
+ return serialize(async () => {
2135
+ const ledger = ledgerFor(ws);
2136
+ await mountWorkspace(eng, ws, ledger);
2137
+ const body = JSON.parse((await readBody(req)).toString("utf8") || "{}");
2138
+ const only = body.id || null;
2139
+ const refs = await listRefusedRefs(eng, ws);
2140
+ const retried = [], failed = [];
2141
+ for (const e of refs) {
2142
+ if (only && e.offerId !== only && e.oid !== only) continue;
2143
+ if (!e.intentB64) {
2144
+ failed.push({ intentId: e.offerId, error: "refused ref carries no intent bytes" });
2145
+ continue;
2146
+ }
2147
+ try {
2148
+ const r2 = offerAdmit(eng, ws, unb64(e.intentB64));
2149
+ if (r2.ok) retried.push({ intentId: e.offerId, head: r2.head });
2150
+ else failed.push({ intentId: e.offerId, error: String(r2.error || "").slice(0, 300) });
2151
+ } catch (err) {
2152
+ failed.push({ intentId: e.offerId, error: String(err).slice(0, 300) });
2153
+ }
2154
+ }
2155
+ if (retried.length) await commitAndPush(eng, ws, ledger);
2156
+ for (const r2 of retried) await deleteRefusedRef(eng, ws, ledger, r2.intentId);
2157
+ sendJson(res, { ok: true, ws, retried, failed, remaining: (await listRefusedRefs(eng, ws)).length });
2158
+ });
2159
+ }
2160
+ if (sub[0] === "dead-letters" && (sub[1] === "discard" || req.method === "DELETE")) {
2161
+ return serialize(async () => {
2162
+ const ledger = ledgerFor(ws);
2163
+ await mountWorkspace(eng, ws, ledger);
2164
+ const body = req.method === "DELETE" ? {} : JSON.parse((await readBody(req)).toString("utf8") || "{}");
2165
+ const only = body.id || url.searchParams.get("id") || null;
2166
+ const targets = (await listRefusedRefs(eng, ws)).filter((e) => !only || e.offerId === only || e.oid === only);
2167
+ for (const e of targets) await deleteRefusedRef(eng, ws, ledger, e.offerId || e.oid);
2168
+ sendJson(res, { ok: true, ws, discarded: targets.length });
2169
+ });
2170
+ }
2171
+ if (sub[0] === "rotate-secret" && req.method === "POST") {
2172
+ return sendJson(res, { ok: false, retired: true, error: "rotate-secret is retired \u2014 ownership is a ledger fact (sha256 of the push password on the law record); re-own by re-birthing via the upload lane" }, 410);
2173
+ }
1906
2174
  if (sub[0] === "pack" && req.method === "GET") {
1907
2175
  return serialize(async () => {
1908
2176
  await mountWorkspace(eng, ws, ledgerFor(ws));
@@ -1951,7 +2219,17 @@ async function startDevCloud(opts = {}) {
1951
2219
  const resolved = resolveGatewayOperation(spec, opName, method);
1952
2220
  if (resolved.ambiguous) return sendJson(res, { ok: false, error: "ambiguous operation", candidates: resolved.ambiguous }, 409);
1953
2221
  const x = resolved.op && resolved.op["x-nomos"];
1954
- if (!x) return sendJson(res, { ok: false, error: "unknown operation", see: `GET /v2/workspaces/${ws}` }, 404);
2222
+ if (!x) {
2223
+ if (RECOGNIZED_UNSUPPORTED.has(opName)) {
2224
+ return sendJson(res, {
2225
+ ok: false,
2226
+ unsupported: true,
2227
+ route: opName,
2228
+ error: `'${opName}' is a recognized Nomos Cloud route that 'githolon cloud dev' (single-node local) does not serve \u2014 ${UNSUPPORTED_WHY[opName] || "a multi-node / Cloudflare-infra concern"}. This is NOT an app bug; if you need it locally, ask the Nomos devs.`
2229
+ }, 501);
2230
+ }
2231
+ return sendJson(res, { ok: false, error: "unknown operation", see: `GET /v2/workspaces/${ws}` }, 404);
2232
+ }
1955
2233
  const principal = principalOf(req);
1956
2234
  const authToken = authTokenOf(req);
1957
2235
  if (x.kind === "offer" && method === "post") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "githolon",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "type": "module",
5
5
  "description": "githolon — the Nomos developer CLI: Rails-style generators for @githolon/dsl domains + the package compiler. Kernel-independent.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@bjorn3/browser_wasi_shim": "0.4.2",
32
- "@githolon/client": "^0.85.0",
33
- "@githolon/dsl": "^0.85.0",
32
+ "@githolon/client": "^0.86.0",
33
+ "@githolon/dsl": "^0.86.0",
34
34
  "isomorphic-git": "^1.38.4"
35
35
  },
36
36
  "devDependencies": {