@githolon/dsl 0.14.0 → 0.16.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/example/nomos_webview_transport.dart +2 -2
- package/dart/lib/nomos_dsl.dart +4 -4
- package/dart/lib/src/{dispatch.dart → offer.dart} +7 -7
- package/dart/lib/src/provider.dart +30 -30
- package/dart/lib/src/subscriptions.dart +3 -3
- package/dart/lib/src/webview_bridge.dart +56 -21
- package/dart/test/webview_bridge_test.dart +35 -34
- package/package.json +5 -3
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/build_package.ts +1 -1
- package/src/capability_exports.ts +2 -2
- package/src/codegen_dart.ts +34 -24
- package/src/codegen_dart_client.ts +74 -12
- package/src/codegen_provider_dart.ts +67 -61
- package/src/codegen_ts.ts +45 -17
- package/src/compile_package_main.ts +103 -36
- package/src/ctx.ts +13 -0
- package/src/engine_entry.ts +22 -5
- package/src/fields.ts +44 -0
- package/src/flow_gate.ts +208 -0
- package/src/framework/capability.ts +29 -11
- package/src/framework/encryption_scope.ts +9 -1
- package/src/framework/genesis.ts +5 -1
- package/src/framework/impure_capability.ts +73 -67
- package/src/framework/relations.ts +23 -4
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/sync_lifecycle.ts +3 -3
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +7 -0
- package/src/index.ts +25 -1
- package/src/manifest.ts +11 -17
- package/src/ops.ts +42 -1
- package/src/principal_evidence.ts +112 -0
- package/src/relation_sugar.ts +10 -4
- package/src/shape_compat.ts +208 -0
- package/src/usd.ts +287 -0
- package/src/usd_layers.ts +213 -26
- package/src/wire.ts +8 -0
- package/src/wire_encode.ts +75 -2
- package/src/workspace_sharding.ts +197 -24
- package/src/workspace_type.ts +1 -0
package/src/flow_gate.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// NOMOS — the domain security model as a STATIC information-flow gate
|
|
2
|
+
// (architecture/security_model.md). A domain's security is a property of its
|
|
3
|
+
// compiled law; this pass makes leaky law UNREPRESENTABLE — the compiler refuses a
|
|
4
|
+
// domain that could ever emit a secret through one of the offer kernel's finite
|
|
5
|
+
// public sinks (refusal/ref transition, captured read, read group/key/where,
|
|
6
|
+
// public field, outbound offer). The irreducible remainder — flows the author
|
|
7
|
+
// INTENDS — is forced into an explicit, audited `declassify(reason)` (a later slice).
|
|
8
|
+
//
|
|
9
|
+
// v1 (this file): the two CONFIDENTIALITY rules that never have a legitimate
|
|
10
|
+
// exception, so they hard-refuse with no `declassify` escape, and both read only
|
|
11
|
+
// canonical manifest IR (no plan-AST interpreter needed — the captured-read-private
|
|
12
|
+
// constraint already severed the plan from foreign secrets):
|
|
13
|
+
// Rule 1 — no encrypt-then-index (an .encrypted() field used as a group/key/where/value)
|
|
14
|
+
// Rule 2 — no private read on chain (a captured read returning a requires(cap) type)
|
|
15
|
+
// Integrity (who may WRITE) is gated natively at admission by `.requires(relation)`;
|
|
16
|
+
// the integrity-flow dual + rule 4 (secret-correlated refusal) + `declassify` + the
|
|
17
|
+
// `nomosFlowGate` era key are the next slices.
|
|
18
|
+
import type { CanonicalManifest } from "./manifest.js";
|
|
19
|
+
import type { CanonicalPred } from "./predicate.js";
|
|
20
|
+
|
|
21
|
+
/** A read projection, read structurally — every read kind (query / count / sum / extremum /
|
|
22
|
+
* ordered) carries its target type (`of`, or a string `returns`) and the fields it observes
|
|
23
|
+
* (`key` / `by` / `valueField` / `where`). Loose by design — one walk over all read shapes. */
|
|
24
|
+
type Projection = {
|
|
25
|
+
readonly id?: string;
|
|
26
|
+
readonly queryId?: string;
|
|
27
|
+
readonly of?: string;
|
|
28
|
+
readonly returns?: unknown;
|
|
29
|
+
readonly key?: readonly string[];
|
|
30
|
+
readonly by?: string;
|
|
31
|
+
readonly valueField?: string;
|
|
32
|
+
readonly where?: CanonicalPred;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The raw module bits rule 4 needs that the canonical manifest strips (the executable
|
|
36
|
+
* invariant `.reads` + the declassify waivers). Read structurally — no coupling to DomainModule. */
|
|
37
|
+
type FlowModule = {
|
|
38
|
+
readonly aggregates?: readonly {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly hasInvariant?: boolean;
|
|
41
|
+
readonly invariantDeclassify?: string;
|
|
42
|
+
readonly encryptedFields?: readonly string[];
|
|
43
|
+
readonly fields?: Record<string, unknown>;
|
|
44
|
+
}[];
|
|
45
|
+
readonly workspaceInvariants?: readonly {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly on: string;
|
|
48
|
+
readonly declassify?: string;
|
|
49
|
+
readonly reads: (ctx: { intent: Record<string, unknown>; principal?: string }) => readonly { readonly aggregate: string }[];
|
|
50
|
+
}[];
|
|
51
|
+
readonly impureCapabilities?: readonly {
|
|
52
|
+
readonly name?: string;
|
|
53
|
+
readonly capability?: string;
|
|
54
|
+
readonly endpoint?: string;
|
|
55
|
+
readonly declassify?: string;
|
|
56
|
+
readonly aggregate: { readonly id: string };
|
|
57
|
+
}[];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** A typed flow-gate violation — the message names the field, the sink, and the rule. */
|
|
61
|
+
export class FlowGateError extends Error {
|
|
62
|
+
constructor(message: string) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "FlowGateError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The set of field names a `where` predicate OBSERVES (a filter on a value is observing it). */
|
|
69
|
+
function predFields(p: CanonicalPred | undefined): string[] {
|
|
70
|
+
if (!p) return [];
|
|
71
|
+
if (p.kind === "eq" || p.kind === "ne") return [p.field];
|
|
72
|
+
return p.clauses.flatMap(predFields);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Run the static information-flow gate over a domain's canonical manifest. Throws a
|
|
77
|
+
* {@link FlowGateError} on the first violation (a typed compile error). Pure over
|
|
78
|
+
* manifest IR — no side effects, no AST.
|
|
79
|
+
*/
|
|
80
|
+
export function assertFlowGate(m: CanonicalManifest, mod?: FlowModule): void {
|
|
81
|
+
// ── RULE 1 — NO ENCRYPT-THEN-INDEX ──────────────────────────────────────────
|
|
82
|
+
// A field that is .encrypted() (operator-blind ciphertext) MUST NOT appear in any
|
|
83
|
+
// read's group-by / key / value / where over its own type: grouping/indexing/
|
|
84
|
+
// filtering/summing BY a value observes it, so the encryption is defeated and the
|
|
85
|
+
// ciphertext-size + membership metadata leaks. Pure manifest-shape check.
|
|
86
|
+
const encByType = new Map<string, Set<string>>();
|
|
87
|
+
for (const a of m.aggregates) {
|
|
88
|
+
if (a.encryptedFields && a.encryptedFields.length > 0) encByType.set(a.id, new Set(a.encryptedFields));
|
|
89
|
+
}
|
|
90
|
+
if (encByType.size > 0) {
|
|
91
|
+
const typeOf = (p: Projection): string | undefined =>
|
|
92
|
+
p.of ?? (typeof p.returns === "string" ? p.returns : undefined);
|
|
93
|
+
const fieldsOf = (p: Projection): (string | undefined)[] =>
|
|
94
|
+
[...(p.key ?? []), p.by, p.valueField, ...predFields(p.where)];
|
|
95
|
+
const reads: { list: readonly Projection[]; sink: string }[] = [
|
|
96
|
+
{ list: (m.queries ?? []) as unknown as Projection[], sink: "a query key" },
|
|
97
|
+
{ list: (m.counts ?? []) as unknown as Projection[], sink: "a count group / where" },
|
|
98
|
+
{ list: (m.sums ?? []) as unknown as Projection[], sink: "a sum group / value / where" },
|
|
99
|
+
{ list: [...(m.mins ?? []), ...(m.maxes ?? [])] as unknown as Projection[], sink: "an extremum group / value / where" },
|
|
100
|
+
{ list: (m.orderedReads ?? []) as unknown as Projection[], sink: "an ordered-read key / where" },
|
|
101
|
+
];
|
|
102
|
+
for (const { list, sink } of reads) {
|
|
103
|
+
for (const p of list) {
|
|
104
|
+
const type = typeOf(p);
|
|
105
|
+
if (type === undefined) continue;
|
|
106
|
+
const enc = encByType.get(type);
|
|
107
|
+
if (!enc) continue;
|
|
108
|
+
for (const f of fieldsOf(p)) {
|
|
109
|
+
if (f !== undefined && enc.has(f)) {
|
|
110
|
+
throw new FlowGateError(
|
|
111
|
+
`[nomosFlowGate rule 1 — no encrypt-then-index] '${type}.${f}' is .encrypted() but is used as ${sink} ` +
|
|
112
|
+
`('${p.id ?? p.queryId ?? "?"}'). Grouping / indexing / filtering / summing BY a value observes it — the ` +
|
|
113
|
+
`encryption is defeated and the ciphertext-size + membership metadata leaks. Encrypt the field OR index a ` +
|
|
114
|
+
`plaintext one, never both.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── RULE 2 — NO PRIVATE READ RESULT ON THE CHAIN ────────────────────────────
|
|
123
|
+
// A captured read (`.reads(Q)` on the write path) whose Q returns a private type
|
|
124
|
+
// would write its PLAINTEXT result onto the intent on main — public, signed,
|
|
125
|
+
// replay-verified — bypassing the read gate AND the encryption envelope. Private
|
|
126
|
+
// data may only leave via the gated read path (query/qbyid), never a captured read.
|
|
127
|
+
const privateTypes = new Set(m.aggregates.filter((a) => a.requiresRead !== undefined).map((a) => a.id));
|
|
128
|
+
if (privateTypes.size > 0) {
|
|
129
|
+
for (const d of m.directives) {
|
|
130
|
+
for (const cr of (d.capturedReads ?? []) as { queryId: string; returns: string }[]) {
|
|
131
|
+
if (privateTypes.has(cr.returns)) {
|
|
132
|
+
throw new FlowGateError(
|
|
133
|
+
`[nomosFlowGate rule 2 — no private read on the chain] directive '${d.id}': captured read '${cr.queryId}' ` +
|
|
134
|
+
`returns the PRIVATE type '${cr.returns}'. A .reads() of a requires(cap) query writes its plaintext result ` +
|
|
135
|
+
`onto the intent on main as public, signed, replay-verified data — bypassing the read gate. Private data may ` +
|
|
136
|
+
`only leave via the gated read path (query / qbyid), never a captured read.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── RULE 4 — NO SECRET-CORRELATED REFUSAL (abort-cohort, Send Prop 4) ────────
|
|
144
|
+
// A refusal is a PUBLIC ref transition (refs/heads/{refused,parked}/<offer>). If the
|
|
145
|
+
// DECISION to refuse depends on a secret, the refusal leaks it. Conservative + declared-
|
|
146
|
+
// set based (no predicate-body analysis): an invariant that READS a private aggregate may
|
|
147
|
+
// correlate its refusal with that secret. This FLAGS (require declassify) rather than
|
|
148
|
+
// hard-refusing, because a secret-gated refusal is often INTENDED (the overdraft case).
|
|
149
|
+
if (mod && privateTypes.size > 0) {
|
|
150
|
+
// 4a — an aggregate `invariant` over a private aggregate with a plaintext field.
|
|
151
|
+
for (const a of mod.aggregates ?? []) {
|
|
152
|
+
if (!a.hasInvariant || !privateTypes.has(a.id) || a.invariantDeclassify) continue;
|
|
153
|
+
const enc = new Set(a.encryptedFields ?? []);
|
|
154
|
+
const hasPlaintext = Object.keys(a.fields ?? {}).some((f) => !enc.has(f));
|
|
155
|
+
if (!hasPlaintext) continue; // fully-encrypted: the invariant reads only ciphertext — can't leak this way
|
|
156
|
+
throw new FlowGateError(
|
|
157
|
+
`[nomosFlowGate rule 4 — no secret-correlated refusal] aggregate '${a.id}' is private (requires a read cap) and ` +
|
|
158
|
+
`declares an invariant over plaintext field(s) — its refusal is a public ref transition that can correlate with ` +
|
|
159
|
+
`the secret. If that leak is intended, acknowledge it once: ` +
|
|
160
|
+
`aggregate('${a.id}', fields, { invariant, invariantDeclassify: "<why a refusal reveals this>" }).`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
// 4b — a `workspaceInvariant` whose DECLARED reads touch a private aggregate (in this domain).
|
|
164
|
+
for (const wi of mod.workspaceInvariants ?? []) {
|
|
165
|
+
if (wi.declassify) continue;
|
|
166
|
+
let refs: readonly { readonly aggregate: string }[];
|
|
167
|
+
try {
|
|
168
|
+
refs = wi.reads({ intent: new Proxy({}, { get: () => "" }) as Record<string, unknown>, principal: "" }) ?? [];
|
|
169
|
+
} catch {
|
|
170
|
+
continue; // the read-set can't be resolved on a probe intent — skip (no false refusal; conservative on availability)
|
|
171
|
+
}
|
|
172
|
+
for (const r of refs) {
|
|
173
|
+
const short = r.aggregate.includes(".") ? r.aggregate.slice(r.aggregate.lastIndexOf(".") + 1) : r.aggregate;
|
|
174
|
+
if (privateTypes.has(short)) {
|
|
175
|
+
throw new FlowGateError(
|
|
176
|
+
`[nomosFlowGate rule 4 — no secret-correlated refusal] workspaceInvariant '${wi.id}' (on '${wi.on}') reads the ` +
|
|
177
|
+
`PRIVATE aggregate '${r.aggregate}' — its refusal is a public ref transition that can correlate with that secret. ` +
|
|
178
|
+
`If intended (e.g. an overdraft refusal reveals balance < amount), acknowledge it once: ` +
|
|
179
|
+
`workspaceInvariant('${wi.id}').on(...).reads(...).declassify("<why a refusal reveals this>").assert(...).`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── RULE 5 — NO PRIVATE BYTES IN AN OUTBOUND OFFER (Send's external send) ────
|
|
187
|
+
// The outbox (refs/heads/outbox/<offer>) POSTs a capability's obligation to its endpoint —
|
|
188
|
+
// a DIFFERENT observer set (the external adapter). If the capability's task aggregate is
|
|
189
|
+
// private (requires a read cap) or encrypted, those bytes cross the confidentiality boundary.
|
|
190
|
+
// Refuse unless declassified. CURRENTLY VACUOUS BY CONSTRUCTION (capability task aggregates are
|
|
191
|
+
// public, and rule 2 severs the plan from foreign secrets — so nothing private reaches the
|
|
192
|
+
// outbox), but this ARMS the fifth and last sink so the static analysis is provably complete:
|
|
193
|
+
// it fires the moment a private/encrypted capability becomes expressible.
|
|
194
|
+
if (mod && privateTypes.size > 0) {
|
|
195
|
+
for (const cap of mod.impureCapabilities ?? []) {
|
|
196
|
+
if (!cap.endpoint || cap.declassify) continue;
|
|
197
|
+
if (privateTypes.has(cap.aggregate.id)) {
|
|
198
|
+
const name = cap.name ?? cap.capability ?? cap.aggregate.id;
|
|
199
|
+
throw new FlowGateError(
|
|
200
|
+
`[nomosFlowGate rule 5 — no private bytes in an outbound offer] capability '${name}' POSTs its obligation to ` +
|
|
201
|
+
`'${cap.endpoint}', but its task aggregate '${cap.aggregate.id}' is PRIVATE — the outbound bytes cross to a ` +
|
|
202
|
+
`different observer set (the external adapter). If that send is intended, acknowledge it once: ` +
|
|
203
|
+
`capability('${name}', { request, endpoint, declassify: "<why these bytes may leave the workspace>" }).`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
/**
|
|
9
9
|
* framework/capability.ts — `capability()`: THE MARKETPLACE DECLARATION
|
|
10
10
|
* (`architecture/capability_marketplace.md` §4, ruling M2 — the framework-hidden
|
|
11
|
-
* lane). ONE call declares a typed
|
|
12
|
-
* sees a typed
|
|
11
|
+
* lane). ONE call declares a typed Obligation→Receipt capability interface; the dev
|
|
12
|
+
* sees a typed obligation call and a typed task row, and the framework supplies
|
|
13
13
|
* everything else through the SHIPPED `impureCapability()` quartet — so the gate
|
|
14
|
-
* law (`
|
|
14
|
+
* law (`check_obligation_receipt`), the kernel recognizer, codegen, and the stable-id
|
|
15
15
|
* lockfile all see a perfectly ordinary impure capability. Hidden means *not
|
|
16
|
-
* hand-rolled*, never *not auditable*: the
|
|
16
|
+
* hand-rolled*, never *not auditable*: the Obligation and Receipt facts ride the
|
|
17
17
|
* ledger verbatim.
|
|
18
18
|
*
|
|
19
19
|
* ```ts
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* receipt: { tokensIn: t.int(), tokensOut: t.int() },
|
|
23
23
|
* });
|
|
24
24
|
* // derived, deterministically from the name:
|
|
25
|
-
* // aggregate LlmCompleteTask
|
|
25
|
+
* // aggregate LlmCompleteTask obligation obligeLlmComplete
|
|
26
26
|
* // complete completeLlmComplete fail/block/deadLetter likewise
|
|
27
27
|
* // capability "llm.complete" (the CapabilityBinding the executor must hold)
|
|
28
28
|
* ```
|
|
@@ -84,7 +84,23 @@ export interface CapabilityDecl<
|
|
|
84
84
|
> extends ImpureCapability<string, RequestShape<Req>, Req & Rec> {
|
|
85
85
|
/** The capability name — the `CapabilityBinding` slot the executor must hold. */
|
|
86
86
|
readonly name: string;
|
|
87
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* THE CAPABILITY ENDPOINT (outbox / `capability_offers_https.md`): the HTTPS URL
|
|
89
|
+
* the obligation is offered to. Sealed into the law (the `nomosCapabilities` key);
|
|
90
|
+
* the bailiff reads it from `declared_capabilities` and POSTs each requested
|
|
91
|
+
* obligation there ("a capability is a holon at a URL"). Omitted ⇒ obligations
|
|
92
|
+
* rest at `requested` with nowhere to go (the deploy notice says so).
|
|
93
|
+
*/
|
|
94
|
+
readonly endpoint?: string;
|
|
95
|
+
/**
|
|
96
|
+
* THE DECLASSIFY WAIVER (nomosFlowGate rule 5 — no private bytes in an outbound offer): present
|
|
97
|
+
* iff the author acknowledged that this capability POSTs data derived from a private/encrypted
|
|
98
|
+
* source to its endpoint — a DIFFERENT observer set (the external adapter, Send's external send).
|
|
99
|
+
* Rides the `nomosFlowGate` era key (replay-verified audit surface). Absent on an emitting
|
|
100
|
+
* capability over a private task aggregate ⇒ the flow gate refuses to compile.
|
|
101
|
+
*/
|
|
102
|
+
readonly declassify?: string;
|
|
103
|
+
/** The request fields' zod object (what `oblige<Pascal>`'s params validate as). */
|
|
88
104
|
readonly requestSchema: z.ZodObject<RequestShape<Req>>;
|
|
89
105
|
/**
|
|
90
106
|
* THE EXECUTOR'S SCAN LANE — a declared, indexed query over the task
|
|
@@ -112,7 +128,7 @@ export function capability<
|
|
|
112
128
|
const Name extends string,
|
|
113
129
|
Req extends Record<string, Field>,
|
|
114
130
|
Rec extends Record<string, Field> = Record<never, Field>,
|
|
115
|
-
>(name: Name, spec: { request: Req; receipt?: Rec }): CapabilityDecl<Req, Rec> {
|
|
131
|
+
>(name: Name, spec: { request: Req; receipt?: Rec; endpoint?: string; declassify?: string }): CapabilityDecl<Req, Rec> {
|
|
116
132
|
if (!CAPABILITY_NAME_RE.test(name)) {
|
|
117
133
|
throw new Error(
|
|
118
134
|
`capability('${name}'): the name must match ${CAPABILITY_NAME_RE} (lowercase, dot-namespaced — e.g. 'llm.complete'); it doubles as the CapabilityBinding slot the executor must hold`,
|
|
@@ -127,7 +143,7 @@ export function capability<
|
|
|
127
143
|
for (const k of Object.keys(fields)) {
|
|
128
144
|
if (reserved.has(k)) {
|
|
129
145
|
throw new Error(
|
|
130
|
-
`capability('${name}'): ${side} field '${k}' shadows the reserved
|
|
146
|
+
`capability('${name}'): ${side} field '${k}' shadows the reserved Obligation→Receipt envelope — rename it (the envelope is framework law: ${[...reserved].join(", ")})`,
|
|
131
147
|
);
|
|
132
148
|
}
|
|
133
149
|
}
|
|
@@ -135,7 +151,7 @@ export function capability<
|
|
|
135
151
|
for (const k of Object.keys(receipt)) {
|
|
136
152
|
if (k in spec.request) {
|
|
137
153
|
throw new Error(
|
|
138
|
-
`capability('${name}'): '${k}' is declared on BOTH request and receipt — a field has one writer (the
|
|
154
|
+
`capability('${name}'): '${k}' is declared on BOTH request and receipt — a field has one writer (the obligation writes requests, the complete Receipt writes receipts); split or rename it`,
|
|
139
155
|
);
|
|
140
156
|
}
|
|
141
157
|
}
|
|
@@ -144,7 +160,7 @@ export function capability<
|
|
|
144
160
|
const aggregateId = `${pascal}Task`;
|
|
145
161
|
|
|
146
162
|
// Receipt fields fold only when the complete Receipt lands — auto-`.optional()`
|
|
147
|
-
// so a freshly-
|
|
163
|
+
// so a freshly-obligated task decodes on the generated read type (the same
|
|
148
164
|
// read-decode discipline the envelope's own optionals follow).
|
|
149
165
|
const receiptFields = Object.fromEntries(
|
|
150
166
|
Object.entries(receipt).map(([k, f]) => [k, f.optional()]),
|
|
@@ -162,7 +178,7 @@ export function capability<
|
|
|
162
178
|
|
|
163
179
|
const quartet = impureCapability({
|
|
164
180
|
aggregateId,
|
|
165
|
-
|
|
181
|
+
obligationDirectiveId: `oblige${pascal}`,
|
|
166
182
|
completeDirectiveId: `complete${pascal}`,
|
|
167
183
|
failDirectiveId: `fail${pascal}`,
|
|
168
184
|
blockDirectiveId: `block${pascal}`,
|
|
@@ -204,6 +220,8 @@ export function capability<
|
|
|
204
220
|
aggregate: taskAggregate,
|
|
205
221
|
instanceId: impureCapabilityInstanceId,
|
|
206
222
|
name,
|
|
223
|
+
...(spec.endpoint !== undefined ? { endpoint: spec.endpoint } : {}),
|
|
224
|
+
...(spec.declassify !== undefined ? { declassify: spec.declassify } : {}),
|
|
207
225
|
requestSchema: z.object(requestShape),
|
|
208
226
|
receiptSchema: z.object(receiptShape),
|
|
209
227
|
encodeResult: (values) => JSON.stringify(z.object(receiptShape).parse(values)),
|
|
@@ -40,7 +40,15 @@ import { directive } from "../directive.js";
|
|
|
40
40
|
import { set, withMarker, type PlannedOp } from "../ops.js";
|
|
41
41
|
import { query } from "../query.js";
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// A SCOPE is the read cap (relation), OPTIONALLY object-qualified for a PER-RECORD scope:
|
|
44
|
+
// `<relation>` — workspace-wide (≡ `workspace:self#<relation>`; the era rule "no `#` ⇒
|
|
45
|
+
// workspace:self" keeps every pre-feature chain byte-identical), or
|
|
46
|
+
// `<type>:<id>#<relation>` — per-record (the roster = Expand(<type>:<id>#<relation>), the §6 grain).
|
|
47
|
+
// The object half mirrors the relation-tuple OBJECT_RE; the relation half is a cap name. `#` is in neither
|
|
48
|
+
// char class, so the split is unambiguous. The kernel parses the same way (executor `parse_scope_object`).
|
|
49
|
+
const REL_PART = "[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}";
|
|
50
|
+
const OBJ_PART = "[A-Za-z0-9][A-Za-z0-9_.-]*:[A-Za-z0-9][A-Za-z0-9_.:/-]*";
|
|
51
|
+
const CAP_RE = new RegExp(`^(?:${OBJ_PART}#)?${REL_PART}$`);
|
|
44
52
|
const B64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
45
53
|
|
|
46
54
|
/** Deterministic ids — the scope head + a per-member wrap, both replay-stable. */
|
package/src/framework/genesis.ts
CHANGED
|
@@ -81,7 +81,11 @@ export const seedGenesis = directive("seedGenesis")
|
|
|
81
81
|
.payload(
|
|
82
82
|
z.object({
|
|
83
83
|
principal: z.string().min(1),
|
|
84
|
-
|
|
84
|
+
// EMPTY ⇒ a CONTROL-PLANE genesis: records the founding principal (arming the kernel's genesis
|
|
85
|
+
// bootstrap clause for the founding admin grant) WITHOUT a self-id, so the workspace stays
|
|
86
|
+
// UNWARRANTED (host-stamped authorship, like root) — deploys/governance/child births keep working
|
|
87
|
+
// unsigned. NON-EMPTY ⇒ a TENANT (signed-author) self-id (the source binding author signatures cover).
|
|
88
|
+
workspaceId: z.string(),
|
|
85
89
|
bornAt: z.string().min(1),
|
|
86
90
|
}),
|
|
87
91
|
)
|