Skip to content

feat(server): report what a credential may do through an access route - #1073

Merged
ragnorc merged 3 commits into
ModernRelay:mainfrom
ragnorc:feat/graph-access-route
Oct 19, 2026
Merged

ragnorc merged 3 commits into
ModernRelay:mainfrom
ragnorc:feat/graph-access-route

Conversation

@ragnorc

@ragnorc ragnorc commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What & why

GET /graphs/{graph_id}/access and the MCP access tool tell a caller what its own credential may do: its actor, the actions it holds graph-wide, and, given branch (and target_branch), the actions it holds there. Every answer comes from the same authorize() decision the routes make, so a client can learn its access without probing writes. It requires authentication like every graph route and no graph action; target_branch without branch is a bad request.

Backing issue / RFC

Checklist

  • Change is focused (one logical change)
  • Tests added/updated: auth_policy::access_reports_the_callers_own_actions_as_routes_decide_them, MCP parity rows and tool lists, OpenAPI expected paths
  • Public docs updated: policy guide ("Learn your access"), server route table, MCP tool table, the skill's server-policy reference; changelog.d/graph-access-route.added.md
  • Reviewed against docs/dev/invariants.md: the route reports decisions, never supplies an actor (invariant 10)

Local verification

  • RUST_MIN_STACK=16777216 cargo test -p omnigraph-server (after rebasing onto main 0a62bb7) — all pass
  • cargo clippy -p omnigraph-server -p omnigraph-api-types --all-targets --locked -- -D warnings -W clippy::dbg_macro — clean
  • cargo fmt --all --check, python3 scripts/check-docs.py — clean

Notes for reviewers

openapi.json is regenerated for the new path. The HTTP contract version is not bumped here; it moves once at release preparation.

GET /graphs/{graph_id}/access and the MCP access tool return the
caller's resolved actor, the actions its credential allows on the whole
graph, and, for a named branch and target branch, the actions allowed
there. Each action is evaluated through authorize with the branch and
target branch shaped as the route that performs it shapes them, so the
answer matches those routes' decisions. The route needs no action of
its own, reads no graph data and judges branches by name.
@ragnorc
ragnorc marked this pull request as ready for review October 18, 2026 20:30
@ragnorc
ragnorc requested a review from aaltshuler as a code owner October 18, 2026 20:30

ragnorc commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

This route tells a caller which of its own actions the installed policy allows, so the caller can show those actions before a write.

GET /graphs/{graph_id}/access and the MCP access tool return the resolved actor, the actions allowed with no branch, and the actions allowed for a named branch and target branch. The route is on the authenticated graph router. It requires no graph action. A missing credential is 401. target_branch without branch is 400. A branch name that is not canonical is 400.

access_body calls the same authorize the routes call. read, export, and change use the named branch as the source. schema_apply, branch_delete, commit_publish, and commit_drop use that branch as the target. branch_create and branch_merge use the source and the target together. The handler reads no rows and does not check that the branch exists.

The HTTP contract stays at 0.13. That matches the RFC plan to move the version once, together with the actor header and the error outcome.

RFC comparison

I compared this commit with draft RFC #1017 at 7c72c7e7ed2b42fb0e5b3f5140d8235444ddcf33, rule 7 (Access) and the HTTP API table.

The code agrees with the RFC on these points:

  • The route requires authentication, as every graph route does, and it requires no graph action.
  • The answer is the installed policy for the resolved actor.
  • Branch names are judged as written. The route does not look the branch up, so it cannot probe a 404.
  • A static token with no policy gets read only. A signed data credential with no policy gets no actions. Both results come from authorize.
  • The body names actions, not rules, and it names only the caller.
  • Actor views can still refuse a change the policy allows. The body has no view field. The RFC reserves that field for the actor-views work.
  • This pull request does not bump the HTTP contract version.

Two differences are in the RFC text, and I would keep the code:

  • The RFC sample is the CLI envelope (protocol, target, source) and it omits target_branch. This route returns the server body and always includes target_branch. The CLI envelope is feat(cli): say who you are at a target and what you may do #1081.
  • The RFC names "per branch" as one shape. The code uses the source slot or the target slot, which is how each route builds its PolicyRequest. One slot for every action would disagree with branch_delete, commit_publish, commit_drop, and schema_apply.

Tradeoff

The shape table in access_body is a second copy of the route conventions. That copy is the new liability. A match on PolicyAction would make a new action fail compilation until someone classifies it. This pull request leaves omnigraph-api-types free of omnigraph-policy. That split is right until #1081 shares one evaluator with the CLI. Up to nineteen in-process Cedar decisions per call is a bounded cost for an introspection call. The route removes the need to send a write in order to learn a 403.

Ordering

Merge this after the RFC is accepted, and before #1081. #1081 already contains this commit and then moves the decision into AccessOutput::evaluate. #1074 puts omnigraph-expect-actor on this same router, so this route inherits that header. Do not add the header in this pull request. openapi.json will conflict with #1074, #1076, #1078, #1080, #1081, and #1012. #1059 and #675 do not touch these files. #1075 and #1077 change the policy a local server installs. This route will report that policy through the same authorize path.

Findings

Blocking: none.

Optional:

  1. crates/omnigraph-server/src/handlers.rs, access_policy_action. The match is exhaustive on AccessAction. It is not exhaustive on PolicyAction. A new policy action will compile and stay out of this report. Add one match on PolicyAction that classifies each action: source branch, target branch, transition, graph-wide, or not reported. Leave admin, graph_list, and config_manage unreported. feat(cli): say who you are at a target and what you may do #1081 moves this function into AccessOutput::evaluate. Put the match in that one place so the route and the CLI cannot classify a new action differently.

  2. docs/user/operations/policy.md, "Learn your access". graph_wide is the decision with no branch. branch_scope: any compiles to when { true }, so that decision is also an allow on every named branch. A protected rule plus an unprotected rule allows every named branch and still leaves the action out of graph_wide. read in graph_wide is also the scope-free decision for GET /branches, GET /schema, show, and commit show. schema_apply in actions is the target-branch decision for the branch the caller named. Schema apply and the system-column upgrade authorize target main only. Storage upgrade authorizes each live branch. Say that beside the actor-view sentence. A client that sees schema_apply on feature will treat it as permission to apply schema there. The sample body does not come from the sample policy earlier on this page. That policy grants read and invoke_query, and it does not grant change.

  3. crates/omnigraph-server/tests/integration/auth_policy.rs, access_reports_the_callers_own_actions_as_routes_decide_them. This fixture locks read, export, change, branch_delete, and branch_merge. It does not grant schema_apply, commit_publish, commit_drop, branch_create, or invoke_query. The open-mode case allows every slot, so a swapped source and target still passes. Add one allowing rule for each of those actions, and assert the slot the route uses. The RFC also says a signed data credential with no policy gets an empty list. This test does not send that credential. authorize already denies it, and a direct case here would lock the route to that result.

Recommendation: approve.

Validation

Reviewed commit: f025bce5055ae15e817c50162bd803a96a0af7d7. Base: 0a62bb7b2aa5b99b012f413807c216277f7f9596.

Local, from a detached checkout of that commit:

  • cargo fmt --all --check — exit 0
  • python3 scripts/check-docs.py — exit 0
  • python3 scripts/release_notes.py preview --working-tree — exit 0. The new note is included, and its link resolves to docs/user/operations/policy.md#learn-your-access
  • RUST_MIN_STACK=16777216 cargo test -p omnigraph-server --test integration --locked -- auth_policy:: — 41 passed
  • RUST_MIN_STACK=16777216 cargo test -p omnigraph-server --test integration --locked -- openapi:: mcp:: — 106 passed

GitHub checks on this commit are success, including Lint (clippy), Format (rustfmt), Test Workspace, GQ Logic Tests, and Test omnigraph-server --features aws. Devin Review completed as a skip because the trial has no credits, so it is not a review of this head.

This change adds a route. It does not fix a query or storage defect. GQT cannot see an HTTP authorization report, so there is no .gqt regression. The owner is auth_policy::access_reports_the_callers_own_actions_as_routes_decide_them, and that test passed on this commit.

I did not re-run the full workspace gate locally. I used the green checks on this commit for that. The change does not call Lance, so I did not re-read the Lance format pages.

An actor whose rules allow each remaining action only through its own
slot (schema apply and the commit controls by target, a fork by target
into an unprotected branch, `invoke_query` graph-wide) now fixes which
slot each action's decision uses, so a swapped source and target fails.

The guide's sample comes from the page's example policy, and it says
what the branch-free decision covers, that rules covering every branch
only together do not reach it, and that `schema_apply` on a branch other
than `main` does not make schema apply possible there.
@ragnorc

ragnorc commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Optional items, addressed:

  1. Exhaustive classification. One match on PolicyAction gives each action its access slot: source, target, transition, graph-wide or unreported, with admin, graph_list and config_manage unreported. It lives in omnigraph-api-types' evaluator in feat(cli): say who you are at a target and what you may do #1081 (6c96abc), so the route and omnigraph access share it, and a new policy action fails to compile until it is classified.
  2. Docs (6bb1108). The sample now comes from the page's example policy. The text says what the branch-free decision covers, that a protected rule plus an unprotected one reaches every named branch but not the branch-free decision, and that schema_apply on a branch other than main does not make schema apply possible there.
  3. Slots (6bb1108). An actor whose rules allow schema_apply, commit_publish, commit_drop, branch_create and invoke_query only through their own slot now pins which slot each route uses, so a swapped source and target fails.

I did not add the signed-data-credential case: it needs a data-token trust fixture, and authorize already denies it.

@cursor

cursor Bot commented Oct 19, 2026

Copy link
Copy Markdown

Recommendation: approve. Blocking findings: none. Optional findings: two.

GET /graphs/{graph_id}/access and the MCP access tool tell a caller what its own credential may do. Each answer is the authorize decision the route for that action already makes, so a client can show the actions before a write. The route sits on the authenticated graph router. It requires no graph action. It reads no rows. It judges branch names as written.

Earlier findings

The previous review is #1073 (comment) at f025bce5. The author replied in #1073 (comment). Status at 6bb11088:

  1. Exhaustive classification. Resolved in feat(cli): say who you are at a target and what you may do #1081, which is the place that review named. This head still maps AccessAction to PolicyAction in access_policy_action. feat(cli): say who you are at a target and what you may do #1081 commit 6c96abc5 matches on PolicyAction in access_slot. The slots are the same. Source: read, export, change. Target: schema_apply, branch_delete, commit_publish, commit_drop. Transition: branch_create, branch_merge. Graph-wide: invoke_query. Unreported: admin, graph_list, config_manage. A new policy action fails to compile in that one function. This pull request should keep a single copy.

  2. Guide text. Partly resolved. The sample now comes from the example policy on the same page. For act-alice, branch=feature, and target_branch=main, the body is graph_wide: ["read","invoke_query"] and actions: ["read"]. That matches read with branch_scope: any (when { true }) and invoke_query with no branch condition. The new sentences match the code I read. graph_wide is the decision with no branch. A protected rule and an unprotected rule together allow each named branch and leave the no-branch decision denied, because both conditions require has_branch or has_target_branch. Schema apply and the system-column upgrade call enforce with TargetBranch("main") (schema_apply.rs, schema_apply/prepared.rs, system_column_upgrade.rs). A storage upgrade calls check with TargetBranch for each live branch (upgrade.rs). GET /schema, GET /branches, show, and GET /commits/{commit_id} authorize read with no branch. The OpenAPI description of graph_wide still has the old sentence. See optional finding 1.

  3. Slots. Partly resolved. The new actor may schema_apply only on a protected target, commit_publish and commit_drop only on an unprotected target, branch_create only into an unprotected target, and invoke_query with no branch. ?branch=main&target_branch=feature returns ["schema_apply","branch_create"]. ?branch=feature&target_branch=main returns ["commit_publish","commit_drop"]. graph_wide is ["invoke_query"]. Those equalities fail if the route swaps the source and the target. The test passed. The signed-data credential with no policy is still absent. See optional finding 2.

RFC #1017 at 7c72c7e7

Compared with rule 7 (Access) and the HTTP API table in docs/rfcs/2026-10-16-graph-access-serving-and-cross-process-writes.md.

The code agrees with the RFC on these points:

  • The route requires authentication, as every graph route does, and it requires no graph action.
  • The answer is the installed policy for the resolved actor.
  • Branch names are judged as written. The route does not look a branch up, so it cannot probe a 404.
  • A static token with no policy gets read only. A signed data credential with no policy gets no actions. Both results come from authorize.
  • The body names actions, and it names only the caller.
  • The body has no view field.
  • This pull request leaves the HTTP contract at 0.13. The RFC moves that version once, together with the actor header, outcome, and the code registry.

The code and the RFC differ in three places:

  • The RFC sample is the CLI envelope (protocol, target, source) and it omits target_branch. This route returns AccessOutput and always sends target_branch (null when the caller omitted it). The CLI envelope belongs to feat(cli): say who you are at a target and what you may do #1081.
  • The RFC calls "per branch" one shape. This route uses two shapes for one named branch: the source slot (read, export, change) and the target slot (schema_apply, branch_delete, commit_publish, commit_drop). That is how those routes build PolicyRequest. Update the Access bullet with both slots before the RFC is accepted. One slot for every action would disagree with the target-scoped routes.
  • The RFC evidence line asks integration/auth_policy.rs to show the signed credential's empty list. This head does not. The behavior is in authorize. The test is optional finding 2.

Blocking

None.

Optional

  1. crates/omnigraph-api-types/src/lib.rs, AccessOutput.graph_wide. The rustdoc says the field lists actions allowed whatever the branch, including a branch-scoped action a rule allows on every branch. openapi.json copies that sentence. The guide now says the other fact: a protected rule plus an unprotected rule allows every named branch and leaves graph_wide empty. graph_wide is the authorize call with branch and target_branch both absent. Write that in the rustdoc and regenerate openapi.json. In the guide, name every always-true form. A rule with no scope has no when. branch_scope: any and target_branch_scope: any compile to when { true }. All three enter graph_wide. The guide names only branch_scope: any. Also say commit show for the scope-free read. A commit list authorizes read on the named branch, and on main when the query omits the branch (server_commit_list).

  2. Signed data credential, no installed policy. authorize denies that credential before the static-token read default (AuthSource::SignedData and policy.is_none()). This route then returns 200 with empty graph_wide and empty actions. The same credential on GET /snapshot returns 403. The snapshot assertion does not lock this body. data_tokens::DataTokens already lives in auth_policy.rs. signed_data_requires_cedar_and_rejects_forgery_on_every_protected_route lists protected routes by hand and omits GET /access. Add one access call: status 200, the resolved actor, both action lists empty.

The #1081 blocker

The #1081 review (#1081 (comment)) blocks on the CLI store arm. That arm does not call this route. It reports every action for a path it did not open. This route reports the installed policy through authorize. The route's contract is not the fault in that finding.

Siblings and order

Merge this after RFC #1017 is accepted, and before #1081. #1081 already contains 6bb11088, then moves the decision into AccessOutput::evaluate (6c96abc5). #1074 puts omnigraph-expect-actor on this same router, so this route inherits that header. Leave the header out of this pull request.

File overlap against 0a62bb7b:

Tradeoff and liability

The slot table in access_body is a second copy of the way each route fills PolicyRequest. That copy is the new liability. #1081 deletes the copy by classifying every PolicyAction once. This pull request keeps omnigraph-api-types free of omnigraph-policy until that move. Nineteen in-process Cedar decisions per call is a fixed bound on an introspection call. The route removes the need to send a write in order to learn a 403.

The new test makes a swapped slot fail in the existing owner. The guide tells a client to read actions for a named branch. The OpenAPI sentence still tells that client that graph_wide means "on every branch". Align those two texts. After that, the second copy is the liability this pull request still carries, and #1081 is the removal.

A later introspection field will copy the slot table again if the classifier stays in the handler. One match on PolicyAction is the shape to keep.

The route-shaped decision is the contract. One branch argument for every action would report schema_apply against the source branch. Schema apply authorizes the target, and that target is main.

Validation

Reviewed commit: 6bb11088bb00025a27ce60527485ccf1475f3ac8. Parent: f025bce5055ae15e817c50162bd803a96a0af7d7. Base: 0a62bb7b2aa5b99b012f413807c216277f7f9596. RFC head: 7c72c7e7ed2b42fb0e5b3f5140d8235444ddcf33.

Local. rustc 1.97.1. Detached checkout /tmp/pr-1073. CARGO_TARGET_DIR=/workspace/target. RUST_MIN_STACK=16777216.

  • cargo fmt --all --check — exit 0
  • python3 scripts/check-docs.py — exit 0, after python3 -m pip install -r scripts/requirements-docs.txt
  • python3 scripts/release_notes.py preview --working-tree — exit 0. The new note is included. Its link resolves to docs/user/operations/policy.md#learn-your-access
  • cargo test -p omnigraph-server --test integration --locked -- auth_policy::access_reports_the_callers_own_actions_as_routes_decide_them openapi:: mcp:: — 107 passed, 0 failed
  • cargo test -p omnigraph-server --test integration --locked -- auth_policy:: — 41 passed, 0 failed

GitHub checks on 6bb11088 are success, including Lint (clippy), Format (rustfmt), Test Workspace, GQ Logic Tests, and Test omnigraph-server --features aws. Devin Review is a success status and has no written review of this head. No other review comment covers this head.

This change adds a route and extends its owner test. It does not fix a query or storage defect. A .gqt case cannot see an HTTP authorization report, an actor, or a policy decision. The owner is auth_policy::access_reports_the_callers_own_actions_as_routes_decide_them, and that test passed on this commit.

I did not re-run the workspace gate, Clippy, or cargo deny locally. The green checks on this commit cover those. The route does not call Lance, so I did not re-read the Lance format pages. I did not measure the nineteen decisions.

Comment thread crates/omnigraph-server/tests/integration/auth_policy.rs
Comment thread crates/omnigraph-api-types/src/lib.rs
@ragnorc
ragnorc force-pushed the feat/graph-access-route branch from be18e46 to 6bb1108 Compare October 19, 2026 07:03
`graph_wide` holds an action one rule allows on every branch: a rule
with no branch scope, `branch_scope: any` or `target_branch_scope: any`.
Rules that cover every branch only together leave the action out, so a
caller reads `actions` for a branch. The type's description, which
openapi.json copies, and the policy guide now say so, and the guide names
`commit show` as the scope-free read; a commit list authorizes `read` on
its branch. The signed-data test now asks the access route for a
credential with no policy and expects its actor with both lists empty,
and checks that the route refuses forged and retired tokens like every
other protected route.
@ragnorc
ragnorc added this pull request to the merge queue Oct 19, 2026
Merged via the queue into ModernRelay:main with commit 1998e94 Oct 19, 2026
32 checks passed
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 19, 2026
The access route from ModernRelay#1073 now rides HTTP contract 0.14 with the actor
header: its policy guide example sends 0.14, and its OpenAPI operation
lists the contract value and the optional omnigraph-expect-actor
parameter like every other protected route.
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 19, 2026
Brings in main through ModernRelay#1074: the access route from ModernRelay#1073 under HTTP
contract 0.14.
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 19, 2026
Brings in main through ModernRelay#1076: the access route from ModernRelay#1073 under HTTP
contract 0.14.
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 19, 2026
Brings in the access route from ModernRelay#1073; its tests and the local server's
sit side by side at the end of auth_policy.rs.
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 19, 2026
Brings in main through ModernRelay#1075: the access route from ModernRelay#1073, whose tests
follow the local cluster server's at the end of auth_policy.rs.
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 19, 2026
Brings in main through ModernRelay#1078: the access route from ModernRelay#1073 under HTTP
contract 0.14.
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 19, 2026
Brings in main through ModernRelay#1080: the access route as ModernRelay#1073 landed, whose
handler and output type this branch already carries in their evaluated
form (the conflict keeps this side), HTTP contract 0.14, and `schema
plan` refusing a root target.
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 20, 2026
… reads

The review of c4641d7 found the engine section did not match the
publisher on main: a row change must record a graph commit, and the
preconditions read only the head and branch identity, so a
head-preserving tombstone or drop record stopped nobody.

- Control facts (tombstone, registration, drop record) change no table
  row and record no commit; check_publish_precondition reads the facts
  that name an attempt on every attempt, for every precondition.
- A create registers on main before it writes its ref, so the
  registration is the visibility point and cleanup reads it instead of
  an age. Until PR 963 lands, a registration refuses when main's
  contract moved past its fork; after it, PR 963's rule wins.
- Only the upgrade census raises the new stamp.
- RFC 0059 (PR ModernRelay#1090) owns addressing and the target and credential
  commands; deployments pass their root through --target.
- The access route is ModernRelay#1073's; this RFC adds the command. A direct
  graph inside a cluster refuses and names the root.
- Storage outcomes are PR 1093's; storage_unavailable goes, and
  disposition.check is a later phase.
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 21, 2026
* docs(rfc): propose local graph sessions and client access

Draft RFC for issue ModernRelay#980: `omnigraph serve` local sessions with session
records, a thin `omnigraph studio` launcher, `omnigraph mcp` for agents,
and `omnigraph credential` for static tokens. Wider-blast-radius changes
are listed in Appendix A with their filed issues.

* docs(rfc): add installing Studio as a later, signature-gated phase

* docs(rfc): link Studio's release prerequisites and correct their current state

* docs(rfc): reconcile local sessions with main and open PRs, and define the target writer

* docs(rfc): name local servers and settle records, tokens and agent paths

Rename the session to a local server and --session to --via-server.
Move server records from /tmp, which macOS ages out after three days,
to per-user directories resolved from the account, with the machine
they belong to. Never write a token to a terminal, list live local
servers in targets --json, give each agent context one write path,
add platform coverage, and gather every refusal code in one table.

* docs(rfc): redesign graph access around seven rules and fold in cross-process writes

Rewrite the RFC from first principles: name the target, never the path;
correctness in the engine across processes; one set of operations on three
surfaces; one serving concept with explicit audiences, folding the
omnigraph-server binary into omnigraph serve; --as as a declaration or an
assertion; one outcome model and exit table; and introspection through
target list and access. Bring cross-process writer safety and the access
route into scope, with the engine design on the __manifest version CAS,
and land the interfaces before the engine phase. Rename the file to match
the new title.

* docs(rfc): align outcomes, serving lifecycle and late writes with the storage-uncertainty fix

* docs(rfc): elect one local server per destination and close the reuse, handoff and MCP gaps from review

* rfc: align graph access with the phase 1 and phase 3 implementations

The actor header rides the HTTP contract bump to 0.14 instead of a
capability response header, and /mcp compares it after authentication
alone. Rule 6 states the outcome table the server implements, with an
earlier publication marked on the engine's error and outcome.check waiting
for an attempt id. The local embedding API names what the implementation
settled: LocalBinding::local for local roots, ReadWriteUnavailable,
ClusterGraph instead of promoting a graph to its cluster, start_local and
LocalServing::release, and one captured ledger revision per read-only
cluster server.

* rfc: align graph access with the disposition, the If-Actor header and the records' machine id

Phase 1's review renamed the error object and the actor header, and
phase 3's review tightened the records' machine id. The RFC now says
what the implementations do:

- The actor header is Omnigraph-If-Actor, after HTTP's conditional
  If-* headers, since a mismatch is a failed condition (412).
- The error object is `disposition`: `execution` is gone because it
  only repeated `effects`; `effects` gains `partial` for a refused
  step after an earlier publication of the same operation; `action`
  gains `fix` for a request that fails the same way on resend. Each
  code names its own action, and partial effects exit 2.
- A record's machine is 32 lowercase hexadecimal digits that are not
  all zeros. Any other value, systemd's first-boot `uninitialized`
  marker included, refuses rather than falling back to another source.
- Windows records wait for their own pull request: until they check
  the directory's access list and read MachineGuid, they refuse.
- The CLI's MCP bridge bullet lists what the bridge actually changes:
  the graph binding, the readOnlyHint filter and local cancellation.

* rfc: decide branch control through control facts each publish attempt reads

The review of c4641d7 found the engine section did not match the
publisher on main: a row change must record a graph commit, and the
preconditions read only the head and branch identity, so a
head-preserving tombstone or drop record stopped nobody.

- Control facts (tombstone, registration, drop record) change no table
  row and record no commit; check_publish_precondition reads the facts
  that name an attempt on every attempt, for every precondition.
- A create registers on main before it writes its ref, so the
  registration is the visibility point and cleanup reads it instead of
  an age. Until PR 963 lands, a registration refuses when main's
  contract moved past its fork; after it, PR 963's rule wins.
- Only the upgrade census raises the new stamp.
- RFC 0059 (PR ModernRelay#1090) owns addressing and the target and credential
  commands; deployments pass their root through --target.
- The access route is ModernRelay#1073's; this RFC adds the command. A direct
  graph inside a cluster refuses and names the root.
- Storage outcomes are PR 1093's; storage_unavailable goes, and
  disposition.check is a later phase.

* docs(rfc): discontinue the second serving model

* docs(rfc): record MCP bridge closure and defer selective reuse

* rfc: treat RFC 0059's implementation as merged

ModernRelay#1090 landed on main, so the RFC no longer waits on it: blocked_on is
empty, and the Motivation, rollout and Appendix A5 say the target
commands build on main.

* rfc: accept graph access and cross-process writes

Maintainer decision: the actor assertion, the error-code registry and
disposition, the exit table, the access command, operation parity, the
existing-server MCP bridge and Studio opener, and the engine control-fact
design with its qualification gates. Appendix A stays separate decisions.

* rfc: give every control fact its attempt id and gate the upgrade on registrations

The review of eeb6e12 found two holes in the engine section:

- A control fact records no graph commit, so the head-equality read-back
  cannot tell two facts apart after a lost acknowledgement, and the
  owner's own retry met its refusal. Each control-fact operation now mints
  one attempt id that every fact it writes records: the read-back compares
  it, a pending control fact is keyed by it, and a retry treats its own
  fact as landed. Deletes and drops converge.
- System-column upgrade pins main's head, not its version, so a
  registration did not stop it. Its attempts now read main's
  registrations every attempt, until PR 963.

Cleanup's registration rule now covers refs as well as trees, under the
collector's dependency closure; a create no longer completes a crashed
create's registration; the pending-branch service_unavailable covers one
branch; branch list shows a registered branch without a ref as not ready;
the --as help quote is current.

* rfc: name the access command's test owner and target terms

* rfc: settle a control fact from the fact, and keep the collector's keep set

The review of ef44175 recommended merging, with four optional
corrections: read-back has three results; a caller settles a control
fact from the fact, since the head does not move; cleanup candidates
still pass protected_manifest_branches and retained_merge_bases; a crash
loses the attempt id, so only delete and drop converge after one. A
not-ready branch's listing fields come from its registration.

* rfc: fence an unknown control fact by its version key

Storage outcomes per operation dropped its pending-publication registry:
an unknown publication is recorded nowhere and blocks nothing, and its
version key fences it. A control fact follows the same fence, and the
one-branch service_unavailable row goes. Read-back follows ModernRelay#1092's
split, a not-ready branch's absent listing fields are named, and the
access evidence uses RFC 0059's target kinds.

* rfc: align the access view field and disposition.check with actor views

* rfc: settle a control fact when its attempted version is readable

---------

Co-authored-by: aaltshuler <andrew@collectivelab.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant