@githolon/dsl 0.108.0 → 0.108.2
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 +5 -0
- package/dart/lib/src/application_framework_calls.dart +299 -0
- package/dart/lib/src/generated_call.dart +96 -0
- package/dart/lib/src/nms_generated_runtime.dart +29 -0
- package/dart/lib/src/node_runner_transport.dart +77 -1
- package/dart/lib/src/result_usda.dart +487 -0
- package/dart/lib/src/runner/node-entry.mjs +6401 -2102
- package/dart/lib/src/runner/offline-install.mjs +1538 -163
- package/dart/lib/src/runtime_v2_gateway.dart +54 -0
- package/dart/lib/src/runtime_v2_transport.dart +117 -0
- package/dart/lib/src/session_bound.dart +7 -2
- package/dart/lib/src/test_harness.dart +193 -112
- package/dart/lib/src/webview_bridge.dart +2120 -569
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/acquire_session_test.dart +68 -14
- package/dart/test/adv_realm_interference_test.dart +6 -4
- package/dart/test/cluster_births_test.dart +11 -8
- package/dart/test/cluster_local_autosync_test.dart +1 -1
- package/dart/test/cluster_scale_test.dart +3 -2
- package/dart/test/dlq_session_surface_test.dart +52 -35
- package/dart/test/first_app_gauntlet_test.dart +1 -1
- package/dart/test/local_sync_inert_test.dart +23 -26
- package/dart/test/offer_authentication_proof_test.dart +193 -0
- package/dart/test/result_usda_test.dart +49 -0
- package/dart/test/session_bound_test.dart +8 -11
- package/dart/test/stale_bundle_test.dart +18 -3
- package/dart/test/test_harness_test.dart +14 -27
- package/dart/test/webview_bridge_test.dart +504 -83
- package/dart_types/CHANGELOG.md +7 -0
- package/dart_types/lib/nomos_kernel_protocol.dart +3 -2
- package/dart_types/lib/src/custody.dart +64 -19
- package/dart_types/lib/src/local_work_rejected.dart +30 -45
- package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
- package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +2021 -234
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +175 -8
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
- package/dart_types/lib/src/surfaced.dart +30 -25
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +2 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +5736 -2047
- package/src/authoring.ts +3 -2
- package/src/births.ts +21 -0
- package/src/build_package.ts +30 -0
- package/src/business_birth.ts +39 -35
- package/src/codegen_call_usda.ts +1111 -0
- package/src/codegen_dart.ts +207 -282
- package/src/codegen_dart_application.ts +195 -252
- package/src/codegen_dart_client.ts +1978 -593
- package/src/codegen_dart_framework_calls.ts +291 -0
- package/src/codegen_dart_scenario.ts +7 -0
- package/src/codegen_proof.ts +3 -4
- package/src/codegen_provider_dart.ts +21 -7
- package/src/codegen_query_usda.ts +214 -0
- package/src/codegen_result_usda.ts +685 -0
- package/src/codegen_ts.ts +3945 -1111
- package/src/compile_package_main.ts +360 -32
- package/src/directive.ts +19 -4
- package/src/engine_entry.ts +45 -4
- package/src/framework/application_home.ts +2 -0
- package/src/framework/auth_provider_law.ts +262 -20
- package/src/framework/home.ts +32 -28
- package/src/framework/owned_custody.ts +19 -11
- package/src/framework/owned_custody_value.ts +51 -0
- package/src/framework/relations.ts +57 -21
- package/src/framework/signing_identity.ts +34 -0
- package/src/framework/workspaces.ts +31 -9
- package/src/framework_call_definitions.ts +53 -0
- package/src/framework_read_definitions.ts +45 -0
- package/src/identity_frontier.ts +58 -0
- package/src/index.ts +20 -1
- package/src/intent.ts +216 -16
- package/src/law_check.ts +19 -1
- package/src/law_graph.ts +57 -8
- package/src/law_graph_usda.ts +73 -5
- package/src/law_value_type.ts +106 -19
- package/src/read.ts +47 -0
- package/src/read_projection_namespace.ts +7 -0
- package/src/shape_compat.ts +38 -39
- package/src/stable_ids.ts +37 -2
- package/src/stable_ids_for_module.ts +64 -4
- package/src/test_call_usda.ts +83 -0
- package/src/wire.ts +36 -2
- package/src/wire_encode.ts +45 -1
- package/src/workspace_sharing.ts +27 -10
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbjson.dart +0 -6096
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbserver.dart +0 -50
- package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbjson.dart +0 -178
- package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pb.dart +0 -828
- package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbenum.dart +0 -465
- package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbjson.dart +0 -573
- package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbserver.dart +0 -50
|
@@ -20,7 +20,9 @@ import 'package:meta/meta.dart' show internal;
|
|
|
20
20
|
import 'dart:async';
|
|
21
21
|
import 'dart:convert';
|
|
22
22
|
import 'dart:math';
|
|
23
|
+
import 'dart:typed_data';
|
|
23
24
|
|
|
25
|
+
import 'package:fixnum/fixnum.dart' as fixnum;
|
|
24
26
|
import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
|
|
25
27
|
import 'package:nomos_types/nomos_types.dart'
|
|
26
28
|
show
|
|
@@ -45,6 +47,14 @@ import 'package:nomos_types/src/foreign_ref.dart'
|
|
|
45
47
|
|
|
46
48
|
import 'session_bound.dart'
|
|
47
49
|
show DomainClientModelAdoptionRequired, DomainNotInstalledForSession;
|
|
50
|
+
import 'generated_call.dart' show NmsGeneratedCall, NmsGeneratedQuery;
|
|
51
|
+
import 'application_framework_calls.dart' show OwnedCustody, OwnedDeviceRequest;
|
|
52
|
+
import 'runtime_v2_gateway.dart' show NmsRuntimeV2Gateway;
|
|
53
|
+
import 'runtime_v2_transport.dart'
|
|
54
|
+
show
|
|
55
|
+
NomosKernelCallTransport,
|
|
56
|
+
NomosRuntimeV2EventTransport,
|
|
57
|
+
NomosRuntimeV2Transport;
|
|
48
58
|
|
|
49
59
|
/// THE ENGINE (architecture/session_bound_clients.md): the multi-workspace RUNTIME — mount/connect,
|
|
50
60
|
/// the session registry, sync, custody + recovery, the home ceremony, and [NomosBridge.follow]
|
|
@@ -76,6 +86,48 @@ typedef NomosBirthCertificate = nomos_protocol.BirthCertificate;
|
|
|
76
86
|
/// with its signature populated; the signing secret is a separate argument and never enters the value.
|
|
77
87
|
typedef NomosBirthCertificateSpec = nomos_protocol.BirthCertificate;
|
|
78
88
|
|
|
89
|
+
/// Typed compare-and-merge witness carried by an offer. The protobuf schema is
|
|
90
|
+
/// the only transport shape; generated conflict helpers never rebuild it as a
|
|
91
|
+
/// string-keyed map.
|
|
92
|
+
typedef NomosConflictSnapshot = nomos_protocol.ConflictSnapshot;
|
|
93
|
+
|
|
94
|
+
/// Typed result of one child-workspace birth. The kernel protobuf is the
|
|
95
|
+
/// transport authority; hosts do not reinterpret birth state through maps.
|
|
96
|
+
typedef NomosBirthOutcome = nomos_protocol.BirthOutcome;
|
|
97
|
+
|
|
98
|
+
typedef NomosBirthOutcomeStatus = nomos_protocol.BirthOutcomeStatus;
|
|
99
|
+
|
|
100
|
+
NomosBirthOutcomeStatus _birthOutcomeStatusFromLegacyWire(Object? value) {
|
|
101
|
+
switch (value?.toString().toLowerCase()) {
|
|
102
|
+
case 'born':
|
|
103
|
+
return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN;
|
|
104
|
+
case 'deferred':
|
|
105
|
+
return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_DEFERRED;
|
|
106
|
+
case 'failed':
|
|
107
|
+
return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_FAILED;
|
|
108
|
+
case 'refused':
|
|
109
|
+
return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_REFUSED;
|
|
110
|
+
case 'unwarranted_cert':
|
|
111
|
+
case 'unwarrantedcert':
|
|
112
|
+
return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_UNWARRANTED_CERT;
|
|
113
|
+
default:
|
|
114
|
+
return NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_UNSPECIFIED;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
NomosBirthOutcome _birthOutcomeFromLegacyWire(Map<dynamic, dynamic> value) =>
|
|
119
|
+
NomosBirthOutcome(
|
|
120
|
+
workspace: value['workspace']?.toString(),
|
|
121
|
+
status: _birthOutcomeStatusFromLegacyWire(value['status']),
|
|
122
|
+
reason: value['reason']?.toString(),
|
|
123
|
+
code: value['code']?.toString(),
|
|
124
|
+
replayed: value['replayed'] == true,
|
|
125
|
+
lawHash: value['lawHash']?.toString(),
|
|
126
|
+
parentWorkspace: value['parentWorkspace']?.toString(),
|
|
127
|
+
childWorkspace: value['childWorkspace']?.toString(),
|
|
128
|
+
certificateRefusalReason: value['certificateRefusalReason']?.toString(),
|
|
129
|
+
);
|
|
130
|
+
|
|
79
131
|
/// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
|
|
80
132
|
const int kWebviewBridgeVersion = 1;
|
|
81
133
|
|
|
@@ -107,10 +159,10 @@ class StaleRunnerBundle implements Exception {
|
|
|
107
159
|
'NomosBridge(..., allowStaleRunnerBundle: true) for a deliberate mixed-version setup.';
|
|
108
160
|
}
|
|
109
161
|
|
|
110
|
-
/// The
|
|
111
|
-
///
|
|
112
|
-
///
|
|
113
|
-
abstract interface class BridgeTransport {
|
|
162
|
+
/// The application transport. Every host must provide the generated Runtime
|
|
163
|
+
/// v2 frame lane; [send] and [messages] remain only for operations and pushed
|
|
164
|
+
/// events that have not completed that migration yet.
|
|
165
|
+
abstract interface class BridgeTransport implements NomosRuntimeV2Transport {
|
|
114
166
|
void send(Map<String, Object?> message);
|
|
115
167
|
Stream<Map<String, Object?>> get messages;
|
|
116
168
|
}
|
|
@@ -132,11 +184,17 @@ class TransferProgress {
|
|
|
132
184
|
? null
|
|
133
185
|
: (received / total!).clamp(0.0, 1.0);
|
|
134
186
|
bool get done => total != null && received >= total!;
|
|
135
|
-
factory TransferProgress.
|
|
187
|
+
factory TransferProgress.fromRuntime(
|
|
188
|
+
nomos_protocol.TransferProgress progress,
|
|
189
|
+
) =>
|
|
136
190
|
TransferProgress(
|
|
137
|
-
received:
|
|
138
|
-
total:
|
|
139
|
-
kind:
|
|
191
|
+
received: progress.receivedBytes.toInt(),
|
|
192
|
+
total: progress.hasTotalBytes() ? progress.totalBytes.toInt() : null,
|
|
193
|
+
kind: switch (progress.kind) {
|
|
194
|
+
nomos_protocol.TransferKind.TRANSFER_KIND_FULL => 'full',
|
|
195
|
+
nomos_protocol.TransferKind.TRANSFER_KIND_PUSH => 'push',
|
|
196
|
+
_ => 'delta',
|
|
197
|
+
},
|
|
140
198
|
);
|
|
141
199
|
}
|
|
142
200
|
|
|
@@ -215,46 +273,56 @@ final class KernelProgress {
|
|
|
215
273
|
double? get fraction =>
|
|
216
274
|
(total <= 0) ? null : (completed / total).clamp(0.0, 1.0);
|
|
217
275
|
|
|
218
|
-
factory KernelProgress.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
276
|
+
factory KernelProgress.fromRuntime(
|
|
277
|
+
nomos_protocol.KernelProgressObserved observed,
|
|
278
|
+
) {
|
|
279
|
+
final snapshot = observed.snapshot;
|
|
280
|
+
return KernelProgress(
|
|
281
|
+
sequence: snapshot.sequence.toInt(),
|
|
282
|
+
operation: snapshot.operation.value == 1
|
|
283
|
+
? KernelProgressOperation.restoreMain
|
|
284
|
+
: KernelProgressOperation.unspecified,
|
|
285
|
+
phase: switch (snapshot.phase.value) {
|
|
286
|
+
1 => KernelProgressPhase.checkCustody,
|
|
287
|
+
2 => KernelProgressPhase.readChain,
|
|
288
|
+
3 => KernelProgressPhase.initializeEngine,
|
|
289
|
+
4 => KernelProgressPhase.readTimeline,
|
|
290
|
+
5 => KernelProgressPhase.hydrateLaw,
|
|
291
|
+
6 => KernelProgressPhase.recoveryPrescan,
|
|
292
|
+
7 => KernelProgressPhase.verifyIntents,
|
|
293
|
+
8 => KernelProgressPhase.promoteCustody,
|
|
294
|
+
9 => KernelProgressPhase.rebuildFrontier,
|
|
295
|
+
10 => KernelProgressPhase.complete,
|
|
296
|
+
_ => KernelProgressPhase.unspecified,
|
|
297
|
+
},
|
|
298
|
+
completed: snapshot.completedUnits.toInt(),
|
|
299
|
+
total: snapshot.totalUnits.toInt(),
|
|
300
|
+
unit: switch (snapshot.unit.value) {
|
|
301
|
+
1 => KernelProgressUnit.intent,
|
|
302
|
+
2 => KernelProgressUnit.lawPackage,
|
|
303
|
+
_ => KernelProgressUnit.unspecified,
|
|
304
|
+
},
|
|
305
|
+
terminal: snapshot.terminal,
|
|
306
|
+
lawHydrationStep: switch (snapshot.lawHydrationStep.value) {
|
|
307
|
+
1 => KernelLawHydrationStep.readClosure,
|
|
308
|
+
2 => KernelLawHydrationStep.composeStage,
|
|
309
|
+
3 => KernelLawHydrationStep.deriveSchemas,
|
|
310
|
+
4 => KernelLawHydrationStep.loadPolicyShell,
|
|
311
|
+
5 => KernelLawHydrationStep.packageComplete,
|
|
312
|
+
_ => KernelLawHydrationStep.unspecified,
|
|
313
|
+
},
|
|
314
|
+
lawPackageSha256: snapshot.lawPackageSha256,
|
|
315
|
+
hostObservedElapsedMicros: observed.hostObservedElapsedMicros.toInt(),
|
|
316
|
+
hostObservedPhaseElapsedMicros:
|
|
317
|
+
observed.hostObservedPhaseElapsedMicros.toInt(),
|
|
318
|
+
kernelAllocatorLiveBytes: snapshot.allocatorLiveBytes.toInt(),
|
|
319
|
+
kernelAllocatorPeakBytes: snapshot.allocatorPeakBytes.toInt(),
|
|
320
|
+
processResidentBytes: observed.processResidentBytes.toInt(),
|
|
321
|
+
processPeakResidentBytes: observed.processPeakResidentBytes.toInt(),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
254
324
|
}
|
|
255
325
|
|
|
256
|
-
int? _wireInteger(Object? value) => value is num ? value.toInt() : null;
|
|
257
|
-
|
|
258
326
|
/// One opaque point in this workspace's verified history. Applications use it as the handle passed to a
|
|
259
327
|
/// generated client's `past.at(moment)`; [id] is framework identity, not a business field and need never be
|
|
260
328
|
/// parsed. The remaining fields make a timeline useful without exposing commit or event-sourcing ceremony.
|
|
@@ -387,7 +455,7 @@ class NomosCreated {
|
|
|
387
455
|
/// so the CANONICAL axes are axis-neutral (`[minX, minY] .. [maxX, maxY]`); the axes' meaning
|
|
388
456
|
/// (earth coordinates, floor-plan mm, anything) is the tenant's business. The original geographic
|
|
389
457
|
/// `minLng`/`minLat`/`maxLng`/`maxLat` named params remain accepted forever (era rule) — pass either
|
|
390
|
-
/// shape.
|
|
458
|
+
/// source-level shape. The exact Runtime v2 protobuf boundary carries only the canonical axes.
|
|
391
459
|
class NomosBbox {
|
|
392
460
|
final double minX;
|
|
393
461
|
final double minY;
|
|
@@ -427,18 +495,6 @@ class NomosBbox {
|
|
|
427
495
|
'Spatial axes are generic; use maxX/maxY. Kept for source compat.',
|
|
428
496
|
)
|
|
429
497
|
double get maxLat => maxY;
|
|
430
|
-
|
|
431
|
-
Map<String, Object?> toJson() => {
|
|
432
|
-
'minX': minX,
|
|
433
|
-
'minY': minY,
|
|
434
|
-
'maxX': maxX,
|
|
435
|
-
'maxY': maxY,
|
|
436
|
-
// Geographic aliases so a pre-genericisation runner (which reads minLng/…) still works.
|
|
437
|
-
'minLng': minX,
|
|
438
|
-
'minLat': minY,
|
|
439
|
-
'maxLng': maxX,
|
|
440
|
-
'maxLat': maxY,
|
|
441
|
-
};
|
|
442
498
|
}
|
|
443
499
|
|
|
444
500
|
/// One member's structural compat verdict — `ok`, plus the exact break [reason] when not.
|
|
@@ -526,7 +582,7 @@ class NomosScopeKey {
|
|
|
526
582
|
final String? ownedKeySubject;
|
|
527
583
|
|
|
528
584
|
/// Framework-private encrypted handoff, present only while seating the first device.
|
|
529
|
-
final
|
|
585
|
+
final OwnedCustody? ownedCustody;
|
|
530
586
|
|
|
531
587
|
/// Framework-private deterministic child address, present only for an unborn owned business aggregate.
|
|
532
588
|
final String? workspace;
|
|
@@ -548,14 +604,18 @@ class NomosScopeKey {
|
|
|
548
604
|
};
|
|
549
605
|
}
|
|
550
606
|
|
|
551
|
-
/// The entire app-facing multi-device surface for owned business state. The
|
|
552
|
-
///
|
|
607
|
+
/// The entire app-facing multi-device surface for owned business state. The
|
|
608
|
+
/// request is one compiler-generated framework value; transport encoders may
|
|
609
|
+
/// package it as USDA for QR/share without turning it into an opaque string.
|
|
553
610
|
class NomosDevices {
|
|
554
611
|
final NomosBridge bridge;
|
|
555
612
|
final String domain;
|
|
556
613
|
final String domainHash;
|
|
557
614
|
final String? actor;
|
|
558
615
|
final String scope;
|
|
616
|
+
final Future<NomosOfferOutcome> Function(
|
|
617
|
+
NomosOwnedDeviceApprovalPreparation preparation,
|
|
618
|
+
) approvePrepared;
|
|
559
619
|
|
|
560
620
|
const NomosDevices({
|
|
561
621
|
required this.bridge,
|
|
@@ -563,11 +623,12 @@ class NomosDevices {
|
|
|
563
623
|
required this.domainHash,
|
|
564
624
|
required this.actor,
|
|
565
625
|
required this.scope,
|
|
626
|
+
required this.approvePrepared,
|
|
566
627
|
});
|
|
567
628
|
|
|
568
|
-
Future<
|
|
629
|
+
Future<OwnedDeviceRequest> request() => bridge.nomosOwnedDeviceRequest(scope);
|
|
569
630
|
|
|
570
|
-
Future<NomosOfferOutcome> approve(
|
|
631
|
+
Future<NomosOfferOutcome> approve(OwnedDeviceRequest request) async {
|
|
571
632
|
final principal = actor ?? bridge.principal;
|
|
572
633
|
if (principal == null || principal.trim().isEmpty) {
|
|
573
634
|
throw StateError('devices.approve() needs a signed-in Nomos session');
|
|
@@ -576,13 +637,7 @@ class NomosDevices {
|
|
|
576
637
|
request,
|
|
577
638
|
scope,
|
|
578
639
|
);
|
|
579
|
-
return
|
|
580
|
-
domain: domain,
|
|
581
|
-
directiveId: 'nomos_internal_approve_owned_device_7c1f5e',
|
|
582
|
-
payload: payload,
|
|
583
|
-
actor: principal,
|
|
584
|
-
domainHash: domainHash,
|
|
585
|
-
);
|
|
640
|
+
return approvePrepared(payload);
|
|
586
641
|
}
|
|
587
642
|
}
|
|
588
643
|
|
|
@@ -739,8 +794,8 @@ class EncryptionRosterMismatch implements Exception {
|
|
|
739
794
|
/// Without this, the app would open the child and poll `head:null` until timeout with no reason. Callers catch
|
|
740
795
|
/// it to see the child name(s), the kernel [code], and the [reason]. [head] is the parent write that landed.
|
|
741
796
|
class BirthIncomplete implements Exception {
|
|
742
|
-
/// The
|
|
743
|
-
final List<
|
|
797
|
+
/// The failed typed kernel outcomes, status != [NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN].
|
|
798
|
+
final List<NomosBirthOutcome> failedBirths;
|
|
744
799
|
|
|
745
800
|
/// The parent write's local head — the parent offer itself sealed; only the child did not come up.
|
|
746
801
|
final String head;
|
|
@@ -749,28 +804,25 @@ class BirthIncomplete implements Exception {
|
|
|
749
804
|
|
|
750
805
|
/// The first failed child's kernel code (e.g. `BirthLawPackageUnavailable`), or `BirthFailed`.
|
|
751
806
|
String get code =>
|
|
752
|
-
|
|
753
|
-
? failedBirths.first
|
|
754
|
-
:
|
|
755
|
-
'BirthFailed';
|
|
807
|
+
failedBirths.isNotEmpty && failedBirths.first.code.isNotEmpty
|
|
808
|
+
? failedBirths.first.code
|
|
809
|
+
: 'BirthFailed';
|
|
756
810
|
|
|
757
811
|
/// The child workspace name(s) that did not come up.
|
|
758
|
-
List<String> get workspaces =>
|
|
759
|
-
.map((o) =>
|
|
760
|
-
.where((s) => s.isNotEmpty)
|
|
761
|
-
.toList();
|
|
812
|
+
List<String> get workspaces =>
|
|
813
|
+
failedBirths.map((o) => o.workspace).where((s) => s.isNotEmpty).toList();
|
|
762
814
|
|
|
763
815
|
@override
|
|
764
816
|
String toString() {
|
|
765
817
|
final list = failedBirths
|
|
766
818
|
.map(
|
|
767
819
|
(o) =>
|
|
768
|
-
'${o
|
|
769
|
-
'${o
|
|
820
|
+
'${o.workspace.isEmpty ? '?' : o.workspace} [${o.status.name}${o.code.isNotEmpty ? ': ${o.code}' : ''}]'
|
|
821
|
+
'${o.reason.isNotEmpty ? ' — ${o.reason}' : ''}',
|
|
770
822
|
)
|
|
771
823
|
.join('; ');
|
|
772
824
|
final lawUnavailable = failedBirths.any(
|
|
773
|
-
(o) => o
|
|
825
|
+
(o) => o.code == 'BirthLawPackageUnavailable',
|
|
774
826
|
);
|
|
775
827
|
return 'BirthIncomplete: ${failedBirths.length} child workspace(s) did NOT come up — $list. '
|
|
776
828
|
'The parent offer admitted, but the child was not fully born, so opening it would poll head:null '
|
|
@@ -792,6 +844,254 @@ class NomosEpochResult {
|
|
|
792
844
|
});
|
|
793
845
|
}
|
|
794
846
|
|
|
847
|
+
String _nomosOwnedRequiredString(
|
|
848
|
+
Map<String, Object?> source,
|
|
849
|
+
String field,
|
|
850
|
+
) {
|
|
851
|
+
final value = source[field];
|
|
852
|
+
if (value is! String || value.isEmpty) {
|
|
853
|
+
throw FormatException('Nomos owned preparation is missing String $field');
|
|
854
|
+
}
|
|
855
|
+
return value;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
int _nomosOwnedRequiredInt(Map<String, Object?> source, String field) {
|
|
859
|
+
final value = source[field];
|
|
860
|
+
if (value is! num || value.toInt() != value) {
|
|
861
|
+
throw FormatException('Nomos owned preparation is missing int $field');
|
|
862
|
+
}
|
|
863
|
+
return value.toInt();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
List<String> _nomosOwnedRequiredStrings(
|
|
867
|
+
Map<String, Object?> source,
|
|
868
|
+
String field,
|
|
869
|
+
) {
|
|
870
|
+
final value = source[field];
|
|
871
|
+
if (value is! List || value.any((item) => item is! String)) {
|
|
872
|
+
throw FormatException(
|
|
873
|
+
'Nomos owned preparation is missing List<String> $field');
|
|
874
|
+
}
|
|
875
|
+
return value.cast<String>().toList(growable: false);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
OwnedDeviceRequest _nomosOwnedDeviceRequestFromTransport(Object? raw) {
|
|
879
|
+
if (raw is! Map) {
|
|
880
|
+
throw const FormatException('Nomos owned device request is not an object');
|
|
881
|
+
}
|
|
882
|
+
final value = Map<String, Object?>.from(raw);
|
|
883
|
+
const fields = <String>{
|
|
884
|
+
'v',
|
|
885
|
+
'workspace',
|
|
886
|
+
'principal',
|
|
887
|
+
'scope',
|
|
888
|
+
'signingPublic',
|
|
889
|
+
'wrappingPublic',
|
|
890
|
+
'signature',
|
|
891
|
+
};
|
|
892
|
+
if (value.keys.any((key) => !fields.contains(key)) ||
|
|
893
|
+
fields.any((key) => !value.containsKey(key))) {
|
|
894
|
+
throw const FormatException(
|
|
895
|
+
'Nomos owned device request has an invalid field set');
|
|
896
|
+
}
|
|
897
|
+
return OwnedDeviceRequest(
|
|
898
|
+
v: _nomosOwnedRequiredInt(value, 'v'),
|
|
899
|
+
workspace: _nomosOwnedRequiredString(value, 'workspace'),
|
|
900
|
+
principal: _nomosOwnedRequiredString(value, 'principal'),
|
|
901
|
+
scope: _nomosOwnedRequiredString(value, 'scope'),
|
|
902
|
+
signingPublic: _nomosOwnedRequiredString(value, 'signingPublic'),
|
|
903
|
+
wrappingPublic: _nomosOwnedRequiredString(value, 'wrappingPublic'),
|
|
904
|
+
signature: _nomosOwnedRequiredString(value, 'signature'),
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
OwnedCustody _nomosOwnedCustodyFromTransport(Object? raw) {
|
|
909
|
+
if (raw is! Map) {
|
|
910
|
+
throw const FormatException('Nomos owned custody is not an object');
|
|
911
|
+
}
|
|
912
|
+
final value = Map<String, Object?>.from(raw);
|
|
913
|
+
const fields = <String>{
|
|
914
|
+
'v',
|
|
915
|
+
'request',
|
|
916
|
+
'hpkeEpk',
|
|
917
|
+
'ct',
|
|
918
|
+
'keyCommitment',
|
|
919
|
+
'principalWrappingPublic',
|
|
920
|
+
};
|
|
921
|
+
if (value.keys.any((key) => !fields.contains(key)) ||
|
|
922
|
+
fields.any((key) => !value.containsKey(key))) {
|
|
923
|
+
throw const FormatException('Nomos owned custody has an invalid field set');
|
|
924
|
+
}
|
|
925
|
+
return OwnedCustody(
|
|
926
|
+
v: _nomosOwnedRequiredInt(value, 'v'),
|
|
927
|
+
request: _nomosOwnedDeviceRequestFromTransport(value['request']),
|
|
928
|
+
hpkeEpk: _nomosOwnedRequiredString(value, 'hpkeEpk'),
|
|
929
|
+
ct: _nomosOwnedRequiredString(value, 'ct'),
|
|
930
|
+
keyCommitment: _nomosOwnedRequiredString(value, 'keyCommitment'),
|
|
931
|
+
principalWrappingPublic:
|
|
932
|
+
_nomosOwnedRequiredString(value, 'principalWrappingPublic'),
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/// One exact key wrap prepared by the framework-owned custody machinery.
|
|
937
|
+
/// Generated clients lower this value directly to canonical USDA; it is never a
|
|
938
|
+
/// tenant-authored dictionary.
|
|
939
|
+
class NomosOwnedWrapPreparation {
|
|
940
|
+
final String recipientPrincipal;
|
|
941
|
+
final String recipientPubKey;
|
|
942
|
+
final String hpkeEpk;
|
|
943
|
+
final String ct;
|
|
944
|
+
|
|
945
|
+
const NomosOwnedWrapPreparation({
|
|
946
|
+
required this.recipientPrincipal,
|
|
947
|
+
required this.recipientPubKey,
|
|
948
|
+
required this.hpkeEpk,
|
|
949
|
+
required this.ct,
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
factory NomosOwnedWrapPreparation.fromTransport(
|
|
953
|
+
Map<String, Object?> value,
|
|
954
|
+
) {
|
|
955
|
+
return NomosOwnedWrapPreparation(
|
|
956
|
+
recipientPrincipal:
|
|
957
|
+
_nomosOwnedRequiredString(value, 'recipientPrincipal'),
|
|
958
|
+
recipientPubKey: _nomosOwnedRequiredString(value, 'recipientPubKey'),
|
|
959
|
+
hpkeEpk: _nomosOwnedRequiredString(value, 'hpkeEpk'),
|
|
960
|
+
ct: _nomosOwnedRequiredString(value, 'ct'),
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/// A wrap which explicitly identifies the historic epoch it opens.
|
|
966
|
+
class NomosHistoricOwnedWrapPreparation extends NomosOwnedWrapPreparation {
|
|
967
|
+
final int epoch;
|
|
968
|
+
|
|
969
|
+
const NomosHistoricOwnedWrapPreparation({
|
|
970
|
+
required super.recipientPrincipal,
|
|
971
|
+
required super.recipientPubKey,
|
|
972
|
+
required super.hpkeEpk,
|
|
973
|
+
required super.ct,
|
|
974
|
+
required this.epoch,
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
factory NomosHistoricOwnedWrapPreparation.fromTransport(
|
|
978
|
+
Map<String, Object?> value,
|
|
979
|
+
) {
|
|
980
|
+
return NomosHistoricOwnedWrapPreparation(
|
|
981
|
+
recipientPrincipal:
|
|
982
|
+
_nomosOwnedRequiredString(value, 'recipientPrincipal'),
|
|
983
|
+
recipientPubKey: _nomosOwnedRequiredString(value, 'recipientPubKey'),
|
|
984
|
+
hpkeEpk: _nomosOwnedRequiredString(value, 'hpkeEpk'),
|
|
985
|
+
ct: _nomosOwnedRequiredString(value, 'ct'),
|
|
986
|
+
epoch: _nomosOwnedRequiredInt(value, 'epoch'),
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/// Exhaustive result of the framework-owned record preparation operation.
|
|
992
|
+
/// Each subtype corresponds to one law directive and therefore one generated
|
|
993
|
+
/// USDA encoder.
|
|
994
|
+
sealed class NomosOwnedRecordPreparation {
|
|
995
|
+
final String ownedKeySubject;
|
|
996
|
+
final OwnedCustody? ownedCustody;
|
|
997
|
+
|
|
998
|
+
const NomosOwnedRecordPreparation({
|
|
999
|
+
required this.ownedKeySubject,
|
|
1000
|
+
this.ownedCustody,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
class NomosOwnedSharePreparation extends NomosOwnedRecordPreparation {
|
|
1005
|
+
final String recordId;
|
|
1006
|
+
final String peer;
|
|
1007
|
+
final int epoch;
|
|
1008
|
+
final String sharedAt;
|
|
1009
|
+
final List<NomosHistoricOwnedWrapPreparation> wraps;
|
|
1010
|
+
|
|
1011
|
+
const NomosOwnedSharePreparation({
|
|
1012
|
+
required super.ownedKeySubject,
|
|
1013
|
+
super.ownedCustody,
|
|
1014
|
+
required this.recordId,
|
|
1015
|
+
required this.peer,
|
|
1016
|
+
required this.epoch,
|
|
1017
|
+
required this.sharedAt,
|
|
1018
|
+
required this.wraps,
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
class NomosOwnedUnsharePreparation extends NomosOwnedRecordPreparation {
|
|
1023
|
+
final String recordId;
|
|
1024
|
+
final String peer;
|
|
1025
|
+
final int epoch;
|
|
1026
|
+
final String changedAt;
|
|
1027
|
+
final List<NomosOwnedWrapPreparation> wraps;
|
|
1028
|
+
|
|
1029
|
+
const NomosOwnedUnsharePreparation({
|
|
1030
|
+
required super.ownedKeySubject,
|
|
1031
|
+
super.ownedCustody,
|
|
1032
|
+
required this.recordId,
|
|
1033
|
+
required this.peer,
|
|
1034
|
+
required this.epoch,
|
|
1035
|
+
required this.changedAt,
|
|
1036
|
+
required this.wraps,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
class NomosOwnedTransferPreparation extends NomosOwnedRecordPreparation {
|
|
1041
|
+
final String recordId;
|
|
1042
|
+
final String newOwner;
|
|
1043
|
+
final List<String> previousReaders;
|
|
1044
|
+
final int epoch;
|
|
1045
|
+
final String changedAt;
|
|
1046
|
+
final List<NomosHistoricOwnedWrapPreparation> historicWraps;
|
|
1047
|
+
|
|
1048
|
+
const NomosOwnedTransferPreparation({
|
|
1049
|
+
required super.ownedKeySubject,
|
|
1050
|
+
super.ownedCustody,
|
|
1051
|
+
required this.recordId,
|
|
1052
|
+
required this.newOwner,
|
|
1053
|
+
required this.previousReaders,
|
|
1054
|
+
required this.epoch,
|
|
1055
|
+
required this.changedAt,
|
|
1056
|
+
required this.historicWraps,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/// Exact framework preparation for the atomic device-approval directive.
|
|
1061
|
+
class NomosOwnedDeviceApprovalPreparation {
|
|
1062
|
+
final String newKeySubject;
|
|
1063
|
+
final OwnedCustody ownedCustody;
|
|
1064
|
+
final String verifiedVia;
|
|
1065
|
+
final int validFromEpoch;
|
|
1066
|
+
final String approvedAt;
|
|
1067
|
+
final String ownedKeySubject;
|
|
1068
|
+
|
|
1069
|
+
const NomosOwnedDeviceApprovalPreparation({
|
|
1070
|
+
required this.newKeySubject,
|
|
1071
|
+
required this.ownedCustody,
|
|
1072
|
+
required this.verifiedVia,
|
|
1073
|
+
required this.validFromEpoch,
|
|
1074
|
+
required this.approvedAt,
|
|
1075
|
+
required this.ownedKeySubject,
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
factory NomosOwnedDeviceApprovalPreparation.fromTransport(
|
|
1079
|
+
Map<String, Object?> value,
|
|
1080
|
+
) {
|
|
1081
|
+
return NomosOwnedDeviceApprovalPreparation(
|
|
1082
|
+
newKeySubject: _nomosOwnedRequiredString(value, 'newKeySubject'),
|
|
1083
|
+
ownedCustody: _nomosOwnedCustodyFromTransport(value['ownedCustody']),
|
|
1084
|
+
verifiedVia: _nomosOwnedRequiredString(value, 'verifiedVia'),
|
|
1085
|
+
validFromEpoch: _nomosOwnedRequiredInt(value, 'validFromEpoch'),
|
|
1086
|
+
approvedAt: _nomosOwnedRequiredString(value, 'approvedAt'),
|
|
1087
|
+
ownedKeySubject: _nomosOwnedRequiredString(
|
|
1088
|
+
value,
|
|
1089
|
+
'__nomos_owned_device_key_subject_7c1f5e',
|
|
1090
|
+
),
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
795
1095
|
/// THE OWNER CEREMONY surface — `<Domain>Client(bridge, actor: owner).encryption`. All three verbs open a
|
|
796
1096
|
/// FRESH epoch (a fresh 32-byte secret) and fan the wraps to the resolved roster; the membership change (if
|
|
797
1097
|
/// any) is the domain grant/revoke dispatched first, in the same call.
|
|
@@ -1059,6 +1359,67 @@ class NomosEncryption {
|
|
|
1059
1359
|
/// for a `.births()` directive — the child workspace(s) the offer brought into being on this local plane.
|
|
1060
1360
|
enum NomosOperationPhase { locallyAccepted, admittedToRemoteCustody, converged }
|
|
1061
1361
|
|
|
1362
|
+
/// The proof material a generated offer presents to the kernel.
|
|
1363
|
+
///
|
|
1364
|
+
/// Generated law chooses this; application callers do not. [sessionCredentials] preserves the
|
|
1365
|
+
/// ordinary session posture. [enrolledDevice] deliberately withholds any provider token so a
|
|
1366
|
+
/// stale provider epoch cannot prevent an already enrolled device from authoring a recovery offer.
|
|
1367
|
+
enum NomosOfferAuthenticationProof { sessionCredentials, enrolledDevice }
|
|
1368
|
+
|
|
1369
|
+
/// The kernel's exact diagnosis of the provider credential held by this session.
|
|
1370
|
+
///
|
|
1371
|
+
/// This deliberately does not expose protobuf names to generated application
|
|
1372
|
+
/// clients. It is a typed framework fact used to decide whether a parent-law
|
|
1373
|
+
/// epoch recovery is applicable; callers never inspect an error string.
|
|
1374
|
+
enum NomosProviderTokenVerification {
|
|
1375
|
+
unspecified,
|
|
1376
|
+
valid,
|
|
1377
|
+
noMatchingKey,
|
|
1378
|
+
expired,
|
|
1379
|
+
notYetValid,
|
|
1380
|
+
issuerMismatch,
|
|
1381
|
+
audienceMismatch,
|
|
1382
|
+
badSignature,
|
|
1383
|
+
malformed,
|
|
1384
|
+
unsupportedAlgorithm,
|
|
1385
|
+
badKey,
|
|
1386
|
+
missingSubject,
|
|
1387
|
+
noVerifier,
|
|
1388
|
+
missingToken,
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/// Current kernel-authenticated facts needed before retrying an ordinary offer.
|
|
1392
|
+
final class NomosAuthenticationPosture {
|
|
1393
|
+
const NomosAuthenticationPosture({
|
|
1394
|
+
required this.providerToken,
|
|
1395
|
+
required this.enrolledDevice,
|
|
1396
|
+
required this.resolvedPrincipal,
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
final NomosProviderTokenVerification providerToken;
|
|
1400
|
+
final bool enrolledDevice;
|
|
1401
|
+
final String resolvedPrincipal;
|
|
1402
|
+
|
|
1403
|
+
bool isReadyFor(String expectedPrincipal) =>
|
|
1404
|
+
providerToken == NomosProviderTokenVerification.valid &&
|
|
1405
|
+
enrolledDevice &&
|
|
1406
|
+
expectedPrincipal.isNotEmpty &&
|
|
1407
|
+
resolvedPrincipal == expectedPrincipal;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/// A typed kernel-call refusal. Generated clients may inspect [code] only to
|
|
1411
|
+
/// choose another typed kernel query; prose remains diagnostic, never control
|
|
1412
|
+
/// flow.
|
|
1413
|
+
final class NomosKernelCallFailure implements Exception {
|
|
1414
|
+
const NomosKernelCallFailure({required this.code, required this.message});
|
|
1415
|
+
|
|
1416
|
+
final String code;
|
|
1417
|
+
final String message;
|
|
1418
|
+
|
|
1419
|
+
@override
|
|
1420
|
+
String toString() => 'NomosKernelCallFailure($code: $message)';
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1062
1423
|
/// Product-facing progress for one accepted write. Transport topology and custody identifiers remain private.
|
|
1063
1424
|
final class NomosOperationStatus {
|
|
1064
1425
|
const NomosOperationStatus(this.phase, {this.updatedAt});
|
|
@@ -1126,8 +1487,8 @@ class NomosOfferOutcome {
|
|
|
1126
1487
|
/// it IN-PLACE and opens it as a live sibling session — reads/writes flow into the child at once (#42).
|
|
1127
1488
|
final List<String> born;
|
|
1128
1489
|
|
|
1129
|
-
///
|
|
1130
|
-
final List<
|
|
1490
|
+
/// Typed per-child outcomes reported by the kernel, when present.
|
|
1491
|
+
final List<NomosBirthOutcome> birthOutcomes;
|
|
1131
1492
|
|
|
1132
1493
|
/// LEGACY GATEWAY LANE ONLY: the parent gateway's verdict, when this outcome wraps a deprecated
|
|
1133
1494
|
/// cross-workspace `createWorkspace` relay (the old generated-birth shape). Null on the one
|
|
@@ -1175,6 +1536,31 @@ class NomosOfferOutcome {
|
|
|
1175
1536
|
.toList();
|
|
1176
1537
|
}
|
|
1177
1538
|
|
|
1539
|
+
/// A generated protobuf offer response that did not admit the proposed
|
|
1540
|
+
/// change. Keeping the response typed preserves every exhaustive kernel
|
|
1541
|
+
/// outcome without translating it through an exception-shaped map.
|
|
1542
|
+
final class NomosTypedOfferFailure implements Exception {
|
|
1543
|
+
const NomosTypedOfferFailure(this.response);
|
|
1544
|
+
|
|
1545
|
+
final nomos_protocol.OfferResponse response;
|
|
1546
|
+
|
|
1547
|
+
@override
|
|
1548
|
+
String toString() {
|
|
1549
|
+
final detail = switch (response.whichOutcome()) {
|
|
1550
|
+
nomos_protocol.OfferResponse_Outcome.refused =>
|
|
1551
|
+
'${response.refused.refusal.code.name}: ${response.refused.refusal.message}',
|
|
1552
|
+
nomos_protocol.OfferResponse_Outcome.deadLettered =>
|
|
1553
|
+
response.deadLettered.reason,
|
|
1554
|
+
nomos_protocol.OfferResponse_Outcome.quarantined =>
|
|
1555
|
+
response.quarantined.reason,
|
|
1556
|
+
nomos_protocol.OfferResponse_Outcome.blockedOnDependency =>
|
|
1557
|
+
response.blockedOnDependency.detail,
|
|
1558
|
+
final outcome => outcome.name,
|
|
1559
|
+
};
|
|
1560
|
+
return 'NomosTypedOfferFailure($detail)';
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1178
1564
|
/// OPEN A REF'S WORKSPACE DIRECTLY (co2 DX ask) — `await outcome.bornRefs.single.acquire(bridge)`. Sugar over
|
|
1179
1565
|
/// [NomosBridge.acquireSession] keyed by the ref's `workspace`: an already-open session returns as-is; a
|
|
1180
1566
|
/// device-resident/parked/just-born workspace opens LOCAL-ONLY in place (no cloud, no credentials); only a
|
|
@@ -1261,6 +1647,7 @@ class NomosBridge {
|
|
|
1261
1647
|
final Map<int, Completer<Object?>> _pending = {};
|
|
1262
1648
|
final Map<int, String> _pendingOps = {};
|
|
1263
1649
|
final Map<int, NomosTraceContext> _pendingTraces = {};
|
|
1650
|
+
final Map<int, StackTrace> _pendingCallStacks = {};
|
|
1264
1651
|
final Map<String, StreamController<List<dynamic>>> _watches = {};
|
|
1265
1652
|
// SHAPE COMPATIBILITY (#72): compat bindings emit a verdict MAP, not a row list.
|
|
1266
1653
|
final Map<String, StreamController<Map<String, Object?>>> _compatWatches = {};
|
|
@@ -1272,8 +1659,8 @@ class NomosBridge {
|
|
|
1272
1659
|
final Map<String, StreamController<NomosCompat>> _lawChangedBySession = {};
|
|
1273
1660
|
final StreamController<NomosCompat> _lawChanged =
|
|
1274
1661
|
StreamController<NomosCompat>.broadcast();
|
|
1275
|
-
final StreamController<
|
|
1276
|
-
StreamController<
|
|
1662
|
+
final StreamController<nomos_protocol.RuntimeEvent> _syncEvents =
|
|
1663
|
+
StreamController<nomos_protocol.RuntimeEvent>.broadcast();
|
|
1277
1664
|
final StreamController<Map<String, Object?>> _signalEvents =
|
|
1278
1665
|
StreamController<Map<String, Object?>>.broadcast();
|
|
1279
1666
|
// CUSTODY LIFECYCLE (rebirth_and_custody_lifecycle.md): each `custody` event carries the raw map (session +
|
|
@@ -1321,13 +1708,20 @@ class NomosBridge {
|
|
|
1321
1708
|
// workspace is a live session on it. [_sessionNames] tracks what [connect] has opened; [_sessionViews]
|
|
1322
1709
|
// caches the per-workspace [NomosSession] handle so `bridge.session(ws)` is stable + cheap.
|
|
1323
1710
|
final Set<String> _sessionNames = {};
|
|
1711
|
+
final Map<String, String> _workspaceBySession = {};
|
|
1324
1712
|
final Map<String, NomosSession> _sessionViews = {};
|
|
1325
1713
|
// The gate-bound identity retained from each session's auth token. Generated clients consume this so
|
|
1326
1714
|
// application code never has to thread an actor into ordinary business methods.
|
|
1327
1715
|
final Map<String, String> _sessionPrincipals = {};
|
|
1716
|
+
// Runtime-v2 offers need the exact captured credentials, not a reconstructed
|
|
1717
|
+
// JSON envelope. They remain process-local and are passed only as protobuf
|
|
1718
|
+
// bytes to the resident kernel.
|
|
1719
|
+
final Map<String, String> _sessionAuthTokens = {};
|
|
1328
1720
|
// Device signing custody follows the application session internally. A generated application opening a
|
|
1329
1721
|
// locally-born child inherits its current session's device key; tenant code never handles authorSecret.
|
|
1330
1722
|
final Map<String, String> _sessionAuthorSecrets = {};
|
|
1723
|
+
final Map<String, int> _sessionOfferSequences = {};
|
|
1724
|
+
final Map<String, int> _sessionReplicas = {};
|
|
1331
1725
|
// Stateful generated applications retain the refreshable cloud access that opened their primary
|
|
1332
1726
|
// session. A generated read following a NomosRef can therefore mount a genuinely-absent workspace
|
|
1333
1727
|
// without asking tenant code for cloud URLs, tokens, sessions or signing keys. Kept per source
|
|
@@ -1352,12 +1746,165 @@ class NomosBridge {
|
|
|
1352
1746
|
// methods expose only the typed receipt and its product-language lifecycle.
|
|
1353
1747
|
final Map<String, Map<String, NomosOperationReceipt>> _operationReceipts = {};
|
|
1354
1748
|
final Map<String, NomosOperationReceipt> _birthReceipts = {};
|
|
1749
|
+
final Map<String, StreamController<Uint8List>> _nominalWatchControllers = {};
|
|
1355
1750
|
int _seq = 0;
|
|
1751
|
+
late final NmsRuntimeV2Gateway _runtimeV2 = NmsRuntimeV2Gateway(_transport);
|
|
1356
1752
|
final String _traceId = _newTraceId();
|
|
1357
1753
|
late final StreamSubscription<Map<String, Object?>> _sub;
|
|
1754
|
+
StreamSubscription<nomos_protocol.RuntimeEvent>? _runtimeEventSub;
|
|
1358
1755
|
|
|
1359
1756
|
NomosBridge(this._transport, {this.allowStaleRunnerBundle = false}) {
|
|
1360
1757
|
_sub = _transport.messages.listen(_onMessage, onError: _onTransportError);
|
|
1758
|
+
final transport = _transport;
|
|
1759
|
+
if (transport is NomosRuntimeV2EventTransport) {
|
|
1760
|
+
final eventTransport = transport as NomosRuntimeV2EventTransport;
|
|
1761
|
+
_runtimeEventSub = eventTransport.runtimeEvents.listen(
|
|
1762
|
+
_onRuntimeEvent,
|
|
1763
|
+
onError: _onTransportError,
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
void _onRuntimeEvent(nomos_protocol.RuntimeEvent event) {
|
|
1769
|
+
if (event.hasReady()) {
|
|
1770
|
+
final stale = _staleBundleError(event.ready.runtimeVersion);
|
|
1771
|
+
if (stale != null && !allowStaleRunnerBundle) {
|
|
1772
|
+
if (!_ready.isCompleted) _ready.completeError(stale);
|
|
1773
|
+
} else if (!_ready.isCompleted) {
|
|
1774
|
+
_ready.complete();
|
|
1775
|
+
}
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (event.hasWatchChanged()) {
|
|
1779
|
+
final changed = event.watchChanged;
|
|
1780
|
+
final controller =
|
|
1781
|
+
_eventPlane._nominalWatchControllers[changed.subscriptionId];
|
|
1782
|
+
if (controller == null || controller.isClosed) return;
|
|
1783
|
+
if (changed.hasFailure()) {
|
|
1784
|
+
controller.addError(
|
|
1785
|
+
StateError('${changed.failure.code}: ${changed.failure.message}'),
|
|
1786
|
+
);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
controller.add(Uint8List.fromList(changed.resultUsda));
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
if (event.hasSyncCompleted()) {
|
|
1793
|
+
final completed = event.syncCompleted;
|
|
1794
|
+
final session = event.hasSession() ? event.session : null;
|
|
1795
|
+
if (!_eventPlane._syncEvents.isClosed) {
|
|
1796
|
+
_eventPlane._syncEvents.add(event);
|
|
1797
|
+
}
|
|
1798
|
+
if (completed.hasFailure()) {
|
|
1799
|
+
if (_eventPlane._syncNeedsCredentialRefresh(completed) &&
|
|
1800
|
+
session != null) {
|
|
1801
|
+
unawaited(_eventPlane._refreshSessionCredential(session));
|
|
1802
|
+
}
|
|
1803
|
+
_eventPlane._updateScopedStatus(
|
|
1804
|
+
session,
|
|
1805
|
+
(current) => current.copyWith(
|
|
1806
|
+
phase: SyncPhase.error,
|
|
1807
|
+
clearTransfer: true,
|
|
1808
|
+
lastError: completed.failure.message,
|
|
1809
|
+
),
|
|
1810
|
+
);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
final result = completed.result;
|
|
1814
|
+
if (session != null) {
|
|
1815
|
+
_eventPlane._advanceOperationReceipts(session, result);
|
|
1816
|
+
if (_eventPlane._syncNeedsCredentialRefresh(completed)) {
|
|
1817
|
+
unawaited(_eventPlane._refreshSessionCredential(session));
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (result.offline) {
|
|
1821
|
+
_eventPlane._updateScopedStatus(
|
|
1822
|
+
session,
|
|
1823
|
+
(current) => current.copyWith(
|
|
1824
|
+
phase: SyncPhase.offline,
|
|
1825
|
+
clearTransfer: true,
|
|
1826
|
+
lastError: result.hasPushError() ? result.pushError.message : null,
|
|
1827
|
+
),
|
|
1828
|
+
);
|
|
1829
|
+
} else if (result.pushFailed) {
|
|
1830
|
+
_eventPlane._updateScopedStatus(
|
|
1831
|
+
session,
|
|
1832
|
+
(current) => current.copyWith(
|
|
1833
|
+
phase: SyncPhase.error,
|
|
1834
|
+
clearTransfer: true,
|
|
1835
|
+
lastError: _eventPlane._syncPushFailureDetail(result),
|
|
1836
|
+
),
|
|
1837
|
+
);
|
|
1838
|
+
} else {
|
|
1839
|
+
_eventPlane._updateScopedStatus(
|
|
1840
|
+
session,
|
|
1841
|
+
(current) => current.copyWith(
|
|
1842
|
+
phase: SyncPhase.idle,
|
|
1843
|
+
clearTransfer: true,
|
|
1844
|
+
clearError: true,
|
|
1845
|
+
lastSyncedAt: DateTime.now(),
|
|
1846
|
+
),
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
if (event.hasLinkChanged()) {
|
|
1852
|
+
if (!_eventPlane._syncEvents.isClosed) {
|
|
1853
|
+
_eventPlane._syncEvents.add(event);
|
|
1854
|
+
}
|
|
1855
|
+
if (event.linkChanged.status ==
|
|
1856
|
+
nomos_protocol.LinkStatus.LINK_STATUS_CLOSED) {
|
|
1857
|
+
_eventPlane._updateScopedStatus(
|
|
1858
|
+
event.hasSession() ? event.session : null,
|
|
1859
|
+
(current) => current.copyWith(phase: SyncPhase.offline),
|
|
1860
|
+
);
|
|
1861
|
+
}
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (event.hasCustodyChanged()) {
|
|
1865
|
+
if (!_eventPlane._custodyEvents.isClosed) {
|
|
1866
|
+
_eventPlane._custodyEvents.add(
|
|
1867
|
+
NomosCustodyEvent.fromRuntime(
|
|
1868
|
+
event.custodyChanged,
|
|
1869
|
+
session: event.hasSession() ? event.session : null,
|
|
1870
|
+
),
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
if (event.hasLocalWorkRejected()) {
|
|
1876
|
+
if (!_eventPlane._localWorkRejectedEvents.isClosed) {
|
|
1877
|
+
_eventPlane._localWorkRejectedEvents.add(
|
|
1878
|
+
NomosLocalWorkRejectedEvent.fromRuntime(
|
|
1879
|
+
event.localWorkRejected,
|
|
1880
|
+
session: event.hasSession() ? event.session : null,
|
|
1881
|
+
),
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (event.hasTransferProgress()) {
|
|
1887
|
+
final progress = TransferProgress.fromRuntime(event.transferProgress);
|
|
1888
|
+
if (!_eventPlane._transferEvents.isClosed) {
|
|
1889
|
+
_eventPlane._transferEvents.add(progress);
|
|
1890
|
+
}
|
|
1891
|
+
_eventPlane._updateScopedStatus(
|
|
1892
|
+
event.hasSession() ? event.session : null,
|
|
1893
|
+
(current) => current.copyWith(
|
|
1894
|
+
phase: progress.done ? SyncPhase.syncing : SyncPhase.transferring,
|
|
1895
|
+
transfer: progress,
|
|
1896
|
+
clearTransfer: progress.done,
|
|
1897
|
+
),
|
|
1898
|
+
);
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
if (event.hasKernelProgress()) {
|
|
1902
|
+
if (!_eventPlane._kernelProgressEvents.isClosed) {
|
|
1903
|
+
_eventPlane._kernelProgressEvents.add(
|
|
1904
|
+
KernelProgress.fromRuntime(event.kernelProgress),
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1361
1908
|
}
|
|
1362
1909
|
|
|
1363
1910
|
void _onTransportError(Object error, StackTrace stackTrace) {
|
|
@@ -1366,6 +1913,7 @@ class NomosBridge {
|
|
|
1366
1913
|
_pending.clear();
|
|
1367
1914
|
_pendingOps.clear();
|
|
1368
1915
|
_pendingTraces.clear();
|
|
1916
|
+
_pendingCallStacks.clear();
|
|
1369
1917
|
for (final completer in pending) {
|
|
1370
1918
|
if (!completer.isCompleted) completer.completeError(error, stackTrace);
|
|
1371
1919
|
}
|
|
@@ -1508,114 +2056,6 @@ class NomosBridge {
|
|
|
1508
2056
|
}
|
|
1509
2057
|
return;
|
|
1510
2058
|
}
|
|
1511
|
-
if (event == 'sync' || event == 'link') {
|
|
1512
|
-
// autoSync telemetry: each round { event:'sync', reason, result } and socket state { event:'link', status }.
|
|
1513
|
-
if (!_syncEvents.isClosed) _syncEvents.add(m);
|
|
1514
|
-
if (event == 'sync') {
|
|
1515
|
-
final result = (m['result'] as Map?)?.cast<String, Object?>();
|
|
1516
|
-
final session = m['session']?.toString();
|
|
1517
|
-
if (session != null && result != null) {
|
|
1518
|
-
_advanceOperationReceipts(session, result);
|
|
1519
|
-
if (_syncNeedsCredentialRefresh(m, result)) {
|
|
1520
|
-
unawaited(_refreshSessionCredential(session));
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
if (m['ok'] == false) {
|
|
1524
|
-
// A rejected/failed round is not a successful sync timestamp. Keep the stable typed envelope on
|
|
1525
|
-
// telemetry; the app-ready status needs only one legible error and an honest error phase.
|
|
1526
|
-
final detail = m['error']?.toString() ?? 'automatic sync failed';
|
|
1527
|
-
_updateScopedStatus(
|
|
1528
|
-
session,
|
|
1529
|
-
(current) => current.copyWith(
|
|
1530
|
-
phase: SyncPhase.error,
|
|
1531
|
-
clearTransfer: true,
|
|
1532
|
-
lastError: detail,
|
|
1533
|
-
),
|
|
1534
|
-
);
|
|
1535
|
-
} else if (result?['offline'] == true) {
|
|
1536
|
-
// Offline is expected transport debt. The durable local branch remains ahead and autoSync owns the
|
|
1537
|
-
// retry; do not claim a completed sync or overwrite the previous successful timestamp.
|
|
1538
|
-
_updateScopedStatus(
|
|
1539
|
-
session,
|
|
1540
|
-
(current) => current.copyWith(
|
|
1541
|
-
phase: SyncPhase.offline,
|
|
1542
|
-
clearTransfer: true,
|
|
1543
|
-
lastError: (result?['pushError'] as Map?)?['error']?.toString(),
|
|
1544
|
-
),
|
|
1545
|
-
);
|
|
1546
|
-
} else if (result?['pushFailed'] == true) {
|
|
1547
|
-
// The sync round itself completed, but shared custody refused the
|
|
1548
|
-
// push. `stillAhead` is durable debt, not success: surface the
|
|
1549
|
-
// cloud's typed refusal on the owning workspace so generated
|
|
1550
|
-
// `whenCurrent()` calls fail with the real cause instead of timing
|
|
1551
|
-
// out behind a generic saving state.
|
|
1552
|
-
final detail = _syncPushFailureDetail(result!);
|
|
1553
|
-
_updateScopedStatus(
|
|
1554
|
-
session,
|
|
1555
|
-
(current) => current.copyWith(
|
|
1556
|
-
phase: SyncPhase.error,
|
|
1557
|
-
clearTransfer: true,
|
|
1558
|
-
lastError: detail,
|
|
1559
|
-
),
|
|
1560
|
-
);
|
|
1561
|
-
} else {
|
|
1562
|
-
// A completed round → idle (the next metrics poll folds in residual ahead/behind), stamp the time.
|
|
1563
|
-
_updateScopedStatus(
|
|
1564
|
-
session,
|
|
1565
|
-
(current) => current.copyWith(
|
|
1566
|
-
phase: SyncPhase.idle,
|
|
1567
|
-
clearTransfer: true,
|
|
1568
|
-
clearError: true,
|
|
1569
|
-
lastSyncedAt: DateTime.now(),
|
|
1570
|
-
),
|
|
1571
|
-
);
|
|
1572
|
-
}
|
|
1573
|
-
} else if (m['status'] == 'closed' || m['status'] == 'offline') {
|
|
1574
|
-
_updateScopedStatus(
|
|
1575
|
-
m['session']?.toString(),
|
|
1576
|
-
(current) => current.copyWith(phase: SyncPhase.offline),
|
|
1577
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
if (event == 'custody') {
|
|
1582
|
-
// Custody lifecycle: { event:'custody', session, custody:{ kind, break, … } }. Decode + fan out so the
|
|
1583
|
-
// app can react (kind "ask" → show a dialog then call resolveCustodyBreak; others are informational).
|
|
1584
|
-
final ev = NomosCustodyEvent.fromMessage(m);
|
|
1585
|
-
if (ev != null && !_custodyEvents.isClosed) _custodyEvents.add(ev);
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
if (event == 'localWorkRejected') {
|
|
1589
|
-
// LOCAL-WORK-REJECTED: { event:'localWorkRejected', session, localWorkRejected:{ workspace,
|
|
1590
|
-
// refused, parked, resetTo, retryable, source } }. The framework already parked the refused
|
|
1591
|
-
// intents + replayed survivors; the app's only job is the honest message (retryable ⇒ "queued,
|
|
1592
|
-
// will retry"; terminal ⇒ "preserved for review — contact support").
|
|
1593
|
-
final ev = NomosLocalWorkRejectedEvent.fromMessage(m);
|
|
1594
|
-
if (ev != null && !_localWorkRejectedEvents.isClosed) {
|
|
1595
|
-
_localWorkRejectedEvents.add(ev);
|
|
1596
|
-
}
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
if (event == 'transfer') {
|
|
1600
|
-
// LIVE pack byte-progress { received, total, kind } — the real loading bar (host Content-Length).
|
|
1601
|
-
final tp = TransferProgress.fromMessage(m);
|
|
1602
|
-
if (!_transferEvents.isClosed) _transferEvents.add(tp);
|
|
1603
|
-
_updateScopedStatus(
|
|
1604
|
-
m['session']?.toString(),
|
|
1605
|
-
(current) => current.copyWith(
|
|
1606
|
-
phase: tp.done ? SyncPhase.syncing : SyncPhase.transferring,
|
|
1607
|
-
transfer: tp,
|
|
1608
|
-
clearTransfer: tp.done,
|
|
1609
|
-
),
|
|
1610
|
-
);
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
if (event == 'kernelProgress') {
|
|
1614
|
-
if (!_kernelProgressEvents.isClosed) {
|
|
1615
|
-
_kernelProgressEvents.add(KernelProgress.fromMessage(m));
|
|
1616
|
-
}
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
2059
|
if (event == 'trace') {
|
|
1620
2060
|
// the realtime cloud flamepath: { event:'trace', trace:{at, oid, edge, clients} } — one per admission.
|
|
1621
2061
|
if (!_flameEvents.isClosed) _flameEvents.add(m);
|
|
@@ -1647,6 +2087,7 @@ class NomosBridge {
|
|
|
1647
2087
|
final completer = _pending.remove(id);
|
|
1648
2088
|
final op = _pendingOps.remove(id);
|
|
1649
2089
|
final trace = _pendingTraces.remove(id) ?? const NomosTraceContext();
|
|
2090
|
+
final callStack = _pendingCallStacks.remove(id) ?? StackTrace.current;
|
|
1650
2091
|
if (completer == null) return;
|
|
1651
2092
|
if (m['ok'] == true) {
|
|
1652
2093
|
completer.complete(m['result']);
|
|
@@ -1656,14 +2097,15 @@ class NomosBridge {
|
|
|
1656
2097
|
// [BirthIncomplete] here (apps switch on the type + entries, never scrape the prose).
|
|
1657
2098
|
final failedBirths = ((m['errorInfo'] as Map?)?['failedBirths'] as List?)
|
|
1658
2099
|
?.whereType<Map>()
|
|
1659
|
-
.map(
|
|
1660
|
-
.toList();
|
|
2100
|
+
.map(_birthOutcomeFromLegacyWire)
|
|
2101
|
+
.toList(growable: false);
|
|
1661
2102
|
if (failedBirths != null && failedBirths.isNotEmpty) {
|
|
1662
2103
|
completer.completeError(
|
|
1663
2104
|
BirthIncomplete(
|
|
1664
2105
|
failedBirths,
|
|
1665
2106
|
head: ((m['errorInfo'] as Map?)?['head'] ?? '').toString(),
|
|
1666
2107
|
),
|
|
2108
|
+
callStack,
|
|
1667
2109
|
);
|
|
1668
2110
|
return;
|
|
1669
2111
|
}
|
|
@@ -1673,7 +2115,7 @@ class NomosBridge {
|
|
|
1673
2115
|
fallbackTrace: trace,
|
|
1674
2116
|
fallbackMessage: 'the githolon refused the request',
|
|
1675
2117
|
);
|
|
1676
|
-
completer.completeError(nomosErrorFromInfo(info));
|
|
2118
|
+
completer.completeError(nomosErrorFromInfo(info), callStack);
|
|
1677
2119
|
}
|
|
1678
2120
|
}
|
|
1679
2121
|
|
|
@@ -1691,6 +2133,7 @@ class NomosBridge {
|
|
|
1691
2133
|
sampled: true,
|
|
1692
2134
|
);
|
|
1693
2135
|
_pendingTraces[id] = trace;
|
|
2136
|
+
_pendingCallStacks[id] = StackTrace.current;
|
|
1694
2137
|
try {
|
|
1695
2138
|
_transport.send({
|
|
1696
2139
|
'v': kWebviewBridgeVersion,
|
|
@@ -1710,11 +2153,49 @@ class NomosBridge {
|
|
|
1710
2153
|
_pending.remove(id);
|
|
1711
2154
|
_pendingOps.remove(id);
|
|
1712
2155
|
_pendingTraces.remove(id);
|
|
2156
|
+
_pendingCallStacks.remove(id);
|
|
1713
2157
|
completer.completeError(error, stackTrace);
|
|
1714
2158
|
}
|
|
1715
2159
|
return completer.future;
|
|
1716
2160
|
}
|
|
1717
2161
|
|
|
2162
|
+
/// Invoke one generated application-runtime command. Request correlation and
|
|
2163
|
+
/// every outcome discriminator stay in generated protobuf types; callers
|
|
2164
|
+
/// must exhaustively select the response arm they requested.
|
|
2165
|
+
Future<nomos_protocol.RuntimeResponse> _invokeRuntimeV2(
|
|
2166
|
+
nomos_protocol.RuntimeRequest request,
|
|
2167
|
+
) async {
|
|
2168
|
+
return _eventPlane._runtimeV2.invoke(request);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
String _runtimeSession([String? explicit]) {
|
|
2172
|
+
if (explicit != null && explicit.isNotEmpty) return explicit;
|
|
2173
|
+
final scoped = _sessionName;
|
|
2174
|
+
if (scoped != null && scoped.isNotEmpty) return scoped;
|
|
2175
|
+
final sessions = _eventPlane._sessionNames;
|
|
2176
|
+
if (sessions.length == 1) return sessions.single;
|
|
2177
|
+
if (sessions.isEmpty) {
|
|
2178
|
+
throw StateError('No Nomos workspace is connected');
|
|
2179
|
+
}
|
|
2180
|
+
throw StateError(
|
|
2181
|
+
'Multiple Nomos workspaces are connected; select a session');
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
Future<void> _unwatch(String subscriptionId) async {
|
|
2185
|
+
final response = await _invokeRuntimeV2(
|
|
2186
|
+
nomos_protocol.RuntimeRequest(
|
|
2187
|
+
unwatch: nomos_protocol.UnwatchRequest(
|
|
2188
|
+
subscriptionId: subscriptionId,
|
|
2189
|
+
),
|
|
2190
|
+
),
|
|
2191
|
+
);
|
|
2192
|
+
if (!response.hasWatchStopped()) {
|
|
2193
|
+
throw StateError(
|
|
2194
|
+
'Runtime v2 returned ${response.whichOutcome().name} for unwatch',
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
1718
2199
|
Map<String, Object?> _asMap(Object? v) =>
|
|
1719
2200
|
(v as Map?)?.cast<String, Object?>() ?? const {};
|
|
1720
2201
|
List<Map<String, dynamic>> _asRows(Object? v) =>
|
|
@@ -1722,23 +2203,6 @@ class NomosBridge {
|
|
|
1722
2203
|
.map((e) => (e as Map).cast<String, dynamic>())
|
|
1723
2204
|
.toList();
|
|
1724
2205
|
|
|
1725
|
-
String? _principalFromToken(String? token) {
|
|
1726
|
-
if (token == null || token.isEmpty) return null;
|
|
1727
|
-
try {
|
|
1728
|
-
final parts = token.split('.');
|
|
1729
|
-
if (parts.length < 2) return null;
|
|
1730
|
-
final payload = jsonDecode(
|
|
1731
|
-
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
|
|
1732
|
-
);
|
|
1733
|
-
final sub = payload is Map ? payload['sub'] : null;
|
|
1734
|
-
if (sub is! String || sub.trim().isEmpty) return null;
|
|
1735
|
-
final value = sub.trim();
|
|
1736
|
-
return 'user:$value';
|
|
1737
|
-
} catch (_) {
|
|
1738
|
-
return null;
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
2206
|
NomosOperationReceipt _registerOperationReceipt(
|
|
1743
2207
|
String workspace,
|
|
1744
2208
|
String intentId,
|
|
@@ -1770,19 +2234,19 @@ class NomosBridge {
|
|
|
1770
2234
|
|
|
1771
2235
|
void _advanceOperationReceipts(
|
|
1772
2236
|
String workspace,
|
|
1773
|
-
|
|
2237
|
+
nomos_protocol.SyncWorkspaceResponse result,
|
|
1774
2238
|
) {
|
|
1775
2239
|
final pending = _eventPlane._operationReceipts[workspace];
|
|
1776
2240
|
if (pending == null || pending.isEmpty) return;
|
|
1777
2241
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
for (final intentId in ids('admittedIntentIds')) {
|
|
2242
|
+
for (final intentId in result.admittedIntentIds.where(
|
|
2243
|
+
(value) => value.isNotEmpty,
|
|
2244
|
+
)) {
|
|
1783
2245
|
pending[intentId]?._advance(NomosOperationPhase.admittedToRemoteCustody);
|
|
1784
2246
|
}
|
|
1785
|
-
for (final intentId in
|
|
2247
|
+
for (final intentId in result.convergedIntentIds.where(
|
|
2248
|
+
(value) => value.isNotEmpty,
|
|
2249
|
+
)) {
|
|
1786
2250
|
final receipt = pending.remove(intentId);
|
|
1787
2251
|
receipt?._advance(NomosOperationPhase.converged);
|
|
1788
2252
|
}
|
|
@@ -1790,24 +2254,24 @@ class NomosBridge {
|
|
|
1790
2254
|
}
|
|
1791
2255
|
|
|
1792
2256
|
bool _syncNeedsCredentialRefresh(
|
|
1793
|
-
|
|
1794
|
-
Map<String, Object?> result,
|
|
2257
|
+
nomos_protocol.SyncCompleted event,
|
|
1795
2258
|
) {
|
|
1796
|
-
final
|
|
1797
|
-
|
|
2259
|
+
final status =
|
|
2260
|
+
event.result.hasPushError() ? event.result.pushError.status : 0;
|
|
1798
2261
|
if (status == 401 || status == 403) return true;
|
|
1799
|
-
final text =
|
|
1800
|
-
|
|
2262
|
+
final text = '${event.hasFailure() ? event.failure.message : ''} '
|
|
2263
|
+
'${event.result.hasPushError() ? event.result.pushError.message : ''}'
|
|
2264
|
+
.toLowerCase();
|
|
1801
2265
|
return text.contains('unauthenticated') ||
|
|
1802
2266
|
text.contains('token expired') ||
|
|
1803
2267
|
text.contains('invalid token');
|
|
1804
2268
|
}
|
|
1805
2269
|
|
|
1806
|
-
String _syncPushFailureDetail(
|
|
1807
|
-
final push = (result
|
|
1808
|
-
final status = push
|
|
1809
|
-
final reason = push
|
|
1810
|
-
final prefix = status == null
|
|
2270
|
+
String _syncPushFailureDetail(nomos_protocol.SyncWorkspaceResponse result) {
|
|
2271
|
+
final push = result.hasPushError() ? result.pushError : null;
|
|
2272
|
+
final status = push?.status;
|
|
2273
|
+
final reason = push?.message;
|
|
2274
|
+
final prefix = status == null || status == 0
|
|
1811
2275
|
? 'Shared custody refused this workspace change'
|
|
1812
2276
|
: 'Shared custody refused this workspace change (HTTP $status)';
|
|
1813
2277
|
return reason == null || reason.toString().trim().isEmpty
|
|
@@ -1888,19 +2352,18 @@ class NomosBridge {
|
|
|
1888
2352
|
/// call `bridge.acquireSession(ws)` / `ref.acquire(bridge)` before using it;
|
|
1889
2353
|
/// • `"parked"` — custody resident but the holon is LRU-disposed; it re-mounts on demand (acquire).
|
|
1890
2354
|
/// `syncPosture` is `"local-only"` (no cloud counterpart) or `"cloud-bound"`.
|
|
1891
|
-
Future<List<
|
|
1892
|
-
final
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
}).toList();
|
|
2355
|
+
Future<List<nomos_protocol.WorkspaceSummary>> listWorkspaces() async {
|
|
2356
|
+
final response = await _invokeRuntimeV2(
|
|
2357
|
+
nomos_protocol.RuntimeRequest(
|
|
2358
|
+
listWorkspaces: nomos_protocol.ListWorkspacesRequest(),
|
|
2359
|
+
),
|
|
2360
|
+
);
|
|
2361
|
+
if (!response.hasWorkspaceList()) {
|
|
2362
|
+
throw StateError(
|
|
2363
|
+
'Runtime v2 returned ${response.whichOutcome().name} for listWorkspaces',
|
|
2364
|
+
);
|
|
2365
|
+
}
|
|
2366
|
+
return response.workspaceList.workspaces;
|
|
1904
2367
|
}
|
|
1905
2368
|
|
|
1906
2369
|
/// Framework-private plane persistence primitive. It snapshots exact local custody even when [workspace]
|
|
@@ -1974,6 +2437,7 @@ class NomosBridge {
|
|
|
1974
2437
|
_eventPlane._sessionNames.add(
|
|
1975
2438
|
ws,
|
|
1976
2439
|
); // the open succeeded — the session is real now
|
|
2440
|
+
_eventPlane._workspaceBySession[ws] = workspace;
|
|
1977
2441
|
// Opening succeeded, but scoped metrics have not yet proved that this
|
|
1978
2442
|
// workspace is current. Unknown readiness must fail closed: otherwise a
|
|
1979
2443
|
// generated `whenCurrent()` can observe the default idle value and return
|
|
@@ -1983,9 +2447,13 @@ class NomosBridge {
|
|
|
1983
2447
|
() => const SyncStatus(phase: SyncPhase.syncing),
|
|
1984
2448
|
);
|
|
1985
2449
|
_eventPlane._foldApplicationStatusFromSessions();
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2450
|
+
// An opaque bearer token is proof material for the kernel, not a JSON
|
|
2451
|
+
// identity document for the host. The session acquires a principal claim
|
|
2452
|
+
// only from an explicit generated application context; decoding `sub`
|
|
2453
|
+
// here would falsely turn an unverified host observation into authority.
|
|
2454
|
+
if (authToken != null && authToken.isNotEmpty) {
|
|
2455
|
+
_eventPlane._sessionAuthTokens[ws] = authToken;
|
|
2456
|
+
}
|
|
1989
2457
|
if (authorSecret != null && authorSecret.isNotEmpty) {
|
|
1990
2458
|
_eventPlane._sessionAuthorSecrets[ws] = authorSecret;
|
|
1991
2459
|
}
|
|
@@ -2023,19 +2491,37 @@ class NomosBridge {
|
|
|
2023
2491
|
);
|
|
2024
2492
|
|
|
2025
2493
|
/// Continue an `ask`-policy custody break with the user's decision (adopt|reject) — the async UX lane.
|
|
2026
|
-
Future<
|
|
2027
|
-
|
|
2494
|
+
Future<nomos_protocol.CustodyBreakResolved> resolveCustodyBreak(
|
|
2495
|
+
nomos_protocol.CustodyBreakDecision decision, {
|
|
2028
2496
|
String? session,
|
|
2029
|
-
}) async
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2497
|
+
}) async {
|
|
2498
|
+
if (decision ==
|
|
2499
|
+
nomos_protocol
|
|
2500
|
+
.CustodyBreakDecision.CUSTODY_BREAK_DECISION_UNSPECIFIED) {
|
|
2501
|
+
throw ArgumentError.value(
|
|
2502
|
+
decision,
|
|
2503
|
+
'decision',
|
|
2504
|
+
'must be ADOPT or REJECT',
|
|
2035
2505
|
);
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2506
|
+
}
|
|
2507
|
+
final response = await _invokeRuntimeV2(
|
|
2508
|
+
nomos_protocol.RuntimeRequest(
|
|
2509
|
+
resolveCustodyBreak: nomos_protocol.ResolveCustodyBreakRequest(
|
|
2510
|
+
session: _runtimeSession(session),
|
|
2511
|
+
decision: decision,
|
|
2512
|
+
),
|
|
2513
|
+
),
|
|
2514
|
+
);
|
|
2515
|
+
if (!response.hasCustodyBreakResolved()) {
|
|
2516
|
+
throw StateError(
|
|
2517
|
+
'Runtime v2 returned ${response.whichOutcome().name} for resolveCustodyBreak',
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
return response.custodyBreakResolved;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
/// The parked salvaged intents (re-offerable local work an `adopt` rescued) — metadata only.
|
|
2524
|
+
Future<List<Map<String, Object?>>> salvagedIntents({String? session}) async {
|
|
2039
2525
|
final r = _asMap(
|
|
2040
2526
|
await _call('salvagedIntents', {if (session != null) 'session': session}),
|
|
2041
2527
|
);
|
|
@@ -2075,42 +2561,68 @@ class NomosBridge {
|
|
|
2075
2561
|
// parks the FULL intent durably in the workspace tree (it rides export()/restore); the app
|
|
2076
2562
|
// inspects, the domain dev ships a law fix, retry unjams. ──
|
|
2077
2563
|
|
|
2078
|
-
/// The parked dead letters
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
final
|
|
2082
|
-
|
|
2564
|
+
/// The parked dead letters; intent bytes stay in durable custody.
|
|
2565
|
+
Future<List<nomos_protocol.DeadLetterEntry>> deadLetters(
|
|
2566
|
+
{String? session}) async {
|
|
2567
|
+
final response = await _invokeRuntimeV2(
|
|
2568
|
+
nomos_protocol.RuntimeRequest(
|
|
2569
|
+
deadLetter: nomos_protocol.DeadLetterRequest(
|
|
2570
|
+
session: _runtimeSession(session),
|
|
2571
|
+
list: nomos_protocol.ListDeadLettersRequest(),
|
|
2572
|
+
),
|
|
2573
|
+
),
|
|
2083
2574
|
);
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
.
|
|
2575
|
+
if (!response.hasDeadLetter() || !response.deadLetter.hasList()) {
|
|
2576
|
+
throw StateError(
|
|
2577
|
+
'Runtime v2 returned ${response.whichOutcome().name} for deadLetters',
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
return List.unmodifiable(response.deadLetter.list.entries);
|
|
2087
2581
|
}
|
|
2088
2582
|
|
|
2089
2583
|
/// Retry one dead letter (after the law fix landed): pulls canon first (it may already be
|
|
2090
2584
|
/// admitted), else re-offers the parked bytes locally. `{ok, head?|alreadyOnMain?|error?}` —
|
|
2091
2585
|
/// a refusal leaves the entry parked (with the fresh error), never lost.
|
|
2092
|
-
Future<
|
|
2586
|
+
Future<nomos_protocol.DeadLetterRetry> retryDeadLetter(
|
|
2093
2587
|
String id, {
|
|
2094
2588
|
String? session,
|
|
2095
|
-
}) async
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2589
|
+
}) async {
|
|
2590
|
+
final response = await _invokeRuntimeV2(
|
|
2591
|
+
nomos_protocol.RuntimeRequest(
|
|
2592
|
+
deadLetter: nomos_protocol.DeadLetterRequest(
|
|
2593
|
+
session: _runtimeSession(session),
|
|
2594
|
+
retry: nomos_protocol.RetryDeadLetterRequest(intentId: id),
|
|
2595
|
+
),
|
|
2596
|
+
),
|
|
2597
|
+
);
|
|
2598
|
+
if (!response.hasDeadLetter() || !response.deadLetter.hasRetry()) {
|
|
2599
|
+
throw StateError(
|
|
2600
|
+
'Runtime v2 returned ${response.whichOutcome().name} for retryDeadLetter',
|
|
2101
2601
|
);
|
|
2602
|
+
}
|
|
2603
|
+
return response.deadLetter.retry;
|
|
2604
|
+
}
|
|
2102
2605
|
|
|
2103
2606
|
/// Discard one dead letter — the app's EXPLICIT choice to drop the work. `{ok}`.
|
|
2104
|
-
Future<
|
|
2607
|
+
Future<nomos_protocol.DeadLetterDiscard> discardDeadLetter(
|
|
2105
2608
|
String id, {
|
|
2106
2609
|
String? session,
|
|
2107
|
-
}) async
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2610
|
+
}) async {
|
|
2611
|
+
final response = await _invokeRuntimeV2(
|
|
2612
|
+
nomos_protocol.RuntimeRequest(
|
|
2613
|
+
deadLetter: nomos_protocol.DeadLetterRequest(
|
|
2614
|
+
session: _runtimeSession(session),
|
|
2615
|
+
discard: nomos_protocol.DiscardDeadLetterRequest(intentId: id),
|
|
2616
|
+
),
|
|
2617
|
+
),
|
|
2618
|
+
);
|
|
2619
|
+
if (!response.hasDeadLetter() || !response.deadLetter.hasDiscard()) {
|
|
2620
|
+
throw StateError(
|
|
2621
|
+
'Runtime v2 returned ${response.whichOutcome().name} for discardDeadLetter',
|
|
2113
2622
|
);
|
|
2623
|
+
}
|
|
2624
|
+
return response.deadLetter.discard;
|
|
2625
|
+
}
|
|
2114
2626
|
|
|
2115
2627
|
/// SALVAGE — best-effort extraction of this holon's local-only authored work (un-synced pending
|
|
2116
2628
|
/// writes + the DLQ), each with its full `changeB64` bytes: `{pendingWrites, deadLetters}`. The
|
|
@@ -2233,6 +2745,164 @@ class NomosBridge {
|
|
|
2233
2745
|
birthCertificate: birthCertificate,
|
|
2234
2746
|
);
|
|
2235
2747
|
|
|
2748
|
+
/// The existing offer operation with its tenant intent authored as canonical
|
|
2749
|
+
/// USDA and carried by the generated protobuf ABI. This is not a second write
|
|
2750
|
+
/// endpoint: generated domain methods use it to reach the same kernel
|
|
2751
|
+
/// [OfferRequest] without a generic map or JavaScript serializer.
|
|
2752
|
+
@internal
|
|
2753
|
+
Future<NomosOfferOutcome> offerCallUsdaOutcome({
|
|
2754
|
+
required NmsGeneratedCall call,
|
|
2755
|
+
String? actor,
|
|
2756
|
+
String? domainHash,
|
|
2757
|
+
String? session,
|
|
2758
|
+
String? authorSecret,
|
|
2759
|
+
NomosBirthCertificate? birthCertificate,
|
|
2760
|
+
Iterable<NomosConflictSnapshot> conflictSnapshots = const [],
|
|
2761
|
+
NomosOfferAuthenticationProof authenticationProof =
|
|
2762
|
+
NomosOfferAuthenticationProof.sessionCredentials,
|
|
2763
|
+
}) async {
|
|
2764
|
+
final runtimeSession = session ?? _sessionName;
|
|
2765
|
+
if (runtimeSession == null || runtimeSession.isEmpty) {
|
|
2766
|
+
throw StateError(
|
|
2767
|
+
'A typed offer must be bound to a connected NomosSession',
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
final transport = _transport;
|
|
2771
|
+
if (transport is! NomosKernelCallTransport) {
|
|
2772
|
+
throw UnsupportedError(
|
|
2773
|
+
'This host does not implement the generated protobuf kernel lane',
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
final kernelTransport = transport as NomosKernelCallTransport;
|
|
2777
|
+
final plane = _eventPlane;
|
|
2778
|
+
final workspace =
|
|
2779
|
+
plane._workspaceBySession[runtimeSession] ?? runtimeSession;
|
|
2780
|
+
final sequence = (plane._sessionOfferSequences[runtimeSession] ?? 0) + 1;
|
|
2781
|
+
plane._sessionOfferSequences[runtimeSession] = sequence;
|
|
2782
|
+
final random = Random.secure();
|
|
2783
|
+
final replica = plane._sessionReplicas.putIfAbsent(
|
|
2784
|
+
workspace,
|
|
2785
|
+
() => random.nextInt(0x7fffffff) + 1,
|
|
2786
|
+
);
|
|
2787
|
+
final now = DateTime.now().millisecondsSinceEpoch;
|
|
2788
|
+
final sessionToken = plane._sessionAuthTokens[runtimeSession];
|
|
2789
|
+
final secret = authorSecret ?? plane._sessionAuthorSecrets[runtimeSession];
|
|
2790
|
+
if (authenticationProof == NomosOfferAuthenticationProof.enrolledDevice &&
|
|
2791
|
+
(secret == null || secret.isEmpty)) {
|
|
2792
|
+
throw StateError(
|
|
2793
|
+
'This law requires proof from an enrolled device, but the session has no device key',
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
final token = switch (authenticationProof) {
|
|
2797
|
+
NomosOfferAuthenticationProof.sessionCredentials => sessionToken,
|
|
2798
|
+
NomosOfferAuthenticationProof.enrolledDevice => null,
|
|
2799
|
+
};
|
|
2800
|
+
final response = await kernelTransport.callKernel(
|
|
2801
|
+
session: runtimeSession,
|
|
2802
|
+
request: nomos_protocol.CallRequest(
|
|
2803
|
+
protocolVersion: 1,
|
|
2804
|
+
requestId: '$runtimeSession:offer:$sequence',
|
|
2805
|
+
offer: nomos_protocol.OfferRequest(
|
|
2806
|
+
target: nomos_protocol.WorkspaceTarget(
|
|
2807
|
+
repo: '/work/ws/$runtimeSession',
|
|
2808
|
+
workspace: workspace,
|
|
2809
|
+
branch: 'main',
|
|
2810
|
+
),
|
|
2811
|
+
call: nomos_protocol.DirectiveCall(
|
|
2812
|
+
domain: call.domain,
|
|
2813
|
+
directive: call.directive,
|
|
2814
|
+
callUsda: call.canonicalUsda,
|
|
2815
|
+
),
|
|
2816
|
+
authoring: nomos_protocol.AuthoringOptions(
|
|
2817
|
+
actor: actor ?? principal ?? '',
|
|
2818
|
+
authToken: token == null ? const <int>[] : utf8.encode(token),
|
|
2819
|
+
authorSecret: secret == null ? const <int>[] : base64Decode(secret),
|
|
2820
|
+
rng: List<double>.generate(64, (_) => random.nextDouble()),
|
|
2821
|
+
deferProjection: true,
|
|
2822
|
+
spans: true,
|
|
2823
|
+
sequence: fixnum.Int64(sequence),
|
|
2824
|
+
clock: nomos_protocol.CapturedClock(
|
|
2825
|
+
physicalMillis: fixnum.Int64(now),
|
|
2826
|
+
logical: 0,
|
|
2827
|
+
replica: fixnum.Int64(replica),
|
|
2828
|
+
),
|
|
2829
|
+
),
|
|
2830
|
+
controllerLawHash: domainHash,
|
|
2831
|
+
conflictSnapshots: conflictSnapshots,
|
|
2832
|
+
birthCertificate: birthCertificate,
|
|
2833
|
+
),
|
|
2834
|
+
),
|
|
2835
|
+
);
|
|
2836
|
+
if (response.hasFailure()) {
|
|
2837
|
+
throw NomosKernelCallFailure(
|
|
2838
|
+
code: response.failure.code,
|
|
2839
|
+
message: response.failure.message,
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
if (!response.hasOffer()) {
|
|
2843
|
+
throw StateError(
|
|
2844
|
+
'Kernel returned ${response.whichOutcome().name} for an offer request',
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
final offer = response.offer;
|
|
2848
|
+
if (!offer.hasAdmitted()) throw NomosTypedOfferFailure(offer);
|
|
2849
|
+
final admitted = offer.admitted;
|
|
2850
|
+
final birthOutcomes = List<NomosBirthOutcome>.unmodifiable(
|
|
2851
|
+
admitted.birthOutcomes,
|
|
2852
|
+
);
|
|
2853
|
+
final failedBirths = birthOutcomes
|
|
2854
|
+
.where(
|
|
2855
|
+
(birth) =>
|
|
2856
|
+
birth.status != NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
|
|
2857
|
+
)
|
|
2858
|
+
.toList(growable: false);
|
|
2859
|
+
if (failedBirths.isNotEmpty) {
|
|
2860
|
+
throw BirthIncomplete(failedBirths, head: admitted.head);
|
|
2861
|
+
}
|
|
2862
|
+
final receipt =
|
|
2863
|
+
_registerOperationReceipt(runtimeSession, admitted.intentId);
|
|
2864
|
+
final born = admitted.born
|
|
2865
|
+
.map((child) => child.workspace)
|
|
2866
|
+
.where((child) => child.isNotEmpty)
|
|
2867
|
+
.toList(growable: false);
|
|
2868
|
+
for (final child in born) {
|
|
2869
|
+
plane._birthReceipts[child] = receipt;
|
|
2870
|
+
final offerPrincipal = actor ?? principal;
|
|
2871
|
+
if (offerPrincipal != null && offerPrincipal.isNotEmpty) {
|
|
2872
|
+
plane._sessionPrincipals[child] = offerPrincipal;
|
|
2873
|
+
}
|
|
2874
|
+
if (secret != null && secret.isNotEmpty) {
|
|
2875
|
+
plane._sessionAuthorSecrets[child] = secret;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
return NomosOfferOutcome(
|
|
2879
|
+
head: admitted.head,
|
|
2880
|
+
intentId: admitted.intentId,
|
|
2881
|
+
receipt: receipt,
|
|
2882
|
+
workspace: workspace,
|
|
2883
|
+
born: born,
|
|
2884
|
+
birthOutcomes: birthOutcomes,
|
|
2885
|
+
created: admitted.created
|
|
2886
|
+
.map(
|
|
2887
|
+
(created) => NomosCreated(
|
|
2888
|
+
type: created.type,
|
|
2889
|
+
id: created.id,
|
|
2890
|
+
workspace: workspace,
|
|
2891
|
+
),
|
|
2892
|
+
)
|
|
2893
|
+
.toList(growable: false),
|
|
2894
|
+
affected: admitted.affected
|
|
2895
|
+
.map(
|
|
2896
|
+
(affected) => NomosCreated(
|
|
2897
|
+
type: affected.type,
|
|
2898
|
+
id: affected.id,
|
|
2899
|
+
workspace: workspace,
|
|
2900
|
+
),
|
|
2901
|
+
)
|
|
2902
|
+
.toList(growable: false),
|
|
2903
|
+
);
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2236
2906
|
/// THE ONE OUTCOME-RETURNING OFFER LANE — [offerDirective] answering the full typed [NomosOfferOutcome]
|
|
2237
2907
|
/// (head + law-minted `created` NomosRefs + births), for EVERY generated method (create or mutate). A pure
|
|
2238
2908
|
/// mutation returns `created: []`; a `.creates(...)` plan returns the minted aggregate id(s), each already
|
|
@@ -2278,12 +2948,15 @@ class NomosBridge {
|
|
|
2278
2948
|
base64Encode(birthCertificate.writeToBuffer()),
|
|
2279
2949
|
}),
|
|
2280
2950
|
);
|
|
2281
|
-
} catch (e) {
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2951
|
+
} catch (e, stack) {
|
|
2952
|
+
Error.throwWithStackTrace(
|
|
2953
|
+
await _wrapDomainNotInstalled(
|
|
2954
|
+
e,
|
|
2955
|
+
domain: domain,
|
|
2956
|
+
domainHash: domainHash,
|
|
2957
|
+
requestedWorkspace: session,
|
|
2958
|
+
),
|
|
2959
|
+
stack,
|
|
2287
2960
|
);
|
|
2288
2961
|
}
|
|
2289
2962
|
final t1 = DateTime.now().millisecondsSinceEpoch;
|
|
@@ -2314,15 +2987,18 @@ class NomosBridge {
|
|
|
2314
2987
|
}
|
|
2315
2988
|
final births = ((r['birthOutcomes'] as List?) ?? const [])
|
|
2316
2989
|
.whereType<Map>()
|
|
2317
|
-
.map(
|
|
2318
|
-
.toList();
|
|
2990
|
+
.map(_birthOutcomeFromLegacyWire)
|
|
2991
|
+
.toList(growable: false);
|
|
2319
2992
|
// FAIL FAST on a birth that did NOT come up. The JS holon (index.mjs) already throws on this, so we
|
|
2320
2993
|
// normally never see it here — but a runner/bundle skew (an older runner that passes birthOutcomes
|
|
2321
2994
|
// through without throwing) must NOT let a silent partial birth reach the app to strand on head:null.
|
|
2322
2995
|
// A `status` other than "born" is a failure; throw the typed [BirthIncomplete].
|
|
2323
2996
|
final failedBirths = births
|
|
2324
|
-
.where(
|
|
2325
|
-
|
|
2997
|
+
.where(
|
|
2998
|
+
(birth) =>
|
|
2999
|
+
birth.status != NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
|
|
3000
|
+
)
|
|
3001
|
+
.toList(growable: false);
|
|
2326
3002
|
if (failedBirths.isNotEmpty) {
|
|
2327
3003
|
throw BirthIncomplete(failedBirths, head: (r['head'] ?? '').toString());
|
|
2328
3004
|
}
|
|
@@ -2335,9 +3011,7 @@ class NomosBridge {
|
|
|
2335
3011
|
(e) => (e is Map ? (e['workspace'] ?? '') : e).toString(),
|
|
2336
3012
|
)
|
|
2337
3013
|
else
|
|
2338
|
-
...births
|
|
2339
|
-
.map((m) => (m['workspace'] ?? '').toString())
|
|
2340
|
-
.where((s) => s.isNotEmpty),
|
|
3014
|
+
...births.map((birth) => birth.workspace).where((s) => s.isNotEmpty),
|
|
2341
3015
|
]..removeWhere((s) => s.isEmpty);
|
|
2342
3016
|
final birthPrincipal = actor ?? principal;
|
|
2343
3017
|
if (birthPrincipal != null && birthPrincipal.isNotEmpty) {
|
|
@@ -2446,11 +3120,21 @@ class NomosBridge {
|
|
|
2446
3120
|
String? directiveId,
|
|
2447
3121
|
required String packageHash,
|
|
2448
3122
|
}) async {
|
|
2449
|
-
await
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
3123
|
+
final response = await _invokeRuntimeV2(
|
|
3124
|
+
nomos_protocol.RuntimeRequest(
|
|
3125
|
+
prepareDomain: nomos_protocol.PrepareDomainRequest(
|
|
3126
|
+
session: _runtimeSession(),
|
|
3127
|
+
domain: domain,
|
|
3128
|
+
packageSha256: packageHash,
|
|
3129
|
+
directive: directiveId,
|
|
3130
|
+
),
|
|
3131
|
+
),
|
|
3132
|
+
);
|
|
3133
|
+
if (!response.hasDomainPrepared()) {
|
|
3134
|
+
throw StateError(
|
|
3135
|
+
'Runtime v2 returned ${response.whichOutcome().name} for prepareDomain',
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
2454
3138
|
}
|
|
2455
3139
|
|
|
2456
3140
|
/// FOLLOW A [NomosRef] — THE cross-workspace primitive (session_bound_clients.md ask 8): mount
|
|
@@ -2615,7 +3299,7 @@ class NomosBridge {
|
|
|
2615
3299
|
var resident = false;
|
|
2616
3300
|
try {
|
|
2617
3301
|
resident = (await listWorkspaces()).any(
|
|
2618
|
-
(
|
|
3302
|
+
(candidate) => candidate.workspace == workspace,
|
|
2619
3303
|
);
|
|
2620
3304
|
} catch (_) {
|
|
2621
3305
|
// a pre-connect plane has no registry yet — fall through to the credentials decision below.
|
|
@@ -2718,6 +3402,13 @@ class NomosBridge {
|
|
|
2718
3402
|
// together. Otherwise home signs correctly after first-device enrolment
|
|
2719
3403
|
// while a later catalogue/estate opens with the old empty default key.
|
|
2720
3404
|
final session = _sessionName;
|
|
3405
|
+
if (session != null && authToken != null) {
|
|
3406
|
+
if (authToken.isEmpty) {
|
|
3407
|
+
_eventPlane._sessionAuthTokens.remove(session);
|
|
3408
|
+
} else {
|
|
3409
|
+
_eventPlane._sessionAuthTokens[session] = authToken;
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
2721
3412
|
if (session != null && authorSecret != null) {
|
|
2722
3413
|
if (authorSecret.isEmpty) {
|
|
2723
3414
|
_eventPlane._sessionAuthorSecrets.remove(session);
|
|
@@ -2836,12 +3527,15 @@ class NomosBridge {
|
|
|
2836
3527
|
if (authToken != null) 'authToken': authToken,
|
|
2837
3528
|
}),
|
|
2838
3529
|
);
|
|
2839
|
-
} catch (e) {
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
3530
|
+
} catch (e, stack) {
|
|
3531
|
+
Error.throwWithStackTrace(
|
|
3532
|
+
await _wrapDomainNotInstalled(
|
|
3533
|
+
e,
|
|
3534
|
+
domain: domain,
|
|
3535
|
+
domainHash: domainHash,
|
|
3536
|
+
requestedWorkspace: parent,
|
|
3537
|
+
),
|
|
3538
|
+
stack,
|
|
2845
3539
|
);
|
|
2846
3540
|
}
|
|
2847
3541
|
final verdict = (r['verdict'] as Map?)?.cast<String, Object?>() ?? r;
|
|
@@ -2949,20 +3643,30 @@ class NomosBridge {
|
|
|
2949
3643
|
/// Framework-private exact-history adoption seam. Generated clients expose this as
|
|
2950
3644
|
/// `model.adopt()` and, only when prior business outcomes differ, a chained
|
|
2951
3645
|
/// `decision.accept(reason)`. Candidate identities, heads and receipts stay here.
|
|
2952
|
-
Future<
|
|
3646
|
+
Future<nomos_protocol.ModelLifecycleResponse>
|
|
3647
|
+
nomosFrameworkAdoptGeneratedModel({
|
|
2953
3648
|
required String domain,
|
|
2954
3649
|
required String candidateHash,
|
|
2955
3650
|
String? because,
|
|
2956
3651
|
}) async {
|
|
2957
|
-
final
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3652
|
+
final response = await _invokeRuntimeV2(
|
|
3653
|
+
nomos_protocol.RuntimeRequest(
|
|
3654
|
+
modelLifecycle: nomos_protocol.ModelLifecycleRequest(
|
|
3655
|
+
session: _runtimeSession(),
|
|
3656
|
+
adopt: nomos_protocol.AdoptGeneratedModelRequest(
|
|
3657
|
+
domain: domain,
|
|
3658
|
+
candidateHash: candidateHash,
|
|
3659
|
+
acceptanceReason: because,
|
|
3660
|
+
),
|
|
3661
|
+
),
|
|
3662
|
+
),
|
|
2963
3663
|
);
|
|
2964
|
-
|
|
2965
|
-
|
|
3664
|
+
if (!response.hasModelLifecycle()) {
|
|
3665
|
+
throw StateError(
|
|
3666
|
+
'Runtime v2 returned ${response.whichOutcome().name} for model adoption',
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
return response.modelLifecycle;
|
|
2966
3670
|
}
|
|
2967
3671
|
|
|
2968
3672
|
/// Framework-private generated-model staging seam. The generated client owns
|
|
@@ -2973,30 +3677,54 @@ class NomosBridge {
|
|
|
2973
3677
|
required String candidateHash,
|
|
2974
3678
|
required List<int> lawUsdz,
|
|
2975
3679
|
}) async {
|
|
2976
|
-
await
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3680
|
+
final response = await _invokeRuntimeV2(
|
|
3681
|
+
nomos_protocol.RuntimeRequest(
|
|
3682
|
+
modelLifecycle: nomos_protocol.ModelLifecycleRequest(
|
|
3683
|
+
session: _runtimeSession(),
|
|
3684
|
+
stage: nomos_protocol.StageGeneratedModelRequest(
|
|
3685
|
+
domain: domain,
|
|
3686
|
+
candidateHash: candidateHash,
|
|
3687
|
+
lawUsdz: lawUsdz,
|
|
3688
|
+
),
|
|
3689
|
+
),
|
|
3690
|
+
),
|
|
3691
|
+
);
|
|
3692
|
+
if (!response.hasModelLifecycle() || !response.modelLifecycle.accepted) {
|
|
3693
|
+
throw StateError(
|
|
3694
|
+
response.hasModelLifecycle() && response.modelLifecycle.hasMessage()
|
|
3695
|
+
? response.modelLifecycle.message
|
|
3696
|
+
: 'Runtime v2 refused generated model staging',
|
|
3697
|
+
);
|
|
3698
|
+
}
|
|
2981
3699
|
}
|
|
2982
3700
|
|
|
2983
3701
|
/// Framework-private exact-history restoration seam. Generated clients expose
|
|
2984
3702
|
/// `model.restorePrevious()` and a chained `decision.accept(reason)`; predecessor
|
|
2985
3703
|
/// identity, derived dispositions, heads and receipts stay below this bridge.
|
|
2986
|
-
Future<
|
|
3704
|
+
Future<nomos_protocol.ModelLifecycleResponse>
|
|
3705
|
+
nomosFrameworkRestorePreviousGeneratedModel({
|
|
2987
3706
|
required String domain,
|
|
2988
3707
|
required String currentHash,
|
|
2989
3708
|
String? because,
|
|
2990
3709
|
}) async {
|
|
2991
|
-
final
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
3710
|
+
final response = await _invokeRuntimeV2(
|
|
3711
|
+
nomos_protocol.RuntimeRequest(
|
|
3712
|
+
modelLifecycle: nomos_protocol.ModelLifecycleRequest(
|
|
3713
|
+
session: _runtimeSession(),
|
|
3714
|
+
restore: nomos_protocol.RestoreGeneratedModelRequest(
|
|
3715
|
+
domain: domain,
|
|
3716
|
+
currentHash: currentHash,
|
|
3717
|
+
acceptanceReason: because,
|
|
3718
|
+
),
|
|
3719
|
+
),
|
|
3720
|
+
),
|
|
2997
3721
|
);
|
|
2998
|
-
|
|
2999
|
-
|
|
3722
|
+
if (!response.hasModelLifecycle()) {
|
|
3723
|
+
throw StateError(
|
|
3724
|
+
'Runtime v2 returned ${response.whichOutcome().name} for model restoration',
|
|
3725
|
+
);
|
|
3726
|
+
}
|
|
3727
|
+
return response.modelLifecycle;
|
|
3000
3728
|
}
|
|
3001
3729
|
|
|
3002
3730
|
/// HISTORY of one aggregate (a todo) — the full action timeline from the ledger: every intent that touched
|
|
@@ -3231,10 +3959,62 @@ class NomosBridge {
|
|
|
3231
3959
|
|
|
3232
3960
|
/// Framework-private receiving-group setup. Generated application hosts own this ceremony; tenant code
|
|
3233
3961
|
/// receives only the resulting business application.
|
|
3234
|
-
Future<
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3962
|
+
Future<nomos_protocol.ApplicationCustodyEstablished>
|
|
3963
|
+
nomosInternalBirthReceivingGroup({
|
|
3964
|
+
required String parent,
|
|
3965
|
+
required String principal,
|
|
3966
|
+
required String subject,
|
|
3967
|
+
required Map<String, Object?> device,
|
|
3968
|
+
required String authToken,
|
|
3969
|
+
required String idToken,
|
|
3970
|
+
required String verifiedVia,
|
|
3971
|
+
required String bornAt,
|
|
3972
|
+
required NmsGeneratedCall enrollFirstDeviceCall,
|
|
3973
|
+
required NmsGeneratedCall birthReceivingGroupCall,
|
|
3974
|
+
bool confirm = true,
|
|
3975
|
+
}) async {
|
|
3976
|
+
final secret = device['secret'];
|
|
3977
|
+
final public = device['public'];
|
|
3978
|
+
if (secret is! String || public is! String) {
|
|
3979
|
+
throw ArgumentError(
|
|
3980
|
+
'application custody requires a generated device keypair',
|
|
3981
|
+
);
|
|
3982
|
+
}
|
|
3983
|
+
final response = await _invokeRuntimeV2(
|
|
3984
|
+
nomos_protocol.RuntimeRequest(
|
|
3985
|
+
establishApplicationCustody:
|
|
3986
|
+
nomos_protocol.EstablishApplicationCustodyRequest(
|
|
3987
|
+
session: _runtimeSession(),
|
|
3988
|
+
parent: parent,
|
|
3989
|
+
principal: principal,
|
|
3990
|
+
subject: subject,
|
|
3991
|
+
deviceSecret: base64Decode(secret),
|
|
3992
|
+
devicePublic: base64Decode(public),
|
|
3993
|
+
authToken: authToken,
|
|
3994
|
+
idToken: idToken,
|
|
3995
|
+
verifiedVia: verifiedVia,
|
|
3996
|
+
bornAt: bornAt,
|
|
3997
|
+
enrollFirstDevice: nomos_protocol.DirectiveCall(
|
|
3998
|
+
domain: enrollFirstDeviceCall.domain,
|
|
3999
|
+
directive: enrollFirstDeviceCall.directive,
|
|
4000
|
+
callUsda: enrollFirstDeviceCall.canonicalUsda,
|
|
4001
|
+
),
|
|
4002
|
+
birthReceivingGroup: nomos_protocol.DirectiveCall(
|
|
4003
|
+
domain: birthReceivingGroupCall.domain,
|
|
4004
|
+
directive: birthReceivingGroupCall.directive,
|
|
4005
|
+
callUsda: birthReceivingGroupCall.canonicalUsda,
|
|
4006
|
+
),
|
|
4007
|
+
confirm: confirm,
|
|
4008
|
+
),
|
|
4009
|
+
),
|
|
4010
|
+
);
|
|
4011
|
+
if (!response.hasApplicationCustodyEstablished()) {
|
|
4012
|
+
throw StateError(
|
|
4013
|
+
'Runtime v2 returned ${response.whichOutcome().name} for application custody',
|
|
4014
|
+
);
|
|
4015
|
+
}
|
|
4016
|
+
return response.applicationCustodyEstablished;
|
|
4017
|
+
}
|
|
3238
4018
|
|
|
3239
4019
|
/// Sign a cross-custody SHARE attestation `{secret, asserterWorkspace, object, relation, subject,
|
|
3240
4020
|
/// attestedAt}` → `{publicKey, signature}`. This is what drives `recordShare` into an invitee's home
|
|
@@ -3274,75 +4054,772 @@ class NomosBridge {
|
|
|
3274
4054
|
}),
|
|
3275
4055
|
);
|
|
3276
4056
|
|
|
3277
|
-
|
|
3278
|
-
/// scope keys so encrypted fields AEAD-open in-wasm; absent ⇒ they stay opaque envelopes (today's
|
|
3279
|
-
/// behavior). `principal` stamps the private-read gate (defaults to the connection's bound principal).
|
|
3280
|
-
/// Both are OMITTED from the wire when absent, so a public un-keyed read is byte-identical to before.
|
|
3281
|
-
Future<List<Map<String, dynamic>>> query(
|
|
3282
|
-
String queryId, [
|
|
3283
|
-
Map<String, Object?> params = const {},
|
|
4057
|
+
nomos_protocol.QueryAuthority _nmsQueryAuthority(
|
|
3284
4058
|
List<NomosScopeKey>? keys,
|
|
3285
4059
|
String? principal,
|
|
3286
4060
|
String? session,
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
4061
|
+
) {
|
|
4062
|
+
final runtimeSession = session ?? _sessionName;
|
|
4063
|
+
if (runtimeSession == null || runtimeSession.isEmpty) {
|
|
4064
|
+
throw StateError(
|
|
4065
|
+
'A nominal query must be bound to a connected NomosSession');
|
|
4066
|
+
}
|
|
4067
|
+
final plane = _eventPlane;
|
|
4068
|
+
final token = plane._sessionAuthTokens[runtimeSession];
|
|
4069
|
+
final secret = plane._sessionAuthorSecrets[runtimeSession];
|
|
4070
|
+
return nomos_protocol.QueryAuthority(
|
|
4071
|
+
principalClaim:
|
|
4072
|
+
principal ?? plane._sessionPrincipals[runtimeSession] ?? '',
|
|
4073
|
+
authToken: token == null ? const <int>[] : utf8.encode(token),
|
|
4074
|
+
nowUnix: fixnum.Int64(
|
|
4075
|
+
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
|
4076
|
+
),
|
|
4077
|
+
authorSecret: secret == null ? const <int>[] : base64Decode(secret),
|
|
4078
|
+
keys: (keys ?? const <NomosScopeKey>[]).map(
|
|
4079
|
+
(key) => nomos_protocol.InjectedKey(
|
|
4080
|
+
scope: key.scope,
|
|
4081
|
+
epoch: fixnum.Int64(key.epoch),
|
|
4082
|
+
key: base64Decode(key.key),
|
|
4083
|
+
),
|
|
4084
|
+
),
|
|
3295
4085
|
);
|
|
3296
4086
|
}
|
|
3297
4087
|
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
Map<String, Object?> params = const {},
|
|
3303
|
-
int pageSize = 100,
|
|
3304
|
-
String? cursor,
|
|
3305
|
-
List<NomosScopeKey>? keys,
|
|
3306
|
-
String? principal,
|
|
3307
|
-
String? session,
|
|
4088
|
+
Future<Uint8List> _nmsKernelQueryUsda({
|
|
4089
|
+
required String? session,
|
|
4090
|
+
required nomos_protocol.QueryAuthority authority,
|
|
4091
|
+
required nomos_protocol.QueryRequest query,
|
|
3308
4092
|
}) async {
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
await _call('queryPage', {
|
|
3314
|
-
'queryId': queryId,
|
|
3315
|
-
'params': params,
|
|
3316
|
-
'pageSize': pageSize,
|
|
3317
|
-
if (cursor != null) 'cursor': cursor,
|
|
3318
|
-
...?_readAuthArgs(keys, principal),
|
|
3319
|
-
if (session != null) 'session': session,
|
|
3320
|
-
}),
|
|
4093
|
+
final response = await _nmsKernelQuery(
|
|
4094
|
+
session: session,
|
|
4095
|
+
authority: authority,
|
|
4096
|
+
query: query,
|
|
3321
4097
|
);
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
4098
|
+
if (!response.hasResultUsda()) {
|
|
4099
|
+
throw StateError(
|
|
4100
|
+
'Kernel returned ${response.whichResult().name} without a nominal USDA query result',
|
|
4101
|
+
);
|
|
4102
|
+
}
|
|
4103
|
+
return Uint8List.fromList(response.resultUsda);
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
Future<nomos_protocol.QueryResponse> _nmsKernelQuery({
|
|
4107
|
+
required String? session,
|
|
4108
|
+
required nomos_protocol.QueryAuthority authority,
|
|
4109
|
+
required nomos_protocol.QueryRequest query,
|
|
4110
|
+
}) async {
|
|
4111
|
+
final runtimeSession = session ?? _sessionName;
|
|
4112
|
+
if (runtimeSession == null || runtimeSession.isEmpty) {
|
|
4113
|
+
throw StateError(
|
|
4114
|
+
'A nominal query must be bound to a connected NomosSession');
|
|
4115
|
+
}
|
|
4116
|
+
final workspace =
|
|
4117
|
+
_eventPlane._workspaceBySession[runtimeSession] ?? runtimeSession;
|
|
4118
|
+
final transport = _transport;
|
|
4119
|
+
if (transport is! NomosKernelCallTransport) {
|
|
4120
|
+
throw UnsupportedError(
|
|
4121
|
+
'This host does not implement the generated Runtime v2 protobuf lane',
|
|
4122
|
+
);
|
|
4123
|
+
}
|
|
4124
|
+
query
|
|
4125
|
+
..target = nomos_protocol.WorkspaceTarget(
|
|
4126
|
+
repo: '/work/ws/$runtimeSession',
|
|
4127
|
+
workspace: workspace,
|
|
4128
|
+
branch: 'main',
|
|
4129
|
+
)
|
|
4130
|
+
..authority = authority
|
|
4131
|
+
..spans = true
|
|
4132
|
+
..resultFormat =
|
|
4133
|
+
nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA;
|
|
4134
|
+
final requestNumber = ++_seq;
|
|
4135
|
+
final response = await (transport as NomosKernelCallTransport).callKernel(
|
|
4136
|
+
session: runtimeSession,
|
|
4137
|
+
request: nomos_protocol.CallRequest(
|
|
4138
|
+
protocolVersion: 1,
|
|
4139
|
+
requestId: '$runtimeSession:query:$requestNumber',
|
|
4140
|
+
query: query,
|
|
4141
|
+
),
|
|
3326
4142
|
);
|
|
4143
|
+
if (response.hasFailure()) {
|
|
4144
|
+
throw StateError('${response.failure.code}: ${response.failure.message}');
|
|
4145
|
+
}
|
|
4146
|
+
if (!response.hasQuery()) {
|
|
4147
|
+
throw StateError(
|
|
4148
|
+
'Kernel returned ${response.whichOutcome().name} without a query result',
|
|
4149
|
+
);
|
|
4150
|
+
}
|
|
4151
|
+
return response.query;
|
|
3327
4152
|
}
|
|
3328
4153
|
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
4154
|
+
NomosProviderTokenVerification _providerTokenVerification(
|
|
4155
|
+
nomos_protocol.ProviderTokenVerificationStatus status,
|
|
4156
|
+
) =>
|
|
4157
|
+
switch (status) {
|
|
4158
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4159
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_VALID =>
|
|
4160
|
+
NomosProviderTokenVerification.valid,
|
|
4161
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4162
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_NO_MATCHING_KEY =>
|
|
4163
|
+
NomosProviderTokenVerification.noMatchingKey,
|
|
4164
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4165
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_EXPIRED =>
|
|
4166
|
+
NomosProviderTokenVerification.expired,
|
|
4167
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4168
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_NOT_YET_VALID =>
|
|
4169
|
+
NomosProviderTokenVerification.notYetValid,
|
|
4170
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4171
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_ISSUER_MISMATCH =>
|
|
4172
|
+
NomosProviderTokenVerification.issuerMismatch,
|
|
4173
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4174
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_AUDIENCE_MISMATCH =>
|
|
4175
|
+
NomosProviderTokenVerification.audienceMismatch,
|
|
4176
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4177
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_BAD_SIGNATURE =>
|
|
4178
|
+
NomosProviderTokenVerification.badSignature,
|
|
4179
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4180
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_MALFORMED =>
|
|
4181
|
+
NomosProviderTokenVerification.malformed,
|
|
4182
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4183
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_UNSUPPORTED_ALGORITHM =>
|
|
4184
|
+
NomosProviderTokenVerification.unsupportedAlgorithm,
|
|
4185
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4186
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_BAD_KEY =>
|
|
4187
|
+
NomosProviderTokenVerification.badKey,
|
|
4188
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4189
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_MISSING_SUBJECT =>
|
|
4190
|
+
NomosProviderTokenVerification.missingSubject,
|
|
4191
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4192
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_NO_VERIFIER =>
|
|
4193
|
+
NomosProviderTokenVerification.noVerifier,
|
|
4194
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4195
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_MISSING_TOKEN =>
|
|
4196
|
+
NomosProviderTokenVerification.missingToken,
|
|
4197
|
+
nomos_protocol.ProviderTokenVerificationStatus
|
|
4198
|
+
.PROVIDER_TOKEN_VERIFICATION_STATUS_UNSPECIFIED =>
|
|
4199
|
+
NomosProviderTokenVerification.unspecified,
|
|
4200
|
+
_ => NomosProviderTokenVerification.unspecified,
|
|
4201
|
+
};
|
|
4202
|
+
|
|
4203
|
+
/// Ask the kernel—not the host—to verify the session's current provider
|
|
4204
|
+
/// token and enrolled-device relation against the workspace's current law.
|
|
4205
|
+
@internal
|
|
4206
|
+
Future<NomosAuthenticationPosture> nmsAuthenticationPosture({
|
|
3334
4207
|
String? session,
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
4208
|
+
String? principal,
|
|
4209
|
+
}) async {
|
|
4210
|
+
final response = await _nmsKernelQuery(
|
|
4211
|
+
session: session,
|
|
4212
|
+
authority: _nmsQueryAuthority(null, principal, session),
|
|
4213
|
+
query: nomos_protocol.QueryRequest(
|
|
4214
|
+
authenticationPosture: nomos_protocol.AuthenticationPostureQuery(),
|
|
4215
|
+
),
|
|
4216
|
+
);
|
|
4217
|
+
if (!response.hasAuthenticationPosture()) {
|
|
4218
|
+
throw StateError(
|
|
4219
|
+
'Kernel returned ${response.whichResult().name}, expected authenticationPosture',
|
|
3342
4220
|
);
|
|
4221
|
+
}
|
|
4222
|
+
final posture = response.authenticationPosture;
|
|
4223
|
+
return NomosAuthenticationPosture(
|
|
4224
|
+
providerToken: _providerTokenVerification(posture.providerToken),
|
|
4225
|
+
enrolledDevice: posture.enrolledDevice,
|
|
4226
|
+
resolvedPrincipal: posture.resolvedPrincipal,
|
|
4227
|
+
);
|
|
4228
|
+
}
|
|
3343
4229
|
|
|
3344
|
-
///
|
|
3345
|
-
///
|
|
4230
|
+
/// Drive actual custody synchronization until the kernel proves that the
|
|
4231
|
+
/// provider token verifies, the device remains enrolled, and the resolved
|
|
4232
|
+
/// principal is the session principal. No timer or receipt is treated as the
|
|
4233
|
+
/// business postcondition.
|
|
4234
|
+
@internal
|
|
4235
|
+
Future<NomosAuthenticationPosture> nmsAwaitAuthenticationReady({
|
|
4236
|
+
String? session,
|
|
4237
|
+
String? principal,
|
|
4238
|
+
int maximumSyncRounds = 4,
|
|
4239
|
+
}) async {
|
|
4240
|
+
final runtimeSession = session ?? _sessionName;
|
|
4241
|
+
final expectedPrincipal = principal ??
|
|
4242
|
+
this.principal ??
|
|
4243
|
+
(runtimeSession == null
|
|
4244
|
+
? null
|
|
4245
|
+
: _eventPlane._sessionPrincipals[runtimeSession]) ??
|
|
4246
|
+
'';
|
|
4247
|
+
var posture = await nmsAuthenticationPosture(
|
|
4248
|
+
session: session,
|
|
4249
|
+
principal: expectedPrincipal,
|
|
4250
|
+
);
|
|
4251
|
+
if (posture.isReadyFor(expectedPrincipal)) return posture;
|
|
4252
|
+
|
|
4253
|
+
for (var round = 0; round < maximumSyncRounds; round++) {
|
|
4254
|
+
final result = await sync(admit: true, session: session);
|
|
4255
|
+
if (result.offline || result.pushFailed) {
|
|
4256
|
+
throw StateError(
|
|
4257
|
+
'Authentication recovery could not converge custody: '
|
|
4258
|
+
'${result.hasPushError() ? result.pushError.message : 'workspace is offline'}',
|
|
4259
|
+
);
|
|
4260
|
+
}
|
|
4261
|
+
posture = await nmsAuthenticationPosture(
|
|
4262
|
+
session: session,
|
|
4263
|
+
principal: expectedPrincipal,
|
|
4264
|
+
);
|
|
4265
|
+
if (posture.isReadyFor(expectedPrincipal)) return posture;
|
|
4266
|
+
}
|
|
4267
|
+
throw StateError(
|
|
4268
|
+
'Authentication recovery did not establish a valid provider token and enrolled device '
|
|
4269
|
+
'for $expectedPrincipal after $maximumSyncRounds custody rounds '
|
|
4270
|
+
'(token: ${posture.providerToken.name}, enrolled: ${posture.enrolledDevice}, '
|
|
4271
|
+
'resolved: ${posture.resolvedPrincipal})',
|
|
4272
|
+
);
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
Future<nomos_protocol.QueryResponse> _nmsKernelHistoricQuery({
|
|
4276
|
+
required NomosMoment moment,
|
|
4277
|
+
required String? session,
|
|
4278
|
+
required nomos_protocol.QueryAuthority authority,
|
|
4279
|
+
required nomos_protocol.QueryRequest query,
|
|
4280
|
+
}) async {
|
|
4281
|
+
final runtimeSession = session ?? _sessionName;
|
|
4282
|
+
if (runtimeSession == null || runtimeSession.isEmpty) {
|
|
4283
|
+
throw StateError(
|
|
4284
|
+
'A nominal historic query must be bound to a connected NomosSession');
|
|
4285
|
+
}
|
|
4286
|
+
final workspace =
|
|
4287
|
+
_eventPlane._workspaceBySession[runtimeSession] ?? runtimeSession;
|
|
4288
|
+
query
|
|
4289
|
+
..target = nomos_protocol.WorkspaceTarget(
|
|
4290
|
+
repo: '/work/ws/$runtimeSession',
|
|
4291
|
+
workspace: workspace,
|
|
4292
|
+
branch: 'main',
|
|
4293
|
+
)
|
|
4294
|
+
..authority = authority
|
|
4295
|
+
..spans = true
|
|
4296
|
+
..resultFormat =
|
|
4297
|
+
nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA;
|
|
4298
|
+
final response = await _invokeRuntimeV2(
|
|
4299
|
+
nomos_protocol.RuntimeRequest(
|
|
4300
|
+
historicQuery: nomos_protocol.HistoricQueryRequest(
|
|
4301
|
+
session: runtimeSession,
|
|
4302
|
+
moment: moment.id,
|
|
4303
|
+
query: query,
|
|
4304
|
+
),
|
|
4305
|
+
),
|
|
4306
|
+
);
|
|
4307
|
+
if (response.hasFailure()) {
|
|
4308
|
+
throw StateError('${response.failure.code}: ${response.failure.message}');
|
|
4309
|
+
}
|
|
4310
|
+
if (!response.hasKernelCall()) {
|
|
4311
|
+
throw StateError(
|
|
4312
|
+
'Runtime v2 returned ${response.whichOutcome().name} for a historic query',
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
final kernel = response.kernelCall;
|
|
4316
|
+
if (kernel.hasFailure()) {
|
|
4317
|
+
throw StateError('${kernel.failure.code}: ${kernel.failure.message}');
|
|
4318
|
+
}
|
|
4319
|
+
if (!kernel.hasQuery()) {
|
|
4320
|
+
throw StateError(
|
|
4321
|
+
'Kernel returned ${kernel.whichOutcome().name} without a historic query result',
|
|
4322
|
+
);
|
|
4323
|
+
}
|
|
4324
|
+
return kernel.query;
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
Future<Uint8List> _nmsKernelHistoricQueryUsda({
|
|
4328
|
+
required NomosMoment moment,
|
|
4329
|
+
required String? session,
|
|
4330
|
+
required nomos_protocol.QueryAuthority authority,
|
|
4331
|
+
required nomos_protocol.QueryRequest query,
|
|
4332
|
+
}) async {
|
|
4333
|
+
final response = await _nmsKernelHistoricQuery(
|
|
4334
|
+
moment: moment,
|
|
4335
|
+
session: session,
|
|
4336
|
+
authority: authority,
|
|
4337
|
+
query: query,
|
|
4338
|
+
);
|
|
4339
|
+
if (!response.hasResultUsda()) {
|
|
4340
|
+
throw StateError(
|
|
4341
|
+
'Kernel returned ${response.whichResult().name} without a nominal historic USDA result',
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4344
|
+
return Uint8List.fromList(response.resultUsda);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
/// A declared query's rows from the local projection. `keys` (E2E sealing) injects the reader's held
|
|
4348
|
+
/// scope keys so encrypted fields AEAD-open in-wasm; absent ⇒ they stay opaque envelopes (today's
|
|
4349
|
+
/// behavior). `principal` stamps the private-read gate (defaults to the connection's bound principal).
|
|
4350
|
+
/// Both are OMITTED from the wire when absent, so a public un-keyed read is byte-identical to before.
|
|
4351
|
+
Future<List<Map<String, dynamic>>> query(
|
|
4352
|
+
String queryId, [
|
|
4353
|
+
Map<String, Object?> params = const {},
|
|
4354
|
+
List<NomosScopeKey>? keys,
|
|
4355
|
+
String? principal,
|
|
4356
|
+
String? session,
|
|
4357
|
+
]) async {
|
|
4358
|
+
return _asRows(
|
|
4359
|
+
await _call('query', {
|
|
4360
|
+
'queryId': queryId,
|
|
4361
|
+
'params': params,
|
|
4362
|
+
...?_readAuthArgs(keys, principal),
|
|
4363
|
+
if (session != null) 'session': session,
|
|
4364
|
+
}),
|
|
4365
|
+
);
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
/// Framework-private tenant read lane. The command crosses the resident host
|
|
4369
|
+
/// as one generated Runtime v2/Kernel v1 protobuf frame and the domain result
|
|
4370
|
+
/// returns as canonical USDA bytes for the generated domain decoder.
|
|
4371
|
+
Future<Uint8List> nmsQueryUsda(
|
|
4372
|
+
NmsGeneratedQuery query, [
|
|
4373
|
+
List<NomosScopeKey>? keys,
|
|
4374
|
+
String? principal,
|
|
4375
|
+
String? session,
|
|
4376
|
+
]) =>
|
|
4377
|
+
_nmsKernelQueryUsda(
|
|
4378
|
+
session: session,
|
|
4379
|
+
authority: _nmsQueryAuthority(keys, principal, session),
|
|
4380
|
+
query: nomos_protocol.QueryRequest(
|
|
4381
|
+
rows: nomos_protocol.RowsQuery(
|
|
4382
|
+
id: query.query,
|
|
4383
|
+
principal: principal ?? '',
|
|
4384
|
+
argumentsUsda: query.canonicalArgumentsUsda,
|
|
4385
|
+
),
|
|
4386
|
+
),
|
|
4387
|
+
);
|
|
4388
|
+
|
|
4389
|
+
/// The same generated declared query at one authenticated historic moment.
|
|
4390
|
+
/// Runtime v2 pins custody; the kernel still owns query semantics and emits
|
|
4391
|
+
/// the same nominal USDA result schema as the live read.
|
|
4392
|
+
Future<Uint8List> nmsQueryAtUsda(
|
|
4393
|
+
NomosMoment moment,
|
|
4394
|
+
NmsGeneratedQuery query, [
|
|
4395
|
+
List<NomosScopeKey>? keys,
|
|
4396
|
+
String? principal,
|
|
4397
|
+
String? session,
|
|
4398
|
+
]) =>
|
|
4399
|
+
_nmsKernelHistoricQueryUsda(
|
|
4400
|
+
moment: moment,
|
|
4401
|
+
session: session,
|
|
4402
|
+
authority: _nmsQueryAuthority(keys, principal, session),
|
|
4403
|
+
query: nomos_protocol.QueryRequest(
|
|
4404
|
+
rows: nomos_protocol.RowsQuery(
|
|
4405
|
+
id: query.query,
|
|
4406
|
+
principal: principal ?? '',
|
|
4407
|
+
argumentsUsda: query.canonicalArgumentsUsda,
|
|
4408
|
+
),
|
|
4409
|
+
),
|
|
4410
|
+
);
|
|
4411
|
+
|
|
4412
|
+
/// Reactive form of [nmsQueryUsda]. Subscription mechanics are Runtime v2
|
|
4413
|
+
/// protobuf; each emission is the kernel's nominal USDA result and is decoded
|
|
4414
|
+
/// only by the generated domain client which owns [query].
|
|
4415
|
+
Stream<Uint8List> nmsWatchUsda(
|
|
4416
|
+
NmsGeneratedQuery query, {
|
|
4417
|
+
List<NomosScopeKey>? keys,
|
|
4418
|
+
String? principal,
|
|
4419
|
+
String? session,
|
|
4420
|
+
int intervalMs = 2000,
|
|
4421
|
+
}) =>
|
|
4422
|
+
_nmsWatchQueryUsda(
|
|
4423
|
+
nomos_protocol.QueryRequest(
|
|
4424
|
+
rows: nomos_protocol.RowsQuery(
|
|
4425
|
+
id: query.query,
|
|
4426
|
+
principal: principal ?? '',
|
|
4427
|
+
argumentsUsda: query.canonicalArgumentsUsda,
|
|
4428
|
+
),
|
|
4429
|
+
),
|
|
4430
|
+
keys: keys,
|
|
4431
|
+
principal: principal,
|
|
4432
|
+
session: session,
|
|
4433
|
+
intervalMs: intervalMs,
|
|
4434
|
+
);
|
|
4435
|
+
|
|
4436
|
+
/// Reactive by-id read. The generated domain decoder supplies the nominal
|
|
4437
|
+
/// aggregate type; this framework method owns only the protobuf mechanics.
|
|
4438
|
+
Stream<Uint8List> nmsWatchByIdUsda(
|
|
4439
|
+
String aggregateType,
|
|
4440
|
+
String id, {
|
|
4441
|
+
List<NomosScopeKey>? keys,
|
|
4442
|
+
String? principal,
|
|
4443
|
+
String? session,
|
|
4444
|
+
int intervalMs = 2000,
|
|
4445
|
+
}) =>
|
|
4446
|
+
_nmsWatchQueryUsda(
|
|
4447
|
+
nomos_protocol.QueryRequest(
|
|
4448
|
+
byId: nomos_protocol.ByIdQuery(
|
|
4449
|
+
aggregateType: aggregateType,
|
|
4450
|
+
aggregateId: id,
|
|
4451
|
+
principal: principal ?? '',
|
|
4452
|
+
),
|
|
4453
|
+
),
|
|
4454
|
+
keys: keys,
|
|
4455
|
+
principal: principal,
|
|
4456
|
+
session: session,
|
|
4457
|
+
intervalMs: intervalMs,
|
|
4458
|
+
);
|
|
4459
|
+
|
|
4460
|
+
/// Reactive type-index read. The aggregate type is a dedicated protobuf
|
|
4461
|
+
/// field, never a magic application query name or argument bag.
|
|
4462
|
+
Stream<Uint8List> nmsWatchByTypeUsda(
|
|
4463
|
+
String aggregateType, {
|
|
4464
|
+
List<NomosScopeKey>? keys,
|
|
4465
|
+
String? principal,
|
|
4466
|
+
String? session,
|
|
4467
|
+
int intervalMs = 2000,
|
|
4468
|
+
}) =>
|
|
4469
|
+
_nmsWatchQueryUsda(
|
|
4470
|
+
nomos_protocol.QueryRequest(
|
|
4471
|
+
byType: nomos_protocol.ByTypeQuery(
|
|
4472
|
+
aggregateType: aggregateType,
|
|
4473
|
+
principal: principal ?? '',
|
|
4474
|
+
),
|
|
4475
|
+
),
|
|
4476
|
+
keys: keys,
|
|
4477
|
+
principal: principal,
|
|
4478
|
+
session: session,
|
|
4479
|
+
intervalMs: intervalMs,
|
|
4480
|
+
);
|
|
4481
|
+
|
|
4482
|
+
Stream<Uint8List> _nmsWatchQueryUsda(
|
|
4483
|
+
nomos_protocol.QueryRequest request, {
|
|
4484
|
+
List<NomosScopeKey>? keys,
|
|
4485
|
+
String? principal,
|
|
4486
|
+
String? session,
|
|
4487
|
+
int intervalMs = 2000,
|
|
4488
|
+
}) {
|
|
4489
|
+
final runtimeSession = session ?? _sessionName;
|
|
4490
|
+
if (runtimeSession == null || runtimeSession.isEmpty) {
|
|
4491
|
+
throw StateError(
|
|
4492
|
+
'A nominal watch must be bound to a connected NomosSession');
|
|
4493
|
+
}
|
|
4494
|
+
final transport = _transport;
|
|
4495
|
+
if (transport is! NomosRuntimeV2EventTransport) {
|
|
4496
|
+
throw UnsupportedError(
|
|
4497
|
+
'This host does not implement Runtime v2 event frames');
|
|
4498
|
+
}
|
|
4499
|
+
final plane = _eventPlane;
|
|
4500
|
+
final workspace =
|
|
4501
|
+
plane._workspaceBySession[runtimeSession] ?? runtimeSession;
|
|
4502
|
+
final subscriptionId = '$runtimeSession:watch:${++plane._seq}';
|
|
4503
|
+
late final StreamController<Uint8List> controller;
|
|
4504
|
+
controller = StreamController<Uint8List>.broadcast(
|
|
4505
|
+
onCancel: () async {
|
|
4506
|
+
plane._nominalWatchControllers.remove(subscriptionId);
|
|
4507
|
+
try {
|
|
4508
|
+
await _unwatch(subscriptionId);
|
|
4509
|
+
} finally {
|
|
4510
|
+
if (!controller.isClosed) await controller.close();
|
|
4511
|
+
}
|
|
4512
|
+
},
|
|
4513
|
+
);
|
|
4514
|
+
plane._nominalWatchControllers[subscriptionId] = controller;
|
|
4515
|
+
request
|
|
4516
|
+
..target = nomos_protocol.WorkspaceTarget(
|
|
4517
|
+
repo: '/work/ws/$runtimeSession',
|
|
4518
|
+
workspace: workspace,
|
|
4519
|
+
branch: 'main',
|
|
4520
|
+
)
|
|
4521
|
+
..authority = _nmsQueryAuthority(keys, principal, runtimeSession)
|
|
4522
|
+
..spans = true
|
|
4523
|
+
..resultFormat =
|
|
4524
|
+
nomos_protocol.QueryResultFormat.QUERY_RESULT_FORMAT_NOMINAL_USDA;
|
|
4525
|
+
unawaited(() async {
|
|
4526
|
+
try {
|
|
4527
|
+
final response = await _invokeRuntimeV2(
|
|
4528
|
+
nomos_protocol.RuntimeRequest(
|
|
4529
|
+
watch: nomos_protocol.WatchRequest(
|
|
4530
|
+
session: runtimeSession,
|
|
4531
|
+
subscriptionId: subscriptionId,
|
|
4532
|
+
kind: nomos_protocol.WatchKind.WATCH_KIND_QUERY,
|
|
4533
|
+
query: request,
|
|
4534
|
+
intervalMillis: intervalMs,
|
|
4535
|
+
),
|
|
4536
|
+
),
|
|
4537
|
+
);
|
|
4538
|
+
if (!response.hasWatchStarted() ||
|
|
4539
|
+
response.watchStarted.subscriptionId != subscriptionId) {
|
|
4540
|
+
throw StateError(
|
|
4541
|
+
'Runtime v2 returned ${response.whichOutcome().name} for nominal watch',
|
|
4542
|
+
);
|
|
4543
|
+
}
|
|
4544
|
+
} catch (error, stackTrace) {
|
|
4545
|
+
plane._nominalWatchControllers.remove(subscriptionId);
|
|
4546
|
+
if (!controller.isClosed) controller.addError(error, stackTrace);
|
|
4547
|
+
}
|
|
4548
|
+
}());
|
|
4549
|
+
return controller.stream;
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
/// One bounded query page. Unlike [query], this never requires the kernel to materialize or serialize
|
|
4553
|
+
/// the complete result set merely to show the first screen.
|
|
4554
|
+
Future<NomosPage<Map<String, dynamic>>> queryPage(
|
|
4555
|
+
String queryId, {
|
|
4556
|
+
Map<String, Object?> params = const {},
|
|
4557
|
+
int pageSize = 100,
|
|
4558
|
+
String? cursor,
|
|
4559
|
+
List<NomosScopeKey>? keys,
|
|
4560
|
+
String? principal,
|
|
4561
|
+
String? session,
|
|
4562
|
+
}) async {
|
|
4563
|
+
if (pageSize < 1 || pageSize > 1000) {
|
|
4564
|
+
throw RangeError.range(pageSize, 1, 1000, 'pageSize');
|
|
4565
|
+
}
|
|
4566
|
+
final result = _asMap(
|
|
4567
|
+
await _call('queryPage', {
|
|
4568
|
+
'queryId': queryId,
|
|
4569
|
+
'params': params,
|
|
4570
|
+
'pageSize': pageSize,
|
|
4571
|
+
if (cursor != null) 'cursor': cursor,
|
|
4572
|
+
...?_readAuthArgs(keys, principal),
|
|
4573
|
+
if (session != null) 'session': session,
|
|
4574
|
+
}),
|
|
4575
|
+
);
|
|
4576
|
+
return NomosPage<Map<String, dynamic>>(
|
|
4577
|
+
rows: _asRows(result),
|
|
4578
|
+
cursor: result['cursor'] as String?,
|
|
4579
|
+
done: result['done'] == true,
|
|
4580
|
+
);
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
/// Framework-private bounded form of [nmsQueryUsda]. Page metadata lives in
|
|
4584
|
+
/// the same USDA result; no object envelope is reconstructed in the host.
|
|
4585
|
+
Future<Uint8List> nmsQueryPageUsda(
|
|
4586
|
+
NmsGeneratedQuery query, {
|
|
4587
|
+
int pageSize = 100,
|
|
4588
|
+
String? cursor,
|
|
4589
|
+
List<NomosScopeKey>? keys,
|
|
4590
|
+
String? principal,
|
|
4591
|
+
String? session,
|
|
4592
|
+
}) {
|
|
4593
|
+
if (pageSize < 1 || pageSize > 1000) {
|
|
4594
|
+
throw RangeError.range(pageSize, 1, 1000, 'pageSize');
|
|
4595
|
+
}
|
|
4596
|
+
return _nmsKernelQueryUsda(
|
|
4597
|
+
session: session,
|
|
4598
|
+
authority: _nmsQueryAuthority(keys, principal, session),
|
|
4599
|
+
query: nomos_protocol.QueryRequest(
|
|
4600
|
+
page: nomos_protocol.PageQuery(
|
|
4601
|
+
id: query.query,
|
|
4602
|
+
limit: pageSize,
|
|
4603
|
+
after: cursor ?? '',
|
|
4604
|
+
principal: principal ?? '',
|
|
4605
|
+
argumentsUsda: query.canonicalArgumentsUsda,
|
|
4606
|
+
),
|
|
4607
|
+
),
|
|
4608
|
+
);
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
/// A single aggregate by id (the by-id read). `keys`/`principal`: see [query].
|
|
4612
|
+
Future<List<Map<String, dynamic>>> queryById(
|
|
4613
|
+
String id, [
|
|
4614
|
+
List<NomosScopeKey>? keys,
|
|
4615
|
+
String? principal,
|
|
4616
|
+
String? session,
|
|
4617
|
+
]) async =>
|
|
4618
|
+
_asRows(
|
|
4619
|
+
await _call('queryById', {
|
|
4620
|
+
'id': id,
|
|
4621
|
+
...?_readAuthArgs(keys, principal),
|
|
4622
|
+
if (session != null) 'session': session,
|
|
4623
|
+
}),
|
|
4624
|
+
);
|
|
4625
|
+
|
|
4626
|
+
/// Framework-private by-id form of [nmsQueryUsda].
|
|
4627
|
+
Future<Uint8List> nmsQueryByIdUsda(
|
|
4628
|
+
String aggregateType,
|
|
4629
|
+
String id, [
|
|
4630
|
+
List<NomosScopeKey>? keys,
|
|
4631
|
+
String? principal,
|
|
4632
|
+
String? session,
|
|
4633
|
+
]) =>
|
|
4634
|
+
_nmsKernelQueryUsda(
|
|
4635
|
+
session: session,
|
|
4636
|
+
authority: _nmsQueryAuthority(keys, principal, session),
|
|
4637
|
+
query: nomos_protocol.QueryRequest(
|
|
4638
|
+
byId: nomos_protocol.ByIdQuery(
|
|
4639
|
+
aggregateType: aggregateType,
|
|
4640
|
+
aggregateId: id,
|
|
4641
|
+
principal: principal ?? '',
|
|
4642
|
+
),
|
|
4643
|
+
),
|
|
4644
|
+
);
|
|
4645
|
+
|
|
4646
|
+
Future<Uint8List> nmsQueryByIdAtUsda(
|
|
4647
|
+
NomosMoment moment,
|
|
4648
|
+
String aggregateType,
|
|
4649
|
+
String id, [
|
|
4650
|
+
List<NomosScopeKey>? keys,
|
|
4651
|
+
String? principal,
|
|
4652
|
+
String? session,
|
|
4653
|
+
]) =>
|
|
4654
|
+
_nmsKernelHistoricQueryUsda(
|
|
4655
|
+
moment: moment,
|
|
4656
|
+
session: session,
|
|
4657
|
+
authority: _nmsQueryAuthority(keys, principal, session),
|
|
4658
|
+
query: nomos_protocol.QueryRequest(
|
|
4659
|
+
byId: nomos_protocol.ByIdQuery(
|
|
4660
|
+
aggregateType: aggregateType,
|
|
4661
|
+
aggregateId: id,
|
|
4662
|
+
principal: principal ?? '',
|
|
4663
|
+
),
|
|
4664
|
+
),
|
|
4665
|
+
);
|
|
4666
|
+
|
|
4667
|
+
/// Framework-private type-index read. Aggregate type is a fixed protobuf field, not a fake
|
|
4668
|
+
/// application query argument.
|
|
4669
|
+
Future<Uint8List> nmsQueryByTypeUsda(
|
|
4670
|
+
String aggregateType, [
|
|
4671
|
+
List<NomosScopeKey>? keys,
|
|
4672
|
+
String? principal,
|
|
4673
|
+
String? session,
|
|
4674
|
+
]) =>
|
|
4675
|
+
_nmsKernelQueryUsda(
|
|
4676
|
+
session: session,
|
|
4677
|
+
authority: _nmsQueryAuthority(keys, principal, session),
|
|
4678
|
+
query: nomos_protocol.QueryRequest(
|
|
4679
|
+
byType: nomos_protocol.ByTypeQuery(
|
|
4680
|
+
aggregateType: aggregateType,
|
|
4681
|
+
principal: principal ?? '',
|
|
4682
|
+
),
|
|
4683
|
+
),
|
|
4684
|
+
);
|
|
4685
|
+
|
|
4686
|
+
Future<Uint8List> nmsQueryByTypeAtUsda(
|
|
4687
|
+
NomosMoment moment,
|
|
4688
|
+
String aggregateType, [
|
|
4689
|
+
List<NomosScopeKey>? keys,
|
|
4690
|
+
String? principal,
|
|
4691
|
+
String? session,
|
|
4692
|
+
]) =>
|
|
4693
|
+
_nmsKernelHistoricQueryUsda(
|
|
4694
|
+
moment: moment,
|
|
4695
|
+
session: session,
|
|
4696
|
+
authority: _nmsQueryAuthority(keys, principal, session),
|
|
4697
|
+
query: nomos_protocol.QueryRequest(
|
|
4698
|
+
byType: nomos_protocol.ByTypeQuery(
|
|
4699
|
+
aggregateType: aggregateType,
|
|
4700
|
+
principal: principal ?? '',
|
|
4701
|
+
),
|
|
4702
|
+
),
|
|
4703
|
+
);
|
|
4704
|
+
|
|
4705
|
+
/// Exact spatial-index read for generated domain clients. [spatialId] is
|
|
4706
|
+
/// generated from the installed DSL graph; the application supplies only
|
|
4707
|
+
/// the typed [bbox]. Rows remain canonical USDA until its generated decoder.
|
|
4708
|
+
Future<Uint8List> nmsSpatialWithinUsda(
|
|
4709
|
+
String spatialId,
|
|
4710
|
+
NomosBbox bbox, [
|
|
4711
|
+
String? session,
|
|
4712
|
+
]) =>
|
|
4713
|
+
_nmsKernelQueryUsda(
|
|
4714
|
+
session: session,
|
|
4715
|
+
authority: _nmsQueryAuthority(null, null, session),
|
|
4716
|
+
query: nomos_protocol.QueryRequest(
|
|
4717
|
+
spatial: nomos_protocol.SpatialQuery(
|
|
4718
|
+
id: spatialId,
|
|
4719
|
+
minX: bbox.minX,
|
|
4720
|
+
minY: bbox.minY,
|
|
4721
|
+
maxX: bbox.maxX,
|
|
4722
|
+
maxY: bbox.maxY,
|
|
4723
|
+
),
|
|
4724
|
+
),
|
|
4725
|
+
);
|
|
4726
|
+
|
|
4727
|
+
/// Reactive counterpart of [nmsSpatialWithinUsda]. Runtime v2 owns the
|
|
4728
|
+
/// subscription; every value is still the kernel's canonical USDA image.
|
|
4729
|
+
Stream<Uint8List> nmsWatchSpatialUsda(
|
|
4730
|
+
String spatialId,
|
|
4731
|
+
NomosBbox bbox, {
|
|
4732
|
+
String? session,
|
|
4733
|
+
int intervalMs = 2000,
|
|
4734
|
+
}) =>
|
|
4735
|
+
_nmsWatchQueryUsda(
|
|
4736
|
+
nomos_protocol.QueryRequest(
|
|
4737
|
+
spatial: nomos_protocol.SpatialQuery(
|
|
4738
|
+
id: spatialId,
|
|
4739
|
+
minX: bbox.minX,
|
|
4740
|
+
minY: bbox.minY,
|
|
4741
|
+
maxX: bbox.maxX,
|
|
4742
|
+
maxY: bbox.maxY,
|
|
4743
|
+
),
|
|
4744
|
+
),
|
|
4745
|
+
session: session,
|
|
4746
|
+
intervalMs: intervalMs,
|
|
4747
|
+
);
|
|
4748
|
+
|
|
4749
|
+
Future<int> _nmsTally(
|
|
4750
|
+
String id,
|
|
4751
|
+
String? groupKey, {
|
|
4752
|
+
required bool sum,
|
|
4753
|
+
String? session,
|
|
4754
|
+
NomosMoment? moment,
|
|
4755
|
+
}) async {
|
|
4756
|
+
final tally = nomos_protocol.TallyQuery(
|
|
4757
|
+
id: id,
|
|
4758
|
+
groupKey: groupKey,
|
|
4759
|
+
);
|
|
4760
|
+
final query = nomos_protocol.QueryRequest();
|
|
4761
|
+
if (sum) {
|
|
4762
|
+
query.sum = tally;
|
|
4763
|
+
} else {
|
|
4764
|
+
query.count = tally;
|
|
4765
|
+
}
|
|
4766
|
+
final authority = _nmsQueryAuthority(null, null, session);
|
|
4767
|
+
final result = moment == null
|
|
4768
|
+
? await _nmsKernelQuery(
|
|
4769
|
+
session: session,
|
|
4770
|
+
authority: authority,
|
|
4771
|
+
query: query,
|
|
4772
|
+
)
|
|
4773
|
+
: await _nmsKernelHistoricQuery(
|
|
4774
|
+
moment: moment,
|
|
4775
|
+
session: session,
|
|
4776
|
+
authority: authority,
|
|
4777
|
+
query: query,
|
|
4778
|
+
);
|
|
4779
|
+
if (!result.hasIntegerValue()) {
|
|
4780
|
+
throw StateError(
|
|
4781
|
+
'Kernel returned ${result.whichResult().name} without an exact integer tally',
|
|
4782
|
+
);
|
|
4783
|
+
}
|
|
4784
|
+
return result.integerValue.toInt();
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
Future<int> nmsCount(String countId, [String? groupKey, String? session]) =>
|
|
4788
|
+
_nmsTally(countId, groupKey, sum: false, session: session);
|
|
4789
|
+
|
|
4790
|
+
Future<int> nmsCountAt(
|
|
4791
|
+
NomosMoment moment,
|
|
4792
|
+
String countId, [
|
|
4793
|
+
String? groupKey,
|
|
4794
|
+
String? session,
|
|
4795
|
+
]) =>
|
|
4796
|
+
_nmsTally(
|
|
4797
|
+
countId,
|
|
4798
|
+
groupKey,
|
|
4799
|
+
sum: false,
|
|
4800
|
+
session: session,
|
|
4801
|
+
moment: moment,
|
|
4802
|
+
);
|
|
4803
|
+
|
|
4804
|
+
Future<int> nmsSum(String sumId, [String? groupKey, String? session]) =>
|
|
4805
|
+
_nmsTally(sumId, groupKey, sum: true, session: session);
|
|
4806
|
+
|
|
4807
|
+
Future<int> nmsSumAt(
|
|
4808
|
+
NomosMoment moment,
|
|
4809
|
+
String sumId, [
|
|
4810
|
+
String? groupKey,
|
|
4811
|
+
String? session,
|
|
4812
|
+
]) =>
|
|
4813
|
+
_nmsTally(
|
|
4814
|
+
sumId,
|
|
4815
|
+
groupKey,
|
|
4816
|
+
sum: true,
|
|
4817
|
+
session: session,
|
|
4818
|
+
moment: moment,
|
|
4819
|
+
);
|
|
4820
|
+
|
|
4821
|
+
/// Compare an immutable publication pin with the stable series' one compiled
|
|
4822
|
+
/// current pointer. This is an O(current-state) check: it neither resolves the
|
|
3346
4823
|
/// pinned publication nor reconstructs aggregate history.
|
|
3347
4824
|
Future<ReferenceVersionStatus<TVersion>>
|
|
3348
4825
|
checkReferenceVersion<TVersion, TSeries>(
|
|
@@ -3432,39 +4909,6 @@ class NomosBridge {
|
|
|
3432
4909
|
}),
|
|
3433
4910
|
);
|
|
3434
4911
|
|
|
3435
|
-
/// Maintained historic count/sum values at [moment]. These are the temporal counterparts of [count]/[sum].
|
|
3436
|
-
Future<int> countAt(
|
|
3437
|
-
NomosMoment moment,
|
|
3438
|
-
String countId, [
|
|
3439
|
-
String group = '',
|
|
3440
|
-
String? session,
|
|
3441
|
-
]) async =>
|
|
3442
|
-
((_asMap(
|
|
3443
|
-
await _call('countAt', {
|
|
3444
|
-
'commit': moment.id,
|
|
3445
|
-
'countId': countId,
|
|
3446
|
-
'group': group,
|
|
3447
|
-
if (session != null) 'session': session,
|
|
3448
|
-
}),
|
|
3449
|
-
)['count']) as num)
|
|
3450
|
-
.toInt();
|
|
3451
|
-
|
|
3452
|
-
Future<int> sumAt(
|
|
3453
|
-
NomosMoment moment,
|
|
3454
|
-
String sumId, [
|
|
3455
|
-
String group = '',
|
|
3456
|
-
String? session,
|
|
3457
|
-
]) async =>
|
|
3458
|
-
((_asMap(
|
|
3459
|
-
await _call('sumAt', {
|
|
3460
|
-
'commit': moment.id,
|
|
3461
|
-
'sumId': sumId,
|
|
3462
|
-
'group': group,
|
|
3463
|
-
if (session != null) 'session': session,
|
|
3464
|
-
}),
|
|
3465
|
-
)['sum']) as num)
|
|
3466
|
-
.toInt();
|
|
3467
|
-
|
|
3468
4912
|
/// The read auth sidecar (private reads + E2E): `keys`/`principal` as top-level call args, or null when
|
|
3469
4913
|
/// neither is present (so the wire is byte-identical to before this existed).
|
|
3470
4914
|
Map<String, Object?>? _readAuthArgs(
|
|
@@ -3527,7 +4971,9 @@ class NomosBridge {
|
|
|
3527
4971
|
epoch: (out['epoch'] as num?)?.toInt() ?? 0,
|
|
3528
4972
|
key: (out['key'] ?? '').toString(),
|
|
3529
4973
|
ownedKeySubject: out['ownedKeySubject']?.toString(),
|
|
3530
|
-
ownedCustody: out['ownedCustody']
|
|
4974
|
+
ownedCustody: out['ownedCustody'] == null
|
|
4975
|
+
? null
|
|
4976
|
+
: _nomosOwnedCustodyFromTransport(out['ownedCustody']),
|
|
3531
4977
|
);
|
|
3532
4978
|
}
|
|
3533
4979
|
|
|
@@ -3552,7 +4998,9 @@ class NomosBridge {
|
|
|
3552
4998
|
epoch: (key['epoch'] as num?)?.toInt() ?? 0,
|
|
3553
4999
|
key: (key['key'] ?? '').toString(),
|
|
3554
5000
|
ownedKeySubject: key['ownedKeySubject']?.toString(),
|
|
3555
|
-
ownedCustody: key['ownedCustody']
|
|
5001
|
+
ownedCustody: key['ownedCustody'] == null
|
|
5002
|
+
? null
|
|
5003
|
+
: _nomosOwnedCustodyFromTransport(key['ownedCustody']),
|
|
3556
5004
|
),
|
|
3557
5005
|
)
|
|
3558
5006
|
.toList();
|
|
@@ -3589,23 +5037,95 @@ class NomosBridge {
|
|
|
3589
5037
|
);
|
|
3590
5038
|
|
|
3591
5039
|
/// Prepare the private framework payload behind generated share/unshare/transfer methods.
|
|
3592
|
-
Future<
|
|
5040
|
+
Future<NomosOwnedRecordPreparation>
|
|
5041
|
+
nomosPrepareOwnedRecordAction<TAggregate>({
|
|
3593
5042
|
required String action,
|
|
3594
5043
|
required String scope,
|
|
3595
5044
|
required NomosRef<TAggregate> record,
|
|
3596
5045
|
required String peer,
|
|
3597
|
-
}) async
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
5046
|
+
}) async {
|
|
5047
|
+
final value = _asMap(
|
|
5048
|
+
await _call('cryptoOwnedPrepareRecordAction', {
|
|
5049
|
+
'action': action,
|
|
5050
|
+
'scope': scope,
|
|
5051
|
+
'record': {
|
|
5052
|
+
'type': NmsInternalNomosRefs.type(record),
|
|
5053
|
+
'id': NmsInternalNomosRefs.id(record),
|
|
5054
|
+
},
|
|
5055
|
+
'peer': peer,
|
|
5056
|
+
}),
|
|
5057
|
+
);
|
|
5058
|
+
final ownedKeySubject = _nomosOwnedRequiredString(
|
|
5059
|
+
value,
|
|
5060
|
+
'__nomos_owned_device_key_subject_7c1f5e',
|
|
5061
|
+
);
|
|
5062
|
+
final ownedCustodyRaw = value['__nomos_owned_device_custody_7c1f5e'];
|
|
5063
|
+
final ownedCustody = ownedCustodyRaw == null
|
|
5064
|
+
? null
|
|
5065
|
+
: _nomosOwnedCustodyFromTransport(ownedCustodyRaw);
|
|
5066
|
+
switch (action) {
|
|
5067
|
+
case 'share':
|
|
5068
|
+
final wraps = value['wraps'];
|
|
5069
|
+
if (wraps is! List) {
|
|
5070
|
+
throw const FormatException(
|
|
5071
|
+
'Nomos share preparation is missing its wraps',
|
|
5072
|
+
);
|
|
5073
|
+
}
|
|
5074
|
+
return NomosOwnedSharePreparation(
|
|
5075
|
+
ownedKeySubject: ownedKeySubject,
|
|
5076
|
+
ownedCustody: ownedCustody,
|
|
5077
|
+
recordId: _nomosOwnedRequiredString(value, 'recordId'),
|
|
5078
|
+
peer: _nomosOwnedRequiredString(value, 'peer'),
|
|
5079
|
+
epoch: _nomosOwnedRequiredInt(value, 'epoch'),
|
|
5080
|
+
sharedAt: _nomosOwnedRequiredString(value, 'sharedAt'),
|
|
5081
|
+
wraps: wraps
|
|
5082
|
+
.map(_asMap)
|
|
5083
|
+
.map(NomosHistoricOwnedWrapPreparation.fromTransport)
|
|
5084
|
+
.toList(growable: false),
|
|
5085
|
+
);
|
|
5086
|
+
case 'unshare':
|
|
5087
|
+
final wraps = value['wraps'];
|
|
5088
|
+
if (wraps is! List) {
|
|
5089
|
+
throw const FormatException(
|
|
5090
|
+
'Nomos unshare preparation is missing its wraps',
|
|
5091
|
+
);
|
|
5092
|
+
}
|
|
5093
|
+
return NomosOwnedUnsharePreparation(
|
|
5094
|
+
ownedKeySubject: ownedKeySubject,
|
|
5095
|
+
ownedCustody: ownedCustody,
|
|
5096
|
+
recordId: _nomosOwnedRequiredString(value, 'recordId'),
|
|
5097
|
+
peer: _nomosOwnedRequiredString(value, 'peer'),
|
|
5098
|
+
epoch: _nomosOwnedRequiredInt(value, 'epoch'),
|
|
5099
|
+
changedAt: _nomosOwnedRequiredString(value, 'changedAt'),
|
|
5100
|
+
wraps: wraps
|
|
5101
|
+
.map(_asMap)
|
|
5102
|
+
.map(NomosOwnedWrapPreparation.fromTransport)
|
|
5103
|
+
.toList(growable: false),
|
|
5104
|
+
);
|
|
5105
|
+
case 'transfer':
|
|
5106
|
+
final wraps = value['historicWraps'];
|
|
5107
|
+
if (wraps is! List) {
|
|
5108
|
+
throw const FormatException(
|
|
5109
|
+
'Nomos transfer preparation is missing its historic wraps',
|
|
5110
|
+
);
|
|
5111
|
+
}
|
|
5112
|
+
return NomosOwnedTransferPreparation(
|
|
5113
|
+
ownedKeySubject: ownedKeySubject,
|
|
5114
|
+
ownedCustody: ownedCustody,
|
|
5115
|
+
recordId: _nomosOwnedRequiredString(value, 'recordId'),
|
|
5116
|
+
newOwner: _nomosOwnedRequiredString(value, 'newOwner'),
|
|
5117
|
+
previousReaders: _nomosOwnedRequiredStrings(value, 'previousReaders'),
|
|
5118
|
+
epoch: _nomosOwnedRequiredInt(value, 'epoch'),
|
|
5119
|
+
changedAt: _nomosOwnedRequiredString(value, 'changedAt'),
|
|
5120
|
+
historicWraps: wraps
|
|
5121
|
+
.map(_asMap)
|
|
5122
|
+
.map(NomosHistoricOwnedWrapPreparation.fromTransport)
|
|
5123
|
+
.toList(growable: false),
|
|
5124
|
+
);
|
|
5125
|
+
default:
|
|
5126
|
+
throw ArgumentError.value(action, 'action', 'unknown owned action');
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
3609
5129
|
|
|
3610
5130
|
/// FRAMEWORK-INTERNAL business birth: derive the unborn child's owned key from its deterministic address.
|
|
3611
5131
|
Future<NomosScopeKey> nomosOwnedBirthScopeKey(
|
|
@@ -3628,29 +5148,68 @@ class NomosBridge {
|
|
|
3628
5148
|
epoch: (out['epoch'] as num?)?.toInt() ?? 0,
|
|
3629
5149
|
key: (out['key'] ?? '').toString(),
|
|
3630
5150
|
ownedKeySubject: out['ownedKeySubject']?.toString(),
|
|
3631
|
-
ownedCustody: out['ownedCustody']
|
|
5151
|
+
ownedCustody: out['ownedCustody'] == null
|
|
5152
|
+
? null
|
|
5153
|
+
: _nomosOwnedCustodyFromTransport(out['ownedCustody']),
|
|
3632
5154
|
workspace: out['workspace']?.toString(),
|
|
3633
5155
|
);
|
|
3634
5156
|
}
|
|
3635
5157
|
|
|
3636
5158
|
/// FRAMEWORK-INTERNAL: issue an opaque request for this session's current device.
|
|
3637
|
-
Future<
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
.
|
|
5159
|
+
Future<OwnedDeviceRequest> nomosOwnedDeviceRequest(String scope) async {
|
|
5160
|
+
final response = await _nmsKernelQuery(
|
|
5161
|
+
session: _sessionName,
|
|
5162
|
+
authority: _nmsQueryAuthority(null, null, _sessionName),
|
|
5163
|
+
query: nomos_protocol.QueryRequest(
|
|
5164
|
+
ownedDeviceRequest:
|
|
5165
|
+
nomos_protocol.OwnedDeviceRequestQuery(scope: scope),
|
|
5166
|
+
),
|
|
5167
|
+
);
|
|
5168
|
+
if (!response.hasOwnedDeviceRequest()) {
|
|
5169
|
+
throw StateError(
|
|
5170
|
+
'Kernel returned ${response.whichResult().name}, expected ownedDeviceRequest',
|
|
5171
|
+
);
|
|
5172
|
+
}
|
|
5173
|
+
return OwnedDeviceRequest.decodeUsda(
|
|
5174
|
+
Uint8List.fromList(response.ownedDeviceRequest.requestUsda),
|
|
5175
|
+
);
|
|
5176
|
+
}
|
|
3642
5177
|
|
|
3643
5178
|
/// FRAMEWORK-INTERNAL: validate an opaque request and prepare the atomic signer + custody offer.
|
|
3644
|
-
Future<
|
|
3645
|
-
|
|
5179
|
+
Future<NomosOwnedDeviceApprovalPreparation> nomosPrepareOwnedDeviceApproval(
|
|
5180
|
+
OwnedDeviceRequest request,
|
|
3646
5181
|
String scope,
|
|
3647
|
-
) async
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
5182
|
+
) async {
|
|
5183
|
+
final response = await _nmsKernelQuery(
|
|
5184
|
+
session: _sessionName,
|
|
5185
|
+
authority: _nmsQueryAuthority(null, null, _sessionName),
|
|
5186
|
+
query: nomos_protocol.QueryRequest(
|
|
5187
|
+
ownedPrepareApproval: nomos_protocol.OwnedPrepareApprovalQuery(
|
|
5188
|
+
scope: scope,
|
|
5189
|
+
requestUsda: request.encodeUsda(),
|
|
5190
|
+
),
|
|
5191
|
+
),
|
|
5192
|
+
);
|
|
5193
|
+
if (!response.hasOwnedPrepareApproval()) {
|
|
5194
|
+
throw StateError(
|
|
5195
|
+
'Kernel returned ${response.whichResult().name}, expected ownedPrepareApproval',
|
|
3653
5196
|
);
|
|
5197
|
+
}
|
|
5198
|
+
final prepared = response.ownedPrepareApproval;
|
|
5199
|
+
return NomosOwnedDeviceApprovalPreparation(
|
|
5200
|
+
newKeySubject: prepared.newKeySubject,
|
|
5201
|
+
ownedCustody: OwnedCustody.decodeUsda(
|
|
5202
|
+
Uint8List.fromList(prepared.ownedCustodyUsda),
|
|
5203
|
+
),
|
|
5204
|
+
verifiedVia: prepared.verifiedVia,
|
|
5205
|
+
validFromEpoch: prepared.validFromEpoch.toInt(),
|
|
5206
|
+
approvedAt: DateTime.fromMillisecondsSinceEpoch(
|
|
5207
|
+
prepared.approvedAtUnixMillis.toInt(),
|
|
5208
|
+
isUtc: true,
|
|
5209
|
+
).toIso8601String(),
|
|
5210
|
+
ownedKeySubject: prepared.ownedKeySubject,
|
|
5211
|
+
);
|
|
5212
|
+
}
|
|
3654
5213
|
|
|
3655
5214
|
/// Wrap a scope key TO a recipient's public key (committer side): `{hpkeEpk, ct}` base64 ciphertext.
|
|
3656
5215
|
Future<Map<String, Object?>> cryptoWrapKey({
|
|
@@ -3680,94 +5239,54 @@ class NomosBridge {
|
|
|
3680
5239
|
'')
|
|
3681
5240
|
.toString();
|
|
3682
5241
|
|
|
3683
|
-
///
|
|
3684
|
-
///
|
|
3685
|
-
Future<
|
|
3686
|
-
String
|
|
3687
|
-
NomosBbox bbox, [
|
|
5242
|
+
/// Framework-filtered maintained conflicts. The kernel returns authenticated
|
|
5243
|
+
/// USDA; only the generated client knows the composed application field types.
|
|
5244
|
+
Future<Uint8List> nmsPendingConflictsUsda(
|
|
5245
|
+
String? aggregateType, [
|
|
3688
5246
|
String? session,
|
|
3689
|
-
])
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
/// A maintained O(1) count / sum.
|
|
3699
|
-
Future<int> count(
|
|
3700
|
-
String countId, [
|
|
3701
|
-
String group = '',
|
|
3702
|
-
String? session,
|
|
3703
|
-
]) async =>
|
|
3704
|
-
((_asMap(
|
|
3705
|
-
await _call('count', {
|
|
3706
|
-
'countId': countId,
|
|
3707
|
-
'group': group,
|
|
3708
|
-
if (session != null) 'session': session,
|
|
3709
|
-
}),
|
|
3710
|
-
)['count']) as num)
|
|
3711
|
-
.toInt();
|
|
3712
|
-
Future<int> sum(String sumId, [String group = '', String? session]) async =>
|
|
3713
|
-
((_asMap(
|
|
3714
|
-
await _call('sum', {
|
|
3715
|
-
'sumId': sumId,
|
|
3716
|
-
'group': group,
|
|
3717
|
-
if (session != null) 'session': session,
|
|
3718
|
-
}),
|
|
3719
|
-
)['sum']) as num)
|
|
3720
|
-
.toInt();
|
|
3721
|
-
|
|
3722
|
-
/// THE PENDING-CONFLICTS READ (Surfaced merge conflicts — the DX layer): the MAINTAINED,
|
|
3723
|
-
/// workspace-wide "decisions needed" index (reserved id `nomosPendingConflicts`). One row per
|
|
3724
|
-
/// (aggregate, field) whose folded state carries a Surfaced pending set of 2+ observations —
|
|
3725
|
-
/// `{aggregateType, aggregateId, field, observations: [{value, author, occurredAt}]}`.
|
|
3726
|
-
/// DERIVED, NOT DRAINED: a resolution write folding in removes its row by derivation.
|
|
3727
|
-
/// Empty for a Surfaced-free workspace. (The generated typed client wraps these rows as
|
|
3728
|
-
/// `PendingConflictRow` and pairs them with the designated `resolve…` helpers.)
|
|
3729
|
-
Future<List<Map<String, dynamic>>> pendingConflicts([
|
|
3730
|
-
String? session,
|
|
3731
|
-
]) async =>
|
|
3732
|
-
_asRows(
|
|
3733
|
-
await _call(
|
|
3734
|
-
'pendingConflicts', {if (session != null) 'session': session}),
|
|
3735
|
-
);
|
|
3736
|
-
|
|
3737
|
-
/// Framework-filtered pending conflicts for one generated aggregate type. This is the
|
|
3738
|
-
/// primitive generated clients use; application code never filters the heterogeneous queue.
|
|
3739
|
-
Future<List<Map<String, dynamic>>> pendingConflictsFor(
|
|
3740
|
-
String aggregateType, [
|
|
3741
|
-
String? session,
|
|
3742
|
-
]) async =>
|
|
3743
|
-
_asRows(
|
|
3744
|
-
await _call('pendingConflicts', {
|
|
3745
|
-
'aggregateType': aggregateType,
|
|
3746
|
-
if (session != null) 'session': session,
|
|
3747
|
-
}),
|
|
5247
|
+
]) =>
|
|
5248
|
+
_nmsKernelQueryUsda(
|
|
5249
|
+
session: session,
|
|
5250
|
+
authority: _nmsQueryAuthority(null, null, session),
|
|
5251
|
+
query: nomos_protocol.QueryRequest(
|
|
5252
|
+
pendingConflicts: nomos_protocol.PendingConflictsQuery(
|
|
5253
|
+
aggregateType: aggregateType,
|
|
5254
|
+
),
|
|
5255
|
+
),
|
|
3748
5256
|
);
|
|
3749
5257
|
|
|
3750
5258
|
/// Counts-only workspace summary over the maintained conflict index.
|
|
3751
|
-
Future<
|
|
5259
|
+
Future<Uint8List> nmsPendingConflictSummaryUsda([
|
|
3752
5260
|
String? session,
|
|
3753
|
-
])
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
5261
|
+
]) =>
|
|
5262
|
+
_nmsKernelQueryUsda(
|
|
5263
|
+
session: session,
|
|
5264
|
+
authority: _nmsQueryAuthority(null, null, session),
|
|
5265
|
+
query: nomos_protocol.QueryRequest(
|
|
5266
|
+
pendingConflictSummary: nomos_protocol.PendingConflictSummaryQuery(),
|
|
5267
|
+
),
|
|
3758
5268
|
);
|
|
3759
5269
|
|
|
3760
5270
|
/// Push local writes → the session branch → cloud admission, and pull canonical main back.
|
|
3761
|
-
Future<
|
|
5271
|
+
Future<nomos_protocol.SyncWorkspaceResponse> sync({
|
|
3762
5272
|
bool admit = true,
|
|
3763
5273
|
String? session,
|
|
3764
|
-
}) async
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
5274
|
+
}) async {
|
|
5275
|
+
final response = await _invokeRuntimeV2(
|
|
5276
|
+
nomos_protocol.RuntimeRequest(
|
|
5277
|
+
syncWorkspace: nomos_protocol.SyncWorkspaceRequest(
|
|
5278
|
+
session: _runtimeSession(session),
|
|
5279
|
+
admit: admit,
|
|
5280
|
+
),
|
|
5281
|
+
),
|
|
5282
|
+
);
|
|
5283
|
+
if (!response.hasSyncWorkspace()) {
|
|
5284
|
+
throw StateError(
|
|
5285
|
+
'Runtime v2 returned ${response.whichOutcome().name} for sync',
|
|
3770
5286
|
);
|
|
5287
|
+
}
|
|
5288
|
+
return response.syncWorkspace;
|
|
5289
|
+
}
|
|
3771
5290
|
|
|
3772
5291
|
/// PERSIST: a base64 snapshot of the whole local holon (the bare nomos.git incl. un-synced writes + the
|
|
3773
5292
|
/// DLQ). Store it on disk and pass it back as `connect(restoreFrom:)` next launch — the app then boots from
|
|
@@ -3789,23 +5308,46 @@ class NomosBridge {
|
|
|
3789
5308
|
int? fallbackMs,
|
|
3790
5309
|
String? signalId,
|
|
3791
5310
|
}) async {
|
|
3792
|
-
await
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
5311
|
+
final response = await _invokeRuntimeV2(
|
|
5312
|
+
nomos_protocol.RuntimeRequest(
|
|
5313
|
+
startAutoSync: nomos_protocol.StartAutoSyncRequest(
|
|
5314
|
+
session: _runtimeSession(),
|
|
5315
|
+
debounceMillis: debounceMs,
|
|
5316
|
+
fallbackMillis: fallbackMs,
|
|
5317
|
+
signalId: signalId,
|
|
5318
|
+
),
|
|
5319
|
+
),
|
|
5320
|
+
);
|
|
5321
|
+
if (!response.hasAutoSyncStarted()) {
|
|
5322
|
+
throw StateError(
|
|
5323
|
+
'Runtime v2 returned ${response.whichOutcome().name} for autoSync',
|
|
5324
|
+
);
|
|
5325
|
+
}
|
|
3797
5326
|
final workspace = _sessionName;
|
|
3798
5327
|
if (workspace != null) _eventPlane._autoSyncSessions.add(workspace);
|
|
3799
5328
|
}
|
|
3800
5329
|
|
|
3801
5330
|
Future<void> stopAutoSync() async {
|
|
3802
|
-
await
|
|
5331
|
+
final response = await _invokeRuntimeV2(
|
|
5332
|
+
nomos_protocol.RuntimeRequest(
|
|
5333
|
+
stopAutoSync: nomos_protocol.StopAutoSyncRequest(
|
|
5334
|
+
session: _runtimeSession(),
|
|
5335
|
+
),
|
|
5336
|
+
),
|
|
5337
|
+
);
|
|
5338
|
+
if (!response.hasAutoSyncStopped()) {
|
|
5339
|
+
throw StateError(
|
|
5340
|
+
'Runtime v2 returned ${response.whichOutcome().name} for stopAutoSync',
|
|
5341
|
+
);
|
|
5342
|
+
}
|
|
3803
5343
|
final workspace = _sessionName;
|
|
3804
5344
|
if (workspace != null) _eventPlane._autoSyncSessions.remove(workspace);
|
|
3805
5345
|
}
|
|
3806
5346
|
|
|
3807
|
-
///
|
|
3808
|
-
|
|
5347
|
+
/// Generated background-sync telemetry. Each item is a Runtime v2 event
|
|
5348
|
+
/// whose exact arm is either [SyncCompleted] or [LinkChanged].
|
|
5349
|
+
Stream<nomos_protocol.RuntimeEvent> get syncEvents =>
|
|
5350
|
+
_eventPlane._syncEvents.stream;
|
|
3809
5351
|
|
|
3810
5352
|
/// LIVE pack byte-progress — the real loading bar. Each event carries `received`/`total` (the host's
|
|
3811
5353
|
/// actual `Content-Length`, known before the body) + `kind` ('delta'|'full'). Emits only while a `/pack`
|
|
@@ -4185,24 +5727,6 @@ class NomosBridge {
|
|
|
4185
5727
|
principal: principal,
|
|
4186
5728
|
);
|
|
4187
5729
|
|
|
4188
|
-
/// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
|
|
4189
|
-
Stream<List<dynamic>> watchSpatial(
|
|
4190
|
-
String spatialId,
|
|
4191
|
-
NomosBbox bbox, [
|
|
4192
|
-
String? session,
|
|
4193
|
-
]) =>
|
|
4194
|
-
_watchStream(
|
|
4195
|
-
kind: 'spatial',
|
|
4196
|
-
spatialId: spatialId,
|
|
4197
|
-
bbox: bbox,
|
|
4198
|
-
session: session,
|
|
4199
|
-
);
|
|
4200
|
-
|
|
4201
|
-
/// Reactive PENDING-CONFLICTS read: re-emits the maintained Surfaced "decisions needed"
|
|
4202
|
-
/// rows whenever the index changes (derived, not drained — a resolution write clears its row).
|
|
4203
|
-
Stream<List<dynamic>> watchPendingConflicts([String? session]) =>
|
|
4204
|
-
_watchStream(kind: 'pendingConflicts', session: session);
|
|
4205
|
-
|
|
4206
5730
|
/// SHAPE COMPATIBILITY (#72): one-shot verdict for this client's structural [requirement]
|
|
4207
5731
|
/// (its baked `…InterfaceJson`, decoded) against the workspace's ON-LEDGER offer (auto-sourced).
|
|
4208
5732
|
/// `{ ok, missing[], incompatible[] }` — names the exact directive/aggregate/field, not a hash.
|
|
@@ -4340,7 +5864,7 @@ class NomosBridge {
|
|
|
4340
5864
|
// Cancellation is a local lifecycle boundary and must complete even if the runner is slow,
|
|
4341
5865
|
// disconnected, or being disposed. The remote release remains best-effort; bridge/runner disposal
|
|
4342
5866
|
// also drops every outstanding watch, so awaiting this acknowledgement only creates teardown hangs.
|
|
4343
|
-
unawaited(
|
|
5867
|
+
unawaited(_unwatch(subId).catchError((_) => null));
|
|
4344
5868
|
},
|
|
4345
5869
|
);
|
|
4346
5870
|
_eventPlane._compatWatches[subId] = ctrl;
|
|
@@ -4352,8 +5876,6 @@ class NomosBridge {
|
|
|
4352
5876
|
String? queryId,
|
|
4353
5877
|
Map<String, Object?>? params,
|
|
4354
5878
|
String? id,
|
|
4355
|
-
String? spatialId,
|
|
4356
|
-
NomosBbox? bbox,
|
|
4357
5879
|
String? base,
|
|
4358
5880
|
int? limit,
|
|
4359
5881
|
String? session,
|
|
@@ -4373,8 +5895,6 @@ class NomosBridge {
|
|
|
4373
5895
|
if (queryId != null) 'queryId': queryId,
|
|
4374
5896
|
if (params != null) 'params': params,
|
|
4375
5897
|
if (id != null) 'id': id,
|
|
4376
|
-
if (spatialId != null) 'spatialId': spatialId,
|
|
4377
|
-
if (bbox != null) 'bbox': bbox.toJson(),
|
|
4378
5898
|
if (base != null) 'base': base,
|
|
4379
5899
|
if (limit != null) 'limit': limit,
|
|
4380
5900
|
if (principal != null) 'principal': principal,
|
|
@@ -4394,7 +5914,7 @@ class NomosBridge {
|
|
|
4394
5914
|
_eventPlane._watches.remove(subId);
|
|
4395
5915
|
// A caller owns its local subscription; it must never need a healthy transport in order to release
|
|
4396
5916
|
// it. Runner-side cleanup is still requested immediately and is made total by runner disposal.
|
|
4397
|
-
unawaited(
|
|
5917
|
+
unawaited(_unwatch(subId).catchError((_) => null));
|
|
4398
5918
|
},
|
|
4399
5919
|
);
|
|
4400
5920
|
_eventPlane._watches[subId] = ctrl;
|
|
@@ -4407,13 +5927,24 @@ class NomosBridge {
|
|
|
4407
5927
|
/// watches and continuous-sync loops must not survive this client.
|
|
4408
5928
|
Future<void> dispose() async {
|
|
4409
5929
|
try {
|
|
4410
|
-
await
|
|
5930
|
+
final response = await _invokeRuntimeV2(
|
|
5931
|
+
nomos_protocol.RuntimeRequest(
|
|
5932
|
+
closeRealm: nomos_protocol.CloseRealmRequest(),
|
|
5933
|
+
),
|
|
5934
|
+
).timeout(const Duration(seconds: 10));
|
|
5935
|
+
if (!response.hasRealmClosed()) {
|
|
5936
|
+
throw StateError(
|
|
5937
|
+
'Runtime v2 returned ${response.whichOutcome().name} for closeRealm',
|
|
5938
|
+
);
|
|
5939
|
+
}
|
|
4411
5940
|
} catch (_) {
|
|
4412
5941
|
// A dead/unstarted embedding has no reachable runner to release. The
|
|
4413
5942
|
// enclosing engine host is still disposed below by NomosScope; never let
|
|
4414
5943
|
// teardown strand the Flutter lifecycle on an unavailable transport.
|
|
4415
5944
|
}
|
|
4416
5945
|
await _sub.cancel();
|
|
5946
|
+
await _runtimeEventSub?.cancel();
|
|
5947
|
+
_runtimeEventSub = null;
|
|
4417
5948
|
final pending = _pending.entries
|
|
4418
5949
|
.map(
|
|
4419
5950
|
(entry) => (
|
|
@@ -4426,6 +5957,7 @@ class NomosBridge {
|
|
|
4426
5957
|
_pending.clear();
|
|
4427
5958
|
_pendingOps.clear();
|
|
4428
5959
|
_pendingTraces.clear();
|
|
5960
|
+
_pendingCallStacks.clear();
|
|
4429
5961
|
for (final entry in pending) {
|
|
4430
5962
|
final c = entry.completer;
|
|
4431
5963
|
if (!c.isCompleted) {
|
|
@@ -4447,6 +5979,12 @@ class NomosBridge {
|
|
|
4447
5979
|
// let its done event complete if/when that consumer resumes.
|
|
4448
5980
|
if (!w.isClosed) unawaited(w.close());
|
|
4449
5981
|
}
|
|
5982
|
+
final nominalWatches =
|
|
5983
|
+
_nominalWatchControllers.values.toList(growable: false);
|
|
5984
|
+
_nominalWatchControllers.clear();
|
|
5985
|
+
for (final watch in nominalWatches) {
|
|
5986
|
+
if (!watch.isClosed) unawaited(watch.close());
|
|
5987
|
+
}
|
|
4450
5988
|
final compatWatches = _compatWatches.values.toList(growable: false);
|
|
4451
5989
|
_compatWatches.clear();
|
|
4452
5990
|
for (final w in compatWatches) {
|
|
@@ -4581,13 +6119,26 @@ class NomosSession extends NomosBridge {
|
|
|
4581
6119
|
'Generated child model inheritance needs a bound parent session.',
|
|
4582
6120
|
);
|
|
4583
6121
|
}
|
|
4584
|
-
await
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
6122
|
+
final response = await _invokeRuntimeV2(
|
|
6123
|
+
nomos_protocol.RuntimeRequest(
|
|
6124
|
+
modelLifecycle: nomos_protocol.ModelLifecycleRequest(
|
|
6125
|
+
session: childWorkspace,
|
|
6126
|
+
stage: nomos_protocol.StageGeneratedModelRequest(
|
|
6127
|
+
domain: domain,
|
|
6128
|
+
candidateHash: candidateHash,
|
|
6129
|
+
lawUsdz: lawUsdz,
|
|
6130
|
+
sourceSession: source,
|
|
6131
|
+
),
|
|
6132
|
+
),
|
|
6133
|
+
),
|
|
6134
|
+
);
|
|
6135
|
+
if (!response.hasModelLifecycle() || !response.modelLifecycle.accepted) {
|
|
6136
|
+
throw StateError(
|
|
6137
|
+
response.hasModelLifecycle() && response.modelLifecycle.hasMessage()
|
|
6138
|
+
? response.modelLifecycle.message
|
|
6139
|
+
: 'Runtime v2 refused generated child model staging',
|
|
6140
|
+
);
|
|
6141
|
+
}
|
|
4591
6142
|
}
|
|
4592
6143
|
|
|
4593
6144
|
/// Retain the application's typed model-decision policy for generated child
|