@githolon/dsl 0.107.4 → 0.108.1
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 +24 -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 +10634 -6122
- package/dart/lib/src/runner/offline-install.mjs +4187 -2716
- 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 +373 -282
- package/dart/lib/src/webview_bridge.dart +2146 -725
- package/dart/pubspec.lock +17 -1
- package/dart/pubspec.yaml +3 -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 +17 -0
- package/dart_types/lib/nomos_kernel_protocol.dart +11 -0
- 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 +17945 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +933 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pb.dart +245 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbenum.dart +160 -0
- 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.lock +16 -0
- package/dart_types/pubspec.yaml +3 -1
- package/dart_types/test/surfaced_test.dart +2 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +5760 -1879
- package/src/authoring.ts +3 -2
- package/src/birth_meter_ensure.ts +2 -0
- package/src/births.ts +21 -0
- package/src/build_package.ts +30 -0
- package/src/business_birth.ts +41 -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 +392 -46
- package/src/directive.ts +19 -4
- package/src/engine_entry.ts +45 -4
- package/src/framework/application_home.ts +4 -0
- package/src/framework/auth_provider_law.ts +296 -20
- package/src/framework/birth_certificate_law.ts +24 -0
- package/src/framework/home.ts +83 -40
- package/src/framework/owned_custody.ts +25 -12
- package/src/framework/owned_custody_value.ts +51 -0
- package/src/framework/relations.ts +47 -21
- package/src/framework/signing_identity.ts +100 -17
- package/src/framework/workspaces.ts +39 -10
- 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 +22 -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 +44 -42
- package/src/stable_ids.ts +37 -2
- 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
|
@@ -20,7 +20,10 @@ 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;
|
|
26
|
+
import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
|
|
24
27
|
import 'package:nomos_types/nomos_types.dart'
|
|
25
28
|
show
|
|
26
29
|
nomosErrorFromInfo,
|
|
@@ -44,6 +47,14 @@ import 'package:nomos_types/src/foreign_ref.dart'
|
|
|
44
47
|
|
|
45
48
|
import 'session_bound.dart'
|
|
46
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;
|
|
47
58
|
|
|
48
59
|
/// THE ENGINE (architecture/session_bound_clients.md): the multi-workspace RUNTIME — mount/connect,
|
|
49
60
|
/// the session registry, sync, custody + recovery, the home ceremony, and [NomosBridge.follow]
|
|
@@ -66,150 +77,56 @@ final class NomosPage<T> {
|
|
|
66
77
|
final bool done;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
/// Typed authority evidence carried by an offer that plans exactly one child birth.
|
|
70
|
-
///
|
|
71
|
-
///
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
String camel,
|
|
106
|
-
String snake,
|
|
107
|
-
) =>
|
|
108
|
-
(wire[camel] ?? wire[snake] ?? '').toString();
|
|
109
|
-
|
|
110
|
-
static List<int> _bytes(
|
|
111
|
-
Map<String, Object?> wire,
|
|
112
|
-
String camel,
|
|
113
|
-
String snake,
|
|
114
|
-
) =>
|
|
115
|
-
((wire[camel] ?? wire[snake]) as List? ?? const <Object?>[])
|
|
116
|
-
.map((value) => (value as num).toInt())
|
|
117
|
-
.toList(growable: false);
|
|
118
|
-
|
|
119
|
-
factory NomosBirthCertificate.fromWire(
|
|
120
|
-
Map<String, Object?> wire,
|
|
121
|
-
) =>
|
|
122
|
-
NomosBirthCertificate(
|
|
123
|
-
child: _string(wire, 'child', 'child'),
|
|
124
|
-
founder: _string(wire, 'founder', 'founder'),
|
|
125
|
-
verified: wire['verified'] == true,
|
|
126
|
-
parentKey: _bytes(wire, 'parentKey', 'parent_key'),
|
|
127
|
-
parentId: _string(wire, 'parentId', 'parent_id'),
|
|
128
|
-
epoch: (wire['epoch'] as num?)?.toInt() ?? 0,
|
|
129
|
-
signature: _bytes(wire, 'signature', 'signature'),
|
|
130
|
-
verifiedVia: (wire['verifiedVia'] ?? wire['verified_via'])?.toString(),
|
|
131
|
-
idpToken: (wire['idpToken'] ?? wire['idp_token'])?.toString(),
|
|
132
|
-
capturedClock:
|
|
133
|
-
((wire['capturedClock'] ?? wire['captured_clock']) as num?)
|
|
134
|
-
?.toInt(),
|
|
135
|
-
authProviderIssuer:
|
|
136
|
-
(wire['authProviderIssuer'] ?? wire['auth_provider_issuer'])
|
|
137
|
-
?.toString(),
|
|
138
|
-
authProviderJwks:
|
|
139
|
-
(wire['authProviderJwks'] ?? wire['auth_provider_jwks'])
|
|
140
|
-
?.toString(),
|
|
141
|
-
authProviderAudience:
|
|
142
|
-
(wire['authProviderAudience'] ?? wire['auth_provider_audience'])
|
|
143
|
-
?.toString(),
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
Map<String, Object?> toWire() => <String, Object?>{
|
|
147
|
-
'child': child,
|
|
148
|
-
'founder': founder,
|
|
149
|
-
'verified': verified,
|
|
150
|
-
'parentKey': parentKey,
|
|
151
|
-
'parentId': parentId,
|
|
152
|
-
'epoch': epoch,
|
|
153
|
-
'signature': signature,
|
|
154
|
-
if (verifiedVia != null) 'verifiedVia': verifiedVia,
|
|
155
|
-
if (idpToken != null) 'idpToken': idpToken,
|
|
156
|
-
if (capturedClock != null) 'capturedClock': capturedClock,
|
|
157
|
-
if (authProviderIssuer != null)
|
|
158
|
-
'authProviderIssuer': authProviderIssuer,
|
|
159
|
-
if (authProviderJwks != null) 'authProviderJwks': authProviderJwks,
|
|
160
|
-
if (authProviderAudience != null)
|
|
161
|
-
'authProviderAudience': authProviderAudience,
|
|
162
|
-
};
|
|
80
|
+
/// Typed authority evidence carried by an offer that plans exactly one child birth. The protobuf
|
|
81
|
+
/// declaration is the one source of truth for Rust, TypeScript and Dart; USDA is its durable state
|
|
82
|
+
/// projection, not another transport type. The legacy bridge envelope carries only these protobuf bytes.
|
|
83
|
+
typedef NomosBirthCertificate = nomos_protocol.BirthCertificate;
|
|
84
|
+
|
|
85
|
+
/// An unsigned [NomosBirthCertificate]. [NomosBridge.signBirthCert] returns the same generated message
|
|
86
|
+
/// with its signature populated; the signing secret is a separate argument and never enters the value.
|
|
87
|
+
typedef NomosBirthCertificateSpec = nomos_protocol.BirthCertificate;
|
|
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
|
+
}
|
|
163
116
|
}
|
|
164
117
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
this.capturedClock,
|
|
178
|
-
this.authProviderIssuer,
|
|
179
|
-
this.authProviderJwks,
|
|
180
|
-
this.authProviderAudience,
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
final String child;
|
|
184
|
-
final String founder;
|
|
185
|
-
final bool verified;
|
|
186
|
-
final String parentKey;
|
|
187
|
-
final String parentId;
|
|
188
|
-
final int epoch;
|
|
189
|
-
final String? verifiedVia;
|
|
190
|
-
final String? idpToken;
|
|
191
|
-
final int? capturedClock;
|
|
192
|
-
final String? authProviderIssuer;
|
|
193
|
-
final String? authProviderJwks;
|
|
194
|
-
final String? authProviderAudience;
|
|
195
|
-
|
|
196
|
-
Map<String, Object?> toWire() => <String, Object?>{
|
|
197
|
-
'child': child,
|
|
198
|
-
'founder': founder,
|
|
199
|
-
'verified': verified,
|
|
200
|
-
'parentKey': parentKey,
|
|
201
|
-
'parentId': parentId,
|
|
202
|
-
'epoch': epoch,
|
|
203
|
-
if (verifiedVia != null) 'verifiedVia': verifiedVia,
|
|
204
|
-
if (idpToken != null) 'idpToken': idpToken,
|
|
205
|
-
if (capturedClock != null) 'capturedClock': capturedClock,
|
|
206
|
-
if (authProviderIssuer != null)
|
|
207
|
-
'authProviderIssuer': authProviderIssuer,
|
|
208
|
-
if (authProviderJwks != null) 'authProviderJwks': authProviderJwks,
|
|
209
|
-
if (authProviderAudience != null)
|
|
210
|
-
'authProviderAudience': authProviderAudience,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
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
|
+
);
|
|
213
130
|
|
|
214
131
|
/// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
|
|
215
132
|
const int kWebviewBridgeVersion = 1;
|
|
@@ -242,10 +159,10 @@ class StaleRunnerBundle implements Exception {
|
|
|
242
159
|
'NomosBridge(..., allowStaleRunnerBundle: true) for a deliberate mixed-version setup.';
|
|
243
160
|
}
|
|
244
161
|
|
|
245
|
-
/// The
|
|
246
|
-
///
|
|
247
|
-
///
|
|
248
|
-
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 {
|
|
249
166
|
void send(Map<String, Object?> message);
|
|
250
167
|
Stream<Map<String, Object?>> get messages;
|
|
251
168
|
}
|
|
@@ -267,11 +184,17 @@ class TransferProgress {
|
|
|
267
184
|
? null
|
|
268
185
|
: (received / total!).clamp(0.0, 1.0);
|
|
269
186
|
bool get done => total != null && received >= total!;
|
|
270
|
-
factory TransferProgress.
|
|
187
|
+
factory TransferProgress.fromRuntime(
|
|
188
|
+
nomos_protocol.TransferProgress progress,
|
|
189
|
+
) =>
|
|
271
190
|
TransferProgress(
|
|
272
|
-
received:
|
|
273
|
-
total:
|
|
274
|
-
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
|
+
},
|
|
275
198
|
);
|
|
276
199
|
}
|
|
277
200
|
|
|
@@ -350,46 +273,56 @@ final class KernelProgress {
|
|
|
350
273
|
double? get fraction =>
|
|
351
274
|
(total <= 0) ? null : (completed / total).clamp(0.0, 1.0);
|
|
352
275
|
|
|
353
|
-
factory KernelProgress.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
}
|
|
389
324
|
}
|
|
390
325
|
|
|
391
|
-
int? _wireInteger(Object? value) => value is num ? value.toInt() : null;
|
|
392
|
-
|
|
393
326
|
/// One opaque point in this workspace's verified history. Applications use it as the handle passed to a
|
|
394
327
|
/// generated client's `past.at(moment)`; [id] is framework identity, not a business field and need never be
|
|
395
328
|
/// parsed. The remaining fields make a timeline useful without exposing commit or event-sourcing ceremony.
|
|
@@ -522,7 +455,7 @@ class NomosCreated {
|
|
|
522
455
|
/// so the CANONICAL axes are axis-neutral (`[minX, minY] .. [maxX, maxY]`); the axes' meaning
|
|
523
456
|
/// (earth coordinates, floor-plan mm, anything) is the tenant's business. The original geographic
|
|
524
457
|
/// `minLng`/`minLat`/`maxLng`/`maxLat` named params remain accepted forever (era rule) — pass either
|
|
525
|
-
/// shape.
|
|
458
|
+
/// source-level shape. The exact Runtime v2 protobuf boundary carries only the canonical axes.
|
|
526
459
|
class NomosBbox {
|
|
527
460
|
final double minX;
|
|
528
461
|
final double minY;
|
|
@@ -562,18 +495,6 @@ class NomosBbox {
|
|
|
562
495
|
'Spatial axes are generic; use maxX/maxY. Kept for source compat.',
|
|
563
496
|
)
|
|
564
497
|
double get maxLat => maxY;
|
|
565
|
-
|
|
566
|
-
Map<String, Object?> toJson() => {
|
|
567
|
-
'minX': minX,
|
|
568
|
-
'minY': minY,
|
|
569
|
-
'maxX': maxX,
|
|
570
|
-
'maxY': maxY,
|
|
571
|
-
// Geographic aliases so a pre-genericisation runner (which reads minLng/…) still works.
|
|
572
|
-
'minLng': minX,
|
|
573
|
-
'minLat': minY,
|
|
574
|
-
'maxLng': maxX,
|
|
575
|
-
'maxLat': maxY,
|
|
576
|
-
};
|
|
577
498
|
}
|
|
578
499
|
|
|
579
500
|
/// One member's structural compat verdict — `ok`, plus the exact break [reason] when not.
|
|
@@ -661,7 +582,7 @@ class NomosScopeKey {
|
|
|
661
582
|
final String? ownedKeySubject;
|
|
662
583
|
|
|
663
584
|
/// Framework-private encrypted handoff, present only while seating the first device.
|
|
664
|
-
final
|
|
585
|
+
final OwnedCustody? ownedCustody;
|
|
665
586
|
|
|
666
587
|
/// Framework-private deterministic child address, present only for an unborn owned business aggregate.
|
|
667
588
|
final String? workspace;
|
|
@@ -683,14 +604,18 @@ class NomosScopeKey {
|
|
|
683
604
|
};
|
|
684
605
|
}
|
|
685
606
|
|
|
686
|
-
/// The entire app-facing multi-device surface for owned business state. The
|
|
687
|
-
///
|
|
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.
|
|
688
610
|
class NomosDevices {
|
|
689
611
|
final NomosBridge bridge;
|
|
690
612
|
final String domain;
|
|
691
613
|
final String domainHash;
|
|
692
614
|
final String? actor;
|
|
693
615
|
final String scope;
|
|
616
|
+
final Future<NomosOfferOutcome> Function(
|
|
617
|
+
NomosOwnedDeviceApprovalPreparation preparation,
|
|
618
|
+
) approvePrepared;
|
|
694
619
|
|
|
695
620
|
const NomosDevices({
|
|
696
621
|
required this.bridge,
|
|
@@ -698,11 +623,12 @@ class NomosDevices {
|
|
|
698
623
|
required this.domainHash,
|
|
699
624
|
required this.actor,
|
|
700
625
|
required this.scope,
|
|
626
|
+
required this.approvePrepared,
|
|
701
627
|
});
|
|
702
628
|
|
|
703
|
-
Future<
|
|
629
|
+
Future<OwnedDeviceRequest> request() => bridge.nomosOwnedDeviceRequest(scope);
|
|
704
630
|
|
|
705
|
-
Future<NomosOfferOutcome> approve(
|
|
631
|
+
Future<NomosOfferOutcome> approve(OwnedDeviceRequest request) async {
|
|
706
632
|
final principal = actor ?? bridge.principal;
|
|
707
633
|
if (principal == null || principal.trim().isEmpty) {
|
|
708
634
|
throw StateError('devices.approve() needs a signed-in Nomos session');
|
|
@@ -711,13 +637,7 @@ class NomosDevices {
|
|
|
711
637
|
request,
|
|
712
638
|
scope,
|
|
713
639
|
);
|
|
714
|
-
return
|
|
715
|
-
domain: domain,
|
|
716
|
-
directiveId: 'nomos_internal_approve_owned_device_7c1f5e',
|
|
717
|
-
payload: payload,
|
|
718
|
-
actor: principal,
|
|
719
|
-
domainHash: domainHash,
|
|
720
|
-
);
|
|
640
|
+
return approvePrepared(payload);
|
|
721
641
|
}
|
|
722
642
|
}
|
|
723
643
|
|
|
@@ -874,8 +794,8 @@ class EncryptionRosterMismatch implements Exception {
|
|
|
874
794
|
/// Without this, the app would open the child and poll `head:null` until timeout with no reason. Callers catch
|
|
875
795
|
/// it to see the child name(s), the kernel [code], and the [reason]. [head] is the parent write that landed.
|
|
876
796
|
class BirthIncomplete implements Exception {
|
|
877
|
-
/// The
|
|
878
|
-
final List<
|
|
797
|
+
/// The failed typed kernel outcomes, status != [NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN].
|
|
798
|
+
final List<NomosBirthOutcome> failedBirths;
|
|
879
799
|
|
|
880
800
|
/// The parent write's local head — the parent offer itself sealed; only the child did not come up.
|
|
881
801
|
final String head;
|
|
@@ -884,28 +804,25 @@ class BirthIncomplete implements Exception {
|
|
|
884
804
|
|
|
885
805
|
/// The first failed child's kernel code (e.g. `BirthLawPackageUnavailable`), or `BirthFailed`.
|
|
886
806
|
String get code =>
|
|
887
|
-
|
|
888
|
-
? failedBirths.first
|
|
889
|
-
:
|
|
890
|
-
'BirthFailed';
|
|
807
|
+
failedBirths.isNotEmpty && failedBirths.first.code.isNotEmpty
|
|
808
|
+
? failedBirths.first.code
|
|
809
|
+
: 'BirthFailed';
|
|
891
810
|
|
|
892
811
|
/// The child workspace name(s) that did not come up.
|
|
893
|
-
List<String> get workspaces =>
|
|
894
|
-
.map((o) =>
|
|
895
|
-
.where((s) => s.isNotEmpty)
|
|
896
|
-
.toList();
|
|
812
|
+
List<String> get workspaces =>
|
|
813
|
+
failedBirths.map((o) => o.workspace).where((s) => s.isNotEmpty).toList();
|
|
897
814
|
|
|
898
815
|
@override
|
|
899
816
|
String toString() {
|
|
900
817
|
final list = failedBirths
|
|
901
818
|
.map(
|
|
902
819
|
(o) =>
|
|
903
|
-
'${o
|
|
904
|
-
'${o
|
|
820
|
+
'${o.workspace.isEmpty ? '?' : o.workspace} [${o.status.name}${o.code.isNotEmpty ? ': ${o.code}' : ''}]'
|
|
821
|
+
'${o.reason.isNotEmpty ? ' — ${o.reason}' : ''}',
|
|
905
822
|
)
|
|
906
823
|
.join('; ');
|
|
907
824
|
final lawUnavailable = failedBirths.any(
|
|
908
|
-
(o) => o
|
|
825
|
+
(o) => o.code == 'BirthLawPackageUnavailable',
|
|
909
826
|
);
|
|
910
827
|
return 'BirthIncomplete: ${failedBirths.length} child workspace(s) did NOT come up — $list. '
|
|
911
828
|
'The parent offer admitted, but the child was not fully born, so opening it would poll head:null '
|
|
@@ -927,6 +844,254 @@ class NomosEpochResult {
|
|
|
927
844
|
});
|
|
928
845
|
}
|
|
929
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
|
+
|
|
930
1095
|
/// THE OWNER CEREMONY surface — `<Domain>Client(bridge, actor: owner).encryption`. All three verbs open a
|
|
931
1096
|
/// FRESH epoch (a fresh 32-byte secret) and fan the wraps to the resolved roster; the membership change (if
|
|
932
1097
|
/// any) is the domain grant/revoke dispatched first, in the same call.
|
|
@@ -1194,6 +1359,67 @@ class NomosEncryption {
|
|
|
1194
1359
|
/// for a `.births()` directive — the child workspace(s) the offer brought into being on this local plane.
|
|
1195
1360
|
enum NomosOperationPhase { locallyAccepted, admittedToRemoteCustody, converged }
|
|
1196
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
|
+
|
|
1197
1423
|
/// Product-facing progress for one accepted write. Transport topology and custody identifiers remain private.
|
|
1198
1424
|
final class NomosOperationStatus {
|
|
1199
1425
|
const NomosOperationStatus(this.phase, {this.updatedAt});
|
|
@@ -1261,8 +1487,8 @@ class NomosOfferOutcome {
|
|
|
1261
1487
|
/// it IN-PLACE and opens it as a live sibling session — reads/writes flow into the child at once (#42).
|
|
1262
1488
|
final List<String> born;
|
|
1263
1489
|
|
|
1264
|
-
///
|
|
1265
|
-
final List<
|
|
1490
|
+
/// Typed per-child outcomes reported by the kernel, when present.
|
|
1491
|
+
final List<NomosBirthOutcome> birthOutcomes;
|
|
1266
1492
|
|
|
1267
1493
|
/// LEGACY GATEWAY LANE ONLY: the parent gateway's verdict, when this outcome wraps a deprecated
|
|
1268
1494
|
/// cross-workspace `createWorkspace` relay (the old generated-birth shape). Null on the one
|
|
@@ -1310,6 +1536,31 @@ class NomosOfferOutcome {
|
|
|
1310
1536
|
.toList();
|
|
1311
1537
|
}
|
|
1312
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
|
+
|
|
1313
1564
|
/// OPEN A REF'S WORKSPACE DIRECTLY (co2 DX ask) — `await outcome.bornRefs.single.acquire(bridge)`. Sugar over
|
|
1314
1565
|
/// [NomosBridge.acquireSession] keyed by the ref's `workspace`: an already-open session returns as-is; a
|
|
1315
1566
|
/// device-resident/parked/just-born workspace opens LOCAL-ONLY in place (no cloud, no credentials); only a
|
|
@@ -1396,6 +1647,7 @@ class NomosBridge {
|
|
|
1396
1647
|
final Map<int, Completer<Object?>> _pending = {};
|
|
1397
1648
|
final Map<int, String> _pendingOps = {};
|
|
1398
1649
|
final Map<int, NomosTraceContext> _pendingTraces = {};
|
|
1650
|
+
final Map<int, StackTrace> _pendingCallStacks = {};
|
|
1399
1651
|
final Map<String, StreamController<List<dynamic>>> _watches = {};
|
|
1400
1652
|
// SHAPE COMPATIBILITY (#72): compat bindings emit a verdict MAP, not a row list.
|
|
1401
1653
|
final Map<String, StreamController<Map<String, Object?>>> _compatWatches = {};
|
|
@@ -1407,8 +1659,8 @@ class NomosBridge {
|
|
|
1407
1659
|
final Map<String, StreamController<NomosCompat>> _lawChangedBySession = {};
|
|
1408
1660
|
final StreamController<NomosCompat> _lawChanged =
|
|
1409
1661
|
StreamController<NomosCompat>.broadcast();
|
|
1410
|
-
final StreamController<
|
|
1411
|
-
StreamController<
|
|
1662
|
+
final StreamController<nomos_protocol.RuntimeEvent> _syncEvents =
|
|
1663
|
+
StreamController<nomos_protocol.RuntimeEvent>.broadcast();
|
|
1412
1664
|
final StreamController<Map<String, Object?>> _signalEvents =
|
|
1413
1665
|
StreamController<Map<String, Object?>>.broadcast();
|
|
1414
1666
|
// CUSTODY LIFECYCLE (rebirth_and_custody_lifecycle.md): each `custody` event carries the raw map (session +
|
|
@@ -1456,13 +1708,20 @@ class NomosBridge {
|
|
|
1456
1708
|
// workspace is a live session on it. [_sessionNames] tracks what [connect] has opened; [_sessionViews]
|
|
1457
1709
|
// caches the per-workspace [NomosSession] handle so `bridge.session(ws)` is stable + cheap.
|
|
1458
1710
|
final Set<String> _sessionNames = {};
|
|
1711
|
+
final Map<String, String> _workspaceBySession = {};
|
|
1459
1712
|
final Map<String, NomosSession> _sessionViews = {};
|
|
1460
1713
|
// The gate-bound identity retained from each session's auth token. Generated clients consume this so
|
|
1461
1714
|
// application code never has to thread an actor into ordinary business methods.
|
|
1462
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 = {};
|
|
1463
1720
|
// Device signing custody follows the application session internally. A generated application opening a
|
|
1464
1721
|
// locally-born child inherits its current session's device key; tenant code never handles authorSecret.
|
|
1465
1722
|
final Map<String, String> _sessionAuthorSecrets = {};
|
|
1723
|
+
final Map<String, int> _sessionOfferSequences = {};
|
|
1724
|
+
final Map<String, int> _sessionReplicas = {};
|
|
1466
1725
|
// Stateful generated applications retain the refreshable cloud access that opened their primary
|
|
1467
1726
|
// session. A generated read following a NomosRef can therefore mount a genuinely-absent workspace
|
|
1468
1727
|
// without asking tenant code for cloud URLs, tokens, sessions or signing keys. Kept per source
|
|
@@ -1487,12 +1746,165 @@ class NomosBridge {
|
|
|
1487
1746
|
// methods expose only the typed receipt and its product-language lifecycle.
|
|
1488
1747
|
final Map<String, Map<String, NomosOperationReceipt>> _operationReceipts = {};
|
|
1489
1748
|
final Map<String, NomosOperationReceipt> _birthReceipts = {};
|
|
1749
|
+
final Map<String, StreamController<Uint8List>> _nominalWatchControllers = {};
|
|
1490
1750
|
int _seq = 0;
|
|
1751
|
+
late final NmsRuntimeV2Gateway _runtimeV2 = NmsRuntimeV2Gateway(_transport);
|
|
1491
1752
|
final String _traceId = _newTraceId();
|
|
1492
1753
|
late final StreamSubscription<Map<String, Object?>> _sub;
|
|
1754
|
+
StreamSubscription<nomos_protocol.RuntimeEvent>? _runtimeEventSub;
|
|
1493
1755
|
|
|
1494
1756
|
NomosBridge(this._transport, {this.allowStaleRunnerBundle = false}) {
|
|
1495
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
|
+
}
|
|
1496
1908
|
}
|
|
1497
1909
|
|
|
1498
1910
|
void _onTransportError(Object error, StackTrace stackTrace) {
|
|
@@ -1501,6 +1913,7 @@ class NomosBridge {
|
|
|
1501
1913
|
_pending.clear();
|
|
1502
1914
|
_pendingOps.clear();
|
|
1503
1915
|
_pendingTraces.clear();
|
|
1916
|
+
_pendingCallStacks.clear();
|
|
1504
1917
|
for (final completer in pending) {
|
|
1505
1918
|
if (!completer.isCompleted) completer.completeError(error, stackTrace);
|
|
1506
1919
|
}
|
|
@@ -1643,114 +2056,6 @@ class NomosBridge {
|
|
|
1643
2056
|
}
|
|
1644
2057
|
return;
|
|
1645
2058
|
}
|
|
1646
|
-
if (event == 'sync' || event == 'link') {
|
|
1647
|
-
// autoSync telemetry: each round { event:'sync', reason, result } and socket state { event:'link', status }.
|
|
1648
|
-
if (!_syncEvents.isClosed) _syncEvents.add(m);
|
|
1649
|
-
if (event == 'sync') {
|
|
1650
|
-
final result = (m['result'] as Map?)?.cast<String, Object?>();
|
|
1651
|
-
final session = m['session']?.toString();
|
|
1652
|
-
if (session != null && result != null) {
|
|
1653
|
-
_advanceOperationReceipts(session, result);
|
|
1654
|
-
if (_syncNeedsCredentialRefresh(m, result)) {
|
|
1655
|
-
unawaited(_refreshSessionCredential(session));
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
if (m['ok'] == false) {
|
|
1659
|
-
// A rejected/failed round is not a successful sync timestamp. Keep the stable typed envelope on
|
|
1660
|
-
// telemetry; the app-ready status needs only one legible error and an honest error phase.
|
|
1661
|
-
final detail = m['error']?.toString() ?? 'automatic sync failed';
|
|
1662
|
-
_updateScopedStatus(
|
|
1663
|
-
session,
|
|
1664
|
-
(current) => current.copyWith(
|
|
1665
|
-
phase: SyncPhase.error,
|
|
1666
|
-
clearTransfer: true,
|
|
1667
|
-
lastError: detail,
|
|
1668
|
-
),
|
|
1669
|
-
);
|
|
1670
|
-
} else if (result?['offline'] == true) {
|
|
1671
|
-
// Offline is expected transport debt. The durable local branch remains ahead and autoSync owns the
|
|
1672
|
-
// retry; do not claim a completed sync or overwrite the previous successful timestamp.
|
|
1673
|
-
_updateScopedStatus(
|
|
1674
|
-
session,
|
|
1675
|
-
(current) => current.copyWith(
|
|
1676
|
-
phase: SyncPhase.offline,
|
|
1677
|
-
clearTransfer: true,
|
|
1678
|
-
lastError: (result?['pushError'] as Map?)?['error']?.toString(),
|
|
1679
|
-
),
|
|
1680
|
-
);
|
|
1681
|
-
} else if (result?['pushFailed'] == true) {
|
|
1682
|
-
// The sync round itself completed, but shared custody refused the
|
|
1683
|
-
// push. `stillAhead` is durable debt, not success: surface the
|
|
1684
|
-
// cloud's typed refusal on the owning workspace so generated
|
|
1685
|
-
// `whenCurrent()` calls fail with the real cause instead of timing
|
|
1686
|
-
// out behind a generic saving state.
|
|
1687
|
-
final detail = _syncPushFailureDetail(result!);
|
|
1688
|
-
_updateScopedStatus(
|
|
1689
|
-
session,
|
|
1690
|
-
(current) => current.copyWith(
|
|
1691
|
-
phase: SyncPhase.error,
|
|
1692
|
-
clearTransfer: true,
|
|
1693
|
-
lastError: detail,
|
|
1694
|
-
),
|
|
1695
|
-
);
|
|
1696
|
-
} else {
|
|
1697
|
-
// A completed round → idle (the next metrics poll folds in residual ahead/behind), stamp the time.
|
|
1698
|
-
_updateScopedStatus(
|
|
1699
|
-
session,
|
|
1700
|
-
(current) => current.copyWith(
|
|
1701
|
-
phase: SyncPhase.idle,
|
|
1702
|
-
clearTransfer: true,
|
|
1703
|
-
clearError: true,
|
|
1704
|
-
lastSyncedAt: DateTime.now(),
|
|
1705
|
-
),
|
|
1706
|
-
);
|
|
1707
|
-
}
|
|
1708
|
-
} else if (m['status'] == 'closed' || m['status'] == 'offline') {
|
|
1709
|
-
_updateScopedStatus(
|
|
1710
|
-
m['session']?.toString(),
|
|
1711
|
-
(current) => current.copyWith(phase: SyncPhase.offline),
|
|
1712
|
-
);
|
|
1713
|
-
}
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
|
-
if (event == 'custody') {
|
|
1717
|
-
// Custody lifecycle: { event:'custody', session, custody:{ kind, break, … } }. Decode + fan out so the
|
|
1718
|
-
// app can react (kind "ask" → show a dialog then call resolveCustodyBreak; others are informational).
|
|
1719
|
-
final ev = NomosCustodyEvent.fromMessage(m);
|
|
1720
|
-
if (ev != null && !_custodyEvents.isClosed) _custodyEvents.add(ev);
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
if (event == 'localWorkRejected') {
|
|
1724
|
-
// LOCAL-WORK-REJECTED: { event:'localWorkRejected', session, localWorkRejected:{ workspace,
|
|
1725
|
-
// refused, parked, resetTo, retryable, source } }. The framework already parked the refused
|
|
1726
|
-
// intents + replayed survivors; the app's only job is the honest message (retryable ⇒ "queued,
|
|
1727
|
-
// will retry"; terminal ⇒ "preserved for review — contact support").
|
|
1728
|
-
final ev = NomosLocalWorkRejectedEvent.fromMessage(m);
|
|
1729
|
-
if (ev != null && !_localWorkRejectedEvents.isClosed) {
|
|
1730
|
-
_localWorkRejectedEvents.add(ev);
|
|
1731
|
-
}
|
|
1732
|
-
return;
|
|
1733
|
-
}
|
|
1734
|
-
if (event == 'transfer') {
|
|
1735
|
-
// LIVE pack byte-progress { received, total, kind } — the real loading bar (host Content-Length).
|
|
1736
|
-
final tp = TransferProgress.fromMessage(m);
|
|
1737
|
-
if (!_transferEvents.isClosed) _transferEvents.add(tp);
|
|
1738
|
-
_updateScopedStatus(
|
|
1739
|
-
m['session']?.toString(),
|
|
1740
|
-
(current) => current.copyWith(
|
|
1741
|
-
phase: tp.done ? SyncPhase.syncing : SyncPhase.transferring,
|
|
1742
|
-
transfer: tp,
|
|
1743
|
-
clearTransfer: tp.done,
|
|
1744
|
-
),
|
|
1745
|
-
);
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
1748
|
-
if (event == 'kernelProgress') {
|
|
1749
|
-
if (!_kernelProgressEvents.isClosed) {
|
|
1750
|
-
_kernelProgressEvents.add(KernelProgress.fromMessage(m));
|
|
1751
|
-
}
|
|
1752
|
-
return;
|
|
1753
|
-
}
|
|
1754
2059
|
if (event == 'trace') {
|
|
1755
2060
|
// the realtime cloud flamepath: { event:'trace', trace:{at, oid, edge, clients} } — one per admission.
|
|
1756
2061
|
if (!_flameEvents.isClosed) _flameEvents.add(m);
|
|
@@ -1782,6 +2087,7 @@ class NomosBridge {
|
|
|
1782
2087
|
final completer = _pending.remove(id);
|
|
1783
2088
|
final op = _pendingOps.remove(id);
|
|
1784
2089
|
final trace = _pendingTraces.remove(id) ?? const NomosTraceContext();
|
|
2090
|
+
final callStack = _pendingCallStacks.remove(id) ?? StackTrace.current;
|
|
1785
2091
|
if (completer == null) return;
|
|
1786
2092
|
if (m['ok'] == true) {
|
|
1787
2093
|
completer.complete(m['result']);
|
|
@@ -1791,14 +2097,15 @@ class NomosBridge {
|
|
|
1791
2097
|
// [BirthIncomplete] here (apps switch on the type + entries, never scrape the prose).
|
|
1792
2098
|
final failedBirths = ((m['errorInfo'] as Map?)?['failedBirths'] as List?)
|
|
1793
2099
|
?.whereType<Map>()
|
|
1794
|
-
.map(
|
|
1795
|
-
.toList();
|
|
2100
|
+
.map(_birthOutcomeFromLegacyWire)
|
|
2101
|
+
.toList(growable: false);
|
|
1796
2102
|
if (failedBirths != null && failedBirths.isNotEmpty) {
|
|
1797
2103
|
completer.completeError(
|
|
1798
2104
|
BirthIncomplete(
|
|
1799
2105
|
failedBirths,
|
|
1800
2106
|
head: ((m['errorInfo'] as Map?)?['head'] ?? '').toString(),
|
|
1801
2107
|
),
|
|
2108
|
+
callStack,
|
|
1802
2109
|
);
|
|
1803
2110
|
return;
|
|
1804
2111
|
}
|
|
@@ -1808,7 +2115,7 @@ class NomosBridge {
|
|
|
1808
2115
|
fallbackTrace: trace,
|
|
1809
2116
|
fallbackMessage: 'the githolon refused the request',
|
|
1810
2117
|
);
|
|
1811
|
-
completer.completeError(nomosErrorFromInfo(info));
|
|
2118
|
+
completer.completeError(nomosErrorFromInfo(info), callStack);
|
|
1812
2119
|
}
|
|
1813
2120
|
}
|
|
1814
2121
|
|
|
@@ -1826,6 +2133,7 @@ class NomosBridge {
|
|
|
1826
2133
|
sampled: true,
|
|
1827
2134
|
);
|
|
1828
2135
|
_pendingTraces[id] = trace;
|
|
2136
|
+
_pendingCallStacks[id] = StackTrace.current;
|
|
1829
2137
|
try {
|
|
1830
2138
|
_transport.send({
|
|
1831
2139
|
'v': kWebviewBridgeVersion,
|
|
@@ -1845,11 +2153,49 @@ class NomosBridge {
|
|
|
1845
2153
|
_pending.remove(id);
|
|
1846
2154
|
_pendingOps.remove(id);
|
|
1847
2155
|
_pendingTraces.remove(id);
|
|
2156
|
+
_pendingCallStacks.remove(id);
|
|
1848
2157
|
completer.completeError(error, stackTrace);
|
|
1849
2158
|
}
|
|
1850
2159
|
return completer.future;
|
|
1851
2160
|
}
|
|
1852
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
|
+
|
|
1853
2199
|
Map<String, Object?> _asMap(Object? v) =>
|
|
1854
2200
|
(v as Map?)?.cast<String, Object?>() ?? const {};
|
|
1855
2201
|
List<Map<String, dynamic>> _asRows(Object? v) =>
|
|
@@ -1857,23 +2203,6 @@ class NomosBridge {
|
|
|
1857
2203
|
.map((e) => (e as Map).cast<String, dynamic>())
|
|
1858
2204
|
.toList();
|
|
1859
2205
|
|
|
1860
|
-
String? _principalFromToken(String? token) {
|
|
1861
|
-
if (token == null || token.isEmpty) return null;
|
|
1862
|
-
try {
|
|
1863
|
-
final parts = token.split('.');
|
|
1864
|
-
if (parts.length < 2) return null;
|
|
1865
|
-
final payload = jsonDecode(
|
|
1866
|
-
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
|
|
1867
|
-
);
|
|
1868
|
-
final sub = payload is Map ? payload['sub'] : null;
|
|
1869
|
-
if (sub is! String || sub.trim().isEmpty) return null;
|
|
1870
|
-
final value = sub.trim();
|
|
1871
|
-
return 'user:$value';
|
|
1872
|
-
} catch (_) {
|
|
1873
|
-
return null;
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
2206
|
NomosOperationReceipt _registerOperationReceipt(
|
|
1878
2207
|
String workspace,
|
|
1879
2208
|
String intentId,
|
|
@@ -1905,19 +2234,19 @@ class NomosBridge {
|
|
|
1905
2234
|
|
|
1906
2235
|
void _advanceOperationReceipts(
|
|
1907
2236
|
String workspace,
|
|
1908
|
-
|
|
2237
|
+
nomos_protocol.SyncWorkspaceResponse result,
|
|
1909
2238
|
) {
|
|
1910
2239
|
final pending = _eventPlane._operationReceipts[workspace];
|
|
1911
2240
|
if (pending == null || pending.isEmpty) return;
|
|
1912
2241
|
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
for (final intentId in ids('admittedIntentIds')) {
|
|
2242
|
+
for (final intentId in result.admittedIntentIds.where(
|
|
2243
|
+
(value) => value.isNotEmpty,
|
|
2244
|
+
)) {
|
|
1918
2245
|
pending[intentId]?._advance(NomosOperationPhase.admittedToRemoteCustody);
|
|
1919
2246
|
}
|
|
1920
|
-
for (final intentId in
|
|
2247
|
+
for (final intentId in result.convergedIntentIds.where(
|
|
2248
|
+
(value) => value.isNotEmpty,
|
|
2249
|
+
)) {
|
|
1921
2250
|
final receipt = pending.remove(intentId);
|
|
1922
2251
|
receipt?._advance(NomosOperationPhase.converged);
|
|
1923
2252
|
}
|
|
@@ -1925,24 +2254,24 @@ class NomosBridge {
|
|
|
1925
2254
|
}
|
|
1926
2255
|
|
|
1927
2256
|
bool _syncNeedsCredentialRefresh(
|
|
1928
|
-
|
|
1929
|
-
Map<String, Object?> result,
|
|
2257
|
+
nomos_protocol.SyncCompleted event,
|
|
1930
2258
|
) {
|
|
1931
|
-
final
|
|
1932
|
-
|
|
2259
|
+
final status =
|
|
2260
|
+
event.result.hasPushError() ? event.result.pushError.status : 0;
|
|
1933
2261
|
if (status == 401 || status == 403) return true;
|
|
1934
|
-
final text =
|
|
1935
|
-
|
|
2262
|
+
final text = '${event.hasFailure() ? event.failure.message : ''} '
|
|
2263
|
+
'${event.result.hasPushError() ? event.result.pushError.message : ''}'
|
|
2264
|
+
.toLowerCase();
|
|
1936
2265
|
return text.contains('unauthenticated') ||
|
|
1937
2266
|
text.contains('token expired') ||
|
|
1938
2267
|
text.contains('invalid token');
|
|
1939
2268
|
}
|
|
1940
2269
|
|
|
1941
|
-
String _syncPushFailureDetail(
|
|
1942
|
-
final push = (result
|
|
1943
|
-
final status = push
|
|
1944
|
-
final reason = push
|
|
1945
|
-
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
|
|
1946
2275
|
? 'Shared custody refused this workspace change'
|
|
1947
2276
|
: 'Shared custody refused this workspace change (HTTP $status)';
|
|
1948
2277
|
return reason == null || reason.toString().trim().isEmpty
|
|
@@ -2023,19 +2352,18 @@ class NomosBridge {
|
|
|
2023
2352
|
/// call `bridge.acquireSession(ws)` / `ref.acquire(bridge)` before using it;
|
|
2024
2353
|
/// • `"parked"` — custody resident but the holon is LRU-disposed; it re-mounts on demand (acquire).
|
|
2025
2354
|
/// `syncPosture` is `"local-only"` (no cloud counterpart) or `"cloud-bound"`.
|
|
2026
|
-
Future<List<
|
|
2027
|
-
final
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
}).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;
|
|
2039
2367
|
}
|
|
2040
2368
|
|
|
2041
2369
|
/// Framework-private plane persistence primitive. It snapshots exact local custody even when [workspace]
|
|
@@ -2109,6 +2437,7 @@ class NomosBridge {
|
|
|
2109
2437
|
_eventPlane._sessionNames.add(
|
|
2110
2438
|
ws,
|
|
2111
2439
|
); // the open succeeded — the session is real now
|
|
2440
|
+
_eventPlane._workspaceBySession[ws] = workspace;
|
|
2112
2441
|
// Opening succeeded, but scoped metrics have not yet proved that this
|
|
2113
2442
|
// workspace is current. Unknown readiness must fail closed: otherwise a
|
|
2114
2443
|
// generated `whenCurrent()` can observe the default idle value and return
|
|
@@ -2118,9 +2447,13 @@ class NomosBridge {
|
|
|
2118
2447
|
() => const SyncStatus(phase: SyncPhase.syncing),
|
|
2119
2448
|
);
|
|
2120
2449
|
_eventPlane._foldApplicationStatusFromSessions();
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
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
|
+
}
|
|
2124
2457
|
if (authorSecret != null && authorSecret.isNotEmpty) {
|
|
2125
2458
|
_eventPlane._sessionAuthorSecrets[ws] = authorSecret;
|
|
2126
2459
|
}
|
|
@@ -2158,16 +2491,34 @@ class NomosBridge {
|
|
|
2158
2491
|
);
|
|
2159
2492
|
|
|
2160
2493
|
/// Continue an `ask`-policy custody break with the user's decision (adopt|reject) — the async UX lane.
|
|
2161
|
-
Future<
|
|
2162
|
-
|
|
2494
|
+
Future<nomos_protocol.CustodyBreakResolved> resolveCustodyBreak(
|
|
2495
|
+
nomos_protocol.CustodyBreakDecision decision, {
|
|
2163
2496
|
String? session,
|
|
2164
|
-
}) async
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
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',
|
|
2505
|
+
);
|
|
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',
|
|
2170
2518
|
);
|
|
2519
|
+
}
|
|
2520
|
+
return response.custodyBreakResolved;
|
|
2521
|
+
}
|
|
2171
2522
|
|
|
2172
2523
|
/// The parked salvaged intents (re-offerable local work an `adopt` rescued) — metadata only.
|
|
2173
2524
|
Future<List<Map<String, Object?>>> salvagedIntents({String? session}) async {
|
|
@@ -2210,42 +2561,68 @@ class NomosBridge {
|
|
|
2210
2561
|
// parks the FULL intent durably in the workspace tree (it rides export()/restore); the app
|
|
2211
2562
|
// inspects, the domain dev ships a law fix, retry unjams. ──
|
|
2212
2563
|
|
|
2213
|
-
/// The parked dead letters
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
final
|
|
2217
|
-
|
|
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
|
+
),
|
|
2218
2574
|
);
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
.
|
|
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);
|
|
2222
2581
|
}
|
|
2223
2582
|
|
|
2224
2583
|
/// Retry one dead letter (after the law fix landed): pulls canon first (it may already be
|
|
2225
2584
|
/// admitted), else re-offers the parked bytes locally. `{ok, head?|alreadyOnMain?|error?}` —
|
|
2226
2585
|
/// a refusal leaves the entry parked (with the fresh error), never lost.
|
|
2227
|
-
Future<
|
|
2586
|
+
Future<nomos_protocol.DeadLetterRetry> retryDeadLetter(
|
|
2228
2587
|
String id, {
|
|
2229
2588
|
String? session,
|
|
2230
|
-
}) async
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
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',
|
|
2236
2601
|
);
|
|
2602
|
+
}
|
|
2603
|
+
return response.deadLetter.retry;
|
|
2604
|
+
}
|
|
2237
2605
|
|
|
2238
2606
|
/// Discard one dead letter — the app's EXPLICIT choice to drop the work. `{ok}`.
|
|
2239
|
-
Future<
|
|
2607
|
+
Future<nomos_protocol.DeadLetterDiscard> discardDeadLetter(
|
|
2240
2608
|
String id, {
|
|
2241
2609
|
String? session,
|
|
2242
|
-
}) async
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
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',
|
|
2248
2622
|
);
|
|
2623
|
+
}
|
|
2624
|
+
return response.deadLetter.discard;
|
|
2625
|
+
}
|
|
2249
2626
|
|
|
2250
2627
|
/// SALVAGE — best-effort extraction of this holon's local-only authored work (un-synced pending
|
|
2251
2628
|
/// writes + the DLQ), each with its full `changeB64` bytes: `{pendingWrites, deadLetters}`. The
|
|
@@ -2327,7 +2704,8 @@ class NomosBridge {
|
|
|
2327
2704
|
if (session != null) 'session': session,
|
|
2328
2705
|
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
2329
2706
|
if (birthCertificate != null)
|
|
2330
|
-
'
|
|
2707
|
+
'birthCertificateProtobuf':
|
|
2708
|
+
base64Encode(birthCertificate.writeToBuffer()),
|
|
2331
2709
|
}),
|
|
2332
2710
|
);
|
|
2333
2711
|
return r['head'].toString();
|
|
@@ -2367,6 +2745,164 @@ class NomosBridge {
|
|
|
2367
2745
|
birthCertificate: birthCertificate,
|
|
2368
2746
|
);
|
|
2369
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
|
+
|
|
2370
2906
|
/// THE ONE OUTCOME-RETURNING OFFER LANE — [offerDirective] answering the full typed [NomosOfferOutcome]
|
|
2371
2907
|
/// (head + law-minted `created` NomosRefs + births), for EVERY generated method (create or mutate). A pure
|
|
2372
2908
|
/// mutation returns `created: []`; a `.creates(...)` plan returns the minted aggregate id(s), each already
|
|
@@ -2408,15 +2944,19 @@ class NomosBridge {
|
|
|
2408
2944
|
if (session != null) 'session': session,
|
|
2409
2945
|
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
2410
2946
|
if (birthCertificate != null)
|
|
2411
|
-
'
|
|
2947
|
+
'birthCertificateProtobuf':
|
|
2948
|
+
base64Encode(birthCertificate.writeToBuffer()),
|
|
2412
2949
|
}),
|
|
2413
2950
|
);
|
|
2414
|
-
} catch (e) {
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2951
|
+
} catch (e, stack) {
|
|
2952
|
+
Error.throwWithStackTrace(
|
|
2953
|
+
await _wrapDomainNotInstalled(
|
|
2954
|
+
e,
|
|
2955
|
+
domain: domain,
|
|
2956
|
+
domainHash: domainHash,
|
|
2957
|
+
requestedWorkspace: session,
|
|
2958
|
+
),
|
|
2959
|
+
stack,
|
|
2420
2960
|
);
|
|
2421
2961
|
}
|
|
2422
2962
|
final t1 = DateTime.now().millisecondsSinceEpoch;
|
|
@@ -2447,15 +2987,18 @@ class NomosBridge {
|
|
|
2447
2987
|
}
|
|
2448
2988
|
final births = ((r['birthOutcomes'] as List?) ?? const [])
|
|
2449
2989
|
.whereType<Map>()
|
|
2450
|
-
.map(
|
|
2451
|
-
.toList();
|
|
2990
|
+
.map(_birthOutcomeFromLegacyWire)
|
|
2991
|
+
.toList(growable: false);
|
|
2452
2992
|
// FAIL FAST on a birth that did NOT come up. The JS holon (index.mjs) already throws on this, so we
|
|
2453
2993
|
// normally never see it here — but a runner/bundle skew (an older runner that passes birthOutcomes
|
|
2454
2994
|
// through without throwing) must NOT let a silent partial birth reach the app to strand on head:null.
|
|
2455
2995
|
// A `status` other than "born" is a failure; throw the typed [BirthIncomplete].
|
|
2456
2996
|
final failedBirths = births
|
|
2457
|
-
.where(
|
|
2458
|
-
|
|
2997
|
+
.where(
|
|
2998
|
+
(birth) =>
|
|
2999
|
+
birth.status != NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
|
|
3000
|
+
)
|
|
3001
|
+
.toList(growable: false);
|
|
2459
3002
|
if (failedBirths.isNotEmpty) {
|
|
2460
3003
|
throw BirthIncomplete(failedBirths, head: (r['head'] ?? '').toString());
|
|
2461
3004
|
}
|
|
@@ -2468,9 +3011,7 @@ class NomosBridge {
|
|
|
2468
3011
|
(e) => (e is Map ? (e['workspace'] ?? '') : e).toString(),
|
|
2469
3012
|
)
|
|
2470
3013
|
else
|
|
2471
|
-
...births
|
|
2472
|
-
.map((m) => (m['workspace'] ?? '').toString())
|
|
2473
|
-
.where((s) => s.isNotEmpty),
|
|
3014
|
+
...births.map((birth) => birth.workspace).where((s) => s.isNotEmpty),
|
|
2474
3015
|
]..removeWhere((s) => s.isEmpty);
|
|
2475
3016
|
final birthPrincipal = actor ?? principal;
|
|
2476
3017
|
if (birthPrincipal != null && birthPrincipal.isNotEmpty) {
|
|
@@ -2579,11 +3120,21 @@ class NomosBridge {
|
|
|
2579
3120
|
String? directiveId,
|
|
2580
3121
|
required String packageHash,
|
|
2581
3122
|
}) async {
|
|
2582
|
-
await
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
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
|
+
}
|
|
2587
3138
|
}
|
|
2588
3139
|
|
|
2589
3140
|
/// FOLLOW A [NomosRef] — THE cross-workspace primitive (session_bound_clients.md ask 8): mount
|
|
@@ -2748,7 +3299,7 @@ class NomosBridge {
|
|
|
2748
3299
|
var resident = false;
|
|
2749
3300
|
try {
|
|
2750
3301
|
resident = (await listWorkspaces()).any(
|
|
2751
|
-
(
|
|
3302
|
+
(candidate) => candidate.workspace == workspace,
|
|
2752
3303
|
);
|
|
2753
3304
|
} catch (_) {
|
|
2754
3305
|
// a pre-connect plane has no registry yet — fall through to the credentials decision below.
|
|
@@ -2851,6 +3402,13 @@ class NomosBridge {
|
|
|
2851
3402
|
// together. Otherwise home signs correctly after first-device enrolment
|
|
2852
3403
|
// while a later catalogue/estate opens with the old empty default key.
|
|
2853
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
|
+
}
|
|
2854
3412
|
if (session != null && authorSecret != null) {
|
|
2855
3413
|
if (authorSecret.isEmpty) {
|
|
2856
3414
|
_eventPlane._sessionAuthorSecrets.remove(session);
|
|
@@ -2969,12 +3527,15 @@ class NomosBridge {
|
|
|
2969
3527
|
if (authToken != null) 'authToken': authToken,
|
|
2970
3528
|
}),
|
|
2971
3529
|
);
|
|
2972
|
-
} catch (e) {
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
3530
|
+
} catch (e, stack) {
|
|
3531
|
+
Error.throwWithStackTrace(
|
|
3532
|
+
await _wrapDomainNotInstalled(
|
|
3533
|
+
e,
|
|
3534
|
+
domain: domain,
|
|
3535
|
+
domainHash: domainHash,
|
|
3536
|
+
requestedWorkspace: parent,
|
|
3537
|
+
),
|
|
3538
|
+
stack,
|
|
2978
3539
|
);
|
|
2979
3540
|
}
|
|
2980
3541
|
final verdict = (r['verdict'] as Map?)?.cast<String, Object?>() ?? r;
|
|
@@ -3082,20 +3643,30 @@ class NomosBridge {
|
|
|
3082
3643
|
/// Framework-private exact-history adoption seam. Generated clients expose this as
|
|
3083
3644
|
/// `model.adopt()` and, only when prior business outcomes differ, a chained
|
|
3084
3645
|
/// `decision.accept(reason)`. Candidate identities, heads and receipts stay here.
|
|
3085
|
-
Future<
|
|
3646
|
+
Future<nomos_protocol.ModelLifecycleResponse>
|
|
3647
|
+
nomosFrameworkAdoptGeneratedModel({
|
|
3086
3648
|
required String domain,
|
|
3087
3649
|
required String candidateHash,
|
|
3088
3650
|
String? because,
|
|
3089
3651
|
}) async {
|
|
3090
|
-
final
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
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
|
+
),
|
|
3096
3663
|
);
|
|
3097
|
-
|
|
3098
|
-
|
|
3664
|
+
if (!response.hasModelLifecycle()) {
|
|
3665
|
+
throw StateError(
|
|
3666
|
+
'Runtime v2 returned ${response.whichOutcome().name} for model adoption',
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3669
|
+
return response.modelLifecycle;
|
|
3099
3670
|
}
|
|
3100
3671
|
|
|
3101
3672
|
/// Framework-private generated-model staging seam. The generated client owns
|
|
@@ -3106,30 +3677,54 @@ class NomosBridge {
|
|
|
3106
3677
|
required String candidateHash,
|
|
3107
3678
|
required List<int> lawUsdz,
|
|
3108
3679
|
}) async {
|
|
3109
|
-
await
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
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
|
+
}
|
|
3114
3699
|
}
|
|
3115
3700
|
|
|
3116
3701
|
/// Framework-private exact-history restoration seam. Generated clients expose
|
|
3117
3702
|
/// `model.restorePrevious()` and a chained `decision.accept(reason)`; predecessor
|
|
3118
3703
|
/// identity, derived dispositions, heads and receipts stay below this bridge.
|
|
3119
|
-
Future<
|
|
3704
|
+
Future<nomos_protocol.ModelLifecycleResponse>
|
|
3705
|
+
nomosFrameworkRestorePreviousGeneratedModel({
|
|
3120
3706
|
required String domain,
|
|
3121
3707
|
required String currentHash,
|
|
3122
3708
|
String? because,
|
|
3123
3709
|
}) async {
|
|
3124
|
-
final
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
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
|
+
),
|
|
3130
3721
|
);
|
|
3131
|
-
|
|
3132
|
-
|
|
3722
|
+
if (!response.hasModelLifecycle()) {
|
|
3723
|
+
throw StateError(
|
|
3724
|
+
'Runtime v2 returned ${response.whichOutcome().name} for model restoration',
|
|
3725
|
+
);
|
|
3726
|
+
}
|
|
3727
|
+
return response.modelLifecycle;
|
|
3133
3728
|
}
|
|
3134
3729
|
|
|
3135
3730
|
/// HISTORY of one aggregate (a todo) — the full action timeline from the ledger: every intent that touched
|
|
@@ -3202,23 +3797,23 @@ class NomosBridge {
|
|
|
3202
3797
|
_asMap(await _call('mintDeviceKey'));
|
|
3203
3798
|
|
|
3204
3799
|
/// VA ROOT-OF-TRUST: sign a BirthCert through the kernel `cert_sign` op (the SINGLE source of the cert
|
|
3205
|
-
/// byte-layout — no Dart copy).
|
|
3206
|
-
///
|
|
3207
|
-
///
|
|
3800
|
+
/// byte-layout — no Dart copy). [spec] is the generated certificate message with an empty signature;
|
|
3801
|
+
/// `deviceSecret` is the base64 secret half, handed to the LOCAL kernel only (it never reaches a server).
|
|
3802
|
+
/// Returns the same generated type with its signature populated, suitable for an offer.
|
|
3208
3803
|
Future<NomosBirthCertificate> signBirthCert(
|
|
3209
3804
|
NomosBirthCertificateSpec spec,
|
|
3210
3805
|
String deviceSecret,
|
|
3211
|
-
) async
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3806
|
+
) async {
|
|
3807
|
+
final result = _asMap(
|
|
3808
|
+
await _call('signBirthCert', {
|
|
3809
|
+
'certificateProtobuf': base64Encode(spec.writeToBuffer()),
|
|
3810
|
+
'deviceSecret': deviceSecret,
|
|
3811
|
+
}),
|
|
3812
|
+
);
|
|
3813
|
+
return nomos_protocol.BirthCertificate.fromBuffer(
|
|
3814
|
+
base64Decode(result['certificateProtobuf']?.toString() ?? ''),
|
|
3815
|
+
);
|
|
3816
|
+
}
|
|
3222
3817
|
|
|
3223
3818
|
/// MINT THE KEYLESS ENROLL-ONCE DELEGATION (`enrollFirstDevice.delegationCert`, the verified-first-device
|
|
3224
3819
|
/// lane). The device SELF-signs it; its authority is the carried `idpToken` (the kernel re-verifies that to
|
|
@@ -3226,17 +3821,20 @@ class NomosBridge {
|
|
|
3226
3821
|
/// `{user, deviceKey, childName, parentId, verifiedVia, idpToken, capturedClock, epoch}`. WITHOUT this the
|
|
3227
3822
|
/// keyless home is born with a malformed cert chain (the platform_cert falls into the delegation slot →
|
|
3228
3823
|
/// `home cert founder != delegation cert founder` at the home's own verify door).
|
|
3229
|
-
Future<
|
|
3824
|
+
Future<NomosBirthCertificate> mintKeylessDelegation(
|
|
3230
3825
|
Map<String, Object?> spec,
|
|
3231
3826
|
String deviceSecret,
|
|
3232
|
-
) async
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3827
|
+
) async {
|
|
3828
|
+
final result = _asMap(
|
|
3829
|
+
await _call('mintKeylessDelegation', {
|
|
3830
|
+
'spec': spec,
|
|
3831
|
+
'deviceSecret': deviceSecret,
|
|
3832
|
+
}),
|
|
3833
|
+
);
|
|
3834
|
+
return nomos_protocol.BirthCertificate.fromBuffer(
|
|
3835
|
+
base64Decode(result['certificateProtobuf']?.toString() ?? ''),
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3240
3838
|
|
|
3241
3839
|
/// ENROLL THIS DEVICE — the LIVE gate: bind a device key's PUBLIC half to `principal` on-chain as a
|
|
3242
3840
|
/// `signer` so the holon's gate can PROVE this device's authorship on replay (and the VA chain attest it).
|
|
@@ -3361,10 +3959,62 @@ class NomosBridge {
|
|
|
3361
3959
|
|
|
3362
3960
|
/// Framework-private receiving-group setup. Generated application hosts own this ceremony; tenant code
|
|
3363
3961
|
/// receives only the resulting business application.
|
|
3364
|
-
Future<
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
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
|
+
}
|
|
3368
4018
|
|
|
3369
4019
|
/// Sign a cross-custody SHARE attestation `{secret, asserterWorkspace, object, relation, subject,
|
|
3370
4020
|
/// attestedAt}` → `{publicKey, signature}`. This is what drives `recordShare` into an invitee's home
|
|
@@ -3404,63 +4054,565 @@ class NomosBridge {
|
|
|
3404
4054
|
}),
|
|
3405
4055
|
);
|
|
3406
4056
|
|
|
3407
|
-
|
|
3408
|
-
/// scope keys so encrypted fields AEAD-open in-wasm; absent ⇒ they stay opaque envelopes (today's
|
|
3409
|
-
/// behavior). `principal` stamps the private-read gate (defaults to the connection's bound principal).
|
|
3410
|
-
/// Both are OMITTED from the wire when absent, so a public un-keyed read is byte-identical to before.
|
|
3411
|
-
Future<List<Map<String, dynamic>>> query(
|
|
3412
|
-
String queryId, [
|
|
3413
|
-
Map<String, Object?> params = const {},
|
|
4057
|
+
nomos_protocol.QueryAuthority _nmsQueryAuthority(
|
|
3414
4058
|
List<NomosScopeKey>? keys,
|
|
3415
4059
|
String? principal,
|
|
3416
4060
|
String? session,
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
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
|
+
),
|
|
3425
4085
|
);
|
|
3426
4086
|
}
|
|
3427
4087
|
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
Map<String, Object?> params = const {},
|
|
3433
|
-
int pageSize = 100,
|
|
3434
|
-
String? cursor,
|
|
3435
|
-
List<NomosScopeKey>? keys,
|
|
3436
|
-
String? principal,
|
|
3437
|
-
String? session,
|
|
4088
|
+
Future<Uint8List> _nmsKernelQueryUsda({
|
|
4089
|
+
required String? session,
|
|
4090
|
+
required nomos_protocol.QueryAuthority authority,
|
|
4091
|
+
required nomos_protocol.QueryRequest query,
|
|
3438
4092
|
}) async {
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
await _call('queryPage', {
|
|
3444
|
-
'queryId': queryId,
|
|
3445
|
-
'params': params,
|
|
3446
|
-
'pageSize': pageSize,
|
|
3447
|
-
if (cursor != null) 'cursor': cursor,
|
|
3448
|
-
...?_readAuthArgs(keys, principal),
|
|
3449
|
-
if (session != null) 'session': session,
|
|
3450
|
-
}),
|
|
3451
|
-
);
|
|
3452
|
-
return NomosPage<Map<String, dynamic>>(
|
|
3453
|
-
rows: _asRows(result),
|
|
3454
|
-
cursor: result['cursor'] as String?,
|
|
3455
|
-
done: result['done'] == true,
|
|
4093
|
+
final response = await _nmsKernelQuery(
|
|
4094
|
+
session: session,
|
|
4095
|
+
authority: authority,
|
|
4096
|
+
query: query,
|
|
3456
4097
|
);
|
|
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);
|
|
3457
4104
|
}
|
|
3458
4105
|
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
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
|
+
),
|
|
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;
|
|
4152
|
+
}
|
|
4153
|
+
|
|
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({
|
|
4207
|
+
String? session,
|
|
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',
|
|
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
|
+
}
|
|
4229
|
+
|
|
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,
|
|
3464
4616
|
String? session,
|
|
3465
4617
|
]) async =>
|
|
3466
4618
|
_asRows(
|
|
@@ -3471,6 +4623,201 @@ class NomosBridge {
|
|
|
3471
4623
|
}),
|
|
3472
4624
|
);
|
|
3473
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
|
+
|
|
3474
4821
|
/// Compare an immutable publication pin with the stable series' one compiled
|
|
3475
4822
|
/// current pointer. This is an O(current-state) check: it neither resolves the
|
|
3476
4823
|
/// pinned publication nor reconstructs aggregate history.
|
|
@@ -3562,39 +4909,6 @@ class NomosBridge {
|
|
|
3562
4909
|
}),
|
|
3563
4910
|
);
|
|
3564
4911
|
|
|
3565
|
-
/// Maintained historic count/sum values at [moment]. These are the temporal counterparts of [count]/[sum].
|
|
3566
|
-
Future<int> countAt(
|
|
3567
|
-
NomosMoment moment,
|
|
3568
|
-
String countId, [
|
|
3569
|
-
String group = '',
|
|
3570
|
-
String? session,
|
|
3571
|
-
]) async =>
|
|
3572
|
-
((_asMap(
|
|
3573
|
-
await _call('countAt', {
|
|
3574
|
-
'commit': moment.id,
|
|
3575
|
-
'countId': countId,
|
|
3576
|
-
'group': group,
|
|
3577
|
-
if (session != null) 'session': session,
|
|
3578
|
-
}),
|
|
3579
|
-
)['count']) as num)
|
|
3580
|
-
.toInt();
|
|
3581
|
-
|
|
3582
|
-
Future<int> sumAt(
|
|
3583
|
-
NomosMoment moment,
|
|
3584
|
-
String sumId, [
|
|
3585
|
-
String group = '',
|
|
3586
|
-
String? session,
|
|
3587
|
-
]) async =>
|
|
3588
|
-
((_asMap(
|
|
3589
|
-
await _call('sumAt', {
|
|
3590
|
-
'commit': moment.id,
|
|
3591
|
-
'sumId': sumId,
|
|
3592
|
-
'group': group,
|
|
3593
|
-
if (session != null) 'session': session,
|
|
3594
|
-
}),
|
|
3595
|
-
)['sum']) as num)
|
|
3596
|
-
.toInt();
|
|
3597
|
-
|
|
3598
4912
|
/// The read auth sidecar (private reads + E2E): `keys`/`principal` as top-level call args, or null when
|
|
3599
4913
|
/// neither is present (so the wire is byte-identical to before this existed).
|
|
3600
4914
|
Map<String, Object?>? _readAuthArgs(
|
|
@@ -3657,7 +4971,9 @@ class NomosBridge {
|
|
|
3657
4971
|
epoch: (out['epoch'] as num?)?.toInt() ?? 0,
|
|
3658
4972
|
key: (out['key'] ?? '').toString(),
|
|
3659
4973
|
ownedKeySubject: out['ownedKeySubject']?.toString(),
|
|
3660
|
-
ownedCustody: out['ownedCustody']
|
|
4974
|
+
ownedCustody: out['ownedCustody'] == null
|
|
4975
|
+
? null
|
|
4976
|
+
: _nomosOwnedCustodyFromTransport(out['ownedCustody']),
|
|
3661
4977
|
);
|
|
3662
4978
|
}
|
|
3663
4979
|
|
|
@@ -3682,7 +4998,9 @@ class NomosBridge {
|
|
|
3682
4998
|
epoch: (key['epoch'] as num?)?.toInt() ?? 0,
|
|
3683
4999
|
key: (key['key'] ?? '').toString(),
|
|
3684
5000
|
ownedKeySubject: key['ownedKeySubject']?.toString(),
|
|
3685
|
-
ownedCustody: key['ownedCustody']
|
|
5001
|
+
ownedCustody: key['ownedCustody'] == null
|
|
5002
|
+
? null
|
|
5003
|
+
: _nomosOwnedCustodyFromTransport(key['ownedCustody']),
|
|
3686
5004
|
),
|
|
3687
5005
|
)
|
|
3688
5006
|
.toList();
|
|
@@ -3719,23 +5037,95 @@ class NomosBridge {
|
|
|
3719
5037
|
);
|
|
3720
5038
|
|
|
3721
5039
|
/// Prepare the private framework payload behind generated share/unshare/transfer methods.
|
|
3722
|
-
Future<
|
|
5040
|
+
Future<NomosOwnedRecordPreparation>
|
|
5041
|
+
nomosPrepareOwnedRecordAction<TAggregate>({
|
|
3723
5042
|
required String action,
|
|
3724
5043
|
required String scope,
|
|
3725
5044
|
required NomosRef<TAggregate> record,
|
|
3726
5045
|
required String peer,
|
|
3727
|
-
}) async
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
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
|
+
}
|
|
3739
5129
|
|
|
3740
5130
|
/// FRAMEWORK-INTERNAL business birth: derive the unborn child's owned key from its deterministic address.
|
|
3741
5131
|
Future<NomosScopeKey> nomosOwnedBirthScopeKey(
|
|
@@ -3758,29 +5148,68 @@ class NomosBridge {
|
|
|
3758
5148
|
epoch: (out['epoch'] as num?)?.toInt() ?? 0,
|
|
3759
5149
|
key: (out['key'] ?? '').toString(),
|
|
3760
5150
|
ownedKeySubject: out['ownedKeySubject']?.toString(),
|
|
3761
|
-
ownedCustody: out['ownedCustody']
|
|
5151
|
+
ownedCustody: out['ownedCustody'] == null
|
|
5152
|
+
? null
|
|
5153
|
+
: _nomosOwnedCustodyFromTransport(out['ownedCustody']),
|
|
3762
5154
|
workspace: out['workspace']?.toString(),
|
|
3763
5155
|
);
|
|
3764
5156
|
}
|
|
3765
5157
|
|
|
3766
5158
|
/// FRAMEWORK-INTERNAL: issue an opaque request for this session's current device.
|
|
3767
|
-
Future<
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
.
|
|
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
|
+
}
|
|
3772
5177
|
|
|
3773
5178
|
/// FRAMEWORK-INTERNAL: validate an opaque request and prepare the atomic signer + custody offer.
|
|
3774
|
-
Future<
|
|
3775
|
-
|
|
5179
|
+
Future<NomosOwnedDeviceApprovalPreparation> nomosPrepareOwnedDeviceApproval(
|
|
5180
|
+
OwnedDeviceRequest request,
|
|
3776
5181
|
String scope,
|
|
3777
|
-
) async
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
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',
|
|
3783
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
|
+
}
|
|
3784
5213
|
|
|
3785
5214
|
/// Wrap a scope key TO a recipient's public key (committer side): `{hpkeEpk, ct}` base64 ciphertext.
|
|
3786
5215
|
Future<Map<String, Object?>> cryptoWrapKey({
|
|
@@ -3810,94 +5239,54 @@ class NomosBridge {
|
|
|
3810
5239
|
'')
|
|
3811
5240
|
.toString();
|
|
3812
5241
|
|
|
3813
|
-
///
|
|
3814
|
-
///
|
|
3815
|
-
Future<
|
|
3816
|
-
String
|
|
3817
|
-
NomosBbox bbox, [
|
|
3818
|
-
String? session,
|
|
3819
|
-
]) async =>
|
|
3820
|
-
_asRows(
|
|
3821
|
-
await _call('spatialWithin', {
|
|
3822
|
-
'spatialId': spatialId,
|
|
3823
|
-
'bbox': bbox.toJson(),
|
|
3824
|
-
if (session != null) 'session': session,
|
|
3825
|
-
}),
|
|
3826
|
-
);
|
|
3827
|
-
|
|
3828
|
-
/// A maintained O(1) count / sum.
|
|
3829
|
-
Future<int> count(
|
|
3830
|
-
String countId, [
|
|
3831
|
-
String group = '',
|
|
3832
|
-
String? session,
|
|
3833
|
-
]) async =>
|
|
3834
|
-
((_asMap(
|
|
3835
|
-
await _call('count', {
|
|
3836
|
-
'countId': countId,
|
|
3837
|
-
'group': group,
|
|
3838
|
-
if (session != null) 'session': session,
|
|
3839
|
-
}),
|
|
3840
|
-
)['count']) as num)
|
|
3841
|
-
.toInt();
|
|
3842
|
-
Future<int> sum(String sumId, [String group = '', String? session]) async =>
|
|
3843
|
-
((_asMap(
|
|
3844
|
-
await _call('sum', {
|
|
3845
|
-
'sumId': sumId,
|
|
3846
|
-
'group': group,
|
|
3847
|
-
if (session != null) 'session': session,
|
|
3848
|
-
}),
|
|
3849
|
-
)['sum']) as num)
|
|
3850
|
-
.toInt();
|
|
3851
|
-
|
|
3852
|
-
/// THE PENDING-CONFLICTS READ (Surfaced merge conflicts — the DX layer): the MAINTAINED,
|
|
3853
|
-
/// workspace-wide "decisions needed" index (reserved id `nomosPendingConflicts`). One row per
|
|
3854
|
-
/// (aggregate, field) whose folded state carries a Surfaced pending set of 2+ observations —
|
|
3855
|
-
/// `{aggregateType, aggregateId, field, observations: [{value, author, occurredAt}]}`.
|
|
3856
|
-
/// DERIVED, NOT DRAINED: a resolution write folding in removes its row by derivation.
|
|
3857
|
-
/// Empty for a Surfaced-free workspace. (The generated typed client wraps these rows as
|
|
3858
|
-
/// `PendingConflictRow` and pairs them with the designated `resolve…` helpers.)
|
|
3859
|
-
Future<List<Map<String, dynamic>>> pendingConflicts([
|
|
3860
|
-
String? session,
|
|
3861
|
-
]) async =>
|
|
3862
|
-
_asRows(
|
|
3863
|
-
await _call(
|
|
3864
|
-
'pendingConflicts', {if (session != null) 'session': session}),
|
|
3865
|
-
);
|
|
3866
|
-
|
|
3867
|
-
/// Framework-filtered pending conflicts for one generated aggregate type. This is the
|
|
3868
|
-
/// primitive generated clients use; application code never filters the heterogeneous queue.
|
|
3869
|
-
Future<List<Map<String, dynamic>>> pendingConflictsFor(
|
|
3870
|
-
String aggregateType, [
|
|
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, [
|
|
3871
5246
|
String? session,
|
|
3872
|
-
])
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
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
|
+
),
|
|
3878
5256
|
);
|
|
3879
5257
|
|
|
3880
5258
|
/// Counts-only workspace summary over the maintained conflict index.
|
|
3881
|
-
Future<
|
|
5259
|
+
Future<Uint8List> nmsPendingConflictSummaryUsda([
|
|
3882
5260
|
String? session,
|
|
3883
|
-
])
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
5261
|
+
]) =>
|
|
5262
|
+
_nmsKernelQueryUsda(
|
|
5263
|
+
session: session,
|
|
5264
|
+
authority: _nmsQueryAuthority(null, null, session),
|
|
5265
|
+
query: nomos_protocol.QueryRequest(
|
|
5266
|
+
pendingConflictSummary: nomos_protocol.PendingConflictSummaryQuery(),
|
|
5267
|
+
),
|
|
3888
5268
|
);
|
|
3889
5269
|
|
|
3890
5270
|
/// Push local writes → the session branch → cloud admission, and pull canonical main back.
|
|
3891
|
-
Future<
|
|
5271
|
+
Future<nomos_protocol.SyncWorkspaceResponse> sync({
|
|
3892
5272
|
bool admit = true,
|
|
3893
5273
|
String? session,
|
|
3894
|
-
}) async
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
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',
|
|
3900
5286
|
);
|
|
5287
|
+
}
|
|
5288
|
+
return response.syncWorkspace;
|
|
5289
|
+
}
|
|
3901
5290
|
|
|
3902
5291
|
/// PERSIST: a base64 snapshot of the whole local holon (the bare nomos.git incl. un-synced writes + the
|
|
3903
5292
|
/// DLQ). Store it on disk and pass it back as `connect(restoreFrom:)` next launch — the app then boots from
|
|
@@ -3919,23 +5308,46 @@ class NomosBridge {
|
|
|
3919
5308
|
int? fallbackMs,
|
|
3920
5309
|
String? signalId,
|
|
3921
5310
|
}) async {
|
|
3922
|
-
await
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
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
|
+
}
|
|
3927
5326
|
final workspace = _sessionName;
|
|
3928
5327
|
if (workspace != null) _eventPlane._autoSyncSessions.add(workspace);
|
|
3929
5328
|
}
|
|
3930
5329
|
|
|
3931
5330
|
Future<void> stopAutoSync() async {
|
|
3932
|
-
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
|
+
}
|
|
3933
5343
|
final workspace = _sessionName;
|
|
3934
5344
|
if (workspace != null) _eventPlane._autoSyncSessions.remove(workspace);
|
|
3935
5345
|
}
|
|
3936
5346
|
|
|
3937
|
-
///
|
|
3938
|
-
|
|
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;
|
|
3939
5351
|
|
|
3940
5352
|
/// LIVE pack byte-progress — the real loading bar. Each event carries `received`/`total` (the host's
|
|
3941
5353
|
/// actual `Content-Length`, known before the body) + `kind` ('delta'|'full'). Emits only while a `/pack`
|
|
@@ -4315,24 +5727,6 @@ class NomosBridge {
|
|
|
4315
5727
|
principal: principal,
|
|
4316
5728
|
);
|
|
4317
5729
|
|
|
4318
|
-
/// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
|
|
4319
|
-
Stream<List<dynamic>> watchSpatial(
|
|
4320
|
-
String spatialId,
|
|
4321
|
-
NomosBbox bbox, [
|
|
4322
|
-
String? session,
|
|
4323
|
-
]) =>
|
|
4324
|
-
_watchStream(
|
|
4325
|
-
kind: 'spatial',
|
|
4326
|
-
spatialId: spatialId,
|
|
4327
|
-
bbox: bbox,
|
|
4328
|
-
session: session,
|
|
4329
|
-
);
|
|
4330
|
-
|
|
4331
|
-
/// Reactive PENDING-CONFLICTS read: re-emits the maintained Surfaced "decisions needed"
|
|
4332
|
-
/// rows whenever the index changes (derived, not drained — a resolution write clears its row).
|
|
4333
|
-
Stream<List<dynamic>> watchPendingConflicts([String? session]) =>
|
|
4334
|
-
_watchStream(kind: 'pendingConflicts', session: session);
|
|
4335
|
-
|
|
4336
5730
|
/// SHAPE COMPATIBILITY (#72): one-shot verdict for this client's structural [requirement]
|
|
4337
5731
|
/// (its baked `…InterfaceJson`, decoded) against the workspace's ON-LEDGER offer (auto-sourced).
|
|
4338
5732
|
/// `{ ok, missing[], incompatible[] }` — names the exact directive/aggregate/field, not a hash.
|
|
@@ -4470,7 +5864,7 @@ class NomosBridge {
|
|
|
4470
5864
|
// Cancellation is a local lifecycle boundary and must complete even if the runner is slow,
|
|
4471
5865
|
// disconnected, or being disposed. The remote release remains best-effort; bridge/runner disposal
|
|
4472
5866
|
// also drops every outstanding watch, so awaiting this acknowledgement only creates teardown hangs.
|
|
4473
|
-
unawaited(
|
|
5867
|
+
unawaited(_unwatch(subId).catchError((_) => null));
|
|
4474
5868
|
},
|
|
4475
5869
|
);
|
|
4476
5870
|
_eventPlane._compatWatches[subId] = ctrl;
|
|
@@ -4482,8 +5876,6 @@ class NomosBridge {
|
|
|
4482
5876
|
String? queryId,
|
|
4483
5877
|
Map<String, Object?>? params,
|
|
4484
5878
|
String? id,
|
|
4485
|
-
String? spatialId,
|
|
4486
|
-
NomosBbox? bbox,
|
|
4487
5879
|
String? base,
|
|
4488
5880
|
int? limit,
|
|
4489
5881
|
String? session,
|
|
@@ -4503,8 +5895,6 @@ class NomosBridge {
|
|
|
4503
5895
|
if (queryId != null) 'queryId': queryId,
|
|
4504
5896
|
if (params != null) 'params': params,
|
|
4505
5897
|
if (id != null) 'id': id,
|
|
4506
|
-
if (spatialId != null) 'spatialId': spatialId,
|
|
4507
|
-
if (bbox != null) 'bbox': bbox.toJson(),
|
|
4508
5898
|
if (base != null) 'base': base,
|
|
4509
5899
|
if (limit != null) 'limit': limit,
|
|
4510
5900
|
if (principal != null) 'principal': principal,
|
|
@@ -4524,7 +5914,7 @@ class NomosBridge {
|
|
|
4524
5914
|
_eventPlane._watches.remove(subId);
|
|
4525
5915
|
// A caller owns its local subscription; it must never need a healthy transport in order to release
|
|
4526
5916
|
// it. Runner-side cleanup is still requested immediately and is made total by runner disposal.
|
|
4527
|
-
unawaited(
|
|
5917
|
+
unawaited(_unwatch(subId).catchError((_) => null));
|
|
4528
5918
|
},
|
|
4529
5919
|
);
|
|
4530
5920
|
_eventPlane._watches[subId] = ctrl;
|
|
@@ -4537,13 +5927,24 @@ class NomosBridge {
|
|
|
4537
5927
|
/// watches and continuous-sync loops must not survive this client.
|
|
4538
5928
|
Future<void> dispose() async {
|
|
4539
5929
|
try {
|
|
4540
|
-
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
|
+
}
|
|
4541
5940
|
} catch (_) {
|
|
4542
5941
|
// A dead/unstarted embedding has no reachable runner to release. The
|
|
4543
5942
|
// enclosing engine host is still disposed below by NomosScope; never let
|
|
4544
5943
|
// teardown strand the Flutter lifecycle on an unavailable transport.
|
|
4545
5944
|
}
|
|
4546
5945
|
await _sub.cancel();
|
|
5946
|
+
await _runtimeEventSub?.cancel();
|
|
5947
|
+
_runtimeEventSub = null;
|
|
4547
5948
|
final pending = _pending.entries
|
|
4548
5949
|
.map(
|
|
4549
5950
|
(entry) => (
|
|
@@ -4556,6 +5957,7 @@ class NomosBridge {
|
|
|
4556
5957
|
_pending.clear();
|
|
4557
5958
|
_pendingOps.clear();
|
|
4558
5959
|
_pendingTraces.clear();
|
|
5960
|
+
_pendingCallStacks.clear();
|
|
4559
5961
|
for (final entry in pending) {
|
|
4560
5962
|
final c = entry.completer;
|
|
4561
5963
|
if (!c.isCompleted) {
|
|
@@ -4577,6 +5979,12 @@ class NomosBridge {
|
|
|
4577
5979
|
// let its done event complete if/when that consumer resumes.
|
|
4578
5980
|
if (!w.isClosed) unawaited(w.close());
|
|
4579
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
|
+
}
|
|
4580
5988
|
final compatWatches = _compatWatches.values.toList(growable: false);
|
|
4581
5989
|
_compatWatches.clear();
|
|
4582
5990
|
for (final w in compatWatches) {
|
|
@@ -4711,13 +6119,26 @@ class NomosSession extends NomosBridge {
|
|
|
4711
6119
|
'Generated child model inheritance needs a bound parent session.',
|
|
4712
6120
|
);
|
|
4713
6121
|
}
|
|
4714
|
-
await
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
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
|
+
}
|
|
4721
6142
|
}
|
|
4722
6143
|
|
|
4723
6144
|
/// Retain the application's typed model-decision policy for generated child
|