Repository navigation
feat(policy): native expiration / single-use bounds for approved policy grants (expires_at, max_uses) #2881
Description
Summary
OpenShell has no native way to make an approved policy grant time-bounded or single-use. Once a draft/chunk is approved and applied, the resulting allow rule stays in effect until a client explicitly calls the API to revoke it. There is no expires_at, ttl, or max_uses on the grant itself, so OpenShell will neither auto-expire nor one-shot-limit it.
Motivation
We are integrating an external policy-recommendation service with OpenShell (the external service produces a recommendation; OpenShell remains the enforcement authority). Many of these grants should be tightly bounded in time and usage — e.g. "allow this sandbox one egress call to an approved external endpoint, valid for the next 10 minutes."
Today we emulate that with apply-then-revoke: approve/apply the rule, then issue a follow-up API call to remove it after the window or after a single use. It works, but it pushes all single-use/expiry/replay enforcement into the external service and leaves a gap — nothing at the OpenShell layer bounds the grant to exactly one use or a fixed lifetime before the external caller revokes it.
Broader use cases:
- Just-in-time, least-privilege grants that auto-expire.
- Single-use grants scoped to one action (submit one envelope, run one query).
- Break-glass / exception approvals that should lapse automatically.
- Any third-party policy advisor that wants OpenShell to enforce the time/use bound rather than trusting the advisor to call back and revoke.
Proposed capability
Add optional expiration / usage bounds to an approved policy grant (the draft/chunk or its resulting rule):
expires_at(absolute) orttl(relative): the grant auto-lapses (reverts to prior state) when it passes.max_uses: the grant is consumed after N matching requests (default unlimited).- optional
nonce/ idempotency key: prevents replay of the same grant.
Enforced server-side, so the grant is bounded even if the external service never calls back. Expiry vs. manual revocation should be distinguishable in status/events.
Prior art
#2591 — feat(sandbox): add server-managed expiration leases applies the same server-managed TTL/expiration-lease pattern at the sandbox level (a reconciler auto-terminates expired sandboxes). This request is the policy-grant analog: the same lease model, but the grant auto-reverts instead of the sandbox being deleted.
Current workaround
Apply-then-revoke from the external decision layer: our decision store tracks expiry/nonce/uses and issues a follow-up deny/drop API call. Functional, but the guarantee lives outside OpenShell and is subject to a revoke race.
Acceptance criteria (rough)
- A grant can be created with
expires_at/ttland/ormax_uses(and optionallynonce). - OpenShell auto-reverts/denies once the bound is hit, with no external call required.
- Status and events distinguish auto-expiry from manual revocation.
Related: #2591 (sandbox expiration leases); the draft/chunk approval flow (ApproveDraftChunk, policy reload).
Activity
elezar commentedon Aug 25, 2026
gomission commentedon Sep 30, 2026
One concrete lifecycle from Mission's Trust Graduation work that may help scope the design: one bounded, non-streaming HTTP POST, with one permitted admission and a ten-minute grant expiry. This is a proposed OpenShell use case, not an existing integration or a tested OpenShell guarantee.
The agent requests the grant; a human approves through an external decision service; OpenShell owns the final admission/use check; the agent and operator can read the persisted decision/status after restarting. Pending approval does not require keeping a request or stream open.
For this example, bind the grant to the sandbox identity and an exact request: method, origin, path/query, fixed content type and approved body bytes. Exact-body binding is an additional constraint for this example, beyond the issue's expiry/use bounds. Keep grant identity, request identity and payload identity separate: a retry of the same request can read its status, but must not authorize another dispatch; reusing that identity with different bytes is a conflict.
I would define a use as a durable atomic claim before forwarding, followed by at most one dispatch attempt. Expiry/current revocation state are checked at that admission boundary. Consumption survives gateway/supervisor restarts, and a timeout or HTTP error does not replenish it. This deliberately trades availability for a clear bound: a crash after the claim but before forwarding can leave an unresolved execution. A consumed grant proves admission, not that the backend effect happened.
The acceptance cases I would start with, under a test policy with no other standing permission for the endpoint:
The backend lookup belongs to the caller/provider contract, not a promise that OpenShell can independently verify arbitrary external effects. Likewise, the existing admission/replay work in #3321, #3323 and #3324 is relevant prior work, rather than a reason to add a parallel receipt subsystem.
Would this be useful as the one-shot request-authorization case, separately from a general expiring policy overlay? If that boundary fits, we can contribute deterministic fake-clock/concurrency/crash cases against the agreed enforcement path. No OpenShell fixture has been run for this proposal yet.
github-actions commentedon Oct 15, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 14 days if there is no further activity. Comment or remove the state:stale label to keep it open.