Repository navigation
Conversation
Draft RFC for fine-grained read and write permissions (type, property and row level) and for sharing parts of a graph with external parties. Defines the non-interference contract, the schema annotations and policy block, behaviour for every read and write surface, decisions D1-D5 with evidence, and implementation phases 0-10. Appendix A records every code and Lance 11 assumption the design relies on, checked at 6e31d0a.
Abstract member sets come from the actor's schema view instead of the polymorphic types draft's policy_denied_member refusal; completeness is signalled by view.restricted with an opt-in @complete refusal. Security annotations on interface properties cannot be dropped or added per member, removing one by any migration route is declassification, @secured_by may name an interface of securables, and authorization edges may use polymorphic endpoints.
Collapse the declaration layer into one concept, the scope: seven annotations (@principal, @scope, @member, @grant, @deny, @Within, @in) plus @expires, a version-2 policy block holding only unrestricted and limits, and no new Cedar action. Sensitivity classes become scope rows, the everyone principal becomes the everyone scope, scope keys share one namespace, fixed placements become checked references and access data is itself placed. Decisions are renumbered D1-D7, the rollout is re-cut into phases 0-9, and Appendix B maps each part of the model to prior art.
ragnorc
left a comment
There was a problem hiding this comment.
Recommendation: request changes to the design before acceptance. I am posting a COMMENT because the authenticated account is the PR author. These findings concern proposed behavior; this PR changes documentation only.
Today a reader can see a whole graph branch or none of it. This RFC would let people and agents query only their allowed types, columns, rows and edges. Ordinary graph nodes represent scopes. Memberships and grants build the readable set; denies and containment narrow it. The engine would apply that set to every read and write. A recipient principal would also define a live share or a copied subgraph.
The inline findings identify five contract gaps: principal reassignment, access/data capture order, private data in “public” BM25 statistics, hidden-data dependence in approximate search, and constraint outcomes that disclose more than the stated exceptions. Each comment gives a concrete affected case and the contract that needs to change.
Tradeoffs and long-term cost. Keeping access facts in the existing manifest publication is a sound direction. It avoids a second authorization database and its cross-store consistency protocol. A fixed relation vocabulary and typed view predicate can compose across operators. Cache entries keyed by immutable access pins are also preferable to invalidating on unrelated content writes.
The proposal adds substantial obligations: global identity constraints, policy format 2, actor-aware engine entry points, view-bound plan acceptance, search scorer construction, expiry-aware caches and, later, copy reconciliation. These costs are justified only when one capture and validation contract covers every caller. Five extensions that each add their own search or replay exception would multiply security maintenance. Start with the smallest complete contract: protected identity bindings, coherent captures, fixed safe scoring and exact restricted vector search. That removes the identified leak classes without new authoritative state. It can cost more CPU for large visible sets; the stated measurements must decide whether that cost is acceptable before adding acceleration.
Alignment. I checked the other 29 open PRs for overlap. The material coordination points are #839/#815 (plan acceptance, replay and diagnostics), #792/#793 (scoring and exact/approximate retrieval), and #884 (polymorphic members). Put view requirements into the same accepted-plan boundary. #884's policy amendment is already acknowledged here. Also resolve the scoring contract with #792: it defines statistics over policy-visible entities, while this draft uses public/uniform statistics and compares results with an unrestricted materialized graph. Those definitions do not in general give equal scores or ranks.
I checked current upstream direction as well. Lance #9798 adds search-effort control, and #9847 adds an external indexed input to hybrid FTS. Both are merged upstream, beyond this PR's stock Lance 11 pin. They may reduce future adapter work. Neither establishes the privacy guarantees in this RFC. Keep the pin-specific guards and reassess the adapter at an upgrade.
Validation and limits. Reviewed detached head 5ae580272a54167bc961dd76b23ff50a8224724c in a clean isolated checkout. The diff from merge base 6e31d0ab42c8e2eb6cb38090ab197e1cca7e27df is only the RFC and registry row: 1,570 added lines.
/tmp/review-916-docs/bin/python scripts/check-docs.py: passed, 249 Markdown files.bash scripts/check-agents-md.sh: passed, 54 links and 50 docs.typos docs/rfcs/2026-10-11-actor-views-and-subgraph-sharing.md docs/rfcs/README.md: passed.git diff 6e31d0ab42c8e2eb6cb38090ab197e1cca7e27df...HEAD --check: passed.- CI on the exact head is green. The GQT workflow skipped its test steps because no GQT input changed; this is no runtime proof.
The findings use the RFC's set rules and inspection of actual engine capture, mutation, validation and search callers, plus pinned Lance implementation and tests. I read the repository guides and relevant complete upstream storage/search pages. No actor-view runtime exists on either commit, so a before/after GQT or Rust regression cannot prove this proposal yet. No source or test edits were made. Add the small identity, hidden-statistics and constraint cases to the planned actor-capable GQT owner, and the capture interleaving to DST when implementation lands. No scale, recall, ranking-quality, S3/Azure or latency measurements are claimed.
| - These edges, and `@principal` properties, are written only on `main`. A branch | ||
| write or a merge that touches them is refused. Without that rule a branch | ||
| forked before a revocation would bring the revoked access back when merged. |
There was a problem hiding this comment.
[P1] Authorize principal binding changes, not only their branch
The owner rule above applies to access edges, but the only rule for @principal is “main only.” In the sample, Person is unplaced and actor is nullable. A restricted actor with change on main can clear their own binding, then put their actor id on an existing privileged Person. Uniqueness still holds; the next read resolves that Person and inherits its memberships and grants. No access edge changed. The current mutation gate checks the branch action, not authority to assign identity. Define one protected binding lifecycle for create, replace, clear, delete and keyed upsert, enforced in the shared mutation/load validation. Restricting these operations to unrestricted is a small initial rule using an existing primitive. Include this takeover case in the planned actor tests.
There was a problem hiding this comment.
Addressed in 7f3d601 and 90f3ec0. A @principal value is now an identity binding (Identity bindings, D9). Assigning, changing or clearing one is a bypass-only write on main. Every other actor is refused with identity_binding_denied in the shared attempt validation of mutation, load and merge. Evidence includes this takeover case: a restricted writer moving its actor id onto a privileged principal.
| A read captures two things under one schema permit, in this order: | ||
|
|
||
| 1. ***Authorization state***: the applied policy digest and the `__manifest` | ||
| pins on `main` of every ***access table*** (each scope type, each principal | ||
| type, each `@member`, `@grant`, `@deny` and `@within` edge type), each pin | ||
| being the table's identity, published dataset version, staged version and | ||
| transaction id. | ||
| 2. ***Data snapshot***: the read target, as today. |
There was a problem hiding this comment.
[P1] Capture authorization no earlier than the data it protects
The listed order permits: capture access A; commit a revocation; commit new content; capture data B. The request then exposes B using pre-revocation access. The shared schema permit does not exclude these writes: commit_all takes the shared side too. This race exists in the supported single-writer process. A version probe alone does not fix it unless its result replaces or validates the authorization capture after the data cut. Specify one manifest image for both on main, and a capture/revalidation order for branch and historical targets that proves authorization is at least as recent as the protected data. This is the ordering requirement in Zanzibar §2.2. The DST exit case should force this interleaving; testing against the request's already-stale authorization state would miss it.
There was a problem hiding this comment.
Addressed in 7f3d601 (Capturing a view).
- Reads on
maintake authorization and data from one__manifestimage. - Branch and historical reads capture the data first and
main's authorization after it, under one schema permit. - Writes on
mainmake the access pins expected versions of the publication's compare-and-swap.
90f3ec0 states the one remaining window, a branch write racing a revocation on main, in the contract, What is not promised and the Phase 3 exit. Evidence has a DST schedule for the read race you describe.
| - ***Public statistics***: corpus statistics over rows placed in scopes granted | ||
| to `*`, built by explicit index reconciliation and stored as derived state | ||
| keyed by index identity and the everyone scope's grants. |
There was a problem hiding this comment.
[P1] Do not classify a wildcard grant as a public statistics corpus
The worked example already supplies a counterexample: space:acme-digest is granted to *, but Alice cannot read it because of @within. Its documents would nevertheless enter this statistics artifact. Changing their terms can change Alice's scores and ranks over unchanged visible rows. Denies, additional placements and delegated caveats cause the same problem. Lance's MemBM25Scorer uses the supplied document frequencies and average length; row prefiltering cannot remove this influence. Also, index identity plus everyone grants does not track a new containment restriction or denial. Use the fixed uniform source initially, or define and prove a statistics population safe for every consuming view, including invalidation on all facts that establish that property. Ranking-quality measurements alone cannot validate this confidentiality condition.
There was a problem hiding this comment.
Addressed in 7f3d601: public and uniform statistics are withdrawn (D10). Restricted bm25 runs Lance's flat operator over exactly the visible rows with a zero base scorer. The statistics are therefore those of the actor's view: visible rows with nonempty analyzed text (90f3ec0). A grant, deny, placement or caveat elsewhere cannot change a restricted score.
| - **Indexed**: otherwise use the IVF index with probes set up front to about | ||
| `k · partitions / (s · N) × 2`, where `s` is the visible fraction. | ||
| - **Fallback**: if that probe count exceeds half the partitions, use exact. | ||
|
|
||
| The existing overfetch and exact-pass loop | ||
| (`crates/omnigraph/src/engine/mod.rs:473-555`) remains the safety net. Whole-table |
There was a problem hiding this comment.
[P1] Keep shared ANN state outside the strict non-interference contract
Above the exact threshold, the proposed IVF path can change visible results when only hidden vectors change. Lance trains centroids from dataset rows, selects partitions without the prefilter, then filters candidates within those partitions. Hidden rows can therefore change routing and which visible neighbors are found. Returning k visible rows does not prove the same top-k: Lance stops widening once it has k, and OmniGraph's exact-pass ladder runs for short answers. A full but different answer bypasses both safety nets. This violates the RFC's equality over rows and ranks, even at 95% recall, and propagates into fusion. Use exact search for restricted views until acceleration has a proof under the same observable contract, or explicitly revise the promised disclosure boundary. Add a paired-data acceptance case with identical visible vectors and different hidden vectors; recall against ground truth alone does not test non-interference.
There was a problem hiding this comment.
Addressed in 7f3d601: restricted nearest on a placed type is always exact. It runs use_index(false) with the view conjunct prefiltered, is bounded by max_exact_search_rows and is refused above that bound. Restricted actors get no IVF path. Accelerated restricted search is Phase 8, and it returns only with a proof under the pair contract.
| | Edge insert | Both endpoints must be visible; a hidden endpoint fails exactly as a missing one. | | ||
| | `load --mode overwrite` | Refused when the actor's view of any type in the batch is restricted. | | ||
| | Branch merge | Refused unless every row in the merge diff is writable by the actor. Conflicts are reported without entity ids. | | ||
| | Constraint violations | Reported by constraint and property name only. `@unique` omits the other holder's id; `@card` omits the count. | |
There was a problem hiding this comment.
[P2] Account for the constraint outcome itself as a disclosure
Removing the other holder's id does not close this channel. Give a placed type an opaque slug @key and email @unique. A writer inserts a fresh slug in an editable scope and guesses an email. Success versus @unique failure reveals whether that email exists in a hidden scope, although the guessed slug did not exist in either case. This is outside the stated key-existence exception. Maximum-cardinality checks can similarly reveal hidden edges during insertion, outside the delete exception. The current constraint validator enforces these constraints globally, so redaction cannot solve the general problem. Define the allowed disclosure for every global constraint, or restrict constraints/writes whose integrity check requires hidden facts. Carry that decision through mutation, load and merge, and add paired visible/hidden constraint cases to the planned tests.
There was a problem hiding this comment.
Addressed in 7f3d601 and 90f3ec0. Constraint disclosures now lists every write check whose outcome can depend on hidden rows or hidden schema, with what it may reveal and what is withheld:
@key;@unique, which covers your opaque-slug and guessed-email case;- the
@cardmaximum and minimum; - cascade on delete;
- branch merge.
The lint flags human-meaningful keys and @unique properties. Uniqueness scoped to a placement, and cardinality over visible edges, are open questions.
Split the scope concept into @group and @securable roles, add @inherits so audience expressions are closed under union, make denial cut membership through a denied group, and replace the public * subject with a policy-defined organization group so recipients never receive company-wide grants. State the semantics as a least fixed point with one negation layer and derive every construct. Adopt field names (@securable, @requires, role, bypass, recipient), add @starts, edge placement, computed placement and incremental readable-set maintenance, and compare the model with query-based access in D8 and Appendix C.
…issions-sharing-e4c4af # Conflicts: # docs/rfcs/README.md
The polymorphic types draft now adopts this RFC's rules for abstract types and stores edge endpoints as packed node references from integer node identity. Describe that design instead of tag columns, close the cross-RFC question, and record typed placement values under integer identity as an open question.
| `materialize(V(a))` is a graph containing exactly the types, properties, rows | ||
| and edges `V(a)` admits. The equality covers rows, order where the query orders, | ||
| counts, aggregates, search ranks, errors and the result schema. It holds at the | ||
| snapshot the read targets, under the authorization state captured for the | ||
| request. |
There was a problem hiding this comment.
[P1] State non-interference so that restricted search can satisfy it
This equation says a restricted result equals a bypass result on materialize(V(a)), and the equality includes search ranks. On that materialized graph, a bypass actor uses index-wide BM25 statistics (line 940). Those are the statistics of the visible rows only. A restricted actor uses uniform or public statistics (lines 919-933). These give different scores, and they can give different ranks.
Example: the query is acme merger, and two visible messages have equal length. Message 1 contains acme three times. Message 2 contains merger two times. In the view, acme is common and merger is rare. View statistics give merger the high IDF, so message 2 ranks first. Uniform statistics give every term the same IDF, so term frequency decides and message 1 ranks first.
Thus Phase 2 ships uniform statistics but cannot pass its own exit gate (lines 1539-1541) or the planned owner (proptest_equivalence, line 1394). Indexed nearest has the same problem: on the bypass side, an IVF index trained on the materialized rows gives different approximate neighbours.
The security property you need is narrower: the answer depends only on the view. Please state it for a pair of graphs: if V_G(a) = V_G'(a), then answer(q, G, a) = answer(q, G', a). Keep the materialized equation as the oracle for operators whose result is a function of rows only (scans, aggregates, traversals, exact nearest). For bm25, indexed nearest and rrf, define restricted scoring as a declared function of the visible rows and fixed constants. The alternative is view statistics. They make the equation exact and match the "policy-visible entities" rule in #792, but their per-query cost needs a measurement.
In both cases, the differential test must build pairs of graphs that differ only in hidden rows. That pair test is also the acceptance case that the open ANN thread asks for.
There was a problem hiding this comment.
Addressed in 7f3d601. The contract is now stated over pairs of graphs with equal views. The materialized equation is the test oracle, evaluated with the same evaluators. Restricted search scores from view statistics (D10), so both forms hold for every operator offered to restricted actors.
| fallback when the artifact is missing or stale. The choice is gated on ranking | ||
| measurements (`blocked_on`). The flat path adds its own filtered input rows to a | ||
| supplied scorer (`lance-index-11.0.0/src/scalar/inverted/flat_search.rs:548-559`); | ||
| because the engine filters by expression, those rows are visible rows, which |
There was a problem hiding this comment.
[P1] Re-anchor restricted full-text search on main's BM25 eligibility rule (#839)
Two facts on main (0b5e17fa) change this section.
-
The view conjunct would run after scoring. feat(planner): accept every read plan against its checked query (RFC 0047) #839 merged after this head. Its planner puts a BM25 scan's whole eligibility (pushed filter, gate set, search members) after scoring when full-text coverage is partial (optimizer.rs:2589-2603). The scan then sets
prefilter(false)(scan.rs:268). This RFC makes the view conjunct an ordinary pushed filter. On a table with an uncovered tail, Lance would then score with whole-corpus statistics, hidden rows included, and drop hidden rows only afterwards. That is the leak this section exists to close. The statement at lines 872-875 ("prefilter(true)on every scan with a filter") is no longer true. -
Restricted scores would depend on index coverage. The flat path adds its counted input to any supplied scorer (
initialize_scorer,lance-index-11.0.0/src/scalar/inverted/index/flat_search.rs:539-574; the cited pathinverted/flat_search.rsdoes not exist). An indexed segment with a base scorer uses that scorer only. So a visible row gets uniform statistics when its fragment is indexed, and uniform plus visible-tail statistics when it is not. An optimize run that changes no data can then change restricted scores and ranks. That violates invariant 7 ("missing coverage may change cost but not correctness"). It is the same defect class that feat(planner): accept every read plan against its checked query (RFC 0047) #839 fixed for unrestricted reads. The main guardfts_prefilter_changes_unindexed_scores_and_postfilter_keeps_thempins the Lance fact: a prefilter changes an uncovered row's score, and an indexed row always scores from the index's statistics. Upstream Lance main (v14 betas) has the sameinitialize_scorer, andwith_shared_scoreris stillpub(crate), so a Lance bump does not remove this.
Please:
- give the view conjunct its own eligibility: always before scoring, never moved by the user-filter rule, and checked at plan acceptance;
- specify a restricted scoring path on Lance 11 whose scores depend neither on coverage nor on hidden rows, and add a surface guard for it to the table at line 1172.
A refusal based on coverage is not a valid fix. The deny-list forbids a logical precondition on physical index coverage.
There was a problem hiding this comment.
Addressed in 7f3d601. The view conjunct has its own eligibility, always before scoring, independent of the user's filter eligibility (Scans, Plan acceptance). Restricted bm25 never reaches the prefilter(!(filter_after_scoring && ...)) path. It runs the flat operator over the visible rows, with the user's eligibility after scoring. No postings are read, so index coverage cannot change a restricted score. Appendix A records the AfterScoring fact on main.
|
|
||
| - ***Public statistics***: corpus statistics over rows placed in securables | ||
| granted to the organization group, built by explicit index reconciliation and |
There was a problem hiding this comment.
[P1] Public statistics now include rows that recipients and other restricted actors cannot read
The earlier thread on wildcard statistics still applies. The change from * to the organization group adds two direct counterexamples from this RFC:
- Recipients. A recipient is never in the organization group (line 572). But a recipient is a restricted actor, so it gets public statistics (line 919). Those statistics come from exactly the rows that recipients must not see. A change to the terms in
channel:generalthen changes the scores and ranks thatclient:globexgets over its unchangedfolder:globex-sharedrows. The rows stay hidden, but information about them flows to the recipient. - Embargo. Lines 526-528 put an embargoed release in a securable that is granted to the organization with
@starts. Before the start time, no actor can read it. But the artifact is "keyed by index identity and that group's grants", without time. So the release text enters every restricted actor's statistics before the release.
A deny on an organization-granted securable, an additional placement, @requires and a caveat give the same result. A grant from the organization group does not prove that a row is in every view that consumes the statistics.
Please make the design statistics a function of the consuming view only (see the comment on the contract at line 119). Remove public statistics, or define a population that is provably inside every consuming view. Its invalidation must cover every fact that establishes that property, time windows included. Ranking measurements (blocked_on) cannot validate this confidentiality condition.
There was a problem hiding this comment.
Addressed in 7f3d601: public statistics are withdrawn (D10), for exactly these two reasons, recipients and embargoed rows. Restricted bm25 now scores from the actor's own visible rows only. See the reply on the earlier statistics thread.
| - **The organization group.** A `@group` row keyed `organization` is the | ||
| ***organization group***. Its members are implicit: every actor in the cluster | ||
| policy groups listed under `views.organization`. A grant from it is a | ||
| company-wide grant. Recipients and other outside principals are never in | ||
| those policy groups, so no company-wide grant reaches them. |
There was a problem hiding this comment.
[P1] Make the organization group's membership come only from policy
Line 678 and the Authority table make the organization group a root of trust that policy defines. The rules do not enforce that:
- Step 1 of Computing access (lines 303-304) follows every live
@memberedge fromp. Nothing refuses an edgeclient:globex -member→ organization, or a member edge intoorganizationfrom a group that contains a recipient. With such an edge, "no company-wide grant reaches them" (lines 258-259) is false, and so is the Phase 1 exit criterion (line 1517). - The row is found by the key
organization. The RFC does not say which@grouptype holds it, who may create or delete it, or what happens when two group types each hold that key. Only securable keys share one namespace (line 249). - Suppose the row is in a type that is also a securable, such as
Team. Its creator writes the owner grant (line 477). The creator can then write@memberedges into it (line 471) and@denyedges on it. A deny on it removes the denied subjects from the organization (step 3 never enters a group inD), and thus from every company-wide grant.
Please make this structural:
- the organization group has no explicit incoming
@memberedges and is never a@denytarget, and write validation refuses both; - its key is reserved across every
@grouptype; - only bypass actors create or delete it.
Add a case with a recipient that has an explicit member edge into the organization to the generated access graphs (line 1418).
There was a problem hiding this comment.
Addressed in 7f3d601 and 90f3ec0.
- The group is now the internal group, keyed
internal, and the key is reserved across every@grouptype. - Its members come only from
views.internal. - Write validation refuses an explicit
@memberedge into it and a@denyonto it. - Only bypass actors create it, on
main, and it is never deleted.
The rename also keeps it apart from the managed control plane's organization, whose members include invited outside collaborators.
| - Access edges and `@principal` properties are written only on `main`. A branch | ||
| write or a merge that touches them is refused. Without that rule a branch | ||
| forked before a revocation would bring the revoked access back when merged. | ||
| - A new securable row must be written together with an `owner` grant into it | ||
| from a subject in the writer's membership closure; otherwise the write is | ||
| refused with `securable_owner_required`. |
There was a problem hiding this comment.
[P1] Protect securable keys and actor ids as identity, not as free text
The earlier principal-binding thread is still open. This rule still controls only the branch of a @principal write. It does not say who may assign, move or clear an actor id. The new model adds the same gap for securables. A placement names a securable by a string key. This rule lets any writer with change on main create any securable key that does not exist yet, and become its owner.
- Squatting. A writer creates
Channel { slug: "slack:C777" }with an owner grant before the connector mirrors that private channel. The bypass connector's keyed insert then upserts the row. The writer's owner grant stays, so the writer reads every message that the connector puts there. A non-bypass connector getskey_conflictinstead and cannot mirror the channel. - Reuse. Delete of a securable is refused only when a fixed
@innames it (lines 266-268). Delete of an unplaced securable such asPersonneeds onlychange. Rows placed per row keep the deleted key, and whoever creates that key next gets those rows. Example: aPersonpersonal securable keyed by email is deleted at offboarding. A new hire later gets the same email. Throughrole(s) ≥ viewer if s ∈ C, the new hire reads the private rows of the earlier person.
This is the rule of invariant 6 ("never infer identity from an alias, path, … or name"), applied to access identity. Please define one protected lifecycle for every identity binding (actor id, securable key, organization row), enforced in the shared mutation, load and merge validation. For example:
- refuse a restricted writer's create of a securable whose key a placement already names;
- refuse delete of a securable while a placement names it, and define who may delete an unplaced securable.
The per-row @in BTREE makes each check one IsIn count per placed type.
Unresolved question 8 then is also a security question, not only a cost question. Packed references from the integer node identity draft do not appear to reuse an id (that draft refuses an id below the reserved frontier), so they would remove the reuse case.
There was a problem hiding this comment.
Addressed in 7f3d601 and 90f3ec0. Actor ids, securable keys and the internal group share one protected lifecycle (Identity bindings, D9):
- Only bypass actors create securables or assign actor ids, and only on
main. - Keys never change.
- In the first scope securables are never deleted, so no key ever names a second securable.
Squatting, takeover and reuse are all Evidence cases. Restricted creation and deletion return with packed references (Phase 6).
| narrow: a list of securables, a list of types, an action subset and an expiry. | ||
| A caveat's securable list covers the listed securables and every securable that | ||
| inherits from them. The effective readable set is |
There was a problem hiding this comment.
[P3, optional] Caveat coverage through inheritance also covers derived content from other sources
digest:acme inherits channel:deal-acme and requires channel:general. A caveat that lists only channel:deal-acme covers digest:acme by this rule. So an agent that is limited to the deal room reads summaries that also derive from channel:general. The result stays inside R(user) ∩ R(agent), so nothing goes past the user's own view. But it is wider than the caveat says. Consider covering an inheritor only when every one of its @requires targets is covered too, or state the current rule as intended.
There was a problem hiding this comment.
Taken in 7f3d601: a caveat covers an inheritor only when every one of its @requires targets is covered too. Evidence has the case.
| **Coverage validator.** A new check runs in `engine::execute`, beside | ||
| `validate_aggregate_specs` and `validate_output_schemas`, so every door including | ||
| replay passes it (`crates/omnigraph/src/engine/mod.rs:409-418`). It refuses a | ||
| plan that: | ||
|
|
||
| - scans a restricted placed type without its view conjuncts; | ||
| - uses `MetadataCount` on a restricted type (its lowering ignores the scan filter, | ||
| `crates/omnigraph/src/engine/lower.rs:473-487`); | ||
| - uses the CSR, the `count`/`not` degree shortcut | ||
| (`crates/omnigraph/src/engine/lower.rs:230-277`) or the CSR-derived search gates on | ||
| a restricted type; | ||
| - expands into a restricted type, or over a placed edge type, without a mask. | ||
|
|
||
| The bound plan's assumptions gain the view digest. Replay recomputes the view | ||
| for the replaying actor and refuses if the digest differs, so a saved plan | ||
| cannot be replayed by another actor or after a revocation. |
There was a problem hiding this comment.
[P2] Put the view requirement into the plan-acceptance door that is now on main
#839 merged RFC 0047 plan acceptance. accept_query and accept_replay are now the one door from a plan to execution (validate/mod.rs, replay.rs:178-215). Replay compares catalog_digest, which is the schema_ir_hash of the bound schema (replay.rs:220-225). A second check in engine::execute plus a separate view digest in plan assumptions would make two acceptance doors that can drift.
Two details also need a decision:
- A view catalog that keeps
CatalogIdentity::Bound(schema)produces the full schema hash. TheIncompatibleFactsrefusal prints both hashes. A change to a hidden type then becomes visible through replay. The Surfaces table does not list replay envelopes or replay refusals. - The view digest is per actor. So it can be the identity of the view catalog itself, and the existing replay comparison then refuses another actor's plan with no new mechanism.
Please express the coverage rules as acceptance Requirements, and make the view digest part of the catalog identity that accept_replay checks.
There was a problem hiding this comment.
Addressed in 7f3d601 (Plan acceptance).
AcceptInputcarries the view spec, and view requirements are checked inRequirements::check.- The view digest joins
catalog_digest, recomputed from the replaying request's own view.accept_replaytherefore refuses another actor's plan, or a plan whose view changed, with no new mechanism. - For restricted actors, refusals from
accept_replay,plan_pins_snapshotandreplayed_coverage_holdsare redacted.
| **Placement on interfaces.** Under that RFC, property annotations on an | ||
| interface property apply to every implementor that inherits it. A fixed | ||
| `@in("key")` on an interface property follows that rule with two additions: a | ||
| member that redeclares the property may not drop it (a refusal, not that RFC's | ||
| lint warning, because dropping it would expose the column), and a member may not | ||
| add a fixed `@in` to an inherited property, so the property's visibility is the | ||
| same for every member. Type-level `@in` belongs to concrete types and is not | ||
| inherited through `implements`. |
There was a problem hiding this comment.
[P3, optional] Two statements about #884 are now stale
- rfc: polymorphic types with interfaces, unions and polymorphic edges #884 at
d9caeb69refuses a redeclaration that drops an inherited placement itself, and it refuses a member that adds a fixed placement to an inherited property (its2026-10-07-polymorphic-types.md, Interfaces). "A refusal, not that RFC's lint warning" no longer describes it. - rfc: polymorphic types with interfaces, unions and polymorphic edges #884's Policy section says an interface-wide mutation "checks every arm against the actor's view before staging". This RFC checks writes in end-of-attempt validation and again in the
commit_allrevalidation (lines 972-982). Please align the two texts on one enforcement point. The later point is the correct one, because it sees cascades and the committed before-image.
Review of
|
…issions-sharing-e4c4af # Conflicts: # docs/rfcs/README.md
Address the review findings on the actor views RFC, align with main at 9575b04 and with the open drafts that touch access. - State non-interference over pairs of graphs with equal views; keep the materialized equation as the test oracle under identical evaluators. - Make restricted search exact and scored from the view: bm25 through Lance's flat operator over the visible rows with a zero base scorer, nearest through the exact route. Withdraw public and uniform statistics. - Give the view conjunct its own eligibility, always before scoring, and move view requirements and the view digest into plan acceptance. - Capture authorization no older than the data; make access pins compare-and-swap expectations for writes. - Protect identity bindings (actor ids, securable keys, the internal group) with one lifecycle; rename the company-wide group to the internal group. - State constraint disclosures exhaustively, fix caveat coverage, and cover direct and served access, MCP, unpublished commits, unattached snapshots, the error registry and every surface. - Ask acceptance for Phases 0-3 and re-cut the later phases. - Cite code by file and symbol at 9575b04.
Review of
|
| access tables are not part of, so the compare-and-swap cannot fence a | ||
| revocation on `main`. Such a write is authorized against the authorization | ||
| state captured in its attempt; a revocation published on `main` during that | ||
| attempt can miss at most that attempt. Every later read applies the current |
There was a problem hiding this comment.
Blocking. This one-attempt miss disagrees with the write contract and with the Phase 3 exit.
Lines 149–152 say a write succeeds only when every touched row is writable in the actor's view. This paragraph says a revocation on main during a branch attempt can miss that attempt. Lines 1869–1872 say a write that races an access change in another process retries or refuses. "What is not promised" does not list the window.
commit_all takes the schema permit shared, then the attempt branch's gate (crates/omnigraph/src/exec/staging.rs:973-979). The comment at staging.rs:942-943 says the manifest compare-and-swap is the foreign-process authority. Appendix A records the same fact (line 2038). A per-branch gate does not serialize a main revocation against a feature-branch commit. ExactGraphHead fences a publish on main because an access change moves main's head. It does not fence another branch's chain.
This is invariant 3: one attempt uses one coherent authority, and a retry starts fresh. The class is every publication whose chain is not main. Put one rule in the contract, in this section and in the Phase 3 exit:
- If acceptance allows the miss, add the window to "What is not promised" and state it in lines 149–152 and in the Phase 3 exit.
- If acceptance refuses the miss, re-read
main's access pins at branch commit under a gate that also holdsmain's branch gate, and write the lock order down. A revocation onmainthen waits until the branch commit finishes. KeepExactGraphHeadfor publishes onmain.
There was a problem hiding this comment.
Addressed in 90f3ec0 by stating the window rather than closing it. No compare-and-swap spans main's chain and a branch's chain. Holding main's gate in commit_all would be a process-local lock that fences nothing across processes, which the deny-list rejects.
- The write contract now says a write is checked at its attempt's captured authorization state. On
mainthat is also the state at publication. - What is not promised states the one-attempt window and its bounds: the write deadline, rows landing only on that branch, every later read applying the revocation, and a merge into
mainre-checking the rows for the merging actor. - Capturing a view cites the gate order in
commit_all. The Phase 3 exit and a DST case test exactly that window.
| Refusals carry `identity_binding_denied` or, for a named securable, | ||
| `securable_in_use`. With these rules a restricted writer cannot take over | ||
| another person's identity by moving an actor id, cannot squat a securable key | ||
| before a connector mirrors it, and cannot inherit rows by re-creating a deleted |
There was a problem hiding this comment.
Blocking. The delete check covers one committed state, so a recreated string key still inherits branch rows.
Line 360 says a recreated key inherits no rows. The check at lines 983–995 reads the committed state of the write: one IsIn on each per-row placement BTREE, plus each fixed @in. Placement values also live on branches (line 884). Ordinary branch reads are in Phases 0–3 and follow the branch's placements (lines 681–682). Securable creates are outside the main-only list, which covers access edges and @principal (lines 575–576).
A bypass offboarding can clear the placements on main, delete the securable and create a new securable with the same string. IsIn on main sees no placement. A branch forked earlier still stores rows whose @in is that string. A reader of the branch resolves the string through the new securable and sees the old rows.
The class is every string placement that outlives the securable row: another live branch in the first scope, and any snapshot a later publish or a Phase 9 read can still open. Invariant 6 says a dropped identity is not the next row that reuses the name. D9 adopts that rule for securable keys. The check belongs on the identity lifecycle and has to see every snapshot the accepted scope can read.
State all three of these:
- Securable rows and the internal group are written only on
main, with the access edges. - Delete is refused while any live branch still stores a placement that names the key.
- A branch read resolves a placement through the captured securable row. A key that was deleted and created again is a new securable, so the old branch rows stay hidden. Phase 6 packed references are the durable form of that rule. Until they land, the first scope can claim "a recreated key inherits nothing" only when branch reads already follow that resolution, or when create-after-delete of the string is refused while any such branch remains.
There was a problem hiding this comment.
Addressed in 90f3ec0. A delete-time check cannot see every branch chain or retained snapshot, so the first scope never deletes a securable.
- A securable is retired by deleting its access edges and moving or deleting its rows. Its key-only row stays.
- Securable, group and access rows and
@principalvalues are written only onmain. - No key ever names a second securable, on any branch or snapshot.
- Keys are immutable source ids. The example now keys
Personby identity-provider subject, and the lint flags human-meaningful securable keys.
Deletion returns with Phase 6 packed references.
| **Search across members.** That RFC admits `bm25` across members only under one | ||
| corpus contract, with the statistics source this RFC selects, through one engine | ||
| module shared by both RFCs, and refuses it until an upstream Lance scanner | ||
| option ships. This revision changes the source it names for restricted actors |
There was a problem hiding this comment.
Blocking. #884 does not yet name view statistics.
This sentence, and the decision log at lines 1938–1941, say the polymorphic-types draft now names view statistics for restricted actors. #884 at fca9e350 is open and was updated after this commit. Its search section still says index-wide statistics for bypass actors and view-independent (public or uniform) statistics for restricted actors (that draft, lines 567–575). Its policy section still checks an interface-wide mutation before staging (that draft, lines 761–762).
Lines 1349–1352 already ask #884 to name attempt validation and commit revalidation. The statistics sentence describes a settlement that the other draft does not contain. Accepting both texts would put the public-statistics leak back beside D10.
Correct this paragraph and the decision log: #884 still has to drop the public/uniform sentence, name view statistics, and name the same write point. The single-member flat operator here and #884's "needs an upstream scanner option" are a disagreement to write down in both drafts. #792 at c5d33c2931a1 already matches D10: policy-visible entities with nonempty analyzed values, and a graph filter does not rescope the statistics.
There was a problem hiding this comment.
#884 adopted view statistics in 98a4ed8 (2026-10-18), after this review, and now records the same plan split:
- The bypass route waits for the upstream scanner option.
- The restricted route reads no postings and builds its flat plan in the engine. Each arm's base scorer is the other arms' view statistics, collected through the public
flat_bm25_search_stream_with_options_and_scorer.
Its Policy section now names attempt validation and the commit revalidation (24b60eb). 90f3ec0 rewrites this paragraph and the decision log to match, and keeps multi-member bm25 refused for every actor until #884 admits it. #792's population, visible rows with nonempty analyzed values that graph filters do not rescope, is now stated explicitly.
| of scope for live access; they are one reason external parties get | ||
| materialized shares. | ||
| - **Write disclosures.** A writer of a placed type can learn, from whether its | ||
| write succeeds, exactly these facts and nothing more |
There was a problem hiding this comment.
Blocking. "Exactly these facts" leaves two channels out, and the @card row covers only the maximum.
card_violations reports the maximum and then the minimum (crates/omnigraph/src/validate/kernels.rs:494-515). The message names the source and the count (kernels.rs:578). Deleting the last visible edge fails when the global count would fall below min, and succeeds when hidden edges keep the count at or above min. Success versus failure reveals hidden edges. "Reached the bound" in the table (line 593) describes the maximum. State the minimum channel, or evaluate min and max on the visible edges where hidden edges are not required for integrity.
A fixed @in on a non-null property removes that property from the schema view, so a restricted insert cannot set it. resolve_assignments refuses a null on a non-nullable property before any batch and names the property (crates/omnigraph/src/exec/mutation.rs:19 and mutation.rs:32-36). The same insert succeeds on a schema with no such property. The error reveals a hidden column. Schema apply must refuse a non-null property with a fixed @in, or the property must be nullable and an omitted value must stay null with no distinct error.
A merge whose diff contains a row outside the view is the same class. Line 565 reports conflicts with no entity ids. That refusal has to be view_write_denied with no conflict payload.
The class is every global check whose outcome depends on hidden rows or hidden schema. Add each one to this list and to the table, or change the check so the outcome follows the visible rows.
There was a problem hiding this comment.
Addressed in 90f3ec0. The table is now stated as complete for write checks.
@cardminimum. It has its own row (on an edge delete) beside the maximum. The server strips the source and count fromcard_violations' message.- Hidden columns. Schema apply refuses a fixed
@inon a non-null property. A writer who cannot see the column leaves it null, and no error names it. - Merge. A restricted merge is refused with
view_write_deniedand no conflict payload unless every row changed on either side since the merge base is writable. That hidden rows changed is a listed disclosure. - Schema apply and maintenance. Their validations and reports read every row, so they need a bypass actor.
- State the one-attempt window in which a branch write can miss a revocation on main, in the contract, What is not promised and the Phase 3 exit: no compare-and-swap spans two branch chains, and the write gates are process-local. - Never delete a securable in the first scope, and write securable, group and access rows only on main, so no key ever names a second securable on a branch or in a retained snapshot. Key securables by immutable source ids; deletion returns with packed references. - List every write check whose outcome depends on hidden rows or schema: add the @card minimum and branch merge, make placed columns nullable, refuse a restricted merge with hidden changes without a conflict payload, and require a bypass actor for schema apply and maintenance. - Restrict bm25 statistics to visible rows with nonempty analyzed text, unaffected by the query's filters. - Align with the polymorphic types draft's 2026-10-18 corpus procedure.
…ition model Use the one cluster-backed serving model, the Omnigraph-If-Actor assertion and the disposition object, and settle a restricted writer's unknown outcome through disposition.check once that phase ships.
|
Recommendation: request changes before acceptance. This pull request changes documentation only. The findings are about the proposed behavior. What this revision doesWhy. The review of What. This revision states the one-attempt branch-write window, keeps a securable key for the life of the graph, extends the disclosure table, and follows the graph access RFC on serving and How. Impact. The window and the statistics agreement with #884 can stand. Three rules still name the wrong door, or they leave a path of the same class open. Earlier findingsFrom the review of
Optional items from that review:
Blocking findings
Optional findings
Tradeoffs and long-term costThis revision removes a cross-chain lock and a delete-time scan of every branch. That matches the deny-list and invariant 6. The cost is an explicit one-attempt window and a key row that stays. That is the right trade for the first scope. Approximate search stays deferred, so efficiency stays the quality this acceptance does not buy. The remaining cost is a second description of the write door. Alignment
Validation and limitsReviewed Local, on the detached head:
CI on this commit: CI Gate succeeded. Check AGENTS.md Links succeeded. Test Workspace, clippy, and rustfmt were skipped. GQ Logic Tests succeeded with the GQT steps skipped. DST succeeded with the pinned suite skipped. CI gives no runtime proof. No actor-view runtime exists on Lance claims were read in the pinned Lance 13.0.0 sources named above. I did not re-run the full-text guard test. No scale, recall, latency, S3, or Azure result is claimed. Inline comments
Blocking. Put this check in On
"A check before staging could not see staged placement values" describes
Blocking. Schema apply can still drop this identity.
Add two rows: schema apply refuses
Blocking. This sentence still gives every write the fence the window section withdrew. A branch publication's compare-and-swap does not include Invariant 10 still says direct and local access are storage-holder access. The serving table now has one cluster-backed server, including on localhost, and that server applies the view. Direct CLI and embedded access are the storage-holder path.
Blocking. A hidden column on a visible row still follows the full row.
Treat a change to a property outside the view as |
Review of
|
What & why
Adds a draft RFC, Actor views and subgraph sharing, for permissions finer than graph × branch × action, and for sharing chosen parts of a graph with external parties.
A company brain built from Slack, email, docs and CRM data has thousands of overlapping audiences. Today an actor can read all of a branch or none of it:
docs/user/operations/policy.mdsays "Per-entity and per-property authorization is not currently supported".Backing issue / RFC
Checklist
Local verification
python3 scripts/check-docs.py(in a venv fromscripts/requirements-docs.txt):Documentation OK (333 Markdown files checked)typos docs/rfcs/2026-10-11-actor-views-and-subgraph-sharing.md docs/rfcs/README.md: no findings9575b044(the 2026-10-20 additions also at7fd9cc90), and every Lance line anchor inlance-11.0.0/lance-index-11.0.0.Notes for reviewers
Scope of acceptance. The contract, the model and Phases 0–3 (hardening, the access model with type and column placement, row reads, row writes). Phases 4–11 (agents, incremental maintenance, packed placement references, data-derived and edge placement, search acceleration, view-filtered history and feeds, sharing, vaults) are designed to show the model extends, and each is accepted by amendment with its evidence.
Contract: non-interference over pairs. Two graphs that give an actor the same view give it the same answer to every query: rows, counts, aggregates, traversals, search scores and ranks, errors and the result schema. The materialized-view equation is the test oracle, evaluated with the same evaluators. Enforcement sits at plan acceptance for reads and at attempt validation plus the publication compare-and-swap for writes.
Model. Groups (sets of actors) and securables (nodes data is placed in), with a fixed vocabulary:
@principal@group,@member@securable@grantwith aroleof viewer, editor or owner@inherits@requires@deny@in@starts/@expiresCluster policy version 2 holds only the roots of trust (
internal,bypass) andlimits. No new Cedar action.Revision of 2026-10-20 (review of 2026-10-18):
mainmisses it is stated in the contract, What is not promised and the Phase 3 exit. No compare-and-swap spans two branch chains, and a process-local gate fences nothing across processes.main, so no key ever names a second securable on any branch or snapshot. Keys are immutable source ids; deletion returns with packed references.@cardminimum and branch merge are added;bm25statistics cover visible rows with nonempty analyzed text, unaffected by the query's filters (rfc: analyzed lexical search on the shared expression model #792's population).Omnigraph-If-Actor, thedispositionobject.Revision of 2026-10-17:
bm25runs Lance's publicFlatMatchQueryExecover the visible rows with a zero base scorer, so statistics come only from visible rows and neither hidden rows nor index coverage nor stale postings change a score. Restrictednearestis exact, because IVF partition choice ignores the prefilter. The earlier public and uniform statistics are withdrawn: one leaked, the other diverged from the oracle.main, partly indexedbm25moves user filters after scoring.Requirements::check, and the view digest joinscatalog_digest, so replay of another actor's plan is refused without a new mechanism. Replay refusals are redacted.main; data then authorization elsewhere; access pins as compare-and-swap expectations for writes.@requirestarget is covered.Alignment with open work.
24b60eba). Since98a4ed83it uses view statistics for restricted actors and records the same plan split. Multi-memberbm25stays refused for every actor until its scanner option ships.Blocked on two measurements: exact restricted search costs at target sizes, and readable-set construction at 10^5 principals and 10^6 securables.