OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay commits / dd35e86

feat: add application privacy policy SDK
dd35e8670c8817c151b8c72b8d13dd7b71d00d26
by bigdestiny2 <33146784+bigdestiny2@users.noreply.github.com> · 2026-07-29T21:07:57.000Z

Changed files (15) +4011 -18

.github/workflows/test.yml +16 -0
……@@ -29,6 +29,22 @@ jobs:
2929 - run: npm ci
3030 - run: npm run test:unit
3131
32 blind-sdk-tests:
33 runs-on: ubuntu-latest
34 steps:
35 - uses: actions/checkout@v7
36 - uses: actions/setup-node@v6
37 with:
38 node-version: 22
39 cache: npm
40 - run: npm ci
41 - run: >-
42 ./node_modules/.bin/brittle-node --timeout 120000
43 packages/blind-client/test/policy.test.js
44 packages/blind-client/test/cells.test.js
45 - run: ./node_modules/.bin/brittle-node --timeout 120000 test/simulation/blind-capacity-lab.test.js
46 - run: npm run verify:blind-client:browser-artifact:release
47
3248 integration-tests:
3349 runs-on: ubuntu-latest
3450 steps:
CHANGELOG.md +17 -0
……@@ -6,6 +6,23 @@ documented here. Dates in YYYY-MM-DD.
66
77The packages are versioned in lockstep.
88
9## [Unreleased]
10
11### Added
12- **Draft application privacy policy SDK.** `@hiverelay/blind-client/policy`
13 provides pure workload placement, privacy presets, durability and cost
14 constraints, stable blocked/queueable outcomes, and explicit evidence
15 requirements without treating inventory hints as executable qualification.
16- **Exact Cell envelope capacity accounting.** Capacity report schema v3 binds
17 the current wire-authority hashes and charges the minimum-fitting symmetric
18 v1 outer class for each Cell request/result direction, replica, and fanout.
19
20### Documentation
21- Added the complete Application Privacy SDK v1 contract, including the
22 proposed Pear Deploy compiler boundary, and the staged Blind Envelope
23 Efficiency Plan. The root README now links both candidate replacement
24 surfaces and keeps them distinct from the published legacy fleet.
25
926## [0.24.3] — 2026-07-08
1027
1128### Added
README.md +15 -0
……@@ -18,6 +18,15 @@ maps existing semantic relay patterns onto generic cells, encrypted cores,
1818opaque inboxes, signed discovery, and client-owned validation/repair without an
1919operator install or per-application configuration.
2020
21The draft [`@hiverelay/blind-client` policy SDK](packages/blind-client/README.md)
22adds pure workload, privacy, durability, and cost planning without manufacturing
23runtime evidence. Its complete contract is the
24[Application Privacy SDK v1](docs/protocol/HIVERELAY-APPLICATION-PRIVACY-SDK-V1.md),
25and the exact current-wire cost model plus safe protocol evolution plan is the
26[Blind Envelope Efficiency Plan](docs/protocol/HIVERELAY-BLIND-ENVELOPE-EFFICIENCY-PLAN.md).
27Both are candidate replacement surfaces, not claims about the published legacy
28fleet.
29
2130The currently published `v0.24.3` fleet is still the application-aware legacy
2231implementation. It must not be described as the blind substrate. This branch is
2332building its complete replacement and intentionally refuses to bind production
……@@ -84,6 +93,12 @@ The repo has moved from a relay prototype into a Core3 infrastructure stack:
8493- **Blind substrate for encrypted workloads**: atomic custody processes
8594 ciphertext only, rejects plaintext/key-material fields at the schema boundary,
8695 and can prove non-serving after expiry without decrypting content.
96- **Application privacy policy SDK and exact capacity accounting**: the draft
97 blind client routes records, rendezvous, history, live traffic, and semantic
98 work through explicit privacy/durability/cost constraints; the capacity lab
99 reports exact minimum-fitting v1 Cell outer-envelope classes and provenance.
100 Draft plans remain non-executable until endpoint, adapter, health, capture,
101 consent, and conformance evidence is qualified.
87102- **Verified durability and repair**: Ed25519 anchor proofs decide whether a
88103 peer counts toward archive replication; AutoHeal recruits diverse relays and
89104 repairs missing blocks peer-to-peer once a publisher has been online once.
docs/protocol/HIVERELAY-APPLICATION-PRIVACY-SDK-V1.md +1320 -0
……@@ -0,0 +1,1320 @@
1# HiveRelay Application Privacy SDK v1
2
3Status: proposed application SDK contract; not a public protocol authority and
4not release evidence.
5
6This specification defines the proposed developer-facing composition layer
7above the frozen HiveRelay blind substrate. It makes workload placement,
8transport privacy, durability, downgrade behavior, cost, recovery, and
9non-blind service disclosure explicit without adding application semantics to
10a relay.
11
12The intended product surface is Pear Deploy: deployment intent fixes the
13minimum route policies and required evidence for a release, while this SDK
14performs fresh, operation-bound qualification and execution at runtime. Pear
15Deploy is the compiler, infrastructure, and evidence plane; it is not an
16application-data proxy, capability vault, or substitute for runtime privacy
17checks.
18
19The implementation target is `@hiverelay/blind-client`. The existing
20`p2p-hiverelay-client` package remains a legacy Hyperdrive/service compatibility
21surface and may expose a thin `/blind` re-export during migration. It MUST NOT
22silently reinterpret its current app-aware methods as strict blind operations.
23
24Examples below describe a target API, not the package's current exported
25surface. In particular, a pure `/policy` helper, a registry constant, or an
26availability boolean is not an executable privacy path. `BlindClient`, durable
27journal/vault backends, and each transport adapter remain unavailable until the
28corresponding package export, runtime implementation, conformance tests, and
29evidence exist.
30
31## 1. Design rule
32
33Privacy is not one ascending mode that every byte should traverse. The SDK MUST
34resolve these dimensions independently:
35
361. workload semantics and the correct substrate primitive;
372. representation privacy and replica correlation;
383. network transport and adjacent-role visibility;
394. metadata shaping;
405. identity exposure;
416. read-interest exposure;
427. durability evidence and repair policy; and
438. downgrade or external-service disclosure.
44
45Presets make common choices easy. Every draft, prepared plan, result, and UI
46status exposes the orthogonal decisions so a preset can never hide a tradeoff.
47The SDK reports representation, transport, read-interest, and durability claims
48separately. An aggregate label MUST NOT imply the strongest component when any
49other component has a lower or unverified ceiling.
50
51## 2. Package boundary
52
53The target package surface is:
54
55```text
56@hiverelay/blind-client
57 /policy pure, non-executable draft planner
58 /cells immutable opaque records
59 /inbox bounded rendezvous
60 /core native encrypted streams
61 /journal durable logical intents and ambiguous results
62 /vault capabilities and encrypted recovery state
63 /selection relay and operator-diversity policy
64 /transports/direct
65 /transports/ohttp
66 /transports/native
67 /transports/tor
68 /runtime/browser
69 /runtime/node
70 /runtime/bare
71 /testing conformance fixtures
72```
73
74The existing byte-oriented Cells, Inbox, Core, qualification, intent, attempt,
75selection, and durability modules remain the security foundation. The new layer
76coordinates them; it does not copy their codecs or relax their endpoint pins.
77
78The existing `p2p-hiverelay-client/privacy-policy.js` is useful prior art for
79orthogonal axes and fail-closed path selection, but its legacy path vocabulary
80does not model workload representation, OHTTP, primitive-specific durability,
81operation-bound qualification, or external semantic disclosure. It remains
82unchanged. A future compatibility subpath may re-export this contract only after
83version, package-closure, and cross-runtime gates pass; the legacy resolver is
84not silently treated as an `AppPrivacyIntentV1` compiler.
85
86`/policy` has no network authority. Its output MUST NOT be accepted by
87`execute()`, a transport adapter, or an intent sender. Only
88`BlindClient.prepare()` (and the `BlindClient.plan()` convenience form that
89calls it) may combine a draft with fresh qualification and return an opaque
90executable plan.
91
92Until an adapter and its capture/conformance evidence exist, it is unselectable.
93A registry constant, relay advertisement, configuration flag, or caller-supplied
94boolean alone never makes a path deployable or an axis satisfied.
95
96## 3. Workload routing
97
98The SDK uses these closed workload and primitive classes:
99
100| Workload | Default primitive | Permitted primitives | Intended use | Important limit |
101| --- | --- | --- | --- | --- |
102| `records` | `cell` | `cell` | Signed immutable records, random access, browser state | No cross-cell transaction or semantic query |
103| `rendezvous` | `inbox` | `inbox` | Wake-up hints, bounded discovery, invitations | Not history, authority, or completeness |
104| `history` | `core` natively; `cell` plus checkpoints in ordinary browsers | `core`, `cell` | Append streams, bulk history, larger media manifests | Core needs an authenticated stream transport |
105| `live` | `p2p-direct` | `p2p-direct` | Calls, games, cursors, live collaboration | Not durable; source/path metadata is visible |
106| `semantic` | `local-compute` when possible; otherwise `external` | `local-compute`, `external` | Search, ranking, AI, transcoding, consensus | External execution is outside the blind substrate trust boundary |
107
108The closed primitive IDs are `cell`, `inbox`, `core`, `p2p-direct`,
109`local-compute`, and `external`. An application MAY override a representation
110only within the permitted set above. Any other pair is blocked with
111`WORKLOAD_PRIMITIVE_UNSUPPORTED`; it MUST NOT fall through to a generic storage
112or transport planner. In particular, records cannot be relabeled as P2P or
113external work, ordinary-browser Core cannot be enabled by a boolean, and
114semantic work cannot be presented as encrypted Cell/Inbox/Core input.
115
116`operation` remains the application workload verb. A separate canonical
117`wireOperation` binds executable substrate work. Records `put|get` map to
118`CELL.PUT|GET`; rendezvous verbs map to their exact Inbox operations; native
119history `put` maps to `CORE.MIRROR` and history `get` maps to
120`CORE.OPEN_REPLICATION`. Core proof challenges are durability/qualification
121work, not a relabelled application read. P2P and semantic routes have no blind
122wire operation. A planner must never emit a generic `put` as a Core wire verb.
123
124`local-compute` means application-owned code operating on locally available
125plaintext under the application's own process/origin boundary. It performs no
126relay request, needs no external-service disclosure, and makes no claim about
127the privacy of later synchronization. `external` always requires the disclosure
128and consent contract in section 11.
129
130Large media SHOULD use direct P2P or Blind Core for the body and Cells for signed
131manifests, thumbnails, keys, and checkpoints. An ordinary browser MAY use
132chunked Cells, but the plan MUST expose the resulting transaction count and
133blind-envelope and adapter/fleet amplification before execution. If no
134app-owned signed manifest is supplied, the result is
135`CHUNK_MANIFEST_REQUIRED`, never an executable single-Cell plan.
136
137Legacy BlindShard/PVSS custody is not a generic SDK primitive or an automatic
138durability upgrade. It multiplies ciphertext/share placement, roster and proof
139work, and repair coordination while retaining publisher/custody linkage and
140giving public readers threshold-share fetch behavior. A product that genuinely
141needs threshold custody must select a separately versioned experimental custody
142profile with its own operator-set assumptions, cost model, recovery semantics,
143and disclosure; ordinary records use independent Cell replicas instead.
144
145## 4. Privacy presets and custom constraints
146
147The required fixed presets are:
148
149| Preset | Browser | Native | Downgrade | Intended behavior |
150| --- | --- | --- | --- | --- |
151| `fast` | `direct-blind-v1` | `direct-blind-v1` | none | Lowest latency; storage sees adjacent source and access pattern |
152| `private` | `split-web-ohttp-v1` | `split-native-protomux-v1` | explicit prompt only | Normal source-separated path under non-collusion assumptions |
153| `high-privacy` | Tor Browser full-onion path | `tor-native-full-v1` | deny | Background-preferred and fail-closed; no clearnet race |
154
155Fixed presets are immutable closed values. `privacy.fast()`,
156`privacy.private()`, and `privacy.highPrivacy()` accept no field overrides;
157attempting one is `PRESET_IMMUTABLE`. Applications needing different behavior
158use `privacy.custom()`, optionally naming a preset as a base whose resolved
159fields are copied into a new custom intent. A custom intent does not modify or
160extend a canonical transport profile or wire registry.
161
162`custom` accepts constraints over:
163
164```js
165{
166 base: undefined | 'fast' | 'private' | 'high-privacy',
167 transport: 'direct' | 'source-separated' | 'tor',
168 metadataShaping: 'none' | 'padded' | 'bucketed-experimental',
169 identityExposure: 'stable' | 'session' | 'anonymous-reply',
170 readInterest: 'observable' | 'bucketed-experimental',
171 downgradePolicy: 'deny' | 'prompt',
172 backgroundPreferred: boolean
173}
174```
175
176Every pure-draft axis has an explicit prospective resolution record:
177
178```js
179{
180 requested,
181 planned,
182 state: 'qualification-required' |
183 'adapter-qualification-required' |
184 'connector-qualification-required' |
185 'session-preparation-required' |
186 'application-policy-required' |
187 'known-visible' |
188 'unsupported' |
189 'not-applicable',
190 requiredEvidence: ['closed-evidence-id'],
191 assumptions: [],
192 claimCeiling
193}
194```
195
196Every state ending in `-required` has a non-empty `requiredEvidence` set. The
197pure scaffold exports the closed `EVIDENCE_REQUIREMENT` IDs for signed profile,
198verified endpoint, fresh health, adapter/conformance capture, class or stream
199shaping, application identity boundary, Inbox anonymous reply, universal read
200buckets, P2P session preparation, role separation, no-direct-race,
201no-clearnet, browser opaque origin, external endpoint/connector, and
202disclosure authorization evidence. Unknown evidence IDs fail closed when a
203prepared plan or Pear Deploy release predicate resolves them; an empty set can
204never make a required state satisfied.
205
206The SDK MUST NOT report an axis as satisfied merely because the caller requested
207it. Satisfaction requires the primitive or compatible runtime adapter, signed
208endpoint and transport profile, fresh qualification, and the evidence named by
209that axis. `unsupported` is blocked. `unverified` cannot become `ready`; it is
210`queueable` only when a conforming adapter/path may later obtain the missing
211evidence, otherwise it is blocked. In particular,
212`bucketed-experimental`, `anonymous-reply`, role non-collusion, Tor no-clearnet,
213and Tor Browser opaque-origin claims each need their own evidence and cannot be
214inferred from transport selection.
215
216`not-applicable` is permitted only when the closed workload/primitive pair has
217no such surface, such as network transport for `local-compute`. It is not a way
218to erase a caller-requested constraint from a networked operation. Pure drafts
219have no `actual` field and never copy `requested` into `satisfied`. Only a
220prepared plan may add `actual`, `evidenceRefs`, and the final
221`satisfied | unsupported | unverified | not-applicable` state.
222
223Plans and results expose at least:
224
225```js
226{
227 privacyResolution: {
228 transport,
229 metadataShaping,
230 identityExposure,
231 readInterest
232 },
233 claims: {
234 representation,
235 transport,
236 readInterest,
237 durability
238 }
239}
240```
241
242Representation is a primitive claim rather than a caller-selectable privacy
243axis, so it lives under `claims.representation`; it MUST NOT be duplicated as a
244second, independently satisfiable axis. A prepared plan may add `actual`,
245`evidenceRefs`, and final satisfaction state to each `privacyResolution` member
246without renaming this shape.
247
248`prompt` grants permission to present a fallback proposal, not to take it. When
249the requested path is temporarily unavailable, the preparation result remains
250`queueable` under the requested policy and MAY attach
251`DOWNGRADE_CONSENT_REQUIRED` with one suggested lower profile; an
252interactive-only caller MAY instead request a blocked result. It never executes
253the proposal. After visible consent, the future qualified-client
254`authorizeDowngrade()` operation issues an opaque,
255expiring, single-use local authorization bound to the draft ID,
256requested-policy hash, workload/primitive, suggested actual profile, disclosure
257hash if any, and user-visible prompt version. `prepare()` additionally binds the
258resulting ready plan to the exact endpoint/path. A generic boolean, stored
259preference, consent for another draft, or application restart is not
260authorization. `high-privacy` always uses `deny` and cannot mint a downgrade
261authorization.
262
263`authorizeDowngrade()` is not exported by the pure `/policy` module. A pure
264draft rejects caller-supplied downgrade or disclosure authorizations; the
265future `BlindClient` qualification boundary owns their creation and use.
266
267OHTTP and Tor do not hide which locator storage serves. Read-interest privacy is
268`observable` unless a separately qualified universal bucket/PIR profile is in
269use.
270
271## 5. Public API
272
273The ergonomic target separates pure drafting from evidence-qualified
274preparation:
275
276```js
277import { BlindClient } from '@hiverelay/blind-client'
278import { RUNTIME, draftWorkload, privacy } from '@hiverelay/blind-client/policy'
279
280const app = await BlindClient.open({
281 storage,
282 bootstrap,
283 // Each value is an evidence-bearing adapter, not an availability boolean.
284 transports: { direct, ohttp, native, tor, p2p },
285 privacy: privacy.private(),
286 durability: {
287 cell: {
288 replicaTarget: 3,
289 acknowledgementTarget: 2,
290 readbackTarget: 2,
291 fetchTarget: 1,
292 independentOperatorGroups: 2
293 },
294 inbox: {
295 stripeCountLog2: 3,
296 replicaTargetPerStripe: 3,
297 appendAcknowledgementTargetPerStripe: 2,
298 readbackTargetPerStripe: 2,
299 independentOperatorGroups: 2
300 },
301 core: {
302 mirrorTarget: 3,
303 proofTarget: 1,
304 recentlyServedTarget: 2,
305 independentOperatorGroups: 2
306 }
307 }
308})
309
310const draft = draftWorkload({
311 workload: { kind: 'records', byteLength: encoded.byteLength },
312 runtime: RUNTIME.BROWSER,
313 privacy: privacy.private(),
314 costBudget: { maxBlindEnvelopeAmplification: 32 }
315})
316
317if (draft.status === 'draft') {
318 const prepared = await app.prepare(draft, encoded, { signal })
319 if (prepared.status === 'ready') {
320 const result = await app.execute(prepared.plan, encoded, { signal })
321 } else if (prepared.status === 'queueable') {
322 await app.enqueue(draft, encoded, { signal })
323 }
324}
325```
326
327`app.plan(input, encoded, options)` is a convenience for
328`draftWorkload(input)` followed by `app.prepare(draft, encoded, options)`. It is
329asynchronous and returns the same evidence-qualified `ready`, `queueable`, or
330`blocked` preparation result. A `/policy` draft is never called a ready plan.
331`enqueue()` is valid only for locally durable mutation workloads; it journals
332the signed logical intent and does not perform network I/O.
333
334Convenience scopes make the same planner explicit:
335
336```js
337const records = app.cells({ privacy: privacy.private() })
338const rendezvous = app.inbox({ privacy: privacy.private(), stripeCountLog2: 3 })
339const history = app.core({ privacy: privacy.fast() })
340const live = app.p2p({ privacy: privacy.fast() })
341
342const localSearch = app.localCompute('search', {
343 purpose: 'full-text search',
344 executor: localIndex.search
345})
346
347const search = app.external('search', {
348 endpoint,
349 networkPrivacy: privacy.private(),
350 disclosure: {
351 serviceId: 'search-provider-v1',
352 purpose: 'full-text search',
353 operatorSees: ['query', 'result selection', 'timing'],
354 authority: 'advisory',
355 retention: 'provider policy',
356 endpointEvidence
357 },
358 disclosureAuthorization
359})
360```
361
362Labels such as `search` or a local collection name remain local encrypted state.
363They MUST NOT enter a relay request, capability, receipt, log, metric, descriptor,
364or admission token.
365
366## 6. Draft and prepared-plan contract
367
368| Producer | Possible status | May perform network I/O | Executable output |
369| --- | --- | --- | --- |
370| `draftWorkload()` from `/policy` | `draft`, `queueable`, `blocked` | No | Never |
371| `BlindClient.prepare()` / `BlindClient.plan()` | `ready`, `queueable`, `blocked` | Qualification only; no application mutation send | Only the opaque `plan` inside `ready` |
372| `BlindClient.execute()` | Operation result, `queueable`, `PENDING_UNKNOWN`, or error | Yes, under the exact prepared plan | Not applicable |
373
374### 6.1 Pure policy draft
375
376`draftWorkload()` is deterministic, side-effect free, and performs no network
377I/O. Its result is one of:
378
379- `draft`: the closed workload/primitive pair and requested policy are
380 internally valid and may be passed to `prepare()`;
381- `queueable`: the policy is valid but the supplied deployment-inventory hint
382 lacks a candidate path, or a proposed weaker path still needs explicit
383 consent; a mutation may be journaled locally without sending; or
384- `blocked`: the request is invalid, incompatible, unsupported, over a hard
385 budget, or missing a mandatory disclosure descriptor or manifest authority.
386
387A draft contains desired constraints, primitive-specific durability, exact
388blind-envelope estimates where derivable, unresolved axes, and candidate
389transport profiles. It contains no `VerifiedEndpoint`, no fresh-health claim,
390no admission/capacity claim, and no execution authority.
391Inventory booleans are prospective compilation hints only: they are not a
392capability snapshot, path-health result, or proof that any adapter is qualified.
393Changing an inventory hint may change `draft` to `queueable`, but MUST NOT erase
394the deterministic candidate primitive, `wireOperation`, intended transport,
395axis requirements, representation sizing, cost result, claims, assumptions,
396warnings, durability, or disclosure metadata. This lets `explain` and offline UI
397show what is missing without pretending that absence invalidated the policy.
398
399A pure `queueable` draft reports only a prospective action:
400
401```js
402{
403 prospectiveQueue: {
404 state: 'not-journaled',
405 action: 'journal-mutation-intent' | 'wait-for-path',
406 intentCanBeJournaled: boolean,
407 localVisible: false
408 }
409}
410```
411
412Only `enqueue()` or the journal may later report `local-queued`. Reads and
413watches wait for a path and cannot be journaled as mutations.
414
415### 6.2 Evidence-qualified preparation
416
417`BlindClient.prepare()` may perform qualification I/O and local durable
418preparation. It resolves the draft against installed adapters, fresh signed
419descriptors/health, path-role separation, admission, capacity, consent, and
420cost evidence. Its result is `ready`, `queueable`, or `blocked`. `ready` is
421reserved for an opaque plan that pins:
422
423```text
424workload
425primitive
426runtime
427requested privacy intent
428resolved requested/planned/actual/satisfied state for every axis
429canonical transport profile
430family and operation
431qualified endpoint/path roles, adjacent keys, and transport-profile hash
432primitive-specific durability and evidence targets
433cell/frame/session class where applicable
434payload/request commitment and destination identity
435exact blind-envelope costs and separately qualified adapter/fleet estimates
436cost and latency budgets
437downgrade/disclosure authorization identifiers and hashes
438separate representation, transport, read-interest, and durability claims
439expiry/revalidation boundary
440```
441
442A prepared plan snapshots binary evidence rather than recursively freezing
443caller-owned byte views. It is non-forgeable, operation-specific, bounded in
444lifetime, and cannot be reconstructed from JSON or a pure draft.
445
446Required state/reason codes include:
447
448```text
449PRIVACY_PATH_UNAVAILABLE
450PRIVACY_AXIS_UNSUPPORTED
451PRIVACY_EVIDENCE_UNVERIFIED
452DOWNGRADE_CONSENT_REQUIRED
453DOWNGRADE_AUTHORIZATION_MISMATCH
454TRANSPORT_UNSUPPORTED
455CORE_STREAM_UNAVAILABLE
456P2P_PATH_UNAVAILABLE
457WORKLOAD_PRIVACY_CONFLICT
458WORKLOAD_PRIMITIVE_UNSUPPORTED
459WORKLOAD_PRIMITIVE_CONFLICT
460WORKLOAD_DURABILITY_CONFLICT
461WORKLOAD_DISCLOSURE_CONFLICT
462SEMANTIC_SERVICE_DISCLOSURE_REQUIRED
463SEMANTIC_SERVICE_CONSENT_REQUIRED
464EXTERNAL_SERVICE_UNAVAILABLE
465LOCAL_COMPUTE_UNAVAILABLE
466COST_BUDGET_EXCEEDED
467COST_EVIDENCE_UNAVAILABLE
468CHUNK_MANIFEST_REQUIRED
469RENDEZVOUS_FRAME_TOO_LARGE
470OFFLINE_QUALIFICATION_PENDING
471```
472
473The SDK MUST revalidate endpoint health, signed profile pins, admission,
474capacity, cost ceiling, and local consent immediately before execution. A plan
475cannot be replayed against another endpoint, operation, profile, class, or
476destination. If revalidation becomes temporarily unavailable without disproving
477the policy, a mutation returns to `queueable`; it never races or silently chooses
478a weaker route.
479
480## 7. Operation lifecycle
481
482Every mutation follows:
483
484```text
485POLICY_DRAFT
486 -> DRAFT_LOCAL
487 -> IDENTITY_COMMITTED
488 -> INNER_EVENT_SIGNED
489 -> INTENT_JOURNALED
490 -> LOCAL_VISIBLE
491 -> QUEUEABLE_OFFLINE <-> PREPARING_PATH
492 -> TARGET_PREPARED
493 -> SENT
494 -> ACKNOWLEDGED | PENDING_UNKNOWN
495 -> READBACK_VERIFIED
496 -> REPAIR_SCHEDULED | POLICY_COMPLETE
497```
498
499The logical event is locally durable and visible before network publication. A
500`QUEUEABLE_OFFLINE` intent retains the exact requested policy and retry
501conditions. Waking, reconnecting, or discovering a direct relay does not weaken
502that policy. Qualification may move it to `TARGET_PREPARED` only after all axes,
503consent, costs, and primitive-specific durability targets can be bound.
504
505`TARGET_PREPARED` means the exact destination, operation, profile, class,
506request commitment, fresh replica capability material, and authorization hashes
507have been persisted before the first possible send. Changing any of those fields
508creates a new attempt. A different replica also uses a fresh slot, keys, nonce,
509wrapper, ciphertext, capability set, and request identity; it remains part of
510the same app-level logical event only through the journal's explicit replica
511relationship.
512
513A timeout after possible delivery becomes `PENDING_UNKNOWN`. The SDK reconciles
514the exact request identity and MUST NOT create another logical event or silently
515change destination/profile. A downgrade authorization or external disclosure
516authorization is journaled by opaque identifier and binding hash, never by an
517unbound `true` flag.
518
519Every asynchronous API accepts `AbortSignal` and a bounded deadline. Abort before
520the commit boundary cancels. Abort after a possibly committed send records an
521ambiguous state and preserves reconciliation data.
522
523## 8. Capability vault and recovery
524
525The SDK supplies an encrypted vault interface:
526
527```js
528{
529 read(key),
530 compareAndSwap(key, expectedVersion, encryptedValue),
531 list(prefix),
532 remove(key, expectedVersion),
533 close()
534}
535```
536
537Browser implementations use an origin-isolated IndexedDB backend with multi-tab
538compare-and-swap. Native implementations use an encrypted local store with
539explicit fsync. Application identity keys never become storage-management keys.
540
541The capability taxonomy distinguishes canonical client-composition values from
542opaque SDK vault handles:
543
544| Capability or authority | Scope | Conversion rule |
545| --- | --- | --- |
546| `ReadCellCapV1` | Read and decrypt one exact relay-bound Cell replica | Cannot renew, drop, create, append, or reveal identity authority |
547| `WriteCellCapV1` | Aggregate client-side Cell record containing the read cap plus independent create, renew, and drop private authority | MUST NOT be derived from a read cap or exported where read-only authority was requested |
548| `CellCreateAuthority` | Create the one prepared replica/request | Single-purpose; cannot be used as renew or drop authority |
549| `CellRenewAuthority` | Renew the bound Cell replica | Cannot read, create, or drop |
550| `CellDropAuthority` | Drop the bound Cell replica | Cannot read, create, or renew |
551| `InboxReadCap` | Read/watch one random physical Inbox topic within its bounded shape | Cannot append or manage lifecycle |
552| `InboxAppendCap` | Append within the bound authorization mode, frame classes, and retention policy | Cannot read, renew, or close |
553| `InboxWriteCap` | Aggregate client-side Inbox management record containing read plus create, optional append, renew, and close authority | Least-authority views are explicit handles, never inferred key conversion |
554| `InboxCreateAuthority` | Create the prepared Inbox replica | Cannot append, read, renew, or close |
555| `InboxRenewAuthority` | Renew the bound Inbox replica | Cannot append, read, create, or close |
556| `InboxCloseAuthority` | Close the bound Inbox replica | Cannot append, read, create, or renew |
557| `BlindCoreReadCapV1` | Read/decrypt a witnessed Core history under its fork/head bounds | Does not grant relay admission, mirroring, or application writer authority |
558
559Names ending in `V1` above refer to existing canonical client-composition
560schemas. Other names are proposed opaque SDK handles over separately stored key
561material; this specification does not add them to the public relay wire. Core
562mirroring/admission authority, P2P session keys, application signing identity,
563and external-service credentials are separate authority classes and MUST NOT be
564treated as convertible storage capabilities.
565
566The SDK provides:
567
568```text
569exportRecoveryBundle
570importRecoveryBundle
571inspectCapability
572rotateReplica
573renew
574drop
575challenge
576repair
577```
578
579Recovery exports are encrypted, versioned, authenticated, bounded, and include
580the exact protocol/profile pins needed to reject a downgrade. Export policy may
581select read-only, append-only, lifecycle-management, or full recovery authority;
582it MUST NOT silently widen the requested set. Export UI states plainly that loss
583of read material loses data access, while loss of create/append/renew/drop/close
584material loses the corresponding publication or lifecycle control.
585
586## 9. Durability and UI evidence
587
588One receipt is one acknowledged remote operation, not a quorum. Durability is
589primitive-specific:
590
591| Primitive | Meaningful durability evidence | Claims that are forbidden |
592| --- | --- | --- |
593| Cell | Locally journaled intent; distinct relay-bound acknowledgements; exact GET/PROVE readback; qualified continuity roots/operator groups; lease state | One receipt is not replication; replica count alone is not operator independence |
594| Inbox | Create/manage receipt per replica; append acknowledgement per bounded stripe/replica; read/watch snapshot evidence; lease and retention horizon | Inbox append/read/watch is not durable history, global ordering, authority, or completeness |
595| Core | Mirrored signed head/fork/length; relay acknowledgement; challenged block/proof readback; independent mirror/operator evidence; lease state | Opening a replication stream or observing a peer is not durable mirroring |
596| Direct P2P | Session establishment and peer acknowledgement only | No offline or storage durability; pair with Cell/Core if required |
597| Local compute | Application-defined local persistence of inputs/results | No network replication claim |
598| External service | Provider-specific, explicitly non-blind receipt and retention statement | Never counted as Blind Cell/Inbox/Core durability |
599
600Replica, acknowledgement, readback, operator-diversity, stripe, session, mirror,
601lease, and retention fields therefore live in a tagged per-primitive durability
602schema. Generic Cell defaults MUST NOT be copied onto Inbox, Core, P2P, local
603compute, or external plans. Evidence targets must be jointly satisfiable; for
604example, operator-diversity evidence counted only over readback-verified replicas
605cannot exceed the readback target used to complete the policy.
606
607SDK results and events expose exact evidence:
608
609```js
610{
611 localState,
612 intentState,
613 durability: {
614 primitive,
615 acknowledgedReplicas,
616 readbackVerifiedReplicas,
617 qualifiedIndependentOperatorGroups,
618 externallyWitnessedReplicas,
619 repairPending,
620 leaseRenewalDeadline,
621 retentionHorizon,
622 coreHead
623 },
624 requestedPrivacy,
625 actualTransportProfile,
626 privacyResolution,
627 downgradeAuthorizationId,
628 claims: {
629 representation,
630 transport,
631 readInterest,
632 durability
633 },
634 assumptions
635}
636```
637
638Allowed derived labels are `local-queued`, `remote-stored`,
639`remote-readback-verified`, `replicated`, and `resilient-multi-operator`. The UI
640MUST display the primitive and evidence behind the label and MUST NOT collapse
641them into `online`, `safe`, or `secure`. `queueable` describes current execution
642state, not achieved remote durability.
643
644## 10. Cost, padding, batching, and chunking
645
646Cost reporting separates cryptographically exact blind framing from
647adapter/network modelling:
648
649```js
650{
651 usefulBytes,
652 opaqueBytes,
653 blindEnvelope: {
654 requestOuterBytes,
655 responseOuterBytes,
656 roundTripBytesPerDestination,
657 destinationCount,
658 totalOuterBytes,
659 usefulByteAmplification,
660 exact: true
661 },
662 adapter: {
663 profile,
664 clientEgressBytes,
665 hopBytes,
666 encapsulationBytes,
667 handshakeBytesAmortized,
668 retryAllowanceBytes,
669 evidence: 'captured' | 'bounded-model' | 'unknown'
670 },
671 fleet: {
672 totalBytes,
673 linkCount,
674 evidence: 'captured' | 'bounded-model' | 'unknown'
675 }
676}
677```
678
679For the current v1 same-class profile, `blindEnvelope` values are derived from
680the frozen outer classes, not `payload + estimated headers`. They are exact
681outer-envelope bytes, not exact HTTP, OHTTP, Noise, Tor, TLS, handshake, retry,
682or whole-fleet bytes. Adapter and fleet costs differ by profile and path and MUST
683come from that adapter's bounded model or capture evidence. Unknown values remain
684`unknown`; they MUST NOT be filled with direct-transport values or multiplied by
685replicas and called fleet cost.
686
687An exact Cell estimate additionally requires an explicit useful-byte/Cell-class
688basis and an operation-specific destination count. PUT uses `replicaTarget`;
689foreground GET uses `fetchTarget`; post-write durability readback remains the
690separate `readbackTarget`. Without the size basis, a draft exposes
691`estimateStatus: size-basis-required` and no `blindEnvelope`; any hard envelope
692ceiling then blocks with `COST_EVIDENCE_UNAVAILABLE`.
693
694Inbox planning similarly selects the minimum universal 4/16/64-KiB opaque frame
695class from an explicit positive byte-length basis. Application framing and
696encryption must fit inside that selected frame. A value above 65,536 bytes is
697`RENDEZVOUS_FRAME_TOO_LARGE` before transport selection; it never becomes a
698draft append. Inbox outer-envelope, fanout, and fleet costs remain unknown until
699the corresponding adapter model or capture supplies them.
700
701A caller may set `maxBlindEnvelopeAmplification`, `maxAdapterBytes`,
702`maxFleetBytes`, `maxForegroundBytes`, or `maxInteractiveLatencyMs`. A hard
703adapter/fleet/latency ceiling requires evidence with matching coverage; otherwise
704the result is `COST_EVIDENCE_UNAVAILABLE`, not `ready`. The SDK blocks rather
705than overruns a hard ceiling. It reports costs per primitive and operation:
706Cell PUT/GET/PROVE, Inbox create/append/read/watch, Core mirror/prove/session,
707P2P session, and external-service request are not interchangeable estimates.
708
709The SDK SHOULD recommend batching when a small event would sparsely occupy a
7104-KiB Cell. It MUST NOT automatically combine events with different authors,
711authorization, atomicity, visibility, retention, or privacy policy. The app owns
712the batching codec and flush policy.
713
714Objects larger than the maximum Cell payload require an app-owned signed chunk
715manifest. Without one, drafting is blocked with `CHUNK_MANIFEST_REQUIRED`. With
716one, the prepared plan pins the manifest commitment, exact chunk count/classes,
717per-chunk destinations and operations, total blind-envelope costs, available
718adapter/fleet bounds, and completion rule. Partial chunks remain incomplete
719state; the SDK never reports the logical object complete until the manifest and
720every required chunk validate. The SDK never invents an application chunk codec
721or signs a manifest on the application's behalf.
722
723## 11. External semantic services
724
725Search, ranking, recommendation, transcoding, remote AI, fraud evaluation,
726consensus, and server-authoritative game transitions are not blind merely because
727their input is encrypted in transit.
728
729The application first chooses `local-compute` or `external`. Local compute uses
730an application-owned executor and reports only the local representation and
731process/origin assumptions; it does not manufacture a transport plan or an
732external disclosure.
733
734An external-service pure draft requires a separate network intent and a
735secret-free semantic-disclosure descriptor, but no consent handle:
736
737```js
738{
739 workload: { kind: 'semantic', execution: 'external' },
740 runtime: 'browser',
741 privacy: privacy.private(),
742 externalDisclosure: {
743 serviceId,
744 purpose,
745 operatorSees: [...],
746 authority: 'advisory' | 'authoritative',
747 retention,
748 endpointEvidence: 'sha256:<64-hex>',
749 networkTransport: 'source-separated'
750 }
751}
752```
753
754The pure draft canonicalizes `operatorSees`, retains only the content-addressed
755endpoint-evidence digest, and returns
756`consentRequired: true, consentStatus: qualification-required`. It rejects a
757caller-shaped `userConsent`, `disclosureAuthorization`, or raw evidence bytes.
758This allows Pear Deploy to compile the route without serializing a user's
759runtime consent.
760
761The future high-level `app.external()` call supplies the separately minted
762`disclosureAuthorization` to `prepare()`, never to `draftWorkload()`.
763
764`disclosureAuthorization` is an opaque local authorization bound to the service
765identity and endpoint-evidence hash, purpose, exact `operatorSees` set,
766authority, retention statement, requested network-policy hash, visible prompt
767version, scope/expiry, and application persona. A boolean `userConsent`, an
768authorization for a different service/purpose, or consent to transport downgrade
769is insufficient. Missing, expired, widened, or mismatched authorization is
770`SEMANTIC_SERVICE_CONSENT_REQUIRED` or
771`SEMANTIC_SERVICE_DISCLOSURE_REQUIRED`; it cannot produce `ready`.
772`authorizeDisclosure()` is the only SDK operation that mints this handle after
773the application presents the exact disclosure through a visible consent UI.
774
775Disclosure does not replace transport privacy. `prepare()` still resolves
776`networkPrivacy` to an actual direct, OHTTP/split, or Tor profile and verifies
777its evidence. A high-privacy external request without Tor is queueable or
778blocked exactly like a blind-storage request; it does not become direct because
779the operator can see plaintext. The prepared plan's `transportProfile` remains
780the actual network profile. Semantic visibility is reported separately as:
781
782```js
783{
784 claims: {
785 transport,
786 semanticDisclosure: {
787 claimCeiling: 'explicitly non-blind semantic service',
788 operatorSees,
789 authority,
790 retention
791 }
792 }
793}
794```
795
796The SDK keeps external credentials, telemetry, retry policy, consent, cost, and
797failure state separate from blind storage. Results that alter canonical
798application state are independently validated and signed under the application
799protocol. Provider acknowledgements and retention statements never count as
800Blind Cell/Inbox/Core durability.
801
802Local search, local moderation, and on-device inference need no external
803disclosure and SHOULD be preferred when they meet the product requirement.
804
805## 12. Runtime defaults
806
807### Ordinary browser
808
809- Records and history use Cells; rendezvous uses Inbox.
810- `private` selects OHTTP only after the adapter, opaque-origin capture, and
811 signed profile evidence are ready.
812- Blind Core is unavailable without a production stream bridge.
813- Missing/offline OHTTP yields `queueable` or `blocked`, never a direct race.
814- Direct fallback requires a bound downgrade authorization and a newly prepared
815 plan.
816
817### Pear/Bare/Node
818
819- Records use Cells; long append history and bulk replication prefer Core.
820- `private` selects split Noise/Protomux only after that adapter, its distinct
821 path roles, and capture/collusion evidence pass. An active release that
822 reserves Core/Forward stream operations reports them unavailable.
823- `high-privacy` selects full-onion Tor only after its adapter and operator-side
824 evidence pass; it starts no clearnet DNS, DHT, QUIC, UDP, or fallback race.
825- Live interaction uses direct P2P and pairs with Cells/Core for durability.
826- Missing private/Tor paths leave durable mutations queueable under their exact
827 requested policy.
828
829### Tor Browser
830
831- Full-onion access fails closed.
832- Source-address separation does not by itself prove Origin/app separation.
833- The transport axis reports source-address separation, while the application
834 identity/origin axis remains `unverified`; the SDK reports the lower separate
835 claim ceilings and cannot return a fully satisfied high-privacy plan until the
836 opaque-origin gate passes.
837
838No bullet above asserts that an adapter currently ships. Runtime defaults become
839selectable only through the release and evidence rule in section 2.
840
841## 13. Pear Deploy integration and developer experience
842
843### 13.1 Contract boundary and present status
844
845Pear Deploy is the intended high-level developer experience after the substrate,
846transport adapters, and representative applications pass their conformance
847gates. Its existing record kernel is useful authority machinery, but the
848`pear.deploy.json` compiler, controller, HiveRelay provider adapter, and privacy
849route integration are not yet shipped. This section is a target integration
850contract, not evidence that those components exist.
851
852During this design audit, the legacy application-bridge validator skipped all
85315 discovered applications and reported success with zero evaluated apps; one
854draft contract also used v2 `contentFlow` vocabulary against a v1
855`publishFlow` validator. Neither result is promotion evidence. The integration
856must make zero evaluated applications a failure and converge on one pinned
857contract version before a privacy route can satisfy a release gate.
858
859Three namespaces that all contain a word such as `private` MUST remain distinct:
860
861| Namespace | Example | What it means | What it does not prove |
862| --- | --- | --- | --- |
863| Deployment profile | Pear Deploy `private@1` | Defaults and release gates for an application deployment | That Blind Cells, Core, OHTTP, or Tor are mature or usable |
864| Data classification | `application-encrypted` | Bytes must be authenticated and encrypted before a distributed surface | Source-address, read-interest, identity, or durability privacy |
865| SDK route policy | `privacy.private()` | Minimum per-operation transport and metadata constraints | A fresh endpoint, available adapter, consent, admission, or executable plan |
866
867Selecting a deployment profile MUST NOT manufacture a route-policy result. A
868route policy MUST NOT change a data classification. Passing both gates MUST NOT
869be summarized as one undifferentiated `private: true` claim.
870
871Pear Deploy owns release intent, deterministic compilation, provider leases,
872binding sets, and release evidence. The application owns plaintext, schemas,
873signing identities, storage capabilities, consent handles, logical events, and
874runtime operations. No compiler artifact, provider snapshot, lock, deployment
875record, binding set, proof, catalog entry, or controller log may contain bearer
876capabilities, application keys, payloads, locators, or consent handles.
877
878The dependency direction is one way: the SDK publishes closed policy schemas,
879canonicalization/vectors, a pure planner, and conformance fixtures; a separate
880first-party Pear Deploy compiler/provider integration consumes them. The SDK
881MUST NOT import Pear Deploy, deploy infrastructure, mutate providers, sign a
882release, or report deployment readiness.
883
884### 13.2 Proposed intent extension
885
886Until the route vocabulary is promoted into a frozen Pear Deploy contract, it
887belongs in the schema-referenced `extensions` namespace of the provider-neutral,
888secret-free `pear.deploy.json`. The compiler MUST reject this extension unless
889it recognizes the exact schema and SDK-policy contract digest.
890
891An illustrative intent is:
892
893```json
894{
895 "$schema": "https://schemas.pear.dev/deploy/intent.v1alpha1.json",
896 "apiVersion": "pear.deploy/v1alpha1",
897 "kind": "DeploymentIntent",
898 "project": "private-collaboration",
899 "profile": "private",
900 "data": {
901 "mode": "stateful",
902 "privacy": "mixed"
903 },
904 "bindings": {
905 "network": { "required": true },
906 "identity": { "required": true },
907 "availability": { "required": true, "provider": "auto" },
908 "privateStorage": { "required": true },
909 "compute": { "required": false }
910 },
911 "extensions": {
912 "pear.application-privacy/v1alpha1": {
913 "$schema": "https://schemas.pear.dev/deploy/application-privacy.v1alpha1.json",
914 "policyContract": "hiverelay.application-privacy/v1",
915 "routes": {
916 "records.timeline": {
917 "workload": { "kind": "records", "maxByteLength": 4063 },
918 "operations": ["put", "get"],
919 "privacy": { "preset": "private" },
920 "durability": {
921 "replicaTarget": 3,
922 "acknowledgementTarget": 2,
923 "readbackTarget": 2,
924 "independentOperatorGroups": 2
925 },
926 "runtimeFallback": "prompt",
927 "failureMode": "block-release",
928 "costBudget": { "maxBlindEnvelopeAmplification": 32 }
929 },
930 "rendezvous.invites": {
931 "workload": { "kind": "rendezvous", "maxByteLength": 512 },
932 "operations": ["create", "append", "read", "close"],
933 "privacy": { "preset": "private" },
934 "runtimeFallback": "prompt",
935 "failureMode": "block-release"
936 },
937 "history.messages": {
938 "workload": { "kind": "history" },
939 "operations": ["put", "get"],
940 "primitiveByRuntime": { "browser": "cell", "bare": "core", "node": "core" },
941 "privacy": { "preset": "private" },
942 "runtimeFallback": "prompt",
943 "failureMode": "block-release"
944 },
945 "live.presence": {
946 "workload": { "kind": "live" },
947 "operations": ["session"],
948 "privacy": { "preset": "fast" },
949 "runtimeFallback": "deny",
950 "failureMode": "block-release"
951 },
952 "semantic.search": {
953 "workload": { "kind": "semantic", "execution": "external" },
954 "operations": ["execute"],
955 "required": false,
956 "privacy": { "preset": "private" },
957 "serviceBinding": "compute",
958 "localExecutor": "search/local-index-v1",
959 "bridgeClass": "search-query",
960 "externalDisclosure": {
961 "serviceId": "search-provider-v1",
962 "purpose": "full-text search",
963 "operatorSees": ["query", "result selection", "timing"],
964 "authority": "advisory",
965 "retention": "provider policy",
966 "endpointEvidence": "sha256:0000000000000000000000000000000000000000000000000000000000000000",
967 "networkTransport": "source-separated"
968 },
969 "runtimeFallback": "local-only",
970 "failureMode": "disable-feature"
971 }
972 }
973 }
974 }
975}
976```
977
978The example omits the unrelated required Pear Deploy fields for readability.
979It is not valid against the current unfinished compiler. Production intent MUST
980still contain every required source, surface, artifact, environment,
981availability, name, release, telemetry, and secret declaration. The all-zero
982endpoint-evidence digest is an obvious documentation placeholder; production
983compilation requires the real content digest and qualification requires the
984matching evidence object.
985
986`operations` is a required, non-empty, duplicate-free subset of the closed
987operation set for its workload. The compiler emits a distinct route expansion
988for every `(route, runtime, operation)` tuple and binds each runtime handle to
989only those operations. It MUST NOT rely on the pure planner's convenience
990default because PUT/GET, MIRROR/OPEN_REPLICATION, and Inbox verbs have different
991wire operations, evidence, and cost. `required: false` marks the semantic route
992as optional, and `localExecutor` identifies the reviewed local implementation
993used by `local-only`; neither field permits an undeclared fallback.
994
995The compiler maps `browser` to `RUNTIME.BROWSER`, `bare`, `node`, and Pear
996native surfaces to `RUNTIME.NATIVE`, and a Tor Browser surface to
997`RUNTIME.TOR_BROWSER`. It supplies a declared `maxByteLength` to the pure
998planner as the worst-case `byteLength`; runtime preparation repeats planning
999with the exact operation length. The example's amplification ceiling
1000intentionally fails against the current symmetric v1 Cell envelope and becomes
1001satisfiable only through the qualified compact class-1 mutation profile (or a
1002later more efficient protocol). This is a fail-closed efficiency requirement,
1003not a claim that the compact profile ships today. Before that profile can make
1004the compiler green, a later policy-contract revision MUST add an orthogonal,
1005signed `envelopeProfile` axis, prospective profile-specific estimates, and
1006conformance tuples for compact+direct, compact+OHTTP, compact+split-native, and
1007compact+Tor where each composition is actually supported. Network transport
1008privacy and blind-envelope framing MUST NOT be represented as one competing
1009selector. The v1 `/policy` surface knows only the symmetric envelope and
1010therefore MUST keep this route blocked today.
1011
1012The recognized extension payload is the canonical, secret-free
1013`AppPrivacyIntentV1`. The compiler extracts it to a content-addressed generated
1014artifact so the same bytes can be consumed by conformance tools without making
1015a second human-authored deployment intent. Route names are stable
1016application-local identifiers and are not relay namespaces. The intent
1017expresses minimum semantics, not endpoint choices:
1018
1019- it MAY name a closed workload, permitted primitive per runtime, SDK privacy
1020 preset or complete custom axes, a required closed `operations` list,
1021 primitive-specific durability, hard cost ceilings, and fallback policy;
1022- `runtimeFallback` is `deny`, `prompt`, or, for a semantic route with a local
1023 implementation, `local-only`; `failureMode` is `block-release` or
1024 `disable-feature`, and the latter is valid only for an explicitly optional
1025 route. `deny` and `prompt` MUST equal the selected privacy intent's
1026 `downgradePolicy`; a fixed-preset conflict is rejected rather than silently
1027 rewritten. `local-only` requires a reviewed `localExecutor` and is not a
1028 network privacy downgrade;
1029- an environment overlay cannot weaken production intent unless that exact
1030 weaker revision is compiled, reviewed, locked, and signed;
1031- an external semantic route MUST name a bridge class, service binding, and
1032 complete secret-free disclosure descriptor, while exact disclosure consent
1033 remains a runtime, persona-bound authorization; and
1034- it MUST NOT contain relay endpoints, raw provider descriptors, capabilities,
1035 credentials, app payloads, or consent booleans.
1036
1037### 13.3 Deterministic compiler output
1038
1039The Pear Deploy compiler calls only the pure `/policy` surface. It expands every
1040route across declared runtime surfaces and declared operations, validates the closed
1041workload/primitive/privacy matrix, computes exact Cell envelope bounds where
1042possible, and emits `application-privacy-plan.json` alongside the normal
1043compiled artifacts. Compilation performs no discovery, qualification, consent,
1044or application mutation and therefore never emits `ready`.
1045
1046The generated plan contains:
1047
1048```text
1049schema and policy-contract digests
1050source-intent, deployment-profile, and environment digests
1051SDK package/version/source-closure digest
1052stable route name and route-intent digest
1053runtime/workload/primitive/operation expansion
1054requested axes and honest draft resolution states
1055per-primitive durability and cost requirements
1056required adapter, transport-profile, and conformance classes
1057external bridge-policy references
1058compile verdict, reason codes, assumptions, and claim ceilings
1059```
1060
1061The compiler adds the plan digest to `compiled-intent.json`, derives
1062provider-neutral requirements into `relay-needs.json`, derives allowed external
1063disclosures into deny-by-default `bridge-policy.json`, and pins the exact policy
1064compiler and route digests in `pear.lock`. The lock also pins the provider
1065snapshot, adapter versions, signed capability digests, evidence-policy digest,
1066and conformance bundle digests used for deployment planning. It contains no
1067bearer authority.
1068
1069Each route compiles to versioned logical binding requirements such as
1070`blind-cell`, `blind-inbox`, `blind-core`, `network/direct`, or
1071`external-service`; concrete provider assignments exist only in a later
1072`BindingSet`. Multidimensional SDK axes MUST NOT be flattened into Pear Deploy's
1073legacy scalar `privacyTier`.
1074
1075Identical fully expanded intent, environment, policy implementation, frozen
1076provider snapshot, evidence inputs, and evaluation time MUST produce
1077byte-identical route plans, locks, explanations, and provider action templates.
1078Unknown axes, profiles, primitives, adapters, evidence predicates, or cost
1079models fail closed; the compiler does not invent favorable defaults.
1080
1081### 13.4 Control plane versus runtime data plane
1082
1083The Pear Deploy HiveRelay provider adapter and the SDK transport adapters have
1084different authority and MUST remain separate:
1085
1086| Component | May do | Must not do |
1087| --- | --- | --- |
1088| Pear Deploy compiler | Validate and expand route intent; emit requirements and locks | Perform I/O, choose a live route, mint consent, or call `execute()` |
1089| Pear Deploy controller/provider adapter | Discover signed capabilities, obtain quotes, provision leases/infrastructure, reconcile exact effects, gather release evidence | Read app plaintext or vaults, mint app storage capabilities, send app logical events, or turn a deployment check into an executable operation |
1090| SDK `prepare()` | Qualify a fresh operation-specific endpoint/path, bind payload/request commitment, costs, consent, capabilities, and expiry | Mutate provider infrastructure or rely on deployment-time health as current operation evidence |
1091| SDK `execute()` | Send the exact prepared app operation and preserve ambiguous-result semantics | Select a new endpoint/profile, weaken policy, or repair Pear Deploy bindings |
1092
1093Deployment qualification proves that an environment has evidence-backed
1094coverage for a declared route class. It cannot pre-authorize future payloads.
1095At runtime the SDK consumes the signed deployment policy and current binding-set
1096projection as constraints and discovery inputs, then independently performs
1097fresh endpoint and operation qualification. A deployment-time pass may make a
1098runtime path likely to prepare; it never makes a future plan ready.
1099
1100Qualification uses synthetic, non-secret fixture operations and negative
1101captures against staged infrastructure. An opaque `prepare()` plan is
1102short-lived, non-serializable, and never stored in `application-privacy-plan`,
1103`pear.lock`, a build artifact, a binding set, or release evidence.
1104
1105Binding-set repair may replace a provider only when the new set meets or exceeds
1106every signed route and deployment constraint. The application sees the new
1107signed binding-set revision through discovery; an already prepared operation
1108does not silently migrate to it.
1109
1110### 13.5 Ship gates and evidence
1111
1112`pear deploy ship` MUST refuse a release that advertises a required route until
1113all applicable gates pass:
1114
11151. the exact route extension and `/policy` implementation pass `compiler-v1`
1116 determinism, closed-schema, and cross-runtime vector tests;
11172. every required SDK adapter/profile/runtime tuple has a versioned conformance
1118 manifest, source-closure digest, executable artifact digest, valid evidence
1119 window, and negative-path capture coverage;
11203. the provider adapter passes the applicable `provider-v1` discovery,
1121 verification, effect-reconciliation, observation, renewal, and revocation
1122 gates without claiming app-data authority;
11234. representative applications pass route-level integration tests for offline
1124 queueing, restart, ambiguous results, replica repair, capability leakage,
1125 disclosure consent, denied fallback, and hard cost ceilings;
11265. private and high-privacy captures prove absence of forbidden direct races;
1127 high privacy additionally proves no clearnet DNS, DHT, QUIC, UDP, HTTP, or
1128 fallback traffic for the exercised route;
11296. independent probes verify the exact deployed adapter and provider-profile
1130 coverage from clean browser/native clients;
11317. `release-proof.json` binds every required predicate to the deployment,
1132 binding set, policy digest, producer, subject, evidence digest, observation
1133 time, expiry, strength, and limitations; and
11348. compatibility and application-wave validators evaluate at least one intended
1135 subject under the exact pinned schema; zero evaluated subjects, skipped
1136 generations, or version/field drift are failures rather than green evidence.
1137
1138Suggested release-proof predicates are
1139`privacy-route-policy-compiled`, `privacy-adapter-conformant`,
1140`privacy-negative-path-pass`, `privacy-provider-profile-qualified`, and
1141`privacy-app-route-conformant`. A release proof reports deploy-time coverage
1142only. Its limitations MUST say that per-operation readiness, non-collusion,
1143traffic-analysis resistance outside the tested capture, service retention, and
1144future provider health remain runtime or external assumptions.
1145
1146Evidence is emitted in two separate classes. `AppPrivacyConformanceV1` binds
1147the source/build/tree digest, exact SDK and adapter artifact digests, runtime
1148matrix, route-intent digests, test/capture digests, verdicts, and limitations.
1149Target qualification observations bind an exact staged endpoint, signed
1150transport-profile and capability-snapshot digests, operation fixture, producer,
1151observation time, expiry, and limitations. Raw traces and private topology are
1152metadata-sensitive sidecars; `release-proof.json` contains their digests and
1153typed verdicts, not the raw material.
1154
1155Missing optional-route evidence disables that route in the generated runtime
1156policy. Missing required-route evidence blocks `ship`; it is not a warning.
1157Preview may use a weaker policy only when the preview environment contains an
1158explicit, separately compiled override and the UI identifies every difference.
1159Production intent is never weakened automatically to make a deploy green.
1160
1161A pure `draft` outcome proves only internal policy validity and cannot satisfy a
1162promotion predicate. A `blocked` required route fails promotion. A `queueable`
1163result can prove only that the app's offline/background behavior was exercised;
1164it never proves target-path availability. Promotion still requires fresh staged
1165qualification and every other predicate declared by the route.
1166
1167### 13.6 Target developer flow
1168
1169The target flow is one intent file and one release path:
1170
1171```sh
1172pear deploy check --environment production
1173pear deploy explain privacy --route history.messages
1174pear deploy preview
1175pear deploy ship --environment production
1176```
1177
1178`check` is deterministic and non-mutating. Its privacy table shows, per route
1179and runtime, the chosen primitive, requested axes, expected claim ceilings,
1180exact envelope cost where available, unresolved evidence, fallback behavior,
1181and whether the route is required. `explain` shows why candidates were accepted
1182or rejected. `preview` provisions only the isolated preview policy. `ship`
1183executes the idempotent provider action plan, verifies the result, and requests
1184the existing project-controlled release signatures.
1185
1186The compiler also generates a typed, content-digest-pinned route manifest for
1187the app build. The target SDK experience is:
1188
1189```js
1190import { BlindClient } from '@hiverelay/blind-client'
1191import deployment from 'pear-deploy:runtime'
1192
1193const app = await BlindClient.open({
1194 policy: deployment.applicationPrivacy,
1195 bindings: deployment.bindings,
1196 adapters,
1197 storage,
1198 vault
1199})
1200
1201const timeline = app.route('records.timeline')
1202const result = await timeline.put(encoded, { signal })
1203```
1204
1205The virtual import and `app.route()` are target APIs, not current exports. A
1206generated handle fixes the signed route constraints but still returns the SDK's
1207normal `ready`, `queueable`, `blocked`, or ambiguous operation states. It never
1208contains a hidden direct client or a deployment-time executable plan. An app may
1209use the lower-level SDK directly, but production claim lint SHOULD fail when its
1210effective policy is weaker than the signed deployment route.
1211
1212Compatibility generation is one-way and conservative. A v1 bridge policy is
1213emitted only for disclosure, opt-in, and exclusion semantics it can represent;
1214legacy relay-needs v1/v2 files describe only their actual availability roles.
1215If an SDK axis, failure mode, consent rule, or claim ceiling cannot be represented
1216without loss, the compiler omits or rejects that projection rather than calling
1217it `private`, `p2p-only`, or equivalent.
1218
1219### 13.7 Delivery order
1220
1221The integration is promoted in this order:
1222
12231. freeze and test the pure SDK policy contract and route-plan vectors;
12242. stand up each concrete transport/storage adapter and publish narrowly scoped
1225 evidence without making it selectable early;
12263. exercise the adapters through representative browser, native, offline,
1227 high-loss, and multi-operator applications;
12284. add the namespaced route extension and deterministic compiler output to Pear
1229 Deploy;
12305. implement the non-app-data HiveRelay provider adapter and evidence predicates;
12316. generate runtime route handles and bind them to signed deployment artifacts;
12327. enable `ship` gates route by route only when compiler, provider, verifier,
1233 adapter, and app conformance all pass; and
12348. promote the extension to a stable Pear Deploy contract only after at least
1235 two independent implementations reproduce its vectors.
1236
1237This order allows the one-command experience to be built early without letting
1238its ergonomics outrun the privacy evidence.
1239
1240## 14. Conformance
1241
1242Release requires, at minimum:
1243
12441. every closed workload/primitive/runtime/preset combination resolves to
1245 `draft`, `queueable`, or a stable blocked code, and invalid primitive
1246 overrides never reach generic planning;
12472. only an opaque evidence-qualified prepared plan can become `ready` or enter
1248 `execute()`; a pure draft, JSON reconstruction, registry value, advertisement,
1249 or availability boolean cannot;
12503. every requested custom axis records requested, planned state, required
1251 evidence, assumptions, and claim ceiling in a draft, while only a qualified
1252 prepared plan may add actual/satisfaction/evidence fields;
12534. fixed preset mutation is rejected and high-privacy downgrade authorization
1254 cannot be minted;
12555. a queueable intent performs no network I/O and retains the exact requested
1256 policy across wake, restart, discovery, and reconnect;
12576. family, operation, endpoint/path roles, transport, privacy profile,
1258 destination, class, payload/request commitment, and request identity cannot
1259 mutate within an attempt;
12607. high-privacy capture produces no clearnet DNS, DHT, QUIC, UDP, HTTP, or
1261 fallback race;
12628. private paths never race direct, and every fallback is a new plan with an
1263 exact, unexpired, single-use consent binding;
12649. external disclosure consent is bound to service/purpose/visibility/retention
1265 and remains independent of network transport and downgrade consent;
126610. response loss, restart, reload, and multi-tab races do not duplicate a
1267 logical event;
126811. Cell read/write/create/renew/drop, Inbox read/append/create/renew/close,
1269 Core read/mirror, P2P session, app identity, and external credentials cannot
1270 substitute for one another and never appear in logs, metrics, crash output,
1271 or public state;
127212. known app/schema/author sentinels are absent from every relay-visible surface;
127313. Node, Bare, and browser accept and reject identical canonical vectors;
127414. per-primitive durability labels rise only with the stated receipts,
1275 readbacks, signed heads, witnesses, leases, retention, and operator-group
1276 evidence;
127715. unrelated and previously unknown apps use the unchanged relay ABI;
127816. representation, direct, OHTTP, split-native, Tor, browser-origin,
1279 read-interest, and durability claims each have separate evidence and claim
1280 ceilings;
128117. exact blind-envelope estimates match encoded envelope bytes for every class
1282 and operation, while adapter/fleet estimates match their declared capture or
1283 bounded-model coverage and never masquerade as exact envelope bytes;
128418. oversized Cell inputs block with `CHUNK_MANIFEST_REQUIRED` until an
1285 app-signed manifest is supplied, after which all chunks and costs are pinned;
128619. local compute produces no external disclosure or transport claim, while
1287 external compute cannot become ready without complete disclosure and bound
1288 consent; and
128920. package exports, Node/Bare/browser tests, generated browser closure, and
1290 release evidence cover every advertised API and adapter.
1291
1292The workload vectors additionally cover every Inbox frame cliff and oversize
1293rejection, every application-to-wire operation mapping, and equality of all
1294deterministic plan fields across available versus unavailable inventory hints.
1295
1296## 15. Migration
1297
12981. Preserve `p2p-hiverelay-client` for existing app-aware consumers.
12992. Promote the strict protocol/client packages from the accepted vNext source
1300 and publish them only after their release gates pass.
13013. Ship `/policy` first only as the non-executable draft surface, with its
1302 closed matrix, immutable presets, axis resolution schema, and cost labels.
13034. Ship `BlindClient.prepare()/plan()`, `/journal`, and `/vault` only after
1304 opaque ready-plan, queueing, consent-binding, capability, and restart tests
1305 pass.
13065. Ship concrete adapters incrementally; unavailable or unevidenced adapters
1307 remain unselectable rather than caller-set `true`.
13086. Add `p2p-hiverelay-client/blind` only as a thin version-pinned re-export.
13097. Migrate applications primitive by primitive: Cells, durability, Inbox/Core,
1310 then stronger transports and explicitly disclosed external services.
13118. Add the Pear Deploy route extension first as a namespaced experimental
1312 contract; compile only pure drafts until provider, adapter, verifier, and app
1313 conformance gates are independently green.
13149. Generate runtime route handles and enable production `ship` only for the
1315 exact route/runtime/profile tuples covered by the signed evidence bundle.
131610. Retire legacy OutboxLog, BlindShard, Notify, and semantic relay paths through
1317 the separately signed compatibility cutoff.
1318
1319No SDK change authorizes deployment, publication, DNS, fleet mutation, or a
1320privacy claim without the corresponding independent evidence.
docs/protocol/HIVERELAY-BLIND-ENVELOPE-EFFICIENCY-PLAN.md +202 -0
……@@ -0,0 +1,202 @@
1# HiveRelay Blind Envelope Efficiency Plan
2
3Status: implementation plan. It preserves the frozen public WIRE v1 authority
4and does not authorize a release or fleet change.
5
6## 1. Problem
7
8`CellBlobV1` and public outer envelopes share 4/16/64/256-KiB and 1-MiB
9boundaries. The six-byte outer header and 45-byte dispatch frame mean a complete
10Cell never fits the same-sized outer class. The current client also selects one
11class for both request and response, while Private IPC v2 treats a generous
1216,384-byte operation cap as the actual maximum PUT receipt.
13
14Current one-relay lower bounds are therefore:
15
16| Operation | Request | Response | Total |
17| --- | ---: | ---: | ---: |
18| 4-KiB Cell GET | 16 KiB | 16 KiB | 32 KiB |
19| 1-MiB Cell GET | 8 MiB | 8 MiB | 16 MiB |
20| 4-KiB Cell PUT | 64 KiB | 64 KiB | 128 KiB |
21
22At three replicas the final row is 384 KiB for one 4-KiB Cell before TLS,
23retransmission, OHTTP, or Tor.
24
25## 2. Non-negotiable safety constraints
26
27- Do not alter WIRE v1 bytes, hashes, vectors, class IDs, media type, routes, or
28 existing signed transport profiles.
29- New behavior is server-first and selected only through an exact signed
30 transport-profile hash on a distinct descriptor endpoint/listener authority.
31 A listener has exactly one profile hash and never infers or silently switches
32 response semantics from client behavior.
33- Found, absent, and correlated error outcomes use the same request-bound result
34 class for an operation/profile.
35- No unauthenticated small request may demand a large padded response.
36- Admission and accounting charge exact outer egress bytes, not logical payload.
37- No app-specific class, class set, schedule, endpoint, or padding policy exists.
38
39## 3. Phase 0 — correct planning evidence
40
41The capacity lab now derives Cell PUT/GET ingress and egress from the exact
42minimum-fitting v1 symmetric outer classes selected by the current client default.
43Each operation reports mean class bytes per direction and per-touch round-trip,
44plus ingress, egress, round-trip bytes, and useful-byte amplification for the
45complete logical operation after replica/fanout touches. Deliberate privacy
46up-classing remains an explicit unmodeled input rather than being mistaken for the
47minimum.
48
49Report v3 binds the capacity-model revision and frozen WIRE v1 authority profile,
50protocol version, specification hash, ABI hash, and vector-set hash into its
51scenario manifest and digest. Reports from the older logical-byte model cannot
52share the corrected report identity.
53
54All economic and year-one workload projections that used logical Cell bytes as
55network bytes MUST be regenerated from the corrected report before they support
56bandwidth, pricing, or operator-income decisions.
57
58## 4. Phase 1 — compact mutation results without changing WIRE v1
59
60Introduce the signed direct-HTTPS transport profile
61`hiverelay/direct-https-compact-cell-mutation/1` and Private IPC v3. The existing
62same-class profile and IPC v2 remain byte-identical.
63
64Because a public v1 request carries no transport-profile hash, a relay advertises
65the compact profile on an additional signed descriptor endpoint whose canonical
66listener authority is distinct from the legacy endpoint. The old endpoint remains
67available during compatibility rollout. Each listener is configured with exactly
68one profile hash, and its edge/daemon readiness projection binds that same hash;
69sharing one URL/port and guessing the profile from request size is forbidden. The
70additional endpoint is universal for the profile and is never app-specific.
71
72The structural maximum result sizes are:
73
74```text
75BlindReceiptV1 with durability-profile-2 witness: 1,021 bytes
76BlindErrorV1: 8 bytes
77complete receipt dispatch plus outer framing: 1,072 bytes
78```
79
80A class-1 Cell PUT with a maximum 4,096-byte admission token fits a class-2
81request. Its receipt/error fits a class-1 response:
82
83```text
84current: 64 KiB request + 64 KiB response = 128 KiB/relay
85compact: 16 KiB request + 4 KiB response = 20 KiB/relay
86```
87
88At three replicas this changes the exact blind-envelope lower bound from
89384 KiB to 60 KiB per logical class-1 PUT, an 84.375% reduction before TLS,
90retries, OHTTP, or Tor.
91
92Private IPC v3 carries separate request/result classes and byte counts while
93retaining v2 peer credentials, TLS exporter binding, readiness, replay, FIN,
94half-close, EOF, staging, precommit validation, fsync, and correlation rules.
95
96The first profile's signed allowed tuple is exactly `(CELL, PUT)` with required
97admission and a generated small structural result bound. It does not authorize
98Cell renew/drop, Inbox create/append/renew/close, Core mirror, or any other
99mutation. Adding one of those operations requires a separately versioned
100profile, exact structural bounds, descriptor tuple, vectors, and gates. Reads
101remain same-class. General asymmetric reads would allow a tiny request to elicit
102an 8-MiB padded `NOT_FOUND` response and are therefore forbidden until
103authenticated egress reservation exists.
104
105Required gates:
106
1071. generated structural-bound authority and vectors;
1082. exact `(CELL, PUT)` descriptor/profile rejection for every other family and
109 operation, plus maximum admission-sized PUT for every Cell class;
1103. profile-1 and profile-2 receipts and every correlated error;
1114. separate-direction FIN, fragmentation, EOF, replay, restart, and cancellation;
1125. edge/daemon atomic activation and fail-closed readiness;
1136. distinct-listener profile demultiplexing and dual-endpoint rollout;
1147. old-profile client/server interoperability and downgrade rejection; and
1158. captured exact wire bytes matching the capacity planner.
116
117## 5. Phase 2 — negotiated public protocol major 2
118
119The complete correction belongs to a new `hiverelay-blind/2` protocol major,
120not the existing additive WIRE 1.1/1.2 authorities whose document filenames use
121`WIRE-V2` and `WIRE-V3`. Protocol major 2 uses independent request/result
122classes and a universal headroom ladder:
123
124```text
1254 KiB
1268 KiB
12716 KiB
12832 KiB
12964 KiB
13080 KiB
131256 KiB
132272 KiB
1331 MiB
1341,040 KiB
1354,112 KiB
1368 MiB
137```
138
139The inserted classes carry a fixed Cell or maximum batch/proof body plus framing
140without promotion to the next large bucket. More classes reveal a finer size
141bucket, so high-privacy policy may deliberately choose a coarser class from the
142same universal set.
143
144Each request cryptographically binds an exact `resultOuterClass` in the new
145request commitment. Transport integrity by itself is not egress authority. For an
146optional-admission read, an absent admission permits only
147`resultOuterBytes <= requestOuterBytes`; the client must therefore up-pad the
148request to the result class. A smaller request may select a larger result only
149with an egress-admission grant bound to the operation, request commitment, exact
150result class, endpoint, and expiry. The rule is checked before resource lookup, so
151`NOT_FOUND` cannot be used as an unauthenticated padded-response oracle.
152
153The relay:
154
1551. validates it against the operation/resource result bound;
1562. verifies any required egress-admission binding and reserves global, endpoint,
157 connection, and admission egress before work;
1583. rejects positive unauthenticated response/request amplification before lookup;
1594. charges exact request and result outer bytes;
1605. returns found, absent, and error outcomes in the selected result class; and
1616. echoes the class through authenticated correlation.
162
163Stored Cells and existing capabilities do not need migration. Public protocol,
164descriptor, endpoint masks, routes, transport pins, client negotiation, and
165vectors are versioned independently from v1.
166
167## 6. SDK mitigations available before protocol changes
168
169- Calculate exact v1 envelope and replica amplification in `plan()`.
170- Let applications set hard foreground byte and amplification ceilings.
171- Recommend app-authorized batching for sparse 4-KiB Cells.
172- Use `BATCH_GET` only as a latency optimization; never claim read-interest
173 privacy from it.
174- Prefer Blind Core or direct P2P for native bulk data and media.
175- Put media manifests and checkpoints in Cells rather than every hot body.
176- Queue high-privacy Tor operations for background execution.
177- Never silently switch privacy profile, destination, primitive, or semantic
178 service to meet a latency goal.
179
180## 7. Benchmark matrix
181
182Every accepted run pins:
183
184```text
185protocol/profile: hiverelay-blind/1 same-class | hiverelay/direct-https-compact-cell-mutation/1 | hiverelay-blind/2 (protocol 2.0; ABI v4)
186runtime: browser | Node | Bare/Pear | Tor Browser
187primitive: Cell | Inbox | Core | Forward/P2P
188operation and class
189useful and opaque bytes
190request and result outer bytes
191replicas/fanout and acknowledgement target
192durability profile
193transport: direct | OHTTP | split native | Tor
194warm/cold connection or circuit
195RTT/loss/jitter/bandwidth
196CPU, allocations, copies, fsync, WAL and external quorum
197```
198
199Reports publish p50/p95/p99/p999 latency, useful throughput, exact ingress and
200egress, useful-byte amplification, CPU, peak memory, admission failures, repair
201traffic, and raw samples. Direct/OHTTP/split/Tor results are never combined into
202one percentile.
package-lock.json +30 -0
……@@ -52,6 +52,36 @@
5252 "node": ">=18"
5353 }
5454 },
55 "node_modules/@esbuild/linux-arm64": {
56 "version": "0.28.1",
57 "cpu": [
58 "arm64"
59 ],
60 "dev": true,
61 "license": "MIT",
62 "optional": true,
63 "os": [
64 "linux"
65 ],
66 "engines": {
67 "node": ">=18"
68 }
69 },
70 "node_modules/@esbuild/linux-x64": {
71 "version": "0.28.1",
72 "cpu": [
73 "x64"
74 ],
75 "dev": true,
76 "license": "MIT",
77 "optional": true,
78 "os": [
79 "linux"
80 ],
81 "engines": {
82 "node": ">=18"
83 }
84 },
5585 "node_modules/@eslint-community/eslint-utils": {
5686 "version": "4.9.1",
5787 "dev": true,
packages/blind-client/README.md +47 -0
……@@ -55,3 +55,50 @@ coarse epoch remains current.
5555Applications sign and journal their own event before publication; zero relays
5656means queued delivery, one compatible unregistered relay is one remote replica,
5757and additional readback/operator evidence raises only the durability label.
58
59## Draft workload and privacy planning
60
61`@hiverelay/blind-client/policy` is a pure, side-effect-free planning surface.
62It keeps workload placement separate from network privacy, durability, cost,
63and semantic-service disclosure:
64
65```js
66import {
67 RUNTIME,
68 WORKLOAD,
69 draftWorkloadPlan,
70 privacy
71} from '@hiverelay/blind-client/policy'
72
73const draft = draftWorkloadPlan({
74 workload: { kind: WORKLOAD.RECORDS, byteLength: encoded.byteLength },
75 runtime: RUNTIME.BROWSER,
76 privacy: privacy.private(),
77 inventory: { ohttp: true },
78 costBudget: { maxBlindEnvelopeAmplification: 32 }
79})
80```
81
82A `draft` is never executable. Inventory booleans are deployment hints, not
83endpoint or privacy evidence. The future high-level client must qualify the
84exact endpoint, operation, signed transport-profile hash, fresh health,
85adapter, and capture evidence before it can issue a `ready` plan. Missing
86background paths return `queueable`; incompatible workloads, privacy axes,
87costs, or disclosures return `blocked` with a stable code.
88
89Fixed presets are immutable. `custom` supplies every constraint axis directly
90or inherits a fixed base before overriding it. Pure drafts report requested,
91planned, required-evidence, assumptions, and claim-ceiling fields; they never
92claim evidence-dependent `actual` or `satisfied` state. OHTTP, split-native,
93and Tor remain unavailable for execution until concrete adapters and their
94conformance evidence ship.
95
96The intended production developer experience is a Pear Deploy integration.
97`pear.deploy.json` will declare named application routes and minimum policies;
98the Deploy compiler will call this pure policy surface, pin its deterministic
99route plan and conformance inputs, and block `ship` when a required adapter or
100application test is unevidenced. Deploy-time coverage never becomes an
101executable app operation: the future `BlindClient.prepare()` must still bind
102fresh endpoint, consent, capability, cost, and request evidence at runtime.
103The proposed contract is specified in
104`docs/protocol/HIVERELAY-APPLICATION-PRIVACY-SDK-V1.md` section 13.
packages/blind-client/browser-artifacts/blind-client-control-v1.chromium-evidence.json +5 -5
……@@ -3,11 +3,11 @@
33 "version": 1,
44 "evidenceClass": "real-chromium",
55 "artifactPath": "browser-artifacts/blind-client-control-v1.mjs",
6 "artifactLength": 248195,
7 "artifactHash": "967cbc42a3583ef158045dfdf6afd82f672153c117236a928bc2c2684776b008",
8 "manifestHash": "9f789a8ce4479880986483cfb5b77547bf1cc611697251594fb5d944305a4c0e",
9 "sourceClosureHash": "9e21df70fec4106935ca388478f7ca48f550556df9a05b54483cea631e10b97c",
10 "chromium": "Google Chrome for Testing 149.0.7827.55",
6 "artifactLength": 253673,
7 "artifactHash": "51bf061fb2d86cee91eb905a021e77774a37e27f0bba3ccb6d4e7688dcbc41a2",
8 "manifestHash": "bc40b1fdecea1e1105ed279e62c6f4ece2bb2f7813cee117290cb79b13298d64",
9 "sourceClosureHash": "fe96ce663e6831155db3c5ebe0b79256e7ca75c381713c546070c4ab7d5fccd2",
10 "chromium": "Google Chrome for Testing 151.0.7922.34",
1111 "checks": [
1212 "STANDALONE_ESM_IMPORT",
1313 "REQUIRED_CONTROL_EXPORTS",
packages/blind-client/browser-artifacts/blind-client-control-v1.cross-host-evidence.json +4 -4
……@@ -3,10 +3,10 @@
33 "version": 1,
44 "evidenceClass": "clean-linux-container",
55 "artifactPath": "browser-artifacts/blind-client-control-v1.mjs",
6 "artifactLength": 248195,
7 "artifactHash": "967cbc42a3583ef158045dfdf6afd82f672153c117236a928bc2c2684776b008",
8 "manifestHash": "9f789a8ce4479880986483cfb5b77547bf1cc611697251594fb5d944305a4c0e",
9 "sourceClosureHash": "9e21df70fec4106935ca388478f7ca48f550556df9a05b54483cea631e10b97c",
6 "artifactLength": 253673,
7 "artifactHash": "51bf061fb2d86cee91eb905a021e77774a37e27f0bba3ccb6d4e7688dcbc41a2",
8 "manifestHash": "bc40b1fdecea1e1105ed279e62c6f4ece2bb2f7813cee117290cb79b13298d64",
9 "sourceClosureHash": "fe96ce663e6831155db3c5ebe0b79256e7ca75c381713c546070c4ab7d5fccd2",
1010 "platform": "linux",
1111 "architecture": "arm64",
1212 "containerImageId": "sha256:813a7480f28fdadac1f7f5c824bcdad435b5bc1322a5968bbbdef8d058f9dff4",
packages/blind-client/browser-artifacts/blind-client-control-v1.manifest.cenc +0 -0
(no content changes — binary or rename)
packages/blind-client/package.json +1 -0
……@@ -28,6 +28,7 @@
2828 "./direct-http": "./direct-http.js",
2929 "./forward": "./forward.js",
3030 "./inbox": "./inbox.js",
31 "./policy": "./policy.js",
3132 "./intent": "./intent.js",
3233 "./qualification": "./qualification.js",
3334 "./runtime/node": "./runtime/node.js",
packages/blind-client/policy.js +1327 -0
……@@ -0,0 +1,1327 @@
1import {
2 CELL_SIZE_CLASS,
3 DISPATCH_LIMITS,
4 FAMILY,
5 INBOX_FRAME_CLASS,
6 OPERATION,
7 OUTER_CLASS
8} from '@hiverelay/blind-protocol/wire-runtime-authority'
9import { fail } from './errors.js'
10import { selectedOperationProfile } from './selected-operation-profile.js'
11
12export const WORKLOAD = Object.freeze({
13 RECORDS: 'records',
14 RENDEZVOUS: 'rendezvous',
15 HISTORY: 'history',
16 LIVE: 'live',
17 SEMANTIC: 'semantic'
18})
19
20export const PRIMITIVE = Object.freeze({
21 CELL: 'cell',
22 INBOX: 'inbox',
23 CORE: 'core',
24 P2P_DIRECT: 'p2p-direct',
25 LOCAL_COMPUTE: 'local-compute',
26 EXTERNAL: 'external'
27})
28
29export const RUNTIME = Object.freeze({
30 BROWSER: 'browser',
31 NATIVE: 'native',
32 TOR_BROWSER: 'tor-browser'
33})
34
35export const PRIVACY_PRESET = Object.freeze({
36 FAST: 'fast',
37 PRIVATE: 'private',
38 HIGH_PRIVACY: 'high-privacy',
39 CUSTOM: 'custom'
40})
41
42export const TRANSPORT_REQUIREMENT = Object.freeze({
43 DIRECT: 'direct',
44 SOURCE_SEPARATED: 'source-separated',
45 TOR: 'tor'
46})
47
48export const TRANSPORT_PROFILE = Object.freeze({
49 DIRECT: 'direct-blind-v1',
50 OHTTP: 'split-web-ohttp-v1',
51 SPLIT_NATIVE: 'split-native-protomux-v1',
52 TOR_NATIVE: 'tor-native-full-v1',
53 TOR_BROWSER: 'tor-browser-full-v1',
54 P2P_DIRECT: 'p2p-direct',
55 EXTERNAL_DIRECT: 'external-direct-declared',
56 EXTERNAL_SOURCE_SEPARATED: 'external-source-separated-declared',
57 EXTERNAL_TOR: 'external-tor-declared'
58})
59
60export const DOWNGRADE_POLICY = Object.freeze({
61 DENY: 'deny',
62 PROMPT: 'prompt'
63})
64
65export const PLAN_STATUS = Object.freeze({
66 DRAFT: 'draft',
67 QUEUEABLE: 'queueable',
68 BLOCKED: 'blocked'
69})
70
71export const EVIDENCE_REQUIREMENT = Object.freeze({
72 SIGNED_TRANSPORT_PROFILE: 'signed-transport-profile',
73 VERIFIED_ENDPOINT: 'verified-endpoint',
74 FRESH_SIGNED_HEALTH: 'fresh-signed-health',
75 ADAPTER_CONFORMANCE: 'adapter-conformance',
76 CLASS_SHAPING_CAPTURE: 'class-shaping-capture',
77 STREAM_SHAPING_CAPTURE: 'stream-shaping-capture',
78 APPLICATION_IDENTITY_BOUNDARY: 'application-identity-boundary',
79 INBOX_ANONYMOUS_REPLY_PROFILE: 'inbox-anonymous-reply-profile',
80 UNIVERSAL_READ_BUCKET_PROFILE: 'universal-read-bucket-profile',
81 P2P_SESSION_PREPARATION: 'p2p-session-preparation',
82 ROLE_SEPARATION_CAPTURE: 'role-separation-capture',
83 NO_DIRECT_RACE_CAPTURE: 'no-direct-race-capture',
84 NO_CLEARNET_CAPTURE: 'no-clearnet-capture',
85 BROWSER_OPAQUE_ORIGIN_CAPTURE: 'browser-opaque-origin-capture',
86 EXTERNAL_ENDPOINT_EVIDENCE: 'external-endpoint-evidence',
87 EXTERNAL_CONNECTOR_CONFORMANCE: 'external-connector-conformance',
88 DISCLOSURE_AUTHORIZATION: 'disclosure-authorization'
89})
90
91const METADATA_SHAPING = Object.freeze(['none', 'padded', 'bucketed-experimental'])
92const IDENTITY_EXPOSURE = Object.freeze(['stable', 'session', 'anonymous-reply'])
93const READ_INTEREST = Object.freeze(['observable', 'bucketed-experimental'])
94const OUTER_ENVELOPE_HEADER_BYTES = 6
95const CELL_GET_RESULT_BODY_OVERHEAD_BYTES = 2
96const CELL_BLOB_OVERHEAD_BYTES = 1 + 12 + 16 + 4
97const CELL_PUT_PROFILE = selectedOperationProfile(FAMILY.CELL, OPERATION.CELL.PUT)
98const OUTER_CLASS_BYTES = Object.freeze(Object.values(OUTER_CLASS).sort((left, right) => left - right))
99// Exact maximum canonical request structure, including maximum admission bytes.
100// These are guarded against real canonical encodings by the policy test suite.
101const CELL_PUT_REQUEST_BODY_OVERHEAD_BYTES = 4398
102const CELL_GET_REQUEST_BODY_MAX_BYTES = 4201
103
104function maximumCellContentBytes (sizeClass) {
105 if (!Number.isInteger(sizeClass) || CELL_SIZE_CLASS[sizeClass] == null) {
106 fail('BAD_CLIENT_INPUT', 'sizeClass is outside the frozen cell classes')
107 }
108 return CELL_SIZE_CLASS[sizeClass] - CELL_BLOB_OVERHEAD_BYTES
109}
110
111const PRESETS = Object.freeze({
112 [PRIVACY_PRESET.FAST]: Object.freeze({
113 preset: PRIVACY_PRESET.FAST,
114 transport: TRANSPORT_REQUIREMENT.DIRECT,
115 metadataShaping: 'padded',
116 identityExposure: 'session',
117 readInterest: 'observable',
118 downgradePolicy: DOWNGRADE_POLICY.DENY,
119 backgroundPreferred: false
120 }),
121 [PRIVACY_PRESET.PRIVATE]: Object.freeze({
122 preset: PRIVACY_PRESET.PRIVATE,
123 transport: TRANSPORT_REQUIREMENT.SOURCE_SEPARATED,
124 metadataShaping: 'padded',
125 identityExposure: 'session',
126 readInterest: 'observable',
127 downgradePolicy: DOWNGRADE_POLICY.PROMPT,
128 backgroundPreferred: false
129 }),
130 [PRIVACY_PRESET.HIGH_PRIVACY]: Object.freeze({
131 preset: PRIVACY_PRESET.HIGH_PRIVACY,
132 transport: TRANSPORT_REQUIREMENT.TOR,
133 metadataShaping: 'padded',
134 identityExposure: 'session',
135 readInterest: 'observable',
136 downgradePolicy: DOWNGRADE_POLICY.DENY,
137 backgroundPreferred: true
138 })
139})
140
141const CUSTOM_FIELDS = Object.freeze([
142 'transport',
143 'metadataShaping',
144 'identityExposure',
145 'readInterest',
146 'downgradePolicy',
147 'backgroundPreferred'
148])
149const CUSTOM_OPTION_FIELDS = Object.freeze([...CUSTOM_FIELDS, 'base'])
150
151export const privacy = Object.freeze({
152 fast: options => createPrivacyIntent(PRIVACY_PRESET.FAST, options),
153 private: options => createPrivacyIntent(PRIVACY_PRESET.PRIVATE, options),
154 highPrivacy: options => createPrivacyIntent(PRIVACY_PRESET.HIGH_PRIVACY, options),
155 custom: options => createPrivacyIntent(PRIVACY_PRESET.CUSTOM, options)
156})
157
158export function createPrivacyIntent (preset = PRIVACY_PRESET.PRIVATE, options = {}) {
159 if (options == null) options = {}
160 if (!isPlainObject(options)) fail('BAD_CLIENT_INPUT', 'privacy options must be an object')
161 const fixed = typeof preset === 'string' && hasOwn(PRESETS, preset) ? PRESETS[preset] : null
162 if (fixed) {
163 if (Object.keys(options).length !== 0) {
164 fail('PRESET_IMMUTABLE', `${preset} is an immutable preset; use custom privacy constraints instead`)
165 }
166 return fixed
167 }
168 if (preset !== PRIVACY_PRESET.CUSTOM) fail('BAD_CLIENT_INPUT', `unknown privacy preset ${String(preset)}`)
169 const keys = Object.keys(options)
170 for (const key of keys) {
171 if (!CUSTOM_OPTION_FIELDS.includes(key)) fail('BAD_CLIENT_INPUT', `unknown custom privacy field ${key}`)
172 }
173 const baseName = options.base
174 if (baseName != null && (typeof baseName !== 'string' || !hasOwn(PRESETS, baseName))) {
175 fail('BAD_CLIENT_INPUT', 'custom privacy base is invalid')
176 }
177 const resolved = baseName == null ? {} : { ...PRESETS[baseName] }
178 delete resolved.preset
179 for (const field of CUSTOM_FIELDS) {
180 if (options[field] != null) resolved[field] = options[field]
181 }
182 if (CUSTOM_FIELDS.some(field => resolved[field] == null)) {
183 fail('BAD_CLIENT_INPUT', 'custom privacy must provide every closed constraint axis or inherit it from a fixed base')
184 }
185 const value = { preset, base: baseName || null, ...resolved }
186 validatePrivacyAxes(value)
187 return Object.freeze(value)
188}
189
190export function draftWorkloadPlan (options) {
191 if (!isPlainObject(options)) fail('BAD_CLIENT_INPUT', 'workload planning options are required')
192 assertOnlyFields(Object.keys(options), [
193 'workload',
194 'runtime',
195 'privacy',
196 'inventory',
197 'availability',
198 'durability',
199 'costBudget',
200 'externalDisclosure',
201 'downgradeAuthorization',
202 'disclosureAuthorization'
203 ], 'workload planning options')
204 if (options.downgradeAuthorization != null) {
205 fail('BAD_CLIENT_INPUT', 'the pure draft planner cannot consume downgrade authorization; qualification must remain consent-pending')
206 }
207 if (options.disclosureAuthorization != null) {
208 fail('BAD_CLIENT_INPUT', 'the pure draft planner cannot consume disclosure authorization; connector qualification must bind it later')
209 }
210 const workload = normalizeWorkload(options.workload)
211 const runtime = options.runtime
212 if (!Object.values(RUNTIME).includes(runtime)) fail('BAD_CLIENT_INPUT', 'runtime is invalid')
213 if (options.inventory != null && options.availability != null) {
214 fail('BAD_CLIENT_INPUT', 'provide inventory, not both inventory and the compatibility availability alias')
215 }
216 const inventory = normalizeInventory(options.inventory != null ? options.inventory : options.availability)
217 const intent = normalizePrivacy(options.privacy)
218 const budget = normalizeCostBudget(options.costBudget)
219 const primitiveChoice = choosePrimitive(workload, runtime, inventory)
220 if (primitiveChoice.code) {
221 return blocked(workload, runtime, primitiveChoice.primitive, intent, primitiveChoice.code, primitiveChoice.reason)
222 }
223 const primitive = primitiveChoice.primitive
224 if (workload.kind !== WORKLOAD.SEMANTIC && options.externalDisclosure != null) {
225 return blocked(workload, runtime, primitive, intent, 'WORKLOAD_DISCLOSURE_CONFLICT',
226 'external-service disclosure applies only to semantic workloads')
227 }
228 if (workload.kind === WORKLOAD.SEMANTIC && primitive === PRIMITIVE.LOCAL_COMPUTE &&
229 options.externalDisclosure != null) {
230 return blocked(workload, runtime, primitive, intent, 'WORKLOAD_DISCLOSURE_CONFLICT',
231 'local semantic execution must not carry an external-service disclosure')
232 }
233 if (workload.kind === WORKLOAD.SEMANTIC && primitive === PRIMITIVE.EXTERNAL) {
234 const disclosureError = validateExternalDisclosure(options.externalDisclosure)
235 if (disclosureError) {
236 return blocked(workload, runtime, primitive, intent, disclosureError.code, disclosureError.reason)
237 }
238 }
239 const durabilityResolution = normalizeDurability(options.durability, primitive)
240 if (durabilityResolution.code) {
241 return blocked(workload, runtime, primitive, intent, durabilityResolution.code, durabilityResolution.reason)
242 }
243 const durability = durabilityResolution.value
244 let cell = null
245 let inbox = null
246 if (primitive === PRIMITIVE.CELL) {
247 cell = planCell(workload, durability)
248 if (cell.requiresChunkManifest) {
249 return blocked(workload, runtime, primitive, intent, 'CHUNK_MANIFEST_REQUIRED',
250 'the object exceeds the largest Cell payload; the application must supply a signed chunk manifest', {
251 durability,
252 costBudget: budget,
253 chunking: cell
254 })
255 }
256 }
257 if (primitive === PRIMITIVE.INBOX) {
258 inbox = planInbox(workload)
259 if (inbox.exceedsMaximumFrame) {
260 return blocked(workload, runtime, primitive, intent, 'RENDEZVOUS_FRAME_TOO_LARGE',
261 'the opaque rendezvous payload exceeds the largest universal Inbox frame', {
262 durability,
263 costBudget: budget,
264 inbox
265 })
266 }
267 }
268
269 const axisResolution = resolveNonTransportAxes(intent, primitive)
270 if (axisResolution.blockers.length > 0) {
271 return blocked(workload, runtime, primitive, intent, 'PRIVACY_AXIS_UNSUPPORTED',
272 `unsupported privacy axes: ${axisResolution.blockers.join(', ')}`, { privacyResolution: axisResolution.axes })
273 }
274
275 if (budget != null && primitive !== PRIMITIVE.CELL) {
276 return blocked(workload, runtime, primitive, intent, 'COST_EVIDENCE_UNAVAILABLE',
277 `the ${primitive} adapter has not supplied a qualified cost model for the requested hard ceiling`, {
278 durability,
279 costBudget: budget,
280 privacyResolution: axisResolution.axes
281 })
282 }
283 if (cell != null && budget != null && cell.blindEnvelope == null) {
284 return blocked(workload, runtime, primitive, intent, 'COST_EVIDENCE_UNAVAILABLE',
285 'an exact Cell size basis is required before enforcing a blind-envelope amplification ceiling', {
286 durability,
287 costBudget: budget,
288 cell,
289 privacyResolution: axisResolution.axes
290 })
291 }
292 if (cell != null && budget != null &&
293 cell.blindEnvelope.usefulByteAmplification > budget.maxBlindEnvelopeAmplification) {
294 return blocked(workload, runtime, primitive, intent, 'COST_BUDGET_EXCEEDED',
295 `v1 blind-envelope amplification ${cell.blindEnvelope.usefulByteAmplification} exceeds the configured ceiling`, {
296 estimate: cell.blindEnvelope,
297 suggestion: cell.batchRecommended ? 'batch-small-records' : 'use-core-or-chunked-p2p',
298 durability,
299 costBudget: budget,
300 privacyResolution: axisResolution.axes
301 })
302 }
303
304 if (workload.kind === WORKLOAD.SEMANTIC) {
305 return planSemantic({ workload, runtime, primitive, intent, inventory, axisResolution, disclosure: options.externalDisclosure })
306 }
307 if (workload.kind === WORKLOAD.LIVE) {
308 return planLive({ workload, runtime, primitive, intent, inventory, axisResolution })
309 }
310
311 const transport = resolveTransport({
312 workload,
313 runtime,
314 primitive,
315 intent,
316 inventory
317 })
318 const intendedTransportProfile = transport.profile || intendedProfile(runtime, intent.transport)
319 if (intendedTransportProfile == null) {
320 return blocked(workload, runtime, primitive, intent, 'TRANSPORT_UNSUPPORTED',
321 'the requested transport has no candidate profile in this runtime', {
322 durability,
323 costBudget: budget,
324 cell,
325 inbox,
326 privacyResolution: axisResolution.axes
327 })
328 }
329 const privacyResolution = deepFreeze({
330 ...axisResolution.axes,
331 transport: axisRecord(intent.transport, intendedTransportProfile, 'qualification-required',
332 transportRequiredEvidence(intendedTransportProfile, runtime),
333 assumptions(intendedTransportProfile), transportClaim(intendedTransportProfile))
334 })
335 const planDetails = {
336 wireOperation: wireOperationFor(primitive, workload.operation),
337 privacyResolution,
338 intendedTransportProfile,
339 execution: intent.backgroundPreferred ? 'background-preferred' : 'interactive-capable',
340 durability,
341 costBudget: budget,
342 claims: claimsFor(primitive, intendedTransportProfile, runtime),
343 assumptions: assumptions(intendedTransportProfile),
344 warnings: warningsFor(intent, intendedTransportProfile),
345 cell,
346 inbox
347 }
348 if (primitive === PRIMITIVE.CORE && inventory.core !== true) {
349 return queueable(workload, runtime, primitive, intent, 'QUEUEABLE_CORE_STREAM_UNAVAILABLE',
350 'Blind Core needs an available native stream adapter', planDetails)
351 }
352 if (!transport.profile) {
353 const extra = { ...planDetails, ...transport }
354 delete extra.status
355 delete extra.code
356 delete extra.reason
357 return transport.status === PLAN_STATUS.QUEUEABLE
358 ? queueable(workload, runtime, primitive, intent, transport.code, transport.reason, extra)
359 : blocked(workload, runtime, primitive, intent, transport.code, transport.reason, extra)
360 }
361 if (primitive === PRIMITIVE.CORE && transport.profile === TRANSPORT_PROFILE.OHTTP) {
362 return blocked(workload, runtime, primitive, intent, 'TRANSPORT_UNSUPPORTED',
363 'Blind Core cannot open replication over OHTTP', planDetails)
364 }
365
366 const plan = {
367 status: PLAN_STATUS.DRAFT,
368 code: 'QUALIFICATION_REQUIRED',
369 executable: false,
370 workload: workload.kind,
371 primitive,
372 runtime,
373 operation: workload.operation,
374 requestedPrivacy: intent,
375 ...planDetails,
376 downgraded: false,
377 downgradeAuthorized: false,
378 readiness: 'a qualified endpoint, exact operation, profile hash, adapter evidence and fresh health must still be pinned'
379 }
380
381 return deepFreeze(plan)
382}
383
384export const planWorkload = draftWorkloadPlan
385export const draftWorkload = draftWorkloadPlan
386
387export function estimateCellWireV1 (options) {
388 if (!isPlainObject(options)) fail('BAD_CLIENT_INPUT', 'cell envelope estimate options are required')
389 assertOnlyFields(Object.keys(options), ['operation', 'sizeClass', 'usefulBytes', 'replicas'], 'cell envelope estimate')
390 const sizeClass = options.sizeClass
391 const cellBytes = CELL_SIZE_CLASS[sizeClass]
392 if (cellBytes == null) fail('BAD_CLIENT_INPUT', 'sizeClass is outside the frozen cell classes')
393 const operation = String(options.operation || '').toUpperCase()
394 if (operation !== 'PUT' && operation !== 'GET') fail('BAD_CLIENT_INPUT', 'operation must be PUT or GET')
395 const replicaCount = options.replicas == null ? 1 : options.replicas
396 if (!Number.isSafeInteger(replicaCount) || replicaCount < 1 || replicaCount > 16) {
397 fail('BAD_CLIENT_INPUT', 'replicas must be within 1..16')
398 }
399 const usefulBytes = options.usefulBytes == null ? maximumCellContentBytes(sizeClass) : options.usefulBytes
400 if (!Number.isSafeInteger(usefulBytes) || usefulBytes < 1 || usefulBytes > maximumCellContentBytes(sizeClass)) {
401 fail('BAD_CLIENT_INPUT', 'usefulBytes must fit the selected Cell content capacity')
402 }
403 const requestBodyBytes = operation === 'PUT'
404 ? cellBytes + CELL_PUT_REQUEST_BODY_OVERHEAD_BYTES
405 : CELL_GET_REQUEST_BODY_MAX_BYTES
406 const resultBodyBytes = operation === 'PUT'
407 ? CELL_PUT_PROFILE.maxResultBodyBytes
408 : cellBytes + CELL_GET_RESULT_BODY_OVERHEAD_BYTES
409 const innerBytes = DISPATCH_LIMITS.PREFIX_BYTES + DISPATCH_LIMITS.HEADER_BYTES +
410 Math.max(requestBodyBytes, resultBodyBytes)
411 const outerBytesPerDirection = smallestOuterClassBytes(OUTER_ENVELOPE_HEADER_BYTES + innerBytes)
412 const roundTripBytesPerDestination = outerBytesPerDirection * 2
413 const totalOuterBytes = roundTripBytesPerDestination * replicaCount
414 return Object.freeze({
415 protocol: 'hiverelay-blind-v1-symmetric-outer',
416 accountingBoundary: 'frozen blind outer envelopes only; transport wrappers, handshakes, retries and IP framing excluded',
417 operation,
418 sizeClass,
419 cellBytes,
420 usefulBytes,
421 exact: true,
422 requestOuterBytes: outerBytesPerDirection,
423 responseOuterBytes: outerBytesPerDirection,
424 roundTripBytesPerDestination,
425 destinationCount: replicaCount,
426 totalOuterBytes,
427 usefulByteAmplification: totalOuterBytes / usefulBytes
428 })
429}
430
431function normalizeWorkload (value) {
432 if (typeof value === 'string') value = { kind: value }
433 if (!isPlainObject(value) || !Object.values(WORKLOAD).includes(value.kind)) {
434 fail('BAD_CLIENT_INPUT', 'workload kind is invalid')
435 }
436 assertOnlyFields(Object.keys(value), ['kind', 'byteLength', 'operation', 'execution', 'primitive'], 'workload')
437 const byteLength = value.byteLength == null ? null : value.byteLength
438 if (byteLength != null && (!Number.isSafeInteger(byteLength) || byteLength < 1)) {
439 fail('BAD_CLIENT_INPUT', 'workload byteLength must be a positive safe integer when supplied')
440 }
441 let defaultOperation = 'put'
442 let allowedOperations = ['put', 'get']
443 if (value.kind === WORKLOAD.RENDEZVOUS) {
444 defaultOperation = 'append'
445 allowedOperations = ['create', 'append', 'read', 'watch', 'renew', 'close']
446 } else if (value.kind === WORKLOAD.LIVE) {
447 defaultOperation = 'session'
448 allowedOperations = ['session']
449 } else if (value.kind === WORKLOAD.SEMANTIC) {
450 defaultOperation = 'execute'
451 allowedOperations = ['execute']
452 }
453 const operation = value.operation == null ? defaultOperation : String(value.operation).toLowerCase()
454 if (!allowedOperations.includes(operation)) {
455 fail('BAD_CLIENT_INPUT', `${value.kind} workload operation is invalid`)
456 }
457 if (value.kind !== WORKLOAD.SEMANTIC && value.execution != null) {
458 fail('BAD_CLIENT_INPUT', 'execution applies only to semantic workloads')
459 }
460 if (value.execution != null && value.execution !== 'local' && value.execution !== 'external') {
461 fail('BAD_CLIENT_INPUT', 'semantic execution must be local or external')
462 }
463 let execution = value.execution
464 if (value.kind === WORKLOAD.SEMANTIC && execution == null) {
465 execution = value.primitive === PRIMITIVE.EXTERNAL ? 'external' : 'local'
466 }
467 return Object.freeze({ ...value, byteLength, operation, execution })
468}
469
470function normalizePrivacy (value) {
471 if (value == null) return privacy.private()
472 if (typeof value === 'string') return createPrivacyIntent(value)
473 if (!isPlainObject(value)) fail('BAD_CLIENT_INPUT', 'privacy intent is invalid')
474 const preset = value.preset || PRIVACY_PRESET.CUSTOM
475 const fixed = typeof preset === 'string' && hasOwn(PRESETS, preset) ? PRESETS[preset] : null
476 if (fixed) {
477 const keys = Object.keys(value)
478 if (keys.length !== Object.keys(fixed).length || keys.some(key => fixed[key] !== value[key])) {
479 fail('PRESET_IMMUTABLE', `${preset} is an immutable preset; use custom privacy constraints instead`)
480 }
481 return fixed
482 }
483 const custom = { ...value }
484 delete custom.preset
485 return createPrivacyIntent(preset, custom)
486}
487
488function validatePrivacyAxes (value) {
489 if (!Object.values(TRANSPORT_REQUIREMENT).includes(value.transport)) {
490 fail('BAD_CLIENT_INPUT', 'privacy transport requirement is invalid')
491 }
492 if (!METADATA_SHAPING.includes(value.metadataShaping)) fail('BAD_CLIENT_INPUT', 'metadataShaping is invalid')
493 if (!IDENTITY_EXPOSURE.includes(value.identityExposure)) fail('BAD_CLIENT_INPUT', 'identityExposure is invalid')
494 if (!READ_INTEREST.includes(value.readInterest)) fail('BAD_CLIENT_INPUT', 'readInterest is invalid')
495 if (!Object.values(DOWNGRADE_POLICY).includes(value.downgradePolicy)) fail('BAD_CLIENT_INPUT', 'downgradePolicy is invalid')
496 if (typeof value.backgroundPreferred !== 'boolean') fail('BAD_CLIENT_INPUT', 'backgroundPreferred must be boolean')
497}
498
499function resolveNonTransportAxes (intent, primitive) {
500 let axes
501 if (primitive === PRIMITIVE.LOCAL_COMPUTE) {
502 axes = {
503 metadataShaping: axisRecord(intent.metadataShaping, null, 'not-applicable', [], [],
504 'local compute selects no network metadata-shaping path'),
505 identityExposure: axisRecord(intent.identityExposure, null, 'not-applicable', [], [],
506 'local compute exposes no identity to a network service'),
507 readInterest: axisRecord(intent.readInterest, null, 'not-applicable', [], [],
508 'local compute has no storage read-interest observer'),
509 transport: axisRecord(intent.transport, null, 'not-applicable', [], [],
510 'local compute selects no network transport')
511 }
512 } else if (primitive === PRIMITIVE.EXTERNAL) {
513 axes = {
514 metadataShaping: axisRecord(intent.metadataShaping, null, 'connector-qualification-required',
515 [EVIDENCE_REQUIREMENT.EXTERNAL_CONNECTOR_CONFORMANCE], [],
516 'the external connector must prove its metadata-shaping behavior'),
517 identityExposure: axisRecord(intent.identityExposure, null, 'connector-qualification-required',
518 [EVIDENCE_REQUIREMENT.EXTERNAL_CONNECTOR_CONFORMANCE, EVIDENCE_REQUIREMENT.DISCLOSURE_AUTHORIZATION], [],
519 'the external connector and application must prove the disclosed identity boundary'),
520 readInterest: axisRecord(intent.readInterest, null, 'connector-qualification-required',
521 [EVIDENCE_REQUIREMENT.DISCLOSURE_AUTHORIZATION], [],
522 'the external service can interpret request interest unless its connector proves otherwise')
523 }
524 } else if (primitive === PRIMITIVE.P2P_DIRECT) {
525 axes = {
526 metadataShaping: intent.metadataShaping === 'bucketed-experimental'
527 ? axisRecord(intent.metadataShaping, null, 'unsupported',
528 [EVIDENCE_REQUIREMENT.UNIVERSAL_READ_BUCKET_PROFILE], [],
529 'the direct P2P profile has no qualified bucketed metadata-shaping mode')
530 : axisRecord(intent.metadataShaping, null, 'adapter-qualification-required',
531 [EVIDENCE_REQUIREMENT.P2P_SESSION_PREPARATION, EVIDENCE_REQUIREMENT.ADAPTER_CONFORMANCE], [],
532 'the prepared P2P adapter must prove any padding or shaping behavior'),
533 identityExposure: intent.identityExposure === 'anonymous-reply'
534 ? axisRecord(intent.identityExposure, null, 'unsupported',
535 [EVIDENCE_REQUIREMENT.APPLICATION_IDENTITY_BOUNDARY], [],
536 'peer-authenticated live sessions do not provide anonymous-reply identity exposure')
537 : axisRecord(intent.identityExposure, null, 'adapter-qualification-required',
538 [EVIDENCE_REQUIREMENT.P2P_SESSION_PREPARATION, EVIDENCE_REQUIREMENT.APPLICATION_IDENTITY_BOUNDARY], [],
539 'peer authentication and application identity scope must be proven during session preparation'),
540 readInterest: axisRecord(intent.readInterest, null, 'not-applicable', [], [],
541 'a live P2P session has no storage-locator read-interest axis')
542 }
543 } else {
544 const paddingPlan = primitive === PRIMITIVE.CORE ? 'stream-shaping-adapter-defined' : 'fixed-class-padding'
545 const anonymousReplySupported = primitive === PRIMITIVE.INBOX
546 axes = {
547 metadataShaping: intent.metadataShaping === 'bucketed-experimental'
548 ? axisRecord(intent.metadataShaping, null, 'unsupported',
549 [EVIDENCE_REQUIREMENT.UNIVERSAL_READ_BUCKET_PROFILE], [],
550 'universal read buckets/PIR are not implemented')
551 : axisRecord(intent.metadataShaping, paddingPlan, 'qualification-required',
552 [primitive === PRIMITIVE.CORE
553 ? EVIDENCE_REQUIREMENT.STREAM_SHAPING_CAPTURE
554 : EVIDENCE_REQUIREMENT.CLASS_SHAPING_CAPTURE,
555 EVIDENCE_REQUIREMENT.ADAPTER_CONFORMANCE], [],
556 'the selected adapter must prove the planned class-shaping behavior'),
557 identityExposure: intent.identityExposure === 'anonymous-reply' && !anonymousReplySupported
558 ? axisRecord(intent.identityExposure, null, 'unsupported',
559 [EVIDENCE_REQUIREMENT.INBOX_ANONYMOUS_REPLY_PROFILE], [],
560 'anonymous-reply exposure is available only through a separately prepared Inbox capability profile')
561 : axisRecord(intent.identityExposure,
562 intent.identityExposure === 'anonymous-reply' ? 'capability-scoped-reply' : intent.identityExposure,
563 'application-policy-required',
564 intent.identityExposure === 'anonymous-reply'
565 ? [EVIDENCE_REQUIREMENT.INBOX_ANONYMOUS_REPLY_PROFILE, EVIDENCE_REQUIREMENT.APPLICATION_IDENTITY_BOUNDARY]
566 : [EVIDENCE_REQUIREMENT.APPLICATION_IDENTITY_BOUNDARY], [],
567 'the application adapter must prove that no stronger identity enters relay-visible state'),
568 readInterest: intent.readInterest === 'observable'
569 ? axisRecord(intent.readInterest, 'observable', 'known-visible', [], [],
570 'requested locators and access timing remain visible to storage')
571 : axisRecord(intent.readInterest, null, 'unsupported',
572 [EVIDENCE_REQUIREMENT.UNIVERSAL_READ_BUCKET_PROFILE], [],
573 'bucketed read interest needs a separately qualified universal bucket/PIR profile')
574 }
575 }
576 const blockers = Object.entries(axes).filter(([, resolution]) => resolution.state === 'unsupported').map(([name]) => name)
577 return { axes: deepFreeze(axes), blockers }
578}
579
580function normalizeInventory (value = {}) {
581 if (!isPlainObject(value)) fail('BAD_CLIENT_INPUT', 'deployment inventory must be an object')
582 const fields = Object.keys(value)
583 assertOnlyFields(fields, [
584 'direct',
585 'ohttp',
586 'splitNative',
587 'torNative',
588 'torBrowser',
589 'core',
590 'p2p',
591 'external'
592 ], 'deployment inventory')
593 for (const field of fields) {
594 if (typeof value[field] !== 'boolean') fail('BAD_CLIENT_INPUT', `deployment inventory ${field} must be boolean`)
595 }
596 return Object.freeze({ ...value })
597}
598
599function normalizeDurability (value = {}, primitive) {
600 if (value == null) value = {}
601 if (!isPlainObject(value)) fail('BAD_CLIENT_INPUT', 'durability must be an object')
602 const fields = Object.keys(value)
603 if (primitive === PRIMITIVE.CELL) {
604 assertOnlyFields(fields, [
605 'replicaTarget',
606 'acknowledgementTarget',
607 'readbackTarget',
608 'fetchTarget',
609 'independentOperatorGroups'
610 ], 'Cell durability')
611 const result = {
612 model: 'cell-replicas-v1',
613 replicaTarget: numberOr(value.replicaTarget, 3, 'replicaTarget'),
614 acknowledgementTarget: numberOr(value.acknowledgementTarget, 2, 'acknowledgementTarget'),
615 readbackTarget: numberOr(value.readbackTarget, 2, 'readbackTarget'),
616 fetchTarget: numberOr(value.fetchTarget, 1, 'fetchTarget'),
617 independentOperatorGroups: numberOr(value.independentOperatorGroups, 2, 'independentOperatorGroups')
618 }
619 if (result.acknowledgementTarget > result.replicaTarget || result.readbackTarget > result.replicaTarget ||
620 result.fetchTarget > result.replicaTarget) {
621 fail('BAD_CLIENT_INPUT', 'Cell durability evidence targets cannot exceed replicaTarget')
622 }
623 if (result.independentOperatorGroups > result.readbackTarget) {
624 fail('BAD_CLIENT_INPUT', 'Cell independentOperatorGroups cannot exceed readbackTarget')
625 }
626 return { value: Object.freeze(result) }
627 }
628 if (primitive === PRIMITIVE.INBOX) {
629 assertOnlyFields(fields, [
630 'stripeCountLog2',
631 'replicaTargetPerStripe',
632 'appendAcknowledgementTargetPerStripe',
633 'readbackTargetPerStripe',
634 'independentOperatorGroups'
635 ], 'Inbox durability')
636 const result = {
637 model: 'inbox-replicas-and-stripes-v1',
638 stripeCountLog2: numberBetweenOr(value.stripeCountLog2, 3, 0, 6, 'stripeCountLog2'),
639 replicaTargetPerStripe: numberOr(value.replicaTargetPerStripe, 3, 'replicaTargetPerStripe'),
640 appendAcknowledgementTargetPerStripe: numberOr(value.appendAcknowledgementTargetPerStripe, 2, 'appendAcknowledgementTargetPerStripe'),
641 readbackTargetPerStripe: numberOr(value.readbackTargetPerStripe, 2, 'readbackTargetPerStripe'),
642 independentOperatorGroups: numberOr(value.independentOperatorGroups, 2, 'independentOperatorGroups')
643 }
644 result.stripeCount = 2 ** result.stripeCountLog2
645 if (result.appendAcknowledgementTargetPerStripe > result.replicaTargetPerStripe ||
646 result.readbackTargetPerStripe > result.replicaTargetPerStripe) {
647 fail('BAD_CLIENT_INPUT', 'Inbox durability evidence targets cannot exceed replicaTargetPerStripe')
648 }
649 if (result.independentOperatorGroups > result.readbackTargetPerStripe) {
650 fail('BAD_CLIENT_INPUT', 'Inbox independentOperatorGroups cannot exceed readbackTargetPerStripe')
651 }
652 return { value: Object.freeze(result) }
653 }
654 if (primitive === PRIMITIVE.CORE) {
655 assertOnlyFields(fields, ['mirrorTarget', 'proofTarget', 'recentlyServedTarget', 'independentOperatorGroups'], 'Core durability')
656 const result = {
657 model: 'core-mirrors-v1',
658 mirrorTarget: numberOr(value.mirrorTarget, 3, 'mirrorTarget'),
659 proofTarget: numberOr(value.proofTarget, 1, 'proofTarget'),
660 recentlyServedTarget: numberOr(value.recentlyServedTarget, 2, 'recentlyServedTarget'),
661 independentOperatorGroups: numberOr(value.independentOperatorGroups, 2, 'independentOperatorGroups')
662 }
663 if (result.proofTarget > result.mirrorTarget || result.recentlyServedTarget > result.mirrorTarget) {
664 fail('BAD_CLIENT_INPUT', 'Core durability evidence targets cannot exceed mirrorTarget')
665 }
666 if (result.independentOperatorGroups > result.recentlyServedTarget) {
667 fail('BAD_CLIENT_INPUT', 'Core independentOperatorGroups cannot exceed recentlyServedTarget')
668 }
669 return { value: Object.freeze(result) }
670 }
671 if (fields.length > 0) {
672 return {
673 code: 'WORKLOAD_DURABILITY_CONFLICT',
674 reason: `${primitive} does not provide Blind Cell, Inbox, or Core durability; compose a separate durable workload`
675 }
676 }
677 return { value: null }
678}
679
680function numberOr (value, fallback, field) {
681 return numberBetweenOr(value, fallback, 1, 16, field)
682}
683
684function numberBetweenOr (value, fallback, minimum, maximum, field) {
685 const result = value == null ? fallback : value
686 if (!Number.isSafeInteger(result) || result < minimum || result > maximum) {
687 fail('BAD_CLIENT_INPUT', `durability ${field} is outside ${minimum}..${maximum}`)
688 }
689 return result
690}
691
692function assertOnlyFields (actual, allowed, label) {
693 const unknown = actual.filter(field => !allowed.includes(field))
694 if (unknown.length > 0) fail('BAD_CLIENT_INPUT', `${label} has unknown fields: ${unknown.join(', ')}`)
695}
696
697function choosePrimitive (workload, runtime, inventory) {
698 let primitive = workload.primitive
699 if (primitive != null && !Object.values(PRIMITIVE).includes(primitive)) {
700 fail('BAD_CLIENT_INPUT', 'workload primitive override is invalid')
701 }
702 if (primitive == null) {
703 if (workload.kind === WORKLOAD.RECORDS) primitive = PRIMITIVE.CELL
704 else if (workload.kind === WORKLOAD.RENDEZVOUS) primitive = PRIMITIVE.INBOX
705 else if (workload.kind === WORKLOAD.HISTORY) {
706 primitive = runtime === RUNTIME.NATIVE ? PRIMITIVE.CORE : PRIMITIVE.CELL
707 } else if (workload.kind === WORKLOAD.LIVE) primitive = PRIMITIVE.P2P_DIRECT
708 else primitive = workload.execution === 'local' ? PRIMITIVE.LOCAL_COMPUTE : PRIMITIVE.EXTERNAL
709 }
710 const allowed = {
711 [WORKLOAD.RECORDS]: [PRIMITIVE.CELL],
712 [WORKLOAD.RENDEZVOUS]: [PRIMITIVE.INBOX],
713 [WORKLOAD.HISTORY]: runtime === RUNTIME.NATIVE ? [PRIMITIVE.CELL, PRIMITIVE.CORE] : [PRIMITIVE.CELL],
714 [WORKLOAD.LIVE]: [PRIMITIVE.P2P_DIRECT],
715 [WORKLOAD.SEMANTIC]: [PRIMITIVE.LOCAL_COMPUTE, PRIMITIVE.EXTERNAL]
716 }
717 if (!allowed[workload.kind].includes(primitive)) {
718 return {
719 primitive,
720 code: 'WORKLOAD_PRIMITIVE_UNSUPPORTED',
721 reason: `${primitive} cannot represent the ${workload.kind} workload in the ${runtime} runtime`
722 }
723 }
724 if (workload.kind === WORKLOAD.SEMANTIC) {
725 const expected = workload.execution === 'local' ? PRIMITIVE.LOCAL_COMPUTE : PRIMITIVE.EXTERNAL
726 if (primitive !== expected) {
727 return {
728 primitive,
729 code: 'WORKLOAD_PRIMITIVE_CONFLICT',
730 reason: `${workload.execution} semantic execution requires the ${expected} primitive`
731 }
732 }
733 }
734 return { primitive }
735}
736
737function intendedProfile (runtime, requirement) {
738 if (requirement === TRANSPORT_REQUIREMENT.DIRECT) return TRANSPORT_PROFILE.DIRECT
739 if (requirement === TRANSPORT_REQUIREMENT.SOURCE_SEPARATED) {
740 if (runtime === RUNTIME.BROWSER) return TRANSPORT_PROFILE.OHTTP
741 if (runtime === RUNTIME.NATIVE) return TRANSPORT_PROFILE.SPLIT_NATIVE
742 if (runtime === RUNTIME.TOR_BROWSER) return TRANSPORT_PROFILE.TOR_BROWSER
743 }
744 if (requirement === TRANSPORT_REQUIREMENT.TOR) {
745 if (runtime === RUNTIME.NATIVE) return TRANSPORT_PROFILE.TOR_NATIVE
746 if (runtime === RUNTIME.TOR_BROWSER) return TRANSPORT_PROFILE.TOR_BROWSER
747 }
748 return null
749}
750
751function wireOperationFor (primitive, applicationOperation) {
752 let familyName
753 let operationName
754 if (primitive === PRIMITIVE.CELL) {
755 familyName = 'CELL'
756 operationName = applicationOperation.toUpperCase()
757 } else if (primitive === PRIMITIVE.INBOX) {
758 familyName = 'INBOX'
759 operationName = applicationOperation.toUpperCase()
760 } else if (primitive === PRIMITIVE.CORE) {
761 familyName = 'CORE'
762 operationName = applicationOperation === 'put' ? 'MIRROR' : 'OPEN_REPLICATION'
763 } else {
764 return null
765 }
766 const operationId = OPERATION[familyName][operationName]
767 if (operationId == null) fail('BAD_CLIENT_INPUT', `${primitive} has no wire operation for ${applicationOperation}`)
768 return Object.freeze({
769 family: familyName,
770 familyId: FAMILY[familyName],
771 operation: operationName,
772 operationId
773 })
774}
775
776function resolveTransport (context) {
777 const { runtime, intent, inventory } = context
778 let profile = null
779 let reason = null
780 if (intent.transport === TRANSPORT_REQUIREMENT.DIRECT) {
781 if (inventory.direct === true) profile = TRANSPORT_PROFILE.DIRECT
782 else reason = 'direct transport is unavailable'
783 } else if (intent.transport === TRANSPORT_REQUIREMENT.SOURCE_SEPARATED) {
784 if (runtime === RUNTIME.BROWSER && inventory.ohttp === true) profile = TRANSPORT_PROFILE.OHTTP
785 else if (runtime === RUNTIME.NATIVE && inventory.splitNative === true) profile = TRANSPORT_PROFILE.SPLIT_NATIVE
786 else if (runtime === RUNTIME.TOR_BROWSER && inventory.torBrowser === true) profile = TRANSPORT_PROFILE.TOR_BROWSER
787 else reason = 'the runtime has no available source-separated transport'
788 } else if (runtime === RUNTIME.NATIVE && inventory.torNative === true) profile = TRANSPORT_PROFILE.TOR_NATIVE
789 else if (runtime === RUNTIME.TOR_BROWSER && inventory.torBrowser === true) profile = TRANSPORT_PROFILE.TOR_BROWSER
790 else reason = 'the runtime has no available full Tor transport'
791
792 if (profile) return { profile, downgraded: false }
793 if (intent.downgradePolicy === DOWNGRADE_POLICY.PROMPT && inventory.direct === true) {
794 return {
795 profile: null,
796 status: PLAN_STATUS.QUEUEABLE,
797 code: 'DOWNGRADE_CONSENT_REQUIRED',
798 reason,
799 consentRequired: true,
800 suggestedTransportProfile: TRANSPORT_PROFILE.DIRECT
801 }
802 }
803 return {
804 profile: null,
805 status: PLAN_STATUS.QUEUEABLE,
806 code: 'QUEUEABLE_PRIVACY_PATH_UNAVAILABLE',
807 reason,
808 consentRequired: false
809 }
810}
811
812function planLive (context) {
813 const { workload, runtime, primitive, intent, inventory, axisResolution } = context
814 const profile = TRANSPORT_PROFILE.P2P_DIRECT
815 const details = {
816 wireOperation: null,
817 privacyResolution: deepFreeze({
818 ...axisResolution.axes,
819 transport: axisRecord(intent.transport, profile, 'session-preparation-required',
820 transportRequiredEvidence(profile, runtime),
821 ['peer reachability and direct-path metadata exposure'], transportClaim(profile))
822 }),
823 intendedTransportProfile: profile,
824 execution: 'interactive',
825 durability: null,
826 costBudget: null,
827 claims: claimsFor(primitive, profile, runtime),
828 assumptions: ['peer reachability and direct-path metadata exposure'],
829 warnings: ['pair with Cells or Blind Core when offline durability is required']
830 }
831 if (intent.transport !== TRANSPORT_REQUIREMENT.DIRECT) {
832 return blocked(workload, runtime, primitive, intent, 'WORKLOAD_PRIVACY_CONFLICT',
833 'live P2P is a low-latency direct path; use an asynchronous Cell/Core path for stronger transport privacy', details)
834 }
835 if (inventory.p2p !== true) {
836 return queueable(workload, runtime, primitive, intent, 'P2P_PATH_UNAVAILABLE',
837 'no direct P2P path is currently present in the deployment inventory', details)
838 }
839 return deepFreeze({
840 status: PLAN_STATUS.DRAFT,
841 code: 'P2P_SESSION_PREPARATION_REQUIRED',
842 executable: false,
843 workload: workload.kind,
844 primitive,
845 runtime,
846 operation: workload.operation,
847 requestedPrivacy: intent,
848 ...details,
849 downgraded: false,
850 downgradeAuthorized: false,
851 readiness: 'ICE/NAT traversal, peer authentication and the exact session path must still be prepared'
852 })
853}
854
855function planSemantic (context) {
856 const { workload, runtime, primitive, intent, inventory, axisResolution, disclosure } = context
857 if (primitive === PRIMITIVE.LOCAL_COMPUTE) {
858 if (disclosure != null) {
859 return blocked(workload, runtime, primitive, intent, 'WORKLOAD_DISCLOSURE_CONFLICT',
860 'local semantic execution must not carry an external-service disclosure', {
861 privacyResolution: axisResolution.axes
862 })
863 }
864 return deepFreeze({
865 status: PLAN_STATUS.DRAFT,
866 code: 'LOCAL_COMPUTE_APP_OWNED',
867 executable: false,
868 workload: workload.kind,
869 primitive,
870 runtime,
871 operation: workload.operation,
872 requestedPrivacy: intent,
873 privacyResolution: axisResolution.axes,
874 intendedTransportProfile: null,
875 wireOperation: null,
876 execution: 'local-application-code',
877 durability: null,
878 costBudget: null,
879 claims: claimsFor(primitive, null, runtime),
880 assumptions: ['the application controls the local execution environment'],
881 warnings: [],
882 downgraded: false,
883 downgradeAuthorized: false,
884 readiness: 'local semantic execution is application-owned and never sent to the blind substrate'
885 })
886 }
887 const disclosureError = validateExternalDisclosure(disclosure)
888 if (disclosureError) {
889 return blocked(workload, runtime, primitive, intent, disclosureError.code,
890 disclosureError.reason, { privacyResolution: axisResolution.axes })
891 }
892 if (disclosure.networkTransport !== intent.transport) {
893 return blocked(workload, runtime, primitive, intent, 'PRIVACY_AXIS_UNSUPPORTED',
894 'the disclosed external network transport does not satisfy the requested privacy transport', {
895 privacyResolution: axisResolution.axes
896 })
897 }
898 const profile = externalTransportProfile(disclosure.networkTransport)
899 const disclosureMetadata = externalDisclosureMetadata(disclosure)
900 const claims = deepFreeze({
901 ...claimsFor(primitive, profile, runtime),
902 semanticDisclosure: {
903 claimCeiling: 'explicitly non-blind semantic service',
904 operatorSees: disclosureMetadata.operatorSees,
905 authority: disclosureMetadata.authority,
906 retention: disclosureMetadata.retention
907 }
908 })
909 const details = {
910 wireOperation: null,
911 privacyResolution: deepFreeze({
912 ...axisResolution.axes,
913 transport: axisRecord(intent.transport, profile, 'connector-qualification-required',
914 transportRequiredEvidence(profile, runtime),
915 assumptions(profile), transportClaim(profile))
916 }),
917 intendedTransportProfile: profile,
918 semanticBoundary: 'explicitly-non-blind',
919 execution: 'external-service',
920 durability: null,
921 costBudget: null,
922 claims,
923 assumptions: ['the named service can interpret the disclosed semantic input', ...assumptions(profile)],
924 warnings: ['this operation is outside the blind substrate trust boundary'],
925 externalDisclosure: disclosureMetadata,
926 consentRequired: true,
927 consentStatus: 'qualification-required'
928 }
929 if (inventory.external !== true) {
930 return queueable(workload, runtime, primitive, intent, 'EXTERNAL_SERVICE_UNAVAILABLE',
931 'the declared semantic service is not currently present in the deployment inventory', details)
932 }
933 if (!externalTransportAvailable(profile, runtime, inventory)) {
934 return queueable(workload, runtime, primitive, intent, 'QUEUEABLE_PRIVACY_PATH_UNAVAILABLE',
935 'the declared semantic service lacks the requested network-privacy path in the deployment inventory', details)
936 }
937 return deepFreeze({
938 status: PLAN_STATUS.DRAFT,
939 code: 'EXTERNAL_CONNECTOR_QUALIFICATION_REQUIRED',
940 executable: false,
941 workload: workload.kind,
942 primitive,
943 runtime,
944 operation: workload.operation,
945 requestedPrivacy: intent,
946 ...details,
947 downgraded: false,
948 downgradeAuthorized: false,
949 readiness: 'the named connector must revalidate endpoint evidence, consent scope and network transport before execution'
950 })
951}
952
953function externalTransportAvailable (profile, runtime, inventory) {
954 if (profile === TRANSPORT_PROFILE.EXTERNAL_DIRECT) return inventory.direct === true
955 if (profile === TRANSPORT_PROFILE.EXTERNAL_SOURCE_SEPARATED) {
956 if (runtime === RUNTIME.BROWSER) return inventory.ohttp === true
957 if (runtime === RUNTIME.NATIVE) return inventory.splitNative === true
958 return inventory.torBrowser === true
959 }
960 if (profile === TRANSPORT_PROFILE.EXTERNAL_TOR) {
961 if (runtime === RUNTIME.NATIVE) return inventory.torNative === true
962 if (runtime === RUNTIME.TOR_BROWSER) return inventory.torBrowser === true
963 }
964 return false
965}
966
967function validateExternalDisclosure (value) {
968 const disclosure = reason => ({ code: 'SEMANTIC_SERVICE_DISCLOSURE_REQUIRED', reason })
969 if (!isPlainObject(value)) return disclosure('semantic work must provide a complete external disclosure')
970 assertOnlyFields(Object.keys(value), [
971 'serviceId',
972 'purpose',
973 'operatorSees',
974 'authority',
975 'retention',
976 'endpointEvidence',
977 'networkTransport'
978 ], 'external disclosure')
979 if (typeof value.serviceId !== 'string' || value.serviceId.length === 0) return disclosure('external disclosure requires serviceId')
980 if (typeof value.purpose !== 'string' || value.purpose.length === 0) return disclosure('external disclosure requires purpose')
981 if (!Array.isArray(value.operatorSees) || value.operatorSees.length === 0 ||
982 value.operatorSees.some(item => typeof item !== 'string' || item.length === 0)) return disclosure('external disclosure requires operatorSees')
983 if (value.authority !== 'advisory' && value.authority !== 'authoritative') return disclosure('external disclosure authority is invalid')
984 if (typeof value.retention !== 'string' || value.retention.length === 0) return disclosure('external disclosure requires retention')
985 if (!validEndpointEvidence(value.endpointEvidence)) {
986 return disclosure('external disclosure endpointEvidence must be a content-addressed sha256 or blake2b256 digest')
987 }
988 if (!Object.values(TRANSPORT_REQUIREMENT).includes(value.networkTransport)) {
989 return disclosure('external disclosure requires a closed networkTransport')
990 }
991 return null
992}
993
994function validEndpointEvidence (value) {
995 return typeof value === 'string' && /^(sha256|blake2b256):[0-9a-f]{64}$/i.test(value)
996}
997
998function externalDisclosureMetadata (value) {
999 return deepFreeze({
1000 serviceId: value.serviceId,
1001 purpose: value.purpose,
1002 operatorSees: [...new Set(value.operatorSees)].sort(),
1003 authority: value.authority,
1004 retention: value.retention,
1005 networkTransport: value.networkTransport,
1006 endpointEvidenceMetadata: endpointEvidenceMetadata(value.endpointEvidence)
1007 })
1008}
1009
1010function endpointEvidenceMetadata (value) {
1011 const separator = value.indexOf(':')
1012 return { kind: value.slice(0, separator).toLowerCase(), digest: value.toLowerCase() }
1013}
1014
1015function externalTransportProfile (requirement) {
1016 if (requirement === TRANSPORT_REQUIREMENT.DIRECT) return TRANSPORT_PROFILE.EXTERNAL_DIRECT
1017 if (requirement === TRANSPORT_REQUIREMENT.SOURCE_SEPARATED) return TRANSPORT_PROFILE.EXTERNAL_SOURCE_SEPARATED
1018 return TRANSPORT_PROFILE.EXTERNAL_TOR
1019}
1020
1021function planCell (workload, durability) {
1022 const destinationCountBasis = workload.operation === 'get' ? 'fetchTarget' : 'replicaTarget'
1023 if (workload.byteLength == null) {
1024 return Object.freeze({
1025 requiresChunkManifest: false,
1026 estimateStatus: 'size-basis-required',
1027 sizeClass: null,
1028 maximumCellPayloadBytes: maximumCellContentBytes(Math.max(...Object.keys(CELL_SIZE_CLASS).map(Number))),
1029 batchRecommended: null,
1030 destinationCountBasis,
1031 blindEnvelope: null
1032 })
1033 }
1034 const usefulBytes = Math.max(1, workload.byteLength)
1035 let sizeClass = null
1036 for (const id of Object.keys(CELL_SIZE_CLASS).map(Number).sort((a, b) => a - b)) {
1037 if (usefulBytes <= maximumCellContentBytes(id)) {
1038 sizeClass = id
1039 break
1040 }
1041 }
1042 if (sizeClass == null) {
1043 const maximumPayloadBytes = maximumCellContentBytes(Math.max(...Object.keys(CELL_SIZE_CLASS).map(Number)))
1044 return Object.freeze({
1045 requiresChunkManifest: true,
1046 maximumCellPayloadBytes: maximumPayloadBytes,
1047 minimumChunkCount: Math.ceil(usefulBytes / maximumPayloadBytes)
1048 })
1049 }
1050 const blindEnvelope = estimateCellWireV1({
1051 operation: workload.operation,
1052 sizeClass,
1053 usefulBytes,
1054 replicas: workload.operation === 'get' ? durability.fetchTarget : durability.replicaTarget
1055 })
1056 return Object.freeze({
1057 requiresChunkManifest: false,
1058 sizeClass,
1059 cellBytes: CELL_SIZE_CLASS[sizeClass],
1060 maximumCellPayloadBytes: maximumCellContentBytes(sizeClass),
1061 batchRecommended: sizeClass === 1 && usefulBytes < 2048,
1062 estimateStatus: 'exact-minimum-fitting-v1-envelope',
1063 destinationCountBasis,
1064 blindEnvelope
1065 })
1066}
1067
1068function planInbox (workload) {
1069 const frameClasses = Object.entries(INBOX_FRAME_CLASS)
1070 .map(([id, bytes]) => ({ id: Number(id), bytes }))
1071 .sort((left, right) => left.bytes - right.bytes)
1072 const maximumFrameBytes = frameClasses[frameClasses.length - 1].bytes
1073 if (workload.byteLength == null) {
1074 return Object.freeze({
1075 exceedsMaximumFrame: false,
1076 estimateStatus: 'size-basis-required',
1077 frameClass: null,
1078 frameBytes: null,
1079 maximumFrameBytes
1080 })
1081 }
1082 const selected = frameClasses.find(candidate => workload.byteLength <= candidate.bytes)
1083 if (selected == null) {
1084 return Object.freeze({
1085 exceedsMaximumFrame: true,
1086 estimateStatus: 'over-maximum-universal-frame',
1087 usefulBytes: workload.byteLength,
1088 maximumFrameBytes
1089 })
1090 }
1091 return Object.freeze({
1092 exceedsMaximumFrame: false,
1093 estimateStatus: 'minimum-fitting-universal-frame',
1094 frameClass: selected.id,
1095 frameBytes: selected.bytes,
1096 usefulBytes: workload.byteLength,
1097 paddingAllowanceBytes: selected.bytes - workload.byteLength,
1098 maximumFrameBytes,
1099 accountingBoundary: 'opaque Inbox frame only; application framing and encryption must fit inside the selected class'
1100 })
1101}
1102
1103function normalizeCostBudget (value) {
1104 if (value == null) return null
1105 if (!isPlainObject(value)) fail('BAD_CLIENT_INPUT', 'costBudget must be an object')
1106 const fields = Object.keys(value)
1107 if (value.maxWireAmplification != null) {
1108 fail('BAD_CLIENT_INPUT', 'maxWireAmplification is ambiguous; use maxBlindEnvelopeAmplification or an adapter fleet budget')
1109 }
1110 assertOnlyFields(fields, ['maxBlindEnvelopeAmplification'], 'costBudget')
1111 if (!Number.isFinite(value.maxBlindEnvelopeAmplification) || value.maxBlindEnvelopeAmplification <= 0) {
1112 fail('BAD_CLIENT_INPUT', 'maxBlindEnvelopeAmplification must be a positive finite number')
1113 }
1114 return Object.freeze({ maxBlindEnvelopeAmplification: value.maxBlindEnvelopeAmplification })
1115}
1116
1117function smallestOuterClassBytes (requiredBytes) {
1118 for (const bytes of OUTER_CLASS_BYTES) {
1119 if (requiredBytes <= bytes) return bytes
1120 }
1121 fail('BAD_CLIENT_INPUT', 'operation exceeds every public v1 outer class')
1122}
1123
1124function blocked (workload, runtime, primitive, intent, code, reason, extra = {}) {
1125 return deepFreeze({
1126 status: PLAN_STATUS.BLOCKED,
1127 code,
1128 reason,
1129 executable: false,
1130 workload: workload.kind,
1131 primitive,
1132 runtime,
1133 operation: workload.operation,
1134 requestedPrivacy: intent,
1135 downgraded: false,
1136 downgradeAuthorized: false,
1137 consentRequired: extra.consentRequired === true,
1138 ...extra
1139 })
1140}
1141
1142function queueable (workload, runtime, primitive, intent, code, reason, extra = {}) {
1143 const journalableMutation = ['put', 'create', 'append', 'renew', 'close'].includes(workload.operation)
1144 return deepFreeze({
1145 status: PLAN_STATUS.QUEUEABLE,
1146 code,
1147 reason,
1148 executable: false,
1149 workload: workload.kind,
1150 primitive,
1151 runtime,
1152 operation: workload.operation,
1153 requestedPrivacy: intent,
1154 prospectiveQueue: {
1155 state: 'not-journaled',
1156 action: journalableMutation ? 'journal-mutation-intent' : 'wait-for-path',
1157 intentCanBeJournaled: journalableMutation,
1158 localVisible: false
1159 },
1160 downgraded: false,
1161 downgradeAuthorized: false,
1162 consentRequired: false,
1163 ...extra
1164 })
1165}
1166
1167function axisRecord (requested, planned, state, requiredEvidence, axisAssumptions, claimCeiling) {
1168 return deepFreeze({
1169 requested,
1170 planned,
1171 state,
1172 requiredEvidence: [...requiredEvidence],
1173 assumptions: [...axisAssumptions],
1174 claimCeiling
1175 })
1176}
1177
1178function transportRequiredEvidence (profile, runtime) {
1179 if (profile === TRANSPORT_PROFILE.P2P_DIRECT) {
1180 return [
1181 EVIDENCE_REQUIREMENT.P2P_SESSION_PREPARATION,
1182 EVIDENCE_REQUIREMENT.ADAPTER_CONFORMANCE
1183 ]
1184 }
1185 if (profile === TRANSPORT_PROFILE.EXTERNAL_DIRECT ||
1186 profile === TRANSPORT_PROFILE.EXTERNAL_SOURCE_SEPARATED ||
1187 profile === TRANSPORT_PROFILE.EXTERNAL_TOR) {
1188 const required = [
1189 EVIDENCE_REQUIREMENT.EXTERNAL_ENDPOINT_EVIDENCE,
1190 EVIDENCE_REQUIREMENT.EXTERNAL_CONNECTOR_CONFORMANCE,
1191 EVIDENCE_REQUIREMENT.DISCLOSURE_AUTHORIZATION
1192 ]
1193 if (profile === TRANSPORT_PROFILE.EXTERNAL_SOURCE_SEPARATED) {
1194 required.push(EVIDENCE_REQUIREMENT.ROLE_SEPARATION_CAPTURE)
1195 required.push(EVIDENCE_REQUIREMENT.NO_DIRECT_RACE_CAPTURE)
1196 }
1197 if (profile === TRANSPORT_PROFILE.EXTERNAL_TOR) {
1198 required.push(EVIDENCE_REQUIREMENT.NO_CLEARNET_CAPTURE)
1199 }
1200 if ((profile === TRANSPORT_PROFILE.EXTERNAL_SOURCE_SEPARATED ||
1201 profile === TRANSPORT_PROFILE.EXTERNAL_TOR) &&
1202 (runtime === RUNTIME.BROWSER || runtime === RUNTIME.TOR_BROWSER)) {
1203 required.push(EVIDENCE_REQUIREMENT.BROWSER_OPAQUE_ORIGIN_CAPTURE)
1204 }
1205 return required
1206 }
1207 const required = [
1208 EVIDENCE_REQUIREMENT.SIGNED_TRANSPORT_PROFILE,
1209 EVIDENCE_REQUIREMENT.VERIFIED_ENDPOINT,
1210 EVIDENCE_REQUIREMENT.FRESH_SIGNED_HEALTH,
1211 EVIDENCE_REQUIREMENT.ADAPTER_CONFORMANCE
1212 ]
1213 if (profile === TRANSPORT_PROFILE.OHTTP || profile === TRANSPORT_PROFILE.SPLIT_NATIVE) {
1214 required.push(EVIDENCE_REQUIREMENT.ROLE_SEPARATION_CAPTURE)
1215 required.push(EVIDENCE_REQUIREMENT.NO_DIRECT_RACE_CAPTURE)
1216 }
1217 if (profile === TRANSPORT_PROFILE.OHTTP) {
1218 required.push(EVIDENCE_REQUIREMENT.BROWSER_OPAQUE_ORIGIN_CAPTURE)
1219 }
1220 if (profile === TRANSPORT_PROFILE.TOR_NATIVE || profile === TRANSPORT_PROFILE.TOR_BROWSER) {
1221 required.push(EVIDENCE_REQUIREMENT.NO_CLEARNET_CAPTURE)
1222 }
1223 if (profile === TRANSPORT_PROFILE.TOR_BROWSER) {
1224 required.push(EVIDENCE_REQUIREMENT.BROWSER_OPAQUE_ORIGIN_CAPTURE)
1225 }
1226 return required
1227}
1228
1229function transportClaim (profile) {
1230 if (profile === TRANSPORT_PROFILE.DIRECT) return 'source and access path visible to adjacent storage'
1231 if (profile === TRANSPORT_PROFILE.OHTTP || profile === TRANSPORT_PROFILE.SPLIT_NATIVE) {
1232 return 'source-separated only under adjacent-role non-collusion; timing and volume may correlate'
1233 }
1234 if (profile === TRANSPORT_PROFILE.TOR_NATIVE || profile === TRANSPORT_PROFILE.TOR_BROWSER) {
1235 return 'source/location separation under the Tor threat model; no global-observer claim'
1236 }
1237 if (profile === TRANSPORT_PROFILE.P2P_DIRECT) return 'direct peer path metadata is exposed to the network path'
1238 if (profile === TRANSPORT_PROFILE.EXTERNAL_DIRECT) return 'direct external connector; source and semantic request metadata are visible to the service path'
1239 if (profile === TRANSPORT_PROFILE.EXTERNAL_SOURCE_SEPARATED) {
1240 return 'external source-separated connector only under adjacent-role non-collusion; the service still sees disclosed semantics'
1241 }
1242 if (profile === TRANSPORT_PROFILE.EXTERNAL_TOR) {
1243 return 'external Tor connector under the Tor threat model; the service still sees disclosed semantics'
1244 }
1245 if (profile != null) return 'declared connector path; qualification remains service-specific'
1246 return 'no network transport selected'
1247}
1248
1249function claimsFor (primitive, profile, runtime) {
1250 const representation = {
1251 [PRIMITIVE.CELL]: 'opaque immutable Cell; unlinkable replicas require fresh slots, keys, nonces and ciphertext',
1252 [PRIMITIVE.INBOX]: 'opaque bounded rendezvous; not authority, completeness or durable history',
1253 [PRIMITIVE.CORE]: 'encrypted append stream with stable transport-key linkability',
1254 [PRIMITIVE.P2P_DIRECT]: 'peer-authenticated live session; no storage durability by itself',
1255 [PRIMITIVE.LOCAL_COMPUTE]: 'local application trust boundary',
1256 [PRIMITIVE.EXTERNAL]: 'explicitly non-blind semantic service'
1257 }[primitive]
1258 const durability = {
1259 [PRIMITIVE.CELL]: 'no remote durability until replica receipts and exact readbacks are verified',
1260 [PRIMITIVE.INBOX]: 'bounded stripe/replica evidence only; never durable-history completeness',
1261 [PRIMITIVE.CORE]: 'no mirror durability until signed heads and challenged readback evidence are verified',
1262 [PRIMITIVE.P2P_DIRECT]: 'no offline or storage durability',
1263 [PRIMITIVE.LOCAL_COMPUTE]: 'application-defined local persistence only',
1264 [PRIMITIVE.EXTERNAL]: 'provider-specific retention; never Blind Cell/Inbox/Core durability'
1265 }[primitive]
1266 const readInterest = {
1267 [PRIMITIVE.CELL]: { status: 'known-visible', ceiling: 'requested Cell locators and access timing remain visible to storage' },
1268 [PRIMITIVE.INBOX]: { status: 'known-visible', ceiling: 'requested Inbox topics and access timing remain visible to storage' },
1269 [PRIMITIVE.CORE]: { status: 'known-visible', ceiling: 'requested Core keys and replication timing remain visible to blind peers' },
1270 [PRIMITIVE.P2P_DIRECT]: { status: 'not-applicable', ceiling: 'the storage-locator read-interest axis does not apply to a live peer session' },
1271 [PRIMITIVE.LOCAL_COMPUTE]: { status: 'not-applicable', ceiling: 'local compute has no network read-interest observer' },
1272 [PRIMITIVE.EXTERNAL]: { status: 'provider-visible', ceiling: 'the semantic provider can interpret request interest and result selection' }
1273 }[primitive]
1274 const browserOriginRequired = (runtime === RUNTIME.BROWSER || runtime === RUNTIME.TOR_BROWSER) &&
1275 (profile === TRANSPORT_PROFILE.OHTTP ||
1276 profile === TRANSPORT_PROFILE.TOR_BROWSER ||
1277 profile === TRANSPORT_PROFILE.EXTERNAL_SOURCE_SEPARATED ||
1278 profile === TRANSPORT_PROFILE.EXTERNAL_TOR)
1279 return deepFreeze({
1280 representation: { status: 'design-ceiling', ceiling: representation },
1281 transport: { status: profile == null ? 'not-applicable' : 'qualification-required', ceiling: transportClaim(profile) },
1282 readInterest,
1283 durability: { status: 'unverified-until-evidence', ceiling: durability },
1284 browserOriginGate: {
1285 status: browserOriginRequired ? 'qualification-required' : 'not-applicable',
1286 ceiling: browserOriginRequired
1287 ? 'browser source separation does not prove opaque application origin or Fetch Metadata until the capture gate passes'
1288 : null
1289 }
1290 })
1291}
1292
1293function assumptions (profile) {
1294 if (profile === TRANSPORT_PROFILE.OHTTP || profile === TRANSPORT_PROFILE.SPLIT_NATIVE ||
1295 profile === TRANSPORT_PROFILE.EXTERNAL_SOURCE_SEPARATED) {
1296 return ['entry and storage roles do not collude', 'traffic timing and volume may correlate']
1297 }
1298 if (profile === TRANSPORT_PROFILE.TOR_NATIVE || profile === TRANSPORT_PROFILE.TOR_BROWSER ||
1299 profile === TRANSPORT_PROFILE.EXTERNAL_TOR) {
1300 return ['Tor threat model', 'no clearnet fallback', 'timing and volume remain observable']
1301 }
1302 return ['deployment inventory is planning input, not qualification evidence']
1303}
1304
1305function warningsFor (intent, profile) {
1306 const warnings = []
1307 if (intent.readInterest === 'observable') warnings.push('requested locators and access timing remain visible to storage')
1308 if (intent.metadataShaping === 'padded') warnings.push('padding hides exact lengths only within universal classes')
1309 if (profile === TRANSPORT_PROFILE.TOR_BROWSER) warnings.push('browser Origin/Fetch Metadata claims require the separate opaque-origin gate')
1310 return warnings
1311}
1312
1313function deepFreeze (value) {
1314 if (!value || typeof value !== 'object' || Object.isFrozen(value) || ArrayBuffer.isView(value)) return value
1315 for (const child of Object.values(value)) deepFreeze(child)
1316 return Object.freeze(value)
1317}
1318
1319function isPlainObject (value) {
1320 if (value == null || typeof value !== 'object' || Array.isArray(value) || ArrayBuffer.isView(value)) return false
1321 const prototype = Object.getPrototypeOf(value)
1322 return prototype === Object.prototype || prototype === null
1323}
1324
1325function hasOwn (object, key) {
1326 return Object.prototype.hasOwnProperty.call(object, key)
1327}
packages/blind-client/test/policy.test.js +789 -0
……@@ -0,0 +1,789 @@
1import test from 'brittle'
2import { maximumCellContentBytes } from '../cells.js'
3import {
4 DOWNGRADE_POLICY,
5 EVIDENCE_REQUIREMENT,
6 PLAN_STATUS,
7 PRIMITIVE,
8 RUNTIME,
9 TRANSPORT_PROFILE,
10 WORKLOAD,
11 createPrivacyIntent,
12 draftWorkloadPlan,
13 estimateCellWireV1,
14 privacy
15} from '../policy.js'
16
17const completeDisclosure = Object.freeze({
18 serviceId: 'search-provider',
19 purpose: 'full-text search',
20 operatorSees: ['timing', 'query', 'query'],
21 authority: 'advisory',
22 retention: 'provider policy',
23 endpointEvidence: `sha256:${'0'.repeat(64)}`,
24 networkTransport: 'source-separated'
25})
26
27test('pure policy planning routes workloads but never manufactures executable evidence', t => {
28 const inventory = { direct: true, ohttp: true, splitNative: true, torNative: true, core: true, p2p: true, external: true }
29 const records = draftWorkloadPlan({ workload: WORKLOAD.RECORDS, runtime: RUNTIME.BROWSER, privacy: privacy.fast(), inventory })
30 const inbox = draftWorkloadPlan({ workload: WORKLOAD.RENDEZVOUS, runtime: RUNTIME.BROWSER, privacy: privacy.private(), inventory })
31 const history = draftWorkloadPlan({ workload: WORKLOAD.HISTORY, runtime: RUNTIME.NATIVE, privacy: privacy.private(), inventory })
32 const live = draftWorkloadPlan({ workload: WORKLOAD.LIVE, runtime: RUNTIME.NATIVE, privacy: privacy.fast(), inventory })
33 const semantic = draftWorkloadPlan({
34 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
35 runtime: RUNTIME.BROWSER,
36 privacy: privacy.private(),
37 inventory,
38 externalDisclosure: completeDisclosure
39 })
40
41 for (const plan of [records, inbox, history, live, semantic]) {
42 t.is(plan.status, PLAN_STATUS.DRAFT)
43 t.is(plan.executable, false)
44 for (const axis of Object.values(plan.privacyResolution)) {
45 if (axis.state.endsWith('-required')) t.ok(axis.requiredEvidence.length > 0)
46 }
47 }
48 t.is(records.primitive, PRIMITIVE.CELL)
49 t.is(records.intendedTransportProfile, TRANSPORT_PROFILE.DIRECT)
50 t.is(inbox.primitive, PRIMITIVE.INBOX)
51 t.is(inbox.intendedTransportProfile, TRANSPORT_PROFILE.OHTTP)
52 t.ok(inbox.privacyResolution.transport.requiredEvidence.includes(
53 EVIDENCE_REQUIREMENT.BROWSER_OPAQUE_ORIGIN_CAPTURE
54 ))
55 t.is(inbox.claims.browserOriginGate.status, 'qualification-required')
56 t.is(history.primitive, PRIMITIVE.CORE)
57 t.is(history.intendedTransportProfile, TRANSPORT_PROFILE.SPLIT_NATIVE)
58 t.is(history.wireOperation.operation, 'MIRROR')
59 t.is(history.wireOperation.operationId, 1)
60 t.is(history.claims.representation.ceiling, 'encrypted append stream with stable transport-key linkability')
61 t.is(live.primitive, PRIMITIVE.P2P_DIRECT)
62 t.is(semantic.primitive, PRIMITIVE.EXTERNAL)
63 t.is(semantic.semanticBoundary, 'explicitly-non-blind')
64 t.is(semantic.claims.semanticDisclosure.claimCeiling, 'explicitly non-blind semantic service')
65 t.alike(semantic.claims.semanticDisclosure.operatorSees, ['query', 'timing'])
66 t.ok(semantic.privacyResolution.transport.requiredEvidence.includes(
67 EVIDENCE_REQUIREMENT.ROLE_SEPARATION_CAPTURE
68 ))
69 t.ok(semantic.privacyResolution.transport.requiredEvidence.includes(
70 EVIDENCE_REQUIREMENT.NO_DIRECT_RACE_CAPTURE
71 ))
72 t.ok(semantic.privacyResolution.transport.requiredEvidence.includes(
73 EVIDENCE_REQUIREMENT.BROWSER_OPAQUE_ORIGIN_CAPTURE
74 ))
75 t.is(semantic.claims.browserOriginGate.status, 'qualification-required')
76
77 const torDisclosure = {
78 ...completeDisclosure,
79 networkTransport: 'tor'
80 }
81 const highPrivacyExternal = draftWorkloadPlan({
82 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
83 runtime: RUNTIME.NATIVE,
84 privacy: privacy.highPrivacy(),
85 inventory,
86 externalDisclosure: torDisclosure
87 })
88 t.is(highPrivacyExternal.status, PLAN_STATUS.DRAFT)
89 t.ok(highPrivacyExternal.privacyResolution.transport.requiredEvidence.includes(
90 EVIDENCE_REQUIREMENT.NO_CLEARNET_CAPTURE
91 ))
92 t.is(highPrivacyExternal.claims.browserOriginGate.status, 'not-applicable')
93})
94
95test('ordinary browser history changes representation to Cells without changing privacy', t => {
96 const plan = draftWorkloadPlan({
97 workload: { kind: WORKLOAD.HISTORY, byteLength: 2048 },
98 runtime: RUNTIME.BROWSER,
99 privacy: privacy.private(),
100 inventory: { ohttp: true, core: false }
101 })
102 t.is(plan.status, PLAN_STATUS.DRAFT)
103 t.is(plan.primitive, PRIMITIVE.CELL)
104 t.is(plan.intendedTransportProfile, TRANSPORT_PROFILE.OHTTP)
105 t.is(plan.downgraded, false)
106})
107
108test('private downgrade remains consent-pending in the pure draft planner', t => {
109 const options = {
110 workload: WORKLOAD.RECORDS,
111 runtime: RUNTIME.BROWSER,
112 privacy: privacy.private(),
113 inventory: { direct: true, ohttp: false }
114 }
115 const pending = draftWorkloadPlan(options)
116 t.is(pending.status, PLAN_STATUS.QUEUEABLE)
117 t.is(pending.code, 'DOWNGRADE_CONSENT_REQUIRED')
118 t.is(pending.consentRequired, true)
119 t.is(pending.suggestedTransportProfile, TRANSPORT_PROFILE.DIRECT)
120 t.is(pending.prospectiveQueue.state, 'not-journaled')
121 t.is(pending.prospectiveQueue.action, 'journal-mutation-intent')
122 t.exception(() => draftWorkloadPlan({
123 ...options,
124 downgradeAuthorization: { consentId: 'caller-asserted' }
125 }), /cannot consume downgrade authorization/)
126 t.exception(() => draftWorkloadPlan({
127 ...options,
128 disclosureAuthorization: { consentId: 'caller-asserted' }
129 }), /cannot consume disclosure authorization/)
130})
131
132test('queueable results are prospective and operation-aware', t => {
133 const mutation = draftWorkloadPlan({
134 workload: { kind: WORKLOAD.RECORDS, operation: 'put' },
135 runtime: RUNTIME.NATIVE,
136 privacy: privacy.highPrivacy(),
137 inventory: { direct: true, torNative: false }
138 })
139 t.is(mutation.status, PLAN_STATUS.QUEUEABLE)
140 t.is(mutation.code, 'QUEUEABLE_PRIVACY_PATH_UNAVAILABLE')
141 t.absent(mutation.localState)
142 t.is(mutation.prospectiveQueue.state, 'not-journaled')
143 t.is(mutation.prospectiveQueue.action, 'journal-mutation-intent')
144 t.is(mutation.prospectiveQueue.intentCanBeJournaled, true)
145 t.is(mutation.prospectiveQueue.localVisible, false)
146 t.is(mutation.intendedTransportProfile, TRANSPORT_PROFILE.TOR_NATIVE)
147 t.is(mutation.privacyResolution.transport.state, 'qualification-required')
148 t.is(mutation.cell.estimateStatus, 'size-basis-required')
149 t.ok(mutation.claims.transport)
150
151 const read = draftWorkloadPlan({
152 workload: { kind: WORKLOAD.RECORDS, operation: 'get' },
153 runtime: RUNTIME.NATIVE,
154 privacy: privacy.highPrivacy(),
155 inventory: { torNative: false }
156 })
157 t.is(read.status, PLAN_STATUS.QUEUEABLE)
158 t.is(read.prospectiveQueue.action, 'wait-for-path')
159 t.is(read.prospectiveQueue.intentCanBeJournaled, false)
160 t.is(read.operation, 'get')
161})
162
163test('fixed presets are immutable and axes are prospective per primitive', t => {
164 t.exception(() => privacy.highPrivacy({ transport: 'direct' }), /immutable preset/)
165 t.exception(() => draftWorkloadPlan({
166 workload: WORKLOAD.RECORDS,
167 runtime: RUNTIME.NATIVE,
168 privacy: {
169 ...privacy.highPrivacy(),
170 transport: 'direct'
171 },
172 inventory: { direct: true }
173 }), /immutable preset/)
174
175 const unsupported = draftWorkloadPlan({
176 workload: WORKLOAD.RENDEZVOUS,
177 runtime: RUNTIME.NATIVE,
178 privacy: privacy.custom({
179 transport: 'source-separated',
180 metadataShaping: 'bucketed-experimental',
181 identityExposure: 'anonymous-reply',
182 readInterest: 'bucketed-experimental',
183 downgradePolicy: DOWNGRADE_POLICY.DENY,
184 backgroundPreferred: true
185 }),
186 inventory: { splitNative: true }
187 })
188 t.is(unsupported.status, PLAN_STATUS.BLOCKED)
189 t.is(unsupported.code, 'PRIVACY_AXIS_UNSUPPORTED')
190 t.is(unsupported.privacyResolution.metadataShaping.state, 'unsupported')
191 t.is(unsupported.privacyResolution.identityExposure.state, 'application-policy-required')
192 t.is(unsupported.privacyResolution.readInterest.state, 'unsupported')
193
194 const supported = draftWorkloadPlan({
195 workload: WORKLOAD.RENDEZVOUS,
196 runtime: RUNTIME.NATIVE,
197 privacy: privacy.custom({
198 transport: 'source-separated',
199 metadataShaping: 'padded',
200 identityExposure: 'session',
201 readInterest: 'observable',
202 downgradePolicy: DOWNGRADE_POLICY.DENY,
203 backgroundPreferred: true
204 }),
205 inventory: { splitNative: true }
206 })
207 t.is(supported.status, PLAN_STATUS.DRAFT)
208 t.is(supported.intendedTransportProfile, TRANSPORT_PROFILE.SPLIT_NATIVE)
209 t.is(supported.privacyResolution.metadataShaping.state, 'qualification-required')
210 t.is(supported.privacyResolution.identityExposure.state, 'application-policy-required')
211 t.is(supported.privacyResolution.readInterest.state, 'known-visible')
212
213 const local = draftWorkloadPlan({
214 workload: { kind: WORKLOAD.SEMANTIC, execution: 'local' },
215 runtime: RUNTIME.BROWSER,
216 privacy: privacy.custom({
217 transport: 'tor',
218 metadataShaping: 'bucketed-experimental',
219 identityExposure: 'anonymous-reply',
220 readInterest: 'bucketed-experimental',
221 downgradePolicy: DOWNGRADE_POLICY.DENY,
222 backgroundPreferred: false
223 })
224 })
225 t.is(local.status, PLAN_STATUS.DRAFT)
226 for (const axis of Object.values(local.privacyResolution)) {
227 t.is(axis.state, 'not-applicable')
228 t.is(Object.prototype.hasOwnProperty.call(axis, 'actual'), false)
229 }
230
231 const live = draftWorkloadPlan({
232 workload: WORKLOAD.LIVE,
233 runtime: RUNTIME.NATIVE,
234 privacy: privacy.fast(),
235 inventory: { p2p: true }
236 })
237 t.is(live.privacyResolution.metadataShaping.state, 'adapter-qualification-required')
238 t.is(live.privacyResolution.identityExposure.state, 'adapter-qualification-required')
239 t.is(live.privacyResolution.readInterest.state, 'not-applicable')
240 t.is(Object.prototype.hasOwnProperty.call(live.privacyResolution.metadataShaping, 'actual'), false)
241
242 const core = draftWorkloadPlan({
243 workload: WORKLOAD.HISTORY,
244 runtime: RUNTIME.NATIVE,
245 privacy: privacy.private(),
246 inventory: { core: true, splitNative: true }
247 })
248 t.is(core.privacyResolution.metadataShaping.planned, 'stream-shaping-adapter-defined')
249 t.is(core.privacyResolution.metadataShaping.state, 'qualification-required')
250 t.is(Object.prototype.hasOwnProperty.call(core.privacyResolution.transport, 'actual'), false)
251
252 const based = privacy.custom({ base: 'private', backgroundPreferred: true })
253 t.is(based.preset, 'custom')
254 t.is(based.base, 'private')
255 t.is(based.transport, 'source-separated')
256 t.is(based.backgroundPreferred, true)
257})
258
259test('semantic execution defaults local and external disclosure remains consent-pending', t => {
260 const local = draftWorkloadPlan({
261 workload: { kind: WORKLOAD.SEMANTIC, execution: 'local' },
262 runtime: RUNTIME.BROWSER,
263 privacy: privacy.private()
264 })
265 t.is(local.status, PLAN_STATUS.DRAFT)
266 t.is(local.primitive, PRIMITIVE.LOCAL_COMPUTE)
267 t.is(local.intendedTransportProfile, null)
268 t.is(local.claims.readInterest.status, 'not-applicable')
269
270 const localByDefault = draftWorkloadPlan({
271 workload: WORKLOAD.SEMANTIC,
272 runtime: RUNTIME.BROWSER,
273 privacy: privacy.private()
274 })
275 t.is(localByDefault.status, PLAN_STATUS.DRAFT)
276 t.is(localByDefault.primitive, PRIMITIVE.LOCAL_COMPUTE)
277
278 const missing = draftWorkloadPlan({
279 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
280 runtime: RUNTIME.BROWSER,
281 privacy: privacy.private(),
282 inventory: { external: true, ohttp: true },
283 externalDisclosure: { serviceId: 'search-provider', operatorSees: ['query'] }
284 })
285 t.is(missing.status, PLAN_STATUS.BLOCKED)
286 t.is(missing.code, 'SEMANTIC_SERVICE_DISCLOSURE_REQUIRED')
287
288 const mismatch = draftWorkloadPlan({
289 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
290 runtime: RUNTIME.NATIVE,
291 privacy: privacy.highPrivacy(),
292 inventory: { external: true, ohttp: true },
293 externalDisclosure: completeDisclosure
294 })
295 t.is(mismatch.status, PLAN_STATUS.BLOCKED)
296 t.is(mismatch.code, 'PRIVACY_AXIS_UNSUPPORTED')
297
298 const external = draftWorkloadPlan({
299 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
300 runtime: RUNTIME.BROWSER,
301 privacy: privacy.private(),
302 inventory: { external: true, ohttp: true },
303 externalDisclosure: completeDisclosure
304 })
305 t.is(external.status, PLAN_STATUS.DRAFT)
306 t.is(external.code, 'EXTERNAL_CONNECTOR_QUALIFICATION_REQUIRED')
307 t.is(external.intendedTransportProfile, TRANSPORT_PROFILE.EXTERNAL_SOURCE_SEPARATED)
308 t.absent(external.externalDisclosure.endpointEvidence)
309 t.is(external.externalDisclosure.endpointEvidenceMetadata.kind, 'sha256')
310 t.is(external.externalDisclosure.endpointEvidenceMetadata.digest, completeDisclosure.endpointEvidence)
311 t.alike(external.externalDisclosure.operatorSees, ['query', 'timing'])
312 t.absent(external.externalDisclosure.consentDeclaration)
313 t.is(external.consentRequired, true)
314 t.is(external.consentStatus, 'qualification-required')
315 t.is(external.claims.semanticDisclosure.authority, completeDisclosure.authority)
316 t.is(external.claims.semanticDisclosure.retention, completeDisclosure.retention)
317 t.exception.all(() => {
318 external.externalDisclosure.endpointEvidenceMetadata.digest = 'sha256:changed'
319 }, /read only|Cannot assign/)
320
321 const invalidEvidence = draftWorkloadPlan({
322 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
323 runtime: RUNTIME.BROWSER,
324 privacy: privacy.private(),
325 inventory: { external: true },
326 externalDisclosure: { ...completeDisclosure, endpointEvidence: new Map() }
327 })
328 t.is(invalidEvidence.status, PLAN_STATUS.BLOCKED)
329 t.is(invalidEvidence.code, 'SEMANTIC_SERVICE_DISCLOSURE_REQUIRED')
330
331 t.exception(() => draftWorkloadPlan({
332 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
333 runtime: RUNTIME.BROWSER,
334 privacy: privacy.private(),
335 inventory: { external: true },
336 externalDisclosure: { ...completeDisclosure, userConsent: { consentId: 'caller-shaped', grantedAt: 1 } }
337 }), /unknown fields/)
338
339 const missingOffline = draftWorkloadPlan({
340 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
341 runtime: RUNTIME.BROWSER,
342 privacy: privacy.private(),
343 inventory: { external: false },
344 externalDisclosure: { serviceId: 'search-provider' }
345 })
346 t.is(missingOffline.code, 'SEMANTIC_SERVICE_DISCLOSURE_REQUIRED')
347
348 const unavailable = draftWorkloadPlan({
349 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
350 runtime: RUNTIME.BROWSER,
351 privacy: privacy.private(),
352 inventory: { external: false, ohttp: false },
353 externalDisclosure: completeDisclosure
354 })
355 t.is(unavailable.status, PLAN_STATUS.QUEUEABLE)
356 t.is(unavailable.code, 'EXTERNAL_SERVICE_UNAVAILABLE')
357 t.is(unavailable.prospectiveQueue.action, 'wait-for-path')
358 t.alike(unavailable.externalDisclosure.operatorSees, ['query', 'timing'])
359 t.ok(unavailable.privacyResolution.transport.requiredEvidence.length > 0)
360 t.ok(unavailable.claims.semanticDisclosure)
361})
362
363test('live traffic exposes its direct-path conflict and preparation boundary', t => {
364 const blocked = draftWorkloadPlan({
365 workload: WORKLOAD.LIVE,
366 runtime: RUNTIME.NATIVE,
367 privacy: privacy.highPrivacy(),
368 inventory: { p2p: true, torNative: true }
369 })
370 t.is(blocked.status, PLAN_STATUS.BLOCKED)
371 t.is(blocked.code, 'WORKLOAD_PRIVACY_CONFLICT')
372
373 const draft = draftWorkloadPlan({
374 workload: WORKLOAD.LIVE,
375 runtime: RUNTIME.NATIVE,
376 privacy: privacy.fast(),
377 inventory: { p2p: true }
378 })
379 t.is(draft.status, PLAN_STATUS.DRAFT)
380 t.is(draft.code, 'P2P_SESSION_PREPARATION_REQUIRED')
381
382 const unavailable = draftWorkloadPlan({
383 workload: WORKLOAD.LIVE,
384 runtime: RUNTIME.NATIVE,
385 privacy: privacy.fast(),
386 inventory: { p2p: false }
387 })
388 t.is(unavailable.status, PLAN_STATUS.QUEUEABLE)
389 t.is(unavailable.code, 'P2P_PATH_UNAVAILABLE')
390 t.is(unavailable.prospectiveQueue.action, 'wait-for-path')
391 t.is(unavailable.intendedTransportProfile, TRANSPORT_PROFILE.P2P_DIRECT)
392})
393
394test('primitive overrides use a closed workload and runtime matrix', t => {
395 const externalRecord = draftWorkloadPlan({
396 workload: { kind: WORKLOAD.RECORDS, primitive: PRIMITIVE.EXTERNAL },
397 runtime: RUNTIME.BROWSER,
398 privacy: privacy.fast(),
399 inventory: { direct: true, external: true }
400 })
401 t.is(externalRecord.status, PLAN_STATUS.BLOCKED)
402 t.is(externalRecord.code, 'WORKLOAD_PRIMITIVE_UNSUPPORTED')
403
404 const browserCore = draftWorkloadPlan({
405 workload: { kind: WORKLOAD.HISTORY, primitive: PRIMITIVE.CORE },
406 runtime: RUNTIME.BROWSER,
407 privacy: privacy.fast(),
408 inventory: { direct: true, core: true }
409 })
410 t.is(browserCore.status, PLAN_STATUS.BLOCKED)
411 t.is(browserCore.code, 'WORKLOAD_PRIMITIVE_UNSUPPORTED')
412
413 const localAsExternal = draftWorkloadPlan({
414 workload: { kind: WORKLOAD.SEMANTIC, execution: 'local', primitive: PRIMITIVE.EXTERNAL },
415 runtime: RUNTIME.BROWSER,
416 privacy: privacy.private(),
417 inventory: { external: true },
418 externalDisclosure: completeDisclosure
419 })
420 t.is(localAsExternal.status, PLAN_STATUS.BLOCKED)
421 t.is(localAsExternal.code, 'WORKLOAD_PRIMITIVE_CONFLICT')
422
423 const externalAsLocal = draftWorkloadPlan({
424 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external', primitive: PRIMITIVE.LOCAL_COMPUTE },
425 runtime: RUNTIME.BROWSER,
426 privacy: privacy.private()
427 })
428 t.is(externalAsLocal.status, PLAN_STATUS.BLOCKED)
429 t.is(externalAsLocal.code, 'WORKLOAD_PRIMITIVE_CONFLICT')
430
431 const localWithDisclosure = draftWorkloadPlan({
432 workload: { kind: WORKLOAD.SEMANTIC, execution: 'local' },
433 runtime: RUNTIME.BROWSER,
434 privacy: privacy.private(),
435 externalDisclosure: completeDisclosure
436 })
437 t.is(localWithDisclosure.status, PLAN_STATUS.BLOCKED)
438 t.is(localWithDisclosure.code, 'WORKLOAD_DISCLOSURE_CONFLICT')
439})
440
441test('durability policies are primitive-specific and physically valid', t => {
442 const inbox = draftWorkloadPlan({
443 workload: WORKLOAD.RENDEZVOUS,
444 runtime: RUNTIME.BROWSER,
445 privacy: privacy.fast(),
446 inventory: { direct: true }
447 })
448 t.is(inbox.durability.stripeCountLog2, 3)
449 t.is(inbox.durability.stripeCount, 8)
450 t.is(inbox.durability.replicaTargetPerStripe, 3)
451 t.is(inbox.durability.appendAcknowledgementTargetPerStripe, 2)
452 t.is(inbox.durability.readbackTargetPerStripe, 2)
453 t.is(inbox.durability.independentOperatorGroups, 2)
454 t.is(inbox.inbox.estimateStatus, 'size-basis-required')
455
456 for (const [byteLength, frameClass, frameBytes] of [
457 [1, 1, 4096],
458 [4096, 1, 4096],
459 [4097, 2, 16384],
460 [16385, 3, 65536],
461 [65536, 3, 65536]
462 ]) {
463 const sized = draftWorkloadPlan({
464 workload: { kind: WORKLOAD.RENDEZVOUS, operation: 'append', byteLength },
465 runtime: RUNTIME.BROWSER,
466 privacy: privacy.fast(),
467 inventory: { direct: true }
468 })
469 t.is(sized.status, PLAN_STATUS.DRAFT)
470 t.is(sized.inbox.frameClass, frameClass)
471 t.is(sized.inbox.frameBytes, frameBytes)
472 t.is(sized.wireOperation.operation, 'APPEND')
473 }
474
475 const oversizedInbox = draftWorkloadPlan({
476 workload: { kind: WORKLOAD.RENDEZVOUS, operation: 'append', byteLength: 65537 },
477 runtime: RUNTIME.BROWSER,
478 privacy: privacy.fast(),
479 inventory: { direct: false }
480 })
481 t.is(oversizedInbox.status, PLAN_STATUS.BLOCKED)
482 t.is(oversizedInbox.code, 'RENDEZVOUS_FRAME_TOO_LARGE')
483 t.is(oversizedInbox.inbox.maximumFrameBytes, 65536)
484
485 for (let stripeCountLog2 = 0; stripeCountLog2 <= 6; stripeCountLog2++) {
486 const plan = draftWorkloadPlan({
487 workload: WORKLOAD.RENDEZVOUS,
488 runtime: RUNTIME.BROWSER,
489 privacy: privacy.fast(),
490 inventory: { direct: true },
491 durability: { stripeCountLog2 }
492 })
493 t.is(plan.durability.stripeCount, 2 ** stripeCountLog2)
494 }
495
496 t.exception(() => draftWorkloadPlan({
497 workload: WORKLOAD.RENDEZVOUS,
498 runtime: RUNTIME.BROWSER,
499 privacy: privacy.fast(),
500 inventory: { direct: true },
501 durability: { stripeTarget: 3 }
502 }), /unknown fields/)
503 t.exception(() => draftWorkloadPlan({
504 workload: WORKLOAD.RENDEZVOUS,
505 runtime: RUNTIME.BROWSER,
506 privacy: privacy.fast(),
507 inventory: { direct: true },
508 durability: { replicaTargetPerStripe: 2, appendAcknowledgementTargetPerStripe: 3 }
509 }), /cannot exceed replicaTargetPerStripe/)
510 t.exception(() => draftWorkloadPlan({
511 workload: WORKLOAD.RENDEZVOUS,
512 runtime: RUNTIME.BROWSER,
513 privacy: privacy.fast(),
514 inventory: { direct: true },
515 durability: { readbackTargetPerStripe: 1, independentOperatorGroups: 2 }
516 }), /cannot exceed readbackTargetPerStripe/)
517
518 const cell = draftWorkloadPlan({
519 workload: { kind: WORKLOAD.RECORDS, byteLength: 1024 },
520 runtime: RUNTIME.BROWSER,
521 privacy: privacy.fast(),
522 inventory: { direct: true }
523 })
524 t.is(cell.durability.fetchTarget, 1)
525 t.is(cell.durability.independentOperatorGroups, 2)
526 t.exception(() => draftWorkloadPlan({
527 workload: { kind: WORKLOAD.RECORDS, byteLength: 1024 },
528 runtime: RUNTIME.BROWSER,
529 privacy: privacy.fast(),
530 inventory: { direct: true },
531 durability: { readbackTarget: 1, independentOperatorGroups: 2 }
532 }), /cannot exceed readbackTarget/)
533
534 const live = draftWorkloadPlan({
535 workload: WORKLOAD.LIVE,
536 runtime: RUNTIME.NATIVE,
537 privacy: privacy.fast(),
538 inventory: { p2p: true },
539 durability: { replicaTarget: 3 }
540 })
541 t.is(live.status, PLAN_STATUS.BLOCKED)
542 t.is(live.code, 'WORKLOAD_DURABILITY_CONFLICT')
543})
544
545test('v1 estimator exposes exact blind-envelope amplification for every Cell class', t => {
546 const rows = [
547 { sizeClass: 1, put: 65536, get: 16384 },
548 { sizeClass: 2, put: 65536, get: 65536 },
549 { sizeClass: 3, put: 262144, get: 262144 },
550 { sizeClass: 4, put: 1048576, get: 1048576 },
551 { sizeClass: 5, put: 8388608, get: 8388608 }
552 ]
553 for (const row of rows) {
554 const put = estimateCellWireV1({ operation: 'PUT', sizeClass: row.sizeClass, usefulBytes: 1, replicas: 3 })
555 const get = estimateCellWireV1({ operation: 'GET', sizeClass: row.sizeClass, usefulBytes: 1 })
556 t.is(put.requestOuterBytes, row.put)
557 t.is(put.responseOuterBytes, row.put)
558 t.is(get.requestOuterBytes, row.get)
559 t.is(get.responseOuterBytes, row.get)
560 t.is(put.totalOuterBytes, row.put * 2 * 3)
561 t.is(get.totalOuterBytes, row.get * 2)
562 }
563 const smallPut = estimateCellWireV1({ operation: 'PUT', sizeClass: 1, usefulBytes: 1024, replicas: 3 })
564 t.is(smallPut.totalOuterBytes, 393216)
565 t.is(smallPut.usefulByteAmplification, 384)
566 const maximumContent = estimateCellWireV1({ operation: 'PUT', sizeClass: 1 })
567 t.is(maximumContent.usefulBytes, maximumCellContentBytes(1))
568 t.exception(() => estimateCellWireV1({
569 operation: 'PUT',
570 sizeClass: 1,
571 usefulBytes: 1,
572 transportBytes: 1
573 }), /unknown fields/)
574})
575
576test('Cell representation blockers are invariant under transport availability', t => {
577 const plan = draftWorkloadPlan({
578 workload: { kind: WORKLOAD.RECORDS, byteLength: 2 * 1024 * 1024 },
579 runtime: RUNTIME.BROWSER,
580 privacy: privacy.fast(),
581 inventory: { direct: false },
582 costBudget: { maxBlindEnvelopeAmplification: 100 }
583 })
584 t.is(plan.status, PLAN_STATUS.BLOCKED)
585 t.is(plan.code, 'CHUNK_MANIFEST_REQUIRED')
586 t.ok(plan.chunking.minimumChunkCount > 1)
587})
588
589test('placement and exact Cell cost bases never depend on temporary availability', t => {
590 const availableRecord = draftWorkloadPlan({
591 workload: { kind: WORKLOAD.RECORDS, byteLength: 2048 },
592 runtime: RUNTIME.BROWSER,
593 privacy: privacy.fast(),
594 inventory: { direct: true }
595 })
596 const unavailableRecord = draftWorkloadPlan({
597 workload: { kind: WORKLOAD.RECORDS, byteLength: 2048 },
598 runtime: RUNTIME.BROWSER,
599 privacy: privacy.fast(),
600 inventory: { direct: false }
601 })
602 t.is(availableRecord.status, PLAN_STATUS.DRAFT)
603 t.is(unavailableRecord.status, PLAN_STATUS.QUEUEABLE)
604 for (const field of [
605 'primitive',
606 'wireOperation',
607 'cell',
608 'durability',
609 'costBudget',
610 'intendedTransportProfile',
611 'privacyResolution',
612 'claims',
613 'assumptions',
614 'warnings'
615 ]) t.alike(unavailableRecord[field], availableRecord[field])
616
617 const history = draftWorkloadPlan({
618 workload: WORKLOAD.HISTORY,
619 runtime: RUNTIME.NATIVE,
620 privacy: privacy.fast(),
621 inventory: { direct: true, core: false }
622 })
623 t.is(history.primitive, PRIMITIVE.CORE)
624 t.is(history.status, PLAN_STATUS.QUEUEABLE)
625 t.is(history.code, 'QUEUEABLE_CORE_STREAM_UNAVAILABLE')
626 t.is(history.intendedTransportProfile, TRANSPORT_PROFILE.DIRECT)
627 t.is(history.privacyResolution.transport.state, 'qualification-required')
628 t.is(history.wireOperation.operation, 'MIRROR')
629
630 const historyRead = draftWorkloadPlan({
631 workload: { kind: WORKLOAD.HISTORY, operation: 'get' },
632 runtime: RUNTIME.NATIVE,
633 privacy: privacy.private(),
634 inventory: { core: true, splitNative: true }
635 })
636 t.is(historyRead.status, PLAN_STATUS.DRAFT)
637 t.is(historyRead.operation, 'get')
638 t.is(historyRead.wireOperation.operation, 'OPEN_REPLICATION')
639 t.is(historyRead.wireOperation.operationId, 3)
640
641 const unknownSize = draftWorkloadPlan({
642 workload: { kind: WORKLOAD.RECORDS, operation: 'get' },
643 runtime: RUNTIME.BROWSER,
644 privacy: privacy.fast(),
645 inventory: { direct: true }
646 })
647 t.is(unknownSize.status, PLAN_STATUS.DRAFT)
648 t.is(unknownSize.cell.estimateStatus, 'size-basis-required')
649 t.is(unknownSize.cell.blindEnvelope, null)
650
651 const exactRead = draftWorkloadPlan({
652 workload: { kind: WORKLOAD.RECORDS, operation: 'get', byteLength: 1024 },
653 runtime: RUNTIME.BROWSER,
654 privacy: privacy.fast(),
655 inventory: { direct: true },
656 durability: { replicaTarget: 5, readbackTarget: 2, fetchTarget: 1, independentOperatorGroups: 2 }
657 })
658 t.is(exactRead.cell.destinationCountBasis, 'fetchTarget')
659 t.is(exactRead.cell.blindEnvelope.destinationCount, 1)
660
661 const noCostBasis = draftWorkloadPlan({
662 workload: { kind: WORKLOAD.RECORDS, operation: 'get' },
663 runtime: RUNTIME.BROWSER,
664 privacy: privacy.fast(),
665 inventory: { direct: false },
666 costBudget: { maxBlindEnvelopeAmplification: 1000 }
667 })
668 t.is(noCostBasis.status, PLAN_STATUS.BLOCKED)
669 t.is(noCostBasis.code, 'COST_EVIDENCE_UNAVAILABLE')
670})
671
672test('cost budgets name the exact accounting boundary', t => {
673 const blocked = draftWorkloadPlan({
674 workload: { kind: WORKLOAD.RECORDS, byteLength: 1024 },
675 runtime: RUNTIME.BROWSER,
676 privacy: privacy.fast(),
677 inventory: { direct: true },
678 costBudget: { maxBlindEnvelopeAmplification: 100 }
679 })
680 t.is(blocked.status, PLAN_STATUS.BLOCKED)
681 t.is(blocked.code, 'COST_BUDGET_EXCEEDED')
682 t.is(blocked.suggestion, 'batch-small-records')
683 t.ok(blocked.estimate.accountingBoundary.includes('transport wrappers'))
684
685 t.exception(() => draftWorkloadPlan({
686 workload: { kind: WORKLOAD.RECORDS, byteLength: 1024 },
687 runtime: RUNTIME.BROWSER,
688 privacy: privacy.fast(),
689 inventory: { direct: false },
690 costBudget: { maxWireAmplification: 100 }
691 }), /ambiguous/)
692
693 const live = draftWorkloadPlan({
694 workload: WORKLOAD.LIVE,
695 runtime: RUNTIME.NATIVE,
696 privacy: privacy.fast(),
697 inventory: { p2p: true },
698 costBudget: { maxBlindEnvelopeAmplification: 10 }
699 })
700 t.is(live.status, PLAN_STATUS.BLOCKED)
701 t.is(live.code, 'COST_EVIDENCE_UNAVAILABLE')
702
703 const external = draftWorkloadPlan({
704 workload: { kind: WORKLOAD.SEMANTIC, execution: 'external' },
705 runtime: RUNTIME.BROWSER,
706 privacy: privacy.private(),
707 inventory: { external: true },
708 externalDisclosure: completeDisclosure,
709 costBudget: { maxBlindEnvelopeAmplification: 10 }
710 })
711 t.is(external.status, PLAN_STATUS.BLOCKED)
712 t.is(external.code, 'COST_EVIDENCE_UNAVAILABLE')
713
714 t.exception(() => draftWorkloadPlan({
715 workload: WORKLOAD.RECORDS,
716 runtime: RUNTIME.BROWSER,
717 privacy: privacy.fast(),
718 inventory: {},
719 costBudget: { maxBlindEnvelopeAmplification: 10, maxForegroundBytes: 1 }
720 }), /unknown fields/)
721})
722
723test('planner inputs are closed plain-data schemas', t => {
724 const objectPrototypeWasFrozen = Object.isFrozen(Object.prototype)
725 for (const inheritedName of ['__proto__', 'constructor', 'toString']) {
726 t.exception(() => createPrivacyIntent(inheritedName), /unknown privacy preset/)
727 t.exception(() => privacy.custom({ base: inheritedName }), /custom privacy base is invalid/)
728 t.exception(() => draftWorkloadPlan({
729 workload: WORKLOAD.SEMANTIC,
730 runtime: RUNTIME.BROWSER,
731 privacy: inheritedName
732 }), /unknown privacy preset/)
733 }
734 t.is(Object.isFrozen(Object.prototype), objectPrototypeWasFrozen)
735 t.exception(() => draftWorkloadPlan({
736 workload: { kind: WORKLOAD.RECORDS, byteLength: 0 },
737 runtime: RUNTIME.BROWSER
738 }), /positive safe integer/)
739 t.exception(() => draftWorkloadPlan({
740 workload: { kind: WORKLOAD.RECORDS, byteLenght: 2 * 1024 * 1024 },
741 runtime: RUNTIME.BROWSER,
742 privacy: privacy.fast(),
743 inventory: { direct: true }
744 }), /unknown fields/)
745 t.exception(() => draftWorkloadPlan({
746 workload: WORKLOAD.RECORDS,
747 runtime: RUNTIME.BROWSER,
748 privacy: privacy.fast(),
749 inventory: { driect: true }
750 }), /unknown fields/)
751 t.exception(() => draftWorkloadPlan({
752 workload: WORKLOAD.RECORDS,
753 runtime: RUNTIME.BROWSER,
754 privacy: privacy.fast(),
755 inventory: { direct: 'yes' }
756 }), /must be boolean/)
757 t.exception(() => draftWorkloadPlan({
758 workload: WORKLOAD.RECORDS,
759 runtime: RUNTIME.BROWSER,
760 privacy: privacy.fast(),
761 inventory: false
762 }), /inventory must be an object/)
763 t.exception(() => draftWorkloadPlan({
764 workload: WORKLOAD.RECORDS,
765 runtime: RUNTIME.BROWSER,
766 privacy: privacy.fast(),
767 inventory: { direct: true },
768 costBuget: { maxBlindEnvelopeAmplification: 10 }
769 }), /unknown fields/)
770 t.exception(() => draftWorkloadPlan({
771 workload: { kind: WORKLOAD.RECORDS, primitive: PRIMITIVE.P2P_DIRECT },
772 runtime: RUNTIME.BROWSER,
773 privacy: privacy.fast(),
774 costBudget: { maxWireAmplification: 10 }
775 }), /ambiguous/)
776 const misplacedDisclosure = draftWorkloadPlan({
777 workload: WORKLOAD.RECORDS,
778 runtime: RUNTIME.BROWSER,
779 privacy: privacy.fast(),
780 inventory: { direct: true },
781 externalDisclosure: completeDisclosure
782 })
783 t.is(misplacedDisclosure.status, PLAN_STATUS.BLOCKED)
784 t.is(misplacedDisclosure.code, 'WORKLOAD_DISCLOSURE_CONFLICT')
785 t.exception(() => draftWorkloadPlan(Object.assign(Object.create({ inherited: true }), {
786 workload: WORKLOAD.RECORDS,
787 runtime: RUNTIME.BROWSER
788 })), /options are required/)
789})
scripts/blind-capacity-lab.mjs +108 -9
……@@ -8,20 +8,38 @@ import {
88 CELL_SIZE_CLASS,
99 CORE_SESSION_CLASS,
1010 DISPATCH_LIMITS,
11 FAMILY,
1112 FORWARD_CIRCUIT_CLASS,
1213 INBOX_FRAME_CLASS,
13 STREAM_WIRE_CLASS
14} from '../packages/blind-protocol/registry.js'
15
16export const CAPACITY_LAB_SCHEMA = 'hiverelay/blind-capacity-lab-report/v2'
14 OPERATION,
15 OUTER_CLASS,
16 PROTOCOL,
17 STREAM_WIRE_CLASS,
18 WIRE_RUNTIME_AUTHORITY,
19 operationProfile
20} from '../packages/blind-protocol/wire-runtime-authority.js'
21
22export const CAPACITY_LAB_SCHEMA = 'hiverelay/blind-capacity-lab-report/v3'
1723export const CAPACITY_LAB_CONFIG_SCHEMA = 'hiverelay/blind-capacity-lab-config/v2'
1824export const BLIND_SCENARIO_MANIFEST_SCHEMA = 'hiverelay/blind-scenario-manifest/v1'
25export const CAPACITY_LAB_MODEL_REVISION = 'hiverelay/blind-capacity-model/v3-cell-envelope'
1926
2027const KIB = 1024
2128const MIB = 1024 * KIB
2229const GIB = 1024 * MIB
2330const TIB = 1024 * GIB
2431const YEAR_SECONDS = 365.25 * 24 * 60 * 60
32const OUTER_ENVELOPE_HEADER_BYTES = 6
33const CELL_GET_RESULT_BODY_OVERHEAD_BYTES = 2
34const CELL_PUT_PROFILE = operationProfile(FAMILY.CELL, OPERATION.CELL.PUT)
35const MAX_ADMISSION_TOKEN_BYTES = 4096
36const COMPACT_BYTES_4096_PREFIX_BYTES = 3
37const ADMISSION_V1_MAX_BYTES = 2 + 2 + 32 + COMPACT_BYTES_4096_PREFIX_BYTES + MAX_ADMISSION_TOKEN_BYTES
38const CELL_PUT_REQUEST_FIXED_BYTES = 1 + 32 + 4 + 1 + 1 + 32 + 32 + 32 + 32 + 32 + 64
39const CELL_PUT_REQUEST_BODY_OVERHEAD_BYTES = CELL_PUT_REQUEST_FIXED_BYTES + ADMISSION_V1_MAX_BYTES
40const CELL_GET_REQUEST_FIXED_BYTES = 1 + 32 + 32 + 1
41const CELL_GET_MAX_REQUEST_BODY_BYTES = CELL_GET_REQUEST_FIXED_BYTES + ADMISSION_V1_MAX_BYTES
42const OUTER_CLASS_BYTES = Object.values(OUTER_CLASS).sort((left, right) => left - right)
2543
2644export const DEFAULT_CAPACITY_LAB_CONFIG = deepFreeze({
2745 schema: CAPACITY_LAB_CONFIG_SCHEMA,
……@@ -244,6 +262,7 @@ export function runCapacityLab (input = {}) {
244262 schema: CAPACITY_LAB_SCHEMA,
245263 status,
246264 modelKind: 'deterministic-placement-simulation-and-analytical-resource-model',
265 modelProvenance: clone(scenarioManifest.model),
247266 evidenceClass: 'modeled-not-benchmarked',
248267 disclaimer: 'All throughput and latency values are model estimates derived from supplied costs. Placement/failure counts are deterministic simulation results. This report is not a hardware benchmark and is not production performance evidence.',
249268 configDigest: sha256(stableStringify(config)),
……@@ -278,7 +297,7 @@ export function runCapacityLab (input = {}) {
278297 limitations: [
279298 'Storage projections honor each relay disk budget and deterministic replica count; throughput still assumes homogeneous disk speed, network, CPU, WAL, and memory limits across relays.',
280299 'Relay placement is uniform over distinct relays; operator, region, correlated failure, and adversarial selection policies need a separate topology simulation.',
281 'Resource queues are approximated independently and do not model kernel, filesystem, garbage-collector, DHT, TLS, transport framing implementation, or lock contention.',
300 'CELL unary network bytes include the exact minimum-fitting public v1 symmetric outer-envelope classes selected by the current client default. Deliberate privacy up-classing, other-family transport framing, kernel, filesystem, garbage-collector, DHT, TLS, and lock contention remain outside the model.',
282301 'The effective WAL group size is an input, not a claim that the runtime achieves that batch size.',
283302 'Configured WAL payload sizes are planning allowances; format-2 encoded record sizes must be measured and substituted before a release claim.',
284303 'Successful mutation paths are charged two or three WAL records by family; error, retry, terminal, floor, map, and compaction records are not included in foreground averages.',
……@@ -325,6 +344,7 @@ function buildCapacityScenarioManifest (config, scenario) {
325344 return {
326345 schema: BLIND_SCENARIO_MANIFEST_SCHEMA,
327346 source: 'capacity-model',
347 model: capacityModelProvenance(),
328348 relayCount: config.fleet.relayCount,
329349 storageBytesByRelay: relayDiskBytes(config),
330350 performanceShape: {
……@@ -359,6 +379,22 @@ function buildCapacityScenarioManifest (config, scenario) {
359379 }
360380}
361381
382function capacityModelProvenance () {
383 return {
384 revision: CAPACITY_LAB_MODEL_REVISION,
385 cellWireAccounting: 'exact-minimum-v1-symmetric-outer-envelope',
386 wireAuthority: {
387 profile: WIRE_RUNTIME_AUTHORITY.profile,
388 protocolFamily: PROTOCOL.family,
389 protocolMajor: PROTOCOL.major,
390 protocolMinor: PROTOCOL.minor,
391 specHash: WIRE_RUNTIME_AUTHORITY.specHash,
392 abiHash: WIRE_RUNTIME_AUTHORITY.abiHash,
393 vectorSetHash: WIRE_RUNTIME_AUTHORITY.vectorSetHash
394 }
395 }
396}
397
362398function evaluateFamilyCoverage (config, operationModel) {
363399 const minimum = config.objectives.minimumFamilyBlendFraction
364400 const requiredFamilies = [...config.objectives.requiredFamilies]
……@@ -498,21 +534,33 @@ function buildOperationModel (config, objectModel) {
498534
499535 if (item.family === 'CELL') {
500536 const cell = resolveCellCost(item.storageClass, objectModel)
537 const wire = resolveCellWireCost(item.storageClass, objectModel, item.operation)
501538 base.exactClassSemantics = item.storageClass
539 base.wireAccounting = 'exact-minimum-v1-symmetric-outer-envelope'
540 base.meanOuterClassBytesPerDirection = wire.outerBytesPerDirection
541 base.meanOuterRoundTripBytesPerTouch = wire.outerRoundTripBytes
542 base.meanUsefulPayloadBytesPerLogicalOperation = wire.usefulPayloadBytes
543 base.usefulByteAmplificationPerTouch = wire.usefulByteAmplification
502544 if (item.operation === 'PUT') {
503545 setWalCost(base, item, 2, config)
504546 base.relayTouches = config.fleet.replicationFactor
505547 base.diskWriteBytesPerTouch = cell.storedBytes + base.walBytesPerTouch
506548 base.diskIopsPerTouch = 1 + base.walRecordsPerTouch
507 base.networkIngressBytesPerTouch = cell.opaqueBytes + requestBytes
508 base.networkEgressBytesPerTouch = config.workload.ackBytes
549 base.networkIngressBytesPerTouch = wire.outerBytesPerDirection
550 base.networkEgressBytesPerTouch = wire.outerBytesPerDirection
509551 } else {
510552 base.relayTouches = config.workload.readFanout
511553 base.diskReadBytesPerTouch = cell.readBytes
512554 base.diskIopsPerTouch = 1
513 base.networkIngressBytesPerTouch = requestBytes
514 base.networkEgressBytesPerTouch = cell.opaqueBytes + responseBytes
555 base.networkIngressBytesPerTouch = wire.outerBytesPerDirection
556 base.networkEgressBytesPerTouch = wire.outerBytesPerDirection
515557 }
558 base.meanOuterIngressBytesPerLogicalOperation = fixed(wire.outerBytesPerDirection * base.relayTouches)
559 base.meanOuterEgressBytesPerLogicalOperation = fixed(wire.outerBytesPerDirection * base.relayTouches)
560 base.meanOuterRoundTripBytesPerLogicalOperation = fixed(wire.outerRoundTripBytes * base.relayTouches)
561 base.usefulByteAmplificationPerLogicalOperation = fixed(
562 wire.outerRoundTripBytes * base.relayTouches / wire.usefulPayloadBytes
563 )
516564 } else if (item.family === 'INBOX') {
517565 const frameBytes = INBOX_FRAME_CLASS[item.frameClass]
518566 const frameResidentBytes = roundUp(
……@@ -622,6 +670,14 @@ function buildOperationModel (config, objectModel) {
622670 ])),
623671 exactWireLimits: {
624672 cellSizeClasses: { ...CELL_SIZE_CLASS },
673 outerClasses: { ...OUTER_CLASS },
674 cellBodyBounds: {
675 maximumAdmissionBytes: ADMISSION_V1_MAX_BYTES,
676 maximumPutFixedBytes: CELL_PUT_REQUEST_FIXED_BYTES,
677 maximumPutOverheadBytes: CELL_PUT_REQUEST_BODY_OVERHEAD_BYTES,
678 maximumGetRequestBytes: CELL_GET_MAX_REQUEST_BODY_BYTES,
679 getResultOverheadBytes: CELL_GET_RESULT_BODY_OVERHEAD_BYTES
680 },
625681 inboxFrameClasses: { ...INBOX_FRAME_CLASS },
626682 coreSessionClasses: clone(CORE_SESSION_CLASS),
627683 forwardCircuitClasses: clone(FORWARD_CIRCUIT_CLASS),
……@@ -650,6 +706,49 @@ function resolveCellCost (storageClass, objectModel) {
650706 }
651707}
652708
709function resolveCellWireCost (storageClass, objectModel, operation) {
710 const mix = storageClass === 'weighted-cell-mix'
711 ? objectModel.mix
712 : objectModel.mix.filter(candidate => candidate.name === storageClass)
713 if (mix.length === 0) throw new CapacityConfigError([`CELL operation references unknown storageClass ${storageClass}`])
714 const totalProbability = sum(mix.map(item => item.probability))
715 let outerBytesPerDirection = 0
716 let usefulPayloadBytes = 0
717 for (const item of mix) {
718 const probability = item.probability / totalProbability
719 outerBytesPerDirection += probability * cellOuterBytesPerDirection(operation, item.opaqueCellBytes)
720 usefulPayloadBytes += probability * item.payloadBytes
721 }
722 const outerRoundTripBytes = outerBytesPerDirection * 2
723 return {
724 outerBytesPerDirection: fixed(outerBytesPerDirection),
725 outerRoundTripBytes: fixed(outerRoundTripBytes),
726 usefulPayloadBytes: fixed(usefulPayloadBytes),
727 usefulByteAmplification: fixed(outerRoundTripBytes / usefulPayloadBytes)
728 }
729}
730
731function cellOuterBytesPerDirection (operation, cellBytes) {
732 let requestBodyBytes
733 let resultBodyBytes
734 if (operation === 'PUT') {
735 requestBodyBytes = cellBytes + CELL_PUT_REQUEST_BODY_OVERHEAD_BYTES
736 resultBodyBytes = CELL_PUT_PROFILE.maxResultBodyBytes
737 } else if (operation === 'GET') {
738 requestBodyBytes = CELL_GET_MAX_REQUEST_BODY_BYTES
739 resultBodyBytes = cellBytes + CELL_GET_RESULT_BODY_OVERHEAD_BYTES
740 } else {
741 throw new CapacityConfigError([`unsupported modeled CELL operation ${operation}`])
742 }
743 const innerBytes = DISPATCH_LIMITS.PREFIX_BYTES + DISPATCH_LIMITS.HEADER_BYTES +
744 Math.max(requestBodyBytes, resultBodyBytes)
745 const requiredBytes = OUTER_ENVELOPE_HEADER_BYTES + innerBytes
746 for (const bytes of OUTER_CLASS_BYTES) {
747 if (requiredBytes <= bytes) return bytes
748 }
749 throw new CapacityConfigError([`CELL ${operation} requires ${requiredBytes} bytes and exceeds every public outer class`])
750}
751
653752function setWalCost (target, item, recordCount, config) {
654753 target.walRecordsPerTouch = recordCount
655754 target.walBytesPerTouch = recordCount * roundUp(
test/simulation/blind-capacity-lab.test.js +130 -0
……@@ -1,7 +1,12 @@
11import test from 'brittle'
2import b4a from 'b4a'
23import { spawnSync } from 'node:child_process'
34import { fileURLToPath } from 'node:url'
5import { encodeCanonical } from '../../packages/blind-protocol/codec.js'
6import { blake2b256, cellStorageSlot } from '../../packages/blind-protocol/hashes.js'
7import { getCellV1, putCellV1 } from '../../packages/blind-protocol/schemas.js'
48import {
9 CAPACITY_LAB_MODEL_REVISION,
510 CAPACITY_LAB_SCHEMA,
611 CapacityConfigError,
712 runCapacityLab
……@@ -16,6 +21,7 @@ test('blind capacity lab is deterministic and labels every performance value as
1621
1722 t.alike(first, second)
1823 t.is(first.schema, CAPACITY_LAB_SCHEMA)
24 t.is(first.modelProvenance.revision, CAPACITY_LAB_MODEL_REVISION)
1925 t.is(first.status, 'pass')
2026 t.is(first.evidenceClass, 'modeled-not-benchmarked')
2127 t.ok(first.disclaimer.includes('not a hardware benchmark'))
……@@ -107,6 +113,114 @@ test('CELL storage uses exact frozen classes and application framing stays insid
107113 t.is(maximum.opaquePaddingBytes, 0)
108114})
109115
116test('CELL network accounting applies exact v1 symmetric outer-envelope cliffs', t => {
117 const report = runCapacityLab(compactConfig())
118 const put = report.operationModel.operations.find(operation => operation.name === 'cell-put')
119 const get = report.operationModel.operations.find(operation => operation.name === 'cell-get')
120
121 t.alike(report.operationModel.exactWireLimits.outerClasses, {
122 1: 4096,
123 2: 16384,
124 3: 65536,
125 4: 262144,
126 5: 1048576,
127 6: 8388608
128 })
129 t.alike(report.operationModel.exactWireLimits.cellBodyBounds, {
130 maximumAdmissionBytes: 4135,
131 maximumPutFixedBytes: 263,
132 maximumPutOverheadBytes: 4398,
133 maximumGetRequestBytes: 4201,
134 getResultOverheadBytes: 2
135 })
136 t.is(put.wireAccounting, 'exact-minimum-v1-symmetric-outer-envelope')
137 t.is(put.meanOuterClassBytesPerDirection, 530841.6)
138 t.is(put.meanOuterRoundTripBytesPerTouch, 1061683.2)
139 t.is(put.networkIngressBytesPerTouch, put.meanOuterClassBytesPerDirection)
140 t.is(put.networkEgressBytesPerTouch, put.meanOuterClassBytesPerDirection)
141 t.is(put.meanOuterIngressBytesPerLogicalOperation, 1592524.8)
142 t.is(put.meanOuterEgressBytesPerLogicalOperation, 1592524.8)
143 t.is(put.meanOuterRoundTripBytesPerLogicalOperation, 3185049.6)
144 t.is(put.usefulByteAmplificationPerLogicalOperation, 45.347499)
145 t.is(get.meanOuterClassBytesPerDirection, 496435.2)
146 t.is(get.meanOuterRoundTripBytesPerTouch, 992870.4)
147 t.is(get.networkIngressBytesPerTouch, get.meanOuterClassBytesPerDirection)
148 t.is(get.networkEgressBytesPerTouch, get.meanOuterClassBytesPerDirection)
149 t.is(get.meanOuterRoundTripBytesPerLogicalOperation, get.meanOuterRoundTripBytesPerTouch)
150 t.is(get.usefulByteAmplificationPerLogicalOperation, get.usefulByteAmplificationPerTouch)
151 t.ok(put.usefulByteAmplificationPerTouch > 14)
152 t.ok(get.usefulByteAmplificationPerTouch > 13)
153})
154
155test('CELL wire accounting matches canonical maximum bodies at every frozen class cliff', t => {
156 const rows = [
157 { sizeClass: 1, cellBytes: 4096, putOuterBytes: 65536, getOuterBytes: 16384 },
158 { sizeClass: 2, cellBytes: 16384, putOuterBytes: 65536, getOuterBytes: 65536 },
159 { sizeClass: 3, cellBytes: 65536, putOuterBytes: 262144, getOuterBytes: 262144 },
160 { sizeClass: 4, cellBytes: 262144, putOuterBytes: 1048576, getOuterBytes: 1048576 },
161 { sizeClass: 5, cellBytes: 1048576, putOuterBytes: 8388608, getOuterBytes: 8388608 }
162 ]
163 const admission = maximumAdmission()
164
165 t.is(encodeCanonical(getCellV1, {
166 version: 1,
167 storageSlot: b4a.alloc(32),
168 clientNonce: b4a.alloc(32),
169 admission: null
170 }).byteLength, 66)
171 t.is(encodeCanonical(getCellV1, {
172 version: 1,
173 storageSlot: b4a.alloc(32),
174 clientNonce: b4a.alloc(32),
175 admission
176 }).byteLength, 4201)
177
178 for (const row of rows) {
179 const cellBlob = b4a.alloc(row.cellBytes)
180 const createPublicKey = b4a.alloc(32)
181 const putBody = encodeCanonical(putCellV1, {
182 version: 1,
183 storageSlot: cellStorageSlot({ allocationEpoch: 0, createPublicKey }),
184 allocationEpoch: 0,
185 sizeClass: row.sizeClass,
186 leaseClass: 1,
187 clientNonce: b4a.alloc(32),
188 createPublicKey,
189 renewPublicKey: b4a.alloc(32),
190 dropPublicKey: b4a.alloc(32),
191 declaredBlobHash: blake2b256(cellBlob),
192 createSignature: b4a.alloc(64),
193 admission,
194 cellBlob
195 })
196 const report = runCapacityLab(compactConfig({
197 objects: {
198 mix: [{
199 name: `cell-class-${row.sizeClass}`,
200 family: 'CELL',
201 sizeClass: row.sizeClass,
202 weight: 1,
203 payloadBytes: row.cellBytes - 33
204 }]
205 }
206 }))
207 const put = report.operationModel.operations.find(operation => operation.name === 'cell-put')
208 const get = report.operationModel.operations.find(operation => operation.name === 'cell-get')
209
210 t.is(putBody.byteLength, row.cellBytes + 4398, `class ${row.sizeClass} PUT canonical maximum`)
211 t.is(put.meanOuterClassBytesPerDirection, row.putOuterBytes, `class ${row.sizeClass} PUT outer class`)
212 t.is(get.meanOuterClassBytesPerDirection, row.getOuterBytes, `class ${row.sizeClass} GET outer class`)
213 }
214
215 const classOne = runCapacityLab(compactConfig({
216 objects: {
217 mix: [{ name: 'cell-class-1', family: 'CELL', sizeClass: 1, weight: 1, payloadBytes: 4096 - 33 }]
218 }
219 })).operationModel.operations.find(operation => operation.name === 'cell-put')
220 t.is(classOne.meanOuterRoundTripBytesPerLogicalOperation, 384 * 1024)
221 t.is(classOne.usefulByteAmplificationPerLogicalOperation, 96.779719)
222})
223
110224test('family surfaces distinguish retained frames/corpora from transient streams', t => {
111225 const report = runCapacityLab(compactConfig())
112226 const forward = report.operationModel.operations.find(operation => operation.family === 'FORWARD')
……@@ -297,6 +411,13 @@ test('release evidence requires explicit offered load, complete family coverage,
297411test('scenario manifest binds explicit workload, quorum, and heterogeneous capacity assumptions', t => {
298412 const report = runCapacityLab(compactConfig())
299413 t.is(report.scenarioManifest.schema, 'hiverelay/blind-scenario-manifest/v1')
414 t.is(report.scenarioManifest.model.revision, CAPACITY_LAB_MODEL_REVISION)
415 t.is(report.scenarioManifest.model.cellWireAccounting, 'exact-minimum-v1-symmetric-outer-envelope')
416 t.is(report.scenarioManifest.model.wireAuthority.profile, 'wire-authority-v1')
417 t.is(report.scenarioManifest.model.wireAuthority.specHash, 'c9ddd235c3963461174e3de13c25a4c995b53ff320be822d8304f870766b6592')
418 t.is(report.scenarioManifest.model.wireAuthority.abiHash, '199ba15d94d4d112cfac520a67055ce15ec870f0f6f7bd9adaaf47d552334567')
419 t.is(report.scenarioManifest.model.wireAuthority.vectorSetHash, 'fa54012cd0d7e4e620878c67e61f435ecb31ddec05a6283917987cc84279ee05')
420 t.alike(report.modelProvenance, report.scenarioManifest.model)
300421 t.is(report.scenarioManifest.offeredLoad.logicalOperationsPerSecond, 40)
301422 t.is(report.scenarioManifest.offeredLoad.source, 'explicit-config')
302423 t.is(report.scenarioManifest.durability.replicationFactor, 3)
……@@ -338,6 +459,15 @@ function compactConfig (override = {}) {
338459 }, override)
339460}
340461
462function maximumAdmission () {
463 return {
464 profileId: 1,
465 schemeId: 1,
466 parameterHash: b4a.alloc(32),
467 token: b4a.alloc(4096)
468 }
469}
470
341471function merge (left, right) {
342472 const output = { ...left }
343473 for (const [key, value] of Object.entries(right)) {