@githolon/dsl 0.111.0 → 0.112.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/dart/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.66.9
2
+
3
+ - Prepare delayed workspace enrollment inside Peer Office and retry a proven non-admitted write once. Generated applications can observe preparation while retaining their draft.
4
+
5
+ ## 0.66.8
6
+
7
+ - Recover expired or missing sync credentials from typed failures. Preserve authorization and domain refusals without refreshing credentials based on diagnostic wording or HTTP 403.
8
+
1
9
  ## 0.66.7
2
10
 
3
11
  - Recover sync after loss of the upstream bookmark by fetching provider history and allowing the kernel to verify a shared ancestor. Preserve pending local work and reject unrelated history through the existing admission gate.
@@ -18,7 +26,7 @@
18
26
 
19
27
  ## 0.66.3
20
28
 
21
- - Restaged the generated runtime bridge: regenerated vendored runtime assets. Published pub.dev archives are immutable, so regenerated bytes ride their own version.
29
+ - Recover expired or missing sync credentials from typed failures. Preserve authorization and domain refusals without refreshing credentials based on diagnostic wording or HTTP 403.
22
30
 
23
31
  ## 0.66.2
24
32
 
@@ -114,11 +122,11 @@
114
122
 
115
123
  ## 0.64.47
116
124
 
117
- - Restaged the generated runtime bridge: regenerated vendored runtime assets. Published pub.dev archives are immutable, so regenerated bytes ride their own version.
125
+ - Recover expired or missing sync credentials from typed failures. Preserve authorization and domain refusals without refreshing credentials based on diagnostic wording or HTTP 403.
118
126
 
119
127
  ## 0.64.46
120
128
 
121
- - Restaged the generated runtime bridge: regenerated vendored runtime assets. Published pub.dev archives are immutable, so regenerated bytes ride their own version.
129
+ - Recover expired or missing sync credentials from typed failures. Preserve authorization and domain refusals without refreshing credentials based on diagnostic wording or HTTP 403.
122
130
 
123
131
  ## 0.64.45
124
132
 
@@ -17,6 +17,7 @@ library;
17
17
  import 'package:meta/meta.dart' show internal;
18
18
 
19
19
  import 'dart:async';
20
+ import '../write_progress.dart';
20
21
  import 'dart:convert';
21
22
  import 'dart:developer' as developer;
22
23
  import 'dart:typed_data';
@@ -879,7 +880,16 @@ class DartReception {
879
880
  );
880
881
  }
881
882
 
883
+ final Map<String, void Function(NomosWriteProgress)> _writeObservers = {};
884
+
882
885
  void _onRuntimeEvent(nomos_protocol.RuntimeEvent event) {
886
+ if (event.hasWritePreparation()) {
887
+ final progress = event.writePreparation;
888
+ _eventPlane._writeObservers[progress.requestId]?.call(
889
+ const NomosWriteProgress(NomosWritePhase.preparing),
890
+ );
891
+ return;
892
+ }
883
893
  if (event.hasReady()) {
884
894
  final stale = _staleBundleError(event.ready.runtimeVersion);
885
895
  if (stale != null && !allowStaleRunnerBundle) {
@@ -1719,40 +1729,48 @@ class DartReception {
1719
1729
  // ignore: avoid_print
1720
1730
  print('[nomos-offer] dispatch ${call.domain}/${call.directive} in '
1721
1731
  '$workspace using ${authenticationProof.name}');
1722
- Future<nomos_protocol.CallResponse> attempt() {
1732
+ Future<nomos_protocol.CallResponse> attempt() async {
1723
1733
  final requestNumber = ++plane._sequences.request;
1724
- return kernelTransport.callKernel(
1725
- session: runtimeSession,
1726
- request: nomos_protocol.CallRequest(
1727
- protocolVersion: 1,
1728
- requestId: '$runtimeSession:offer:$requestNumber',
1729
- offer: nomos_protocol.OfferRequest(
1730
- call: nomos_protocol.DirectiveCall(
1731
- domain: call.domain,
1732
- directive: call.directive,
1733
- operation: google_protobuf.Any(
1734
- typeUrl: call.protobufType,
1735
- value: call.protobufPayload,
1734
+ final requestId = '$runtimeSession:offer:$requestNumber';
1735
+ final observer = NomosWrite.captureObserver();
1736
+ if (observer != null) plane._writeObservers[requestId] = observer;
1737
+ try {
1738
+ return await kernelTransport.callKernel(
1739
+ session: runtimeSession,
1740
+ request: nomos_protocol.CallRequest(
1741
+ protocolVersion: 1,
1742
+ requestId: requestId,
1743
+ offer: nomos_protocol.OfferRequest(
1744
+ call: nomos_protocol.DirectiveCall(
1745
+ domain: call.domain,
1746
+ directive: call.directive,
1747
+ operation: google_protobuf.Any(
1748
+ typeUrl: call.protobufType,
1749
+ value: call.protobufPayload,
1750
+ ),
1736
1751
  ),
1752
+ authoring: nomos_protocol.AuthoringOptions(
1753
+ actor: actor ?? '',
1754
+ // Ordinary generated calls select a peer-runtime credential
1755
+ // capability below. An explicit secret is a temporary operator
1756
+ // compatibility lane and remains client-supplied until removed.
1757
+ authorSecret: explicitSecret == null
1758
+ ? const <int>[]
1759
+ : base64Decode(explicitSecret),
1760
+ deferProjection: true,
1761
+ spans: true,
1762
+ ),
1763
+ controllerLawHash: domainHash,
1764
+ conflictSnapshots: conflictSnapshots,
1765
+ birthCertificate: birthCertificate,
1737
1766
  ),
1738
- authoring: nomos_protocol.AuthoringOptions(
1739
- actor: actor ?? '',
1740
- // Ordinary generated calls select a peer-runtime credential
1741
- // capability below. An explicit secret is a temporary operator
1742
- // compatibility lane and remains client-supplied until removed.
1743
- authorSecret: explicitSecret == null
1744
- ? const <int>[]
1745
- : base64Decode(explicitSecret),
1746
- deferProjection: true,
1747
- spans: true,
1748
- ),
1749
- controllerLawHash: domainHash,
1750
- conflictSnapshots: conflictSnapshots,
1751
- birthCertificate: birthCertificate,
1752
1767
  ),
1753
- ),
1754
- credentialUse: credentialUse,
1755
- );
1768
+ credentialUse: credentialUse,
1769
+ );
1770
+ } finally {
1771
+ plane._writeObservers.remove(requestId);
1772
+ observer?.call(const NomosWriteProgress(NomosWritePhase.running));
1773
+ }
1756
1774
  }
1757
1775
 
1758
1776
  var response = await attempt();
@@ -5995,7 +5995,7 @@ var CustodyBreakPolicySchema = /* @__PURE__ */ enumDesc(file_nomos_runtime_v2_re
5995
5995
  var CustodyBreakPolicy = /* @__PURE__ */ tsEnum(CustodyBreakPolicySchema);
5996
5996
 
5997
5997
  // ../../adapter/proto/nomos/runtime/v2/observability_desk_pb.js
5998
- var file_nomos_runtime_v2_observability_desk = /* @__PURE__ */ fileDesc("Cilub21vcy9ydW50aW1lL3YyL29ic2VydmFiaWxpdHlfZGVzay5wcm90bxIQbm9tb3MucnVudGltZS52MiJCCgxUcmFjZUNvbnRleHQSEAoIdHJhY2VfaWQYASABKAwSDwoHc3Bhbl9pZBgCIAEoDBIPCgdzYW1wbGVkGAMgASgIIp0CCg5SdW50aW1lRmFpbHVyZRIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSEQoJY29tcG9uZW50GAMgASgJEhYKCXdvcmtzcGFjZRgEIAEoCUgAiAEBEhMKBmRvbWFpbhgFIAEoCUgBiAEBEhYKCWRpcmVjdGl2ZRgGIAEoCUgCiAEBEjkKCmF0dHJpYnV0ZXMYByADKAsyJS5ub21vcy5ydW50aW1lLnYyLkRpYWdub3N0aWNBdHRyaWJ1dGUSMgoEa2luZBgIIAEoDjIkLm5vbW9zLnJ1bnRpbWUudjIuUnVudGltZUZhaWx1cmVLaW5kQgwKCl93b3Jrc3BhY2VCCQoHX2RvbWFpbkIMCgpfZGlyZWN0aXZlIr4BChJSdW50aW1lVW5hdmFpbGFibGUSOgoGcmVhc29uGAEgASgOMioubm9tb3MucnVudGltZS52Mi5SdW50aW1lVW5hdmFpbGFibGVSZWFzb24SEQoJd29ya3NwYWNlGAIgASgJEhEKCXJldHJ5YWJsZRgDIAEoCBIfChJyZXRyeV9hZnRlcl9taWxsaXMYBCABKA1IAIgBARIOCgZkZXRhaWwYBSABKAlCFQoTX3JldHJ5X2FmdGVyX21pbGxpcyJ+Cg5SdW50aW1lUmVmdXNhbBI2CgZyZWFzb24YASABKA4yJi5ub21vcy5ydW50aW1lLnYyLlJ1bnRpbWVSZWZ1c2FsUmVhc29uEhEKCXdvcmtzcGFjZRgCIAEoCRIRCglyZXRyeWFibGUYAyABKAgSDgoGZGV0YWlsGAQgASgJIqUBChNEaWFnbm9zdGljQXR0cmlidXRlEgwKBG5hbWUYASABKAkSFgoMc3RyaW5nX3ZhbHVlGAIgASgJSAASFwoNaW50ZWdlcl92YWx1ZRgDIAEoEkgAEhYKDGRvdWJsZV92YWx1ZRgEIAEoAUgAEhcKDWJvb2xlYW5fdmFsdWUYBSABKAhIABIVCgtieXRlc192YWx1ZRgGIAEoDEgAQgcKBXZhbHVlIuMKCgxSdW50aW1lRXZlbnQSEAoIc2VxdWVuY2UYASABKAQSFAoHc2Vzc2lvbhgCIAEoCUgBiAEBEi8KBXJlYWR5GAogASgLMh4ubm9tb3MucnVudGltZS52Mi5SdW50aW1lUmVhZHlIABI3Cg13YXRjaF9jaGFuZ2VkGAsgASgLMh4ubm9tb3MucnVudGltZS52Mi5XYXRjaENoYW5nZWRIABI5Cg5zeW5jX2NvbXBsZXRlZBgMIAEoCzIfLm5vbW9zLnJ1bnRpbWUudjIuU3luY0NvbXBsZXRlZEgAEjUKDGxpbmtfY2hhbmdlZBgNIAEoCzIdLm5vbW9zLnJ1bnRpbWUudjIuTGlua0NoYW5nZWRIABI5Cg5zaWduYWxfY2hhbmdlZBgOIAEoCzIfLm5vbW9zLnJ1bnRpbWUudjIuU2lnbmFsQ2hhbmdlZEgAEjsKD2N1c3RvZHlfY2hhbmdlZBgPIAEoCzIgLm5vbW9zLnJ1bnRpbWUudjIuQ3VzdG9keUNoYW5nZWRIABJCChNsb2NhbF93b3JrX3JlamVjdGVkGBAgASgLMiMubm9tb3MucnVudGltZS52Mi5Mb2NhbFdvcmtSZWplY3RlZEgAEjMKC2xhd19jaGFuZ2VkGBEgASgLMhwubm9tb3MucnVudGltZS52Mi5MYXdDaGFuZ2VkSAASQwoPa2VybmVsX3Byb2dyZXNzGBIgASgLMigubm9tb3MucnVudGltZS52Mi5LZXJuZWxQcm9ncmVzc09ic2VydmVkSAASLQoEc3BhbhgTIAEoCzIdLm5vbW9zLnJ1bnRpbWUudjIuUnVudGltZVNwYW5IABI/ChF0cmFuc2Zlcl9wcm9ncmVzcxgUIAEoCzIiLm5vbW9zLnJ1bnRpbWUudjIuVHJhbnNmZXJQcm9ncmVzc0gAEkkKFG1haW50ZW5hbmNlX3Byb2dyZXNzGBUgASgLMikubm9tb3MucnVudGltZS52Mi5SZWFkTWFpbnRlbmFuY2VQcm9ncmVzc0gAEjsKDXNvY2tldF9lZmZlY3QYFiABKAsyIi5ub21vcy5ydW50aW1lLnYyLlBlZXJTb2NrZXRFZmZlY3RIABJUChxjcmVkZW50aWFsX3JlZnJlc2hfcmVxdWVzdGVkGBcgASgLMiwubm9tb3MucnVudGltZS52Mi5DcmVkZW50aWFsUmVmcmVzaFJlcXVlc3RlZEgAElIKG3JlY2VpdmluZ19ncm91cF9jb252ZXJnZW5jZRgYIAEoCzIrLm5vbW9zLnJ1bnRpbWUudjIuUmVjZWl2aW5nR3JvdXBDb252ZXJnZW5jZUgAEkoKF21vZGVsX2Fkb3B0aW9uX3Byb2dyZXNzGBkgASgLMicubm9tb3MucnVudGltZS52Mi5Nb2RlbEFkb3B0aW9uUHJvZ3Jlc3NIABI7Cg9mb2xsb3dzX3N1bW1hcnkYGiABKAsyIC5ub21vcy5ydW50aW1lLnYyLkZvbGxvd3NTdW1tYXJ5SAASRAoUbG9jYWxfY29weV9wcmVzZXJ2ZWQYGyABKAsyJC5ub21vcy5ydW50aW1lLnYyLlByZXNlcnZlZExvY2FsQ29weUgAEk4KGWZvcmVpZ25fcGxhY2VtZW50X2lnbm9yZWQYHCABKAsyKS5ub21vcy5ydW50aW1lLnYyLkZvcmVpZ25QbGFjZW1lbnRJZ25vcmVkSAASQwoTYmluZGluZ19xdWFyYW50aW5lZBgdIAEoCzIkLm5vbW9zLnJ1bnRpbWUudjIuUXVhcmFudGluZWRCaW5kaW5nSABCBwoFZXZlbnRCCgoIX3Nlc3Npb24ivAEKDkZvbGxvd3NTdW1tYXJ5EhAKCGZvbGxvd2VkGAEgASgNEg8KB3NldHRsZWQYAiABKA0SDAoEbGl2ZRgDIAEoDRI3Cgp3b3Jrc3BhY2VzGAQgAygLMiMubm9tb3MucnVudGltZS52Mi5Gb2xsb3dPYnNlcnZhdGlvbhIkChdvYnNlcnZlZF9hdF91bml4X21pbGxpcxgFIAEoBEgAiAEBQhoKGF9vYnNlcnZlZF9hdF91bml4X21pbGxpcyLRAgoRRm9sbG93T2JzZXJ2YXRpb24SEQoJd29ya3NwYWNlGAEgASgJEjcKBXBoYXNlGAIgASgOMigubm9tb3MucnVudGltZS52Mi5Gb2xsb3dPYnNlcnZhdGlvblBoYXNlEgwKBGxpdmUYAyABKAgSEwoGZGV0YWlsGAQgASgJSACIAQESIQoUcmV0cnlfYXRfdW5peF9taWxsaXMYBSABKARIAYgBARIfChJkdWVfYXRfdW5peF9taWxsaXMYBiABKARIAogBARIUCgdhdHRlbXB0GAcgASgNSAOIAQESGgoNYWN0aXZlX2VmZmVjdBgIIAEoCUgEiAEBQgkKB19kZXRhaWxCFwoVX3JldHJ5X2F0X3VuaXhfbWlsbGlzQhUKE19kdWVfYXRfdW5peF9taWxsaXNCCgoIX2F0dGVtcHRCEAoOX2FjdGl2ZV9lZmZlY3QiVgoaQ3JlZGVudGlhbFJlZnJlc2hSZXF1ZXN0ZWQSOAoHcGVuZGluZxgBIAEoCzInLm5vbW9zLnJ1bnRpbWUudjIuQ29ycmVzcG9uZGVuY2VQZW5kaW5nIj4KDFJ1bnRpbWVSZWFkeRIXCg9ydW50aW1lX3ZlcnNpb24YASABKAkSFQoNa2VybmVsX3NoYTI1NhgCIAEoDCL8AQoMV2F0Y2hDaGFuZ2VkEhcKD3N1YnNjcmlwdGlvbl9pZBgBIAEoCRITCgtyZXN1bHRfdXNkYRgCIAEoDBI2CgdmYWlsdXJlGAMgASgLMiAubm9tb3MucnVudGltZS52Mi5SdW50aW1lRmFpbHVyZUgAiAEBEjUKEmFwcGxpY2F0aW9uX3Jlc3VsdBgEIAEoCzIULmdvb2dsZS5wcm90b2J1Zi5BbnlIAYgBARIaCg1pbnRlZ2VyX3ZhbHVlGAUgASgDSAKIAQFCCgoIX2ZhaWx1cmVCFQoTX2FwcGxpY2F0aW9uX3Jlc3VsdEIQCg5faW50ZWdlcl92YWx1ZSKcAQoNU3luY0NvbXBsZXRlZBIOCgZyZWFzb24YASABKAkSNwoGcmVzdWx0GAIgASgLMicubm9tb3MucnVudGltZS52Mi5TeW5jV29ya3NwYWNlUmVzcG9uc2USNgoHZmFpbHVyZRgDIAEoCzIgLm5vbW9zLnJ1bnRpbWUudjIuUnVudGltZUZhaWx1cmVIAIgBAUIKCghfZmFpbHVyZSI7CgtMaW5rQ2hhbmdlZBIsCgZzdGF0dXMYASABKA4yHC5ub21vcy5ydW50aW1lLnYyLkxpbmtTdGF0dXMiOAoNU2lnbmFsQ2hhbmdlZBIOCgZkb21haW4YASABKAkSFwoPcHJvamVjdGlvbl91c2RhGAIgASgMIlAKCkxhd0NoYW5nZWQSDgoGZG9tYWluGAEgASgJEhYKDnBhY2thZ2Vfc2hhMjU2GAIgASgJEhoKEmNvbXBhdGliaWxpdHlfdXNkYRgDIAEoDCLdAQoLUnVudGltZVNwYW4SEAoIdHJhY2VfaWQYASABKAwSDwoHc3Bhbl9pZBgCIAEoDBIbCg5wYXJlbnRfc3Bhbl9pZBgDIAEoDEgAiAEBEgwKBG5hbWUYBCABKAkSGgoSc3RhcnRlZF91bml4X25hbm9zGAUgASgEEhYKDmR1cmF0aW9uX25hbm9zGAYgASgEEjkKCmF0dHJpYnV0ZXMYByADKAsyJS5ub21vcy5ydW50aW1lLnYyLkRpYWdub3N0aWNBdHRyaWJ1dGVCEQoPX3BhcmVudF9zcGFuX2lkIuoBChZLZXJuZWxQcm9ncmVzc09ic2VydmVkEjkKCHNuYXBzaG90GAEgASgLMicubm9tb3Mua2VybmVsLnYxLktlcm5lbFByb2dyZXNzU25hcHNob3QSJAocaG9zdF9vYnNlcnZlZF9lbGFwc2VkX21pY3JvcxgCIAEoBBIqCiJob3N0X29ic2VydmVkX3BoYXNlX2VsYXBzZWRfbWljcm9zGAMgASgEEh4KFnByb2Nlc3NfcmVzaWRlbnRfYnl0ZXMYBCABKAQSIwobcHJvY2Vzc19wZWFrX3Jlc2lkZW50X2J5dGVzGAUgASgEIoIBChBUcmFuc2ZlclByb2dyZXNzEhYKDnJlY2VpdmVkX2J5dGVzGAEgASgEEhgKC3RvdGFsX2J5dGVzGAIgASgESACIAQESLAoEa2luZBgDIAEoDjIeLm5vbW9zLnJ1bnRpbWUudjIuVHJhbnNmZXJLaW5kQg4KDF90b3RhbF9ieXRlcyrKAQoSUnVudGltZUZhaWx1cmVLaW5kEiQKIFJVTlRJTUVfRkFJTFVSRV9LSU5EX1VOU1BFQ0lGSUVEEAASIQodUlVOVElNRV9GQUlMVVJFX0tJTkRfSU5URVJOQUwQARIhCh1SVU5USU1FX0ZBSUxVUkVfS0lORF9QUk9UT0NPTBACEiIKHlJVTlRJTUVfRkFJTFVSRV9LSU5EX0lOVEVHUklUWRADEiQKIFJVTlRJTUVfRkFJTFVSRV9LSU5EX1VOU1VQUE9SVEVEEAQqpwMKGFJ1bnRpbWVVbmF2YWlsYWJsZVJlYXNvbhIqCiZSVU5USU1FX1VOQVZBSUxBQkxFX1JFQVNPTl9VTlNQRUNJRklFRBAAEjIKLlJVTlRJTUVfVU5BVkFJTEFCTEVfUkVBU09OX1JFTU9URV9PTkxZX09GRkxJTkUQARIxCi1SVU5USU1FX1VOQVZBSUxBQkxFX1JFQVNPTl9SRU1PVEVfVU5SRUFDSEFCTEUQAhItCilSVU5USU1FX1VOQVZBSUxBQkxFX1JFQVNPTl9SRU1PVEVfVElNRU9VVBADEjYKMlJVTlRJTUVfVU5BVkFJTEFCTEVfUkVBU09OX0xPQ0FMX1JFUExJQ0FfTk9UX1JFQURZEAQSNQoxUlVOVElNRV9VTkFWQUlMQUJMRV9SRUFTT05fQ1JFREVOVElBTF9VTkFWQUlMQUJMRRAFEjAKLFJVTlRJTUVfVU5BVkFJTEFCTEVfUkVBU09OX1dPUktTUEFDRV9PUEVOSU5HEAYSKAokUlVOVElNRV9VTkFWQUlMQUJMRV9SRUFTT05fUEVFUl9CVVNZEAcq8AEKFFJ1bnRpbWVSZWZ1c2FsUmVhc29uEiYKIlJVTlRJTUVfUkVGVVNBTF9SRUFTT05fVU5TUEVDSUZJRUQQABItCilSVU5USU1FX1JFRlVTQUxfUkVBU09OX0lOVkFMSURfQ1JFREVOVElBTBABEigKJFJVTlRJTUVfUkVGVVNBTF9SRUFTT05fQUNDRVNTX0RFTklFRBACEisKJ1JVTlRJTUVfUkVGVVNBTF9SRUFTT05fUExBQ0VNRU5UX1BPTElDWRADEioKJlJVTlRJTUVfUkVGVVNBTF9SRUFTT05fU0VTU0lPTl9VTkJPVU5EEAQqlQQKFkZvbGxvd09ic2VydmF0aW9uUGhhc2USKAokRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX1VOU1BFQ0lGSUVEEAASJAogRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX0RFU0lSRUQQARIgChxGT0xMT1dfT0JTRVJWQVRJT05fUEhBU0VfRFVFEAISJAogRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX1JVTk5JTkcQAxIkCiBGT0xMT1dfT0JTRVJWQVRJT05fUEhBU0VfU0VUVExFRBAEEjAKLEZPTExPV19PQlNFUlZBVElPTl9QSEFTRV9BV0FJVElOR19ERVBFTkRFTkNZEAUSJAogRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX09GRkxJTkUQBhIvCitGT0xMT1dfT0JTRVJWQVRJT05fUEhBU0VfSU5WQUxJRF9DUkVERU5USUFMEAcSJAogRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX0JBQ0tPRkYQCBIsCihGT0xMT1dfT0JTRVJWQVRJT05fUEhBU0VfUkVCT1JOX1JFTU9URUxZEAkSLAooRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX1JFUExBQ0lOR19MT0NBTBAKEjIKLkZPTExPV19PQlNFUlZBVElPTl9QSEFTRV9MT0NBTF9DT1BZX1VOUkVBREFCTEUQCypXCgpMaW5rU3RhdHVzEhsKF0xJTktfU1RBVFVTX1VOU1BFQ0lGSUVEEAASFAoQTElOS19TVEFUVVNfT1BFThABEhYKEkxJTktfU1RBVFVTX0NMT1NFRBACKnYKDFRyYW5zZmVyS2luZBIdChlUUkFOU0ZFUl9LSU5EX1VOU1BFQ0lGSUVEEAASFwoTVFJBTlNGRVJfS0lORF9ERUxUQRABEhYKElRSQU5TRkVSX0tJTkRfRlVMTBACEhYKElRSQU5TRkVSX0tJTkRfUFVTSBADYgZwcm90bzM", [file_google_protobuf_any, file_nomos_kernel_v1_progress, file_nomos_runtime_v2_correspondence_desk, file_nomos_runtime_v2_custody_desk, file_nomos_runtime_v2_placement_desk, file_nomos_runtime_v2_maintenance_desk, file_nomos_runtime_v2_model_adoption_desk, file_nomos_runtime_v2_receiving_group_desk, file_nomos_runtime_v2_recovery_desk]);
5998
+ var file_nomos_runtime_v2_observability_desk = /* @__PURE__ */ fileDesc("Cilub21vcy9ydW50aW1lL3YyL29ic2VydmFiaWxpdHlfZGVzay5wcm90bxIQbm9tb3MucnVudGltZS52MiJCCgxUcmFjZUNvbnRleHQSEAoIdHJhY2VfaWQYASABKAwSDwoHc3Bhbl9pZBgCIAEoDBIPCgdzYW1wbGVkGAMgASgIIp0CCg5SdW50aW1lRmFpbHVyZRIMCgRjb2RlGAEgASgJEg8KB21lc3NhZ2UYAiABKAkSEQoJY29tcG9uZW50GAMgASgJEhYKCXdvcmtzcGFjZRgEIAEoCUgAiAEBEhMKBmRvbWFpbhgFIAEoCUgBiAEBEhYKCWRpcmVjdGl2ZRgGIAEoCUgCiAEBEjkKCmF0dHJpYnV0ZXMYByADKAsyJS5ub21vcy5ydW50aW1lLnYyLkRpYWdub3N0aWNBdHRyaWJ1dGUSMgoEa2luZBgIIAEoDjIkLm5vbW9zLnJ1bnRpbWUudjIuUnVudGltZUZhaWx1cmVLaW5kQgwKCl93b3Jrc3BhY2VCCQoHX2RvbWFpbkIMCgpfZGlyZWN0aXZlIr4BChJSdW50aW1lVW5hdmFpbGFibGUSOgoGcmVhc29uGAEgASgOMioubm9tb3MucnVudGltZS52Mi5SdW50aW1lVW5hdmFpbGFibGVSZWFzb24SEQoJd29ya3NwYWNlGAIgASgJEhEKCXJldHJ5YWJsZRgDIAEoCBIfChJyZXRyeV9hZnRlcl9taWxsaXMYBCABKA1IAIgBARIOCgZkZXRhaWwYBSABKAlCFQoTX3JldHJ5X2FmdGVyX21pbGxpcyJ+Cg5SdW50aW1lUmVmdXNhbBI2CgZyZWFzb24YASABKA4yJi5ub21vcy5ydW50aW1lLnYyLlJ1bnRpbWVSZWZ1c2FsUmVhc29uEhEKCXdvcmtzcGFjZRgCIAEoCRIRCglyZXRyeWFibGUYAyABKAgSDgoGZGV0YWlsGAQgASgJIqUBChNEaWFnbm9zdGljQXR0cmlidXRlEgwKBG5hbWUYASABKAkSFgoMc3RyaW5nX3ZhbHVlGAIgASgJSAASFwoNaW50ZWdlcl92YWx1ZRgDIAEoEkgAEhYKDGRvdWJsZV92YWx1ZRgEIAEoAUgAEhcKDWJvb2xlYW5fdmFsdWUYBSABKAhIABIVCgtieXRlc192YWx1ZRgGIAEoDEgAQgcKBXZhbHVlIqQLCgxSdW50aW1lRXZlbnQSEAoIc2VxdWVuY2UYASABKAQSFAoHc2Vzc2lvbhgCIAEoCUgBiAEBEi8KBXJlYWR5GAogASgLMh4ubm9tb3MucnVudGltZS52Mi5SdW50aW1lUmVhZHlIABI3Cg13YXRjaF9jaGFuZ2VkGAsgASgLMh4ubm9tb3MucnVudGltZS52Mi5XYXRjaENoYW5nZWRIABI5Cg5zeW5jX2NvbXBsZXRlZBgMIAEoCzIfLm5vbW9zLnJ1bnRpbWUudjIuU3luY0NvbXBsZXRlZEgAEjUKDGxpbmtfY2hhbmdlZBgNIAEoCzIdLm5vbW9zLnJ1bnRpbWUudjIuTGlua0NoYW5nZWRIABI5Cg5zaWduYWxfY2hhbmdlZBgOIAEoCzIfLm5vbW9zLnJ1bnRpbWUudjIuU2lnbmFsQ2hhbmdlZEgAEjsKD2N1c3RvZHlfY2hhbmdlZBgPIAEoCzIgLm5vbW9zLnJ1bnRpbWUudjIuQ3VzdG9keUNoYW5nZWRIABJCChNsb2NhbF93b3JrX3JlamVjdGVkGBAgASgLMiMubm9tb3MucnVudGltZS52Mi5Mb2NhbFdvcmtSZWplY3RlZEgAEjMKC2xhd19jaGFuZ2VkGBEgASgLMhwubm9tb3MucnVudGltZS52Mi5MYXdDaGFuZ2VkSAASQwoPa2VybmVsX3Byb2dyZXNzGBIgASgLMigubm9tb3MucnVudGltZS52Mi5LZXJuZWxQcm9ncmVzc09ic2VydmVkSAASLQoEc3BhbhgTIAEoCzIdLm5vbW9zLnJ1bnRpbWUudjIuUnVudGltZVNwYW5IABI/ChF0cmFuc2Zlcl9wcm9ncmVzcxgUIAEoCzIiLm5vbW9zLnJ1bnRpbWUudjIuVHJhbnNmZXJQcm9ncmVzc0gAEkkKFG1haW50ZW5hbmNlX3Byb2dyZXNzGBUgASgLMikubm9tb3MucnVudGltZS52Mi5SZWFkTWFpbnRlbmFuY2VQcm9ncmVzc0gAEjsKDXNvY2tldF9lZmZlY3QYFiABKAsyIi5ub21vcy5ydW50aW1lLnYyLlBlZXJTb2NrZXRFZmZlY3RIABJUChxjcmVkZW50aWFsX3JlZnJlc2hfcmVxdWVzdGVkGBcgASgLMiwubm9tb3MucnVudGltZS52Mi5DcmVkZW50aWFsUmVmcmVzaFJlcXVlc3RlZEgAElIKG3JlY2VpdmluZ19ncm91cF9jb252ZXJnZW5jZRgYIAEoCzIrLm5vbW9zLnJ1bnRpbWUudjIuUmVjZWl2aW5nR3JvdXBDb252ZXJnZW5jZUgAEkoKF21vZGVsX2Fkb3B0aW9uX3Byb2dyZXNzGBkgASgLMicubm9tb3MucnVudGltZS52Mi5Nb2RlbEFkb3B0aW9uUHJvZ3Jlc3NIABI7Cg9mb2xsb3dzX3N1bW1hcnkYGiABKAsyIC5ub21vcy5ydW50aW1lLnYyLkZvbGxvd3NTdW1tYXJ5SAASRAoUbG9jYWxfY29weV9wcmVzZXJ2ZWQYGyABKAsyJC5ub21vcy5ydW50aW1lLnYyLlByZXNlcnZlZExvY2FsQ29weUgAEk4KGWZvcmVpZ25fcGxhY2VtZW50X2lnbm9yZWQYHCABKAsyKS5ub21vcy5ydW50aW1lLnYyLkZvcmVpZ25QbGFjZW1lbnRJZ25vcmVkSAASQwoTYmluZGluZ19xdWFyYW50aW5lZBgdIAEoCzIkLm5vbW9zLnJ1bnRpbWUudjIuUXVhcmFudGluZWRCaW5kaW5nSAASPwoRd3JpdGVfcHJlcGFyYXRpb24YHiABKAsyIi5ub21vcy5ydW50aW1lLnYyLldyaXRlUHJlcGFyYXRpb25IAEIHCgVldmVudEIKCghfc2Vzc2lvbiK8AQoORm9sbG93c1N1bW1hcnkSEAoIZm9sbG93ZWQYASABKA0SDwoHc2V0dGxlZBgCIAEoDRIMCgRsaXZlGAMgASgNEjcKCndvcmtzcGFjZXMYBCADKAsyIy5ub21vcy5ydW50aW1lLnYyLkZvbGxvd09ic2VydmF0aW9uEiQKF29ic2VydmVkX2F0X3VuaXhfbWlsbGlzGAUgASgESACIAQFCGgoYX29ic2VydmVkX2F0X3VuaXhfbWlsbGlzItECChFGb2xsb3dPYnNlcnZhdGlvbhIRCgl3b3Jrc3BhY2UYASABKAkSNwoFcGhhc2UYAiABKA4yKC5ub21vcy5ydW50aW1lLnYyLkZvbGxvd09ic2VydmF0aW9uUGhhc2USDAoEbGl2ZRgDIAEoCBITCgZkZXRhaWwYBCABKAlIAIgBARIhChRyZXRyeV9hdF91bml4X21pbGxpcxgFIAEoBEgBiAEBEh8KEmR1ZV9hdF91bml4X21pbGxpcxgGIAEoBEgCiAEBEhQKB2F0dGVtcHQYByABKA1IA4gBARIaCg1hY3RpdmVfZWZmZWN0GAggASgJSASIAQFCCQoHX2RldGFpbEIXChVfcmV0cnlfYXRfdW5peF9taWxsaXNCFQoTX2R1ZV9hdF91bml4X21pbGxpc0IKCghfYXR0ZW1wdEIQCg5fYWN0aXZlX2VmZmVjdCJWChpDcmVkZW50aWFsUmVmcmVzaFJlcXVlc3RlZBI4CgdwZW5kaW5nGAEgASgLMicubm9tb3MucnVudGltZS52Mi5Db3JyZXNwb25kZW5jZVBlbmRpbmciPgoMUnVudGltZVJlYWR5EhcKD3J1bnRpbWVfdmVyc2lvbhgBIAEoCRIVCg1rZXJuZWxfc2hhMjU2GAIgASgMIvwBCgxXYXRjaENoYW5nZWQSFwoPc3Vic2NyaXB0aW9uX2lkGAEgASgJEhMKC3Jlc3VsdF91c2RhGAIgASgMEjYKB2ZhaWx1cmUYAyABKAsyIC5ub21vcy5ydW50aW1lLnYyLlJ1bnRpbWVGYWlsdXJlSACIAQESNQoSYXBwbGljYXRpb25fcmVzdWx0GAQgASgLMhQuZ29vZ2xlLnByb3RvYnVmLkFueUgBiAEBEhoKDWludGVnZXJfdmFsdWUYBSABKANIAogBAUIKCghfZmFpbHVyZUIVChNfYXBwbGljYXRpb25fcmVzdWx0QhAKDl9pbnRlZ2VyX3ZhbHVlIpwBCg1TeW5jQ29tcGxldGVkEg4KBnJlYXNvbhgBIAEoCRI3CgZyZXN1bHQYAiABKAsyJy5ub21vcy5ydW50aW1lLnYyLlN5bmNXb3Jrc3BhY2VSZXNwb25zZRI2CgdmYWlsdXJlGAMgASgLMiAubm9tb3MucnVudGltZS52Mi5SdW50aW1lRmFpbHVyZUgAiAEBQgoKCF9mYWlsdXJlIjsKC0xpbmtDaGFuZ2VkEiwKBnN0YXR1cxgBIAEoDjIcLm5vbW9zLnJ1bnRpbWUudjIuTGlua1N0YXR1cyI4Cg1TaWduYWxDaGFuZ2VkEg4KBmRvbWFpbhgBIAEoCRIXCg9wcm9qZWN0aW9uX3VzZGEYAiABKAwiUAoKTGF3Q2hhbmdlZBIOCgZkb21haW4YASABKAkSFgoOcGFja2FnZV9zaGEyNTYYAiABKAkSGgoSY29tcGF0aWJpbGl0eV91c2RhGAMgASgMIt0BCgtSdW50aW1lU3BhbhIQCgh0cmFjZV9pZBgBIAEoDBIPCgdzcGFuX2lkGAIgASgMEhsKDnBhcmVudF9zcGFuX2lkGAMgASgMSACIAQESDAoEbmFtZRgEIAEoCRIaChJzdGFydGVkX3VuaXhfbmFub3MYBSABKAQSFgoOZHVyYXRpb25fbmFub3MYBiABKAQSOQoKYXR0cmlidXRlcxgHIAMoCzIlLm5vbW9zLnJ1bnRpbWUudjIuRGlhZ25vc3RpY0F0dHJpYnV0ZUIRCg9fcGFyZW50X3NwYW5faWQi6gEKFktlcm5lbFByb2dyZXNzT2JzZXJ2ZWQSOQoIc25hcHNob3QYASABKAsyJy5ub21vcy5rZXJuZWwudjEuS2VybmVsUHJvZ3Jlc3NTbmFwc2hvdBIkChxob3N0X29ic2VydmVkX2VsYXBzZWRfbWljcm9zGAIgASgEEioKImhvc3Rfb2JzZXJ2ZWRfcGhhc2VfZWxhcHNlZF9taWNyb3MYAyABKAQSHgoWcHJvY2Vzc19yZXNpZGVudF9ieXRlcxgEIAEoBBIjChtwcm9jZXNzX3BlYWtfcmVzaWRlbnRfYnl0ZXMYBSABKAQiggEKEFRyYW5zZmVyUHJvZ3Jlc3MSFgoOcmVjZWl2ZWRfYnl0ZXMYASABKAQSGAoLdG90YWxfYnl0ZXMYAiABKARIAIgBARIsCgRraW5kGAMgASgOMh4ubm9tb3MucnVudGltZS52Mi5UcmFuc2ZlcktpbmRCDgoMX3RvdGFsX2J5dGVzIjkKEFdyaXRlUHJlcGFyYXRpb24SEgoKcmVxdWVzdF9pZBgBIAEoCRIRCgl3b3Jrc3BhY2UYAiABKAkqygEKElJ1bnRpbWVGYWlsdXJlS2luZBIkCiBSVU5USU1FX0ZBSUxVUkVfS0lORF9VTlNQRUNJRklFRBAAEiEKHVJVTlRJTUVfRkFJTFVSRV9LSU5EX0lOVEVSTkFMEAESIQodUlVOVElNRV9GQUlMVVJFX0tJTkRfUFJPVE9DT0wQAhIiCh5SVU5USU1FX0ZBSUxVUkVfS0lORF9JTlRFR1JJVFkQAxIkCiBSVU5USU1FX0ZBSUxVUkVfS0lORF9VTlNVUFBPUlRFRBAEKqcDChhSdW50aW1lVW5hdmFpbGFibGVSZWFzb24SKgomUlVOVElNRV9VTkFWQUlMQUJMRV9SRUFTT05fVU5TUEVDSUZJRUQQABIyCi5SVU5USU1FX1VOQVZBSUxBQkxFX1JFQVNPTl9SRU1PVEVfT05MWV9PRkZMSU5FEAESMQotUlVOVElNRV9VTkFWQUlMQUJMRV9SRUFTT05fUkVNT1RFX1VOUkVBQ0hBQkxFEAISLQopUlVOVElNRV9VTkFWQUlMQUJMRV9SRUFTT05fUkVNT1RFX1RJTUVPVVQQAxI2CjJSVU5USU1FX1VOQVZBSUxBQkxFX1JFQVNPTl9MT0NBTF9SRVBMSUNBX05PVF9SRUFEWRAEEjUKMVJVTlRJTUVfVU5BVkFJTEFCTEVfUkVBU09OX0NSRURFTlRJQUxfVU5BVkFJTEFCTEUQBRIwCixSVU5USU1FX1VOQVZBSUxBQkxFX1JFQVNPTl9XT1JLU1BBQ0VfT1BFTklORxAGEigKJFJVTlRJTUVfVU5BVkFJTEFCTEVfUkVBU09OX1BFRVJfQlVTWRAHKvABChRSdW50aW1lUmVmdXNhbFJlYXNvbhImCiJSVU5USU1FX1JFRlVTQUxfUkVBU09OX1VOU1BFQ0lGSUVEEAASLQopUlVOVElNRV9SRUZVU0FMX1JFQVNPTl9JTlZBTElEX0NSRURFTlRJQUwQARIoCiRSVU5USU1FX1JFRlVTQUxfUkVBU09OX0FDQ0VTU19ERU5JRUQQAhIrCidSVU5USU1FX1JFRlVTQUxfUkVBU09OX1BMQUNFTUVOVF9QT0xJQ1kQAxIqCiZSVU5USU1FX1JFRlVTQUxfUkVBU09OX1NFU1NJT05fVU5CT1VORBAEKpUEChZGb2xsb3dPYnNlcnZhdGlvblBoYXNlEigKJEZPTExPV19PQlNFUlZBVElPTl9QSEFTRV9VTlNQRUNJRklFRBAAEiQKIEZPTExPV19PQlNFUlZBVElPTl9QSEFTRV9ERVNJUkVEEAESIAocRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX0RVRRACEiQKIEZPTExPV19PQlNFUlZBVElPTl9QSEFTRV9SVU5OSU5HEAMSJAogRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX1NFVFRMRUQQBBIwCixGT0xMT1dfT0JTRVJWQVRJT05fUEhBU0VfQVdBSVRJTkdfREVQRU5ERU5DWRAFEiQKIEZPTExPV19PQlNFUlZBVElPTl9QSEFTRV9PRkZMSU5FEAYSLworRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX0lOVkFMSURfQ1JFREVOVElBTBAHEiQKIEZPTExPV19PQlNFUlZBVElPTl9QSEFTRV9CQUNLT0ZGEAgSLAooRk9MTE9XX09CU0VSVkFUSU9OX1BIQVNFX1JFQk9STl9SRU1PVEVMWRAJEiwKKEZPTExPV19PQlNFUlZBVElPTl9QSEFTRV9SRVBMQUNJTkdfTE9DQUwQChIyCi5GT0xMT1dfT0JTRVJWQVRJT05fUEhBU0VfTE9DQUxfQ09QWV9VTlJFQURBQkxFEAsqVwoKTGlua1N0YXR1cxIbChdMSU5LX1NUQVRVU19VTlNQRUNJRklFRBAAEhQKEExJTktfU1RBVFVTX09QRU4QARIWChJMSU5LX1NUQVRVU19DTE9TRUQQAip2CgxUcmFuc2ZlcktpbmQSHQoZVFJBTlNGRVJfS0lORF9VTlNQRUNJRklFRBAAEhcKE1RSQU5TRkVSX0tJTkRfREVMVEEQARIWChJUUkFOU0ZFUl9LSU5EX0ZVTEwQAhIWChJUUkFOU0ZFUl9LSU5EX1BVU0gQA2IGcHJvdG8z", [file_google_protobuf_any, file_nomos_kernel_v1_progress, file_nomos_runtime_v2_correspondence_desk, file_nomos_runtime_v2_custody_desk, file_nomos_runtime_v2_placement_desk, file_nomos_runtime_v2_maintenance_desk, file_nomos_runtime_v2_model_adoption_desk, file_nomos_runtime_v2_receiving_group_desk, file_nomos_runtime_v2_recovery_desk]);
5999
5999
  var RuntimeFailureSchema = /* @__PURE__ */ messageDesc(file_nomos_runtime_v2_observability_desk, 1);
6000
6000
  var RuntimeUnavailableSchema = /* @__PURE__ */ messageDesc(file_nomos_runtime_v2_observability_desk, 2);
6001
6001
  var RuntimeRefusalSchema = /* @__PURE__ */ messageDesc(file_nomos_runtime_v2_observability_desk, 3);
@@ -6009,6 +6009,7 @@ var WatchChangedSchema = /* @__PURE__ */ messageDesc(file_nomos_runtime_v2_obser
6009
6009
  var SyncCompletedSchema = /* @__PURE__ */ messageDesc(file_nomos_runtime_v2_observability_desk, 11);
6010
6010
  var LinkChangedSchema = /* @__PURE__ */ messageDesc(file_nomos_runtime_v2_observability_desk, 12);
6011
6011
  var RuntimeSpanSchema = /* @__PURE__ */ messageDesc(file_nomos_runtime_v2_observability_desk, 15);
6012
+ var WritePreparationSchema = /* @__PURE__ */ messageDesc(file_nomos_runtime_v2_observability_desk, 18);
6012
6013
  var RuntimeFailureKindSchema = /* @__PURE__ */ enumDesc(file_nomos_runtime_v2_observability_desk, 0);
6013
6014
  var RuntimeFailureKind = /* @__PURE__ */ tsEnum(RuntimeFailureKindSchema);
6014
6015
  var RuntimeUnavailableReasonSchema = /* @__PURE__ */ enumDesc(file_nomos_runtime_v2_observability_desk, 1);
@@ -16356,6 +16357,18 @@ var CredentialSessions = class {
16356
16357
  // ../src/runtime/credential_recovery_desk.ts
16357
16358
  init_define_NOMOS_KERNEL_RELEASE();
16358
16359
 
16360
+ // ../src/runtime/credential_failure.ts
16361
+ init_define_NOMOS_KERNEL_RELEASE();
16362
+ function recoverableCredential(failure) {
16363
+ return failure.code === "nomos.auth.token_expired" || failure.code === "nomos.auth.token_missing";
16364
+ }
16365
+ function syncNeedsCredentialRefresh(result, failure) {
16366
+ if (failure !== void 0 && recoverableCredential(failure)) return true;
16367
+ const offer = result?.offerReceipt?.outcome;
16368
+ const pushNeedsRefresh = offer?.case === "failure" ? recoverableCredential(offer.value) : result?.pushError?.status === 401;
16369
+ return pushNeedsRefresh || result?.discoveryError?.status === 401;
16370
+ }
16371
+
16359
16372
  // ../src/runtime/credential_renewal.ts
16360
16373
  init_define_NOMOS_KERNEL_RELEASE();
16361
16374
  var RENEW_BEFORE_MILLIS = 5 * 6e4;
@@ -16621,7 +16634,7 @@ var CredentialRecoveryDesk = class {
16621
16634
  return false;
16622
16635
  }
16623
16636
  observeSync(session, event) {
16624
- if (!syncNeedsCredentialRefresh(event) || this.#tokenless(session)) return;
16637
+ if (!syncNeedsCredentialRefresh(event.result, event.failure) || this.#tokenless(session)) return;
16625
16638
  this.#prune();
16626
16639
  const capability = this.#ports.capabilityFor(session);
16627
16640
  const existing = this.#active.get(capability);
@@ -16823,15 +16836,6 @@ var CredentialRecoveryDesk = class {
16823
16836
  this.#completed.clear();
16824
16837
  }
16825
16838
  };
16826
- function recoverableCredential(failure) {
16827
- return failure.code === "nomos.auth.token_expired" || failure.code === "nomos.auth.token_missing";
16828
- }
16829
- function syncNeedsCredentialRefresh(event) {
16830
- const failures = [event.result?.pushError, event.result?.discoveryError];
16831
- if (failures.some((failure) => failure?.status === 401 || failure?.status === 403)) return true;
16832
- const text = `${event.failure?.message ?? ""} ${failures.map((failure) => failure?.message ?? "").join(" ")}`.toLowerCase();
16833
- return text.includes("unauthenticated") || text.includes("token expired") || text.includes("invalid token");
16834
- }
16835
16839
 
16836
16840
  // ../src/runtime/device_identity_desk.ts
16837
16841
  init_define_NOMOS_KERNEL_RELEASE();
@@ -17750,6 +17754,7 @@ var ReceivingGroupDesk = class {
17750
17754
  #accounts = /* @__PURE__ */ new Map();
17751
17755
  /** D22: device key hash → the workspaces the console reported this key enrolled in (this process's memory, not truth). */
17752
17756
  #consoleEnrolments = /* @__PURE__ */ new Map();
17757
+ #preparingWrites = /* @__PURE__ */ new Map();
17753
17758
  #sequence = 0;
17754
17759
  #internalRequestId = 30000000000n;
17755
17760
  constructor(ports) {
@@ -17789,16 +17794,124 @@ var ReceivingGroupDesk = class {
17789
17794
  if (head === account.observedHead) return;
17790
17795
  this.#tails.set(workspace, { ...account, inFlight: false, attempts: 0, notBefore: 0n });
17791
17796
  }
17792
- /** A refused nominal Home action can wait for framework recovery and retry once.
17793
- * Sealed/pending history is never re-signed, and no child is mistaken for Home. */
17797
+ /** Only a proven non-admission can enter recovery. Console-enrolled children
17798
+ * share the owning application's route, selected by credential capability and
17799
+ * principal, never by workspace-name guessing or an unverified payload. */
17794
17800
  recoverOffer(request, response) {
17795
- if (!this.#ports.call || request.command.case !== "kernelCall" || request.command.value.credentialUse !== CredentialUse.SESSION || request.command.value.request?.operation.case !== "offer" || request.command.value.request.operation.value.intent.case !== "call" || response.outcome.case !== "failure" || !["nomos.authz.signer_enrollment_missing", "nomos.authz.device_key_revoked"].includes(response.outcome.value.code)) return;
17801
+ if (!this.#ports.call || request.command.case !== "kernelCall" || request.command.value.credentialUse !== CredentialUse.SESSION || request.command.value.request?.operation.case !== "offer" || request.command.value.request.operation.value.intent.case !== "call") return;
17802
+ const missing = response.outcome.case === "failure" ? response.outcome.value.code === "nomos.authz.signer_enrollment_missing" : response.outcome.case === "offer" && response.outcome.value.outcome.case === "refused" && response.outcome.value.outcome.value.refusal?.code === KernelRefusalCode.FORGED_PRINCIPAL && response.outcome.value.outcome.value.refusal.details?.fields.actor?.kind.case === "text" && response.outcome.value.outcome.value.refusal.details.fields.actor.kind.value === "unenrolled-signer-key";
17803
+ const revoked = response.outcome.case === "failure" && response.outcome.value.code === "nomos.authz.device_key_revoked";
17804
+ if (!missing && !revoked) return;
17796
17805
  const binding = this.#ports.binding(request.command.value.session);
17797
- const account = binding ? this.#accounts.get(binding.workspace) : void 0;
17798
- if (!account) return;
17806
+ if (!binding) return;
17807
+ const enrolled = [...this.#accounts.values()].find((account2) => {
17808
+ const owner = this.#ports.binding(account2.session);
17809
+ return account2.enrolment && owner?.credentialCapability === binding.credentialCapability && owner.principal === binding.principal;
17810
+ });
17799
17811
  const copy3 = fromBinary(RuntimeRequestSchema, toBinary(RuntimeRequestSchema, request));
17812
+ if (enrolled?.enrolment) {
17813
+ if (revoked) return this.#writeRefused(binding.workspace, "This device's access has been revoked.");
17814
+ return this.#begin(request, `enrolled-write-${++this.#sequence}`, (workId2) => this.#prepareEnrolledWrite(copy3, binding, enrolled.application, enrolled.enrolment, workId2));
17815
+ }
17816
+ const account = this.#accounts.get(binding.workspace);
17817
+ if (!account) return;
17800
17818
  return this.#begin(request, `receiving-group-authoring-${++this.#sequence}`, (workId2) => this.#recoverOffer(copy3, response, account, workId2));
17801
17819
  }
17820
+ #writeRefused(workspace, detail, reason = RuntimeRefusalReason.ACCESS_DENIED) {
17821
+ return { case: "refused", value: create(RuntimeRefusalSchema, {
17822
+ reason,
17823
+ workspace,
17824
+ retryable: false,
17825
+ detail
17826
+ }) };
17827
+ }
17828
+ #writeWaiting(workspace, detail) {
17829
+ return { case: "unavailable", value: create(RuntimeUnavailableSchema, {
17830
+ reason: RuntimeUnavailableReason.CREDENTIAL_UNAVAILABLE,
17831
+ workspace,
17832
+ retryable: true,
17833
+ retryAfterMillis: 1e3,
17834
+ detail
17835
+ }) };
17836
+ }
17837
+ *#prepareEnrolledWrite(request, binding, application, route, workId2) {
17838
+ if (request.command.case !== "kernelCall") throw new Error("write preparation lost its call");
17839
+ const command = request.command.value;
17840
+ const principal = binding.principal;
17841
+ const unavailable = () => this.#writeWaiting(
17842
+ binding.workspace,
17843
+ "This device is still being prepared to save changes. Your change has not been saved. Try again when connected."
17844
+ );
17845
+ this.#ports.emit(binding.session, { case: "writePreparation", value: create(WritePreparationSchema, {
17846
+ requestId: command.request.requestId,
17847
+ workspace: binding.workspace
17848
+ }) });
17849
+ const key = JSON.stringify([binding.credentialCapability, principal, binding.workspace]);
17850
+ const shared = this.#preparingWrites.get(key);
17851
+ const deadline = this.#ports.clock.monotonicNanoseconds() + 30000000000n;
17852
+ let wake = 0;
17853
+ if (shared) {
17854
+ while (!shared.settled) {
17855
+ if (this.#ports.clock.monotonicNanoseconds() >= deadline) return unavailable();
17856
+ const receipt = yield { case: "wake", value: create(PeerWakeEffectSchema, {
17857
+ effectId: `${workId2}:join-${++wake}`,
17858
+ delayMillis: 250
17859
+ }) };
17860
+ if (receipt.outcome.case !== "woken") return unavailable();
17861
+ }
17862
+ if (shared.result) return shared.result;
17863
+ } else {
17864
+ const preparation = { settled: false };
17865
+ this.#preparingWrites.set(key, preparation);
17866
+ try {
17867
+ const secret = this.#ports.authorSecretIfPresent(binding.session);
17868
+ if (!secret) return preparation.result = this.#writeRefused(binding.workspace, "Sign in again to set up this device.", RuntimeRefusalReason.INVALID_CREDENTIAL);
17869
+ const keyHash = sha256Hex(this.#ports.deriveSigningPublicKey(secret));
17870
+ let posture = this.#ports.posture(request, binding.session, principal, secret, false);
17871
+ if (posture.deviceSignerStatus === DeviceSignerStatus.REVOKED) {
17872
+ return preparation.result = this.#writeRefused(binding.workspace, "This device's access has been revoked.");
17873
+ }
17874
+ if (!posture.enrolledDevice || posture.deviceSignerStatus !== DeviceSignerStatus.ACTIVE) {
17875
+ const enrolled = yield* this.#enrolAtConsole(
17876
+ route,
17877
+ binding,
17878
+ `${application}|${principal}`,
17879
+ principal,
17880
+ keyHash,
17881
+ [binding.workspace],
17882
+ `${workId2}:console`
17883
+ );
17884
+ if (enrolled.case === "deferred") return preparation.result = enrolled.credentialRefused ? this.#writeRefused(binding.workspace, "Sign in again to set up this device.", RuntimeRefusalReason.INVALID_CREDENTIAL) : unavailable();
17885
+ if (!(binding.workspace in enrolled.value.heads)) {
17886
+ return preparation.result = this.#writeRefused(binding.workspace, "This device could not obtain access to save changes in this workspace.");
17887
+ }
17888
+ while (true) {
17889
+ posture = this.#ports.posture(request, binding.session, principal, secret, false);
17890
+ if (posture.deviceSignerStatus === DeviceSignerStatus.REVOKED) {
17891
+ return preparation.result = this.#writeRefused(binding.workspace, "This device's access has been revoked.");
17892
+ }
17893
+ if (posture.enrolledDevice && posture.deviceSignerStatus === DeviceSignerStatus.ACTIVE) break;
17894
+ if (this.#ports.clock.monotonicNanoseconds() >= deadline) return preparation.result = unavailable();
17895
+ const receipt = yield { case: "wake", value: create(PeerWakeEffectSchema, {
17896
+ effectId: `${workId2}:observe-${++wake}`,
17897
+ delayMillis: 500
17898
+ }) };
17899
+ if (receipt.outcome.case !== "woken") return preparation.result = unavailable();
17900
+ }
17901
+ }
17902
+ } catch (error) {
17903
+ if (error instanceof ReceivingGroupRefusal) {
17904
+ return preparation.result = this.#writeRefused(binding.workspace, "This device could not obtain access to save changes in this workspace.");
17905
+ }
17906
+ preparation.result = unavailable();
17907
+ throw error;
17908
+ } finally {
17909
+ preparation.settled = true;
17910
+ this.#preparingWrites.delete(key);
17911
+ }
17912
+ }
17913
+ return { case: "retried", request, response: this.#ports.call(request, command) };
17914
+ }
17802
17915
  *#recoverOffer(request, response, account, workId2) {
17803
17916
  const workspace = this.#ports.binding(account.session).workspace;
17804
17917
  const deadline = this.#ports.clock.monotonicNanoseconds() + 60000000000n;
@@ -17866,6 +17979,7 @@ var ReceivingGroupDesk = class {
17866
17979
  this.#opening.clear();
17867
17980
  this.#accounts.clear();
17868
17981
  this.#consoleEnrolments.clear();
17982
+ this.#preparingWrites.clear();
17869
17983
  }
17870
17984
  #begin(request, workId2, procedure) {
17871
17985
  const held = {
@@ -21821,12 +21935,12 @@ var ReplicaFollowController = class {
21821
21935
  this.#desk.transition(result.workspace, { case: ReplicaFollowEventKind.WentOffline, nowUnixMillis });
21822
21936
  return delayUntilDue(this.#desk.get(result.workspace), nowUnixMillis);
21823
21937
  }
21824
- const credentialFailure = [result.pushError, result.discoveryError].find((failure) => failure?.status === 401 || failure?.status === 403);
21825
- if (credentialFailure) {
21938
+ if (syncNeedsCredentialRefresh(result)) {
21939
+ const offer = result.offerReceipt?.outcome;
21826
21940
  const transition = this.#desk.transition(result.workspace, {
21827
21941
  case: ReplicaFollowEventKind.CredentialRejected,
21828
21942
  nowUnixMillis,
21829
- failure: credentialFailure.message || `remote rejected credential with HTTP ${credentialFailure.status}`
21943
+ failure: (offer?.case === "failure" ? offer.value.message : void 0) || result.pushError?.message || result.discoveryError?.message || "remote requires a refreshed credential"
21830
21944
  });
21831
21945
  if (transition.effects.some((effect) => effect.case === ReplicaFollowEffectKind.RefreshCredential)) {
21832
21946
  return 0;
@@ -30152,6 +30266,7 @@ var PeerOffice = class {
30152
30266
  return this.#responses.modelAdoption(request, step);
30153
30267
  }
30154
30268
  #receivingGroupResponse(request, step) {
30269
+ if (step.case === "refused") return this.#response(request, { case: "refused", value: step.value });
30155
30270
  if (step.case === "retried") return this.#birthCustodyResponse(step.request, this.#birthCustody.afterKernelCall(step.request, step.response));
30156
30271
  if (step.case === "pending") return this.#response(request, { case: "correspondencePending", value: step.value });
30157
30272
  if (step.case === "failure") return this.#failure(request, step.code, step.message);
@@ -7,7 +7,6 @@ import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
7
7
  import 'package:nomos_types/nomos_types.dart'
8
8
  show
9
9
  NomosErrorInfo,
10
- NomosRefusalReason,
11
10
  NomosRefused,
12
11
  NomosRuntimeFailure,
13
12
  NomosRuntimeFailureKind,
@@ -46,27 +45,7 @@ final class NmsRuntimeV2Gateway {
46
45
  throw NomosUnavailable.fromRuntime(response.unavailable);
47
46
  }
48
47
  if (response.hasRefused()) {
49
- final refused = response.refused;
50
- throw NomosRefused(
51
- reason: switch (refused.reason) {
52
- nomos_protocol.RuntimeRefusalReason
53
- .RUNTIME_REFUSAL_REASON_INVALID_CREDENTIAL =>
54
- NomosRefusalReason.invalidCredential,
55
- nomos_protocol
56
- .RuntimeRefusalReason.RUNTIME_REFUSAL_REASON_ACCESS_DENIED =>
57
- NomosRefusalReason.accessDenied,
58
- nomos_protocol
59
- .RuntimeRefusalReason.RUNTIME_REFUSAL_REASON_PLACEMENT_POLICY =>
60
- NomosRefusalReason.placementPolicy,
61
- nomos_protocol
62
- .RuntimeRefusalReason.RUNTIME_REFUSAL_REASON_SESSION_UNBOUND =>
63
- NomosRefusalReason.sessionUnbound,
64
- _ => NomosRefusalReason.unknown,
65
- },
66
- workspace: refused.workspace,
67
- retryable: refused.retryable,
68
- detail: refused.detail,
69
- );
48
+ throw NomosRefused.fromRuntime(response.refused);
70
49
  }
71
50
  if (response.hasFailure()) {
72
51
  final failure = response.failure;
@@ -16,7 +16,8 @@ import 'package:nomos_peer_host/nomos_peer_host.dart'
16
16
  NomosProviderCredentialStore,
17
17
  NomosSignIn;
18
18
  import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
19
- import 'package:nomos_types/nomos_types.dart' show NomosUnavailable;
19
+ import 'package:nomos_types/nomos_types.dart'
20
+ show NomosUnavailable, NomosRefused;
20
21
 
21
22
  import 'console_enrolment.dart';
22
23
  import 'console_http_stub.dart' if (dart.library.io) 'console_http_io.dart'
@@ -648,6 +649,9 @@ mixin NomosRuntimeV2KernelCalls
648
649
  if (response.hasUnavailable()) {
649
650
  throw NomosUnavailable.fromRuntime(response.unavailable);
650
651
  }
652
+ if (response.hasRefused()) {
653
+ throw NomosRefused.fromRuntime(response.refused);
654
+ }
651
655
  if (!response.hasKernelCall()) {
652
656
  throw StateError(
653
657
  'Runtime v2 returned ${response.whichOutcome().name} for a kernel call',
@@ -0,0 +1,48 @@
1
+ import 'dart:async';
2
+
3
+ /// Presentation of one application action. Completion is the action's local
4
+ /// result; remote durability remains on its existing operation receipt.
5
+ enum NomosWritePhase { running, preparing, completed, failed }
6
+
7
+ final class NomosWriteProgress {
8
+ const NomosWriteProgress(this.phase);
9
+ final NomosWritePhase phase;
10
+ }
11
+
12
+ /// Optional observation around a generated business action, including any
13
+ /// prerequisite calls it makes. Ordinary unobserved calls recover identically.
14
+ /// This holds no queue, credentials, authority or retry policy. It does not
15
+ /// persist a pending action across process death or cancel an accepted write.
16
+ abstract final class NomosWrite {
17
+ static final Object _observerKey = Object();
18
+
19
+ static Future<T> observe<T>(
20
+ Future<T> Function() action, {
21
+ required void Function(NomosWriteProgress) onProgress,
22
+ }) async {
23
+ void report(NomosWritePhase phase) {
24
+ try {
25
+ onProgress(NomosWriteProgress(phase));
26
+ } catch (_) {
27
+ // Presentation cannot change the operation's outcome.
28
+ }
29
+ }
30
+
31
+ report(NomosWritePhase.running);
32
+ try {
33
+ final result = await runZoned(action, zoneValues: {
34
+ _observerKey: (NomosWriteProgress progress) => report(progress.phase),
35
+ });
36
+ report(NomosWritePhase.completed);
37
+ return result;
38
+ } catch (_) {
39
+ report(NomosWritePhase.failed);
40
+ rethrow;
41
+ }
42
+ }
43
+
44
+ /// Framework projection seam. Capture before dispatch so an event delivered
45
+ /// outside the calling zone still reaches only its own action's observer.
46
+ static void Function(NomosWriteProgress)? captureObserver() =>
47
+ Zone.current[_observerKey] as void Function(NomosWriteProgress)?;
48
+ }
package/dart/pubspec.lock CHANGED
@@ -222,7 +222,7 @@ packages:
222
222
  path: "../dart_types"
223
223
  relative: true
224
224
  source: path
225
- version: "0.11.2"
225
+ version: "0.11.3"
226
226
  package_config:
227
227
  dependency: "direct main"
228
228
  description:
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.66.7
10
+ version: 0.66.9
11
11
 
12
12
  environment:
13
13
  sdk: ^3.5.0
@@ -18,7 +18,7 @@ dependencies:
18
18
  # The pure-data core (wire types, offer codec, NomosReads, schema validation, error
19
19
  # info). nomos_client depends on and re-exports it, so existing imports are unchanged.
20
20
  # For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
21
- nomos_types: ^0.11.2
21
+ nomos_types: ^0.11.3
22
22
  fixnum: ^1.1.1
23
23
  json_schema: ^5.2.2
24
24
  crypto: ^3.0.0
@@ -3,6 +3,7 @@ import 'dart:convert';
3
3
  import 'dart:typed_data';
4
4
 
5
5
  import 'package:nomos_client/src/nms_generated_runtime.dart';
6
+ import 'package:nomos_client/src/write_progress.dart';
6
7
  import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
7
8
  import 'package:test/test.dart';
8
9
 
@@ -22,6 +23,7 @@ final class _RepairTransport
22
23
  var syncCalls = 0;
23
24
  final syncSessions = <String>[];
24
25
  var offerCalls = 0;
26
+ Completer<void>? holdOffer;
25
27
  var expireNextSessionOffer = false;
26
28
  var refuseSignerEnrollmentOffers = 0;
27
29
  var refuseKernelSignerOffers = 0;
@@ -142,6 +144,14 @@ final class _RepairTransport
142
144
  }
143
145
  if (request.hasOffer()) {
144
146
  offerCalls += 1;
147
+ if (holdOffer != null) {
148
+ pushRuntime(protocol.RuntimeEvent(
149
+ writePreparation: protocol.WritePreparation(
150
+ requestId: request.requestId,
151
+ workspace: session,
152
+ )));
153
+ await holdOffer!.future;
154
+ }
145
155
  if (refuseKernelSignerOffers > 0) {
146
156
  refuseKernelSignerOffers -= 1;
147
157
  return protocol.CallResponse(
@@ -225,6 +235,42 @@ Future<DartReception> _connected(_RepairTransport transport) async {
225
235
  }
226
236
 
227
237
  void main() {
238
+ test('runtime preparation reaches only its pending generated action observer',
239
+ () async {
240
+ final transport = _RepairTransport()..holdOffer = Completer<void>();
241
+ final bridge = await _connected(transport);
242
+ addTearDown(bridge.dispose);
243
+ final phases = <NomosWritePhase>[];
244
+ final pending = NomosWrite.observe(
245
+ () => bridge
246
+ .session('estate-1')
247
+ .offerGeneratedCallOutcome(call: _estateCall()),
248
+ onProgress: (progress) => phases.add(progress.phase));
249
+ await Future<void>.delayed(Duration.zero);
250
+ expect(phases, [NomosWritePhase.running, NomosWritePhase.preparing]);
251
+ transport.pushRuntime(protocol.RuntimeEvent(
252
+ writePreparation: protocol.WritePreparation(
253
+ requestId: 'another-action',
254
+ workspace: 'estate-1',
255
+ )));
256
+ await Future<void>.delayed(Duration.zero);
257
+ expect(phases.length, 2);
258
+ final requestId = transport.lastCall!.requestId;
259
+ transport.holdOffer!.complete();
260
+ await pending;
261
+ expect(phases.last, NomosWritePhase.completed);
262
+ expect(transport.offerCalls, 1);
263
+ final completed = List<NomosWritePhase>.of(phases);
264
+ transport.pushRuntime(protocol.RuntimeEvent(
265
+ writePreparation: protocol.WritePreparation(
266
+ requestId: requestId,
267
+ workspace: 'estate-1',
268
+ )));
269
+ await Future<void>.delayed(Duration.zero);
270
+ expect(phases, completed,
271
+ reason: 'completed operations detach their observers');
272
+ });
273
+
228
274
  test('kernel unenrolled signer refusal repairs the child and retries once',
229
275
  () async {
230
276
  final transport = _RepairTransport()..refuseKernelSignerOffers = 1;
@@ -30,6 +30,35 @@ final class _ScriptedKernelCalls extends _ScriptedRuntime
30
30
  }
31
31
 
32
32
  void main() {
33
+ test('a refused generated write preserves typed sign-in and access outcomes',
34
+ () async {
35
+ for (final reason in [
36
+ protocol.RuntimeRefusalReason.RUNTIME_REFUSAL_REASON_INVALID_CREDENTIAL,
37
+ protocol.RuntimeRefusalReason.RUNTIME_REFUSAL_REASON_ACCESS_DENIED,
38
+ ]) {
39
+ final runtime =
40
+ _ScriptedKernelCalls((request) => protocol.RuntimeResponse(
41
+ protocolVersion: 2,
42
+ requestId: request.requestId,
43
+ refused: protocol.RuntimeRefusal(
44
+ reason: reason, workspace: 'estate', retryable: false),
45
+ ));
46
+ await expectLater(
47
+ runtime.callKernel(
48
+ session: 'estate',
49
+ request: protocol.CallRequest(),
50
+ credentialUse: protocol.CredentialUse.CREDENTIAL_USE_SESSION),
51
+ throwsA(isA<NomosRefused>().having(
52
+ (e) => e.reason,
53
+ 'reason',
54
+ reason ==
55
+ protocol.RuntimeRefusalReason
56
+ .RUNTIME_REFUSAL_REASON_INVALID_CREDENTIAL
57
+ ? NomosRefusalReason.invalidCredential
58
+ : NomosRefusalReason.accessDenied)));
59
+ }
60
+ });
61
+
33
62
  test('a kernel call on a copy not ready yet is the typed wait reads sit out',
34
63
  () async {
35
64
  // A fresh device's first read of a workspace it is still fetching (node, web and in-app