@githolon/dsl 0.105.3 → 0.105.5
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/dart/CHANGELOG.md +19 -0
- package/dart/lib/src/runner/node-entry.mjs +69 -3
- package/dart/lib/src/runner/offline-install.mjs +181 -10
- package/dart/lib/src/webview_bridge.dart +34 -2
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +4 -0
- package/dart_types/lib/src/local_work_rejected.dart +117 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +65 -3
- package/src/compile_package_main.ts +7 -3
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## 0.64.20
|
|
2
|
+
|
|
3
|
+
- NEW: `bridge.localWorkRejectedEvents` — a typed stream of `NomosLocalWorkRejectedEvent` (from
|
|
4
|
+
nomos_types 0.8.6) forwarded over the runner bridge, so apps render honest UX ("queued, will
|
|
5
|
+
retry" vs "preserved for review — contact support") instead of scraping raw refusal prose.
|
|
6
|
+
- `parentSignerEnrollment` gains `remote: true` — force the CLOUD-gateway read even when this holon
|
|
7
|
+
IS the parent: the seated barrier's proof-by-read (enrolled there ⇒ durably in custody, not merely
|
|
8
|
+
locally folded).
|
|
9
|
+
- Rebuild the bundled runner from @githolon/client 0.105.5 (the seal-obligation release: footage-first
|
|
10
|
+
offer lane, scoreboard truth, FF-only seals, seal retry, no-evict-while-dirty).
|
|
11
|
+
|
|
12
|
+
## 0.64.19
|
|
13
|
+
|
|
14
|
+
- Rebuild the bundled runner assets from @githolon/client 0.105.3: fixes `ensureParentSignerEnrollment`
|
|
15
|
+
routing `enrollFirstDevice` when the caller is already connected directly to the target workspace
|
|
16
|
+
(`parent === workspace`) — the local structural lookup can never find it (deliberately absent from
|
|
17
|
+
public business OpenAPI), and the framework-route fallback was previously gated to skip exactly this
|
|
18
|
+
case.
|
|
19
|
+
|
|
1
20
|
## 0.64.18
|
|
2
21
|
|
|
3
22
|
- Restaged with the current kernel candidate: regenerated vendored assets (kernel release identity is baked into them). Published pub.dev archives are immutable, so regenerated bytes ride their own version.
|
|
@@ -338,6 +338,10 @@ function createWebviewRunner({ post: post2, openRealm: openRealm2 }) {
|
|
|
338
338
|
holon.onCustodyEvent?.((evt) => post2({ v: WEBVIEW_BRIDGE_VERSION, event: "custody", session, custody: evt }));
|
|
339
339
|
} catch {
|
|
340
340
|
}
|
|
341
|
+
try {
|
|
342
|
+
holon.onLocalWorkRejectedEvent?.((evt) => post2({ v: WEBVIEW_BRIDGE_VERSION, event: "localWorkRejected", session, localWorkRejected: evt }));
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
341
345
|
if (a.onCustodyBreak === "ask") {
|
|
342
346
|
holon.onCustodyBreak = (brk) => new Promise((resolve) => {
|
|
343
347
|
pendingCustodyAsks.set(session, resolve);
|
|
@@ -8096,7 +8100,7 @@ function createGovernanceOffers(ctx) {
|
|
|
8096
8100
|
* `revoked: true` means the subject's leaf was flipped `status:revoked` (remove-wins — a lawful
|
|
8097
8101
|
* re-enroll of the SAME key is refused; mint + enroll a new device key instead).
|
|
8098
8102
|
*/
|
|
8099
|
-
parentSignerEnrollment: async ({ parent, principal, keyHash, publicKey, authToken } = {}) => {
|
|
8103
|
+
parentSignerEnrollment: async ({ parent, principal, keyHash, publicKey, authToken, remote } = {}) => {
|
|
8100
8104
|
if (!parent || typeof parent !== "string") throw new Error("parentSignerEnrollment requires `parent` (the workspace whose chain holds the signer tuple)");
|
|
8101
8105
|
if (!principal || typeof principal !== "string") throw new Error("parentSignerEnrollment requires `principal` (the auth-provider uid)");
|
|
8102
8106
|
const p = canonicalPrincipal(principal);
|
|
@@ -8106,7 +8110,7 @@ function createGovernanceOffers(ctx) {
|
|
|
8106
8110
|
}
|
|
8107
8111
|
const object = signerObjectOf(p);
|
|
8108
8112
|
let rows;
|
|
8109
|
-
if (parent === workspace) {
|
|
8113
|
+
if (parent === workspace && !remote) {
|
|
8110
8114
|
rows = JSON.parse(call("query", { queryBytes: qBytes2({ kind: "query", queryId: "relationTuplesByObject", paramsJson: JSON.stringify({ object }) }) })) || [];
|
|
8111
8115
|
} else {
|
|
8112
8116
|
const headers = authToken ? { Authorization: `Bearer ${authToken}`, "x-nomos-auth": authToken } : authHeaders;
|
|
@@ -8635,7 +8639,7 @@ function birthOutcomeFailure(birthOutcomes) {
|
|
|
8635
8639
|
err.birthOutcomes = birthOutcomes;
|
|
8636
8640
|
return err;
|
|
8637
8641
|
}
|
|
8638
|
-
async function connectSingle({ cloud, workspace, clientId, authToken, restoreFrom, onSpan, authorSecret, principal, allowUnborn = false, salvageOnly = false, recover = false, mountInPlace = false, __injectCheckpoint, kernelProvider, fetchImpl, historicSnapshots, onCustodyBreak, watchRebind, openWorkspaceInPlane, __knownKernelCapabilities, __testKRootPublic, __testRootSecret }) {
|
|
8642
|
+
async function connectSingle({ cloud, workspace, clientId, authToken, restoreFrom, onSpan, authorSecret, principal, allowUnborn = false, salvageOnly = false, recover = false, mountInPlace = false, __injectCheckpoint, kernelProvider, fetchImpl, historicSnapshots, onCustodyBreak, onLocalWorkRejected, watchRebind, openWorkspaceInPlane, __knownKernelCapabilities, __testKRootPublic, __testRootSecret }) {
|
|
8639
8643
|
if (salvageOnly) allowUnborn = true;
|
|
8640
8644
|
if (!cloud || !workspace) throw new Error("connect({ cloud, workspace }) required");
|
|
8641
8645
|
const _fetch = typeof fetchImpl === "function" ? fetchImpl : globalThis.fetch;
|
|
@@ -8797,6 +8801,47 @@ ${String(commit).toLowerCase()}`;
|
|
|
8797
8801
|
}
|
|
8798
8802
|
};
|
|
8799
8803
|
const onCustodyBreakOpt = typeof onCustodyBreak === "function" ? onCustodyBreak : null;
|
|
8804
|
+
let localWorkRejectedHook = typeof onLocalWorkRejected === "function" ? onLocalWorkRejected : null;
|
|
8805
|
+
const localWorkRejectedListeners = /* @__PURE__ */ new Set();
|
|
8806
|
+
const refusalIsRetryable = (error) => {
|
|
8807
|
+
try {
|
|
8808
|
+
const d = authzDiagnosisFromRefusal(String(error ?? ""));
|
|
8809
|
+
return d?.code === "nomos.authz.signer_enrollment_missing";
|
|
8810
|
+
} catch {
|
|
8811
|
+
return false;
|
|
8812
|
+
}
|
|
8813
|
+
};
|
|
8814
|
+
const emitLocalWorkRejected = (source, refused, resetTo) => {
|
|
8815
|
+
if (!refused.length) return;
|
|
8816
|
+
const items = refused.map((r) => ({
|
|
8817
|
+
intentId: typeof r.intentId === "string" ? r.intentId : null,
|
|
8818
|
+
oid: typeof r.oid === "string" ? r.oid : null,
|
|
8819
|
+
domain: typeof r.domain === "string" ? r.domain : null,
|
|
8820
|
+
directiveId: typeof r.directiveId === "string" ? r.directiveId : null,
|
|
8821
|
+
error: String(r.error ?? "the law refused this offer").slice(0, 600),
|
|
8822
|
+
retryable: refusalIsRetryable(r.error)
|
|
8823
|
+
}));
|
|
8824
|
+
const event = {
|
|
8825
|
+
workspace,
|
|
8826
|
+
refused: items,
|
|
8827
|
+
parked: true,
|
|
8828
|
+
resetTo,
|
|
8829
|
+
retryable: items.every((i) => i.retryable),
|
|
8830
|
+
source
|
|
8831
|
+
};
|
|
8832
|
+
if (localWorkRejectedHook) {
|
|
8833
|
+
try {
|
|
8834
|
+
localWorkRejectedHook(event);
|
|
8835
|
+
} catch {
|
|
8836
|
+
}
|
|
8837
|
+
}
|
|
8838
|
+
for (const l of [...localWorkRejectedListeners]) {
|
|
8839
|
+
try {
|
|
8840
|
+
l(event);
|
|
8841
|
+
} catch {
|
|
8842
|
+
}
|
|
8843
|
+
}
|
|
8844
|
+
};
|
|
8800
8845
|
function captureIncarnation(j) {
|
|
8801
8846
|
const inc = j && typeof j.incarnation === "string" && OID40_RE.test(j.incarnation) ? j.incarnation : null;
|
|
8802
8847
|
if (inc !== null) remoteIncarnation = inc;
|
|
@@ -9383,6 +9428,7 @@ ${String(commit).toLowerCase()}`;
|
|
|
9383
9428
|
}
|
|
9384
9429
|
}
|
|
9385
9430
|
}
|
|
9431
|
+
if (deadLettered.length) emitLocalWorkRejected("sync", deadLettered.map((d) => ({ intentId: d.id, error: d.error })), remoteHead);
|
|
9386
9432
|
invalidateInstalledLawMetadata();
|
|
9387
9433
|
nudgeWatches();
|
|
9388
9434
|
return {
|
|
@@ -11695,6 +11741,22 @@ ${String(commit).toLowerCase()}`;
|
|
|
11695
11741
|
if (typeof cb === "function") custodyListeners.add(cb);
|
|
11696
11742
|
return () => custodyListeners.delete(cb);
|
|
11697
11743
|
},
|
|
11744
|
+
/** THE LOCAL-WORK-REJECTED HOOK (settable; connect({onLocalWorkRejected}) is the default) — the
|
|
11745
|
+
* typed "the cloud refused work your device had accepted" lifecycle event. See
|
|
11746
|
+
* `LocalWorkRejectedEvent`: refused intents are already parked + survivors replayed when it
|
|
11747
|
+
* fires; the app's only job is honest UX (retryable ⇒ "queued, will retry"; terminal ⇒
|
|
11748
|
+
* "preserved for review — contact support"). */
|
|
11749
|
+
get onLocalWorkRejected() {
|
|
11750
|
+
return localWorkRejectedHook;
|
|
11751
|
+
},
|
|
11752
|
+
set onLocalWorkRejected(fn) {
|
|
11753
|
+
localWorkRejectedHook = typeof fn === "function" ? fn : null;
|
|
11754
|
+
},
|
|
11755
|
+
/** Subscribe (multi-listener) to LocalWorkRejectedEvents. Returns an unsubscribe fn. */
|
|
11756
|
+
onLocalWorkRejectedEvent: (cb) => {
|
|
11757
|
+
if (typeof cb === "function") localWorkRejectedListeners.add(cb);
|
|
11758
|
+
return () => localWorkRejectedListeners.delete(cb);
|
|
11759
|
+
},
|
|
11698
11760
|
/**
|
|
11699
11761
|
* INTROSPECTION — where this holon sits on the custody spectrum:
|
|
11700
11762
|
* `{ incarnation, localGenesis, remoteIncarnation, syncPosture, pendingLocalIntents, salvagedIntents,
|
|
@@ -13164,6 +13226,10 @@ ${String(commit).toLowerCase()}`;
|
|
|
13164
13226
|
});
|
|
13165
13227
|
_convSpan.end();
|
|
13166
13228
|
if (result && result.custodyBreak) return await resolveCustodyBreakFlow(result.custodyBreak, { operation: "sync" });
|
|
13229
|
+
if (admissionRefusals && admissionRefusals.size) {
|
|
13230
|
+
const refusedEntries = (admission?.sessions || []).flatMap((s) => (s.rejected || []).map((r) => ({ intentId: r.intent, oid: r.oid, error: r.error })));
|
|
13231
|
+
emitLocalWorkRejected("admission", refusedEntries, result && typeof result.remoteMain === "string" ? result.remoteMain : null);
|
|
13232
|
+
}
|
|
13167
13233
|
try {
|
|
13168
13234
|
result.stillAhead = await resolveBranchHead() !== syncedBase;
|
|
13169
13235
|
} catch {
|
|
@@ -137,6 +137,7 @@ __export(engine_exports, {
|
|
|
137
137
|
cryptoUnwrapKey: () => cryptoUnwrapKey,
|
|
138
138
|
cryptoWrapKey: () => cryptoWrapKey,
|
|
139
139
|
currentLaw: () => currentLaw,
|
|
140
|
+
custodyConfirmedMain: () => custodyConfirmedMain,
|
|
140
141
|
declaredCapabilities: () => declaredCapabilities,
|
|
141
142
|
deepVerifyShard: () => deepVerifyShard,
|
|
142
143
|
delegationCertSign: () => delegationCertSign,
|
|
@@ -163,6 +164,7 @@ __export(engine_exports, {
|
|
|
163
164
|
ingestPackAsCustody: () => ingestPackAsCustody,
|
|
164
165
|
installedLaw: () => installedLaw,
|
|
165
166
|
intentOfferResidencies: () => intentOfferResidencies,
|
|
167
|
+
isDirty: () => isDirty,
|
|
166
168
|
isWarranted: () => isWarranted,
|
|
167
169
|
kernelStamp: () => kernelStamp,
|
|
168
170
|
kgit: () => kgit,
|
|
@@ -6803,6 +6805,40 @@ async function admitIntentOffers(eng2, ws, ledger, offers, { receiptFence = null
|
|
|
6803
6805
|
const s0 = Date.now();
|
|
6804
6806
|
return (end = {}) => timings.spans.push({ id: `intent-offer-${t0}-${seq++}`, track: "cloud", name, kind, depth, t0: s0, t1: Date.now(), ...extra, ...end });
|
|
6805
6807
|
};
|
|
6808
|
+
let footageRef = null;
|
|
6809
|
+
{
|
|
6810
|
+
const parked = [];
|
|
6811
|
+
for (const offer of Array.isArray(offers) ? offers : []) {
|
|
6812
|
+
let blob = offer?.blob instanceof Uint8Array ? offer.blob : null;
|
|
6813
|
+
try {
|
|
6814
|
+
if (!blob && typeof offer?.changeB64 === "string") blob = Uint8Array.from(atob(offer.changeB64), (c) => c.charCodeAt(0));
|
|
6815
|
+
} catch {
|
|
6816
|
+
}
|
|
6817
|
+
if (blob && blob.byteLength) parked.push(blob);
|
|
6818
|
+
}
|
|
6819
|
+
if (parked.length) {
|
|
6820
|
+
footageRef = `refs/heads/parked/offered/${t0.toString(36)}-${Math.floor(Math.random() * 16777215).toString(36)}`;
|
|
6821
|
+
const footageDone = span("park offer footage", "cseal", 1, { offers: parked.length });
|
|
6822
|
+
try {
|
|
6823
|
+
kgit.writeMetaCommit(eng2, gitdirOf(ws), {
|
|
6824
|
+
parents: [],
|
|
6825
|
+
files: parked.map((bytes, i) => ({ path: `${i}.change.usda`, bytes })),
|
|
6826
|
+
message: `offered: ${parked.length} intent(s) parked pre-judgment (footage-first)`,
|
|
6827
|
+
timeSecs: 0,
|
|
6828
|
+
ref: footageRef
|
|
6829
|
+
});
|
|
6830
|
+
await custodyPush(eng2, ws, ledger, { remoteRef: footageRef });
|
|
6831
|
+
} catch (e) {
|
|
6832
|
+
try {
|
|
6833
|
+
kgit.deleteRef(eng2, gitdirOf(ws), footageRef);
|
|
6834
|
+
} catch {
|
|
6835
|
+
}
|
|
6836
|
+
throw new Error(`offer footage not durable \u2014 custody unreachable, nothing was folded; retry the batch: ${String(e).slice(0, 300)}`, { cause: e });
|
|
6837
|
+
} finally {
|
|
6838
|
+
footageDone();
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6806
6842
|
for (const offer of Array.isArray(offers) ? offers : []) {
|
|
6807
6843
|
const cid = typeof offer?.session === "string" && offer.session ? offer.session : "direct";
|
|
6808
6844
|
const rec = sessions.get(cid) || { session: cid, admitted: [], rejected: [], skipped: [] };
|
|
@@ -6878,10 +6914,13 @@ async function admitIntentOffers(eng2, ws, ledger, offers, { receiptFence = null
|
|
|
6878
6914
|
}
|
|
6879
6915
|
timings.sessions = sessions.size;
|
|
6880
6916
|
timings.intents = admittedTotal;
|
|
6917
|
+
let refusalsParked = true;
|
|
6881
6918
|
if (deadLetters.length) {
|
|
6882
6919
|
try {
|
|
6883
|
-
await materializeRefused(eng2, ws, ledger, deadLetters);
|
|
6920
|
+
const m = await materializeRefused(eng2, ws, ledger, deadLetters);
|
|
6921
|
+
refusalsParked = m.unparkedSessions.size === 0;
|
|
6884
6922
|
} catch (e) {
|
|
6923
|
+
refusalsParked = false;
|
|
6885
6924
|
log("refused-materialize-failed", { ws, error: String(e).slice(0, 200) });
|
|
6886
6925
|
}
|
|
6887
6926
|
}
|
|
@@ -6890,6 +6929,17 @@ async function admitIntentOffers(eng2, ws, ledger, offers, { receiptFence = null
|
|
|
6890
6929
|
await commitAndPush(eng2, ws, ledger);
|
|
6891
6930
|
sealDone();
|
|
6892
6931
|
}
|
|
6932
|
+
if (footageRef && refusalsParked) {
|
|
6933
|
+
try {
|
|
6934
|
+
kgit.deleteRef(eng2, gitdirOf(ws), footageRef);
|
|
6935
|
+
} catch {
|
|
6936
|
+
}
|
|
6937
|
+
try {
|
|
6938
|
+
await custodyPush(eng2, ws, ledger, { remoteRef: footageRef, del: true });
|
|
6939
|
+
} catch (e) {
|
|
6940
|
+
log("footage-delete-push-failed", { ws, ref: footageRef, error: String(e).slice(0, 200) });
|
|
6941
|
+
}
|
|
6942
|
+
}
|
|
6893
6943
|
const gitdir = gitdirOf(ws);
|
|
6894
6944
|
const head = kgit.resolveRef(eng2, gitdir, BRANCH) ?? main.head;
|
|
6895
6945
|
const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore ?? "", head ?? "") : null;
|
|
@@ -6903,7 +6953,7 @@ async function admitIntentOffers(eng2, ws, ledger, offers, { receiptFence = null
|
|
|
6903
6953
|
}
|
|
6904
6954
|
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" });
|
|
6905
6955
|
const bornPacked = born.length ? packBornChildren(eng2, born) : born;
|
|
6906
|
-
return { sessions: [...sessions.values()].map((s) => ({ ...s, ...s.skipped.length ? {} : { skipped: void 0 } })), main: head, deadLetters, timings, summary: summary ?? null, born: bornPacked, birthOutcomes, ...resultPackB64 ? { resultPackB64, resultBase: clientBase } : {} };
|
|
6956
|
+
return { sessions: [...sessions.values()].map((s) => ({ ...s, ...s.skipped.length ? {} : { skipped: void 0 } })), main: head, sealed: true, sealedMain: custodyConfirmedMain(eng2, ws), deadLetters, timings, summary: summary ?? null, born: bornPacked, birthOutcomes, ...resultPackB64 ? { resultPackB64, resultBase: clientBase } : {} };
|
|
6907
6957
|
}
|
|
6908
6958
|
function intentOfferResidencies(eng2, offers) {
|
|
6909
6959
|
const unique = /* @__PURE__ */ new Map();
|
|
@@ -7035,6 +7085,7 @@ async function mountWorkspace(eng2, ws, ledger) {
|
|
|
7035
7085
|
throw new Error(`kernel refused restored main ${remoteMain}: ${JSON.stringify(mounted)}`);
|
|
7036
7086
|
}
|
|
7037
7087
|
restored = true;
|
|
7088
|
+
custodyMainOf(eng2).set(ws, remoteMain);
|
|
7038
7089
|
verificationVerdict = jsStruct(mounted.verdict);
|
|
7039
7090
|
}
|
|
7040
7091
|
} catch (e) {
|
|
@@ -7531,7 +7582,7 @@ async function custodyFetch(eng2, ws, ledger, { remoteRef = "refs/heads/main", l
|
|
|
7531
7582
|
if (writeRef) kgit.writeRef(eng2, gitdirOf(ws), fullRef(localRef || remoteRef), tip);
|
|
7532
7583
|
return tip;
|
|
7533
7584
|
}
|
|
7534
|
-
async function custodyPush(eng2, ws, ledger, { localRef = null, remoteRef, del = false, pointer = false, newOid = null }) {
|
|
7585
|
+
async function custodyPush(eng2, ws, ledger, { localRef = null, remoteRef, del = false, pointer = false, newOid = null, ffOnly = false }) {
|
|
7535
7586
|
const headers = ledger.headers || {};
|
|
7536
7587
|
const remoteRefFull = fullRef(remoteRef);
|
|
7537
7588
|
let remote;
|
|
@@ -7547,22 +7598,46 @@ async function custodyPush(eng2, ws, ledger, { localRef = null, remoteRef, del =
|
|
|
7547
7598
|
}
|
|
7548
7599
|
const tgt = newOid || kgit.resolveRef(eng2, gitdirOf(ws), fullRef(localRef || remoteRef));
|
|
7549
7600
|
if (!tgt) throw new Error(`custodyPush: local ref ${localRef || remoteRef} unresolved`);
|
|
7550
|
-
if (tgt === oldOid)
|
|
7601
|
+
if (tgt === oldOid) {
|
|
7602
|
+
if (remoteRefFull === fullRef(BRANCH)) custodyMainOf(eng2).set(ws, tgt);
|
|
7603
|
+
return;
|
|
7604
|
+
}
|
|
7551
7605
|
let pack = null;
|
|
7552
7606
|
if (!pointer) {
|
|
7553
7607
|
pack = oldOid !== ZERO_OID ? packDelta(eng2, ws, oldOid, tgt) : null;
|
|
7608
|
+
if (!pack && ffOnly && oldOid !== ZERO_OID) {
|
|
7609
|
+
throw new Error(`custody ${remoteRefFull} regression refused: remote ${oldOid.slice(0, 12)} is not an ancestor of local ${tgt.slice(0, 12)} \u2014 this engine's view is stale; re-mount from custody instead of forcing`);
|
|
7610
|
+
}
|
|
7554
7611
|
if (!pack) pack = packDelta(eng2, ws, "", tgt);
|
|
7555
7612
|
}
|
|
7556
7613
|
const packBytes = pack?.byteLength ?? 0;
|
|
7557
7614
|
try {
|
|
7558
|
-
|
|
7615
|
+
const r = await pushVia(ledger, () => pushPack(ledger.remote, headers, { updates: [{ ref: remoteRefFull, oldOid, newOid: tgt }], pack }));
|
|
7616
|
+
if (remoteRefFull === fullRef(BRANCH)) custodyMainOf(eng2).set(ws, tgt);
|
|
7617
|
+
return r;
|
|
7559
7618
|
} catch (error) {
|
|
7560
7619
|
throw new Error(`custody receive-pack push failed for ${remoteRefFull} (${packBytes} pack bytes): ${String(error)}`, { cause: error });
|
|
7561
7620
|
}
|
|
7562
7621
|
}
|
|
7622
|
+
function custodyMainOf(eng2) {
|
|
7623
|
+
eng2._custodyMain ??= /* @__PURE__ */ new Map();
|
|
7624
|
+
return eng2._custodyMain;
|
|
7625
|
+
}
|
|
7626
|
+
function custodyConfirmedMain(eng2, ws) {
|
|
7627
|
+
return custodyMainOf(eng2).get(ws) ?? null;
|
|
7628
|
+
}
|
|
7629
|
+
function isDirty(eng2, ws) {
|
|
7630
|
+
try {
|
|
7631
|
+
const resident = kgit.resolveRef(eng2, gitdirOf(ws), BRANCH);
|
|
7632
|
+
if (!resident) return false;
|
|
7633
|
+
return resident !== custodyMainOf(eng2).get(ws);
|
|
7634
|
+
} catch {
|
|
7635
|
+
return false;
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7563
7638
|
async function commitAndPush(eng2, ws, ledger) {
|
|
7564
7639
|
try {
|
|
7565
|
-
await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main" });
|
|
7640
|
+
await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", ffOnly: true });
|
|
7566
7641
|
} catch (e) {
|
|
7567
7642
|
unmount(eng2, ws);
|
|
7568
7643
|
const cause = e instanceof Error ? e.cause : void 0;
|
|
@@ -8238,6 +8313,80 @@ async function admitAll(eng2, ws, ledger, { receiptFence = null, shardRouting =
|
|
|
8238
8313
|
_sessionDone();
|
|
8239
8314
|
}
|
|
8240
8315
|
}
|
|
8316
|
+
const offeredRefPrefix = "refs/heads/parked/offered/";
|
|
8317
|
+
const footageConsumed = [];
|
|
8318
|
+
const footageBorn = [], footageBirthOutcomes = [];
|
|
8319
|
+
try {
|
|
8320
|
+
const offeredRefs = Object.entries(await custodyLsRefs(ledger, offeredRefPrefix));
|
|
8321
|
+
for (const [oref, ooid] of offeredRefs) {
|
|
8322
|
+
if (!oref.startsWith(offeredRefPrefix) || !ooid) continue;
|
|
8323
|
+
const fcid = `offered:${oref.slice(offeredRefPrefix.length)}`;
|
|
8324
|
+
const _fDone = _span(`footage ${fcid.slice(0, 24)}`, "csession", 1, { client: fcid });
|
|
8325
|
+
const admitted = [], rejected = [], skipped = [];
|
|
8326
|
+
try {
|
|
8327
|
+
try {
|
|
8328
|
+
await custodyFetch(eng2, ws, ledger, { remoteRef: oref, have: kgit.resolveRef(eng2, gitdir, BRANCH) });
|
|
8329
|
+
} catch {
|
|
8330
|
+
await custodyFetch(eng2, ws, ledger, { remoteRef: oref, have: null });
|
|
8331
|
+
}
|
|
8332
|
+
const main = await mainIntentIds(eng2, ws);
|
|
8333
|
+
for (let i = 0; ; i++) {
|
|
8334
|
+
let blob = null;
|
|
8335
|
+
try {
|
|
8336
|
+
blob = kgit.readBlob(eng2, gitdir, ooid, `${i}.change.usda`);
|
|
8337
|
+
} catch {
|
|
8338
|
+
blob = null;
|
|
8339
|
+
}
|
|
8340
|
+
if (!blob || !blob.byteLength) break;
|
|
8341
|
+
try {
|
|
8342
|
+
const inspected = kgit.inspectChange(eng2, blob);
|
|
8343
|
+
const intentId = inspected.intentId || null;
|
|
8344
|
+
const oid = String(intentId || `${fcid}#${i}`).slice(0, 10);
|
|
8345
|
+
if (!inspected.authored || !inspected.call) {
|
|
8346
|
+
rejected.push({ oid, error: "parked offer is not an authored nominal call" });
|
|
8347
|
+
continue;
|
|
8348
|
+
}
|
|
8349
|
+
const { domainPath: dom, directivePath: dirId } = inspected.call;
|
|
8350
|
+
if (receiptFence && dirId && Array.isArray(receiptFence[dom]) && receiptFence[dom].includes(dirId)) {
|
|
8351
|
+
const error = `'${dom}/${dirId}' is a capability RECEIPT \u2014 receipts are executor/owner writes, never open-session writes`;
|
|
8352
|
+
rejected.push({ oid, error, deadLettered: true });
|
|
8353
|
+
deadLetters.push({ intentId, oid, session: fcid, error, blob, domain: dom, directiveId: dirId });
|
|
8354
|
+
continue;
|
|
8355
|
+
}
|
|
8356
|
+
if (intentId && main.ids.has(intentId)) {
|
|
8357
|
+
skipped.push({ oid, intent: intentId, alreadyOnMain: true });
|
|
8358
|
+
continue;
|
|
8359
|
+
}
|
|
8360
|
+
const res = offerAdmit(eng2, ws, blob, { defer: canDeferProjection });
|
|
8361
|
+
if (res.ok) {
|
|
8362
|
+
if (intentId) main.ids.add(intentId);
|
|
8363
|
+
main.oids.add(res.head);
|
|
8364
|
+
main.head = res.head;
|
|
8365
|
+
admitted.push({ oid, intent: intentId, head: res.head });
|
|
8366
|
+
totalAdmitted += 1;
|
|
8367
|
+
_intents += 1;
|
|
8368
|
+
if (Array.isArray(res.born) && res.born.length) footageBorn.push(...res.born);
|
|
8369
|
+
if (Array.isArray(res.birthOutcomes) && res.birthOutcomes.length) footageBirthOutcomes.push(...res.birthOutcomes);
|
|
8370
|
+
} else {
|
|
8371
|
+
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`;
|
|
8372
|
+
rejected.push({ oid, error, deadLettered: true });
|
|
8373
|
+
deadLetters.push({ intentId, oid, session: fcid, error, blob, domain: dom, directiveId: dirId });
|
|
8374
|
+
}
|
|
8375
|
+
} catch (e) {
|
|
8376
|
+
rejected.push({ oid: `${fcid}#${i}`.slice(0, 10), error: String(e).slice(0, 200) });
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
footageConsumed.push(oref);
|
|
8380
|
+
results.push({ session: fcid, admitted, rejected, ...skipped.length ? { skipped } : {} });
|
|
8381
|
+
} catch (e) {
|
|
8382
|
+
results.push({ session: fcid, error: String(e).slice(0, 200) });
|
|
8383
|
+
} finally {
|
|
8384
|
+
_fDone({ admitted: admitted.length, rejected: rejected.length });
|
|
8385
|
+
}
|
|
8386
|
+
}
|
|
8387
|
+
} catch (e) {
|
|
8388
|
+
log("footage-scan-failed", { ws, error: String(e).slice(0, 200) });
|
|
8389
|
+
}
|
|
8241
8390
|
if (canDeferProjection && totalAdmitted) {
|
|
8242
8391
|
const _projDone = _span("projection flush (batch)", "cproject", 1, { admitted: totalAdmitted });
|
|
8243
8392
|
try {
|
|
@@ -8261,7 +8410,8 @@ async function admitAll(eng2, ws, ledger, { receiptFence = null, shardRouting =
|
|
|
8261
8410
|
const hasCoordination = !!(summaryCtx || placements.length || splits.length || globalsOut.length);
|
|
8262
8411
|
const deferDurability = defer && !hasCoordination;
|
|
8263
8412
|
let _sealed = true;
|
|
8264
|
-
|
|
8413
|
+
const sealNeeded = totalAdmitted > 0 || isDirty(eng2, ws);
|
|
8414
|
+
if (sealNeeded && !deferDurability) {
|
|
8265
8415
|
const _sealDone = _span(`seal (batch)`, "cseal", 1, { sessions: consumed.length, admitted: totalAdmitted });
|
|
8266
8416
|
const _s = Date.now();
|
|
8267
8417
|
try {
|
|
@@ -8281,17 +8431,38 @@ async function admitAll(eng2, ws, ledger, { receiptFence = null, shardRouting =
|
|
|
8281
8431
|
} catch {
|
|
8282
8432
|
}
|
|
8283
8433
|
}
|
|
8434
|
+
for (const oref of footageConsumed) {
|
|
8435
|
+
const fcid = `offered:${oref.slice(offeredRefPrefix.length)}`;
|
|
8436
|
+
if (unparkedSessions.has(fcid)) continue;
|
|
8437
|
+
try {
|
|
8438
|
+
kgit.deleteRef(eng2, gitdir, oref);
|
|
8439
|
+
} catch {
|
|
8440
|
+
}
|
|
8441
|
+
try {
|
|
8442
|
+
await custodyPush(eng2, ws, ledger, { remoteRef: oref, del: true });
|
|
8443
|
+
} catch {
|
|
8444
|
+
}
|
|
8445
|
+
}
|
|
8284
8446
|
}
|
|
8285
8447
|
const newMain = kgit.resolveRef(eng2, gitdir, BRANCH);
|
|
8286
|
-
log("admit", { ws, sessions: results.length, main: newMain });
|
|
8448
|
+
log("admit", { ws, sessions: results.length, main: newMain, sealed: _sealed, sealedMain: custodyConfirmedMain(eng2, ws) });
|
|
8287
8449
|
const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore ?? "", newMain) : null;
|
|
8288
8450
|
const _total = Date.now() - _t0;
|
|
8289
8451
|
const timings = { totalMs: _total, scanMs: _scanMs, fetchMs: _fetchMs, sealMs: _sealMs, gateFoldMs: Math.max(0, _total - _scanMs - _fetchMs - _sealMs), intents: _intents, sessions: results.length, spans: _spans };
|
|
8290
8452
|
return {
|
|
8453
|
+
// SCOREBOARD TRUTH: `main` is the resident fold head (the ack the client converges to);
|
|
8454
|
+
// `sealedMain` is the custody-confirmed head — THE ONLY head the host may advertise/cache
|
|
8455
|
+
// (seated_barrier_and_seal_obligation.md Part 2). `sealed:false` ⇒ the seal obligation is
|
|
8456
|
+
// outstanding and the caller must re-arm the admit alarm (retry) rather than move on.
|
|
8291
8457
|
sessions: results,
|
|
8292
8458
|
main: newMain,
|
|
8459
|
+
sealed: _sealed,
|
|
8460
|
+
sealedMain: custodyConfirmedMain(eng2, ws),
|
|
8293
8461
|
deadLetters,
|
|
8294
8462
|
timings,
|
|
8463
|
+
// Footage-replayed `.births()` offers: the born child custody + typed verdicts, for the caller
|
|
8464
|
+
// to persist exactly as the live POST lane does (packed here — the caller holds no engine FS).
|
|
8465
|
+
...footageBorn.length ? { born: packBornChildren(eng2, footageBorn), birthOutcomes: footageBirthOutcomes } : {},
|
|
8295
8466
|
...summary ? { summary } : {},
|
|
8296
8467
|
...placements.length ? { placements } : {},
|
|
8297
8468
|
...splits.length ? { splits } : {},
|
|
@@ -8305,7 +8476,7 @@ async function backupHead(eng2, ws, ledger, head) {
|
|
|
8305
8476
|
if (!eng2.mounted.has(ws)) return { ok: false, error: "not mounted" };
|
|
8306
8477
|
if (!head) return { ok: false, error: "no head" };
|
|
8307
8478
|
try {
|
|
8308
|
-
await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", newOid: head });
|
|
8479
|
+
await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", newOid: head, ffOnly: true });
|
|
8309
8480
|
return { ok: true, head };
|
|
8310
8481
|
} catch (e) {
|
|
8311
8482
|
return { ok: false, error: String(e?.message || e).slice(0, 200), head };
|
|
@@ -8318,7 +8489,7 @@ async function backupToCustody(eng2, ws, ledger, { consumed = [] } = {}) {
|
|
|
8318
8489
|
head = kgit.resolveRef(eng2, gitdir, BRANCH);
|
|
8319
8490
|
if (!head) return { ok: false, error: "no local main" };
|
|
8320
8491
|
try {
|
|
8321
|
-
await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main" });
|
|
8492
|
+
await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", ffOnly: true });
|
|
8322
8493
|
} catch (e) {
|
|
8323
8494
|
return { ok: false, error: String(e).slice(0, 200), head };
|
|
8324
8495
|
}
|
|
@@ -34,7 +34,8 @@ import 'package:nomos_types/nomos_types.dart'
|
|
|
34
34
|
ReferenceVersionUnavailable,
|
|
35
35
|
CustodyBreakPolicy,
|
|
36
36
|
NomosCustodyEvent,
|
|
37
|
-
NomosCustodyStatus
|
|
37
|
+
NomosCustodyStatus,
|
|
38
|
+
NomosLocalWorkRejectedEvent;
|
|
38
39
|
import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;
|
|
39
40
|
import 'package:nomos_types/src/versioned_nomos_ref.dart'
|
|
40
41
|
show NmsInternalVersionedNomosRefs;
|
|
@@ -1230,6 +1231,11 @@ class NomosBridge {
|
|
|
1230
1231
|
// [resolveCustodyBreak].
|
|
1231
1232
|
final StreamController<NomosCustodyEvent> _custodyEvents =
|
|
1232
1233
|
StreamController<NomosCustodyEvent>.broadcast();
|
|
1234
|
+
// LOCAL-WORK-REJECTED LIFECYCLE (seated_barrier_and_seal_obligation.md Part 4): the typed "the
|
|
1235
|
+
// cloud refused work this device had accepted" event — refused intents already parked (client DLQ
|
|
1236
|
+
// + refused/* custody refs), survivors already replayed; the app renders honest UX from this.
|
|
1237
|
+
final StreamController<NomosLocalWorkRejectedEvent> _localWorkRejectedEvents =
|
|
1238
|
+
StreamController<NomosLocalWorkRejectedEvent>.broadcast();
|
|
1233
1239
|
final StreamController<Map<String, Object?>> _flameEvents =
|
|
1234
1240
|
StreamController<Map<String, Object?>>.broadcast();
|
|
1235
1241
|
final StreamController<Map<String, Object?>> _spanEvents =
|
|
@@ -1505,6 +1511,17 @@ class NomosBridge {
|
|
|
1505
1511
|
if (ev != null && !_custodyEvents.isClosed) _custodyEvents.add(ev);
|
|
1506
1512
|
return;
|
|
1507
1513
|
}
|
|
1514
|
+
if (event == 'localWorkRejected') {
|
|
1515
|
+
// LOCAL-WORK-REJECTED: { event:'localWorkRejected', session, localWorkRejected:{ workspace,
|
|
1516
|
+
// refused, parked, resetTo, retryable, source } }. The framework already parked the refused
|
|
1517
|
+
// intents + replayed survivors; the app's only job is the honest message (retryable ⇒ "queued,
|
|
1518
|
+
// will retry"; terminal ⇒ "preserved for review — contact support").
|
|
1519
|
+
final ev = NomosLocalWorkRejectedEvent.fromMessage(m);
|
|
1520
|
+
if (ev != null && !_localWorkRejectedEvents.isClosed) {
|
|
1521
|
+
_localWorkRejectedEvents.add(ev);
|
|
1522
|
+
}
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1508
1525
|
if (event == 'transfer') {
|
|
1509
1526
|
// LIVE pack byte-progress { received, total, kind } — the real loading bar (host Content-Length).
|
|
1510
1527
|
final tp = TransferProgress.fromMessage(m);
|
|
@@ -1850,6 +1867,15 @@ class NomosBridge {
|
|
|
1850
1867
|
Stream<NomosCustodyEvent> get custodyEvents =>
|
|
1851
1868
|
_eventPlane._custodyEvents.stream;
|
|
1852
1869
|
|
|
1870
|
+
/// THE LOCAL-WORK-REJECTED STREAM (seated_barrier_and_seal_obligation.md Part 4): the cloud's gate
|
|
1871
|
+
/// refused work this device's own kernel had accepted. Refused intents are already durably parked
|
|
1872
|
+
/// (client DLQ + the workspace's refused/* refs) and surviving local work already replayed when an
|
|
1873
|
+
/// event arrives — bind honest UX to it: `retryable` ⇒ "your changes are queued and will retry";
|
|
1874
|
+
/// otherwise ⇒ "your changes were preserved for review — contact support". Never render the raw
|
|
1875
|
+
/// refusal text as the headline.
|
|
1876
|
+
Stream<NomosLocalWorkRejectedEvent> get localWorkRejectedEvents =>
|
|
1877
|
+
_eventPlane._localWorkRejectedEvents.stream;
|
|
1878
|
+
|
|
1853
1879
|
/// Where a session sits on the custody spectrum (incarnation, posture, pending/salvaged local work,
|
|
1854
1880
|
/// supersession history) — for a status banner or a "you're offline on old truth" indicator.
|
|
1855
1881
|
Future<NomosCustodyStatus> custodyStatus({String? session}) async =>
|
|
@@ -2805,18 +2831,23 @@ class NomosBridge {
|
|
|
2805
2831
|
/// `principal` on `parent`'s chain (`identity:<principal>#signer@key:<sha256(publicKey)>`)? One O(1)
|
|
2806
2832
|
/// relation read over the open gateway; nothing is authored. Returns
|
|
2807
2833
|
/// `{parent, principal, keyHash, object, relation, enrolled, revoked, checkedAt}`.
|
|
2834
|
+
/// Pass `remote: true` to force the CLOUD-gateway read even when this holon IS the parent — the
|
|
2835
|
+
/// seated barrier's proof-by-read: `enrolled: true` then means the shared chain durably holds this
|
|
2836
|
+
/// device's signer tuple, not merely that the local fold ran.
|
|
2808
2837
|
Future<Map<String, Object?>> parentSignerEnrollment(
|
|
2809
2838
|
{required String parent,
|
|
2810
2839
|
required String principal,
|
|
2811
2840
|
String? keyHash,
|
|
2812
2841
|
String? publicKey,
|
|
2813
|
-
String? authToken
|
|
2842
|
+
String? authToken,
|
|
2843
|
+
bool remote = false}) async =>
|
|
2814
2844
|
_asMap(await _call('parentSignerEnrollment', {
|
|
2815
2845
|
'parent': parent,
|
|
2816
2846
|
'principal': principal,
|
|
2817
2847
|
if (keyHash != null) 'keyHash': keyHash,
|
|
2818
2848
|
if (publicKey != null) 'publicKey': publicKey,
|
|
2819
2849
|
if (authToken != null) 'authToken': authToken,
|
|
2850
|
+
if (remote) 'remote': true,
|
|
2820
2851
|
}));
|
|
2821
2852
|
|
|
2822
2853
|
/// IDEMPOTENT PARENT-SIGNER RECOVERY — ensure this device key is a live `signer` on `parent`,
|
|
@@ -3927,6 +3958,7 @@ class NomosBridge {
|
|
|
3927
3958
|
if (!_syncEvents.isClosed) await _syncEvents.close();
|
|
3928
3959
|
if (!_signalEvents.isClosed) await _signalEvents.close();
|
|
3929
3960
|
if (!_custodyEvents.isClosed) await _custodyEvents.close();
|
|
3961
|
+
if (!_localWorkRejectedEvents.isClosed) await _localWorkRejectedEvents.close();
|
|
3930
3962
|
if (!_flameEvents.isClosed) await _flameEvents.close();
|
|
3931
3963
|
if (!_spanEvents.isClosed) await _spanEvents.close();
|
|
3932
3964
|
if (!_transferEvents.isClosed) await _transferEvents.close();
|
package/dart/pubspec.lock
CHANGED
package/dart/pubspec.yaml
CHANGED
|
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
|
|
|
7
7
|
description: >-
|
|
8
8
|
Nomos 2 Dart client — generated domain values, intent payloads and application
|
|
9
9
|
lifecycle types over the typed Nomos kernel protocol and USDA custody model.
|
|
10
|
-
version: 0.64.
|
|
10
|
+
version: 0.64.20
|
|
11
11
|
|
|
12
12
|
environment:
|
|
13
13
|
sdk: ^3.5.0
|
|
@@ -16,7 +16,7 @@ dependencies:
|
|
|
16
16
|
# The pure-data core (wire types, offer codec, NomosReads, schema validation, error
|
|
17
17
|
# info). nomos_client depends on and re-exports it, so existing imports are unchanged.
|
|
18
18
|
# For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
|
|
19
|
-
nomos_types: ^0.8.
|
|
19
|
+
nomos_types: ^0.8.6
|
|
20
20
|
json_schema: ^5.2.2
|
|
21
21
|
crypto: ^3.0.0
|
|
22
22
|
package_config: ^2.1.0
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 0.8.6
|
|
2
|
+
|
|
3
|
+
- NEW: `NomosLocalWorkRejectedEvent` / `NomosRejectedIntent` — the typed "the cloud refused work your
|
|
4
|
+
device had accepted" lifecycle event (architecture/seated_barrier_and_seal_obligation.md Part 4).
|
|
5
|
+
By the time it fires, refused intents are durably parked on BOTH sides (client DLQ + workspace
|
|
6
|
+
refused/* refs) and surviving local work is rebase-replayed — no work is ever lost. `retryable`
|
|
7
|
+
distinguishes transient-premise refusals (auto-retry clears them) from terminal ones.
|
|
8
|
+
|
|
1
9
|
## 0.8.5
|
|
2
10
|
|
|
3
11
|
- Expose bounded generated-startup and referenced-workspace custody failures as typed data so applications
|
|
@@ -50,6 +50,10 @@ export 'src/git_object_id.dart';
|
|
|
50
50
|
// The CUSTODY-LIFECYCLE types (rebirth_and_custody_lifecycle.md): NomosCustodyBreak / NomosCustodyStatus /
|
|
51
51
|
// CustodyBreakPolicy — custody incompatibility as a first-class, typed, app-decidable event. Pure data.
|
|
52
52
|
export 'src/custody.dart';
|
|
53
|
+
// THE LOCAL-WORK-REJECTED LIFECYCLE (seated_barrier_and_seal_obligation.md Part 4):
|
|
54
|
+
// NomosLocalWorkRejectedEvent / NomosRejectedIntent — "the cloud refused work your device had
|
|
55
|
+
// accepted" as a first-class, typed, app-renderable event (refused work is parked, never lost).
|
|
56
|
+
export 'src/local_work_rejected.dart';
|
|
53
57
|
// SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): the read-collapse types for a
|
|
54
58
|
// `.merge(Surfaced(...))` field — SurfacedValue<T> (resolved scalar | pending observations). Pure data.
|
|
55
59
|
export 'src/surfaced.dart';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// THE LOCAL-WORK-REJECTED LIFECYCLE EVENT (architecture/seated_barrier_and_seal_obligation.md Part 4).
|
|
2
|
+
//
|
|
3
|
+
// Fired when work THIS device authored locally — and which its OWN kernel admitted — is REFUSED by the
|
|
4
|
+
// shared workspace's gate on sync/admission. Local-first means local acceptance is a real, honest
|
|
5
|
+
// verdict; the shared chain's gate re-judges from ITS state, and the two can disagree (a premise the
|
|
6
|
+
// device relied on hadn't landed yet, or the write genuinely violates shared law). By the time this
|
|
7
|
+
// event fires the framework has already done the right thing:
|
|
8
|
+
//
|
|
9
|
+
// • every refused intent is durably PARKED on BOTH sides (the client dead-letter queue AND the
|
|
10
|
+
// workspace's refused/* custody refs) — inspectable, re-offerable, NEVER lost;
|
|
11
|
+
// • every surviving unrefused local intent was rebase-replayed onto canonical main;
|
|
12
|
+
// • local state converged to [resetTo].
|
|
13
|
+
//
|
|
14
|
+
// The app's ONLY job is honest UX. [retryable] is the fork in the message:
|
|
15
|
+
// true ⇒ "Your changes are queued and will be retried automatically." (transient premise — e.g.
|
|
16
|
+
// this device's signer enrollment hadn't landed on the chain yet)
|
|
17
|
+
// false ⇒ "Your changes couldn't be accepted and have been preserved for review — contact support."
|
|
18
|
+
// Never show [NomosRejectedIntent.error] raw to an end user — it is the gate's diagnostic text.
|
|
19
|
+
class NomosRejectedIntent {
|
|
20
|
+
final String? intentId;
|
|
21
|
+
final String? oid;
|
|
22
|
+
final String? domain;
|
|
23
|
+
final String? directiveId;
|
|
24
|
+
|
|
25
|
+
/// The gate's own refusal text (diagnostic — for logs/support disclosure, never the headline).
|
|
26
|
+
final String error;
|
|
27
|
+
|
|
28
|
+
/// Transient-premise refusal that auto-retry can clear.
|
|
29
|
+
final bool retryable;
|
|
30
|
+
|
|
31
|
+
const NomosRejectedIntent({
|
|
32
|
+
this.intentId,
|
|
33
|
+
this.oid,
|
|
34
|
+
this.domain,
|
|
35
|
+
this.directiveId,
|
|
36
|
+
required this.error,
|
|
37
|
+
required this.retryable,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
factory NomosRejectedIntent.fromJson(Map<String, dynamic> j) =>
|
|
41
|
+
NomosRejectedIntent(
|
|
42
|
+
intentId: j['intentId'] as String?,
|
|
43
|
+
oid: j['oid'] as String?,
|
|
44
|
+
domain: j['domain'] as String?,
|
|
45
|
+
directiveId: j['directiveId'] as String?,
|
|
46
|
+
error: (j['error'] ?? '') as String,
|
|
47
|
+
retryable: j['retryable'] == true,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
Map<String, dynamic> toJson() => {
|
|
51
|
+
'intentId': intentId,
|
|
52
|
+
'oid': oid,
|
|
53
|
+
'domain': domain,
|
|
54
|
+
'directiveId': directiveId,
|
|
55
|
+
'error': error,
|
|
56
|
+
'retryable': retryable,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// The typed "the cloud refused work your device had accepted" event. See library docs above —
|
|
61
|
+
/// refused work is already parked and survivors replayed when this fires; render honest UX only.
|
|
62
|
+
class NomosLocalWorkRejectedEvent {
|
|
63
|
+
final String workspace;
|
|
64
|
+
final String? session;
|
|
65
|
+
final List<NomosRejectedIntent> refused;
|
|
66
|
+
|
|
67
|
+
/// Always true when the event fires: the refused intents are durably parked (client DLQ +
|
|
68
|
+
/// workspace refused/* refs).
|
|
69
|
+
final bool parked;
|
|
70
|
+
|
|
71
|
+
/// The canonical main local state converged to.
|
|
72
|
+
final String? resetTo;
|
|
73
|
+
|
|
74
|
+
/// True iff EVERY refusal in the batch is retryable.
|
|
75
|
+
final bool retryable;
|
|
76
|
+
|
|
77
|
+
/// "admission" (the edge refused our offers) or "sync" (the offer-gated converge dead-lettered
|
|
78
|
+
/// local tail intents).
|
|
79
|
+
final String source;
|
|
80
|
+
|
|
81
|
+
const NomosLocalWorkRejectedEvent({
|
|
82
|
+
required this.workspace,
|
|
83
|
+
this.session,
|
|
84
|
+
required this.refused,
|
|
85
|
+
required this.parked,
|
|
86
|
+
this.resetTo,
|
|
87
|
+
required this.retryable,
|
|
88
|
+
required this.source,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
static NomosLocalWorkRejectedEvent? fromMessage(Map<String, dynamic> m) {
|
|
92
|
+
final e = (m['localWorkRejected'] as Map?)?.cast<String, dynamic>();
|
|
93
|
+
if (e == null) return null;
|
|
94
|
+
return NomosLocalWorkRejectedEvent(
|
|
95
|
+
workspace: (e['workspace'] ?? '') as String,
|
|
96
|
+
session: m['session'] as String?,
|
|
97
|
+
refused: ((e['refused'] as List?) ?? const [])
|
|
98
|
+
.whereType<Map>()
|
|
99
|
+
.map((r) => NomosRejectedIntent.fromJson(r.cast<String, dynamic>()))
|
|
100
|
+
.toList(growable: false),
|
|
101
|
+
parked: e['parked'] != false,
|
|
102
|
+
resetTo: e['resetTo'] as String?,
|
|
103
|
+
retryable: e['retryable'] == true,
|
|
104
|
+
source: (e['source'] ?? 'sync') as String,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Map<String, dynamic> toJson() => {
|
|
109
|
+
'workspace': workspace,
|
|
110
|
+
'session': session,
|
|
111
|
+
'refused': refused.map((r) => r.toJson()).toList(growable: false),
|
|
112
|
+
'parked': parked,
|
|
113
|
+
'resetTo': resetTo,
|
|
114
|
+
'retryable': retryable,
|
|
115
|
+
'source': source,
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dart_types/pubspec.yaml
CHANGED
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
Pure kernel wire/data types for Nomos Dart frontends — no transport, no bridge,
|
|
10
10
|
no runtime. Typed protobuf and USDA boundary values plus the offer codec,
|
|
11
11
|
reactive read engine, schema validation, and data-only error/trace types.
|
|
12
|
-
version: 0.8.
|
|
12
|
+
version: 0.8.6
|
|
13
13
|
|
|
14
14
|
environment:
|
|
15
15
|
sdk: ^3.5.0
|
package/package.json
CHANGED
|
@@ -7147,7 +7147,7 @@ function createGovernanceOffers(ctx) {
|
|
|
7147
7147
|
* `revoked: true` means the subject's leaf was flipped `status:revoked` (remove-wins — a lawful
|
|
7148
7148
|
* re-enroll of the SAME key is refused; mint + enroll a new device key instead).
|
|
7149
7149
|
*/
|
|
7150
|
-
parentSignerEnrollment: async ({ parent, principal, keyHash, publicKey, authToken } = {}) => {
|
|
7150
|
+
parentSignerEnrollment: async ({ parent, principal, keyHash, publicKey, authToken, remote } = {}) => {
|
|
7151
7151
|
if (!parent || typeof parent !== "string") throw new Error("parentSignerEnrollment requires `parent` (the workspace whose chain holds the signer tuple)");
|
|
7152
7152
|
if (!principal || typeof principal !== "string") throw new Error("parentSignerEnrollment requires `principal` (the auth-provider uid)");
|
|
7153
7153
|
const p = canonicalPrincipal(principal);
|
|
@@ -7157,7 +7157,7 @@ function createGovernanceOffers(ctx) {
|
|
|
7157
7157
|
}
|
|
7158
7158
|
const object = signerObjectOf(p);
|
|
7159
7159
|
let rows;
|
|
7160
|
-
if (parent === workspace) {
|
|
7160
|
+
if (parent === workspace && !remote) {
|
|
7161
7161
|
rows = JSON.parse(call("query", { queryBytes: qBytes2({ kind: "query", queryId: "relationTuplesByObject", paramsJson: JSON.stringify({ object }) }) })) || [];
|
|
7162
7162
|
} else {
|
|
7163
7163
|
const headers = authToken ? { Authorization: `Bearer ${authToken}`, "x-nomos-auth": authToken } : authHeaders;
|
|
@@ -7686,7 +7686,7 @@ function birthOutcomeFailure(birthOutcomes) {
|
|
|
7686
7686
|
err.birthOutcomes = birthOutcomes;
|
|
7687
7687
|
return err;
|
|
7688
7688
|
}
|
|
7689
|
-
async function connectSingle({ cloud, workspace, clientId, authToken, restoreFrom, onSpan, authorSecret, principal, allowUnborn = false, salvageOnly = false, recover = false, mountInPlace = false, __injectCheckpoint, kernelProvider, fetchImpl, historicSnapshots, onCustodyBreak, watchRebind, openWorkspaceInPlane, __knownKernelCapabilities, __testKRootPublic, __testRootSecret }) {
|
|
7689
|
+
async function connectSingle({ cloud, workspace, clientId, authToken, restoreFrom, onSpan, authorSecret, principal, allowUnborn = false, salvageOnly = false, recover = false, mountInPlace = false, __injectCheckpoint, kernelProvider, fetchImpl, historicSnapshots, onCustodyBreak, onLocalWorkRejected, watchRebind, openWorkspaceInPlane, __knownKernelCapabilities, __testKRootPublic, __testRootSecret }) {
|
|
7690
7690
|
if (salvageOnly) allowUnborn = true;
|
|
7691
7691
|
if (!cloud || !workspace) throw new Error("connect({ cloud, workspace }) required");
|
|
7692
7692
|
const _fetch = typeof fetchImpl === "function" ? fetchImpl : globalThis.fetch;
|
|
@@ -7848,6 +7848,47 @@ ${String(commit).toLowerCase()}`;
|
|
|
7848
7848
|
}
|
|
7849
7849
|
};
|
|
7850
7850
|
const onCustodyBreakOpt = typeof onCustodyBreak === "function" ? onCustodyBreak : null;
|
|
7851
|
+
let localWorkRejectedHook = typeof onLocalWorkRejected === "function" ? onLocalWorkRejected : null;
|
|
7852
|
+
const localWorkRejectedListeners = /* @__PURE__ */ new Set();
|
|
7853
|
+
const refusalIsRetryable = (error) => {
|
|
7854
|
+
try {
|
|
7855
|
+
const d = authzDiagnosisFromRefusal(String(error ?? ""));
|
|
7856
|
+
return d?.code === "nomos.authz.signer_enrollment_missing";
|
|
7857
|
+
} catch {
|
|
7858
|
+
return false;
|
|
7859
|
+
}
|
|
7860
|
+
};
|
|
7861
|
+
const emitLocalWorkRejected = (source, refused, resetTo) => {
|
|
7862
|
+
if (!refused.length) return;
|
|
7863
|
+
const items = refused.map((r) => ({
|
|
7864
|
+
intentId: typeof r.intentId === "string" ? r.intentId : null,
|
|
7865
|
+
oid: typeof r.oid === "string" ? r.oid : null,
|
|
7866
|
+
domain: typeof r.domain === "string" ? r.domain : null,
|
|
7867
|
+
directiveId: typeof r.directiveId === "string" ? r.directiveId : null,
|
|
7868
|
+
error: String(r.error ?? "the law refused this offer").slice(0, 600),
|
|
7869
|
+
retryable: refusalIsRetryable(r.error)
|
|
7870
|
+
}));
|
|
7871
|
+
const event = {
|
|
7872
|
+
workspace,
|
|
7873
|
+
refused: items,
|
|
7874
|
+
parked: true,
|
|
7875
|
+
resetTo,
|
|
7876
|
+
retryable: items.every((i) => i.retryable),
|
|
7877
|
+
source
|
|
7878
|
+
};
|
|
7879
|
+
if (localWorkRejectedHook) {
|
|
7880
|
+
try {
|
|
7881
|
+
localWorkRejectedHook(event);
|
|
7882
|
+
} catch {
|
|
7883
|
+
}
|
|
7884
|
+
}
|
|
7885
|
+
for (const l of [...localWorkRejectedListeners]) {
|
|
7886
|
+
try {
|
|
7887
|
+
l(event);
|
|
7888
|
+
} catch {
|
|
7889
|
+
}
|
|
7890
|
+
}
|
|
7891
|
+
};
|
|
7851
7892
|
function captureIncarnation(j) {
|
|
7852
7893
|
const inc = j && typeof j.incarnation === "string" && OID40_RE.test(j.incarnation) ? j.incarnation : null;
|
|
7853
7894
|
if (inc !== null) remoteIncarnation = inc;
|
|
@@ -8434,6 +8475,7 @@ ${String(commit).toLowerCase()}`;
|
|
|
8434
8475
|
}
|
|
8435
8476
|
}
|
|
8436
8477
|
}
|
|
8478
|
+
if (deadLettered.length) emitLocalWorkRejected("sync", deadLettered.map((d) => ({ intentId: d.id, error: d.error })), remoteHead);
|
|
8437
8479
|
invalidateInstalledLawMetadata();
|
|
8438
8480
|
nudgeWatches();
|
|
8439
8481
|
return {
|
|
@@ -10746,6 +10788,22 @@ ${String(commit).toLowerCase()}`;
|
|
|
10746
10788
|
if (typeof cb === "function") custodyListeners.add(cb);
|
|
10747
10789
|
return () => custodyListeners.delete(cb);
|
|
10748
10790
|
},
|
|
10791
|
+
/** THE LOCAL-WORK-REJECTED HOOK (settable; connect({onLocalWorkRejected}) is the default) — the
|
|
10792
|
+
* typed "the cloud refused work your device had accepted" lifecycle event. See
|
|
10793
|
+
* `LocalWorkRejectedEvent`: refused intents are already parked + survivors replayed when it
|
|
10794
|
+
* fires; the app's only job is honest UX (retryable ⇒ "queued, will retry"; terminal ⇒
|
|
10795
|
+
* "preserved for review — contact support"). */
|
|
10796
|
+
get onLocalWorkRejected() {
|
|
10797
|
+
return localWorkRejectedHook;
|
|
10798
|
+
},
|
|
10799
|
+
set onLocalWorkRejected(fn) {
|
|
10800
|
+
localWorkRejectedHook = typeof fn === "function" ? fn : null;
|
|
10801
|
+
},
|
|
10802
|
+
/** Subscribe (multi-listener) to LocalWorkRejectedEvents. Returns an unsubscribe fn. */
|
|
10803
|
+
onLocalWorkRejectedEvent: (cb) => {
|
|
10804
|
+
if (typeof cb === "function") localWorkRejectedListeners.add(cb);
|
|
10805
|
+
return () => localWorkRejectedListeners.delete(cb);
|
|
10806
|
+
},
|
|
10749
10807
|
/**
|
|
10750
10808
|
* INTROSPECTION — where this holon sits on the custody spectrum:
|
|
10751
10809
|
* `{ incarnation, localGenesis, remoteIncarnation, syncPosture, pendingLocalIntents, salvagedIntents,
|
|
@@ -12215,6 +12273,10 @@ ${String(commit).toLowerCase()}`;
|
|
|
12215
12273
|
});
|
|
12216
12274
|
_convSpan.end();
|
|
12217
12275
|
if (result && result.custodyBreak) return await resolveCustodyBreakFlow(result.custodyBreak, { operation: "sync" });
|
|
12276
|
+
if (admissionRefusals && admissionRefusals.size) {
|
|
12277
|
+
const refusedEntries = (admission?.sessions || []).flatMap((s) => (s.rejected || []).map((r) => ({ intentId: r.intent, oid: r.oid, error: r.error })));
|
|
12278
|
+
emitLocalWorkRejected("admission", refusedEntries, result && typeof result.remoteMain === "string" ? result.remoteMain : null);
|
|
12279
|
+
}
|
|
12218
12280
|
try {
|
|
12219
12281
|
result.stillAhead = await resolveBranchHead() !== syncedBase;
|
|
12220
12282
|
} catch {
|
|
@@ -836,6 +836,10 @@ const DART_SUPPORT_CORE_SRC = [
|
|
|
836
836
|
// The custody-lifecycle types (NomosCustodyBreak/NomosCustodyStatus/CustodyBreakPolicy) — the generated
|
|
837
837
|
// client + the vendored bridge reference them through the barrel, so they must vendor. Pure data.
|
|
838
838
|
"custody.dart",
|
|
839
|
+
// THE LOCAL-WORK-REJECTED LIFECYCLE (seated_barrier_and_seal_obligation.md Part 4):
|
|
840
|
+
// NomosLocalWorkRejectedEvent/NomosRejectedIntent — the vendored bridge's localWorkRejectedEvents
|
|
841
|
+
// stream types ride through the barrel, so they must vendor. Pure data.
|
|
842
|
+
"local_work_rejected.dart",
|
|
839
843
|
// SURFACED MERGE CONFLICTS: SurfacedValue<T>/SurfacedObservation<T> — a generated read model with a
|
|
840
844
|
// `.merge(Surfaced(...))` field references them through the barrel, so they must vendor. Pure data.
|
|
841
845
|
"surfaced.dart",
|
|
@@ -1649,12 +1653,12 @@ const dartTypesImportFor = (generatedSource: string): string => {
|
|
|
1649
1653
|
* just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
|
|
1650
1654
|
// 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
|
|
1651
1655
|
// the pendingConflicts read rows.
|
|
1652
|
-
const NOMOS_TYPES_DEP = "^0.8.
|
|
1656
|
+
const NOMOS_TYPES_DEP = "^0.8.6";
|
|
1653
1657
|
// 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
|
|
1654
1658
|
// DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
|
|
1655
1659
|
// REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
|
|
1656
|
-
const NOMOS_CLIENT_DEP = "^0.64.
|
|
1657
|
-
const NOMOS_FLUTTER_DEP = "^0.63.
|
|
1660
|
+
const NOMOS_CLIENT_DEP = "^0.64.20";
|
|
1661
|
+
const NOMOS_FLUTTER_DEP = "^0.63.31";
|
|
1658
1662
|
// DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
|
|
1659
1663
|
// and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
|
|
1660
1664
|
// land back in dependency_overrides (the exact co2 report this closes, third occurrence).
|