@githolon/dsl 0.101.5 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dart/CHANGELOG.md +13 -0
  2. package/dart/lib/nomos_client.dart +3 -52
  3. package/dart/lib/src/dev_cloud.dart +5 -0
  4. package/dart/lib/src/home.dart +10 -15
  5. package/dart/lib/src/nms_conformance_runtime.dart +12 -0
  6. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  7. package/dart/lib/src/nms_test_environment.dart +8 -0
  8. package/dart/lib/src/node_runner_transport.dart +56 -18
  9. package/dart/lib/src/provider.dart +5 -1
  10. package/dart/lib/src/record_origin.dart +55 -0
  11. package/dart/lib/src/runner/node-entry.mjs +9105 -0
  12. package/dart/lib/src/runner/offline-install.mjs +4729 -0
  13. package/dart/lib/src/session_bound.dart +23 -10
  14. package/dart/lib/src/source_trust.dart +51 -0
  15. package/dart/lib/src/test_harness.dart +490 -58
  16. package/dart/lib/src/webview_bridge.dart +167 -58
  17. package/dart/lib/src/workspace_name.dart +7 -1
  18. package/dart/pubspec.lock +3 -3
  19. package/dart/pubspec.yaml +4 -2
  20. package/dart/test/acquire_session_credentials_test.dart +34 -17
  21. package/dart/test/acquire_session_test.dart +109 -24
  22. package/dart/test/adv_realm_interference_test.dart +227 -99
  23. package/dart/test/bundle_version_pairing_test.dart +1 -1
  24. package/dart/test/cloud_bound_topology_test.dart +1 -1
  25. package/dart/test/cluster_attested_test.dart +52 -35
  26. package/dart/test/cluster_births_test.dart +5 -2
  27. package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
  28. package/dart/test/cluster_local_autosync_test.dart +5 -2
  29. package/dart/test/cluster_scale_test.dart +5 -2
  30. package/dart/test/compat_test.dart +1 -1
  31. package/dart/test/dlq_session_surface_test.dart +2 -1
  32. package/dart/test/errors_test.dart +1 -1
  33. package/dart/test/first_app_gauntlet_test.dart +78 -51
  34. package/dart/test/four_layer_nested_topology_test.dart +1 -1
  35. package/dart/test/harness_phase_timeout_test.dart +1 -1
  36. package/dart/test/home_parity_test.dart +9 -5
  37. package/dart/test/local_sync_inert_test.dart +22 -14
  38. package/dart/test/node_runner_transport_test.dart +9 -5
  39. package/dart/test/null_slot_test.dart +1 -1
  40. package/dart/test/parent_signer_recovery_test.dart +5 -2
  41. package/dart/test/schema_validation_test.dart +1 -1
  42. package/dart/test/sealing_client_test.dart +2 -1
  43. package/dart/test/session_bound_test.dart +92 -119
  44. package/dart/test/stale_bundle_test.dart +1 -1
  45. package/dart/test/test_harness_test.dart +77 -22
  46. package/dart/test/vo_read_typing_test.dart +32 -14
  47. package/dart/test/watch_after_offer_stall_test.dart +2 -1
  48. package/dart/test/webview_bridge_test.dart +17 -4
  49. package/dart/test/wire_test.dart +1 -1
  50. package/dart_types/CHANGELOG.md +8 -0
  51. package/dart_types/lib/nomos_types.dart +7 -6
  52. package/dart_types/lib/src/activity.dart +23 -13
  53. package/dart_types/lib/src/foreign_ref.dart +61 -112
  54. package/dart_types/lib/src/invitation.dart +96 -0
  55. package/dart_types/lib/src/nomos_ref.dart +97 -59
  56. package/dart_types/pubspec.yaml +1 -1
  57. package/dart_types/test/surfaced_test.dart +125 -19
  58. package/package.json +7 -3
  59. package/runtime/generated-application-host.js +8851 -0
  60. package/src/activity.ts +2 -2
  61. package/src/aggregate.ts +24 -1
  62. package/src/application_composition.ts +282 -0
  63. package/src/application_topology.ts +151 -0
  64. package/src/attested_core.ts +2 -2
  65. package/src/authoring_context.ts +9 -0
  66. package/src/birth_meter_ensure.ts +1 -1
  67. package/src/births.ts +53 -5
  68. package/src/build_package.ts +79 -71
  69. package/src/business_birth.ts +93 -10
  70. package/src/clear_directive.ts +3 -1
  71. package/src/codegen_dart.ts +369 -420
  72. package/src/codegen_dart_application.ts +1508 -0
  73. package/src/codegen_dart_client.ts +156 -95
  74. package/src/codegen_dart_scenario.ts +299 -0
  75. package/src/codegen_openapi.ts +8 -3
  76. package/src/codegen_proof.ts +1113 -1137
  77. package/src/codegen_provider_dart.ts +12 -1
  78. package/src/codegen_ts.ts +1176 -496
  79. package/src/compat_package_main.ts +22 -10
  80. package/src/compile_package_main.ts +1536 -337
  81. package/src/contract.ts +10 -4
  82. package/src/dart_barrel_guard.ts +5 -1
  83. package/src/describe_model.ts +13 -5
  84. package/src/directive.ts +244 -5
  85. package/src/engine_entry.ts +56 -13
  86. package/src/engine_javascript.ts +31 -0
  87. package/src/evolve_preview.ts +3 -12
  88. package/src/framework/application_home.ts +24 -10
  89. package/src/framework/capability.ts +15 -1
  90. package/src/framework/domain_lifecycle.ts +8 -0
  91. package/src/framework/framework_aggregates.ts +2 -2
  92. package/src/framework/genesis.ts +11 -1
  93. package/src/framework/home.ts +178 -75
  94. package/src/framework/impure_capability.ts +24 -7
  95. package/src/framework/relations.ts +13 -5
  96. package/src/framework/workspaces.ts +13 -12
  97. package/src/generated_client_names.ts +42 -3
  98. package/src/idof_lint.ts +65 -13
  99. package/src/index.ts +46 -12
  100. package/src/intent.ts +587 -0
  101. package/src/law_check.ts +64 -16
  102. package/src/precondition.ts +16 -4
  103. package/src/read_projection_namespace.ts +2 -7
  104. package/src/recipient.ts +33 -0
  105. package/src/relation_schema.ts +93 -0
  106. package/src/secondary_ensure.ts +11 -2
  107. package/src/shape_compat.ts +11 -1
  108. package/src/usd.ts +30 -8
  109. package/src/usd_layers.ts +447 -246
  110. package/src/usd_state.ts +1 -1
  111. package/src/value_object.ts +49 -13
  112. package/src/wire_encode.ts +124 -26
  113. package/src/workspace_sharing.ts +269 -0
  114. package/src/zod_introspection.ts +84 -0
  115. package/dart/example/nomos_secure_key_store.dart +0 -82
  116. package/dart/example/nomos_webview_transport.dart +0 -63
  117. package/dart/lib/nomos_headless.dart +0 -50
  118. package/dart/lib/testing.dart +0 -14
  119. package/dart/test/co2_product_gauntlet_test.dart +0 -244
  120. package/src/framework/identity.ts +0 -537
@@ -12,8 +12,11 @@
12
12
  /// The generated per-domain Dart client wraps a [NomosBridge]; the bridge owns request/reply correlation +
13
13
  /// reactive `watch` streams. The platform TRANSPORT ([BridgeTransport]) is injected — `dart:js_util` on web,
14
14
  /// a WebView JS-channel on mobile — so this protocol logic is pure Dart + fully unit-testable.
15
+ @internal
15
16
  library;
16
17
 
18
+ import 'package:meta/meta.dart' show internal;
19
+
17
20
  import 'dart:async';
18
21
  import 'dart:convert';
19
22
  import 'dart:math';
@@ -24,12 +27,12 @@ import 'package:nomos_types/nomos_types.dart'
24
27
  NomosTraceContext,
25
28
  NomosErrorInfo,
26
29
  NomosRef,
27
- NomosForeignReader,
28
- NomosForeignWorkspaceReader,
29
- ForeignAttestation,
30
30
  CustodyBreakPolicy,
31
31
  NomosCustodyEvent,
32
32
  NomosCustodyStatus;
33
+ import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;
34
+ import 'package:nomos_types/src/foreign_ref.dart'
35
+ show NomosForeignReader, NomosForeignWorkspaceReader;
33
36
 
34
37
  import 'session_bound.dart' show DomainNotInstalledForSession;
35
38
 
@@ -107,14 +110,14 @@ class TransferProgress {
107
110
  class NomosMoment {
108
111
  final String id;
109
112
  final DateTime? occurredAt;
110
- final String? actorSubject;
113
+ final String? performedBy;
111
114
  final String? domain;
112
115
  final String? action;
113
116
 
114
117
  const NomosMoment({
115
118
  required this.id,
116
119
  this.occurredAt,
117
- this.actorSubject,
120
+ this.performedBy,
118
121
  this.domain,
119
122
  this.action,
120
123
  });
@@ -126,7 +129,7 @@ class NomosMoment {
126
129
  occurredAt: at is num
127
130
  ? DateTime.fromMillisecondsSinceEpoch(at.toInt(), isUtc: true)
128
131
  : null,
129
- actorSubject: json['actorSubject']?.toString(),
132
+ performedBy: json['actorSubject']?.toString(),
130
133
  domain: json['domain']?.toString(),
131
134
  action: json['directiveId']?.toString(),
132
135
  );
@@ -198,7 +201,8 @@ class NomosCreated {
198
201
 
199
202
  /// The canonical [NomosRef] view — the `{workspace,type,id}` triple, followable/serializable
200
203
  /// (`nomos://<workspace>/<type>/<id>`). One reference, everywhere.
201
- NomosRef get ref => NomosRef(workspace: workspace, type: type, id: id);
204
+ NomosRef<Object?> get ref => NmsInternalNomosRefs.create<Object?>(
205
+ workspace: workspace, type: type, id: id);
202
206
  @override
203
207
  String toString() => id;
204
208
  }
@@ -838,6 +842,10 @@ class NomosOfferOutcome {
838
842
  final String head;
839
843
  final List<NomosCreated> created;
840
844
 
845
+ /// Every business aggregate touched by the sealed intent, including creates,
846
+ /// mutations, deterministic ensures and archives.
847
+ final List<NomosCreated> affected;
848
+
841
849
  /// The workspace the offer ran on (NomosRef). Additive; '' on older runners. Each [created] ref already
842
850
  /// carries this, so `outcome.created.first.ref` is the full followable `{workspace,type,id}` locator.
843
851
  final String workspace;
@@ -859,6 +867,7 @@ class NomosOfferOutcome {
859
867
  const NomosOfferOutcome({
860
868
  required this.head,
861
869
  required this.created,
870
+ this.affected = const [],
862
871
  this.workspace = '',
863
872
  this.born = const [],
864
873
  this.birthOutcomes = const [],
@@ -874,8 +883,9 @@ class NomosOfferOutcome {
874
883
  /// The born child workspace(s) as followable [NomosRef]s — hand one to [NomosBridge.follow]
875
884
  /// (`final estate = await engine.follow(outcome.bornRefs.single);`) so app code never carries a
876
885
  /// loose workspace string. The ref's type is the reserved `nomos.Workspace` locator tag.
877
- List<NomosRef> get bornRefs => born
878
- .map((w) => NomosRef(workspace: w, type: 'nomos.Workspace', id: w))
886
+ List<NomosRef<Object?>> get bornRefs => born
887
+ .map((w) => NmsInternalNomosRefs.create<Object?>(
888
+ workspace: w, type: 'nomos.Workspace', id: w))
879
889
  .toList();
880
890
  }
881
891
 
@@ -885,11 +895,12 @@ class NomosOfferOutcome {
885
895
  /// genuinely-absent workspace needs [ifAbsentCredentials] (else a named [StateError]). Returns the live
886
896
  /// [NomosSession] — so the failure surfaces HERE (at open), never later on the client's first query. Defined
887
897
  /// in nomos_client (NOT on the pure `nomos_types` [NomosRef]) so the types package stays bridge-free.
888
- extension NomosRefAcquire on NomosRef {
898
+ extension NomosRefAcquire<TAggregate> on NomosRef<TAggregate> {
889
899
  Future<NomosSession> acquire(
890
900
  NomosBridge bridge, {
891
901
  Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
892
902
  }) {
903
+ final workspace = NmsInternalNomosRefs.workspace(this);
893
904
  if (workspace.isEmpty) {
894
905
  throw ArgumentError.value(this, 'ref',
895
906
  'NomosRef.acquire: the ref carries no workspace — only a workspace-complete ref is openable');
@@ -927,6 +938,25 @@ class NomosSessionCredentials {
927
938
  });
928
939
  }
929
940
 
941
+ final class _NomosRetainedCloudAccess {
942
+ final String cloud;
943
+ final Future<String?> Function() signIn;
944
+
945
+ const _NomosRetainedCloudAccess({
946
+ required this.cloud,
947
+ required this.signIn,
948
+ });
949
+
950
+ Future<NomosSessionCredentials> credentials() async {
951
+ final token = await signIn();
952
+ if (token == null || token.trim().isEmpty) {
953
+ throw StateError(
954
+ 'Nomos could not refresh the signed-in identity while opening a referenced business workspace');
955
+ }
956
+ return NomosSessionCredentials(cloud: cloud, authToken: token);
957
+ }
958
+ }
959
+
930
960
  /// THE BRIDGE. Construct over a [BridgeTransport], `await ready`, then `connect(...)` and drive the holon.
931
961
  class NomosBridge {
932
962
  final BridgeTransport _transport;
@@ -980,6 +1010,14 @@ class NomosBridge {
980
1010
  // The gate-bound identity retained from each session's auth token. Generated clients consume this so
981
1011
  // application code never has to thread an actor into ordinary business methods.
982
1012
  final Map<String, String> _sessionPrincipals = {};
1013
+ // Device signing custody follows the application session internally. A generated application opening a
1014
+ // locally-born child inherits its current session's device key; tenant code never handles authorSecret.
1015
+ final Map<String, String> _sessionAuthorSecrets = {};
1016
+ // Stateful generated applications retain the refreshable cloud access that opened their primary
1017
+ // session. A generated read following a NomosRef can therefore mount a genuinely-absent workspace
1018
+ // without asking tenant code for cloud URLs, tokens, sessions or signing keys. Kept per source
1019
+ // session so one local plane may safely host applications from different clouds/identities.
1020
+ final Map<String, _NomosRetainedCloudAccess> _sessionCloudAccess = {};
983
1021
  // [acquireSession]'s per-workspace single-flight: workspace → the in-flight open every concurrent
984
1022
  // acquirer joins (removed when it completes; a live session then hits the fast path instead).
985
1023
  final Map<String, Future<NomosSession>> _acquiring = {};
@@ -1263,7 +1301,7 @@ class NomosBridge {
1263
1301
  final sub = payload is Map ? payload['sub'] : null;
1264
1302
  if (sub is! String || sub.trim().isEmpty) return null;
1265
1303
  final value = sub.trim();
1266
- return value.contains(':') ? value : 'user:$value';
1304
+ return 'user:$value';
1267
1305
  } catch (_) {
1268
1306
  return null;
1269
1307
  }
@@ -1280,8 +1318,8 @@ class NomosBridge {
1280
1318
  NomosSession session(String workspace) => _eventPlane._sessionViews
1281
1319
  .putIfAbsent(workspace, () => NomosSession(_eventPlane, workspace));
1282
1320
 
1283
- /// A [NomosForeignReader] backed by THIS bridge's multi-workspace plane — what a generated `ForeignRef`
1284
- /// (a `t.foreignRef` cross-workspace locator) resolves against: `row.data.listing.resolve(bridge.foreignReader())`.
1321
+ /// A [NomosForeignReader] backed by this bridge's multi-workspace plane. Generated application methods
1322
+ /// use it privately to follow a declared cross-workspace business reference.
1285
1323
  /// `foreignWorkspace(ws).queryById(id)` MOUNTS the foreign workspace on this device's plane ON DEMAND
1286
1324
  /// ([connect] is idempotent — "first connect wins", and `allowUnborn` so a missing workspace reads empty →
1287
1325
  /// `ForeignNotFound`, not a throw) and reads it. [cloud] is the cloud the foreign workspaces sync from
@@ -1380,6 +1418,9 @@ class NomosBridge {
1380
1418
  final boundPrincipal = _principalFromToken(authToken);
1381
1419
  if (boundPrincipal != null)
1382
1420
  _eventPlane._sessionPrincipals[ws] = boundPrincipal;
1421
+ if (authorSecret != null && authorSecret.isNotEmpty) {
1422
+ _eventPlane._sessionAuthorSecrets[ws] = authorSecret;
1423
+ }
1383
1424
  _eventPlane._lastRecovery =
1384
1425
  (r['recovery'] as Map?)?.cast<String, Object?>();
1385
1426
  }
@@ -1670,6 +1711,16 @@ class NomosBridge {
1670
1711
  _eventPlane._sessionPrincipals[born] = birthPrincipal;
1671
1712
  }
1672
1713
  }
1714
+ final sourceSession = session ?? _sessionName;
1715
+ final birthAuthorSecret = authorSecret ??
1716
+ (sourceSession == null
1717
+ ? null
1718
+ : _eventPlane._sessionAuthorSecrets[sourceSession]);
1719
+ if (birthAuthorSecret != null && birthAuthorSecret.isNotEmpty) {
1720
+ for (final born in bornNames) {
1721
+ _eventPlane._sessionAuthorSecrets[born] = birthAuthorSecret;
1722
+ }
1723
+ }
1673
1724
  // The offer ran on this workspace (NomosRef) — stamp every minted ref with it so it is a complete locator.
1674
1725
  final ws = (r['workspace'] ?? '').toString();
1675
1726
  return NomosOfferOutcome(
@@ -1682,6 +1733,13 @@ class NomosBridge {
1682
1733
  id: (m['id'] ?? '').toString(),
1683
1734
  workspace: ws))
1684
1735
  .toList(),
1736
+ affected: ((r['affected'] as List?) ?? const [])
1737
+ .whereType<Map>()
1738
+ .map((m) => NomosCreated(
1739
+ type: (m['type'] ?? '').toString(),
1740
+ id: (m['id'] ?? '').toString(),
1741
+ workspace: ws))
1742
+ .toList(),
1685
1743
  born: bornNames,
1686
1744
  birthOutcomes: births,
1687
1745
  );
@@ -1736,13 +1794,13 @@ class NomosBridge {
1736
1794
  /// cloud (omit for the local plane — a locally-born child mounts in place, no re-clone).
1737
1795
  /// [authorSecret] — the device signing key for the followed session's writes (era-0: an attested-born
1738
1796
  /// child is WARRANTED; write access needs its owner's enrolled device key).
1739
- Future<NomosSession> follow(NomosRef ref,
1797
+ Future<NomosSession> follow<TAggregate>(NomosRef<TAggregate> ref,
1740
1798
  {String? cloud,
1741
1799
  String? clientId,
1742
1800
  String? authToken,
1743
1801
  bool allowUnborn = false,
1744
1802
  String? authorSecret}) async {
1745
- final ws = ref.workspace;
1803
+ final ws = NmsInternalNomosRefs.workspace(ref);
1746
1804
  if (ws.isEmpty) {
1747
1805
  throw ArgumentError.value(ref, 'ref',
1748
1806
  'follow: the NomosRef carries no workspace — only a workspace-complete ref is followable');
@@ -1776,10 +1834,23 @@ class NomosBridge {
1776
1834
  /// the credentials path takes its own `NomosSessionCredentials.authorSecret` (falling back to this).
1777
1835
  Future<NomosSession> acquireSession(String workspace,
1778
1836
  {Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
1779
- String? authorSecret}) {
1837
+ String? authorSecret,
1838
+ String? inheritedPrincipal}) {
1780
1839
  // Fast path: a live session needs no runner round-trip (and never asks for credentials).
1781
1840
  final plane = _eventPlane;
1841
+ final sourcePrincipal = inheritedPrincipal ?? principal;
1842
+ void inheritPrincipal() {
1843
+ if (sourcePrincipal == null || sourcePrincipal.isEmpty) return;
1844
+ final existing = plane._sessionPrincipals[workspace];
1845
+ if (existing != null && existing != sourcePrincipal) {
1846
+ throw StateError(
1847
+ 'acquireSession("$workspace"): the workspace is already bound to a different signed-in person');
1848
+ }
1849
+ plane._sessionPrincipals[workspace] = sourcePrincipal;
1850
+ }
1851
+
1782
1852
  if (plane._sessionNames.contains(workspace)) {
1853
+ inheritPrincipal();
1783
1854
  return Future.value(session(workspace));
1784
1855
  }
1785
1856
  // SINGLE-FLIGHT: one in-flight open per workspace per plane; concurrent callers join it.
@@ -1801,6 +1872,7 @@ class NomosBridge {
1801
1872
  session: workspace,
1802
1873
  allowUnborn: true,
1803
1874
  authorSecret: authorSecret);
1875
+ inheritPrincipal();
1804
1876
  return session(workspace);
1805
1877
  }
1806
1878
  if (ifAbsentCredentials == null) {
@@ -1818,6 +1890,7 @@ class NomosBridge {
1818
1890
  authorSecret: c.authorSecret ?? authorSecret,
1819
1891
  allowUnborn: c.allowUnborn,
1820
1892
  );
1893
+ inheritPrincipal();
1821
1894
  return session(workspace);
1822
1895
  } finally {
1823
1896
  // completed (either way) — the next acquire re-evaluates fresh (a live session hits the fast path).
@@ -1983,7 +2056,7 @@ class NomosBridge {
1983
2056
  required String subject,
1984
2057
  required String relation,
1985
2058
  required String asserterPrincipal,
1986
- required String homeLawHash,
2059
+ String? homeLawHash,
1987
2060
  String? authorSecret,
1988
2061
  String object = 'workspace:self',
1989
2062
  String? asserterWorkspace,
@@ -1996,7 +2069,7 @@ class NomosBridge {
1996
2069
  'relation': relation,
1997
2070
  if (authorSecret != null) 'authorSecret': authorSecret,
1998
2071
  'asserterPrincipal': asserterPrincipal,
1999
- 'homeLawHash': homeLawHash,
2072
+ if (homeLawHash != null) 'homeLawHash': homeLawHash,
2000
2073
  'object': object,
2001
2074
  if (asserterWorkspace != null) 'asserterWorkspace': asserterWorkspace,
2002
2075
  if (attestedAt != null) 'attestedAt': attestedAt,
@@ -2269,17 +2342,11 @@ class NomosBridge {
2269
2342
  Future<Map<String, Object?>> verifyChain() async =>
2270
2343
  _asMap(await _call('verifyChain'));
2271
2344
 
2272
- /// BIRTH A HOME — THE CEREMONY (VA root-of-trust, drift-proof), run end-to-end inside the connected holon:
2273
- /// derive the deterministic home name, mint + enroll the device key, DEVICE-SIGN the home cert through the
2274
- /// kernel `cert_sign` op (the single source of the byte layout), OFFER the PARAMETERIZED parent's `birthHome`
2275
- /// directive carrying the homeCert, poll the parent for the born home, and CONFIRM the born home's own
2276
- /// `verify_chain` is GREEN (the offline peer-verify — replay IS validation). `spec` MUST carry `parent` (the
2277
- /// tenant's platform whose governance includes `birthHome`) and `principal`; the client discovers the
2278
- /// private framework route and active law hashes from that application's live contract. `subject`/
2279
- /// `authToken`/`directiveId` are optional. OURS is the ceremony; the parent is the TENANT's.
2280
- /// Returns `{home, device:{secret,public}, homeCert, born, verify:{valid,…}}`.
2281
- Future<Map<String, Object?>> birthHome(Map<String, Object?> spec) async =>
2282
- _asMap(await _call('birthHome', spec));
2345
+ /// Framework-private receiving-group setup. Generated application hosts own this ceremony; tenant code
2346
+ /// receives only the resulting business application.
2347
+ Future<Map<String, Object?>> nomosInternalBirthReceivingGroup(
2348
+ Map<String, Object?> spec) async =>
2349
+ _asMap(await _call('nomosInternalBirthReceivingGroup', spec));
2283
2350
 
2284
2351
  /// Sign a cross-custody SHARE attestation `{secret, asserterWorkspace, object, relation, subject,
2285
2352
  /// attestedAt}` → `{publicKey, signature}`. This is what drives `recordShare` into an invitee's home
@@ -2508,15 +2575,18 @@ class NomosBridge {
2508
2575
  }));
2509
2576
 
2510
2577
  /// Prepare the private framework payload behind generated share/unshare/transfer methods.
2511
- Future<Map<String, Object?>> nomosPrepareOwnedRecordAction(
2578
+ Future<Map<String, Object?>> nomosPrepareOwnedRecordAction<TAggregate>(
2512
2579
  {required String action,
2513
2580
  required String scope,
2514
- required NomosRef record,
2581
+ required NomosRef<TAggregate> record,
2515
2582
  required String peer}) async =>
2516
2583
  _asMap(await _call('cryptoOwnedPrepareRecordAction', {
2517
2584
  'action': action,
2518
2585
  'scope': scope,
2519
- 'record': {'type': record.type, 'id': record.id},
2586
+ 'record': {
2587
+ 'type': NmsInternalNomosRefs.type(record),
2588
+ 'id': NmsInternalNomosRefs.id(record),
2589
+ },
2520
2590
  'peer': peer,
2521
2591
  }));
2522
2592
 
@@ -2804,11 +2874,19 @@ class NomosBridge {
2804
2874
  /// Reactive local read: a stream of row-sets that re-emits on every local change. Cancelling the
2805
2875
  /// subscription tells the runner to drop the watch.
2806
2876
  Stream<List<dynamic>> watch(String queryId,
2807
- [Map<String, Object?> params = const {}, String? session]) =>
2877
+ [Map<String, Object?> params = const {},
2878
+ String? session,
2879
+ String? principal]) =>
2808
2880
  _watchStream(
2809
- kind: 'query', queryId: queryId, params: params, session: session);
2810
- Stream<List<dynamic>> watchById(String id, [String? session]) =>
2811
- _watchStream(kind: 'byId', id: id, session: session);
2881
+ kind: 'query',
2882
+ queryId: queryId,
2883
+ params: params,
2884
+ session: session,
2885
+ principal: principal);
2886
+ Stream<List<dynamic>> watchById(String id,
2887
+ [String? session, String? principal]) =>
2888
+ _watchStream(
2889
+ kind: 'byId', id: id, session: session, principal: principal);
2812
2890
 
2813
2891
  /// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
2814
2892
  Stream<List<dynamic>> watchSpatial(String spatialId, NomosBbox bbox,
@@ -2931,11 +3009,12 @@ class NomosBridge {
2931
3009
  'requirement': requirement,
2932
3010
  if (session != null) 'session': session,
2933
3011
  }),
2934
- onCancel: () async {
3012
+ onCancel: () {
2935
3013
  _eventPlane._compatWatches.remove(subId);
2936
- try {
2937
- await _call('unwatch', {'subId': subId});
2938
- } catch (_) {}
3014
+ // Cancellation is a local lifecycle boundary and must complete even if the runner is slow,
3015
+ // disconnected, or being disposed. The remote release remains best-effort; bridge/runner disposal
3016
+ // also drops every outstanding watch, so awaiting this acknowledgement only creates teardown hangs.
3017
+ unawaited(_call('unwatch', {'subId': subId}).catchError((_) => null));
2939
3018
  },
2940
3019
  );
2941
3020
  _eventPlane._compatWatches[subId] = ctrl;
@@ -2950,7 +3029,8 @@ class NomosBridge {
2950
3029
  String? spatialId,
2951
3030
  NomosBbox? bbox,
2952
3031
  String? base,
2953
- String? session}) {
3032
+ String? session,
3033
+ String? principal}) {
2954
3034
  // Controllers live on the EVENT PLANE (where _onMessage dispatches); the registration `_call` stays
2955
3035
  // THIS instance's (a session's override stamps `args.session`) — see [_eventPlane].
2956
3036
  final subId = 'w${++_eventPlane._seq}';
@@ -2965,13 +3045,14 @@ class NomosBridge {
2965
3045
  if (spatialId != null) 'spatialId': spatialId,
2966
3046
  if (bbox != null) 'bbox': bbox.toJson(),
2967
3047
  if (base != null) 'base': base,
3048
+ if (principal != null) 'principal': principal,
2968
3049
  if (session != null) 'session': session,
2969
3050
  }),
2970
- onCancel: () async {
3051
+ onCancel: () {
2971
3052
  _eventPlane._watches.remove(subId);
2972
- try {
2973
- await _call('unwatch', {'subId': subId});
2974
- } catch (_) {}
3053
+ // A caller owns its local subscription; it must never need a healthy transport in order to release
3054
+ // it. Runner-side cleanup is still requested immediately and is made total by runner disposal.
3055
+ unawaited(_call('unwatch', {'subId': subId}).catchError((_) => null));
2975
3056
  },
2976
3057
  );
2977
3058
  _eventPlane._watches[subId] = ctrl;
@@ -3078,14 +3159,48 @@ class NomosSession extends NomosBridge {
3078
3159
  /// single-flight registry; a session never owns mounts).
3079
3160
  @override
3080
3161
  Future<NomosSession> acquireSession(String workspace,
3081
- {Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
3082
- String? authorSecret}) =>
3083
- parent.acquireSession(workspace,
3084
- ifAbsentCredentials: ifAbsentCredentials, authorSecret: authorSecret);
3162
+ {Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
3163
+ String? authorSecret,
3164
+ String? inheritedPrincipal}) {
3165
+ final retained = parent._sessionCloudAccess[this.workspace];
3166
+ final opened = parent.acquireSession(
3167
+ workspace,
3168
+ ifAbsentCredentials: ifAbsentCredentials ??
3169
+ (retained == null ? null : retained.credentials),
3170
+ authorSecret:
3171
+ authorSecret ?? parent._sessionAuthorSecrets[this.workspace],
3172
+ inheritedPrincipal:
3173
+ inheritedPrincipal ?? parent._sessionPrincipals[this.workspace],
3174
+ );
3175
+ return opened.then((session) {
3176
+ // Navigation can continue from the newly-opened handle without losing the application's private
3177
+ // credential posture (home -> shared home -> child workspace remains one stateful application).
3178
+ if (retained != null)
3179
+ parent._sessionCloudAccess.putIfAbsent(workspace, () => retained);
3180
+ return session;
3181
+ });
3182
+ }
3183
+
3184
+ /// Generator/runtime-host seam: retain this application's refreshable cloud access privately so
3185
+ /// generated reference reads can mount another authorized workspace after a cold start.
3186
+ ///
3187
+ /// Tenant packages do not export NomosSession; application code reaches this only through generated
3188
+ /// `find`/`watchOne` methods, never through a credential or session callback.
3189
+ void nomosInternalRetainCloudAccess({
3190
+ required String cloud,
3191
+ required Future<String?> Function() signIn,
3192
+ }) {
3193
+ if (cloud.trim().isEmpty) {
3194
+ throw ArgumentError.value(
3195
+ cloud, 'cloud', 'Nomos cloud URL cannot be empty');
3196
+ }
3197
+ parent._sessionCloudAccess[workspace] =
3198
+ _NomosRetainedCloudAccess(cloud: cloud, signIn: signIn);
3199
+ }
3085
3200
 
3086
3201
  /// A followed ref mounts on the SHARED plane — delegate to the parent (a session never owns mounts).
3087
3202
  @override
3088
- Future<NomosSession> follow(NomosRef ref,
3203
+ Future<NomosSession> follow<TAggregate>(NomosRef<TAggregate> ref,
3089
3204
  {String? cloud,
3090
3205
  String? clientId,
3091
3206
  String? authToken,
@@ -3096,7 +3211,8 @@ class NomosSession extends NomosBridge {
3096
3211
  clientId: clientId,
3097
3212
  authToken: authToken,
3098
3213
  allowUnborn: allowUnborn,
3099
- authorSecret: authorSecret);
3214
+ authorSecret:
3215
+ authorSecret ?? parent._sessionAuthorSecrets[workspace]);
3100
3216
 
3101
3217
  @override
3102
3218
  Future<Object?> _call(String op, [Map<String, Object?> args = const {}]) =>
@@ -3169,13 +3285,6 @@ class _BridgeForeignReader implements NomosForeignReader {
3169
3285
  @override
3170
3286
  NomosForeignWorkspaceReader foreignWorkspace(String workspace) =>
3171
3287
  _BridgeForeignWorkspaceReader(_bridge, workspace, _cloud);
3172
-
3173
- // No attestation hook by default — `ForeignRef.attest` throws unless the app wires the attested-read lane
3174
- // (trustSource + .readsFrom). That gate is the security model (a locator is not a foreign key), not a
3175
- // limitation: unverifiable "evidence" would be the real footgun.
3176
- @override
3177
- Future<ForeignAttestation> Function(String workspace, String id, String type)?
3178
- get attestForeign => null;
3179
3288
  }
3180
3289
 
3181
3290
  /// A per-workspace read handle on the bridge plane — mounts [_workspace] lazily on the first read.
@@ -1,3 +1,8 @@
1
+ @internal
2
+ library;
3
+
4
+ import 'package:meta/meta.dart' show internal;
5
+
1
6
  import 'dart:convert';
2
7
 
3
8
  import 'package:crypto/crypto.dart';
@@ -19,6 +24,7 @@ final String workspaceNameSep = String.fromCharCode(31);
19
24
  /// final ws = deriveWorkspaceName('estate', estateId); // open / sync / mount it
20
25
  /// ```
21
26
  String deriveWorkspaceName(String namespace, String key) {
22
- final digest = sha256.convert(utf8.encode('$namespace$workspaceNameSep$key')).toString();
27
+ final digest =
28
+ sha256.convert(utf8.encode('$namespace$workspaceNameSep$key')).toString();
23
29
  return '$namespace-${digest.substring(0, 40)}';
24
30
  }
package/dart/pubspec.lock CHANGED
@@ -162,7 +162,7 @@ packages:
162
162
  source: hosted
163
163
  version: "0.12.20"
164
164
  meta:
165
- dependency: transitive
165
+ dependency: "direct main"
166
166
  description:
167
167
  name: meta
168
168
  sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739
@@ -191,9 +191,9 @@ packages:
191
191
  path: "../dart_types"
192
192
  relative: true
193
193
  source: path
194
- version: "0.7.1"
194
+ version: "0.8.0"
195
195
  package_config:
196
- dependency: transitive
196
+ dependency: "direct main"
197
197
  description:
198
198
  name: package_config
199
199
  sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
package/dart/pubspec.yaml CHANGED
@@ -8,7 +8,7 @@ description: >-
8
8
  Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
9
9
  code-generated per-domain aggregate + directive-payload types. (De)serializes
10
10
  the EXACT serde JSON the Rust kernel + projection produce/consume.
11
- version: 0.63.1
11
+ version: 0.64.0
12
12
 
13
13
  environment:
14
14
  sdk: ^3.5.0
@@ -17,9 +17,11 @@ dependencies:
17
17
  # The pure-data core (wire types, offer codec, NomosReads, schema validation, error
18
18
  # info). nomos_client depends on and re-exports it, so existing imports are unchanged.
19
19
  # For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
20
- nomos_types: ^0.7.1
20
+ nomos_types: ^0.8.0
21
21
  json_schema: ^5.2.2
22
22
  crypto: ^3.0.0
23
+ package_config: ^2.1.0
24
+ meta: ^1.12.0
23
25
 
24
26
  dev_dependencies:
25
27
  test: ^1.25.0
@@ -5,7 +5,8 @@
5
5
  // returns. Runs hermetically on the offline runner (the "cloud" is the runner's offline shim, so
6
6
  // no network is ever contacted; what is under test is the acquire protocol, not the transport).
7
7
  //
8
- // This file also IS the gap-1 regression: it imports ONLY `nomos_headless.dart` and TYPES a
8
+ // This file also IS the internal conformance regression: it imports only the obscured
9
+ // framework conformance runtime and types a
9
10
  // `Future<NomosSessionCredentials> Function()` callback — before the barrel fix that did not compile.
10
11
  @TestOn('vm')
11
12
  library;
@@ -13,18 +14,21 @@ library;
13
14
  import 'dart:async';
14
15
  import 'dart:io';
15
16
 
16
- import 'package:nomos_client/nomos_headless.dart';
17
+ import 'package:nomos_client/src/nms_conformance_runtime.dart';
17
18
  import 'package:test/test.dart';
18
19
 
19
20
  String get _root => '${Directory.current.path}/../..';
20
21
  String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
21
- String get _docsDeploy => '$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
22
+ String get _docsDeploy =>
23
+ '$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
22
24
  String get _runtimeDir =>
23
25
  Platform.environment['NOMOS_DART_RUNTIME'] ??
26
+ Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
24
27
  '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
25
28
 
26
29
  String? _missingPrereq() {
27
- if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
30
+ if (!File(_repoEntry).existsSync())
31
+ return 'in-repo runner not found at $_repoEntry';
28
32
  if (!File(_docsDeploy).existsSync()) {
29
33
  return 'missing fixture $_docsDeploy — compile it: '
30
34
  '(cd bench/scale/fixtures/surfaced && node ../../../../dsl/compile_package.mjs)';
@@ -46,12 +50,14 @@ Future<T> _leg<T>(String name, Future<T> Function() body) async {
46
50
  print('✔ ACQUIRE $name (${sw.elapsedMilliseconds}ms)');
47
51
  return r;
48
52
  } catch (e, st) {
49
- throw TestFailure('ACQUIRE $name FAILED after ${sw.elapsedMilliseconds}ms:\n$e\n$st');
53
+ throw TestFailure(
54
+ 'ACQUIRE $name FAILED after ${sw.elapsedMilliseconds}ms:\n$e\n$st');
50
55
  }
51
56
  }
52
57
 
53
58
  void main() {
54
- test('acquireSession: the absent-workspace lane invokes ifAbsentCredentials exactly once',
59
+ test(
60
+ 'acquireSession: the absent-workspace lane invokes ifAbsentCredentials exactly once',
55
61
  () async {
56
62
  final missing = _missingPrereq();
57
63
  if (missing != null) {
@@ -70,10 +76,13 @@ void main() {
70
76
 
71
77
  // ── LEG 1 (gap 1, compile-time + runtime): the callback TYPE is reachable from the headless
72
78
  // barrel alone, and a credentials object constructs with the documented fields. ──
73
- await _leg('leg 1: NomosSessionCredentials is exported from nomos_headless', () async {
79
+ await _leg(
80
+ 'leg 1: internal conformance runtime carries session credentials',
81
+ () async {
74
82
  // ignore: prefer_function_declarations_over_variables
75
- final Future<NomosSessionCredentials> Function() typedCallback = () async =>
76
- const NomosSessionCredentials(cloud: 'https://offline.invalid', allowUnborn: true);
83
+ final Future<NomosSessionCredentials> Function() typedCallback =
84
+ () async => const NomosSessionCredentials(
85
+ cloud: 'https://offline.invalid', allowUnborn: true);
77
86
  final c = await typedCallback();
78
87
  expect(c.cloud, 'https://offline.invalid');
79
88
  expect(c.allowUnborn, isTrue);
@@ -81,7 +90,9 @@ void main() {
81
90
 
82
91
  // ── LEG 2: a genuinely-absent workspace WITHOUT a callback is a NAMED StateError, never a
83
92
  // silent cloudless mount. ──
84
- await _leg('leg 2: absent workspace without credentials refuses (StateError)', () async {
93
+ await _leg(
94
+ 'leg 2: absent workspace without credentials refuses (StateError)',
95
+ () async {
85
96
  await expectLater(
86
97
  plane.acquireSession('nowhere-at-all'),
87
98
  throwsA(isA<StateError>().having((e) => e.toString(), 'message',
@@ -92,26 +103,32 @@ void main() {
92
103
  // callback fires EXACTLY ONCE (single-flight), the connect is made with its credentials,
93
104
  // and both callers get the SAME mounted session. ──
94
105
  var invocations = 0;
95
- await _leg('leg 3: concurrent double-acquire → one credentials invocation, one handle',
106
+ await _leg(
107
+ 'leg 3: concurrent double-acquire → one credentials invocation, one handle',
96
108
  () async {
97
109
  Future<NomosSessionCredentials> creds() async {
98
110
  invocations++;
99
111
  return const NomosSessionCredentials(
100
- cloud: 'https://offline.invalid', clientId: 'acquired-client', allowUnborn: true);
112
+ cloud: 'https://offline.invalid',
113
+ clientId: 'acquired-client',
114
+ allowUnborn: true);
101
115
  }
102
116
 
103
117
  final acquired = await Future.wait([
104
118
  plane.acquireSession('cloud-opened-ws', ifAbsentCredentials: creds),
105
119
  plane.acquireSession('cloud-opened-ws', ifAbsentCredentials: creds),
106
120
  ]).timeout(const Duration(seconds: 60),
107
- onTimeout: () => throw StateError('the cloud-open acquire never completed'));
121
+ onTimeout: () =>
122
+ throw StateError('the cloud-open acquire never completed'));
108
123
  expect(invocations, 1,
109
- reason: 'a concurrent double-acquire must invoke the credentials callback ONCE');
124
+ reason:
125
+ 'a concurrent double-acquire must invoke the credentials callback ONCE');
110
126
  expect(identical(acquired[0], acquired[1]), isTrue,
111
127
  reason: 'both concurrent acquirers get the SAME session handle');
112
128
  expect(acquired[0].workspace, 'cloud-opened-ws');
113
129
  final listed = await plane.listWorkspaces();
114
- expect(listed.map((w) => w['session'].toString()), contains('cloud-opened-ws'),
130
+ expect(listed.map((w) => w['session'].toString()),
131
+ contains('cloud-opened-ws'),
115
132
  reason: 'the acquired workspace is mounted on the plane');
116
133
  });
117
134
 
@@ -119,8 +136,8 @@ void main() {
119
136
  // NEVER invoked again. ──
120
137
  await _leg('leg 4: re-acquire is credential-free (fast path)', () async {
121
138
  final again = await plane.acquireSession('cloud-opened-ws',
122
- ifAbsentCredentials: () async =>
123
- fail('the workspace is mounted — credentials must not be requested'));
139
+ ifAbsentCredentials: () async => fail(
140
+ 'the workspace is mounted — credentials must not be requested'));
124
141
  expect(again.workspace, 'cloud-opened-ws');
125
142
  expect(invocations, 1);
126
143
  });