@githolon/dsl 0.108.4 → 0.108.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dart/CHANGELOG.md +5 -0
  2. package/dart/lib/src/application_framework_calls.dart +21 -0
  3. package/dart/lib/src/dev_cloud.dart +67 -0
  4. package/dart/lib/src/nms_test_environment.dart +1 -1
  5. package/dart/lib/src/runner/node-entry.mjs +354 -104
  6. package/dart/lib/src/runner/offline-install.mjs +64 -8
  7. package/dart/lib/src/session_bound.dart +58 -30
  8. package/dart/lib/src/test_harness.dart +60 -98
  9. package/dart/lib/src/webview_bridge.dart +212 -32
  10. package/dart/pubspec.lock +1 -1
  11. package/dart/pubspec.yaml +2 -2
  12. package/dart/test/node_runner_transport_test.dart +5 -5
  13. package/dart/test/webview_bridge_test.dart +6 -0
  14. package/dart_types/CHANGELOG.md +4 -0
  15. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +428 -2
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/package.json +1 -1
  18. package/runtime/generated-application-host.js +293 -79
  19. package/src/application_topology.ts +120 -28
  20. package/src/births.ts +27 -0
  21. package/src/codegen_call_usda.ts +9 -4
  22. package/src/codegen_dart_application.ts +146 -37
  23. package/src/codegen_dart_client.ts +225 -160
  24. package/src/codegen_dart_framework_calls.ts +89 -4
  25. package/src/codegen_ts.ts +135 -60
  26. package/src/compile_package_main.ts +3544 -1460
  27. package/src/directive.ts +320 -118
  28. package/src/fields.ts +2 -3
  29. package/src/framework/application_home.ts +3 -0
  30. package/src/framework/auth_provider_law.ts +20 -28
  31. package/src/framework/genesis.ts +32 -1
  32. package/src/framework/home.ts +45 -8
  33. package/src/framework/signing_identity.ts +78 -0
  34. package/src/framework/workspaces.ts +9 -1
  35. package/src/framework_call_dart_enums.ts +33 -0
  36. package/src/framework_call_definitions.ts +11 -2
  37. package/src/index.ts +12 -0
  38. package/src/intent.ts +525 -194
  39. package/src/law_graph.ts +32 -4
  40. package/src/law_graph_usda.ts +3 -0
  41. package/src/package_config.ts +16 -1
  42. package/src/read.ts +33 -11
  43. package/src/wire.ts +1 -0
  44. package/src/wire_encode.ts +3 -3
@@ -4464,6 +4464,7 @@ __export(engine_exports, {
4464
4464
  lawParity: () => lawParity,
4465
4465
  ledgerStats: () => ledgerStats,
4466
4466
  listRefusedRefs: () => listRefusedRefs,
4467
+ localCustodySnapshot: () => localCustodySnapshot,
4467
4468
  materializeRefused: () => materializeRefused,
4468
4469
  memoryDropProbe: () => memoryDropProbe,
4469
4470
  migrateHomes: () => migrateHomes,
@@ -6276,6 +6277,16 @@ function prepareManagedPromotion(report, candidateHash, domain, opts = {}) {
6276
6277
  error: `the package-wide report does not cover the generated business area '${domain}'`
6277
6278
  };
6278
6279
  }
6280
+ const dispositions = report.dispositions;
6281
+ const changesStableIdentity = dispositions !== void 0 && (dispositions.retired.length > 0 || Object.keys(dispositions.retyped).length > 0 || Object.keys(dispositions.rebinds).length > 0);
6282
+ if (changesStableIdentity && !acceptanceReason) {
6283
+ return {
6284
+ ok: false,
6285
+ stage: "acceptance",
6286
+ report,
6287
+ error: "the current-frontier transition changes stable business identities and needs an attributed acceptance reason"
6288
+ };
6289
+ }
6279
6290
  const preflight = {
6280
6291
  candidateHash,
6281
6292
  // A package may be reached through any one of its generated domain clients. The
@@ -11045,6 +11056,10 @@ function presentOfferAdmitted(admitted) {
11045
11056
  };
11046
11057
  }
11047
11058
 
11059
+ // src/_adapter/proto/nomos/custody/v1/snapshot_pb.js
11060
+ var file_nomos_custody_v1_snapshot = /* @__PURE__ */ fileDesc("Ch9ub21vcy9jdXN0b2R5L3YxL3NuYXBzaG90LnByb3RvEhBub21vcy5jdXN0b2R5LnYxIqACChRMb2NhbEN1c3RvZHlTbmFwc2hvdBIWCg5mb3JtYXRfdmVyc2lvbhgBIAEoDRIWCgl3b3Jrc3BhY2UYAiABKAlIAIgBARIMCgR0cmVlGAMgASgMEhcKCmNoZWNrcG9pbnQYBCABKAxIAYgBARIVCghmcm9udGllchgFIAEoDEgCiAEBEhMKBmN1cnNvchgGIAEoCUgDiAEBEhcKCnN0YXRlX3Jvb3QYByABKAlIBIgBARIYCgtzeW5jZWRfYmFzZRgIIAEoCUgFiAEBQgwKCl93b3Jrc3BhY2VCDQoLX2NoZWNrcG9pbnRCCwoJX2Zyb250aWVyQgkKB19jdXJzb3JCDQoLX3N0YXRlX3Jvb3RCDgoMX3N5bmNlZF9iYXNlYgZwcm90bzM");
11061
+ var LocalCustodySnapshotSchema = /* @__PURE__ */ messageDesc(file_nomos_custody_v1_snapshot, 0);
11062
+
11048
11063
  // src/_adapter/engine.ts
11049
11064
  var enc3 = new TextEncoder();
11050
11065
  var dec3 = new TextDecoder();
@@ -12807,8 +12822,11 @@ async function reconcileRequiredCustodyHead(eng2, ws, ledger, requiredCustodyHea
12807
12822
  if (!/^[0-9a-f]{40}$/.test(requiredCustodyHead)) {
12808
12823
  throw new Error("required custody head must be a lowercase 40-hex git object id");
12809
12824
  }
12810
- const current = eng2.mounted.has(ws) ? kgit.resolveRef(eng2, gitdirOf(ws), BRANCH) : "";
12811
- if (current === requiredCustodyHead) return eng2.mounted.get(ws);
12825
+ const resident = eng2.mounted.get(ws);
12826
+ const current = resident ? kgit.resolveRef(eng2, gitdirOf(ws), BRANCH) : "";
12827
+ if (current === requiredCustodyHead && resident?.remoteMain === requiredCustodyHead && !resident.restoreError) {
12828
+ return resident;
12829
+ }
12812
12830
  if (current && isDirty(eng2, ws)) {
12813
12831
  throw new Error(
12814
12832
  `workspace '${ws}' has verified but unsealed resident work; refusing to discard it while reconciling custody`
@@ -12821,6 +12839,12 @@ async function reconcileRequiredCustodyHead(eng2, ws, ledger, requiredCustodyHea
12821
12839
  `workspace '${ws}' could not reconcile required custody ${requiredCustodyHead}: ${mounted.restoreError || "custody has no verified main"}`
12822
12840
  );
12823
12841
  }
12842
+ const reconciled = kgit.resolveRef(eng2, gitdirOf(ws), BRANCH);
12843
+ if (reconciled !== requiredCustodyHead || mounted.remoteMain !== requiredCustodyHead) {
12844
+ throw new Error(
12845
+ `workspace '${ws}' restored custody ${mounted.remoteMain || reconciled || "none"}, not required custody ${requiredCustodyHead}`
12846
+ );
12847
+ }
12824
12848
  return mounted;
12825
12849
  }
12826
12850
  function assertAuthored(res, what) {
@@ -12958,7 +12982,10 @@ async function applyWorkspaceEffect(eng2, ws, ledger, requestBytes, admissionChe
12958
12982
  const result = response.outcome.value.result;
12959
12983
  if (!result || result.outcome.case !== "admitted") {
12960
12984
  const outcome = result?.outcome.case || "empty";
12961
- throw new Error(`target kernel ${outcome} the workspace effect`);
12985
+ const detail = result?.outcome.case === "refused" ? result.outcome.value.refusal?.message : void 0;
12986
+ throw new Error(
12987
+ `target kernel ${outcome} the workspace effect${detail ? `: ${detail}` : ""}`
12988
+ );
12962
12989
  }
12963
12990
  await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", ffOnly: true });
12964
12991
  return toBinary(ApplyWorkspaceEffectResponseSchema, response.outcome.value);
@@ -13069,8 +13096,8 @@ function prepareRemoteOffer(eng2, ws, requestBytes) {
13069
13096
  if (authoring.authorSecret.byteLength !== 0) {
13070
13097
  throw new Error("remote offer preparation refuses client signing secrets");
13071
13098
  }
13072
- if (request.intent.case !== "call") {
13073
- throw new Error("remote offer preparation requires a typed directive call");
13099
+ if (request.intent.case !== "call" && request.intent.case !== "modelLifecycle") {
13100
+ throw new Error("remote offer preparation requires a typed directive or model-lifecycle call");
13074
13101
  }
13075
13102
  const response = callProto(eng2.ex, {
13076
13103
  case: "prepareRemoteOffer",
@@ -13877,7 +13904,13 @@ function packDelta(eng2, ws, base, head) {
13877
13904
  case: "packDelta",
13878
13905
  value: create(PackDeltaRequestSchema, { repo: repoArgOf(ws), base: base || void 0, head })
13879
13906
  }, "packDelta").pack;
13880
- } catch {
13907
+ } catch (error) {
13908
+ console.error("[nomos-custody-pack] packDelta refused", {
13909
+ workspace: ws,
13910
+ base: base || "<full>",
13911
+ head,
13912
+ error: String(error)
13913
+ });
13881
13914
  return null;
13882
13915
  }
13883
13916
  }
@@ -15056,7 +15089,10 @@ async function emitOutbox(eng2, ws, ledger, workspaceEffectBase = "", admissionC
15056
15089
  const targetResult = response.result;
15057
15090
  if (!targetResult || targetResult.outcome.case !== "admitted") {
15058
15091
  const outcome = targetResult?.outcome.case || "empty";
15059
- throw new Error(`target kernel ${outcome} the workspace effect`);
15092
+ const detail = targetResult?.outcome.case === "refused" ? targetResult.outcome.value.refusal?.message : void 0;
15093
+ throw new Error(
15094
+ `target kernel ${outcome} the workspace effect${detail ? `: ${detail}` : ""}`
15095
+ );
15060
15096
  }
15061
15097
  break;
15062
15098
  }
@@ -15088,7 +15124,7 @@ async function emitOutbox(eng2, ws, ledger, workspaceEffectBase = "", admissionC
15088
15124
  } catch (e) {
15089
15125
  errors++;
15090
15126
  const destination = offer.route.case === "httpsRoute" ? offer.route.value.url : offer.route.case === "workspaceRoute" ? offer.route.value.workspace : offer.destination;
15091
- log("outbox-deliver-failed", { ws, offerId: offer.id, destination, error: String(e).slice(0, 200) });
15127
+ log("outbox-deliver-failed", { ws, offerId: offer.id, destination, error: String(e).slice(0, 2e3) });
15092
15128
  }
15093
15129
  }
15094
15130
  if (delivered.length || errors) log("outbox", { ws, delivered: delivered.length, errors });
@@ -15121,6 +15157,26 @@ function snapshot(eng2, ws) {
15121
15157
  const wsDir = dirOf(eng2.preopen.dir.contents.get("ws")).contents.get(ws);
15122
15158
  return serializeTree(wsDir);
15123
15159
  }
15160
+ function localCustodySnapshot(eng2, ws) {
15161
+ const checkpoint2 = exportCheckpoint(eng2, ws);
15162
+ const tree = snapshot(eng2, ws);
15163
+ const head = headOf(eng2, ws);
15164
+ const body = toBinary(LocalCustodySnapshotSchema, create(LocalCustodySnapshotSchema, {
15165
+ formatVersion: 3,
15166
+ workspace: ws,
15167
+ tree,
15168
+ checkpoint: checkpoint2.blob ? bytesFromB64(checkpoint2.blob) : void 0,
15169
+ frontier: checkpoint2.frontier ? bytesFromB64(checkpoint2.frontier) : void 0,
15170
+ cursor: checkpoint2.cursor === void 0 ? void 0 : String(checkpoint2.cursor),
15171
+ stateRoot: checkpoint2.stateRoot,
15172
+ syncedBase: head || void 0
15173
+ }));
15174
+ const prefix = enc3.encode("NOMOS-CUSTODY-PB3\n");
15175
+ const framed = new Uint8Array(prefix.byteLength + body.byteLength);
15176
+ framed.set(prefix);
15177
+ framed.set(body, prefix.byteLength);
15178
+ return framed;
15179
+ }
15124
15180
  function stats(eng2) {
15125
15181
  return {
15126
15182
  mounted: eng2.mounted.size,
@@ -284,6 +284,10 @@ Future<T> nmsInternalConvergeGeneratedModel<T>({
284
284
  try {
285
285
  return await attempt();
286
286
  } on DomainClientModelAdoptionRequired catch (change) {
287
+ // A lifecycle response with no lawful next action is a terminal refusal,
288
+ // not a convergence step. Preserve the kernel's exact diagnostic instead
289
+ // of retrying until a generic stall obscures it.
290
+ if (!change.canStage && !change.canAccept) throw change;
287
291
  final step = '${change.expectedDomainKey}\u0000'
288
292
  '${change.expectedDomainHash}\u0000'
289
293
  '${change.status}\u0000'
@@ -300,10 +304,11 @@ Future<T> nmsInternalConvergeGeneratedModel<T>({
300
304
  }
301
305
  try {
302
306
  await handle(change);
303
- } on DomainClientModelAdoptionRequired {
307
+ } on DomainClientModelAdoptionRequired catch (nextChange) {
304
308
  // A transient preflight/acceptance race re-enters the same typed step.
305
309
  // The per-step budget above prevents a genuine loop without limiting
306
310
  // how many distinct historical generations may converge.
311
+ if (!nextChange.canStage && !nextChange.canAccept) throw nextChange;
307
312
  }
308
313
  }
309
314
  }
@@ -379,35 +384,56 @@ class DomainNotInstalledForSession implements Exception {
379
384
  /// cause classified; returns normally when the session serves the client's law compatibly.
380
385
  extension NomosSessionCompat on NomosSession {
381
386
  Future<void> assertCompatible(NomosClientInterface iface) async {
382
- // 1. REACHABILITY — the session's workspace must answer a read at all.
387
+ // The exact generated package identity is stronger than a structural comparison. The kernel's
388
+ // point-readable `law/current.usda` answers this bounded happy path without materialising the
389
+ // complete interface offer. Structural discovery is reserved for genuine version skew.
383
390
  List<String> activeKeys = const [];
391
+ List<String> activeHashes = const [];
384
392
  try {
385
- final offer = await interfaceOffer();
386
- if (offer != null) activeKeys = offer.keys.toList()..sort();
393
+ final law = await lawStatus(iface.expectedDomainHash);
394
+ activeHashes = ((law['current'] as List?) ?? const [])
395
+ .map((value) => value.toString())
396
+ .toList(growable: false);
397
+ if (activeHashes.contains(iface.expectedDomainHash)) return;
398
+ activeKeys = ((law['currentDomains'] as List?) ?? const [])
399
+ .map((value) => value.toString())
400
+ .toList(growable: false);
401
+ // Compatibility is member-scoped, not one enormous application-wide ceremony. Once the
402
+ // authenticated current-law index proves this domain is served, generated reads decode their
403
+ // exact USDA result types and generated offers are judged by the current kernel route. A new
404
+ // member therefore fails closed when used without blocking every older compatible feature.
405
+ if (activeKeys.contains(iface.domain)) return;
406
+ } catch (_) {
407
+ // Preserve the structural fallback for older kernels which do not expose the typed current
408
+ // frontier. Its error classification below remains the authoritative diagnostic path.
409
+ }
410
+
411
+ // 1. THE SHAPE AXIS (#72) — the one compatibility notion. The kernel compares the
412
+ // compact generated requirement directly with its authenticated current-law index. Do not
413
+ // export/materialise the complete interface merely to prove the successful path.
414
+ final requirement = iface.requirement;
415
+ if (requirement == null) return;
416
+ Map<String, Object?> verdict;
417
+ try {
418
+ final raw = await checkCompatibility(requirement);
419
+ verdict = (raw['verdict'] is Map)
420
+ ? (raw['verdict'] as Map).cast<String, Object?>()
421
+ : raw;
387
422
  } catch (e) {
388
- // co2 blocker #2: a PROJECTION READ FAILURE (an unmountable/failing query shape — the runner surfaces
389
- // `nomos.projection.read_failed` carrying the failed query) is NOT "not reachable" and MUST NOT be
390
- // reclassified as trulyNotDeployed downstream — surface it as its own cause with the real fault text.
391
- final isProjectionFault =
392
- e.toString().contains('nomos.projection.read_failed') ||
393
- e.toString().contains('projection read failed');
394
423
  throw DomainClientSessionMismatch(
395
424
  expectedDomainKey: iface.domain,
396
425
  expectedDomainHash: iface.expectedDomainHash,
397
426
  expectedInterfaceHash: iface.expectedInterfaceHash,
398
427
  sessionId: workspace,
399
428
  sessionWorkspace: workspace,
400
- cause: isProjectionFault
401
- ? SessionMismatchCause.projectionReadFailed
402
- : null,
403
- hint: isProjectionFault
404
- ? 'the workspace law could NOT be READ — a projection/mount failure (NOT undeployed law): $e'
405
- : 'the session workspace is not reachable/mounted on this plane: $e',
429
+ activeDomainKeys: activeKeys,
430
+ activeDomainHashes: activeHashes,
431
+ hint: 'the shape-compatibility check itself failed: $e',
406
432
  );
407
433
  }
434
+ if (verdict['ok'] == true) return;
408
435
 
409
- // 2. HASH AXIS (diagnosis) — the installed law hashes, via the baked compat manifest when present.
410
- List<String> activeHashes = const [];
436
+ // 2. HASH DIAGNOSIS — only a failed verdict needs retained installation inventory.
411
437
  if (iface.compatJson != null && iface.compatJson!.isNotEmpty) {
412
438
  try {
413
439
  final c = await checkCompat(
@@ -415,32 +441,34 @@ extension NomosSessionCompat on NomosSession {
415
441
  iface.requirement);
416
442
  if (c != null) activeHashes = c.workspaceRuns;
417
443
  } catch (_) {
418
- /* diagnosis only — never fail the preflight on the hash axis */
444
+ /* diagnosis only — never replace the shape verdict */
419
445
  }
420
446
  }
421
447
 
422
- // 3. THE SHAPE AXIS (#72) — the one compatibility notion. No baked interface ⇒ nothing more to check.
423
- final requirement = iface.requirement;
424
- if (requirement == null) return;
425
- Map<String, Object?> verdict;
448
+ // 3. DIAGNOSTIC OFFER — only a failed verdict needs the complete active-domain names to
449
+ // distinguish a newer client from the wrong package or a genuinely absent deployment.
426
450
  try {
427
- final raw = await checkCompatibility(requirement);
428
- verdict = (raw['verdict'] is Map)
429
- ? (raw['verdict'] as Map).cast<String, Object?>()
430
- : raw;
451
+ final offer = await interfaceOffer();
452
+ if (offer != null) activeKeys = offer.keys.toList()..sort();
431
453
  } catch (e) {
454
+ final isProjectionFault =
455
+ e.toString().contains('nomos.projection.read_failed') ||
456
+ e.toString().contains('projection read failed');
432
457
  throw DomainClientSessionMismatch(
433
458
  expectedDomainKey: iface.domain,
434
459
  expectedDomainHash: iface.expectedDomainHash,
435
460
  expectedInterfaceHash: iface.expectedInterfaceHash,
436
461
  sessionId: workspace,
437
462
  sessionWorkspace: workspace,
438
- activeDomainKeys: activeKeys,
439
463
  activeDomainHashes: activeHashes,
440
- hint: 'the shape-compatibility check itself failed: $e',
464
+ cause: isProjectionFault
465
+ ? SessionMismatchCause.projectionReadFailed
466
+ : null,
467
+ hint: isProjectionFault
468
+ ? 'the workspace law could NOT be READ — a projection/mount failure (NOT undeployed law): $e'
469
+ : 'the session workspace is not reachable/mounted on this plane: $e',
441
470
  );
442
471
  }
443
- if (verdict['ok'] == true) return;
444
472
 
445
473
  final missing =
446
474
  ((verdict['missing'] as List?) ?? const []).map((e) => '$e').toList();
@@ -43,7 +43,7 @@ import 'dart:io';
43
43
  import 'package:crypto/crypto.dart' show sha256;
44
44
  import 'package:fixnum/fixnum.dart' show Int64;
45
45
  import 'package:nomos_types/nomos_kernel_protocol.dart'
46
- show BirthCertificate, BirthOutcomeStatus, SyncWorkspaceResponse;
46
+ show BirthCertificate, SyncWorkspaceResponse;
47
47
  import 'package:nomos_types/nomos_types.dart' show Sha256Digest;
48
48
  import 'package:package_config/package_config.dart' show loadPackageConfigUri;
49
49
 
@@ -494,6 +494,14 @@ class NomosTestHarness {
494
494
  /// `sha256(base64decode(...))` is the attestor keyHash a reader enrols to trust this source's signed reads.
495
495
  String? devicePublicKey(String workspace) => _devicePublics[workspace];
496
496
 
497
+ /// The enrolled device identity used by provider nonce binding. Tests which
498
+ /// rotate an external IdP can mint the next proof without handling key bytes.
499
+ String? deviceKeyHash(String workspace) {
500
+ final publicKey = _devicePublics[workspace];
501
+ if (publicKey == null || publicKey.isEmpty) return null;
502
+ return sha256.convert(base64Decode(publicKey)).toString();
503
+ }
504
+
497
505
  /// The base64 device SECRET a session was opened with (TEST HARNESS ONLY — the harness IS the device;
498
506
  /// a real app keeps this in secure storage). Sign writes to locally-born children with it, e.g.
499
507
  /// `bridge.acquireSession(childWs, authorSecret: harness.deviceSecret(home))`.
@@ -1480,6 +1488,7 @@ class NomosTestHarness {
1480
1488
  static Future<NomosTestHarness> cloudBound({
1481
1489
  required NomosTestCloud cloud,
1482
1490
  required List<String> lawUsdzPaths,
1491
+ NmsApplicationFrameworkCalls? applicationFrameworkCalls,
1483
1492
  String platformWorkspace = 'test-platform',
1484
1493
  required String subject,
1485
1494
  String? authToken,
@@ -1500,6 +1509,13 @@ class NomosTestHarness {
1500
1509
  'no lawUsdzPaths — pass the build/<name>.law.usdz `npx githolon compile` emits',
1501
1510
  );
1502
1511
  }
1512
+ final frameworkCalls = applicationFrameworkCalls;
1513
+ if (frameworkCalls == null) {
1514
+ throw NomosTestHarnessException(
1515
+ 'cloudBound requires this application\'s compiler-generated '
1516
+ 'NmsApplicationFrameworkCalls; raw framework directive transport is not supported',
1517
+ );
1518
+ }
1503
1519
  final entry = nodeEntry ??
1504
1520
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
1505
1521
  final bareUid =
@@ -1853,56 +1869,18 @@ class NomosTestHarness {
1853
1869
  'capturedClock': DateTime.now().toUtc().millisecondsSinceEpoch,
1854
1870
  'epoch': 0,
1855
1871
  }, secret);
1856
- // Mount the target workspace in the SAME kernel plane and author through its own
1857
- // installed law. The HTTP host is neither given raw business fields nor a signing key;
1858
- // sync carries the already-sealed offer and the receiving kernel replays the one gate.
1859
- // The enroll route was still resolved above to prove the generated public surface
1860
- // names this semantic route exactly once, but route strings grant no authority here.
1861
- final platformAuthorSession = '$clientId-platform-author';
1862
- await plane.connect(
1863
- cloud: cloud.origin,
1864
- workspace: platformWorkspace,
1865
- session: platformAuthorSession,
1866
- clientId: '$clientId-platform-author',
1867
- authToken: idpToken,
1868
- authorSecret: secret,
1869
- );
1870
- await plane.offerDirective(
1871
- domain: applicationHomeDomain,
1872
- directiveId: 'enrollFirstDevice',
1873
- domainHash: controlHomeLawHash,
1874
- actor: subject,
1875
- authorSecret: secret,
1876
- session: platformAuthorSession,
1877
- payload: {
1878
- 'keyHash': keyHash,
1879
- 'verifiedVia': issuer,
1880
- 'validFromEpoch': 0,
1881
- 'grantedAt': DateTime.now().toUtc().toIso8601String(),
1882
- 'idpToken': idpToken,
1883
- // The framework law owns this USDA object shape. Keep the
1884
- // compiler-generated protobuf certificate structured while
1885
- // lowering it into that domain value; never resurrect the
1886
- // retired JSON string envelope (`delegationCert`).
1887
- 'delegationCertificate':
1888
- _birthCertificateLawValue(delegationCert),
1889
- },
1890
- );
1891
- await plane.sync(
1892
- session: platformAuthorSession,
1893
- );
1894
1872
  return (
1895
1873
  publicB64: publicB64,
1896
1874
  secret: secret,
1897
1875
  keyHash: keyHash,
1898
1876
  idpToken: idpToken,
1877
+ issuer: issuer,
1878
+ delegationCert: delegationCert,
1899
1879
  );
1900
1880
  },
1901
1881
  );
1902
- // 5/6 — THE HOME CEREMONY. Author the birth in the already-mounted PARENT kernel, then sync its sealed
1903
- // intent to the remote parent where the same offer gate replays it. The HTTP host never receives
1904
- // business fields or a signing secret and has no birth-authoring lane. The offer carries the
1905
- // typed device-signed certificate. The kernel derives and enrolls the founding signer from it.
1882
+ // 5/6 — THE PRODUCTION HOME CEREMONY. Platform remains remote: Runtime v2 prepares both generated
1883
+ // calls against its live frontier, signs with this device, and opens the resulting Home custody.
1906
1884
  String? home;
1907
1885
  await _phased(
1908
1886
  phase: '5/6: remote home-birth ceremony (attested)',
@@ -1912,65 +1890,49 @@ class NomosTestHarness {
1912
1890
  onDiagnostic: onDiagnostic,
1913
1891
  run: () async {
1914
1892
  final homeName = _receivingGroupNameOf(subject);
1915
- // The plane deliberately still has both the empty onboarding scratch holon and the
1916
- // authenticated platform holon mounted. Bind this parent certificate operation to the
1917
- // platform session explicitly; relying on the single-session convenience API becomes
1918
- // ambiguous as soon as the ceremony correctly opens the target workspace in-kernel.
1919
- final homeCert =
1920
- await plane.session('$clientId-platform-author').signBirthCert(
1921
- BirthCertificate(
1922
- child: homeName,
1923
- founder: subject,
1924
- // The cert's `verified` must MATCH the gate verdict (CAS-as-law): the offer carries
1925
- // the dev-IdP token, so the gate resolves a VERIFIED founder.
1926
- verified: true,
1927
- parentKey: base64Decode(device.publicB64),
1928
- parentId: platformWorkspace,
1929
- epoch: Int64.ZERO,
1930
- ),
1931
- device.secret,
1932
- );
1933
- final localBirth =
1934
- await plane.session('$clientId-platform-author').offerOutcome(
1935
- domain: applicationHomeDomain,
1936
- directiveId: 'nomosInternalBirthReceivingGroup7c1f5e',
1937
- actor: subject,
1938
- authorSecret: device.secret,
1939
- payload: <String, Object?>{
1940
- 'owner': subject,
1941
- 'frameworkHash': frameworkHash,
1942
- 'lawHash': applicationHomeLawHash,
1943
- 'bornAt': DateTime.now().toUtc().toIso8601String(),
1944
- },
1945
- birthCertificate: homeCert,
1946
- );
1947
- if (!localBirth.born.contains(homeName)) {
1948
- throw NomosTestHarnessException(
1949
- 'cloudBound: the local parent kernel admitted the receiving-group offer but did not birth '
1950
- '"$homeName" (born=${localBirth.born}, birthOutcomes=${localBirth.birthOutcomes})',
1951
- );
1952
- }
1953
- final birthSync = await plane.sync(
1954
- session: '$clientId-platform-author',
1955
- );
1956
- if (birthSync.pushFailed ||
1957
- birthSync.diverged ||
1958
- birthSync.stillAhead) {
1893
+ final bornAt = DateTime.now().toUtc().toIso8601String();
1894
+ final result = await plane
1895
+ .session('$clientId-onboard-scratch')
1896
+ .nomosInternalBirthReceivingGroup(
1897
+ parent: platformWorkspace,
1898
+ principal: bareUid,
1899
+ subject: subject,
1900
+ device: {
1901
+ 'public': device.publicB64,
1902
+ 'secret': device.secret,
1903
+ },
1904
+ authToken: device.idpToken,
1905
+ idToken: device.idpToken,
1906
+ verifiedVia: device.issuer,
1907
+ bornAt: bornAt,
1908
+ enrollFirstDeviceCall: frameworkCalls.enrollFirstDevice(
1909
+ domain: applicationHomeDomain,
1910
+ workspace: platformWorkspace,
1911
+ keyHash: device.keyHash,
1912
+ verifiedVia: device.issuer,
1913
+ validFromEpoch: 0,
1914
+ grantedAt: bornAt,
1915
+ idpToken: device.idpToken,
1916
+ delegationCertificate: device.delegationCert,
1917
+ ),
1918
+ birthReceivingGroupCall: frameworkCalls.birthReceivingGroup(
1919
+ domain: applicationHomeDomain,
1920
+ workspace: platformWorkspace,
1921
+ owner: subject,
1922
+ frameworkHash: Sha256Digest(frameworkHash!),
1923
+ lawHash: Sha256Digest(applicationHomeLawHash),
1924
+ bornAt: bornAt,
1925
+ ),
1926
+ confirm: true,
1927
+ );
1928
+ if (result.home != homeName) {
1959
1929
  throw NomosTestHarnessException(
1960
- 'cloudBound: the sealed receiving-group offer did not converge: $birthSync',
1930
+ 'cloudBound: production custody ceremony returned ${result.home} instead of "$homeName"',
1961
1931
  );
1962
1932
  }
1963
- final remoteOutcomes = birthSync.birthOutcomes;
1964
- final failedRemoteBirth = remoteOutcomes.where(
1965
- (entry) =>
1966
- entry.hasWorkspace() &&
1967
- entry.workspace == homeName &&
1968
- entry.status != BirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
1969
- );
1970
- if (failedRemoteBirth.isNotEmpty) {
1933
+ if (!result.verified) {
1971
1934
  throw NomosTestHarnessException(
1972
- 'cloudBound: the remote parent replay refused the receiving-group birth: '
1973
- '${failedRemoteBirth.toList()}',
1935
+ 'cloudBound: born Home failed kernel verification: ${result.verificationError}',
1974
1936
  );
1975
1937
  }
1976
1938
  home = homeName;