Repository navigation
fix(core): give back a bounded use whose decision could not be recorded - #104
Merged
Merged
Conversation
DaXu999
force-pushed
the
fix/bounded-use-release
branch
from
October 4, 2026 19:25
ef1c53e to
87ca15e
Compare
A maxUses grant is spent when the decision is made and the decision's authorization.checked is written after. A sink that threw there refused the call and kept the use, so one audit outage exhausted a maxUses: 1 grant whose provider was never entered. ADR 0023 recorded it as a known limit. GrantUsageStore gains an optional release, an atomic decrement that stops at zero, and the kernel calls it when the record of an allowed, consuming decision is refused. A store without one, or one that throws from it, keeps the use as before, and the caller is told of the audit outage either way. Nothing else gives a use back. The defect was reported in #101, which fixed it with a reservation on a replay record, an effect store and an audit outbox. This is the same outcome on the usage port alone. ADR 0023 is revised.
Implements GrantUsageStore.release as one UPDATE guarded on used > 0, and adds the changelog entry for the fix.
CapabilityAuthorizer.release found the grant to give back by id, and nothing makes a grant id unique within an authority. With a bounded grant and an unbounded one under the same id, the unbounded grant's unrecorded decision gave back the use the bounded one had spent on a call that ran, so a maxUses: 1 tool could run twice. The authorizer now keeps each spent use against the decision that spent it, under the namespace and grant id tryConsume was given, and release takes that decision. A decision that spent nothing (a denial, a non-consuming check, an unbounded grant, one another authorizer made) gives nothing back, and a spent use goes back at most once. Tests cover the duplicate id, and the give-back on a resource request, a direct send and a turn admission, with nothing given back for a refusal or when a record after the decision fails. ADR 0023 says why the use is kept against the decision, and that an ambiguous write leaves a consuming authorization.checked with no operation or turn after it.
The kernel stops every other operation whose decision was not recorded, but a turn is run by the host. A host that admits turns without SharedOSExecutor and runs one after admitTurn rejected now does so on a use that went back, so host-integration.md says not to. The changelog entry no longer says an audit outage never costs a use: a use whose decision was recorded stays spent even when a later record fails before anything runs. errors.md notes the give-back beside the outage paragraph.
DaXu999
force-pushed
the
fix/bounded-use-release
branch
from
November 8, 2026 14:09
87ca15e to
d10f49b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
A
maxUsesgrant is spent when the decision is made, and the decision'sauthorization.checkedis written after. A sink that throws there refuses the call, correctly, and keeps the use. One audit outage exhausts amaxUses: 1grant whose provider was never entered. ADR 0023 recorded this as a known limit.Reported and reproduced by @Alfazal007 in #101, which fixed it with a reservation on a replay record, an effect store and an audit outbox, on top of #99 and #100. This PR reaches the same outcome on the usage port alone, and replaces #101.
What changes
Core (
c9eb3c6,4af27b2)GrantUsageStoregains an optionalrelease(namespaceId, grantId): an atomic decrement that stops at zero.InMemoryGrantUsageStoreimplements it.CapabilityAuthorizer.release(decision)in one place: when the record of a decision is refused, so the call never ran.tryConsumewas given.releasegives back that use, at most once, and nothing for a decision that spent nothing (a denial, a non-consuming check, an unbounded grant, a decision another authorizer made).maxUses: 1tool could run twice.4af27b2fixes that.Reference host and changelog (
44cd1bb)releaseas one guardedUPDATE.Host-facing docs (
d10f49b)docs/host-integration.md: a turn is the one operation the kernel does not run itself, so a host that admits turns withoutSharedOSExecutormust treat anadmitTurnthat rejects as admitting nothing and not run the turn. Its use has gone back.docs/errors.mdnotes the give-back beside the outage paragraph.What it does not do
release, or one that throws from it, keeps the use as before. The caller is told of the audit outage either way.authorization.checkedwith no operation record after it, for a use that went back. Usage rebuilt from the trail should count operation records, not decisions. ADR 0023 says so.Verification
b585bbc); the only conflict wasCHANGELOG.md.pnpm checkexits 0: format, typecheck, 1155 tests, release tests, API reference up to date, conformance manifest unchanged.maxUses: 1.maxUses, and no count went negative or landed on another grant.releaseacross two connections and across worker threads on one file, and against an in-memory database: no change on an empty counter, one use back after a consume, never below zero.