@githolon/dsl 0.101.6 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dart/CHANGELOG.md +13 -0
  2. package/dart/lib/nomos_client.dart +3 -52
  3. package/dart/lib/src/dev_cloud.dart +5 -0
  4. package/dart/lib/src/home.dart +10 -15
  5. package/dart/lib/src/nms_conformance_runtime.dart +12 -0
  6. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  7. package/dart/lib/src/nms_test_environment.dart +8 -0
  8. package/dart/lib/src/node_runner_transport.dart +56 -18
  9. package/dart/lib/src/provider.dart +5 -1
  10. package/dart/lib/src/record_origin.dart +55 -0
  11. package/dart/lib/src/runner/node-entry.mjs +9105 -0
  12. package/dart/lib/src/runner/offline-install.mjs +4729 -0
  13. package/dart/lib/src/session_bound.dart +23 -10
  14. package/dart/lib/src/source_trust.dart +51 -0
  15. package/dart/lib/src/test_harness.dart +490 -58
  16. package/dart/lib/src/webview_bridge.dart +167 -58
  17. package/dart/lib/src/workspace_name.dart +7 -1
  18. package/dart/pubspec.lock +3 -3
  19. package/dart/pubspec.yaml +4 -2
  20. package/dart/test/acquire_session_credentials_test.dart +34 -17
  21. package/dart/test/acquire_session_test.dart +109 -24
  22. package/dart/test/adv_realm_interference_test.dart +227 -99
  23. package/dart/test/bundle_version_pairing_test.dart +1 -1
  24. package/dart/test/cloud_bound_topology_test.dart +1 -1
  25. package/dart/test/cluster_attested_test.dart +52 -35
  26. package/dart/test/cluster_births_test.dart +5 -2
  27. package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
  28. package/dart/test/cluster_local_autosync_test.dart +5 -2
  29. package/dart/test/cluster_scale_test.dart +5 -2
  30. package/dart/test/compat_test.dart +1 -1
  31. package/dart/test/dlq_session_surface_test.dart +2 -1
  32. package/dart/test/errors_test.dart +1 -1
  33. package/dart/test/first_app_gauntlet_test.dart +78 -51
  34. package/dart/test/four_layer_nested_topology_test.dart +1 -1
  35. package/dart/test/harness_phase_timeout_test.dart +1 -1
  36. package/dart/test/home_parity_test.dart +9 -5
  37. package/dart/test/local_sync_inert_test.dart +22 -14
  38. package/dart/test/node_runner_transport_test.dart +9 -5
  39. package/dart/test/null_slot_test.dart +1 -1
  40. package/dart/test/parent_signer_recovery_test.dart +5 -2
  41. package/dart/test/schema_validation_test.dart +1 -1
  42. package/dart/test/sealing_client_test.dart +2 -1
  43. package/dart/test/session_bound_test.dart +92 -119
  44. package/dart/test/stale_bundle_test.dart +1 -1
  45. package/dart/test/test_harness_test.dart +77 -22
  46. package/dart/test/vo_read_typing_test.dart +32 -14
  47. package/dart/test/watch_after_offer_stall_test.dart +2 -1
  48. package/dart/test/webview_bridge_test.dart +17 -4
  49. package/dart/test/wire_test.dart +1 -1
  50. package/dart_types/CHANGELOG.md +8 -0
  51. package/dart_types/lib/nomos_types.dart +7 -6
  52. package/dart_types/lib/src/activity.dart +23 -13
  53. package/dart_types/lib/src/foreign_ref.dart +61 -112
  54. package/dart_types/lib/src/invitation.dart +96 -0
  55. package/dart_types/lib/src/nomos_ref.dart +97 -59
  56. package/dart_types/pubspec.yaml +1 -1
  57. package/dart_types/test/surfaced_test.dart +125 -19
  58. package/package.json +7 -3
  59. package/runtime/generated-application-host.js +8851 -0
  60. package/src/activity.ts +2 -2
  61. package/src/aggregate.ts +24 -1
  62. package/src/application_composition.ts +282 -0
  63. package/src/application_topology.ts +151 -0
  64. package/src/attested_core.ts +2 -2
  65. package/src/authoring_context.ts +9 -0
  66. package/src/birth_meter_ensure.ts +1 -1
  67. package/src/births.ts +53 -5
  68. package/src/build_package.ts +79 -71
  69. package/src/business_birth.ts +93 -10
  70. package/src/clear_directive.ts +3 -1
  71. package/src/codegen_dart.ts +369 -420
  72. package/src/codegen_dart_application.ts +1508 -0
  73. package/src/codegen_dart_client.ts +156 -95
  74. package/src/codegen_dart_scenario.ts +299 -0
  75. package/src/codegen_openapi.ts +8 -3
  76. package/src/codegen_proof.ts +1113 -1137
  77. package/src/codegen_provider_dart.ts +12 -1
  78. package/src/codegen_ts.ts +1176 -496
  79. package/src/compat_package_main.ts +22 -10
  80. package/src/compile_package_main.ts +1536 -337
  81. package/src/contract.ts +10 -4
  82. package/src/dart_barrel_guard.ts +5 -1
  83. package/src/describe_model.ts +13 -5
  84. package/src/directive.ts +244 -5
  85. package/src/engine_entry.ts +56 -13
  86. package/src/engine_javascript.ts +31 -0
  87. package/src/evolve_preview.ts +3 -12
  88. package/src/framework/application_home.ts +24 -10
  89. package/src/framework/capability.ts +15 -1
  90. package/src/framework/domain_lifecycle.ts +8 -0
  91. package/src/framework/framework_aggregates.ts +2 -2
  92. package/src/framework/genesis.ts +11 -1
  93. package/src/framework/home.ts +178 -75
  94. package/src/framework/impure_capability.ts +24 -7
  95. package/src/framework/relations.ts +13 -5
  96. package/src/framework/workspaces.ts +13 -12
  97. package/src/generated_client_names.ts +42 -3
  98. package/src/idof_lint.ts +65 -13
  99. package/src/index.ts +46 -12
  100. package/src/intent.ts +587 -0
  101. package/src/law_check.ts +64 -16
  102. package/src/precondition.ts +16 -4
  103. package/src/read_projection_namespace.ts +2 -7
  104. package/src/recipient.ts +33 -0
  105. package/src/relation_schema.ts +93 -0
  106. package/src/secondary_ensure.ts +11 -2
  107. package/src/shape_compat.ts +11 -1
  108. package/src/usd.ts +30 -8
  109. package/src/usd_layers.ts +447 -246
  110. package/src/usd_state.ts +1 -1
  111. package/src/value_object.ts +49 -13
  112. package/src/wire_encode.ts +124 -26
  113. package/src/workspace_sharing.ts +269 -0
  114. package/src/zod_introspection.ts +84 -0
  115. package/dart/example/nomos_secure_key_store.dart +0 -82
  116. package/dart/example/nomos_webview_transport.dart +0 -63
  117. package/dart/lib/nomos_headless.dart +0 -50
  118. package/dart/lib/testing.dart +0 -14
  119. package/dart/test/co2_product_gauntlet_test.dart +0 -244
  120. package/src/framework/identity.ts +0 -537
@@ -36,6 +36,7 @@ import { domainInterface } from "./shape_compat.js";
36
36
  import type { StableIds } from "./stable_ids_types.js";
37
37
  import type { DomainModule } from "./codegen_dart.js";
38
38
  import type { WireSchema } from "./wire.js";
39
+ import { parseUsdaDocument } from "./usd_layers.js";
39
40
 
40
41
  // ── the normalized law shape both sides of the diff speak ──────────────────────────
41
42
 
@@ -142,20 +143,10 @@ export interface DeployJson {
142
143
  packageUsda?: string;
143
144
  }
144
145
 
145
- /** Extract the embedded USD-IR JSON from a `.package.usda` bundle (the `nomos:usdJsonHex` field —
146
- * the SAME substring contract executor/src/law58.rs::usd_json_from_package uses). */
146
+ /** Reconstruct the effective law directly from a native `.package.usda` stage. */
147
147
  export function usdJsonFromPackage(bundle: string): unknown | undefined {
148
- const key = 'nomos:usdJsonHex = "';
149
- const start = bundle.indexOf(key);
150
- if (start < 0) return undefined;
151
- const from = start + key.length;
152
- const end = bundle.indexOf('"', from);
153
- if (end < 0) return undefined;
154
- const hex = bundle.slice(from, end);
155
- if (hex.length % 2 !== 0) return undefined;
156
148
  try {
157
- const bytes = Buffer.from(hex, "hex");
158
- return JSON.parse(bytes.toString("utf8"));
149
+ return parseUsdaDocument(bundle);
159
150
  } catch {
160
151
  return undefined;
161
152
  }
@@ -22,17 +22,22 @@ import {
22
22
  import { GenesisWarrant, seedGenesis } from "./genesis.js";
23
23
  import {
24
24
  BirthAttestation,
25
- HomeOwner,
25
+ ReceivingGroup,
26
+ ReceivingGroupPolicy,
26
27
  SharedWithMe,
27
28
  birthAttestation,
28
- birthHomeForDomain,
29
- homeNameBindsOwner,
30
- homeRelations,
29
+ birthReceivingGroupForDomain,
31
30
  parentAttestedBirth,
32
31
  pruneShare,
32
+ receivingGroupNameBindsPrincipal,
33
+ receivingGroupAdditionalDeviceFollowsPolicy,
34
+ receivingGroupRejectsKeylessAdditionalDevice,
35
+ receivingGroupRelations,
36
+ receivingGroupRevocationFollowsPolicy,
33
37
  recordShare,
34
38
  seedBirthAttestation,
35
- seedHomeOwner,
39
+ seedReceivingGroup,
40
+ seedReceivingGroupPolicy,
36
41
  sharedByWorkspace,
37
42
  } from "./home.js";
38
43
  import { RelationTuple, relationTuplesByObject } from "./relations.js";
@@ -46,6 +51,8 @@ import {
46
51
  enrollSignerSelf,
47
52
  enrollSignerSelfRequiresVerified,
48
53
  identityRelations,
54
+ revokeSigner,
55
+ revokeSignerEpochStrictlyIncreases,
49
56
  signerDelegationByUser,
50
57
  signerEpochCurrent,
51
58
  signerEpochStrictlyIncreases,
@@ -63,11 +70,16 @@ const directive = <P>(value: Directive<P>): Internal<Directive<P>> => internal(v
63
70
  const query = (value: QueryDecl): Internal<QueryDecl> => internal(value);
64
71
 
65
72
  // Parent primitive + the law its born home uses.
66
- export const __nomos_birthHome = directive(birthHomeForDomain(APPLICATION_HOME_DOMAIN));
67
- export const __nomos_HomeOwner = aggregate(HomeOwner);
68
- export const __nomos_seedHomeOwner = directive(seedHomeOwner);
69
- export const __nomos_homeNameBindsOwner = internal(homeNameBindsOwner);
70
- export const __nomos_homeRelations = internal(homeRelations);
73
+ export const __nomos_birthReceivingGroup = directive(birthReceivingGroupForDomain(APPLICATION_HOME_DOMAIN));
74
+ export const __nomos_ReceivingGroup = aggregate(ReceivingGroup);
75
+ export const __nomos_seedReceivingGroup = directive(seedReceivingGroup);
76
+ export const __nomos_ReceivingGroupPolicy = aggregate(ReceivingGroupPolicy);
77
+ export const __nomos_seedReceivingGroupPolicy = directive(seedReceivingGroupPolicy);
78
+ export const __nomos_receivingGroupNameBindsPrincipal = internal(receivingGroupNameBindsPrincipal);
79
+ export const __nomos_receivingGroupRejectsKeylessAdditionalDevice = internal(receivingGroupRejectsKeylessAdditionalDevice);
80
+ export const __nomos_receivingGroupAdditionalDeviceFollowsPolicy = internal(receivingGroupAdditionalDeviceFollowsPolicy);
81
+ export const __nomos_receivingGroupRevocationFollowsPolicy = internal(receivingGroupRevocationFollowsPolicy);
82
+ export const __nomos_receivingGroupRelations = internal(receivingGroupRelations);
71
83
 
72
84
  // Subject-side discovery/share custody.
73
85
  export const __nomos_SharedWithMe = aggregate(SharedWithMe);
@@ -89,6 +101,8 @@ export const __nomos_enrollSigner = directive(enrollSigner);
89
101
  export const __nomos_enrollFirstDevice = directive(enrollFirstDevice);
90
102
  export const __nomos_enrollSignerSelf = directive(enrollSignerSelf);
91
103
  export const __nomos_enrollSignerSelfRequiresVerified = internal(enrollSignerSelfRequiresVerified);
104
+ export const __nomos_revokeSigner = directive(revokeSigner);
105
+ export const __nomos_revokeSignerEpochStrictlyIncreases = internal(revokeSignerEpochStrictlyIncreases);
92
106
  export const __nomos_SignerEpoch = aggregate(SignerEpoch);
93
107
  export const __nomos_signerEpochCurrent = query(signerEpochCurrent);
94
108
  export const __nomos_advanceSignerEpoch = directive(advanceSignerEpoch);
@@ -175,6 +175,17 @@ export function capability<
175
175
  Object.entries(receipt).map(([k, f]) => [k, f.zod]),
176
176
  ) as { [K in keyof Rec]: Rec[K]["zod"] };
177
177
  const receiptEntries = Object.entries(receipt) as [string, Field][];
178
+ const decodeReceipt = (resultRef: string): Record<string, unknown> => {
179
+ const decoded = JSON.parse(resultRef) as unknown;
180
+ if (decoded === null || typeof decoded !== "object" || Array.isArray(decoded)) {
181
+ throw new Error("a typed capability receipt must encode a JSON object");
182
+ }
183
+ const values = decoded as Record<string, unknown>;
184
+ for (const [k, f] of receiptEntries) {
185
+ if (values[k] !== undefined) f.zod.parse(values[k]);
186
+ }
187
+ return values;
188
+ };
178
189
 
179
190
  const quartet = impureCapability({
180
191
  aggregateId,
@@ -198,8 +209,11 @@ export function capability<
198
209
  // discipline), present keys must validate.
199
210
  ...(receiptEntries.length > 0
200
211
  ? {
212
+ completeResultSchema: z.string().refine((value) => {
213
+ try { decodeReceipt(value); return true; } catch { return false; }
214
+ }, "must encode a valid typed capability receipt JSON object"),
201
215
  completeResult: (taskId: string, resultRef: string): PlannedOp[] => {
202
- const decoded = JSON.parse(resultRef) as Record<string, unknown>;
216
+ const decoded = decodeReceipt(resultRef);
203
217
  const ops: PlannedOp[] = [];
204
218
  for (const [k, f] of receiptEntries) {
205
219
  if (decoded[k] === undefined) continue;
@@ -23,6 +23,14 @@ import { Lww, MapOf } from "../drivers.js";
23
23
  import { attributionField, t } from "../fields.js";
24
24
  import { directive } from "../directive.js";
25
25
  import { set, withMarker, addToSet, type PlannedOp } from "../ops.js";
26
+ import { key, relationType, self, user } from "../relation_schema.js";
27
+
28
+ /** Framework-global authority to install or roll out a model in this workspace.
29
+ * The verified founder receives the first leaf in `seedGenesis`; every later
30
+ * delegation remains an ordinary, replayable Zanzibar edge. */
31
+ export const modelLifecycleRelations = relationType("workspace")
32
+ .asGlobal()
33
+ .relation("installDomain", { rewrite: self(), allow: [user(), key()] });
26
34
 
27
35
  export const DOMAIN_INSTALLATION_PHASES = [
28
36
  "Pending",
@@ -36,13 +36,13 @@ import { nomosSourceFrontierEnsure } from "../attested.js";
36
36
  import * as birthMeter from "../birth_meter.js";
37
37
  import * as birthMeterEnsure from "../birth_meter_ensure.js";
38
38
  import * as birthMeterFramework from "./birth_meter.js";
39
+ import * as authProviderLaw from "./auth_provider_law.js";
39
40
  import * as disclosure from "./disclosure.js";
40
41
  import * as domainLifecycle from "./domain_lifecycle.js";
41
42
  import * as encryptionIdentity from "./encryption_identity.js";
42
43
  import * as encryptionScope from "./encryption_scope.js";
43
44
  import * as genesis from "./genesis.js";
44
45
  import * as home from "./home.js";
45
- import * as identity from "./identity.js";
46
46
  import * as ownedCustody from "./owned_custody.js";
47
47
  import * as relations from "./relations.js";
48
48
  import * as repair from "./repair.js";
@@ -59,13 +59,13 @@ const FRAMEWORK_NAMESPACES: ReadonlyArray<Record<string, unknown>> = [
59
59
  birthMeter,
60
60
  birthMeterEnsure,
61
61
  birthMeterFramework,
62
+ authProviderLaw,
62
63
  disclosure,
63
64
  domainLifecycle,
64
65
  encryptionIdentity,
65
66
  encryptionScope,
66
67
  genesis,
67
68
  home,
68
- identity,
69
69
  ownedCustody,
70
70
  relations,
71
71
  repair,
@@ -32,6 +32,7 @@ import { t } from "../fields.js";
32
32
  import { Lww } from "../drivers.js";
33
33
  import { directive } from "../directive.js";
34
34
  import { set, type PlannedOp } from "../ops.js";
35
+ import { writeTuple } from "./relations.js";
35
36
 
36
37
  /**
37
38
  * Deterministic singleton id of the founding warrant — MUST equal
@@ -94,4 +95,13 @@ export const seedGenesis = directive("seedGenesis")
94
95
  bornAt: z.string().min(1),
95
96
  }),
96
97
  )
97
- .plan((p) => writeGenesisWarrant(p));
98
+ .plan((p) => [
99
+ ...writeGenesisWarrant(p),
100
+ ...writeTuple({
101
+ object: "workspace:self",
102
+ relation: "installDomain",
103
+ subject: /^(user|key):/.test(p.principal) ? p.principal : `user:${p.principal}`,
104
+ grantedBy: p.principal,
105
+ grantedAt: p.bornAt,
106
+ }),
107
+ ]);
@@ -25,13 +25,13 @@
25
25
  * the result, and the owner-authored lane to maintain it.)
26
26
  */
27
27
  import { z } from "zod";
28
- import { aggregate, instance } from "../aggregate.js";
28
+ import { aggregate, instance, OWNED_READ_RELATION } from "../aggregate.js";
29
29
  // The device-key-hash wire shape (same validator signing_identity/workspaces use module-locally).
30
30
  const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/, "expected lowercase sha256 hex (the owner's device pubkey hash)");
31
31
  import { t } from "../fields.js";
32
32
  import { Lww } from "../drivers.js";
33
33
  import { directive } from "../directive.js";
34
- import { create } from "../authoring.js";
34
+ import { ensure } from "../authoring.js";
35
35
  import { set, type PlannedOp } from "../ops.js";
36
36
  import { query } from "../query.js";
37
37
  import { birth } from "../births.js";
@@ -43,6 +43,7 @@ import { writeTuple } from "./relations.js";
43
43
  import { relationType, self, user, selfObject } from "../relation_schema.js";
44
44
  import { requireParentAttestedBirth, type ParentAttestedBirthMarker } from "../births.js";
45
45
  import { GenesisWarrant, seedGenesis } from "./genesis.js";
46
+ import { idOf } from "../value_object.js";
46
47
 
47
48
  // `nomos.sha256` — the TRUSTLESS, pure hash computed IN the githolon (Rust sha2 bound in the sealed engine;
48
49
  // see deterministic-rquickjs). Available as a global to plans + invariant predicates (like `nomos.mint`); it
@@ -60,56 +61,96 @@ export { identityRelations };
60
61
  export { GenesisWarrant, seedGenesis };
61
62
 
62
63
  /**
63
- * THE HOME OWNER (home_holons §5 — the owner stamp). A home holon is born UNWARRANTED (the subject may
64
- * never have logged in — a share can be delivered to a stranger's lazily-born home, before they hold any
65
- * device key, so there is no signed-author regime yet). This singleton records WHO the home belongs to: the
66
- * §3 cross-custody gate binds a delivered quote's subject to THIS principal (`executor::HOME_OWNER_ID`), so
67
- * a quote signed for anyone else fails closed. Seeded ONCE at birth by the host (the control-plane
68
- * `seedHomeOwner`, kept off the open session lane), so a tenant cannot forge ownership.
64
+ * THE RECEIVING GROUP — one sovereign framework identity whose enrolled devices clone this same holon.
65
+ * It is not a tenant Person, an IdP account or a host-owned account. The native global
66
+ * `identity:<principal>#signer@key:<hash>` relation is its device roster; this singleton binds the group's
67
+ * stable principal to the workspace that carries that roster and its access manifest.
68
+ *
69
+ * The wire identifiers are deliberately obscure framework names. Tenant business packages may model their
70
+ * own `ReceivingGroup`, `home` or `group` concepts without silently colliding with this implementation.
69
71
  */
70
- export const HOME_OWNER_ID = "home-owner:self";
71
- /** A home holon's SELF-COMMITTED NAME prefix — `home-<sha256(subject)>` (the client's `homeNameOf` + the
72
- * squat invariant both derive it). The home's chain commits to its own routing name, so the bailiff can
73
- * bind the name to the CONTENT (custody) while the LAW guarantees the name hashes its owner. */
74
- export const HOME_NAME_PREFIX = "home-";
75
- export const HOME_NAME_HASH_LEN = 40;
76
- export const HomeOwner = aggregate("HomeOwner", {
77
- /** The identity this home belongs to — the canonical `user:<uid>` subject the §3 gate binds quotes to. */
72
+ export const RECEIVING_GROUP_ID = "nomos-internal-receiving-group-7c1f5e:self";
73
+ /** A receiving group's self-committed workspace-name prefix. */
74
+ export const RECEIVING_GROUP_NAME_PREFIX = "nomos-rg-";
75
+ export const RECEIVING_GROUP_NAME_HASH_LEN = 40;
76
+ export const RECEIVING_GROUP_POLICY_ID = "nomos-internal-receiving-group-policy-7c1f5e:self";
77
+ export const ReceivingGroup = aggregate("NomosInternalReceivingGroup7c1f5e", {
78
+ /** The stable Zanzibar principal represented by this group. Credentials may locate it but do not own it. */
78
79
  principal: t.string().merge(Lww),
79
- /** THE HOME'S SELF-COMMITTED NAME (`home-<sha256(principal)>`) — the squat invariant asserts it hashes the
80
- * owner, so the chain's claimed name can ONLY be its rightful owner's slot (home_holons §5). */
81
- homeName: t.string().merge(Lww),
80
+ /** The group's self-committed workspace name, deterministically bound to its stable principal. */
81
+ workspaceName: t.string().merge(Lww),
82
82
  /** ISO-8601, caller-stamped (determinism — no clock in the plan). */
83
83
  bornAt: t.string().merge(Lww),
84
- }).public();
84
+ }, { cap: 1 }).public();
85
85
 
86
86
  /**
87
- * THE HOME-OWNER RELATION (the ReBAC authority spine, co2 ask #1). The {@link HomeOwner} aggregate is the
88
- * ownership RECORD; this relation is the AUTHORIZATION spine a tenant can compute from. `seedHomeOwner` seats
89
- * `(home:self, homeOwner, user:<owner>)` ATOMICALLY inside the same child genesis (before the warrant closes
90
- * bootstrap), so a canonical `birthHome` yields a home whose owner passes role gates from intent 0 — a tenant
91
- * declares `projectCreator = computed(homeOwner)` WITHOUT copying birth mechanics. Self-bound (object `home:
92
- * self`, subject the owner) — the {@link homeNameBindsOwner} squat invariant already proves the owner is the
93
- * rightful one, so no separate grant is needed.
87
+ * The framework-owned device lifecycle policy for one receiving group. This is deliberately data, not a
88
+ * client-side branch: every peer replays the same rule. The first supported posture is intentionally small
89
+ * and secure: a live, verified device may add or revoke another device; keyless IdP enrolment is only the
90
+ * pre-group bootstrap lane; all-device-loss recovery requires guardians and remains fail-closed until that
91
+ * proof ceremony is installed.
94
92
  */
95
- export const homeRelations = relationType("home").relation("homeOwner", {
93
+ export const ReceivingGroupPolicy = aggregate("NomosInternalReceivingGroupPolicy7c1f5e", {
94
+ additionalDevice: t.string().merge(Lww),
95
+ revocation: t.string().merge(Lww),
96
+ recovery: t.string().merge(Lww),
97
+ guardianQuorum: t.int().merge(Lww),
98
+ setAt: t.string().merge(Lww),
99
+ }, { cap: 1 }).public();
100
+
101
+ export const DEFAULT_RECEIVING_GROUP_POLICY = {
102
+ additionalDevice: "current-device",
103
+ revocation: "current-device",
104
+ recovery: "guardian-quorum",
105
+ guardianQuorum: 1,
106
+ } as const;
107
+
108
+ function writeReceivingGroupPolicy(p: { setAt: string }): PlannedOp[] {
109
+ const policy = instance(ReceivingGroupPolicy, RECEIVING_GROUP_POLICY_ID);
110
+ return [
111
+ set(policy, "additionalDevice", DEFAULT_RECEIVING_GROUP_POLICY.additionalDevice),
112
+ set(policy, "revocation", DEFAULT_RECEIVING_GROUP_POLICY.revocation),
113
+ set(policy, "recovery", DEFAULT_RECEIVING_GROUP_POLICY.recovery),
114
+ set(policy, "guardianQuorum", DEFAULT_RECEIVING_GROUP_POLICY.guardianQuorum),
115
+ set(policy, "setAt", p.setAt),
116
+ ];
117
+ }
118
+
119
+ /** Genesis-only default policy. Policy changes become a separate, guardian-aware ceremony. */
120
+ export const seedReceivingGroupPolicy = directive("nomosInternalSeedReceivingGroupPolicy7c1f5e")
121
+ .ensures(ReceivingGroupPolicy)
122
+ .payload(z.object({ setAt: z.string().min(1) }))
123
+ .plan((p) => writeReceivingGroupPolicy(p));
124
+
125
+ /**
126
+ * The group's controller relation binds its stable principal inside the same genesis. Business representation
127
+ * remains separate Zanzibar edges; this relation only establishes who controls this receiving-group holon.
128
+ */
129
+ export const receivingGroupRelations = relationType("nomos.internal.receiving-group.7c1f5e").relation("controller", {
96
130
  rewrite: self(),
97
131
  allow: [user()],
98
132
  });
99
133
 
100
- /** The framework PRIMITIVE plan: stamp the home's owner + its self-committed name (singleton at the fixed id),
101
- * AND atomically seat the canonical `homeOwner` relation tuple for the same self-bound principal (co2 #1). */
102
- export function writeHomeOwner(p: { principal: string; homeName: string; bornAt: string }): PlannedOp[] {
103
- const h = instance(HomeOwner, HOME_OWNER_ID);
134
+ /** Stamp the receiving group and atomically seat its controller relation. */
135
+ export function writeReceivingGroup(p: { principal: string; workspaceName: string; bornAt: string }): PlannedOp[] {
136
+ const h = instance(ReceivingGroup, RECEIVING_GROUP_ID);
104
137
  return [
105
138
  set(h, "principal", p.principal),
106
- set(h, "homeName", p.homeName),
139
+ set(h, "workspaceName", p.workspaceName),
107
140
  set(h, "bornAt", p.bornAt),
108
- // ATOMIC ReBAC seat — the owner subject is the canonical `user:<uid>` on HomeOwner.principal; grantedBy is
109
- // the owner itself (self-bound, no external granter) and grantedAt rides the caller-stamped bornAt.
110
141
  ...writeTuple({
111
- object: selfObject(homeRelations),
112
- relation: "homeOwner",
142
+ object: selfObject(receivingGroupRelations),
143
+ relation: "controller",
144
+ subject: p.principal,
145
+ grantedBy: p.principal,
146
+ grantedAt: p.bornAt,
147
+ }),
148
+ // A device clones the encrypted workspace before record-level ownership can be evaluated locally.
149
+ // Seat the group's controller for that ciphertext transport in the same genesis; individual owned
150
+ // records still require their own per-record reader edge before they decrypt or appear in reads.
151
+ ...writeTuple({
152
+ object: "workspace:self",
153
+ relation: OWNED_READ_RELATION,
113
154
  subject: p.principal,
114
155
  grantedBy: p.principal,
115
156
  grantedAt: p.bornAt,
@@ -118,16 +159,12 @@ export function writeHomeOwner(p: { principal: string; homeName: string; bornAt:
118
159
  }
119
160
 
120
161
  /**
121
- * `seedHomeOwner` — the genesis directive that stamps the home's owner + its self-committed `homeName` at
122
- * the fixed singleton id. It is authored by the birth/genesis chain and structurally refused on the open
123
- * session lane. `.ensures` (a deterministic-keyed singleton). The `homeName`
124
- * is caller-supplied but the {@link homeNameBindsOwner} invariant proves it `== home-<sha256(principal)>`,
125
- * so a squatter cannot claim a name slot that doesn't hash to ITS own owner.
162
+ * Genesis-only receiving-group stamp. The name is caller-supplied but the invariant below re-derives it.
126
163
  */
127
- export const seedHomeOwner = directive("seedHomeOwner")
128
- .ensures(HomeOwner)
129
- .payload(z.object({ principal: z.string().min(1), homeName: z.string().min(1), bornAt: z.string().min(1) }))
130
- .plan((p) => writeHomeOwner(p));
164
+ export const seedReceivingGroup = directive("nomosInternalSeedReceivingGroup7c1f5e")
165
+ .ensures(ReceivingGroup)
166
+ .payload(z.object({ principal: z.string().min(1), workspaceName: z.string().min(1), bornAt: z.string().min(1) }))
167
+ .plan((p) => writeReceivingGroup(p));
131
168
 
132
169
  /**
133
170
  * THE SQUAT INVARIANT (home_holons §5) — a home holon's self-committed `homeName` MUST be the hash of its
@@ -137,22 +174,62 @@ export const seedHomeOwner = directive("seedHomeOwner")
137
174
  * never by a host check. The host only routes the chain to the name it commits to (custody). `nomos.sha256`
138
175
  * re-derives the hash in-law on every peer, so the verdict is identical everywhere.
139
176
  */
140
- export const homeNameBindsOwner = workspaceInvariant("HomeNameBindsOwner")
141
- .on("seedHomeOwner")
142
- .reads(() => [refAs("owner", "HomeOwner", HOME_OWNER_ID)])
143
- .assert(({ owner }) => {
144
- const o = (owner ?? {}) as { principal?: string; homeName?: string };
145
- const expected = HOME_NAME_PREFIX + nomos.sha256(String(o.principal ?? "")).slice(0, HOME_NAME_HASH_LEN);
146
- return o.homeName === expected
177
+ export const receivingGroupNameBindsPrincipal = workspaceInvariant("ReceivingGroupNameBindsPrincipal")
178
+ .on("nomosInternalSeedReceivingGroup7c1f5e")
179
+ .reads(() => [refAs("group", "NomosInternalReceivingGroup7c1f5e", RECEIVING_GROUP_ID)])
180
+ .assert(({ group }) => {
181
+ const g = (group ?? {}) as { principal?: string; workspaceName?: string };
182
+ const expected = RECEIVING_GROUP_NAME_PREFIX
183
+ + nomos.sha256(String(g.principal ?? "")).slice(0, RECEIVING_GROUP_NAME_HASH_LEN);
184
+ return g.workspaceName === expected
147
185
  ? { accept: true }
148
- : { reject: "a home's name must hash its owner (home-<sha256(principal)>) — refusing a squatted home" };
186
+ : { reject: "a receiving group's workspace name must hash its stable principal" };
149
187
  });
150
188
 
151
- /** Re-derive a subject's home slot name IN-LAW (`home-<sha256(subject)>`) — the SAME formula the client's
152
- * `homeNameOf` and the {@link homeNameBindsOwner} invariant use. `nomos.sha256` is the trustless pure hash
153
- * bound in the sealed engine, so every replaying peer derives the identical name. */
154
- export function homeNameOf(subject: string): string {
155
- return HOME_NAME_PREFIX + nomos.sha256(String(subject)).slice(0, HOME_NAME_HASH_LEN);
189
+ const receivingGroupPolicyRefs = () => [
190
+ refAs("group", "NomosInternalReceivingGroup7c1f5e", RECEIVING_GROUP_ID),
191
+ refAs("policy", "NomosInternalReceivingGroupPolicy7c1f5e", RECEIVING_GROUP_POLICY_ID),
192
+ ];
193
+
194
+ /** An IdP token may bootstrap the parent before the group exists; it may never bypass an existing group's
195
+ * device policy. A new group device must be approved by a current device or, later, guardian evidence. */
196
+ export const receivingGroupRejectsKeylessAdditionalDevice = workspaceInvariant("ReceivingGroupRejectsKeylessAdditionalDevice")
197
+ .on("enrollFirstDevice")
198
+ .reads(receivingGroupPolicyRefs)
199
+ .assert(({ group }) => typeof group?.principal !== "string" || group.principal.length === 0
200
+ ? { accept: true }
201
+ : { reject: "receiving-group-additional-device-requires-current-device-or-guardian" });
202
+
203
+ /** The shipped additional-device policy: an already enrolled, verified device signs the self-bound enrol. */
204
+ export const receivingGroupAdditionalDeviceFollowsPolicy = workspaceInvariant("ReceivingGroupAdditionalDeviceFollowsPolicy")
205
+ .on("enrollSignerSelf")
206
+ .reads(receivingGroupPolicyRefs)
207
+ .declassify("device lifecycle policy intentionally branches on principal_verified")
208
+ .assert(({ group, policy }, ctx) => {
209
+ if (typeof group?.principal !== "string" || group.principal.length === 0) return { accept: true };
210
+ const p = policy as { additionalDevice?: string } | undefined;
211
+ return p?.additionalDevice === "current-device" && ctx?.principalVerified === true
212
+ ? { accept: true }
213
+ : { reject: "receiving-group-additional-device-policy-refused" };
214
+ });
215
+
216
+ /** The shipped revocation policy: a live verified group device authors the remove-wins signer change. */
217
+ export const receivingGroupRevocationFollowsPolicy = workspaceInvariant("ReceivingGroupRevocationFollowsPolicy")
218
+ .on("revokeSigner")
219
+ .reads(receivingGroupPolicyRefs)
220
+ .declassify("device lifecycle policy intentionally branches on principal_verified")
221
+ .assert(({ group, policy }, ctx) => {
222
+ if (typeof group?.principal !== "string" || group.principal.length === 0) return { accept: true };
223
+ const p = policy as { revocation?: string } | undefined;
224
+ return p?.revocation === "current-device" && ctx?.principalVerified === true
225
+ ? { accept: true }
226
+ : { reject: "receiving-group-revocation-policy-refused" };
227
+ });
228
+
229
+ /** Re-derive a receiving group's workspace name in-law. */
230
+ export function receivingGroupNameOf(principal: string): string {
231
+ return RECEIVING_GROUP_NAME_PREFIX
232
+ + nomos.sha256(String(principal)).slice(0, RECEIVING_GROUP_NAME_HASH_LEN);
156
233
  }
157
234
 
158
235
  /**
@@ -184,7 +261,9 @@ export function homeNameOf(subject: string): string {
184
261
  * so HomeOwner is seeded and `homeNameBindsOwner` passes (a birth without it would be an invalid, un-owned
185
262
  * home).
186
263
  */
187
- export const HOME_LAW_DOMAIN = "home";
264
+ // The kernel's era-0 birth-seed resolver retains `home` as its fallback dispatch domain for standalone
265
+ // framework laws. Compiler-composed business applications use the obscure APPLICATION_HOME_DOMAIN instead.
266
+ export const RECEIVING_GROUP_LAW_DOMAIN = "home";
188
267
  /**
189
268
  * Build the private home-birth primitive for the domain key that carries the home law.
190
269
  *
@@ -193,10 +272,12 @@ export const HOME_LAW_DOMAIN = "home";
193
272
  * tenant remains free to model a business concept called "home". The key is closed over
194
273
  * here and written into every child-genesis step; it is never a tenant payload field.
195
274
  */
196
- export function birthHomeForDomain(homeLawDomain: string) {
197
- if (homeLawDomain.length === 0) throw new Error("birthHomeForDomain requires a non-empty home-law domain");
198
- return directive("birthHome")
199
- .creates(HomeOwner) // the birth's law-declared touched type — the child's HomeOwner is what this offer seeds
275
+ export function birthReceivingGroupForDomain(groupLawDomain: string) {
276
+ if (groupLawDomain.length === 0) {
277
+ throw new Error("birthReceivingGroupForDomain requires a non-empty receiving-group law domain");
278
+ }
279
+ return directive("nomosInternalBirthReceivingGroup7c1f5e")
280
+ .creates(ReceivingGroup)
200
281
  .payload(
201
282
  z.object({
202
283
  /** The identity this home belongs to (a `user:<uid>` subject). The workspace name + seeded homeName are
@@ -240,7 +321,7 @@ export function birthHomeForDomain(homeLawDomain: string) {
240
321
  )
241
322
  .plan((p) => {
242
323
  const founder = p.owner; // the actor for the child's own genesis steps (its self-bound owner subject)
243
- const homeName = homeNameOf(p.owner); // workspace name == seeded homeName — derived from the SAME owner
324
+ const groupName = receivingGroupNameOf(p.owner);
244
325
  // THE PARENT'S DECLARED VERIFIER (one captured read serves BOTH consumers below): the inherited
245
326
  // `setAuthProvider` genesis step AND the enroll step's `verifiedVia` provenance stamp. Read it up
246
327
  // front so the enroll step (which precedes the inheritance step in the chain) can see the issuer.
@@ -270,9 +351,20 @@ export function birthHomeForDomain(homeLawDomain: string) {
270
351
  // from the parent's custody when it folds the birth; never the host). Matches buildGenesisChain's shape.
271
352
  { domain: "bootstrap", directiveId: "installDomain", actor: founder, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
272
353
  { domain: "nomos", directiveId: "installDomain", actor: founder, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
354
+ {
355
+ domain: groupLawDomain,
356
+ directiveId: "nomosInternalSeedReceivingGroupPolicy7c1f5e",
357
+ actor: founder,
358
+ payload: { setAt: p.bornAt },
359
+ },
273
360
  // SEED THE OWNER — the home is born OWNED; the homeNameBindsOwner invariant in the child's law proves
274
361
  // homeName == home-<sha256(owner)> at the child's own gate, so the slot can ONLY be its rightful owner's.
275
- { domain: homeLawDomain, directiveId: "seedHomeOwner", actor: founder, payload: { principal: p.owner, homeName, bornAt: p.bornAt } },
362
+ {
363
+ domain: groupLawDomain,
364
+ directiveId: "nomosInternalSeedReceivingGroup7c1f5e",
365
+ actor: founder,
366
+ payload: { principal: p.owner, workspaceName: groupName, bornAt: p.bornAt },
367
+ },
276
368
  // ENROL THE OWNER'S DEVICE on the warrant-free prefix (when supplied) — enrollSigner self-binds to
277
369
  // ctx.principal() (the owner), so this binds `ownerKeyHash` as the owner's signer BEFORE the warrant
278
370
  // closes bootstrap. This is what makes the born home SIGNER-READY (able to sign + self-serve-birth its own
@@ -282,7 +374,7 @@ export function birthHomeForDomain(homeLawDomain: string) {
282
374
  // ladder: every seed runs UNSIGNED on the still-warrant-free prefix; the warrant closes it). ERA-SAFE: a
283
375
  // home born earlier (pre-feature law, no such step in its recipe) replays byte-identical — the era rule
284
376
  // rides the LAW hash, not this branch.
285
- { domain: homeLawDomain, directiveId: "enrollSigner", actor: founder, payload: { keyHash: p.ownerKeyHash, grantedBy: p.owner, grantedAt: 1, ...(verifiedVia !== undefined ? { verifiedVia } : {}) } },
377
+ { domain: groupLawDomain, directiveId: "enrollSigner", actor: founder, payload: { keyHash: p.ownerKeyHash, grantedBy: p.owner, grantedAt: 1, ...(verifiedVia !== undefined ? { verifiedVia } : {}) } },
286
378
  ];
287
379
  // AUTHPROVIDER INHERITANCE (default ON — see the payload doc): copy the parent's declared provider
288
380
  // (the up-front captured read above) AS DATA into the child genesis. The child owns the copy as its
@@ -292,7 +384,7 @@ export function birthHomeForDomain(homeLawDomain: string) {
292
384
  const issuer = str("issuer");
293
385
  if (issuer !== undefined) {
294
386
  genesisChain.push({
295
- domain: homeLawDomain,
387
+ domain: groupLawDomain,
296
388
  directiveId: "seedAuthProvider",
297
389
  actor: founder,
298
390
  payload: {
@@ -314,14 +406,14 @@ export function birthHomeForDomain(homeLawDomain: string) {
314
406
  // THE DECLARED BIRTH EFFECT (the unix model): the recipe rides the committed intent as a declared output
315
407
  // (captured_ports.births). The kernel — reading the `.births()` declaration off the law manifest — takes
316
408
  // the recipe and spawns + self-validates the child through the child's OWN gate (each step self-verifies).
317
- birth({ workspace: homeName, genesisChain });
409
+ birth({ workspace: groupName, genesisChain });
318
410
  return [];
319
411
  })
320
412
  .reads(authProviderQuery) // the parent-side AuthProvider capture the inheritance copies (the #58 lane)
321
413
  .births(); // BIRTH IS A DECLARED KERNEL PRIMITIVE — emits the `nomosBirthGate` era key; the kernel folds the recipe
322
414
  }
323
415
 
324
- export const birthHome = birthHomeForDomain(HOME_LAW_DOMAIN);
416
+ export const birthReceivingGroup = birthReceivingGroupForDomain(RECEIVING_GROUP_LAW_DOMAIN);
325
417
 
326
418
  /**
327
419
  * THE SHARED-WITH-ME INDEX (§5) — one record per cross-custody attestation delivered to this home holon:
@@ -345,6 +437,15 @@ export const SharedWithMe = aggregate("SharedWithMe", {
345
437
  prunedAt: t.string().merge(Lww),
346
438
  }).public();
347
439
 
440
+ /** One idempotent access-manifest row per source edge in this home. Delivery is at-least-once: a retry,
441
+ * reconnect or source outbox replay must update the same bookmark rather than manufacture another thing the
442
+ * recipient appears to have access to. The home itself scopes the recipient, so the edge coordinate is the
443
+ * source workspace + protected object + granted relation. */
444
+ export function sharedWithMeId(asserterWorkspace: string, object: string, relation: string): string {
445
+ const coordinate = JSON.stringify([asserterWorkspace, object, relation]);
446
+ return `SharedWithMe_${nomos.sha256(coordinate)}`;
447
+ }
448
+
348
449
  /**
349
450
  * `recordShare` — index a cross-custody share into the home holon's access list. AUTHORED on the home
350
451
  * holon (the identity's own subject-side root). The payload carries the §3 CROSS-CUSTODY ATTESTATION — a
@@ -357,7 +458,7 @@ export const SharedWithMe = aggregate("SharedWithMe", {
357
458
  * asserter when the identity opens it (§5 fail-safe); a stale/forged quote can't enter the list at all.
358
459
  */
359
460
  export const recordShare = directive("recordShare")
360
- .creates(SharedWithMe)
461
+ .ensures(SharedWithMe)
361
462
  .payload(
362
463
  z.object({
363
464
  asserterWorkspace: z.string().min(1),
@@ -372,7 +473,9 @@ export const recordShare = directive("recordShare")
372
473
  }),
373
474
  )
374
475
  .plan((p) => {
375
- create(SharedWithMe)
476
+ // ENSURE, never CREATE: signed delivery is retryable and therefore at-least-once. Re-delivery (including
477
+ // a later re-grant after prune) refreshes this exact access edge and makes it live again.
478
+ ensure(SharedWithMe, sharedWithMeId(p.asserterWorkspace, p.object, p.relation))
376
479
  .set("asserterWorkspace", p.asserterWorkspace)
377
480
  .set("object", p.object)
378
481
  .set("relation", p.relation)
@@ -385,7 +488,7 @@ export const recordShare = directive("recordShare")
385
488
  // into the `nomosAttestationGate` era key) — the one gate verifies it against the home's owner (the
386
489
  // LAW-DECLARED owner-source below, NOT a kernel-hardcoded id) + pins the principal to the attestation key,
387
490
  // so the bailiff delivers it host-free to an OFFLINE subject (no device key, no author block).
388
- .attestationAuthorized({ ownerObject: HOME_OWNER_ID, ownerField: "principal" });
491
+ .attestationAuthorized({ ownerObject: RECEIVING_GROUP_ID, ownerField: "principal" });
389
492
 
390
493
  /**
391
494
  * `pruneShare` — fail-safe drop of a hint whose forward `Check` denied (the asserter revoked it). The row
@@ -394,7 +497,7 @@ export const recordShare = directive("recordShare")
394
497
  */
395
498
  export const pruneShare = directive("pruneShare")
396
499
  .mutates(SharedWithMe)
397
- .payload(z.object({ shareId: z.string().min(1), prunedAt: z.string().min(1) }))
500
+ .payload(z.object({ shareId: idOf(SharedWithMe), prunedAt: z.string().min(1) }))
398
501
  .plan((p) => [set(instance(SharedWithMe, p.shareId), "prunedAt", p.prunedAt)]);
399
502
 
400
503
  /** "Show me everything <asserter> shared with me" — an indexed probe over the local access list. */
@@ -485,7 +588,7 @@ export const parentAttestedBirth: ParentAttestedBirthMarker = {
485
588
  // DECLARE the owner + cert locators so the KERNEL reads them (it names neither): the home owner lives at
486
589
  // the `HomeOwner` singleton's `principal`, the travelling cert at the `BirthAttestation` singleton's
487
590
  // `certChain`. Mirrors how `.attestationAuthorized()` declares its owner-source — the kernel hardcodes none.
488
- ownerObject: HOME_OWNER_ID,
591
+ ownerObject: RECEIVING_GROUP_ID,
489
592
  ownerField: "principal",
490
593
  certObject: BIRTH_ATTESTATION_ID,
491
594
  certField: "certChain",