Skip to content

feat: stop a session when its grants change, and keep its policy current - #79

Merged
sstriker merged 9 commits into
mainfrom
feat/access-freshness
Nov 14, 2026
Merged

sstriker merged 9 commits into
mainfrom
feat/access-freshness

Conversation

@sstriker

@sstriker sstriker commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

This is package 10 of PLAN.md: access freshness, section 6 and Appendix E of the paper.

This branch is on top of #78 (the rule lifecycle). Until #78 merges, the diff of this pull request also shows #78. Review commits 71fbc51 and 8441f88 for this package.

The paper says that a request made after a revocation cannot use the old grant. The code kept the grants, the policy and the read filter of a session until the session ended.

What changes

  • Grants. A session records the digest of grants.toml when it starts. Before each read, each decision, each spend check, each call of a hosted tool, each import and each request through the egress, it reads the file again. If the file changed or cannot be read, the session stops: each of those calls gives stopped: .... A new session starts under the grants as they are.
  • Policy. state/policy.stamp changes when a session changes what is in force. The stamp also has the digest of the root's policy/*.rego. A session that sees a different stamp loads the policy again, and computes its read filter and its handling again. If the policy does not load, the call is refused.
  • Expiry. The policy compares the expiry of an exception with the clock at each decision.
  • Recall. recall with a deputy withholds its brief when the read filter changed while the deputy wrote the brief.
  • Board. The board opens a new session when its session stops, and keeps its claims.
  • Demo. Act 3 has a step for a grant that the owner withdraws while a session is open.

A defect on main that this branch corrects

The last step of act 3 printed the length of the answer object of an agent's search. That length is always 1. The step now counts the hits. On a record that I seeded from examples/decisions and docs, the agent gets 0 hits under the narrow policy, and docs/demo-narrative.md now says so.

Not in this change

README.md ("Access freshness, in part") and row 10 of PLAN.md list what is open:

  • Any change to the grants file stops each open session, not only a session that the change affects.
  • A read that waits, for example on the index, does not check again before it gives its answer.
  • Some commands of the owner and of the launcher do not do the check. No memory tool is in this group.
  • The files that a session rendered stay on disk, and the harness of a stopped session continues to run.
  • A grant has no expiry of its own.

Checks

make check passed with 1011 tests on 5a9a3ce. make corpus-check passed on 71fbc51. Act 3 ran on a scratch record with the debug binary: the open session got 5 hits, then two stopped: refusals, and the grants were put back. I did not run the act with a gateway key.

The new assertions failed when I removed the mechanism: the check in fresh() (four tests), the expiry at the decision, the reopen of the board, and the refusal in the egress.

Adversarial pass

One pass ran on b7a80c5. It found that each task operation on the ledger does the check. It confirmed these, and 8441f88 answers them:

  1. A stopped session still made model calls, because the egress read an error from the spend check as "not over the limit". Now an error is a refusal, and the egress checks the session before a model call, a tunnel and a tool request.
  2. A stopped session got a stored source from a hosted tool. Now use_tool checks first.
  3. A stopped session could import a bundle. Now import checks first.
  4. An import, and an entry of any type that replaced a policy entry, did not change the stamp. Open sessions kept the old policy. Now both load the policy again and write the stamp.
  5. A root policy directory that could not be read gave the built-in policy. Now it is an error.
  6. After the policy narrowed and widened again, get and the tasks of the session stayed narrow. Now the handling follows the filter.
  7. The reload of the policy opened the index with its schema set-up and waited five seconds on a writer. Now it opens the index to read.
  8. The board lost its claims when it opened a new session, and answered 500. Now it carries the claims and answers 403.
  9. Four sentences did not agree with the code. The text changed.

Open from the pass:

  • A read that waits does not check again (in the list above).
  • The commands that do not do the check (in the list above).
  • No test holds the reset of the pushed-pack cache when the filter changes, the check in use_tool, or the read-only open of the index.
  • Two exceptions with one id: when the first lapses, the policy also removes the second. This refuses more, not less.
  • Two requests to a stopped board each open a session.

A second pass ran on 5a9a3ce (8441f88 with #78 and main merged in). It confirmed these defects, and 4eb0aca fixes them. Each fix has a test that fails without it, except the script, which a run under three signals showed.

  • A tunnel that was open when the grants changed kept carrying bytes. The egress now closes it.
  • reindex and materialize changed the entries that the policy is compiled from, and open sessions kept the old policy. Both now reload the policy.
  • A board whose session stayed stopped answered 500, gave the domain list, and served the files of a bundle that it exported before. It now answers 403 to each.
  • The trap of act 3 did not put the grants back after a signal. It now does.

The pass also found fixes of 8441f88 that no test holds. These are still open, with no test: the refusal of over_budget on an error; the check in post of the egress; the check in use_tool; the reload after a release from quarantine; the index check in load_policy_again; the reset of the pack cache; adopt_claims on the board.

Two more items are open. The expiry of an exception does not reach a read filter that a root policy computes from the exceptions, or the advisory rules; local/README.md and PLAN.md say so. A demo root with a quote in its path breaks the run item of act 3.

No pass ran on 4eb0aca. This pull request stays a draft until #78 merges.

Posted by Claude on behalf of @sstriker

🤖 Generated with Claude Code

sstriker and others added 9 commits November 12, 2026 14:46
This is package 10 of PLAN.md: access freshness, section 6 and
Appendix E of the paper.

- A session records the digest of the grants file that it started with.
  Before each read, each decision and each spend check, it reads the file
  again. If the file changed or cannot be read, the session stops, and
  each call gives `Error::Stale`.
- `state/policy.stamp` changes when a session changes a policy entry. A
  session that sees a different stamp, or a different root policy, loads
  the policy again. The read filter has the version of the policy that
  computed it, and is computed again for a new version.
- The policy compares the expiry of an exception with the clock at each
  decision. It was compared only when the policy loaded.
- `recall` with a deputy withholds its brief when the read filter changed
  while the deputy wrote the brief.
- The board opens a new session when its session stops.
- Act 3 has a step for a grant withdrawn while a session is open. The
  step and the step after it now count the hits of an agent's search. They
  printed the length of the answer object, which was always 1.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Both sides keep their fields of the policy: the data and the expiries
of this branch, and the engine for advisory modules of the other. The
lint test of the other branch opens its agent session after it changes
the grants, because a session now stops when its grants change.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The policy keeps the data and the expiries of this branch, and one
engine for each advisory module of the other branch.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- A stopped session still made model calls. The egress read an error
  from the spend check as "not over the limit". An error is now a
  refusal, and the egress checks the session before a model call, a
  tunnel and a tool request.
- A stopped session got a stored source from a hosted tool, and could
  import a bundle. Both now check the session first.
- An import, and an entry of any type that replaced a policy entry, left
  the policy stamp as it was, so open sessions kept the old policy. Both
  now load the policy again and write the stamp.
- A root policy directory that could not be read gave the built-in
  policy. It is now an error, and the session is refused.
- After the policy narrowed and widened again, the handling of the
  session stayed narrow. It now follows the filter.
- The reload of the policy opened the index with its schema set-up, and
  waited five seconds on a writer. It now opens the index to read, and it
  refuses when the index cannot be read.
- The board carries its claims to its new session, and answers 403 when
  its session stays stopped.
- Act 3 puts the grants back when its step fails.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…cy after a reindex

A second adversarial pass on 5a9a3ce found these defects. Each fix has a test
that fails without it, except the change to the demo script, which a run under
three signals showed.

- A tunnel that was open when the grants changed kept carrying bytes. The
  egress now asks each second if the session is valid and closes the tunnel
  when it is not.
- `reindex` and `materialize` changed the entries that the policy is compiled
  from, and wrote no stamp. Open sessions, and the session that ran the
  command, kept the old policy. Both now reload the policy.
- A board whose session stayed stopped answered 500, gave the domain list, and
  served the files of a bundle that it exported before. It now answers 403 to
  each of these.
- The trap of act 3 did not put the grants back when a signal ended the act,
  and a second run then overwrote the copy with lowered grants. The trap now
  covers INT, TERM and HUP, and a copy that is left is put back first.

Not fixed, and listed in PLAN.md and local/README.md: the expiry of an
exception does not reach a read filter that a root policy computes from the
exceptions, or the advisory rules.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5DyJRpu5ned5y3d92U7Ts
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5DyJRpu5ned5y3d92U7Ts
@sstriker
sstriker marked this pull request as ready for review November 14, 2026 21:26
Copilot AI balanced review requested due to automatic review settings November 14, 2026 21:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@sstriker
sstriker merged commit 2355ac2 into main Nov 14, 2026
10 checks passed
@sstriker
sstriker deleted the feat/access-freshness branch November 14, 2026 21:53
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.

2 participants