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.
| 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.
| - ***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.
| - **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.
| | 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.
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.
| 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.
|
|
||
| - ***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.
| - **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).
| - 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.
| 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.
| **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.
| **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
|
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 (249 Markdown files checked)typos docs/rfcs/2026-10-11-actor-views-and-subgraph-sharing.md docs/rfcs/README.md: no findings6e31d0ab, and every in-file anchor resolves.Notes for reviewers
Contract: non-interference. A restricted actor's answer to any query equals the answer on a graph that physically holds only what that actor may see. This covers counts, aggregates, traversal, search ranks and errors. A single visibility door enforces it, and a differential test proves it for every operator.
Model. There are two roles:
The constructs, in field-standard names:
@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 (
organization,bypass) andlimits. No new Cedar action is needed.Three properties set the design apart:
Decisions D1–D8. D8 settles "facts, not queries", and Appendix C compares the model with query-based access, in both expressiveness and efficiency. Data-derived rules enter through four routes:
@starts;Scale. Readable sets use a Leopard-style shared closure with incremental maintenance, so a connector sync does not rebuild every actor's view.
Sharing adds no access concept: a recipient is a principal outside the organization group.
Phases. Phases 0–10, each with work items and an exit criterion.
Appendix A records each code and Lance 11 assumption the design relies on, verified against source with a verdict and evidence. The corrections that matter most:
@indexon a String builds a full-text index, not a BTREE.actor-views.Predicate::gq_filters()drops every variant exceptGq, andMetadataCountignores the scan filter.main's manifest pins for the access tables.Alignment with #884 (polymorphic types). Abstract member sets come from the actor's schema view, which replaces #884's
policy_denied_memberrefusal andpolicy_filteredmarker withview.restricted. #884's Policy section still needs the matching amendment; this is unresolved question 8.Acceptance is blocked on two measurements (
blocked_on):