Security model
Scoped read-only grants, current authority and revocation boundaries.
Status: security/domain contract freeze (84A + 84B / W18). Not implemented. Not multi-principal production sharing. First personal companion remains trusted-owner allowlist until sprint 85 enforces this boundary. Imported data never mints authority. UBI bootstrap/pairing values remain a separate blocked contract.
Trust boundaries (unit A)#
| Boundary | Current observation | Freeze rule |
|---|---|---|
| Listener startup credential | Copied at Server.start; not dynamic revoke | Delivery must recheck current principal policy before send; startup copy alone is insufficient |
| Application ordinary documents | !can_write refuses; owner-powered | First bridge uses owner; multi-principal waits for 85 |
| Snapshot leases | Process-local; not authority | Lease ≠ grant; recheck scope on borrow/delivery |
| MCP / wire session | Authenticated store+session | Client-supplied principal/scope never authoritative |
| Browser Origin / CORS | Host checks exist; missing Origin permitted under native policy | Absent Origin is not privilege; browser path needs explicit profile (86) |
Rights vocabulary (normative names)#
Distinct rights (not interchangeable): read, transform, execute, publish, seed, transmit. A grant names grantor, subject, object/field selector, actions, destination, validity window, delegation limit, revocation epoch and audit reference. Owner-only local slice today does not issue these grants. Executable admission today uses owner/can_write / write-gate names from C05; those are current slice gate labels, not members of the frozen grant-rights vocabulary above. Sprint 85 maps or replaces them under accepted semantics.
Normative dimensions (unit B / W18)#
Every authorization decision binds all of the following at the current control-branch head. A stamp from an earlier epoch cannot substitute.
| Dimension | Binding | Notes |
|---|---|---|
| Principal | Authenticated credential -> principal id | Never from caller-asserted context counters |
| Store | store_id of pinned session | Wrong store -> fail closed |
| Branch | Envelope branch_id after namespace rules | Control branch 2 is not ordinary data |
| Action | Operation / right name | read ≠ transform ≠ transmit |
| Object | Collection + record id (or schema/metric id) | Cross-object needs explicit grant covering both |
| Field | Field selector inside object | Partial views omit unauthorized fields |
| Version | Grant revision + authority_epoch | Historical permission never authorizes new delivery |
| Error | Typed deny without sibling disclosure | Prefer forbidden / omit; never leak existence via counts when policy forbids |
Entry -> resolver -> Application -> send chain#
Maps the actual local service path (C05) to fence sites. Exact send-permit hook file remains an open gate; the stage contract does not.
| Stage | Current owner (observation) | Authority obligation |
|---|---|---|
| Framing / route | src/transport listener HTTP entry | Host/Origin/length/method; invalid framing never reaches resolver |
| Authenticate | Service credential registry resolve | Bearer -> principal; startup registry copy is authn only |
| Session / store bind | Request resolver ahead of Application | Pin store_id + session_id; reject foreign store/session |
| Authorize (admission) | Application can_write / capability (src/product / application path) | Bind principal/store/branch/action/object/field/version |
| Perform | Application + engine under admitted snapshot | Commit may produce immutable receipt under then-current epoch |
| Delivery fence | Before response bytes leave the process | Recheck current control-branch authority head |
| Send | Loopback write path (exact file open gate #3) | Queued unsent bytes drop on fence fail; in-flight plaintext not recalled |
Admission vs delivery fence#
| Stage | Gate | Effect if revoke wins race |
|---|---|---|
| Framing / authn | Session + token | No work admitted |
| Authorize | Principal/store/branch/action/object/field/version | Typed deny; zero durable effect |
| Perform | Mutation/query under admitted snapshot | May commit under then-current epoch |
| Delivery fence | Current control head before send | Queued unsent bytes dropped; already-sent plaintext not recalled |
| Receipt | Immutable mutation receipt | May exist after revoke; does not restore delivery rights |
Distinguish: (1) bytes already on the wire, (2) admitted committed operations, (3) queued unsent results. Only (3) is fenced by current-authority delivery.
Ownership, expiry and revoke ordering#
| Resource | Owner | Expiry / revoke | History rule |
|---|---|---|---|
| Snapshot lease | Process that borrowed | Release on cancel/drop; not a grant | Past lease ≠ current authority |
| Mutation admission mutex | Service | Shared by every mutation route; not held across network waits | Lock release ≠ revoke |
| Delivery permit | Current control-branch policy | Recheck at send; revoke drops queue | Prior epoch stamp never reopens send |
| Grant history | Control namespace (future) | Audit trail only | History never grants current authority |
Ordering freeze (implementable without inventing clock semantics):
Revoke updates control-branch authority head (or credential registry for authn-only cases such as
revokeAllReaders).New admission reads the post-revoke head; refused work has zero durable effect.
In-flight perform that already passed admission may still commit a receipt.
Delivery fence reads the current head again; queued results fail closed.
Historical grant/permission stamps remain readable for audit and never authorize a new response.
Wall-clock vs monotonic expiry for grants remains an open gate.
Typed error catalog (compact)#
| Situation | Preferred typed outcome | Disclosure rule |
|---|---|---|
| Authenticated but action/object/field denied | forbidden | No sibling names/counts |
| Missing/invalid bearer or resolved principal | unauthorized (HTTP 401 / slice equivalent) | No store contents |
Wrong/stale session_id | stale_session | No mutation |
Wrong store_id | fail closed / unauthorized store | No cross-store hint |
| MCP client scope override | ScopeOverride / tool error | Non-sensitive |
| Same id, different logical body | idempotency_conflict | Receipt scope only |
| Delivery fence lose race after commit | fence refuse on send | Receipt may already exist; no field leak |
Exact wire string aliases remain owned by C02/C05; this catalog binds deny semantics for sprint 85 without inventing new public error codes here.
Decision tables (threat / authority oracle)#
| Request | Required | Deny / no-disclosure / no-effect |
|---|---|---|
| Read field F | Subject holds read on F at current epoch | Omit field; do not leak via counts/errors |
| Mutate document | Current slice owner/can_write gate; later grant + expected head | Typed forbidden; zero effect |
| Expand evidence | Recheck current authority | Fail closed; no stale handle revival |
| Queue response then revoke | Delivery fence under current control head | Drop queued bytes; receipt may already exist |
| Cross-object or cross-destination | Explicit grant covering both | Deny without naming forbidden siblings |
| Replay same logical id | Bound principal+store+branch+body | idempotency_conflict if body differs |
| Use restored old token | Fresh runtime credentials after restore | Unauthorized / stale_session |
| Import / fixture data | No automatic grant mint | Data ≠ authority |
| MCP scope override | Launch-fixed collection/branch | ScopeOverride / tool error; non-sensitive |
| Stale epoch stamp as delivery permit | Current control head only | Fence refuse; no sibling disclosure |
| Control-branch write via ordinary document cmd | Protected namespace rules | Typed refuse; durable position unchanged |
| Caller-asserted principal / Context counters | Credential resolve only | Ignore counters; authn failure if credential missing |
Preserve existing owner-only operation limits of the executable local slice until sprint 85 replaces them under accepted semantics.
Linearization#
Explicit order: validate framing -> authenticate session -> authorize principal/store/branch/action/object/field/version -> perform -> current-authority delivery fence -> emit bounded bytes. Revocation cannot retract plaintext already received. Cross-branch atomicity is not promised; fence/order instead.
Proposed source leases (implementation follow-on; not this unit)#
| Seam (observed path) | Role |
|---|---|
Listener credential registry (src/transport / service registry) | Authn only; not delivery policy |
Application can_write / principal (src/product application path) | Admission |
Control-branch authority epoch (branch 2 / control head) | Delivery fence head |
| MCP adapter scope (MCP launch pin) | Launch pin; never client override |
Future grant store on branch 2 | Sprint 85+ |
| Exact loopback send-permit file | Open gate #3 — not leased here |
Independent interleavings to prove later (cite W09 / SPRINT-40 schedules):
Revoke-before-
proposal_preparevs committed-receipt replay after later revokeDelivery-fence drop of queued bytes after post-commit revoke
revokeAllReadersresolve-null vs owner retained (authn schedule)Listener startup credential copy vs on-disk rewrite (HTTP 401; authn-only)
Restore credential rotation (W16) — old token never admits
Open gates (do not invent)#
Clock/expiry/restart rules for grants (wall vs monotonic)
Delegation cycle depth and whether agents may edit spending ceilings
Exact transport send-permit hook site (entry vs loopback send)
Creator handoff without implied decryption
Independent security review assignment before sprint 85 dispatch
Relation#
Does not replace ubi-bridge trusted-owner first proof. Does not authorize browser “owner can do everything” shortcut. Implementation is sprint 85 after accepted authority semantics and registry assignment. No new service route or credential model is implied by this freeze alone.