| … | … | @@ -29,6 +29,22 @@ jobs: |
| 29 | 29 | - run: npm ci |
| 30 | 30 | - run: npm run test:unit |
| 31 | 31 | |
| 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 | ||
| 32 | 48 | integration-tests: |
| 33 | 49 | runs-on: ubuntu-latest |
| 34 | 50 | steps: |
| … | … | @@ -6,6 +6,23 @@ documented here. Dates in YYYY-MM-DD. |
| 6 | 6 | |
| 7 | 7 | The packages are versioned in lockstep. |
| 8 | 8 | |
| 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 | ||
| 9 | 26 | ## [0.24.3] — 2026-07-08 |
| 10 | 27 | |
| 11 | 28 | ### Added |
| … | … | @@ -18,6 +18,15 @@ maps existing semantic relay patterns onto generic cells, encrypted cores, |
| 18 | 18 | opaque inboxes, signed discovery, and client-owned validation/repair without an |
| 19 | 19 | operator install or per-application configuration. |
| 20 | 20 | |
| 21 | The draft [`@hiverelay/blind-client` policy SDK](packages/blind-client/README.md) | |
| 22 | adds pure workload, privacy, durability, and cost planning without manufacturing | |
| 23 | runtime evidence. Its complete contract is the | |
| 24 | [Application Privacy SDK v1](docs/protocol/HIVERELAY-APPLICATION-PRIVACY-SDK-V1.md), | |
| 25 | and 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). | |
| 27 | Both are candidate replacement surfaces, not claims about the published legacy | |
| 28 | fleet. | |
| 29 | ||
| 21 | 30 | The currently published `v0.24.3` fleet is still the application-aware legacy |
| 22 | 31 | implementation. It must not be described as the blind substrate. This branch is |
| 23 | 32 | building 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: |
| 84 | 93 | - **Blind substrate for encrypted workloads**: atomic custody processes |
| 85 | 94 | ciphertext only, rejects plaintext/key-material fields at the schema boundary, |
| 86 | 95 | 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. | |
| 87 | 102 | - **Verified durability and repair**: Ed25519 anchor proofs decide whether a |
| 88 | 103 | peer counts toward archive replication; AutoHeal recruits diverse relays and |
| 89 | 104 | repairs missing blocks peer-to-peer once a publisher has been online once. |
| … | … | @@ -0,0 +1,1320 @@ |
| 1 | # HiveRelay Application Privacy SDK v1 | |
| 2 | ||
| 3 | Status: proposed application SDK contract; not a public protocol authority and | |
| 4 | not release evidence. | |
| 5 | ||
| 6 | This specification defines the proposed developer-facing composition layer | |
| 7 | above the frozen HiveRelay blind substrate. It makes workload placement, | |
| 8 | transport privacy, durability, downgrade behavior, cost, recovery, and | |
| 9 | non-blind service disclosure explicit without adding application semantics to | |
| 10 | a relay. | |
| 11 | ||
| 12 | The intended product surface is Pear Deploy: deployment intent fixes the | |
| 13 | minimum route policies and required evidence for a release, while this SDK | |
| 14 | performs fresh, operation-bound qualification and execution at runtime. Pear | |
| 15 | Deploy is the compiler, infrastructure, and evidence plane; it is not an | |
| 16 | application-data proxy, capability vault, or substitute for runtime privacy | |
| 17 | checks. | |
| 18 | ||
| 19 | The implementation target is `@hiverelay/blind-client`. The existing | |
| 20 | `p2p-hiverelay-client` package remains a legacy Hyperdrive/service compatibility | |
| 21 | surface and may expose a thin `/blind` re-export during migration. It MUST NOT | |
| 22 | silently reinterpret its current app-aware methods as strict blind operations. | |
| 23 | ||
| 24 | Examples below describe a target API, not the package's current exported | |
| 25 | surface. In particular, a pure `/policy` helper, a registry constant, or an | |
| 26 | availability boolean is not an executable privacy path. `BlindClient`, durable | |
| 27 | journal/vault backends, and each transport adapter remain unavailable until the | |
| 28 | corresponding package export, runtime implementation, conformance tests, and | |
| 29 | evidence exist. | |
| 30 | ||
| 31 | ## 1. Design rule | |
| 32 | ||
| 33 | Privacy is not one ascending mode that every byte should traverse. The SDK MUST | |
| 34 | resolve these dimensions independently: | |
| 35 | ||
| 36 | 1. workload semantics and the correct substrate primitive; | |
| 37 | 2. representation privacy and replica correlation; | |
| 38 | 3. network transport and adjacent-role visibility; | |
| 39 | 4. metadata shaping; | |
| 40 | 5. identity exposure; | |
| 41 | 6. read-interest exposure; | |
| 42 | 7. durability evidence and repair policy; and | |
| 43 | 8. downgrade or external-service disclosure. | |
| 44 | ||
| 45 | Presets make common choices easy. Every draft, prepared plan, result, and UI | |
| 46 | status exposes the orthogonal decisions so a preset can never hide a tradeoff. | |
| 47 | The SDK reports representation, transport, read-interest, and durability claims | |
| 48 | separately. An aggregate label MUST NOT imply the strongest component when any | |
| 49 | other component has a lower or unverified ceiling. | |
| 50 | ||
| 51 | ## 2. Package boundary | |
| 52 | ||
| 53 | The 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 | ||
| 74 | The existing byte-oriented Cells, Inbox, Core, qualification, intent, attempt, | |
| 75 | selection, and durability modules remain the security foundation. The new layer | |
| 76 | coordinates them; it does not copy their codecs or relax their endpoint pins. | |
| 77 | ||
| 78 | The existing `p2p-hiverelay-client/privacy-policy.js` is useful prior art for | |
| 79 | orthogonal axes and fail-closed path selection, but its legacy path vocabulary | |
| 80 | does not model workload representation, OHTTP, primitive-specific durability, | |
| 81 | operation-bound qualification, or external semantic disclosure. It remains | |
| 82 | unchanged. A future compatibility subpath may re-export this contract only after | |
| 83 | version, package-closure, and cross-runtime gates pass; the legacy resolver is | |
| 84 | not 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 | |
| 89 | calls it) may combine a draft with fresh qualification and return an opaque | |
| 90 | executable plan. | |
| 91 | ||
| 92 | Until an adapter and its capture/conformance evidence exist, it is unselectable. | |
| 93 | A registry constant, relay advertisement, configuration flag, or caller-supplied | |
| 94 | boolean alone never makes a path deployable or an axis satisfied. | |
| 95 | ||
| 96 | ## 3. Workload routing | |
| 97 | ||
| 98 | The 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 | ||
| 108 | The closed primitive IDs are `cell`, `inbox`, `core`, `p2p-direct`, | |
| 109 | `local-compute`, and `external`. An application MAY override a representation | |
| 110 | only within the permitted set above. Any other pair is blocked with | |
| 111 | `WORKLOAD_PRIMITIVE_UNSUPPORTED`; it MUST NOT fall through to a generic storage | |
| 112 | or transport planner. In particular, records cannot be relabeled as P2P or | |
| 113 | external work, ordinary-browser Core cannot be enabled by a boolean, and | |
| 114 | semantic 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 | |
| 119 | history `put` maps to `CORE.MIRROR` and history `get` maps to | |
| 120 | `CORE.OPEN_REPLICATION`. Core proof challenges are durability/qualification | |
| 121 | work, not a relabelled application read. P2P and semantic routes have no blind | |
| 122 | wire 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 | |
| 125 | plaintext under the application's own process/origin boundary. It performs no | |
| 126 | relay request, needs no external-service disclosure, and makes no claim about | |
| 127 | the privacy of later synchronization. `external` always requires the disclosure | |
| 128 | and consent contract in section 11. | |
| 129 | ||
| 130 | Large media SHOULD use direct P2P or Blind Core for the body and Cells for signed | |
| 131 | manifests, thumbnails, keys, and checkpoints. An ordinary browser MAY use | |
| 132 | chunked Cells, but the plan MUST expose the resulting transaction count and | |
| 133 | blind-envelope and adapter/fleet amplification before execution. If no | |
| 134 | app-owned signed manifest is supplied, the result is | |
| 135 | `CHUNK_MANIFEST_REQUIRED`, never an executable single-Cell plan. | |
| 136 | ||
| 137 | Legacy BlindShard/PVSS custody is not a generic SDK primitive or an automatic | |
| 138 | durability upgrade. It multiplies ciphertext/share placement, roster and proof | |
| 139 | work, and repair coordination while retaining publisher/custody linkage and | |
| 140 | giving public readers threshold-share fetch behavior. A product that genuinely | |
| 141 | needs threshold custody must select a separately versioned experimental custody | |
| 142 | profile with its own operator-set assumptions, cost model, recovery semantics, | |
| 143 | and disclosure; ordinary records use independent Cell replicas instead. | |
| 144 | ||
| 145 | ## 4. Privacy presets and custom constraints | |
| 146 | ||
| 147 | The 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 | ||
| 155 | Fixed presets are immutable closed values. `privacy.fast()`, | |
| 156 | `privacy.private()`, and `privacy.highPrivacy()` accept no field overrides; | |
| 157 | attempting one is `PRESET_IMMUTABLE`. Applications needing different behavior | |
| 158 | use `privacy.custom()`, optionally naming a preset as a base whose resolved | |
| 159 | fields are copied into a new custom intent. A custom intent does not modify or | |
| 160 | extend 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 | ||
| 176 | Every 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 | ||
| 196 | Every state ending in `-required` has a non-empty `requiredEvidence` set. The | |
| 197 | pure scaffold exports the closed `EVIDENCE_REQUIREMENT` IDs for signed profile, | |
| 198 | verified endpoint, fresh health, adapter/conformance capture, class or stream | |
| 199 | shaping, application identity boundary, Inbox anonymous reply, universal read | |
| 200 | buckets, P2P session preparation, role separation, no-direct-race, | |
| 201 | no-clearnet, browser opaque origin, external endpoint/connector, and | |
| 202 | disclosure authorization evidence. Unknown evidence IDs fail closed when a | |
| 203 | prepared plan or Pear Deploy release predicate resolves them; an empty set can | |
| 204 | never make a required state satisfied. | |
| 205 | ||
| 206 | The SDK MUST NOT report an axis as satisfied merely because the caller requested | |
| 207 | it. Satisfaction requires the primitive or compatible runtime adapter, signed | |
| 208 | endpoint and transport profile, fresh qualification, and the evidence named by | |
| 209 | that axis. `unsupported` is blocked. `unverified` cannot become `ready`; it is | |
| 210 | `queueable` only when a conforming adapter/path may later obtain the missing | |
| 211 | evidence, otherwise it is blocked. In particular, | |
| 212 | `bucketed-experimental`, `anonymous-reply`, role non-collusion, Tor no-clearnet, | |
| 213 | and Tor Browser opaque-origin claims each need their own evidence and cannot be | |
| 214 | inferred from transport selection. | |
| 215 | ||
| 216 | `not-applicable` is permitted only when the closed workload/primitive pair has | |
| 217 | no such surface, such as network transport for `local-compute`. It is not a way | |
| 218 | to erase a caller-requested constraint from a networked operation. Pure drafts | |
| 219 | have no `actual` field and never copy `requested` into `satisfied`. Only a | |
| 220 | prepared plan may add `actual`, `evidenceRefs`, and the final | |
| 221 | `satisfied | unsupported | unverified | not-applicable` state. | |
| 222 | ||
| 223 | Plans 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 | ||
| 242 | Representation is a primitive claim rather than a caller-selectable privacy | |
| 243 | axis, so it lives under `claims.representation`; it MUST NOT be duplicated as a | |
| 244 | second, independently satisfiable axis. A prepared plan may add `actual`, | |
| 245 | `evidenceRefs`, and final satisfaction state to each `privacyResolution` member | |
| 246 | without renaming this shape. | |
| 247 | ||
| 248 | `prompt` grants permission to present a fallback proposal, not to take it. When | |
| 249 | the 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 | |
| 252 | interactive-only caller MAY instead request a blocked result. It never executes | |
| 253 | the proposal. After visible consent, the future qualified-client | |
| 254 | `authorizeDowngrade()` operation issues an opaque, | |
| 255 | expiring, single-use local authorization bound to the draft ID, | |
| 256 | requested-policy hash, workload/primitive, suggested actual profile, disclosure | |
| 257 | hash if any, and user-visible prompt version. `prepare()` additionally binds the | |
| 258 | resulting ready plan to the exact endpoint/path. A generic boolean, stored | |
| 259 | preference, consent for another draft, or application restart is not | |
| 260 | authorization. `high-privacy` always uses `deny` and cannot mint a downgrade | |
| 261 | authorization. | |
| 262 | ||
| 263 | `authorizeDowngrade()` is not exported by the pure `/policy` module. A pure | |
| 264 | draft rejects caller-supplied downgrade or disclosure authorizations; the | |
| 265 | future `BlindClient` qualification boundary owns their creation and use. | |
| 266 | ||
| 267 | OHTTP 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 | |
| 269 | use. | |
| 270 | ||
| 271 | ## 5. Public API | |
| 272 | ||
| 273 | The ergonomic target separates pure drafting from evidence-qualified | |
| 274 | preparation: | |
| 275 | ||
| 276 | ```js | |
| 277 | import { BlindClient } from '@hiverelay/blind-client' | |
| 278 | import { RUNTIME, draftWorkload, privacy } from '@hiverelay/blind-client/policy' | |
| 279 | ||
| 280 | const 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 | ||
| 310 | const draft = draftWorkload({ | |
| 311 | workload: { kind: 'records', byteLength: encoded.byteLength }, | |
| 312 | runtime: RUNTIME.BROWSER, | |
| 313 | privacy: privacy.private(), | |
| 314 | costBudget: { maxBlindEnvelopeAmplification: 32 } | |
| 315 | }) | |
| 316 | ||
| 317 | if (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 | |
| 329 | asynchronous 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 | |
| 332 | the signed logical intent and does not perform network I/O. | |
| 333 | ||
| 334 | Convenience scopes make the same planner explicit: | |
| 335 | ||
| 336 | ```js | |
| 337 | const records = app.cells({ privacy: privacy.private() }) | |
| 338 | const rendezvous = app.inbox({ privacy: privacy.private(), stripeCountLog2: 3 }) | |
| 339 | const history = app.core({ privacy: privacy.fast() }) | |
| 340 | const live = app.p2p({ privacy: privacy.fast() }) | |
| 341 | ||
| 342 | const localSearch = app.localCompute('search', { | |
| 343 | purpose: 'full-text search', | |
| 344 | executor: localIndex.search | |
| 345 | }) | |
| 346 | ||
| 347 | const 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 | ||
| 362 | Labels such as `search` or a local collection name remain local encrypted state. | |
| 363 | They MUST NOT enter a relay request, capability, receipt, log, metric, descriptor, | |
| 364 | or 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 | |
| 377 | I/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 | ||
| 387 | A draft contains desired constraints, primitive-specific durability, exact | |
| 388 | blind-envelope estimates where derivable, unresolved axes, and candidate | |
| 389 | transport profiles. It contains no `VerifiedEndpoint`, no fresh-health claim, | |
| 390 | no admission/capacity claim, and no execution authority. | |
| 391 | Inventory booleans are prospective compilation hints only: they are not a | |
| 392 | capability snapshot, path-health result, or proof that any adapter is qualified. | |
| 393 | Changing an inventory hint may change `draft` to `queueable`, but MUST NOT erase | |
| 394 | the deterministic candidate primitive, `wireOperation`, intended transport, | |
| 395 | axis requirements, representation sizing, cost result, claims, assumptions, | |
| 396 | warnings, durability, or disclosure metadata. This lets `explain` and offline UI | |
| 397 | show what is missing without pretending that absence invalidated the policy. | |
| 398 | ||
| 399 | A 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 | ||
| 412 | Only `enqueue()` or the journal may later report `local-queued`. Reads and | |
| 413 | watches 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 | |
| 418 | preparation. It resolves the draft against installed adapters, fresh signed | |
| 419 | descriptors/health, path-role separation, admission, capacity, consent, and | |
| 420 | cost evidence. Its result is `ready`, `queueable`, or `blocked`. `ready` is | |
| 421 | reserved for an opaque plan that pins: | |
| 422 | ||
| 423 | ```text | |
| 424 | workload | |
| 425 | primitive | |
| 426 | runtime | |
| 427 | requested privacy intent | |
| 428 | resolved requested/planned/actual/satisfied state for every axis | |
| 429 | canonical transport profile | |
| 430 | family and operation | |
| 431 | qualified endpoint/path roles, adjacent keys, and transport-profile hash | |
| 432 | primitive-specific durability and evidence targets | |
| 433 | cell/frame/session class where applicable | |
| 434 | payload/request commitment and destination identity | |
| 435 | exact blind-envelope costs and separately qualified adapter/fleet estimates | |
| 436 | cost and latency budgets | |
| 437 | downgrade/disclosure authorization identifiers and hashes | |
| 438 | separate representation, transport, read-interest, and durability claims | |
| 439 | expiry/revalidation boundary | |
| 440 | ``` | |
| 441 | ||
| 442 | A prepared plan snapshots binary evidence rather than recursively freezing | |
| 443 | caller-owned byte views. It is non-forgeable, operation-specific, bounded in | |
| 444 | lifetime, and cannot be reconstructed from JSON or a pure draft. | |
| 445 | ||
| 446 | Required state/reason codes include: | |
| 447 | ||
| 448 | ```text | |
| 449 | PRIVACY_PATH_UNAVAILABLE | |
| 450 | PRIVACY_AXIS_UNSUPPORTED | |
| 451 | PRIVACY_EVIDENCE_UNVERIFIED | |
| 452 | DOWNGRADE_CONSENT_REQUIRED | |
| 453 | DOWNGRADE_AUTHORIZATION_MISMATCH | |
| 454 | TRANSPORT_UNSUPPORTED | |
| 455 | CORE_STREAM_UNAVAILABLE | |
| 456 | P2P_PATH_UNAVAILABLE | |
| 457 | WORKLOAD_PRIVACY_CONFLICT | |
| 458 | WORKLOAD_PRIMITIVE_UNSUPPORTED | |
| 459 | WORKLOAD_PRIMITIVE_CONFLICT | |
| 460 | WORKLOAD_DURABILITY_CONFLICT | |
| 461 | WORKLOAD_DISCLOSURE_CONFLICT | |
| 462 | SEMANTIC_SERVICE_DISCLOSURE_REQUIRED | |
| 463 | SEMANTIC_SERVICE_CONSENT_REQUIRED | |
| 464 | EXTERNAL_SERVICE_UNAVAILABLE | |
| 465 | LOCAL_COMPUTE_UNAVAILABLE | |
| 466 | COST_BUDGET_EXCEEDED | |
| 467 | COST_EVIDENCE_UNAVAILABLE | |
| 468 | CHUNK_MANIFEST_REQUIRED | |
| 469 | RENDEZVOUS_FRAME_TOO_LARGE | |
| 470 | OFFLINE_QUALIFICATION_PENDING | |
| 471 | ``` | |
| 472 | ||
| 473 | The SDK MUST revalidate endpoint health, signed profile pins, admission, | |
| 474 | capacity, cost ceiling, and local consent immediately before execution. A plan | |
| 475 | cannot be replayed against another endpoint, operation, profile, class, or | |
| 476 | destination. If revalidation becomes temporarily unavailable without disproving | |
| 477 | the policy, a mutation returns to `queueable`; it never races or silently chooses | |
| 478 | a weaker route. | |
| 479 | ||
| 480 | ## 7. Operation lifecycle | |
| 481 | ||
| 482 | Every mutation follows: | |
| 483 | ||
| 484 | ```text | |
| 485 | POLICY_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 | ||
| 499 | The logical event is locally durable and visible before network publication. A | |
| 500 | `QUEUEABLE_OFFLINE` intent retains the exact requested policy and retry | |
| 501 | conditions. Waking, reconnecting, or discovering a direct relay does not weaken | |
| 502 | that policy. Qualification may move it to `TARGET_PREPARED` only after all axes, | |
| 503 | consent, costs, and primitive-specific durability targets can be bound. | |
| 504 | ||
| 505 | `TARGET_PREPARED` means the exact destination, operation, profile, class, | |
| 506 | request commitment, fresh replica capability material, and authorization hashes | |
| 507 | have been persisted before the first possible send. Changing any of those fields | |
| 508 | creates a new attempt. A different replica also uses a fresh slot, keys, nonce, | |
| 509 | wrapper, ciphertext, capability set, and request identity; it remains part of | |
| 510 | the same app-level logical event only through the journal's explicit replica | |
| 511 | relationship. | |
| 512 | ||
| 513 | A timeout after possible delivery becomes `PENDING_UNKNOWN`. The SDK reconciles | |
| 514 | the exact request identity and MUST NOT create another logical event or silently | |
| 515 | change destination/profile. A downgrade authorization or external disclosure | |
| 516 | authorization is journaled by opaque identifier and binding hash, never by an | |
| 517 | unbound `true` flag. | |
| 518 | ||
| 519 | Every asynchronous API accepts `AbortSignal` and a bounded deadline. Abort before | |
| 520 | the commit boundary cancels. Abort after a possibly committed send records an | |
| 521 | ambiguous state and preserves reconciliation data. | |
| 522 | ||
| 523 | ## 8. Capability vault and recovery | |
| 524 | ||
| 525 | The 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 | ||
| 537 | Browser implementations use an origin-isolated IndexedDB backend with multi-tab | |
| 538 | compare-and-swap. Native implementations use an encrypted local store with | |
| 539 | explicit fsync. Application identity keys never become storage-management keys. | |
| 540 | ||
| 541 | The capability taxonomy distinguishes canonical client-composition values from | |
| 542 | opaque 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 | ||
| 559 | Names ending in `V1` above refer to existing canonical client-composition | |
| 560 | schemas. Other names are proposed opaque SDK handles over separately stored key | |
| 561 | material; this specification does not add them to the public relay wire. Core | |
| 562 | mirroring/admission authority, P2P session keys, application signing identity, | |
| 563 | and external-service credentials are separate authority classes and MUST NOT be | |
| 564 | treated as convertible storage capabilities. | |
| 565 | ||
| 566 | The SDK provides: | |
| 567 | ||
| 568 | ```text | |
| 569 | exportRecoveryBundle | |
| 570 | importRecoveryBundle | |
| 571 | inspectCapability | |
| 572 | rotateReplica | |
| 573 | renew | |
| 574 | drop | |
| 575 | challenge | |
| 576 | repair | |
| 577 | ``` | |
| 578 | ||
| 579 | Recovery exports are encrypted, versioned, authenticated, bounded, and include | |
| 580 | the exact protocol/profile pins needed to reject a downgrade. Export policy may | |
| 581 | select read-only, append-only, lifecycle-management, or full recovery authority; | |
| 582 | it MUST NOT silently widen the requested set. Export UI states plainly that loss | |
| 583 | of read material loses data access, while loss of create/append/renew/drop/close | |
| 584 | material loses the corresponding publication or lifecycle control. | |
| 585 | ||
| 586 | ## 9. Durability and UI evidence | |
| 587 | ||
| 588 | One receipt is one acknowledged remote operation, not a quorum. Durability is | |
| 589 | primitive-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 | ||
| 600 | Replica, acknowledgement, readback, operator-diversity, stripe, session, mirror, | |
| 601 | lease, and retention fields therefore live in a tagged per-primitive durability | |
| 602 | schema. Generic Cell defaults MUST NOT be copied onto Inbox, Core, P2P, local | |
| 603 | compute, or external plans. Evidence targets must be jointly satisfiable; for | |
| 604 | example, operator-diversity evidence counted only over readback-verified replicas | |
| 605 | cannot exceed the readback target used to complete the policy. | |
| 606 | ||
| 607 | SDK 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 | ||
| 638 | Allowed derived labels are `local-queued`, `remote-stored`, | |
| 639 | `remote-readback-verified`, `replicated`, and `resilient-multi-operator`. The UI | |
| 640 | MUST display the primitive and evidence behind the label and MUST NOT collapse | |
| 641 | them into `online`, `safe`, or `secure`. `queueable` describes current execution | |
| 642 | state, not achieved remote durability. | |
| 643 | ||
| 644 | ## 10. Cost, padding, batching, and chunking | |
| 645 | ||
| 646 | Cost reporting separates cryptographically exact blind framing from | |
| 647 | adapter/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 | ||
| 679 | For the current v1 same-class profile, `blindEnvelope` values are derived from | |
| 680 | the frozen outer classes, not `payload + estimated headers`. They are exact | |
| 681 | outer-envelope bytes, not exact HTTP, OHTTP, Noise, Tor, TLS, handshake, retry, | |
| 682 | or whole-fleet bytes. Adapter and fleet costs differ by profile and path and MUST | |
| 683 | come 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 | |
| 685 | replicas and called fleet cost. | |
| 686 | ||
| 687 | An exact Cell estimate additionally requires an explicit useful-byte/Cell-class | |
| 688 | basis and an operation-specific destination count. PUT uses `replicaTarget`; | |
| 689 | foreground GET uses `fetchTarget`; post-write durability readback remains the | |
| 690 | separate `readbackTarget`. Without the size basis, a draft exposes | |
| 691 | `estimateStatus: size-basis-required` and no `blindEnvelope`; any hard envelope | |
| 692 | ceiling then blocks with `COST_EVIDENCE_UNAVAILABLE`. | |
| 693 | ||
| 694 | Inbox planning similarly selects the minimum universal 4/16/64-KiB opaque frame | |
| 695 | class from an explicit positive byte-length basis. Application framing and | |
| 696 | encryption 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 | |
| 698 | draft append. Inbox outer-envelope, fanout, and fleet costs remain unknown until | |
| 699 | the corresponding adapter model or capture supplies them. | |
| 700 | ||
| 701 | A caller may set `maxBlindEnvelopeAmplification`, `maxAdapterBytes`, | |
| 702 | `maxFleetBytes`, `maxForegroundBytes`, or `maxInteractiveLatencyMs`. A hard | |
| 703 | adapter/fleet/latency ceiling requires evidence with matching coverage; otherwise | |
| 704 | the result is `COST_EVIDENCE_UNAVAILABLE`, not `ready`. The SDK blocks rather | |
| 705 | than overruns a hard ceiling. It reports costs per primitive and operation: | |
| 706 | Cell PUT/GET/PROVE, Inbox create/append/read/watch, Core mirror/prove/session, | |
| 707 | P2P session, and external-service request are not interchangeable estimates. | |
| 708 | ||
| 709 | The SDK SHOULD recommend batching when a small event would sparsely occupy a | |
| 710 | 4-KiB Cell. It MUST NOT automatically combine events with different authors, | |
| 711 | authorization, atomicity, visibility, retention, or privacy policy. The app owns | |
| 712 | the batching codec and flush policy. | |
| 713 | ||
| 714 | Objects larger than the maximum Cell payload require an app-owned signed chunk | |
| 715 | manifest. Without one, drafting is blocked with `CHUNK_MANIFEST_REQUIRED`. With | |
| 716 | one, the prepared plan pins the manifest commitment, exact chunk count/classes, | |
| 717 | per-chunk destinations and operations, total blind-envelope costs, available | |
| 718 | adapter/fleet bounds, and completion rule. Partial chunks remain incomplete | |
| 719 | state; the SDK never reports the logical object complete until the manifest and | |
| 720 | every required chunk validate. The SDK never invents an application chunk codec | |
| 721 | or signs a manifest on the application's behalf. | |
| 722 | ||
| 723 | ## 11. External semantic services | |
| 724 | ||
| 725 | Search, ranking, recommendation, transcoding, remote AI, fraud evaluation, | |
| 726 | consensus, and server-authoritative game transitions are not blind merely because | |
| 727 | their input is encrypted in transit. | |
| 728 | ||
| 729 | The application first chooses `local-compute` or `external`. Local compute uses | |
| 730 | an application-owned executor and reports only the local representation and | |
| 731 | process/origin assumptions; it does not manufacture a transport plan or an | |
| 732 | external disclosure. | |
| 733 | ||
| 734 | An external-service pure draft requires a separate network intent and a | |
| 735 | secret-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 | ||
| 754 | The pure draft canonicalizes `operatorSees`, retains only the content-addressed | |
| 755 | endpoint-evidence digest, and returns | |
| 756 | `consentRequired: true, consentStatus: qualification-required`. It rejects a | |
| 757 | caller-shaped `userConsent`, `disclosureAuthorization`, or raw evidence bytes. | |
| 758 | This allows Pear Deploy to compile the route without serializing a user's | |
| 759 | runtime consent. | |
| 760 | ||
| 761 | The 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 | |
| 765 | identity and endpoint-evidence hash, purpose, exact `operatorSees` set, | |
| 766 | authority, retention statement, requested network-policy hash, visible prompt | |
| 767 | version, scope/expiry, and application persona. A boolean `userConsent`, an | |
| 768 | authorization for a different service/purpose, or consent to transport downgrade | |
| 769 | is 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 | |
| 773 | the application presents the exact disclosure through a visible consent UI. | |
| 774 | ||
| 775 | Disclosure does not replace transport privacy. `prepare()` still resolves | |
| 776 | `networkPrivacy` to an actual direct, OHTTP/split, or Tor profile and verifies | |
| 777 | its evidence. A high-privacy external request without Tor is queueable or | |
| 778 | blocked exactly like a blind-storage request; it does not become direct because | |
| 779 | the operator can see plaintext. The prepared plan's `transportProfile` remains | |
| 780 | the 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 | ||
| 796 | The SDK keeps external credentials, telemetry, retry policy, consent, cost, and | |
| 797 | failure state separate from blind storage. Results that alter canonical | |
| 798 | application state are independently validated and signed under the application | |
| 799 | protocol. Provider acknowledgements and retention statements never count as | |
| 800 | Blind Cell/Inbox/Core durability. | |
| 801 | ||
| 802 | Local search, local moderation, and on-device inference need no external | |
| 803 | disclosure 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 | ||
| 838 | No bullet above asserts that an adapter currently ships. Runtime defaults become | |
| 839 | selectable 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 | ||
| 845 | Pear Deploy is the intended high-level developer experience after the substrate, | |
| 846 | transport adapters, and representative applications pass their conformance | |
| 847 | gates. Its existing record kernel is useful authority machinery, but the | |
| 848 | `pear.deploy.json` compiler, controller, HiveRelay provider adapter, and privacy | |
| 849 | route integration are not yet shipped. This section is a target integration | |
| 850 | contract, not evidence that those components exist. | |
| 851 | ||
| 852 | During this design audit, the legacy application-bridge validator skipped all | |
| 853 | 15 discovered applications and reported success with zero evaluated apps; one | |
| 854 | draft contract also used v2 `contentFlow` vocabulary against a v1 | |
| 855 | `publishFlow` validator. Neither result is promotion evidence. The integration | |
| 856 | must make zero evaluated applications a failure and converge on one pinned | |
| 857 | contract version before a privacy route can satisfy a release gate. | |
| 858 | ||
| 859 | Three 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 | ||
| 867 | Selecting a deployment profile MUST NOT manufacture a route-policy result. A | |
| 868 | route policy MUST NOT change a data classification. Passing both gates MUST NOT | |
| 869 | be summarized as one undifferentiated `private: true` claim. | |
| 870 | ||
| 871 | Pear Deploy owns release intent, deterministic compilation, provider leases, | |
| 872 | binding sets, and release evidence. The application owns plaintext, schemas, | |
| 873 | signing identities, storage capabilities, consent handles, logical events, and | |
| 874 | runtime operations. No compiler artifact, provider snapshot, lock, deployment | |
| 875 | record, binding set, proof, catalog entry, or controller log may contain bearer | |
| 876 | capabilities, application keys, payloads, locators, or consent handles. | |
| 877 | ||
| 878 | The dependency direction is one way: the SDK publishes closed policy schemas, | |
| 879 | canonicalization/vectors, a pure planner, and conformance fixtures; a separate | |
| 880 | first-party Pear Deploy compiler/provider integration consumes them. The SDK | |
| 881 | MUST NOT import Pear Deploy, deploy infrastructure, mutate providers, sign a | |
| 882 | release, or report deployment readiness. | |
| 883 | ||
| 884 | ### 13.2 Proposed intent extension | |
| 885 | ||
| 886 | Until the route vocabulary is promoted into a frozen Pear Deploy contract, it | |
| 887 | belongs in the schema-referenced `extensions` namespace of the provider-neutral, | |
| 888 | secret-free `pear.deploy.json`. The compiler MUST reject this extension unless | |
| 889 | it recognizes the exact schema and SDK-policy contract digest. | |
| 890 | ||
| 891 | An 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 | ||
| 978 | The example omits the unrelated required Pear Deploy fields for readability. | |
| 979 | It is not valid against the current unfinished compiler. Production intent MUST | |
| 980 | still contain every required source, surface, artifact, environment, | |
| 981 | availability, name, release, telemetry, and secret declaration. The all-zero | |
| 982 | endpoint-evidence digest is an obvious documentation placeholder; production | |
| 983 | compilation requires the real content digest and qualification requires the | |
| 984 | matching evidence object. | |
| 985 | ||
| 986 | `operations` is a required, non-empty, duplicate-free subset of the closed | |
| 987 | operation set for its workload. The compiler emits a distinct route expansion | |
| 988 | for every `(route, runtime, operation)` tuple and binds each runtime handle to | |
| 989 | only those operations. It MUST NOT rely on the pure planner's convenience | |
| 990 | default because PUT/GET, MIRROR/OPEN_REPLICATION, and Inbox verbs have different | |
| 991 | wire operations, evidence, and cost. `required: false` marks the semantic route | |
| 992 | as optional, and `localExecutor` identifies the reviewed local implementation | |
| 993 | used by `local-only`; neither field permits an undeclared fallback. | |
| 994 | ||
| 995 | The compiler maps `browser` to `RUNTIME.BROWSER`, `bare`, `node`, and Pear | |
| 996 | native surfaces to `RUNTIME.NATIVE`, and a Tor Browser surface to | |
| 997 | `RUNTIME.TOR_BROWSER`. It supplies a declared `maxByteLength` to the pure | |
| 998 | planner as the worst-case `byteLength`; runtime preparation repeats planning | |
| 999 | with the exact operation length. The example's amplification ceiling | |
| 1000 | intentionally fails against the current symmetric v1 Cell envelope and becomes | |
| 1001 | satisfiable only through the qualified compact class-1 mutation profile (or a | |
| 1002 | later more efficient protocol). This is a fail-closed efficiency requirement, | |
| 1003 | not a claim that the compact profile ships today. Before that profile can make | |
| 1004 | the compiler green, a later policy-contract revision MUST add an orthogonal, | |
| 1005 | signed `envelopeProfile` axis, prospective profile-specific estimates, and | |
| 1006 | conformance tuples for compact+direct, compact+OHTTP, compact+split-native, and | |
| 1007 | compact+Tor where each composition is actually supported. Network transport | |
| 1008 | privacy and blind-envelope framing MUST NOT be represented as one competing | |
| 1009 | selector. The v1 `/policy` surface knows only the symmetric envelope and | |
| 1010 | therefore MUST keep this route blocked today. | |
| 1011 | ||
| 1012 | The recognized extension payload is the canonical, secret-free | |
| 1013 | `AppPrivacyIntentV1`. The compiler extracts it to a content-addressed generated | |
| 1014 | artifact so the same bytes can be consumed by conformance tools without making | |
| 1015 | a second human-authored deployment intent. Route names are stable | |
| 1016 | application-local identifiers and are not relay namespaces. The intent | |
| 1017 | expresses 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 | ||
| 1039 | The Pear Deploy compiler calls only the pure `/policy` surface. It expands every | |
| 1040 | route across declared runtime surfaces and declared operations, validates the closed | |
| 1041 | workload/primitive/privacy matrix, computes exact Cell envelope bounds where | |
| 1042 | possible, and emits `application-privacy-plan.json` alongside the normal | |
| 1043 | compiled artifacts. Compilation performs no discovery, qualification, consent, | |
| 1044 | or application mutation and therefore never emits `ready`. | |
| 1045 | ||
| 1046 | The generated plan contains: | |
| 1047 | ||
| 1048 | ```text | |
| 1049 | schema and policy-contract digests | |
| 1050 | source-intent, deployment-profile, and environment digests | |
| 1051 | SDK package/version/source-closure digest | |
| 1052 | stable route name and route-intent digest | |
| 1053 | runtime/workload/primitive/operation expansion | |
| 1054 | requested axes and honest draft resolution states | |
| 1055 | per-primitive durability and cost requirements | |
| 1056 | required adapter, transport-profile, and conformance classes | |
| 1057 | external bridge-policy references | |
| 1058 | compile verdict, reason codes, assumptions, and claim ceilings | |
| 1059 | ``` | |
| 1060 | ||
| 1061 | The compiler adds the plan digest to `compiled-intent.json`, derives | |
| 1062 | provider-neutral requirements into `relay-needs.json`, derives allowed external | |
| 1063 | disclosures into deny-by-default `bridge-policy.json`, and pins the exact policy | |
| 1064 | compiler and route digests in `pear.lock`. The lock also pins the provider | |
| 1065 | snapshot, adapter versions, signed capability digests, evidence-policy digest, | |
| 1066 | and conformance bundle digests used for deployment planning. It contains no | |
| 1067 | bearer authority. | |
| 1068 | ||
| 1069 | Each 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 | |
| 1073 | legacy scalar `privacyTier`. | |
| 1074 | ||
| 1075 | Identical fully expanded intent, environment, policy implementation, frozen | |
| 1076 | provider snapshot, evidence inputs, and evaluation time MUST produce | |
| 1077 | byte-identical route plans, locks, explanations, and provider action templates. | |
| 1078 | Unknown axes, profiles, primitives, adapters, evidence predicates, or cost | |
| 1079 | models fail closed; the compiler does not invent favorable defaults. | |
| 1080 | ||
| 1081 | ### 13.4 Control plane versus runtime data plane | |
| 1082 | ||
| 1083 | The Pear Deploy HiveRelay provider adapter and the SDK transport adapters have | |
| 1084 | different 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 | ||
| 1093 | Deployment qualification proves that an environment has evidence-backed | |
| 1094 | coverage for a declared route class. It cannot pre-authorize future payloads. | |
| 1095 | At runtime the SDK consumes the signed deployment policy and current binding-set | |
| 1096 | projection as constraints and discovery inputs, then independently performs | |
| 1097 | fresh endpoint and operation qualification. A deployment-time pass may make a | |
| 1098 | runtime path likely to prepare; it never makes a future plan ready. | |
| 1099 | ||
| 1100 | Qualification uses synthetic, non-secret fixture operations and negative | |
| 1101 | captures against staged infrastructure. An opaque `prepare()` plan is | |
| 1102 | short-lived, non-serializable, and never stored in `application-privacy-plan`, | |
| 1103 | `pear.lock`, a build artifact, a binding set, or release evidence. | |
| 1104 | ||
| 1105 | Binding-set repair may replace a provider only when the new set meets or exceeds | |
| 1106 | every signed route and deployment constraint. The application sees the new | |
| 1107 | signed binding-set revision through discovery; an already prepared operation | |
| 1108 | does 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 | |
| 1113 | all applicable gates pass: | |
| 1114 | ||
| 1115 | 1. the exact route extension and `/policy` implementation pass `compiler-v1` | |
| 1116 | determinism, closed-schema, and cross-runtime vector tests; | |
| 1117 | 2. 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; | |
| 1120 | 3. the provider adapter passes the applicable `provider-v1` discovery, | |
| 1121 | verification, effect-reconciliation, observation, renewal, and revocation | |
| 1122 | gates without claiming app-data authority; | |
| 1123 | 4. 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; | |
| 1126 | 5. 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; | |
| 1129 | 6. independent probes verify the exact deployed adapter and provider-profile | |
| 1130 | coverage from clean browser/native clients; | |
| 1131 | 7. `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 | |
| 1134 | 8. 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 | ||
| 1138 | Suggested 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 | |
| 1142 | only. Its limitations MUST say that per-operation readiness, non-collusion, | |
| 1143 | traffic-analysis resistance outside the tested capture, service retention, and | |
| 1144 | future provider health remain runtime or external assumptions. | |
| 1145 | ||
| 1146 | Evidence is emitted in two separate classes. `AppPrivacyConformanceV1` binds | |
| 1147 | the source/build/tree digest, exact SDK and adapter artifact digests, runtime | |
| 1148 | matrix, route-intent digests, test/capture digests, verdicts, and limitations. | |
| 1149 | Target qualification observations bind an exact staged endpoint, signed | |
| 1150 | transport-profile and capability-snapshot digests, operation fixture, producer, | |
| 1151 | observation time, expiry, and limitations. Raw traces and private topology are | |
| 1152 | metadata-sensitive sidecars; `release-proof.json` contains their digests and | |
| 1153 | typed verdicts, not the raw material. | |
| 1154 | ||
| 1155 | Missing optional-route evidence disables that route in the generated runtime | |
| 1156 | policy. Missing required-route evidence blocks `ship`; it is not a warning. | |
| 1157 | Preview may use a weaker policy only when the preview environment contains an | |
| 1158 | explicit, separately compiled override and the UI identifies every difference. | |
| 1159 | Production intent is never weakened automatically to make a deploy green. | |
| 1160 | ||
| 1161 | A pure `draft` outcome proves only internal policy validity and cannot satisfy a | |
| 1162 | promotion predicate. A `blocked` required route fails promotion. A `queueable` | |
| 1163 | result can prove only that the app's offline/background behavior was exercised; | |
| 1164 | it never proves target-path availability. Promotion still requires fresh staged | |
| 1165 | qualification and every other predicate declared by the route. | |
| 1166 | ||
| 1167 | ### 13.6 Target developer flow | |
| 1168 | ||
| 1169 | The target flow is one intent file and one release path: | |
| 1170 | ||
| 1171 | ```sh | |
| 1172 | pear deploy check --environment production | |
| 1173 | pear deploy explain privacy --route history.messages | |
| 1174 | pear deploy preview | |
| 1175 | pear deploy ship --environment production | |
| 1176 | ``` | |
| 1177 | ||
| 1178 | `check` is deterministic and non-mutating. Its privacy table shows, per route | |
| 1179 | and runtime, the chosen primitive, requested axes, expected claim ceilings, | |
| 1180 | exact envelope cost where available, unresolved evidence, fallback behavior, | |
| 1181 | and whether the route is required. `explain` shows why candidates were accepted | |
| 1182 | or rejected. `preview` provisions only the isolated preview policy. `ship` | |
| 1183 | executes the idempotent provider action plan, verifies the result, and requests | |
| 1184 | the existing project-controlled release signatures. | |
| 1185 | ||
| 1186 | The compiler also generates a typed, content-digest-pinned route manifest for | |
| 1187 | the app build. The target SDK experience is: | |
| 1188 | ||
| 1189 | ```js | |
| 1190 | import { BlindClient } from '@hiverelay/blind-client' | |
| 1191 | import deployment from 'pear-deploy:runtime' | |
| 1192 | ||
| 1193 | const app = await BlindClient.open({ | |
| 1194 | policy: deployment.applicationPrivacy, | |
| 1195 | bindings: deployment.bindings, | |
| 1196 | adapters, | |
| 1197 | storage, | |
| 1198 | vault | |
| 1199 | }) | |
| 1200 | ||
| 1201 | const timeline = app.route('records.timeline') | |
| 1202 | const result = await timeline.put(encoded, { signal }) | |
| 1203 | ``` | |
| 1204 | ||
| 1205 | The virtual import and `app.route()` are target APIs, not current exports. A | |
| 1206 | generated handle fixes the signed route constraints but still returns the SDK's | |
| 1207 | normal `ready`, `queueable`, `blocked`, or ambiguous operation states. It never | |
| 1208 | contains a hidden direct client or a deployment-time executable plan. An app may | |
| 1209 | use the lower-level SDK directly, but production claim lint SHOULD fail when its | |
| 1210 | effective policy is weaker than the signed deployment route. | |
| 1211 | ||
| 1212 | Compatibility generation is one-way and conservative. A v1 bridge policy is | |
| 1213 | emitted only for disclosure, opt-in, and exclusion semantics it can represent; | |
| 1214 | legacy relay-needs v1/v2 files describe only their actual availability roles. | |
| 1215 | If an SDK axis, failure mode, consent rule, or claim ceiling cannot be represented | |
| 1216 | without loss, the compiler omits or rejects that projection rather than calling | |
| 1217 | it `private`, `p2p-only`, or equivalent. | |
| 1218 | ||
| 1219 | ### 13.7 Delivery order | |
| 1220 | ||
| 1221 | The integration is promoted in this order: | |
| 1222 | ||
| 1223 | 1. freeze and test the pure SDK policy contract and route-plan vectors; | |
| 1224 | 2. stand up each concrete transport/storage adapter and publish narrowly scoped | |
| 1225 | evidence without making it selectable early; | |
| 1226 | 3. exercise the adapters through representative browser, native, offline, | |
| 1227 | high-loss, and multi-operator applications; | |
| 1228 | 4. add the namespaced route extension and deterministic compiler output to Pear | |
| 1229 | Deploy; | |
| 1230 | 5. implement the non-app-data HiveRelay provider adapter and evidence predicates; | |
| 1231 | 6. generate runtime route handles and bind them to signed deployment artifacts; | |
| 1232 | 7. enable `ship` gates route by route only when compiler, provider, verifier, | |
| 1233 | adapter, and app conformance all pass; and | |
| 1234 | 8. promote the extension to a stable Pear Deploy contract only after at least | |
| 1235 | two independent implementations reproduce its vectors. | |
| 1236 | ||
| 1237 | This order allows the one-command experience to be built early without letting | |
| 1238 | its ergonomics outrun the privacy evidence. | |
| 1239 | ||
| 1240 | ## 14. Conformance | |
| 1241 | ||
| 1242 | Release requires, at minimum: | |
| 1243 | ||
| 1244 | 1. 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; | |
| 1247 | 2. 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; | |
| 1250 | 3. 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; | |
| 1253 | 4. fixed preset mutation is rejected and high-privacy downgrade authorization | |
| 1254 | cannot be minted; | |
| 1255 | 5. a queueable intent performs no network I/O and retains the exact requested | |
| 1256 | policy across wake, restart, discovery, and reconnect; | |
| 1257 | 6. family, operation, endpoint/path roles, transport, privacy profile, | |
| 1258 | destination, class, payload/request commitment, and request identity cannot | |
| 1259 | mutate within an attempt; | |
| 1260 | 7. high-privacy capture produces no clearnet DNS, DHT, QUIC, UDP, HTTP, or | |
| 1261 | fallback race; | |
| 1262 | 8. private paths never race direct, and every fallback is a new plan with an | |
| 1263 | exact, unexpired, single-use consent binding; | |
| 1264 | 9. external disclosure consent is bound to service/purpose/visibility/retention | |
| 1265 | and remains independent of network transport and downgrade consent; | |
| 1266 | 10. response loss, restart, reload, and multi-tab races do not duplicate a | |
| 1267 | logical event; | |
| 1268 | 11. 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; | |
| 1272 | 12. known app/schema/author sentinels are absent from every relay-visible surface; | |
| 1273 | 13. Node, Bare, and browser accept and reject identical canonical vectors; | |
| 1274 | 14. per-primitive durability labels rise only with the stated receipts, | |
| 1275 | readbacks, signed heads, witnesses, leases, retention, and operator-group | |
| 1276 | evidence; | |
| 1277 | 15. unrelated and previously unknown apps use the unchanged relay ABI; | |
| 1278 | 16. representation, direct, OHTTP, split-native, Tor, browser-origin, | |
| 1279 | read-interest, and durability claims each have separate evidence and claim | |
| 1280 | ceilings; | |
| 1281 | 17. 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; | |
| 1284 | 18. oversized Cell inputs block with `CHUNK_MANIFEST_REQUIRED` until an | |
| 1285 | app-signed manifest is supplied, after which all chunks and costs are pinned; | |
| 1286 | 19. local compute produces no external disclosure or transport claim, while | |
| 1287 | external compute cannot become ready without complete disclosure and bound | |
| 1288 | consent; and | |
| 1289 | 20. package exports, Node/Bare/browser tests, generated browser closure, and | |
| 1290 | release evidence cover every advertised API and adapter. | |
| 1291 | ||
| 1292 | The workload vectors additionally cover every Inbox frame cliff and oversize | |
| 1293 | rejection, every application-to-wire operation mapping, and equality of all | |
| 1294 | deterministic plan fields across available versus unavailable inventory hints. | |
| 1295 | ||
| 1296 | ## 15. Migration | |
| 1297 | ||
| 1298 | 1. Preserve `p2p-hiverelay-client` for existing app-aware consumers. | |
| 1299 | 2. Promote the strict protocol/client packages from the accepted vNext source | |
| 1300 | and publish them only after their release gates pass. | |
| 1301 | 3. Ship `/policy` first only as the non-executable draft surface, with its | |
| 1302 | closed matrix, immutable presets, axis resolution schema, and cost labels. | |
| 1303 | 4. Ship `BlindClient.prepare()/plan()`, `/journal`, and `/vault` only after | |
| 1304 | opaque ready-plan, queueing, consent-binding, capability, and restart tests | |
| 1305 | pass. | |
| 1306 | 5. Ship concrete adapters incrementally; unavailable or unevidenced adapters | |
| 1307 | remain unselectable rather than caller-set `true`. | |
| 1308 | 6. Add `p2p-hiverelay-client/blind` only as a thin version-pinned re-export. | |
| 1309 | 7. Migrate applications primitive by primitive: Cells, durability, Inbox/Core, | |
| 1310 | then stronger transports and explicitly disclosed external services. | |
| 1311 | 8. 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. | |
| 1314 | 9. Generate runtime route handles and enable production `ship` only for the | |
| 1315 | exact route/runtime/profile tuples covered by the signed evidence bundle. | |
| 1316 | 10. Retire legacy OutboxLog, BlindShard, Notify, and semantic relay paths through | |
| 1317 | the separately signed compatibility cutoff. | |
| 1318 | ||
| 1319 | No SDK change authorizes deployment, publication, DNS, fleet mutation, or a | |
| 1320 | privacy claim without the corresponding independent evidence. |
| … | … | @@ -0,0 +1,202 @@ |
| 1 | # HiveRelay Blind Envelope Efficiency Plan | |
| 2 | ||
| 3 | Status: implementation plan. It preserves the frozen public WIRE v1 authority | |
| 4 | and 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 | |
| 9 | boundaries. The six-byte outer header and 45-byte dispatch frame mean a complete | |
| 10 | Cell never fits the same-sized outer class. The current client also selects one | |
| 11 | class for both request and response, while Private IPC v2 treats a generous | |
| 12 | 16,384-byte operation cap as the actual maximum PUT receipt. | |
| 13 | ||
| 14 | Current 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 | ||
| 22 | At three replicas the final row is 384 KiB for one 4-KiB Cell before TLS, | |
| 23 | retransmission, 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 | ||
| 41 | The capacity lab now derives Cell PUT/GET ingress and egress from the exact | |
| 42 | minimum-fitting v1 symmetric outer classes selected by the current client default. | |
| 43 | Each operation reports mean class bytes per direction and per-touch round-trip, | |
| 44 | plus ingress, egress, round-trip bytes, and useful-byte amplification for the | |
| 45 | complete logical operation after replica/fanout touches. Deliberate privacy | |
| 46 | up-classing remains an explicit unmodeled input rather than being mistaken for the | |
| 47 | minimum. | |
| 48 | ||
| 49 | Report v3 binds the capacity-model revision and frozen WIRE v1 authority profile, | |
| 50 | protocol version, specification hash, ABI hash, and vector-set hash into its | |
| 51 | scenario manifest and digest. Reports from the older logical-byte model cannot | |
| 52 | share the corrected report identity. | |
| 53 | ||
| 54 | All economic and year-one workload projections that used logical Cell bytes as | |
| 55 | network bytes MUST be regenerated from the corrected report before they support | |
| 56 | bandwidth, pricing, or operator-income decisions. | |
| 57 | ||
| 58 | ## 4. Phase 1 — compact mutation results without changing WIRE v1 | |
| 59 | ||
| 60 | Introduce the signed direct-HTTPS transport profile | |
| 61 | `hiverelay/direct-https-compact-cell-mutation/1` and Private IPC v3. The existing | |
| 62 | same-class profile and IPC v2 remain byte-identical. | |
| 63 | ||
| 64 | Because a public v1 request carries no transport-profile hash, a relay advertises | |
| 65 | the compact profile on an additional signed descriptor endpoint whose canonical | |
| 66 | listener authority is distinct from the legacy endpoint. The old endpoint remains | |
| 67 | available during compatibility rollout. Each listener is configured with exactly | |
| 68 | one profile hash, and its edge/daemon readiness projection binds that same hash; | |
| 69 | sharing one URL/port and guessing the profile from request size is forbidden. The | |
| 70 | additional endpoint is universal for the profile and is never app-specific. | |
| 71 | ||
| 72 | The structural maximum result sizes are: | |
| 73 | ||
| 74 | ```text | |
| 75 | BlindReceiptV1 with durability-profile-2 witness: 1,021 bytes | |
| 76 | BlindErrorV1: 8 bytes | |
| 77 | complete receipt dispatch plus outer framing: 1,072 bytes | |
| 78 | ``` | |
| 79 | ||
| 80 | A class-1 Cell PUT with a maximum 4,096-byte admission token fits a class-2 | |
| 81 | request. Its receipt/error fits a class-1 response: | |
| 82 | ||
| 83 | ```text | |
| 84 | current: 64 KiB request + 64 KiB response = 128 KiB/relay | |
| 85 | compact: 16 KiB request + 4 KiB response = 20 KiB/relay | |
| 86 | ``` | |
| 87 | ||
| 88 | At three replicas this changes the exact blind-envelope lower bound from | |
| 89 | 384 KiB to 60 KiB per logical class-1 PUT, an 84.375% reduction before TLS, | |
| 90 | retries, OHTTP, or Tor. | |
| 91 | ||
| 92 | Private IPC v3 carries separate request/result classes and byte counts while | |
| 93 | retaining v2 peer credentials, TLS exporter binding, readiness, replay, FIN, | |
| 94 | half-close, EOF, staging, precommit validation, fsync, and correlation rules. | |
| 95 | ||
| 96 | The first profile's signed allowed tuple is exactly `(CELL, PUT)` with required | |
| 97 | admission and a generated small structural result bound. It does not authorize | |
| 98 | Cell renew/drop, Inbox create/append/renew/close, Core mirror, or any other | |
| 99 | mutation. Adding one of those operations requires a separately versioned | |
| 100 | profile, exact structural bounds, descriptor tuple, vectors, and gates. Reads | |
| 101 | remain same-class. General asymmetric reads would allow a tiny request to elicit | |
| 102 | an 8-MiB padded `NOT_FOUND` response and are therefore forbidden until | |
| 103 | authenticated egress reservation exists. | |
| 104 | ||
| 105 | Required gates: | |
| 106 | ||
| 107 | 1. generated structural-bound authority and vectors; | |
| 108 | 2. exact `(CELL, PUT)` descriptor/profile rejection for every other family and | |
| 109 | operation, plus maximum admission-sized PUT for every Cell class; | |
| 110 | 3. profile-1 and profile-2 receipts and every correlated error; | |
| 111 | 4. separate-direction FIN, fragmentation, EOF, replay, restart, and cancellation; | |
| 112 | 5. edge/daemon atomic activation and fail-closed readiness; | |
| 113 | 6. distinct-listener profile demultiplexing and dual-endpoint rollout; | |
| 114 | 7. old-profile client/server interoperability and downgrade rejection; and | |
| 115 | 8. captured exact wire bytes matching the capacity planner. | |
| 116 | ||
| 117 | ## 5. Phase 2 — negotiated public protocol major 2 | |
| 118 | ||
| 119 | The complete correction belongs to a new `hiverelay-blind/2` protocol major, | |
| 120 | not 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 | |
| 122 | classes and a universal headroom ladder: | |
| 123 | ||
| 124 | ```text | |
| 125 | 4 KiB | |
| 126 | 8 KiB | |
| 127 | 16 KiB | |
| 128 | 32 KiB | |
| 129 | 64 KiB | |
| 130 | 80 KiB | |
| 131 | 256 KiB | |
| 132 | 272 KiB | |
| 133 | 1 MiB | |
| 134 | 1,040 KiB | |
| 135 | 4,112 KiB | |
| 136 | 8 MiB | |
| 137 | ``` | |
| 138 | ||
| 139 | The inserted classes carry a fixed Cell or maximum batch/proof body plus framing | |
| 140 | without promotion to the next large bucket. More classes reveal a finer size | |
| 141 | bucket, so high-privacy policy may deliberately choose a coarser class from the | |
| 142 | same universal set. | |
| 143 | ||
| 144 | Each request cryptographically binds an exact `resultOuterClass` in the new | |
| 145 | request commitment. Transport integrity by itself is not egress authority. For an | |
| 146 | optional-admission read, an absent admission permits only | |
| 147 | `resultOuterBytes <= requestOuterBytes`; the client must therefore up-pad the | |
| 148 | request to the result class. A smaller request may select a larger result only | |
| 149 | with an egress-admission grant bound to the operation, request commitment, exact | |
| 150 | result 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 | ||
| 153 | The relay: | |
| 154 | ||
| 155 | 1. validates it against the operation/resource result bound; | |
| 156 | 2. verifies any required egress-admission binding and reserves global, endpoint, | |
| 157 | connection, and admission egress before work; | |
| 158 | 3. rejects positive unauthenticated response/request amplification before lookup; | |
| 159 | 4. charges exact request and result outer bytes; | |
| 160 | 5. returns found, absent, and error outcomes in the selected result class; and | |
| 161 | 6. echoes the class through authenticated correlation. | |
| 162 | ||
| 163 | Stored Cells and existing capabilities do not need migration. Public protocol, | |
| 164 | descriptor, endpoint masks, routes, transport pins, client negotiation, and | |
| 165 | vectors 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 | ||
| 182 | Every accepted run pins: | |
| 183 | ||
| 184 | ```text | |
| 185 | protocol/profile: hiverelay-blind/1 same-class | hiverelay/direct-https-compact-cell-mutation/1 | hiverelay-blind/2 (protocol 2.0; ABI v4) | |
| 186 | runtime: browser | Node | Bare/Pear | Tor Browser | |
| 187 | primitive: Cell | Inbox | Core | Forward/P2P | |
| 188 | operation and class | |
| 189 | useful and opaque bytes | |
| 190 | request and result outer bytes | |
| 191 | replicas/fanout and acknowledgement target | |
| 192 | durability profile | |
| 193 | transport: direct | OHTTP | split native | Tor | |
| 194 | warm/cold connection or circuit | |
| 195 | RTT/loss/jitter/bandwidth | |
| 196 | CPU, allocations, copies, fsync, WAL and external quorum | |
| 197 | ``` | |
| 198 | ||
| 199 | Reports publish p50/p95/p99/p999 latency, useful throughput, exact ingress and | |
| 200 | egress, useful-byte amplification, CPU, peak memory, admission failures, repair | |
| 201 | traffic, and raw samples. Direct/OHTTP/split/Tor results are never combined into | |
| 202 | one percentile. |
| … | … | @@ -52,6 +52,36 @@ |
| 52 | 52 | "node": ">=18" |
| 53 | 53 | } |
| 54 | 54 | }, |
| 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 | }, | |
| 55 | 85 | "node_modules/@eslint-community/eslint-utils": { |
| 56 | 86 | "version": "4.9.1", |
| 57 | 87 | "dev": true, |
| … | … | @@ -55,3 +55,50 @@ coarse epoch remains current. |
| 55 | 55 | Applications sign and journal their own event before publication; zero relays |
| 56 | 56 | means queued delivery, one compatible unregistered relay is one remote replica, |
| 57 | 57 | and 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. | |
| 62 | It keeps workload placement separate from network privacy, durability, cost, | |
| 63 | and semantic-service disclosure: | |
| 64 | ||
| 65 | ```js | |
| 66 | import { | |
| 67 | RUNTIME, | |
| 68 | WORKLOAD, | |
| 69 | draftWorkloadPlan, | |
| 70 | privacy | |
| 71 | } from '@hiverelay/blind-client/policy' | |
| 72 | ||
| 73 | const 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 | ||
| 82 | A `draft` is never executable. Inventory booleans are deployment hints, not | |
| 83 | endpoint or privacy evidence. The future high-level client must qualify the | |
| 84 | exact endpoint, operation, signed transport-profile hash, fresh health, | |
| 85 | adapter, and capture evidence before it can issue a `ready` plan. Missing | |
| 86 | background paths return `queueable`; incompatible workloads, privacy axes, | |
| 87 | costs, or disclosures return `blocked` with a stable code. | |
| 88 | ||
| 89 | Fixed presets are immutable. `custom` supplies every constraint axis directly | |
| 90 | or inherits a fixed base before overriding it. Pure drafts report requested, | |
| 91 | planned, required-evidence, assumptions, and claim-ceiling fields; they never | |
| 92 | claim evidence-dependent `actual` or `satisfied` state. OHTTP, split-native, | |
| 93 | and Tor remain unavailable for execution until concrete adapters and their | |
| 94 | conformance evidence ship. | |
| 95 | ||
| 96 | The intended production developer experience is a Pear Deploy integration. | |
| 97 | `pear.deploy.json` will declare named application routes and minimum policies; | |
| 98 | the Deploy compiler will call this pure policy surface, pin its deterministic | |
| 99 | route plan and conformance inputs, and block `ship` when a required adapter or | |
| 100 | application test is unevidenced. Deploy-time coverage never becomes an | |
| 101 | executable app operation: the future `BlindClient.prepare()` must still bind | |
| 102 | fresh endpoint, consent, capability, cost, and request evidence at runtime. | |
| 103 | The proposed contract is specified in | |
| 104 | `docs/protocol/HIVERELAY-APPLICATION-PRIVACY-SDK-V1.md` section 13. |
| … | … | @@ -3,11 +3,11 @@ |
| 3 | 3 | "version": 1, |
| 4 | 4 | "evidenceClass": "real-chromium", |
| 5 | 5 | "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", | |
| 11 | 11 | "checks": [ |
| 12 | 12 | "STANDALONE_ESM_IMPORT", |
| 13 | 13 | "REQUIRED_CONTROL_EXPORTS", |
| … | … | @@ -3,10 +3,10 @@ |
| 3 | 3 | "version": 1, |
| 4 | 4 | "evidenceClass": "clean-linux-container", |
| 5 | 5 | "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", | |
| 10 | 10 | "platform": "linux", |
| 11 | 11 | "architecture": "arm64", |
| 12 | 12 | "containerImageId": "sha256:813a7480f28fdadac1f7f5c824bcdad435b5bc1322a5968bbbdef8d058f9dff4", |
| … | … | @@ -28,6 +28,7 @@ |
| 28 | 28 | "./direct-http": "./direct-http.js", |
| 29 | 29 | "./forward": "./forward.js", |
| 30 | 30 | "./inbox": "./inbox.js", |
| 31 | "./policy": "./policy.js", | |
| 31 | 32 | "./intent": "./intent.js", |
| 32 | 33 | "./qualification": "./qualification.js", |
| 33 | 34 | "./runtime/node": "./runtime/node.js", |
| … | … | @@ -0,0 +1,1327 @@ |
| 1 | import { | |
| 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' | |
| 9 | import { fail } from './errors.js' | |
| 10 | import { selectedOperationProfile } from './selected-operation-profile.js' | |
| 11 | ||
| 12 | export const WORKLOAD = Object.freeze({ | |
| 13 | RECORDS: 'records', | |
| 14 | RENDEZVOUS: 'rendezvous', | |
| 15 | HISTORY: 'history', | |
| 16 | LIVE: 'live', | |
| 17 | SEMANTIC: 'semantic' | |
| 18 | }) | |
| 19 | ||
| 20 | export 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 | ||
| 29 | export const RUNTIME = Object.freeze({ | |
| 30 | BROWSER: 'browser', | |
| 31 | NATIVE: 'native', | |
| 32 | TOR_BROWSER: 'tor-browser' | |
| 33 | }) | |
| 34 | ||
| 35 | export const PRIVACY_PRESET = Object.freeze({ | |
| 36 | FAST: 'fast', | |
| 37 | PRIVATE: 'private', | |
| 38 | HIGH_PRIVACY: 'high-privacy', | |
| 39 | CUSTOM: 'custom' | |
| 40 | }) | |
| 41 | ||
| 42 | export const TRANSPORT_REQUIREMENT = Object.freeze({ | |
| 43 | DIRECT: 'direct', | |
| 44 | SOURCE_SEPARATED: 'source-separated', | |
| 45 | TOR: 'tor' | |
| 46 | }) | |
| 47 | ||
| 48 | export 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 | ||
| 60 | export const DOWNGRADE_POLICY = Object.freeze({ | |
| 61 | DENY: 'deny', | |
| 62 | PROMPT: 'prompt' | |
| 63 | }) | |
| 64 | ||
| 65 | export const PLAN_STATUS = Object.freeze({ | |
| 66 | DRAFT: 'draft', | |
| 67 | QUEUEABLE: 'queueable', | |
| 68 | BLOCKED: 'blocked' | |
| 69 | }) | |
| 70 | ||
| 71 | export 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 | ||
| 91 | const METADATA_SHAPING = Object.freeze(['none', 'padded', 'bucketed-experimental']) | |
| 92 | const IDENTITY_EXPOSURE = Object.freeze(['stable', 'session', 'anonymous-reply']) | |
| 93 | const READ_INTEREST = Object.freeze(['observable', 'bucketed-experimental']) | |
| 94 | const OUTER_ENVELOPE_HEADER_BYTES = 6 | |
| 95 | const CELL_GET_RESULT_BODY_OVERHEAD_BYTES = 2 | |
| 96 | const CELL_BLOB_OVERHEAD_BYTES = 1 + 12 + 16 + 4 | |
| 97 | const CELL_PUT_PROFILE = selectedOperationProfile(FAMILY.CELL, OPERATION.CELL.PUT) | |
| 98 | const 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. | |
| 101 | const CELL_PUT_REQUEST_BODY_OVERHEAD_BYTES = 4398 | |
| 102 | const CELL_GET_REQUEST_BODY_MAX_BYTES = 4201 | |
| 103 | ||
| 104 | function 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 | ||
| 111 | const 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 | ||
| 141 | const CUSTOM_FIELDS = Object.freeze([ | |
| 142 | 'transport', | |
| 143 | 'metadataShaping', | |
| 144 | 'identityExposure', | |
| 145 | 'readInterest', | |
| 146 | 'downgradePolicy', | |
| 147 | 'backgroundPreferred' | |
| 148 | ]) | |
| 149 | const CUSTOM_OPTION_FIELDS = Object.freeze([...CUSTOM_FIELDS, 'base']) | |
| 150 | ||
| 151 | export 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 | ||
| 158 | export 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 | ||
| 190 | export 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 | ||
| 384 | export const planWorkload = draftWorkloadPlan | |
| 385 | export const draftWorkload = draftWorkloadPlan | |
| 386 | ||
| 387 | export 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 | ||
| 431 | function 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 | ||
| 470 | function 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 | ||
| 488 | function 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 | ||
| 499 | function 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 | ||
| 580 | function 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 | ||
| 599 | function 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 | ||
| 680 | function numberOr (value, fallback, field) { | |
| 681 | return numberBetweenOr(value, fallback, 1, 16, field) | |
| 682 | } | |
| 683 | ||
| 684 | function 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 | ||
| 692 | function 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 | ||
| 697 | function 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 | ||
| 737 | function 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 | ||
| 751 | function 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 | ||
| 776 | function 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 | ||
| 812 | function 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 | ||
| 855 | function 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 | ||
| 953 | function 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 | ||
| 967 | function 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 | ||
| 994 | function validEndpointEvidence (value) { | |
| 995 | return typeof value === 'string' && /^(sha256|blake2b256):[0-9a-f]{64}$/i.test(value) | |
| 996 | } | |
| 997 | ||
| 998 | function 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 | ||
| 1010 | function endpointEvidenceMetadata (value) { | |
| 1011 | const separator = value.indexOf(':') | |
| 1012 | return { kind: value.slice(0, separator).toLowerCase(), digest: value.toLowerCase() } | |
| 1013 | } | |
| 1014 | ||
| 1015 | function 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 | ||
| 1021 | function 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 | ||
| 1068 | function 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 | ||
| 1103 | function 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 | ||
| 1117 | function 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 | ||
| 1124 | function 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 | ||
| 1142 | function 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 | ||
| 1167 | function 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 | ||
| 1178 | function 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 | ||
| 1229 | function 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 | ||
| 1249 | function 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 | ||
| 1293 | function 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 | ||
| 1305 | function 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 | ||
| 1313 | function 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 | ||
| 1319 | function 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 | ||
| 1325 | function hasOwn (object, key) { | |
| 1326 | return Object.prototype.hasOwnProperty.call(object, key) | |
| 1327 | } |
| … | … | @@ -0,0 +1,789 @@ |
| 1 | import test from 'brittle' | |
| 2 | import { maximumCellContentBytes } from '../cells.js' | |
| 3 | import { | |
| 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 | ||
| 17 | const 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 | ||
| 27 | test('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 | ||
| 95 | test('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 | ||
| 108 | test('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 | ||
| 132 | test('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 | ||
| 163 | test('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 | ||
| 259 | test('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 | ||
| 363 | test('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 | ||
| 394 | test('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 | ||
| 441 | test('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 | ||
| 545 | test('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 | ||
| 576 | test('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 | ||
| 589 | test('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 | ||
| 672 | test('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 | ||
| 723 | test('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 | }) |
| … | … | @@ -8,20 +8,38 @@ import { |
| 8 | 8 | CELL_SIZE_CLASS, |
| 9 | 9 | CORE_SESSION_CLASS, |
| 10 | 10 | DISPATCH_LIMITS, |
| 11 | FAMILY, | |
| 11 | 12 | FORWARD_CIRCUIT_CLASS, |
| 12 | 13 | INBOX_FRAME_CLASS, |
| 13 | STREAM_WIRE_CLASS | |
| 14 | } from '../packages/blind-protocol/registry.js' | |
| 15 | ||
| 16 | export 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 | ||
| 22 | export const CAPACITY_LAB_SCHEMA = 'hiverelay/blind-capacity-lab-report/v3' | |
| 17 | 23 | export const CAPACITY_LAB_CONFIG_SCHEMA = 'hiverelay/blind-capacity-lab-config/v2' |
| 18 | 24 | export const BLIND_SCENARIO_MANIFEST_SCHEMA = 'hiverelay/blind-scenario-manifest/v1' |
| 25 | export const CAPACITY_LAB_MODEL_REVISION = 'hiverelay/blind-capacity-model/v3-cell-envelope' | |
| 19 | 26 | |
| 20 | 27 | const KIB = 1024 |
| 21 | 28 | const MIB = 1024 * KIB |
| 22 | 29 | const GIB = 1024 * MIB |
| 23 | 30 | const TIB = 1024 * GIB |
| 24 | 31 | const YEAR_SECONDS = 365.25 * 24 * 60 * 60 |
| 32 | const OUTER_ENVELOPE_HEADER_BYTES = 6 | |
| 33 | const CELL_GET_RESULT_BODY_OVERHEAD_BYTES = 2 | |
| 34 | const CELL_PUT_PROFILE = operationProfile(FAMILY.CELL, OPERATION.CELL.PUT) | |
| 35 | const MAX_ADMISSION_TOKEN_BYTES = 4096 | |
| 36 | const COMPACT_BYTES_4096_PREFIX_BYTES = 3 | |
| 37 | const ADMISSION_V1_MAX_BYTES = 2 + 2 + 32 + COMPACT_BYTES_4096_PREFIX_BYTES + MAX_ADMISSION_TOKEN_BYTES | |
| 38 | const CELL_PUT_REQUEST_FIXED_BYTES = 1 + 32 + 4 + 1 + 1 + 32 + 32 + 32 + 32 + 32 + 64 | |
| 39 | const CELL_PUT_REQUEST_BODY_OVERHEAD_BYTES = CELL_PUT_REQUEST_FIXED_BYTES + ADMISSION_V1_MAX_BYTES | |
| 40 | const CELL_GET_REQUEST_FIXED_BYTES = 1 + 32 + 32 + 1 | |
| 41 | const CELL_GET_MAX_REQUEST_BODY_BYTES = CELL_GET_REQUEST_FIXED_BYTES + ADMISSION_V1_MAX_BYTES | |
| 42 | const OUTER_CLASS_BYTES = Object.values(OUTER_CLASS).sort((left, right) => left - right) | |
| 25 | 43 | |
| 26 | 44 | export const DEFAULT_CAPACITY_LAB_CONFIG = deepFreeze({ |
| 27 | 45 | schema: CAPACITY_LAB_CONFIG_SCHEMA, |
| … | … | @@ -244,6 +262,7 @@ export function runCapacityLab (input = {}) { |
| 244 | 262 | schema: CAPACITY_LAB_SCHEMA, |
| 245 | 263 | status, |
| 246 | 264 | modelKind: 'deterministic-placement-simulation-and-analytical-resource-model', |
| 265 | modelProvenance: clone(scenarioManifest.model), | |
| 247 | 266 | evidenceClass: 'modeled-not-benchmarked', |
| 248 | 267 | 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.', |
| 249 | 268 | configDigest: sha256(stableStringify(config)), |
| … | … | @@ -278,7 +297,7 @@ export function runCapacityLab (input = {}) { |
| 278 | 297 | limitations: [ |
| 279 | 298 | '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.', |
| 280 | 299 | '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.', | |
| 282 | 301 | 'The effective WAL group size is an input, not a claim that the runtime achieves that batch size.', |
| 283 | 302 | 'Configured WAL payload sizes are planning allowances; format-2 encoded record sizes must be measured and substituted before a release claim.', |
| 284 | 303 | '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) { |
| 325 | 344 | return { |
| 326 | 345 | schema: BLIND_SCENARIO_MANIFEST_SCHEMA, |
| 327 | 346 | source: 'capacity-model', |
| 347 | model: capacityModelProvenance(), | |
| 328 | 348 | relayCount: config.fleet.relayCount, |
| 329 | 349 | storageBytesByRelay: relayDiskBytes(config), |
| 330 | 350 | performanceShape: { |
| … | … | @@ -359,6 +379,22 @@ function buildCapacityScenarioManifest (config, scenario) { |
| 359 | 379 | } |
| 360 | 380 | } |
| 361 | 381 | |
| 382 | function 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 | ||
| 362 | 398 | function evaluateFamilyCoverage (config, operationModel) { |
| 363 | 399 | const minimum = config.objectives.minimumFamilyBlendFraction |
| 364 | 400 | const requiredFamilies = [...config.objectives.requiredFamilies] |
| … | … | @@ -498,21 +534,33 @@ function buildOperationModel (config, objectModel) { |
| 498 | 534 | |
| 499 | 535 | if (item.family === 'CELL') { |
| 500 | 536 | const cell = resolveCellCost(item.storageClass, objectModel) |
| 537 | const wire = resolveCellWireCost(item.storageClass, objectModel, item.operation) | |
| 501 | 538 | 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 | |
| 502 | 544 | if (item.operation === 'PUT') { |
| 503 | 545 | setWalCost(base, item, 2, config) |
| 504 | 546 | base.relayTouches = config.fleet.replicationFactor |
| 505 | 547 | base.diskWriteBytesPerTouch = cell.storedBytes + base.walBytesPerTouch |
| 506 | 548 | 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 | |
| 509 | 551 | } else { |
| 510 | 552 | base.relayTouches = config.workload.readFanout |
| 511 | 553 | base.diskReadBytesPerTouch = cell.readBytes |
| 512 | 554 | base.diskIopsPerTouch = 1 |
| 513 | base.networkIngressBytesPerTouch = requestBytes | |
| 514 | base.networkEgressBytesPerTouch = cell.opaqueBytes + responseBytes | |
| 555 | base.networkIngressBytesPerTouch = wire.outerBytesPerDirection | |
| 556 | base.networkEgressBytesPerTouch = wire.outerBytesPerDirection | |
| 515 | 557 | } |
| 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 | ) | |
| 516 | 564 | } else if (item.family === 'INBOX') { |
| 517 | 565 | const frameBytes = INBOX_FRAME_CLASS[item.frameClass] |
| 518 | 566 | const frameResidentBytes = roundUp( |
| … | … | @@ -622,6 +670,14 @@ function buildOperationModel (config, objectModel) { |
| 622 | 670 | ])), |
| 623 | 671 | exactWireLimits: { |
| 624 | 672 | 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 | }, | |
| 625 | 681 | inboxFrameClasses: { ...INBOX_FRAME_CLASS }, |
| 626 | 682 | coreSessionClasses: clone(CORE_SESSION_CLASS), |
| 627 | 683 | forwardCircuitClasses: clone(FORWARD_CIRCUIT_CLASS), |
| … | … | @@ -650,6 +706,49 @@ function resolveCellCost (storageClass, objectModel) { |
| 650 | 706 | } |
| 651 | 707 | } |
| 652 | 708 | |
| 709 | function 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 | ||
| 731 | function 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 | ||
| 653 | 752 | function setWalCost (target, item, recordCount, config) { |
| 654 | 753 | target.walRecordsPerTouch = recordCount |
| 655 | 754 | target.walBytesPerTouch = recordCount * roundUp( |
| … | … | @@ -1,7 +1,12 @@ |
| 1 | 1 | import test from 'brittle' |
| 2 | import b4a from 'b4a' | |
| 2 | 3 | import { spawnSync } from 'node:child_process' |
| 3 | 4 | import { fileURLToPath } from 'node:url' |
| 5 | import { encodeCanonical } from '../../packages/blind-protocol/codec.js' | |
| 6 | import { blake2b256, cellStorageSlot } from '../../packages/blind-protocol/hashes.js' | |
| 7 | import { getCellV1, putCellV1 } from '../../packages/blind-protocol/schemas.js' | |
| 4 | 8 | import { |
| 9 | CAPACITY_LAB_MODEL_REVISION, | |
| 5 | 10 | CAPACITY_LAB_SCHEMA, |
| 6 | 11 | CapacityConfigError, |
| 7 | 12 | runCapacityLab |
| … | … | @@ -16,6 +21,7 @@ test('blind capacity lab is deterministic and labels every performance value as |
| 16 | 21 | |
| 17 | 22 | t.alike(first, second) |
| 18 | 23 | t.is(first.schema, CAPACITY_LAB_SCHEMA) |
| 24 | t.is(first.modelProvenance.revision, CAPACITY_LAB_MODEL_REVISION) | |
| 19 | 25 | t.is(first.status, 'pass') |
| 20 | 26 | t.is(first.evidenceClass, 'modeled-not-benchmarked') |
| 21 | 27 | 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 |
| 107 | 113 | t.is(maximum.opaquePaddingBytes, 0) |
| 108 | 114 | }) |
| 109 | 115 | |
| 116 | test('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 | ||
| 155 | test('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 | ||
| 110 | 224 | test('family surfaces distinguish retained frames/corpora from transient streams', t => { |
| 111 | 225 | const report = runCapacityLab(compactConfig()) |
| 112 | 226 | const forward = report.operationModel.operations.find(operation => operation.family === 'FORWARD') |
| … | … | @@ -297,6 +411,13 @@ test('release evidence requires explicit offered load, complete family coverage, |
| 297 | 411 | test('scenario manifest binds explicit workload, quorum, and heterogeneous capacity assumptions', t => { |
| 298 | 412 | const report = runCapacityLab(compactConfig()) |
| 299 | 413 | 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) | |
| 300 | 421 | t.is(report.scenarioManifest.offeredLoad.logicalOperationsPerSecond, 40) |
| 301 | 422 | t.is(report.scenarioManifest.offeredLoad.source, 'explicit-config') |
| 302 | 423 | t.is(report.scenarioManifest.durability.replicationFactor, 3) |
| … | … | @@ -338,6 +459,15 @@ function compactConfig (override = {}) { |
| 338 | 459 | }, override) |
| 339 | 460 | } |
| 340 | 461 | |
| 462 | function maximumAdmission () { | |
| 463 | return { | |
| 464 | profileId: 1, | |
| 465 | schemeId: 1, | |
| 466 | parameterHash: b4a.alloc(32), | |
| 467 | token: b4a.alloc(4096) | |
| 468 | } | |
| 469 | } | |
| 470 | ||
| 341 | 471 | function merge (left, right) { |
| 342 | 472 | const output = { ...left } |
| 343 | 473 | for (const [key, value] of Object.entries(right)) { |