@githolon/dsl 0.100.6 → 0.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/CHANGELOG.md +6 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +78 -16
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +29 -3
- package/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +9 -1
- package/src/births.ts +53 -22
- package/src/build_package.ts +55 -14
- package/src/business_birth.ts +48 -7
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +921 -82
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +155 -45
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +47 -2
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- package/src/certified_read.ts +0 -313
|
@@ -48,7 +48,7 @@ const int kWebviewBridgeVersion = 1;
|
|
|
48
48
|
/// pre-stamp bundle, which reports nothing) is a stale baked asset and fails loudly at boot.
|
|
49
49
|
/// MUST move with the paired @githolon/client release — a release-hygiene test pairs it with the
|
|
50
50
|
/// runner stamp (the pubspec-pins pattern).
|
|
51
|
-
const String kExpectedRunnerBundleVersion = '0.
|
|
51
|
+
const String kExpectedRunnerBundleVersion = '0.100.6';
|
|
52
52
|
|
|
53
53
|
/// A STALE BAKED RUNNER BUNDLE: the app's built assets carry an older nomos-runner.js than this
|
|
54
54
|
/// nomos_client release ships — Flutter's build cache kept it across a pub upgrade. Thrown from
|
|
@@ -87,16 +87,52 @@ class TransferProgress {
|
|
|
87
87
|
final int received;
|
|
88
88
|
final int? total;
|
|
89
89
|
final String kind; // 'delta' (incremental) | 'full' (initial clone)
|
|
90
|
-
const TransferProgress(
|
|
91
|
-
|
|
90
|
+
const TransferProgress(
|
|
91
|
+
{required this.received, this.total, this.kind = 'delta'});
|
|
92
|
+
double? get fraction => (total == null || total == 0)
|
|
93
|
+
? null
|
|
94
|
+
: (received / total!).clamp(0.0, 1.0);
|
|
92
95
|
bool get done => total != null && received >= total!;
|
|
93
|
-
factory TransferProgress.fromMessage(Map<String, Object?> m) =>
|
|
96
|
+
factory TransferProgress.fromMessage(Map<String, Object?> m) =>
|
|
97
|
+
TransferProgress(
|
|
94
98
|
received: (m['received'] as num?)?.toInt() ?? 0,
|
|
95
99
|
total: (m['total'] as num?)?.toInt(),
|
|
96
100
|
kind: m['kind']?.toString() ?? 'delta',
|
|
97
101
|
);
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
/// One opaque point in this workspace's verified history. Applications use it as the handle passed to a
|
|
105
|
+
/// generated client's `past.at(moment)`; [id] is framework identity, not a business field and need never be
|
|
106
|
+
/// parsed. The remaining fields make a timeline useful without exposing commit or event-sourcing ceremony.
|
|
107
|
+
class NomosMoment {
|
|
108
|
+
final String id;
|
|
109
|
+
final DateTime? occurredAt;
|
|
110
|
+
final String? actorSubject;
|
|
111
|
+
final String? domain;
|
|
112
|
+
final String? action;
|
|
113
|
+
|
|
114
|
+
const NomosMoment({
|
|
115
|
+
required this.id,
|
|
116
|
+
this.occurredAt,
|
|
117
|
+
this.actorSubject,
|
|
118
|
+
this.domain,
|
|
119
|
+
this.action,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
factory NomosMoment.fromJson(Map<String, Object?> json) {
|
|
123
|
+
final at = json['at'];
|
|
124
|
+
return NomosMoment(
|
|
125
|
+
id: (json['oid'] ?? '').toString(),
|
|
126
|
+
occurredAt: at is num
|
|
127
|
+
? DateTime.fromMillisecondsSinceEpoch(at.toInt(), isUtc: true)
|
|
128
|
+
: null,
|
|
129
|
+
actorSubject: json['actorSubject']?.toString(),
|
|
130
|
+
domain: json['domain']?.toString(),
|
|
131
|
+
action: json['directiveId']?.toString(),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
100
136
|
enum SyncPhase { idle, syncing, transferring, behind, offline, error }
|
|
101
137
|
|
|
102
138
|
/// The app-ready sync state — a truthful aggregation of the low-level signals (sync rounds, the doorbell,
|
|
@@ -123,9 +159,17 @@ class SyncStatus {
|
|
|
123
159
|
this.lastError,
|
|
124
160
|
});
|
|
125
161
|
SyncStatus copyWith({
|
|
126
|
-
SyncPhase? phase,
|
|
127
|
-
|
|
128
|
-
|
|
162
|
+
SyncPhase? phase,
|
|
163
|
+
int? ahead,
|
|
164
|
+
int? behind,
|
|
165
|
+
int? saved,
|
|
166
|
+
int? onMain,
|
|
167
|
+
int? deadLetters,
|
|
168
|
+
TransferProgress? transfer,
|
|
169
|
+
bool clearTransfer = false,
|
|
170
|
+
DateTime? lastSyncedAt,
|
|
171
|
+
String? lastError,
|
|
172
|
+
bool clearError = false,
|
|
129
173
|
}) =>
|
|
130
174
|
SyncStatus(
|
|
131
175
|
phase: phase ?? this.phase,
|
|
@@ -149,7 +193,8 @@ class NomosCreated {
|
|
|
149
193
|
/// The workspace the offer ran on (NomosRef). Additive — defaults to '' on older runners that
|
|
150
194
|
/// don't stamp it; then [ref] is workspace-blank (still a valid locator on the local plane).
|
|
151
195
|
final String workspace;
|
|
152
|
-
const NomosCreated(
|
|
196
|
+
const NomosCreated(
|
|
197
|
+
{required this.type, required this.id, this.workspace = ''});
|
|
153
198
|
|
|
154
199
|
/// The canonical [NomosRef] view — the `{workspace,type,id}` triple, followable/serializable
|
|
155
200
|
/// (`nomos://<workspace>/<type>/<id>`). One reference, everywhere.
|
|
@@ -186,13 +231,17 @@ class NomosBbox {
|
|
|
186
231
|
maxY = (maxY ?? maxLat)!;
|
|
187
232
|
|
|
188
233
|
/// Deprecated geographic accessors — spatial axes are generic. Prefer [minX]/[minY]/[maxX]/[maxY].
|
|
189
|
-
@Deprecated(
|
|
234
|
+
@Deprecated(
|
|
235
|
+
'Spatial axes are generic; use minX/minY. Kept for source compat.')
|
|
190
236
|
double get minLng => minX;
|
|
191
|
-
@Deprecated(
|
|
237
|
+
@Deprecated(
|
|
238
|
+
'Spatial axes are generic; use minX/minY. Kept for source compat.')
|
|
192
239
|
double get minLat => minY;
|
|
193
|
-
@Deprecated(
|
|
240
|
+
@Deprecated(
|
|
241
|
+
'Spatial axes are generic; use maxX/maxY. Kept for source compat.')
|
|
194
242
|
double get maxLng => maxX;
|
|
195
|
-
@Deprecated(
|
|
243
|
+
@Deprecated(
|
|
244
|
+
'Spatial axes are generic; use maxX/maxY. Kept for source compat.')
|
|
196
245
|
double get maxLat => maxY;
|
|
197
246
|
|
|
198
247
|
Map<String, Object?> toJson() => {
|
|
@@ -256,15 +305,18 @@ class NomosCompat {
|
|
|
256
305
|
|
|
257
306
|
static NomosCompat? fromJson(Map<String, Object?>? m) {
|
|
258
307
|
if (m == null) return null;
|
|
259
|
-
final rawMembers =
|
|
308
|
+
final rawMembers =
|
|
309
|
+
(m['members'] as Map?)?.cast<String, Object?>() ?? const {};
|
|
260
310
|
final members = <String, NomosCompatMember>{};
|
|
261
311
|
rawMembers.forEach((k, v) {
|
|
262
|
-
if (v is Map)
|
|
312
|
+
if (v is Map)
|
|
313
|
+
members[k] = NomosCompatMember.fromJson(v.cast<String, Object?>());
|
|
263
314
|
});
|
|
264
315
|
final install = (m['install'] as Map?)?.cast<String, Object?>() ?? const {};
|
|
265
316
|
return NomosCompat(
|
|
266
317
|
generatedFor: (m['generatedFor'] as String?) ?? '',
|
|
267
|
-
workspaceRuns:
|
|
318
|
+
workspaceRuns:
|
|
319
|
+
((m['workspaceRuns'] as List?) ?? const []).map((e) => '$e').toList(),
|
|
268
320
|
matches: m['matches'] == true,
|
|
269
321
|
offerKnown: m['offerKnown'] == true,
|
|
270
322
|
structurallyCompatible: m['structurallyCompatible'] as bool?,
|
|
@@ -285,9 +337,96 @@ class NomosScopeKey {
|
|
|
285
337
|
final String scope;
|
|
286
338
|
final int epoch;
|
|
287
339
|
final String key;
|
|
340
|
+
|
|
341
|
+
/// Framework-private signing-key subject used to pin `.owned()` custody to one approved device.
|
|
342
|
+
final String? ownedKeySubject;
|
|
343
|
+
|
|
344
|
+
/// Framework-private encrypted handoff, present only while seating the first device.
|
|
345
|
+
final String? ownedCustody;
|
|
346
|
+
|
|
347
|
+
/// Framework-private deterministic child address, present only for an unborn owned business aggregate.
|
|
348
|
+
final String? workspace;
|
|
288
349
|
const NomosScopeKey(
|
|
289
|
-
{required this.scope,
|
|
290
|
-
|
|
350
|
+
{required this.scope,
|
|
351
|
+
required this.epoch,
|
|
352
|
+
required this.key,
|
|
353
|
+
this.ownedKeySubject,
|
|
354
|
+
this.ownedCustody,
|
|
355
|
+
this.workspace});
|
|
356
|
+
Map<String, Object?> toJson() => {
|
|
357
|
+
'scope': scope,
|
|
358
|
+
'epoch': epoch,
|
|
359
|
+
'key': key,
|
|
360
|
+
if (ownedKeySubject != null) 'ownedKeySubject': ownedKeySubject,
|
|
361
|
+
if (ownedCustody != null) 'ownedCustody': ownedCustody,
|
|
362
|
+
if (workspace != null) 'workspace': workspace,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// The entire app-facing multi-device surface for owned business state. The request is deliberately an
|
|
367
|
+
/// opaque string: apps may copy, share or QR it, but never parse certificates, key material or signer law.
|
|
368
|
+
class NomosDevices {
|
|
369
|
+
final NomosBridge bridge;
|
|
370
|
+
final String domain;
|
|
371
|
+
final String domainHash;
|
|
372
|
+
final String? actor;
|
|
373
|
+
final String scope;
|
|
374
|
+
|
|
375
|
+
const NomosDevices({
|
|
376
|
+
required this.bridge,
|
|
377
|
+
required this.domain,
|
|
378
|
+
required this.domainHash,
|
|
379
|
+
required this.actor,
|
|
380
|
+
required this.scope,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
Future<String> request() => bridge.nomosOwnedDeviceRequest(scope);
|
|
384
|
+
|
|
385
|
+
Future<NomosOfferOutcome> approve(String request) async {
|
|
386
|
+
final principal = actor ?? bridge.principal;
|
|
387
|
+
if (principal == null || principal.trim().isEmpty) {
|
|
388
|
+
throw StateError('devices.approve() needs a signed-in Nomos session');
|
|
389
|
+
}
|
|
390
|
+
final payload =
|
|
391
|
+
await bridge.nomosPrepareOwnedDeviceApproval(request, scope);
|
|
392
|
+
return bridge.offerOutcome(
|
|
393
|
+
domain: domain,
|
|
394
|
+
directiveId: 'nomos_internal_approve_owned_device_7c1f5e',
|
|
395
|
+
payload: payload,
|
|
396
|
+
actor: principal,
|
|
397
|
+
domainHash: domainHash,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/// The one business-level recovery policy surface. Escrow wraps, Orders, Receipts and retries stay inside Nomos.
|
|
403
|
+
class NomosRecovery {
|
|
404
|
+
final NomosBridge bridge;
|
|
405
|
+
final String domain;
|
|
406
|
+
final String domainHash;
|
|
407
|
+
final String scope;
|
|
408
|
+
|
|
409
|
+
const NomosRecovery({
|
|
410
|
+
required this.bridge,
|
|
411
|
+
required this.domain,
|
|
412
|
+
required this.domainHash,
|
|
413
|
+
required this.scope,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
/// Opt into all-device-loss recovery with [person]. This is intentionally one-way for a workspace:
|
|
417
|
+
/// a principal who has legitimately recovered a key cannot be made to forget it later.
|
|
418
|
+
Future<Map<String, Object?>> protectWith(String person) {
|
|
419
|
+
if (person.trim().isEmpty) {
|
|
420
|
+
throw ArgumentError(
|
|
421
|
+
'recovery.protectWith() needs the person who will recover you');
|
|
422
|
+
}
|
|
423
|
+
return bridge.nomosProtectOwnedIdentity(
|
|
424
|
+
scope: scope,
|
|
425
|
+
domain: domain,
|
|
426
|
+
domainHash: domainHash,
|
|
427
|
+
recoveryPrincipal: person,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
291
430
|
}
|
|
292
431
|
|
|
293
432
|
/// The app-supplied KEY CUSTODY surface (E2E sealing). Supply one to a generated `<Domain>Client` and its
|
|
@@ -341,9 +480,7 @@ class WrappedKeyStore implements NomosKeyStore {
|
|
|
341
480
|
hpkeEpk: d['hpkeEpk'].toString(),
|
|
342
481
|
ct: d['ct'].toString());
|
|
343
482
|
held.add(NomosScopeKey(
|
|
344
|
-
scope: scope,
|
|
345
|
-
epoch: (d['epoch'] as num?)?.toInt() ?? 0,
|
|
346
|
-
key: key));
|
|
483
|
+
scope: scope, epoch: (d['epoch'] as num?)?.toInt() ?? 0, key: key));
|
|
347
484
|
}
|
|
348
485
|
held.sort((a, b) => b.epoch - a.epoch);
|
|
349
486
|
return held;
|
|
@@ -416,7 +553,10 @@ class BirthIncomplete implements Exception {
|
|
|
416
553
|
|
|
417
554
|
/// The first failed child's kernel code (e.g. `BirthLawPackageUnavailable`), or `BirthFailed`.
|
|
418
555
|
String get code =>
|
|
419
|
-
(failedBirths.isNotEmpty
|
|
556
|
+
(failedBirths.isNotEmpty
|
|
557
|
+
? failedBirths.first['code']?.toString()
|
|
558
|
+
: null) ??
|
|
559
|
+
'BirthFailed';
|
|
420
560
|
|
|
421
561
|
/// The child workspace name(s) that did not come up.
|
|
422
562
|
List<String> get workspaces => failedBirths
|
|
@@ -489,8 +629,7 @@ class NomosEncryption {
|
|
|
489
629
|
|
|
490
630
|
Future<List<String>> _readRoster(String scope) async {
|
|
491
631
|
final s = _parseScope(scope);
|
|
492
|
-
final rows =
|
|
493
|
-
await bridge.queryById('reltuple-${s.object}-${s.relation}');
|
|
632
|
+
final rows = await bridge.queryById('reltuple-${s.object}-${s.relation}');
|
|
494
633
|
final subjects = rows.isEmpty
|
|
495
634
|
? const <String, dynamic>{}
|
|
496
635
|
: ((rows.first['data'] as Map?)?['subjects'] as Map?) ??
|
|
@@ -499,9 +638,8 @@ class NomosEncryption {
|
|
|
499
638
|
subjects.forEach((subj, leafRaw) {
|
|
500
639
|
Map? leaf;
|
|
501
640
|
try {
|
|
502
|
-
leaf =
|
|
503
|
-
? jsonDecode(leafRaw) as Map
|
|
504
|
-
: (leafRaw as Map);
|
|
641
|
+
leaf =
|
|
642
|
+
leafRaw is String ? jsonDecode(leafRaw) as Map : (leafRaw as Map);
|
|
505
643
|
} catch (_) {
|
|
506
644
|
leaf = null;
|
|
507
645
|
}
|
|
@@ -524,11 +662,14 @@ class NomosEncryption {
|
|
|
524
662
|
Future<String> _freshScopeKey() async {
|
|
525
663
|
final kp = await bridge.cryptoKeygen();
|
|
526
664
|
final secret = base64Decode((kp['secret'] ?? '').toString());
|
|
527
|
-
return base64Encode(secret.sublist(
|
|
665
|
+
return base64Encode(secret.sublist(
|
|
666
|
+
0, 32)); // the 32-byte epoch secret; NEVER lands on chain
|
|
528
667
|
}
|
|
529
668
|
|
|
530
669
|
Future<String?> _memberPubKey(String principal,
|
|
531
|
-
{String? newcomer,
|
|
670
|
+
{String? newcomer,
|
|
671
|
+
String? newcomerPub,
|
|
672
|
+
Map<String, String>? memberKeys}) async {
|
|
532
673
|
if (memberKeys != null && memberKeys[principal] != null) {
|
|
533
674
|
return memberKeys[principal];
|
|
534
675
|
}
|
|
@@ -589,7 +730,9 @@ class NomosEncryption {
|
|
|
589
730
|
final wraps = <Map<String, Object?>>[];
|
|
590
731
|
for (final principal in members) {
|
|
591
732
|
final pub = await _memberPubKey(principal,
|
|
592
|
-
newcomer: addPrincipal,
|
|
733
|
+
newcomer: addPrincipal,
|
|
734
|
+
newcomerPub: addPublicKey,
|
|
735
|
+
memberKeys: memberKeys);
|
|
593
736
|
if (pub == null) throw EncryptionMemberKeyMissing(principal, scope);
|
|
594
737
|
final w = await bridge.cryptoWrapKey(recipientPub: pub, scopeKey: key);
|
|
595
738
|
wraps.add({
|
|
@@ -599,7 +742,8 @@ class NomosEncryption {
|
|
|
599
742
|
'ct': (w['ct'] ?? '').toString(),
|
|
600
743
|
});
|
|
601
744
|
}
|
|
602
|
-
final epoch =
|
|
745
|
+
final epoch =
|
|
746
|
+
(await _currentEpoch(scope)) + 1; // forward-only: strictly increments
|
|
603
747
|
final head = (await bridge.offerOutcome(
|
|
604
748
|
domain: domain,
|
|
605
749
|
directiveId: 'advanceKeyEpoch',
|
|
@@ -750,7 +894,8 @@ extension NomosRefAcquire on NomosRef {
|
|
|
750
894
|
throw ArgumentError.value(this, 'ref',
|
|
751
895
|
'NomosRef.acquire: the ref carries no workspace — only a workspace-complete ref is openable');
|
|
752
896
|
}
|
|
753
|
-
return bridge.acquireSession(workspace,
|
|
897
|
+
return bridge.acquireSession(workspace,
|
|
898
|
+
ifAbsentCredentials: ifAbsentCredentials);
|
|
754
899
|
}
|
|
755
900
|
}
|
|
756
901
|
|
|
@@ -817,9 +962,12 @@ class NomosBridge {
|
|
|
817
962
|
final StreamController<TransferProgress> _transferEvents =
|
|
818
963
|
StreamController<TransferProgress>.broadcast();
|
|
819
964
|
// The DERIVED app-ready sync state. seeded-on-listen with a metrics poll; updated on sync/signal/transfer.
|
|
820
|
-
late final StreamController<SyncStatus> _syncStatusCtrl =
|
|
965
|
+
late final StreamController<SyncStatus> _syncStatusCtrl =
|
|
966
|
+
StreamController<SyncStatus>.broadcast(
|
|
821
967
|
onListen: _startStatusPoll,
|
|
822
|
-
onCancel: () {
|
|
968
|
+
onCancel: () {
|
|
969
|
+
if (!_syncStatusCtrl.hasListener) _stopStatusPoll();
|
|
970
|
+
},
|
|
823
971
|
);
|
|
824
972
|
SyncStatus _status = const SyncStatus();
|
|
825
973
|
Timer? _metricsTimer;
|
|
@@ -852,7 +1000,8 @@ class NomosBridge {
|
|
|
852
1000
|
StaleRunnerBundle? _staleBundleError(Object? bundleVersion) {
|
|
853
1001
|
final v = bundleVersion is String ? bundleVersion : null;
|
|
854
1002
|
if (v == 'unbundled-dev') return null;
|
|
855
|
-
if (v != null && _semverAtLeast(v, kExpectedRunnerBundleVersion))
|
|
1003
|
+
if (v != null && _semverAtLeast(v, kExpectedRunnerBundleVersion))
|
|
1004
|
+
return null;
|
|
856
1005
|
return StaleRunnerBundle(found: v, expected: kExpectedRunnerBundleVersion);
|
|
857
1006
|
}
|
|
858
1007
|
|
|
@@ -963,7 +1112,8 @@ class NomosBridge {
|
|
|
963
1112
|
if (event == 'lawChanged') {
|
|
964
1113
|
// COMPAT SUPERPOWER (#72b): a synced install/upgrade/retire moved the workspace's offer under this
|
|
965
1114
|
// live client. Cache the fresh verdict + emit it so the app re-checks feature flags without polling.
|
|
966
|
-
final c =
|
|
1115
|
+
final c =
|
|
1116
|
+
NomosCompat.fromJson((m['compat'] as Map?)?.cast<String, Object?>());
|
|
967
1117
|
if (c != null) {
|
|
968
1118
|
_lastCompat = c;
|
|
969
1119
|
final sess = m['session'] as String?;
|
|
@@ -979,7 +1129,9 @@ class NomosBridge {
|
|
|
979
1129
|
if (event == 'sync') {
|
|
980
1130
|
// a completed round → idle (the next metrics poll folds in residual ahead/behind), stamp the time.
|
|
981
1131
|
_pushStatus(_status.copyWith(
|
|
982
|
-
phase: SyncPhase.idle,
|
|
1132
|
+
phase: SyncPhase.idle,
|
|
1133
|
+
clearTransfer: true,
|
|
1134
|
+
lastSyncedAt: DateTime.now()));
|
|
983
1135
|
} else if (m['status'] == 'closed' || m['status'] == 'offline') {
|
|
984
1136
|
_pushStatus(_status.copyWith(phase: SyncPhase.offline));
|
|
985
1137
|
}
|
|
@@ -998,7 +1150,8 @@ class NomosBridge {
|
|
|
998
1150
|
if (!_transferEvents.isClosed) _transferEvents.add(tp);
|
|
999
1151
|
_pushStatus(_status.copyWith(
|
|
1000
1152
|
phase: tp.done ? SyncPhase.syncing : SyncPhase.transferring,
|
|
1001
|
-
transfer: tp,
|
|
1153
|
+
transfer: tp,
|
|
1154
|
+
clearTransfer: tp.done));
|
|
1002
1155
|
return;
|
|
1003
1156
|
}
|
|
1004
1157
|
if (event == 'trace') {
|
|
@@ -1010,7 +1163,8 @@ class NomosBridge {
|
|
|
1010
1163
|
// signal holon projection: ephemeral coordination such as peer frontiers / lag.
|
|
1011
1164
|
if (!_signalEvents.isClosed) _signalEvents.add(m);
|
|
1012
1165
|
// the doorbell says canonical main advanced → we're behind until the next pull lands.
|
|
1013
|
-
if (_status.phase == SyncPhase.idle)
|
|
1166
|
+
if (_status.phase == SyncPhase.idle)
|
|
1167
|
+
_pushStatus(_status.copyWith(phase: SyncPhase.behind));
|
|
1014
1168
|
return;
|
|
1015
1169
|
}
|
|
1016
1170
|
if (event == 'span') {
|
|
@@ -1087,7 +1241,8 @@ class NomosBridge {
|
|
|
1087
1241
|
try {
|
|
1088
1242
|
final parts = token.split('.');
|
|
1089
1243
|
if (parts.length < 2) return null;
|
|
1090
|
-
final payload = jsonDecode(
|
|
1244
|
+
final payload = jsonDecode(
|
|
1245
|
+
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
|
|
1091
1246
|
final sub = payload is Map ? payload['sub'] : null;
|
|
1092
1247
|
if (sub is! String || sub.trim().isEmpty) return null;
|
|
1093
1248
|
final value = sub.trim();
|
|
@@ -1115,7 +1270,8 @@ class NomosBridge {
|
|
|
1115
1270
|
/// `ForeignNotFound`, not a throw) and reads it. [cloud] is the cloud the foreign workspaces sync from
|
|
1116
1271
|
/// (usually the same cloud your primary workspace uses); omit for a purely local plane. This is a LOCATOR
|
|
1117
1272
|
/// resolver, not a foreign key — the foreign holon is sovereign, mounted only when you choose to resolve.
|
|
1118
|
-
NomosForeignReader foreignReader({String? cloud}) =>
|
|
1273
|
+
NomosForeignReader foreignReader({String? cloud}) =>
|
|
1274
|
+
_BridgeForeignReader(this, cloud);
|
|
1119
1275
|
|
|
1120
1276
|
/// The workspace names [connect] has opened on this plane (the live sessions).
|
|
1121
1277
|
List<String> get connectedSessions =>
|
|
@@ -1128,7 +1284,9 @@ class NomosBridge {
|
|
|
1128
1284
|
final plane = _eventPlane;
|
|
1129
1285
|
final scoped = _sessionName;
|
|
1130
1286
|
if (scoped != null) return plane._sessionPrincipals[scoped];
|
|
1131
|
-
return plane._sessionPrincipals.length == 1
|
|
1287
|
+
return plane._sessionPrincipals.length == 1
|
|
1288
|
+
? plane._sessionPrincipals.values.first
|
|
1289
|
+
: null;
|
|
1132
1290
|
}
|
|
1133
1291
|
|
|
1134
1292
|
/// ENUMERATE THE LOCAL PLANE — every workspace on this device + its lifecycle STATE and sync posture:
|
|
@@ -1141,17 +1299,17 @@ class NomosBridge {
|
|
|
1141
1299
|
/// `syncPosture` is `"local-only"` (no cloud counterpart) or `"cloud-bound"`.
|
|
1142
1300
|
Future<List<Map<String, Object?>>> listWorkspaces() async {
|
|
1143
1301
|
final connected = _eventPlane._sessionNames.toSet();
|
|
1144
|
-
return (((_asMap(await _call('listWorkspaces'))['workspaces']) as List?) ??
|
|
1302
|
+
return (((_asMap(await _call('listWorkspaces'))['workspaces']) as List?) ??
|
|
1303
|
+
const [])
|
|
1145
1304
|
.map((e) => (e as Map).cast<String, Object?>())
|
|
1146
1305
|
.map((w) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
.toList();
|
|
1306
|
+
final ws = w['workspace']?.toString() ?? '';
|
|
1307
|
+
// `connected` is the app's own connect() truth; `parked`/resident come from the runner realm.
|
|
1308
|
+
w['state'] = (w['parked'] == true)
|
|
1309
|
+
? 'parked'
|
|
1310
|
+
: (connected.contains(ws) ? 'connected' : 'resident');
|
|
1311
|
+
return w;
|
|
1312
|
+
}).toList();
|
|
1155
1313
|
}
|
|
1156
1314
|
|
|
1157
1315
|
/// Open a workspace on this plane: boot its LOCAL githolon under the installed law (from [restoreFrom]
|
|
@@ -1200,22 +1358,26 @@ class NomosBridge {
|
|
|
1200
1358
|
// fail closed (the break surfaces as an error on the sync/pull that hit it; the holon stays local).
|
|
1201
1359
|
if (onCustodyBreak != null) 'onCustodyBreak': onCustodyBreak.wire,
|
|
1202
1360
|
}));
|
|
1203
|
-
_eventPlane._sessionNames
|
|
1361
|
+
_eventPlane._sessionNames
|
|
1362
|
+
.add(ws); // the open succeeded — the session is real now
|
|
1204
1363
|
final boundPrincipal = _principalFromToken(authToken);
|
|
1205
|
-
if (boundPrincipal != null)
|
|
1206
|
-
|
|
1364
|
+
if (boundPrincipal != null)
|
|
1365
|
+
_eventPlane._sessionPrincipals[ws] = boundPrincipal;
|
|
1366
|
+
_eventPlane._lastRecovery =
|
|
1367
|
+
(r['recovery'] as Map?)?.cast<String, Object?>();
|
|
1207
1368
|
}
|
|
1208
1369
|
|
|
1209
1370
|
/// The custody lifecycle stream (rebirth_and_custody_lifecycle.md) — a rebirth/divergence surfaces here as a
|
|
1210
1371
|
/// typed [NomosCustodyEvent]. Bind a Flutter break dialog / posture banner to it; for a `CustodyBreakPolicy.
|
|
1211
1372
|
/// ask` session, an event of `kind == 'ask'` awaits [resolveCustodyBreak].
|
|
1212
|
-
Stream<NomosCustodyEvent> get custodyEvents =>
|
|
1373
|
+
Stream<NomosCustodyEvent> get custodyEvents =>
|
|
1374
|
+
_eventPlane._custodyEvents.stream;
|
|
1213
1375
|
|
|
1214
1376
|
/// Where a session sits on the custody spectrum (incarnation, posture, pending/salvaged local work,
|
|
1215
1377
|
/// supersession history) — for a status banner or a "you're offline on old truth" indicator.
|
|
1216
1378
|
Future<NomosCustodyStatus> custodyStatus({String? session}) async =>
|
|
1217
|
-
NomosCustodyStatus.fromJson(
|
|
1218
|
-
|
|
1379
|
+
NomosCustodyStatus.fromJson(_asMap(await _call(
|
|
1380
|
+
'custodyStatus', {if (session != null) 'session': session})));
|
|
1219
1381
|
|
|
1220
1382
|
/// Continue an `ask`-policy custody break with the user's decision (adopt|reject) — the async UX lane.
|
|
1221
1383
|
Future<Map<String, Object?>> resolveCustodyBreak(String decision,
|
|
@@ -1227,7 +1389,8 @@ class NomosBridge {
|
|
|
1227
1389
|
|
|
1228
1390
|
/// The parked salvaged intents (re-offerable local work an `adopt` rescued) — metadata only.
|
|
1229
1391
|
Future<List<Map<String, Object?>>> salvagedIntents({String? session}) async {
|
|
1230
|
-
final r = _asMap(await _call(
|
|
1392
|
+
final r = _asMap(await _call(
|
|
1393
|
+
'salvagedIntents', {if (session != null) 'session': session}));
|
|
1231
1394
|
return ((r['salvaged'] as List?) ?? const [])
|
|
1232
1395
|
.map((e) => (e as Map).cast<String, Object?>())
|
|
1233
1396
|
.toList();
|
|
@@ -1235,13 +1398,16 @@ class NomosBridge {
|
|
|
1235
1398
|
|
|
1236
1399
|
/// Re-offer parked salvaged work under the current (adopted) law — the explicit app move (never automatic
|
|
1237
1400
|
/// across incarnations). [id] re-offers one; omitted re-offers all.
|
|
1238
|
-
Future<Map<String, Object?>> reofferSalvaged(
|
|
1401
|
+
Future<Map<String, Object?>> reofferSalvaged(
|
|
1402
|
+
{String? id, String? session}) async =>
|
|
1239
1403
|
_asMap(await _call('reofferSalvaged',
|
|
1240
1404
|
{if (id != null) 'id': id, if (session != null) 'session': session}));
|
|
1241
1405
|
|
|
1242
1406
|
/// Discard one parked salvaged intent — the app's explicit choice.
|
|
1243
|
-
Future<Map<String, Object?>> discardSalvaged(String id,
|
|
1244
|
-
|
|
1407
|
+
Future<Map<String, Object?>> discardSalvaged(String id,
|
|
1408
|
+
{String? session}) async =>
|
|
1409
|
+
_asMap(await _call('discardSalvaged',
|
|
1410
|
+
{'id': id, if (session != null) 'session': session}));
|
|
1245
1411
|
|
|
1246
1412
|
// ── THE DEAD-LETTER QUEUE (refused-but-legitimate work is NEVER lost) — the Dart mirror of the
|
|
1247
1413
|
// JS holon's DLQ surface (`deadLetters()` / `retryDeadLetter()` / `discardDeadLetter()`). A
|
|
@@ -1252,7 +1418,8 @@ class NomosBridge {
|
|
|
1252
1418
|
/// The parked dead letters — metadata only (`{id, oid, source, error, domain, directiveId, at}`);
|
|
1253
1419
|
/// the intent bytes stay in the durable queue.
|
|
1254
1420
|
Future<List<Map<String, Object?>>> deadLetters({String? session}) async {
|
|
1255
|
-
final r = _asMap(
|
|
1421
|
+
final r = _asMap(
|
|
1422
|
+
await _call('deadLetters', {if (session != null) 'session': session}));
|
|
1256
1423
|
return ((r['dead'] as List?) ?? const [])
|
|
1257
1424
|
.map((e) => (e as Map).cast<String, Object?>())
|
|
1258
1425
|
.toList();
|
|
@@ -1261,12 +1428,16 @@ class NomosBridge {
|
|
|
1261
1428
|
/// Retry one dead letter (after the law fix landed): pulls canon first (it may already be
|
|
1262
1429
|
/// admitted), else re-offers the parked bytes locally. `{ok, head?|alreadyOnMain?|error?}` —
|
|
1263
1430
|
/// a refusal leaves the entry parked (with the fresh error), never lost.
|
|
1264
|
-
Future<Map<String, Object?>> retryDeadLetter(String id,
|
|
1265
|
-
|
|
1431
|
+
Future<Map<String, Object?>> retryDeadLetter(String id,
|
|
1432
|
+
{String? session}) async =>
|
|
1433
|
+
_asMap(await _call('retryDeadLetter',
|
|
1434
|
+
{'id': id, if (session != null) 'session': session}));
|
|
1266
1435
|
|
|
1267
1436
|
/// Discard one dead letter — the app's EXPLICIT choice to drop the work. `{ok}`.
|
|
1268
|
-
Future<Map<String, Object?>> discardDeadLetter(String id,
|
|
1269
|
-
|
|
1437
|
+
Future<Map<String, Object?>> discardDeadLetter(String id,
|
|
1438
|
+
{String? session}) async =>
|
|
1439
|
+
_asMap(await _call('discardDeadLetter',
|
|
1440
|
+
{'id': id, if (session != null) 'session': session}));
|
|
1270
1441
|
|
|
1271
1442
|
/// SALVAGE — best-effort extraction of this holon's local-only authored work (un-synced pending
|
|
1272
1443
|
/// writes + the DLQ), each with its full `intentB64` bytes: `{pendingWrites, deadLetters}`. The
|
|
@@ -1278,9 +1449,9 @@ class NomosBridge {
|
|
|
1278
1449
|
/// (a refusal dead-letters — never lost), and dead-letter entries are re-queued into the DLQ.
|
|
1279
1450
|
/// Returns the recovery report `{salvagedWrites, requeuedDeadLetters, unsalvageable}`.
|
|
1280
1451
|
Future<Map<String, Object?>> absorbSalvage(
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1452
|
+
{List<Map<String, Object?>> pendingWrites = const [],
|
|
1453
|
+
List<Map<String, Object?>> deadLetters = const [],
|
|
1454
|
+
String? session}) async =>
|
|
1284
1455
|
_asMap(await _call('absorbSalvage', {
|
|
1285
1456
|
'pendingWrites': pendingWrites,
|
|
1286
1457
|
'deadLetters': deadLetters,
|
|
@@ -1294,7 +1465,8 @@ class NomosBridge {
|
|
|
1294
1465
|
Map<String, Object?>? _lastRecovery;
|
|
1295
1466
|
|
|
1296
1467
|
/// LOCAL-FIRST offer: carry opaque intent bytes into the GitHolon. Returns the new local head; durable on [sync].
|
|
1297
|
-
@Deprecated(
|
|
1468
|
+
@Deprecated(
|
|
1469
|
+
'Engine-level generic writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
1298
1470
|
'dispatch through a generated <Domain>Client bound to a NomosSession '
|
|
1299
1471
|
'(XClient.bind(session)). Retained for CLIs/admin tooling/generated internals only.')
|
|
1300
1472
|
Future<String> offer({required String intentBytes}) async {
|
|
@@ -1312,7 +1484,8 @@ class NomosBridge {
|
|
|
1312
1484
|
/// its trust policy); [force] carries the deploy/evolve force flag through. Both are OMITTED from the wire
|
|
1313
1485
|
/// when absent, so a plain offer is byte-identical to before. [session] pins the colocated workspace in a
|
|
1314
1486
|
/// cluster harness (absent ⇒ the runner's sole-session default).
|
|
1315
|
-
@Deprecated(
|
|
1487
|
+
@Deprecated(
|
|
1488
|
+
'Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
1316
1489
|
'dispatch through a generated <Domain>Client bound to a NomosSession '
|
|
1317
1490
|
'(XClient.bind(session) — no domain/directive strings at call sites). '
|
|
1318
1491
|
'Retained for CLIs/admin tooling/generated internals only.')
|
|
@@ -1323,6 +1496,7 @@ class NomosBridge {
|
|
|
1323
1496
|
String? actor,
|
|
1324
1497
|
String? domainHash,
|
|
1325
1498
|
List<Map<String, Object?>>? attestations,
|
|
1499
|
+
List<Map<String, Object?>>? nomosConflictSnapshots,
|
|
1326
1500
|
bool? force,
|
|
1327
1501
|
String? session,
|
|
1328
1502
|
// Per-offer signing key (base64 device secret): sign THIS offer with a specific device key instead of
|
|
@@ -1336,6 +1510,8 @@ class NomosBridge {
|
|
|
1336
1510
|
if (actor != null) 'actor': actor,
|
|
1337
1511
|
if (domainHash != null) 'domainHash': domainHash,
|
|
1338
1512
|
if (attestations != null) 'attestations': attestations,
|
|
1513
|
+
if (nomosConflictSnapshots != null)
|
|
1514
|
+
'__nomosConflictSnapshots': nomosConflictSnapshots,
|
|
1339
1515
|
if (force != null) 'force': force,
|
|
1340
1516
|
if (session != null) 'session': session,
|
|
1341
1517
|
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
@@ -1347,7 +1523,8 @@ class NomosBridge {
|
|
|
1347
1523
|
/// inside the sealed intent (callers never supply one); the runner hands back what was minted, so a
|
|
1348
1524
|
/// generated create method returns the typed id with NO query-after round-trip. [created] is empty for
|
|
1349
1525
|
/// pure mutations.
|
|
1350
|
-
@Deprecated(
|
|
1526
|
+
@Deprecated(
|
|
1527
|
+
'Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
1351
1528
|
'dispatch through a generated <Domain>Client bound to a NomosSession. '
|
|
1352
1529
|
'Retained for CLIs/admin tooling/generated internals only.')
|
|
1353
1530
|
Future<NomosOfferOutcome> offerCreates({
|
|
@@ -1357,6 +1534,7 @@ class NomosBridge {
|
|
|
1357
1534
|
String? actor,
|
|
1358
1535
|
String? domainHash,
|
|
1359
1536
|
List<Map<String, Object?>>? attestations,
|
|
1537
|
+
List<Map<String, Object?>>? nomosConflictSnapshots,
|
|
1360
1538
|
bool? force,
|
|
1361
1539
|
String? session,
|
|
1362
1540
|
String? authorSecret,
|
|
@@ -1368,6 +1546,7 @@ class NomosBridge {
|
|
|
1368
1546
|
actor: actor,
|
|
1369
1547
|
domainHash: domainHash,
|
|
1370
1548
|
attestations: attestations,
|
|
1549
|
+
nomosConflictSnapshots: nomosConflictSnapshots,
|
|
1371
1550
|
force: force,
|
|
1372
1551
|
session: session,
|
|
1373
1552
|
authorSecret: authorSecret,
|
|
@@ -1385,6 +1564,7 @@ class NomosBridge {
|
|
|
1385
1564
|
String? actor,
|
|
1386
1565
|
String? domainHash,
|
|
1387
1566
|
List<Map<String, Object?>>? attestations,
|
|
1567
|
+
List<Map<String, Object?>>? nomosConflictSnapshots,
|
|
1388
1568
|
bool? force,
|
|
1389
1569
|
String? session,
|
|
1390
1570
|
// Per-offer signing key (base64 device secret): sign THIS offer with a specific device key instead
|
|
@@ -1407,6 +1587,8 @@ class NomosBridge {
|
|
|
1407
1587
|
if (actor != null) 'actor': actor,
|
|
1408
1588
|
if (domainHash != null) 'domainHash': domainHash,
|
|
1409
1589
|
if (attestations != null) 'attestations': attestations,
|
|
1590
|
+
if (nomosConflictSnapshots != null)
|
|
1591
|
+
'__nomosConflictSnapshots': nomosConflictSnapshots,
|
|
1410
1592
|
if (force != null) 'force': force,
|
|
1411
1593
|
if (session != null) 'session': session,
|
|
1412
1594
|
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
@@ -1424,12 +1606,19 @@ class NomosBridge {
|
|
|
1424
1606
|
final x = v.toInt();
|
|
1425
1607
|
return x < t0 ? t0 : (x > t1 ? t1 : x);
|
|
1426
1608
|
}
|
|
1609
|
+
|
|
1427
1610
|
noteLocalSpan(
|
|
1428
|
-
track: 'submit',
|
|
1429
|
-
|
|
1611
|
+
track: 'submit',
|
|
1612
|
+
name: 'offer: bridge ipc (up-leg)',
|
|
1613
|
+
kind: 'bridge-ipc',
|
|
1614
|
+
t0: t0,
|
|
1615
|
+
t1: within(recv));
|
|
1430
1616
|
noteLocalSpan(
|
|
1431
|
-
track: 'submit',
|
|
1432
|
-
|
|
1617
|
+
track: 'submit',
|
|
1618
|
+
name: 'offer: response delivery (down-leg)',
|
|
1619
|
+
kind: 'response-delivery',
|
|
1620
|
+
t0: within(done),
|
|
1621
|
+
t1: t1);
|
|
1433
1622
|
}
|
|
1434
1623
|
}
|
|
1435
1624
|
final births = ((r['birthOutcomes'] as List?) ?? const [])
|
|
@@ -1451,8 +1640,8 @@ class NomosBridge {
|
|
|
1451
1640
|
final bornRaw = (r['born'] as List?);
|
|
1452
1641
|
final bornNames = <String>[
|
|
1453
1642
|
if (bornRaw != null)
|
|
1454
|
-
...bornRaw
|
|
1455
|
-
(e is Map ? (e['workspace'] ?? '') : e).toString())
|
|
1643
|
+
...bornRaw
|
|
1644
|
+
.map((e) => (e is Map ? (e['workspace'] ?? '') : e).toString())
|
|
1456
1645
|
else
|
|
1457
1646
|
...births
|
|
1458
1647
|
.map((m) => (m['workspace'] ?? '').toString())
|
|
@@ -1487,15 +1676,20 @@ class NomosBridge {
|
|
|
1487
1676
|
/// originates in the sealed kernel, so the bridge (not the kernel) owns the ergonomics.
|
|
1488
1677
|
/// Anything that isn't that refusal passes through unchanged.
|
|
1489
1678
|
Future<Object> _wrapDomainNotInstalled(Object e,
|
|
1490
|
-
{required String domain,
|
|
1491
|
-
|
|
1679
|
+
{required String domain,
|
|
1680
|
+
String? domainHash,
|
|
1681
|
+
String? requestedWorkspace}) async {
|
|
1682
|
+
if (!RegExp(r"no active installed law declares domain '")
|
|
1683
|
+
.hasMatch(e.toString())) return e;
|
|
1492
1684
|
final self = this;
|
|
1493
1685
|
final sessionWs = self is NomosSession ? self.workspace : null;
|
|
1494
1686
|
var activeKeys = const <String>[];
|
|
1495
1687
|
try {
|
|
1496
1688
|
final offer = await interfaceOffer();
|
|
1497
1689
|
if (offer != null) activeKeys = offer.keys.toList()..sort();
|
|
1498
|
-
} catch (_) {
|
|
1690
|
+
} catch (_) {
|
|
1691
|
+
/* best-effort enrichment — the typed error still carries the context we have */
|
|
1692
|
+
}
|
|
1499
1693
|
return DomainNotInstalledForSession(
|
|
1500
1694
|
expectedDomainKey: domain,
|
|
1501
1695
|
expectedDomainHash: domainHash,
|
|
@@ -1512,7 +1706,8 @@ class NomosBridge {
|
|
|
1512
1706
|
/// installed package, read off the LOCAL projection (offline). Keys = the ACTIVE domain keys the
|
|
1513
1707
|
/// workspace serves. Null when no on-ledger interface is observed yet (pre-#72e law).
|
|
1514
1708
|
Future<Map<String, Object?>?> interfaceOffer({String? session}) async {
|
|
1515
|
-
final r = _asMap(await _call(
|
|
1709
|
+
final r = _asMap(await _call(
|
|
1710
|
+
'interfaceOffer', {if (session != null) 'session': session}));
|
|
1516
1711
|
return (r['offer'] as Map?)?.cast<String, Object?>();
|
|
1517
1712
|
}
|
|
1518
1713
|
|
|
@@ -1525,7 +1720,11 @@ class NomosBridge {
|
|
|
1525
1720
|
/// [authorSecret] — the device signing key for the followed session's writes (era-0: an attested-born
|
|
1526
1721
|
/// child is WARRANTED; write access needs its owner's enrolled device key).
|
|
1527
1722
|
Future<NomosSession> follow(NomosRef ref,
|
|
1528
|
-
{String? cloud,
|
|
1723
|
+
{String? cloud,
|
|
1724
|
+
String? clientId,
|
|
1725
|
+
String? authToken,
|
|
1726
|
+
bool allowUnborn = false,
|
|
1727
|
+
String? authorSecret}) async {
|
|
1529
1728
|
final ws = ref.workspace;
|
|
1530
1729
|
if (ws.isEmpty) {
|
|
1531
1730
|
throw ArgumentError.value(ref, 'ref',
|
|
@@ -1580,7 +1779,11 @@ class NomosBridge {
|
|
|
1580
1779
|
// a pre-connect plane has no registry yet — fall through to the credentials decision below.
|
|
1581
1780
|
}
|
|
1582
1781
|
if (resident) {
|
|
1583
|
-
await connect(
|
|
1782
|
+
await connect(
|
|
1783
|
+
workspace: workspace,
|
|
1784
|
+
session: workspace,
|
|
1785
|
+
allowUnborn: true,
|
|
1786
|
+
authorSecret: authorSecret);
|
|
1584
1787
|
return session(workspace);
|
|
1585
1788
|
}
|
|
1586
1789
|
if (ifAbsentCredentials == null) {
|
|
@@ -1621,7 +1824,8 @@ class NomosBridge {
|
|
|
1621
1824
|
/// OFFER AN INTENT — the write verb. Plan a directive (domain/directiveId/payload) as the verified [actor];
|
|
1622
1825
|
/// the holon encodes the v2 offer envelope via the single codec and admits it. Returns the new local head.
|
|
1623
1826
|
/// (This is the canonical name; [offerDirective] is the legacy alias the generated clients still call.)
|
|
1624
|
-
@Deprecated(
|
|
1827
|
+
@Deprecated(
|
|
1828
|
+
'Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
1625
1829
|
'dispatch through a generated <Domain>Client bound to a NomosSession. '
|
|
1626
1830
|
'Retained for CLIs/admin tooling/generated internals only.')
|
|
1627
1831
|
Future<String> offerIntent({
|
|
@@ -1631,6 +1835,7 @@ class NomosBridge {
|
|
|
1631
1835
|
String? actor,
|
|
1632
1836
|
String? domainHash,
|
|
1633
1837
|
List<Map<String, Object?>>? attestations,
|
|
1838
|
+
List<Map<String, Object?>>? nomosConflictSnapshots,
|
|
1634
1839
|
bool? force,
|
|
1635
1840
|
String? session,
|
|
1636
1841
|
}) =>
|
|
@@ -1641,6 +1846,7 @@ class NomosBridge {
|
|
|
1641
1846
|
actor: actor,
|
|
1642
1847
|
domainHash: domainHash,
|
|
1643
1848
|
attestations: attestations,
|
|
1849
|
+
nomosConflictSnapshots: nomosConflictSnapshots,
|
|
1644
1850
|
force: force,
|
|
1645
1851
|
session: session);
|
|
1646
1852
|
|
|
@@ -1698,7 +1904,8 @@ class NomosBridge {
|
|
|
1698
1904
|
/// `bridge.createWorkspace(domain: 'co2_platform', directiveId: 'birthEstateWorkspace', domainHash: ...)`.
|
|
1699
1905
|
/// The kernel looks up `(domain, directiveId)` in the ACTIVE installed law — [domain] must name the law
|
|
1700
1906
|
/// that actually declares [directiveId], never assume `'workspaces'`.
|
|
1701
|
-
@Deprecated(
|
|
1907
|
+
@Deprecated(
|
|
1908
|
+
'Engine-level generic writes with a loose parent string are the footgun '
|
|
1702
1909
|
'(session_bound_clients.md asks 1/4/7): births go through the generated <Domain>Client '
|
|
1703
1910
|
'bound to the PARENT NomosSession (its birth method infers parent = the session workspace). '
|
|
1704
1911
|
'Retained for CLIs/admin tooling/generated compatibility shims only.')
|
|
@@ -1807,6 +2014,59 @@ class NomosBridge {
|
|
|
1807
2014
|
'directiveId': directiveId,
|
|
1808
2015
|
}));
|
|
1809
2016
|
|
|
2017
|
+
/// Framework-private business correction seam. Generated clients bind the domain, its actions and actor;
|
|
2018
|
+
/// application code supplies only the correction kind and its mandatory reason.
|
|
2019
|
+
Future<Map<String, Object?>> correctMostRecent({
|
|
2020
|
+
required String kind,
|
|
2021
|
+
required String domain,
|
|
2022
|
+
required List<String> actions,
|
|
2023
|
+
required String reason,
|
|
2024
|
+
String? domainHash,
|
|
2025
|
+
String? actor,
|
|
2026
|
+
}) async =>
|
|
2027
|
+
_asMap(await _call('correctMostRecent', {
|
|
2028
|
+
'kind': kind,
|
|
2029
|
+
'domain': domain,
|
|
2030
|
+
'actions': actions,
|
|
2031
|
+
'reason': reason,
|
|
2032
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
2033
|
+
if (actor != null) 'actor': actor,
|
|
2034
|
+
}));
|
|
2035
|
+
|
|
2036
|
+
/// Framework-private exact-history adoption seam. Generated clients expose this as
|
|
2037
|
+
/// `model.adopt()` and, only when prior business outcomes differ, a chained
|
|
2038
|
+
/// `decision.accept(reason)`. Candidate identities, heads and receipts stay here.
|
|
2039
|
+
Future<Map<String, Object?>> nomosFrameworkAdoptGeneratedModel({
|
|
2040
|
+
required String domain,
|
|
2041
|
+
required String candidateHash,
|
|
2042
|
+
String? because,
|
|
2043
|
+
}) async {
|
|
2044
|
+
final r = _asMap(await _call('adoptGeneratedModel', {
|
|
2045
|
+
'domain': domain,
|
|
2046
|
+
'candidateHash': candidateHash,
|
|
2047
|
+
if (because != null) 'because': because,
|
|
2048
|
+
}));
|
|
2049
|
+
return ((r['verdict'] as Map?) ?? const <String, Object?>{})
|
|
2050
|
+
.cast<String, Object?>();
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/// Framework-private exact-history restoration seam. Generated clients expose
|
|
2054
|
+
/// `model.restorePrevious()` and a chained `decision.accept(reason)`; predecessor
|
|
2055
|
+
/// identity, derived dispositions, heads and receipts stay below this bridge.
|
|
2056
|
+
Future<Map<String, Object?>> nomosFrameworkRestorePreviousGeneratedModel({
|
|
2057
|
+
required String domain,
|
|
2058
|
+
required String currentHash,
|
|
2059
|
+
String? because,
|
|
2060
|
+
}) async {
|
|
2061
|
+
final r = _asMap(await _call('restorePreviousGeneratedModel', {
|
|
2062
|
+
'domain': domain,
|
|
2063
|
+
'currentHash': currentHash,
|
|
2064
|
+
if (because != null) 'because': because,
|
|
2065
|
+
}));
|
|
2066
|
+
return ((r['verdict'] as Map?) ?? const <String, Object?>{})
|
|
2067
|
+
.cast<String, Object?>();
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1810
2070
|
/// HISTORY of one aggregate (a todo) — the full action timeline from the ledger: every intent that touched
|
|
1811
2071
|
/// it + the strikes that retract them (undo/restore/redo), oldest→newest. Each entry:
|
|
1812
2072
|
/// `{id, who, mine, at(ms), action, fields[], done?, text?, removed?, marker, isStrike, targetAction, struck}`.
|
|
@@ -1820,8 +2080,9 @@ class NomosBridge {
|
|
|
1820
2080
|
}
|
|
1821
2081
|
|
|
1822
2082
|
/// RUNTIME activity feed — per-committed-intent rows (newest-first) from the local ledger, each carrying
|
|
1823
|
-
/// `{intentId, domain, directiveId, actorSubject, occurredAt, payload}` (payload = the
|
|
1824
|
-
/// payload
|
|
2083
|
+
/// `{intentId, domain, directiveId, actorSubject, occurredAt, payload, created, affected}` (payload = the
|
|
2084
|
+
/// DECODED directive payload; affected = the aggregate refs its sealed events touched). `base` = a commit-
|
|
2085
|
+
/// oid cursor (only intents newer than it); omitted = from the tip bounded by
|
|
1825
2086
|
/// `limit` (default 200). Generic raw rows — the typed shaping is the codegen `renderActivity`.
|
|
1826
2087
|
Future<List<Map<String, dynamic>>> activityLog(
|
|
1827
2088
|
{String? base, int? limit}) async {
|
|
@@ -1834,6 +2095,15 @@ class NomosBridge {
|
|
|
1834
2095
|
.toList();
|
|
1835
2096
|
}
|
|
1836
2097
|
|
|
2098
|
+
/// Framework-private organisational read used by generated `describe()`. The runner joins the parent
|
|
2099
|
+
/// ledger's verified birth witnesses and lifecycle facts; tenant code only receives safe child links.
|
|
2100
|
+
Future<List<Map<String, dynamic>>> nomosFrameworkChildWorkspaces() async {
|
|
2101
|
+
final r = _asMap(await _call('nomosChildWorkspaces'));
|
|
2102
|
+
return ((r['children'] as List?) ?? const [])
|
|
2103
|
+
.map((e) => (e as Map).cast<String, dynamic>())
|
|
2104
|
+
.toList();
|
|
2105
|
+
}
|
|
2106
|
+
|
|
1837
2107
|
/// Watch the RUNTIME activity feed: a stream that re-emits the newest-first activity rows on every local
|
|
1838
2108
|
/// change (doorbell/sync tick). `base` = a commit-oid cursor. Cancelling drops the watch on the runner.
|
|
1839
2109
|
Stream<List<dynamic>> watchActivity({String? base, String? session}) =>
|
|
@@ -1897,7 +2167,8 @@ class NomosBridge {
|
|
|
1897
2167
|
/// (the secret never leaves the device). The recovery lanes use this when only the secret was
|
|
1898
2168
|
/// persisted (the device-key store keeps just the seed), including low-level device-bound enrollment.
|
|
1899
2169
|
Future<String> devicePublicOf(String secret) async =>
|
|
1900
|
-
(_asMap(await _call('devicePublicOf', {'secret': secret}))['public'] ??
|
|
2170
|
+
(_asMap(await _call('devicePublicOf', {'secret': secret}))['public'] ??
|
|
2171
|
+
'')
|
|
1901
2172
|
.toString();
|
|
1902
2173
|
|
|
1903
2174
|
/// PARENT-AUTHORITY POSTURE (read-only diagnostic): is this device key a LIVE `signer` for
|
|
@@ -1986,8 +2257,9 @@ class NomosBridge {
|
|
|
1986
2257
|
/// kernel `cert_sign` op (the single source of the byte layout), OFFER the PARAMETERIZED parent's `birthHome`
|
|
1987
2258
|
/// directive carrying the homeCert, poll the parent for the born home, and CONFIRM the born home's own
|
|
1988
2259
|
/// `verify_chain` is GREEN (the offline peer-verify — replay IS validation). `spec` MUST carry `parent` (the
|
|
1989
|
-
/// tenant's platform whose governance includes `birthHome`)
|
|
1990
|
-
///
|
|
2260
|
+
/// tenant's platform whose governance includes `birthHome`) and `principal`; the client discovers the
|
|
2261
|
+
/// private framework route and active law hashes from that application's live contract. `subject`/
|
|
2262
|
+
/// `authToken`/`directiveId` are optional. OURS is the ceremony; the parent is the TENANT's.
|
|
1991
2263
|
/// Returns `{home, device:{secret,public}, homeCert, born, verify:{valid,…}}`.
|
|
1992
2264
|
Future<Map<String, Object?>> birthHome(Map<String, Object?> spec) async =>
|
|
1993
2265
|
_asMap(await _call('birthHome', spec));
|
|
@@ -2045,19 +2317,84 @@ class NomosBridge {
|
|
|
2045
2317
|
|
|
2046
2318
|
/// A single aggregate by id (the by-id read). `keys`/`principal`: see [query].
|
|
2047
2319
|
Future<List<Map<String, dynamic>>> queryById(String id,
|
|
2048
|
-
[List<NomosScopeKey>? keys,
|
|
2320
|
+
[List<NomosScopeKey>? keys,
|
|
2321
|
+
String? principal,
|
|
2322
|
+
String? session]) async =>
|
|
2049
2323
|
_asRows(await _call('queryById', {
|
|
2050
2324
|
'id': id,
|
|
2051
2325
|
...?_readAuthArgs(keys, principal),
|
|
2052
2326
|
if (session != null) 'session': session,
|
|
2053
2327
|
}));
|
|
2054
2328
|
|
|
2329
|
+
/// Verified points in this workspace's history, oldest to newest. The generated client presents these
|
|
2330
|
+
/// as `client.past.moments()`; callers pass the opaque [NomosMoment] back to `past.at(moment)` and never
|
|
2331
|
+
/// need to know that the framework uses commit identities underneath.
|
|
2332
|
+
Future<List<NomosMoment>> moments({int limit = 100, String? session}) async {
|
|
2333
|
+
final r = _asMap(await _call('commits', {
|
|
2334
|
+
'limit': limit,
|
|
2335
|
+
if (session != null) 'session': session,
|
|
2336
|
+
}));
|
|
2337
|
+
return ((r['commits'] as List?) ?? const [])
|
|
2338
|
+
.map((e) => NomosMoment.fromJson((e as Map).cast<String, Object?>()))
|
|
2339
|
+
.toList();
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
/// Run one declared business query against the verified world at [moment], without moving the live world.
|
|
2343
|
+
/// `keys`/`principal` have exactly the same private/encrypted-read semantics as [query].
|
|
2344
|
+
Future<List<Map<String, dynamic>>> queryAt(NomosMoment moment, String queryId,
|
|
2345
|
+
[Map<String, Object?> params = const {},
|
|
2346
|
+
List<NomosScopeKey>? keys,
|
|
2347
|
+
String? principal,
|
|
2348
|
+
String? session]) async =>
|
|
2349
|
+
_asRows(await _call('queryAt', {
|
|
2350
|
+
'commit': moment.id,
|
|
2351
|
+
'queryId': queryId,
|
|
2352
|
+
'params': params,
|
|
2353
|
+
...?_readAuthArgs(keys, principal),
|
|
2354
|
+
if (session != null) 'session': session,
|
|
2355
|
+
}));
|
|
2356
|
+
|
|
2357
|
+
/// Read one aggregate from the verified world at [moment]. `keys`/`principal`: see [queryAt].
|
|
2358
|
+
Future<List<Map<String, dynamic>>> queryByIdAt(NomosMoment moment, String id,
|
|
2359
|
+
[List<NomosScopeKey>? keys,
|
|
2360
|
+
String? principal,
|
|
2361
|
+
String? session]) async =>
|
|
2362
|
+
_asRows(await _call('queryByIdAt', {
|
|
2363
|
+
'commit': moment.id,
|
|
2364
|
+
'id': id,
|
|
2365
|
+
...?_readAuthArgs(keys, principal),
|
|
2366
|
+
if (session != null) 'session': session,
|
|
2367
|
+
}));
|
|
2368
|
+
|
|
2369
|
+
/// Maintained historic count/sum values at [moment]. These are the temporal counterparts of [count]/[sum].
|
|
2370
|
+
Future<int> countAt(NomosMoment moment, String countId,
|
|
2371
|
+
[String group = '', String? session]) async =>
|
|
2372
|
+
((_asMap(await _call('countAt', {
|
|
2373
|
+
'commit': moment.id,
|
|
2374
|
+
'countId': countId,
|
|
2375
|
+
'group': group,
|
|
2376
|
+
if (session != null) 'session': session,
|
|
2377
|
+
}))['count']) as num)
|
|
2378
|
+
.toInt();
|
|
2379
|
+
|
|
2380
|
+
Future<int> sumAt(NomosMoment moment, String sumId,
|
|
2381
|
+
[String group = '', String? session]) async =>
|
|
2382
|
+
((_asMap(await _call('sumAt', {
|
|
2383
|
+
'commit': moment.id,
|
|
2384
|
+
'sumId': sumId,
|
|
2385
|
+
'group': group,
|
|
2386
|
+
if (session != null) 'session': session,
|
|
2387
|
+
}))['sum']) as num)
|
|
2388
|
+
.toInt();
|
|
2389
|
+
|
|
2055
2390
|
/// The read auth sidecar (private reads + E2E): `keys`/`principal` as top-level call args, or null when
|
|
2056
2391
|
/// neither is present (so the wire is byte-identical to before this existed).
|
|
2057
|
-
Map<String, Object?>? _readAuthArgs(
|
|
2392
|
+
Map<String, Object?>? _readAuthArgs(
|
|
2393
|
+
List<NomosScopeKey>? keys, String? principal) {
|
|
2058
2394
|
final out = <String, Object?>{};
|
|
2059
2395
|
if (principal != null && principal.isNotEmpty) out['principal'] = principal;
|
|
2060
|
-
if (keys != null && keys.isNotEmpty)
|
|
2396
|
+
if (keys != null && keys.isNotEmpty)
|
|
2397
|
+
out['keys'] = keys.map((k) => k.toJson()).toList();
|
|
2061
2398
|
return out.isEmpty ? null : out;
|
|
2062
2399
|
}
|
|
2063
2400
|
|
|
@@ -2078,15 +2415,127 @@ class NomosBridge {
|
|
|
2078
2415
|
required String fieldSid,
|
|
2079
2416
|
required String plaintext}) async =>
|
|
2080
2417
|
(_asMap(await _call('cryptoSealField', {
|
|
2081
|
-
|
|
2418
|
+
'scopeKey': scopeKey,
|
|
2419
|
+
'scope': scope,
|
|
2420
|
+
'epoch': epoch,
|
|
2421
|
+
'aggSid': aggSid,
|
|
2422
|
+
'fieldSid': fieldSid,
|
|
2423
|
+
'plaintext': plaintext,
|
|
2424
|
+
}))['envelope'] ??
|
|
2425
|
+
'')
|
|
2426
|
+
.toString();
|
|
2427
|
+
|
|
2428
|
+
/// FRAMEWORK-INTERNAL `.owned()` key lane. The connected JS holon derives epoch 0 from its
|
|
2429
|
+
/// already-persisted device credential; generated clients call this without exposing key custody to apps.
|
|
2430
|
+
Future<NomosScopeKey> nomosOwnedScopeKey(String scope,
|
|
2431
|
+
{String? recordType, String? recordId}) async {
|
|
2432
|
+
final out = _asMap(await _call('cryptoOwnedScopeKey', {
|
|
2433
|
+
'scope': scope,
|
|
2434
|
+
if (recordType != null && recordId != null)
|
|
2435
|
+
'record': {'type': recordType, 'id': recordId},
|
|
2436
|
+
}));
|
|
2437
|
+
return NomosScopeKey(
|
|
2438
|
+
scope: (out['scope'] ?? scope).toString(),
|
|
2439
|
+
epoch: (out['epoch'] as num?)?.toInt() ?? 0,
|
|
2440
|
+
key: (out['key'] ?? '').toString(),
|
|
2441
|
+
ownedKeySubject: out['ownedKeySubject']?.toString(),
|
|
2442
|
+
ownedCustody: out['ownedCustody']?.toString(),
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
/// Every key epoch this principal currently holds for one owned record. Generated reads use the
|
|
2447
|
+
/// complete set so history remains readable after rotations, shares and transfers.
|
|
2448
|
+
Future<List<NomosScopeKey>> nomosOwnedScopeKeys(String scope,
|
|
2449
|
+
{required String recordType, required String recordId}) async {
|
|
2450
|
+
final out = _asMap(await _call('cryptoOwnedScopeKeys', {
|
|
2451
|
+
'scope': scope,
|
|
2452
|
+
'record': {'type': recordType, 'id': recordId},
|
|
2453
|
+
}));
|
|
2454
|
+
return (out['keys'] as List? ?? const [])
|
|
2455
|
+
.map((raw) => _asMap(raw))
|
|
2456
|
+
.map((key) => NomosScopeKey(
|
|
2457
|
+
scope: (key['scope'] ?? scope).toString(),
|
|
2458
|
+
epoch: (key['epoch'] as num?)?.toInt() ?? 0,
|
|
2459
|
+
key: (key['key'] ?? '').toString(),
|
|
2460
|
+
ownedKeySubject: key['ownedKeySubject']?.toString(),
|
|
2461
|
+
ownedCustody: key['ownedCustody']?.toString(),
|
|
2462
|
+
))
|
|
2463
|
+
.toList();
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
/// Make this signed-in principal ready to receive record shares. Generated clients call this while
|
|
2467
|
+
/// binding; tenant code never handles a key, certificate or identity tuple.
|
|
2468
|
+
Future<void> nomosEnsureOwnedIdentity(
|
|
2469
|
+
{required String scope,
|
|
2470
|
+
required String domain,
|
|
2471
|
+
String? domainHash}) async {
|
|
2472
|
+
await _call('cryptoOwnedEnsureIdentity', {
|
|
2473
|
+
'scope': scope,
|
|
2474
|
+
'domain': domain,
|
|
2475
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
/// Opt into designated-peer all-device-loss recovery. All protocol fields remain in the JS/kernel client.
|
|
2480
|
+
Future<Map<String, Object?>> nomosProtectOwnedIdentity({
|
|
2481
|
+
required String scope,
|
|
2482
|
+
required String domain,
|
|
2483
|
+
required String domainHash,
|
|
2484
|
+
required String recoveryPrincipal,
|
|
2485
|
+
}) async =>
|
|
2486
|
+
_asMap(await _call('cryptoOwnedProtectIdentity', {
|
|
2082
2487
|
'scope': scope,
|
|
2083
|
-
'
|
|
2084
|
-
'
|
|
2085
|
-
'
|
|
2086
|
-
|
|
2087
|
-
|
|
2488
|
+
'domain': domain,
|
|
2489
|
+
'domainHash': domainHash,
|
|
2490
|
+
'recoveryPrincipal': recoveryPrincipal,
|
|
2491
|
+
}));
|
|
2492
|
+
|
|
2493
|
+
/// Prepare the private framework payload behind generated share/unshare/transfer methods.
|
|
2494
|
+
Future<Map<String, Object?>> nomosPrepareOwnedRecordAction(
|
|
2495
|
+
{required String action,
|
|
2496
|
+
required String scope,
|
|
2497
|
+
required NomosRef record,
|
|
2498
|
+
required String peer}) async =>
|
|
2499
|
+
_asMap(await _call('cryptoOwnedPrepareRecordAction', {
|
|
2500
|
+
'action': action,
|
|
2501
|
+
'scope': scope,
|
|
2502
|
+
'record': {'type': record.type, 'id': record.id},
|
|
2503
|
+
'peer': peer,
|
|
2504
|
+
}));
|
|
2505
|
+
|
|
2506
|
+
/// FRAMEWORK-INTERNAL business birth: derive the unborn child's owned key from its deterministic address.
|
|
2507
|
+
Future<NomosScopeKey> nomosOwnedBirthScopeKey(
|
|
2508
|
+
String scope, String namespace, Object? key) async {
|
|
2509
|
+
final out = _asMap(await _call('cryptoOwnedBirthScopeKey', {
|
|
2510
|
+
'scope': scope,
|
|
2511
|
+
'namespace': namespace,
|
|
2512
|
+
'key': key,
|
|
2513
|
+
}));
|
|
2514
|
+
return NomosScopeKey(
|
|
2515
|
+
scope: (out['scope'] ?? scope).toString(),
|
|
2516
|
+
epoch: (out['epoch'] as num?)?.toInt() ?? 0,
|
|
2517
|
+
key: (out['key'] ?? '').toString(),
|
|
2518
|
+
ownedKeySubject: out['ownedKeySubject']?.toString(),
|
|
2519
|
+
ownedCustody: out['ownedCustody']?.toString(),
|
|
2520
|
+
workspace: out['workspace']?.toString(),
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
/// FRAMEWORK-INTERNAL: issue an opaque request for this session's current device.
|
|
2525
|
+
Future<String> nomosOwnedDeviceRequest(String scope) async =>
|
|
2526
|
+
(_asMap(await _call('cryptoOwnedDeviceRequest', {'scope': scope}))[
|
|
2527
|
+
'request'] ??
|
|
2528
|
+
'')
|
|
2088
2529
|
.toString();
|
|
2089
2530
|
|
|
2531
|
+
/// FRAMEWORK-INTERNAL: validate an opaque request and prepare the atomic signer + custody offer.
|
|
2532
|
+
Future<Map<String, Object?>> nomosPrepareOwnedDeviceApproval(
|
|
2533
|
+
String request, String scope) async =>
|
|
2534
|
+
_asMap(await _call('cryptoOwnedPrepareApproval', {
|
|
2535
|
+
'request': request,
|
|
2536
|
+
'scope': scope,
|
|
2537
|
+
}));
|
|
2538
|
+
|
|
2090
2539
|
/// Wrap a scope key TO a recipient's public key (committer side): `{hpkeEpk, ct}` base64 ciphertext.
|
|
2091
2540
|
Future<Map<String, Object?>> cryptoWrapKey(
|
|
2092
2541
|
{required String recipientPub, required String scopeKey}) async =>
|
|
@@ -2098,8 +2547,11 @@ class NomosBridge {
|
|
|
2098
2547
|
{required String secret,
|
|
2099
2548
|
required String hpkeEpk,
|
|
2100
2549
|
required String ct}) async =>
|
|
2101
|
-
(_asMap(await _call('cryptoUnwrapKey',
|
|
2102
|
-
|
|
2550
|
+
(_asMap(await _call('cryptoUnwrapKey', {
|
|
2551
|
+
'secret': secret,
|
|
2552
|
+
'hpkeEpk': hpkeEpk,
|
|
2553
|
+
'ct': ct
|
|
2554
|
+
}))['scopeKey'] ??
|
|
2103
2555
|
'')
|
|
2104
2556
|
.toString();
|
|
2105
2557
|
|
|
@@ -2115,7 +2567,8 @@ class NomosBridge {
|
|
|
2115
2567
|
}));
|
|
2116
2568
|
|
|
2117
2569
|
/// A maintained O(1) count / sum.
|
|
2118
|
-
Future<int> count(String countId,
|
|
2570
|
+
Future<int> count(String countId,
|
|
2571
|
+
[String group = '', String? session]) async =>
|
|
2119
2572
|
((_asMap(await _call('count', {
|
|
2120
2573
|
'countId': countId,
|
|
2121
2574
|
'group': group,
|
|
@@ -2137,13 +2590,31 @@ class NomosBridge {
|
|
|
2137
2590
|
/// DERIVED, NOT DRAINED: a resolution write folding in removes its row by derivation.
|
|
2138
2591
|
/// Empty for a Surfaced-free workspace. (The generated typed client wraps these rows as
|
|
2139
2592
|
/// `PendingConflictRow` and pairs them with the designated `resolve…` helpers.)
|
|
2140
|
-
Future<List<Map<String, dynamic>>> pendingConflicts(
|
|
2593
|
+
Future<List<Map<String, dynamic>>> pendingConflicts(
|
|
2594
|
+
[String? session]) async =>
|
|
2595
|
+
_asRows(await _call('pendingConflicts', {
|
|
2596
|
+
if (session != null) 'session': session,
|
|
2597
|
+
}));
|
|
2598
|
+
|
|
2599
|
+
/// Framework-filtered pending conflicts for one generated aggregate type. This is the
|
|
2600
|
+
/// primitive generated clients use; application code never filters the heterogeneous queue.
|
|
2601
|
+
Future<List<Map<String, dynamic>>> pendingConflictsFor(String aggregateType,
|
|
2602
|
+
[String? session]) async =>
|
|
2141
2603
|
_asRows(await _call('pendingConflicts', {
|
|
2604
|
+
'aggregateType': aggregateType,
|
|
2605
|
+
if (session != null) 'session': session,
|
|
2606
|
+
}));
|
|
2607
|
+
|
|
2608
|
+
/// Counts-only workspace summary over the maintained conflict index.
|
|
2609
|
+
Future<Map<String, Object?>> pendingConflictSummary(
|
|
2610
|
+
[String? session]) async =>
|
|
2611
|
+
_asMap(await _call('pendingConflictSummary', {
|
|
2142
2612
|
if (session != null) 'session': session,
|
|
2143
2613
|
}));
|
|
2144
2614
|
|
|
2145
2615
|
/// Push local writes → the session branch → cloud admission, and pull canonical main back.
|
|
2146
|
-
Future<Map<String, Object?>> sync(
|
|
2616
|
+
Future<Map<String, Object?>> sync(
|
|
2617
|
+
{bool admit = true, String? session}) async =>
|
|
2147
2618
|
_asMap(await _call('sync', {
|
|
2148
2619
|
'admit': admit,
|
|
2149
2620
|
if (session != null) 'session': session,
|
|
@@ -2198,7 +2669,8 @@ class NomosBridge {
|
|
|
2198
2669
|
void _startStatusPoll() {
|
|
2199
2670
|
_pushStatus(_status); // seed the new listener with the current snapshot
|
|
2200
2671
|
_pollMetricsOnce();
|
|
2201
|
-
_metricsTimer ??=
|
|
2672
|
+
_metricsTimer ??=
|
|
2673
|
+
Timer.periodic(const Duration(seconds: 4), (_) => _pollMetricsOnce());
|
|
2202
2674
|
}
|
|
2203
2675
|
|
|
2204
2676
|
void _stopStatusPoll() {
|
|
@@ -2209,19 +2681,28 @@ class NomosBridge {
|
|
|
2209
2681
|
Future<void> _pollMetricsOnce() async {
|
|
2210
2682
|
try {
|
|
2211
2683
|
final m = await metrics();
|
|
2212
|
-
int? n(String k) =>
|
|
2684
|
+
int? n(String k) =>
|
|
2685
|
+
m.containsKey(k) ? ((m[k] as num?)?.toInt() ?? 0) : null;
|
|
2213
2686
|
final ahead = n('ahead') ?? _status.ahead;
|
|
2214
2687
|
final behind = n('behind') ?? _status.behind;
|
|
2215
2688
|
// Don't stomp an in-flight transfer's phase; otherwise derive idle/behind/syncing from the counts.
|
|
2216
2689
|
final phase = _status.transfer != null
|
|
2217
2690
|
? _status.phase
|
|
2218
|
-
: (behind > 0
|
|
2691
|
+
: (behind > 0
|
|
2692
|
+
? SyncPhase.behind
|
|
2693
|
+
: (ahead > 0 ? SyncPhase.syncing : SyncPhase.idle));
|
|
2219
2694
|
_pushStatus(_status.copyWith(
|
|
2220
|
-
ahead: ahead,
|
|
2221
|
-
|
|
2695
|
+
ahead: ahead,
|
|
2696
|
+
behind: behind,
|
|
2697
|
+
saved: n('saved'),
|
|
2698
|
+
onMain: n('onMain'),
|
|
2699
|
+
deadLetters: n('deadLetters'),
|
|
2700
|
+
phase: phase,
|
|
2701
|
+
clearError: true,
|
|
2222
2702
|
));
|
|
2223
2703
|
} catch (e) {
|
|
2224
|
-
_pushStatus(
|
|
2704
|
+
_pushStatus(
|
|
2705
|
+
_status.copyWith(phase: SyncPhase.error, lastError: e.toString()));
|
|
2225
2706
|
}
|
|
2226
2707
|
}
|
|
2227
2708
|
|
|
@@ -2237,8 +2718,7 @@ class NomosBridge {
|
|
|
2237
2718
|
|
|
2238
2719
|
/// LIVE per-layer spans of the local pipeline (begin/end as they happen): mutex wait, githolon author,
|
|
2239
2720
|
/// push, converge … The host renders in-progress bars so a hang is visible WHILE it blocks, not after.
|
|
2240
|
-
Stream<Map<String, Object?>> get spanEvents =>
|
|
2241
|
-
_eventPlane._spanEvents.stream;
|
|
2721
|
+
Stream<Map<String, Object?>> get spanEvents => _eventPlane._spanEvents.stream;
|
|
2242
2722
|
|
|
2243
2723
|
/// The buffered recent span events — replay these on first build to recover the BOOT flamepath
|
|
2244
2724
|
/// (fetch wasm / clone ledger / instantiate) that streamed during connect(), before the UI subscribed.
|
|
@@ -2262,8 +2742,21 @@ class NomosBridge {
|
|
|
2262
2742
|
final plane = _eventPlane;
|
|
2263
2743
|
final sid = id ?? 'local-${plane._localSpanSeq++}';
|
|
2264
2744
|
for (final m in <Map<String, Object?>>[
|
|
2265
|
-
{
|
|
2266
|
-
|
|
2745
|
+
{
|
|
2746
|
+
'event': 'span',
|
|
2747
|
+
'span': {
|
|
2748
|
+
'phase': 'begin',
|
|
2749
|
+
'id': sid,
|
|
2750
|
+
'track': track,
|
|
2751
|
+
'name': name,
|
|
2752
|
+
'kind': kind,
|
|
2753
|
+
't0': t0
|
|
2754
|
+
}
|
|
2755
|
+
},
|
|
2756
|
+
{
|
|
2757
|
+
'event': 'span',
|
|
2758
|
+
'span': {'phase': 'end', 'id': sid, 't1': t1}
|
|
2759
|
+
},
|
|
2267
2760
|
]) {
|
|
2268
2761
|
plane._spanBuffer.add(m);
|
|
2269
2762
|
if (plane._spanBuffer.length > 400) {
|
|
@@ -2283,7 +2776,8 @@ class NomosBridge {
|
|
|
2283
2776
|
/// them (a HEAD on the serving pack; the host answers Content-Length only, cached ~15s). Sum across your open
|
|
2284
2777
|
/// workspaces for an aggregate "X MB to sync" total BEFORE downloading. `{ bytes, kind:'delta'|'full'|
|
|
2285
2778
|
/// 'up-to-date' }`; `bytes` is null if the host omitted the length.
|
|
2286
|
-
Future<Map<String, Object?>> syncSize() async =>
|
|
2779
|
+
Future<Map<String, Object?>> syncSize() async =>
|
|
2780
|
+
_asMap(await _call('syncSize'));
|
|
2287
2781
|
|
|
2288
2782
|
/// Diagnostics — is the expected law installed/active/drift-free here? (The generated client surfaces it
|
|
2289
2783
|
/// so the app can answer "can I author this directive, and if not what deploy fixes it".)
|
|
@@ -2294,14 +2788,16 @@ class NomosBridge {
|
|
|
2294
2788
|
/// subscription tells the runner to drop the watch.
|
|
2295
2789
|
Stream<List<dynamic>> watch(String queryId,
|
|
2296
2790
|
[Map<String, Object?> params = const {}, String? session]) =>
|
|
2297
|
-
_watchStream(
|
|
2791
|
+
_watchStream(
|
|
2792
|
+
kind: 'query', queryId: queryId, params: params, session: session);
|
|
2298
2793
|
Stream<List<dynamic>> watchById(String id, [String? session]) =>
|
|
2299
2794
|
_watchStream(kind: 'byId', id: id, session: session);
|
|
2300
2795
|
|
|
2301
2796
|
/// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
|
|
2302
2797
|
Stream<List<dynamic>> watchSpatial(String spatialId, NomosBbox bbox,
|
|
2303
2798
|
[String? session]) =>
|
|
2304
|
-
_watchStream(
|
|
2799
|
+
_watchStream(
|
|
2800
|
+
kind: 'spatial', spatialId: spatialId, bbox: bbox, session: session);
|
|
2305
2801
|
|
|
2306
2802
|
/// Reactive PENDING-CONFLICTS read: re-emits the maintained Surfaced "decisions needed"
|
|
2307
2803
|
/// rows whenever the index changes (derived, not drained — a resolution write clears its row).
|
|
@@ -2331,7 +2827,8 @@ class NomosBridge {
|
|
|
2331
2827
|
'compat': compat,
|
|
2332
2828
|
if (requirement != null) 'requirement': requirement,
|
|
2333
2829
|
}));
|
|
2334
|
-
final v =
|
|
2830
|
+
final v =
|
|
2831
|
+
NomosCompat.fromJson((r['compat'] as Map?)?.cast<String, Object?>());
|
|
2335
2832
|
if (v != null) {
|
|
2336
2833
|
_eventPlane._lastCompat = v;
|
|
2337
2834
|
// a session's verdict is per-workspace state on the PLANE (so `session.compat` reads it back).
|
|
@@ -2566,14 +3063,23 @@ class NomosSession extends NomosBridge {
|
|
|
2566
3063
|
Future<NomosSession> acquireSession(String workspace,
|
|
2567
3064
|
{Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
|
|
2568
3065
|
String? authorSecret}) =>
|
|
2569
|
-
parent.acquireSession(workspace,
|
|
3066
|
+
parent.acquireSession(workspace,
|
|
3067
|
+
ifAbsentCredentials: ifAbsentCredentials, authorSecret: authorSecret);
|
|
2570
3068
|
|
|
2571
3069
|
/// A followed ref mounts on the SHARED plane — delegate to the parent (a session never owns mounts).
|
|
2572
3070
|
@override
|
|
2573
3071
|
Future<NomosSession> follow(NomosRef ref,
|
|
2574
|
-
{String? cloud,
|
|
3072
|
+
{String? cloud,
|
|
3073
|
+
String? clientId,
|
|
3074
|
+
String? authToken,
|
|
3075
|
+
bool allowUnborn = false,
|
|
3076
|
+
String? authorSecret}) =>
|
|
2575
3077
|
parent.follow(ref,
|
|
2576
|
-
cloud: cloud,
|
|
3078
|
+
cloud: cloud,
|
|
3079
|
+
clientId: clientId,
|
|
3080
|
+
authToken: authToken,
|
|
3081
|
+
allowUnborn: allowUnborn,
|
|
3082
|
+
authorSecret: authorSecret);
|
|
2577
3083
|
|
|
2578
3084
|
@override
|
|
2579
3085
|
Future<Object?> _call(String op, [Map<String, Object?> args = const {}]) =>
|
|
@@ -2599,7 +3105,11 @@ class NomosSession extends NomosBridge {
|
|
|
2599
3105
|
String? keyHash,
|
|
2600
3106
|
String? authToken}) async {
|
|
2601
3107
|
final v = await canAuthor(directiveId,
|
|
2602
|
-
ws: ws,
|
|
3108
|
+
ws: ws,
|
|
3109
|
+
principal: principal,
|
|
3110
|
+
publicKey: publicKey,
|
|
3111
|
+
keyHash: keyHash,
|
|
3112
|
+
authToken: authToken);
|
|
2603
3113
|
if (v['ok'] == true) return v;
|
|
2604
3114
|
final cause = v['cause']?.toString();
|
|
2605
3115
|
final target = v['ws']?.toString() ?? ws ?? workspace;
|
|
@@ -2647,7 +3157,8 @@ class _BridgeForeignReader implements NomosForeignReader {
|
|
|
2647
3157
|
// (trustSource + .readsFrom). That gate is the security model (a locator is not a foreign key), not a
|
|
2648
3158
|
// limitation: unverifiable "evidence" would be the real footgun.
|
|
2649
3159
|
@override
|
|
2650
|
-
Future<ForeignAttestation> Function(String workspace, String id, String type)?
|
|
3160
|
+
Future<ForeignAttestation> Function(String workspace, String id, String type)?
|
|
3161
|
+
get attestForeign => null;
|
|
2651
3162
|
}
|
|
2652
3163
|
|
|
2653
3164
|
/// A per-workspace read handle on the bridge plane — mounts [_workspace] lazily on the first read.
|
|
@@ -2661,8 +3172,7 @@ class _BridgeForeignWorkspaceReader implements NomosForeignWorkspaceReader {
|
|
|
2661
3172
|
// Mount ONCE (idempotent on the plane — "first connect wins"). `allowUnborn` so a not-yet-born foreign
|
|
2662
3173
|
// workspace reads empty (→ ForeignNotFound) rather than throwing. A genuinely unreachable cloud throws,
|
|
2663
3174
|
// which resolveResult classifies as ForeignUnreachable.
|
|
2664
|
-
Future<void> _ensureMounted() =>
|
|
2665
|
-
_mounting ??= _bridge.connect(
|
|
3175
|
+
Future<void> _ensureMounted() => _mounting ??= _bridge.connect(
|
|
2666
3176
|
cloud: _cloud,
|
|
2667
3177
|
workspace: _workspace,
|
|
2668
3178
|
session: _workspace,
|
|
@@ -2688,10 +3198,14 @@ class _BridgeForeignWorkspaceReader implements NomosForeignWorkspaceReader {
|
|
|
2688
3198
|
// a transient read failure is not fatal to the subscription — the next sync tick retries.
|
|
2689
3199
|
}
|
|
2690
3200
|
}
|
|
3201
|
+
|
|
2691
3202
|
ctrl = StreamController<List<Map<String, dynamic>>>(
|
|
2692
3203
|
onListen: () {
|
|
2693
3204
|
unawaited(emit());
|
|
2694
|
-
sub = _bridge
|
|
3205
|
+
sub = _bridge
|
|
3206
|
+
.session(_workspace)
|
|
3207
|
+
.syncEvents
|
|
3208
|
+
.listen((_) => unawaited(emit()));
|
|
2695
3209
|
},
|
|
2696
3210
|
onCancel: () async {
|
|
2697
3211
|
await sub?.cancel();
|