You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Complete the next durable F5.9 security slice by giving every newly created approval grant one immutable, server-enforced absolute lifetime of exactly 10 minutes.
The usable interval is half-open:
approved_at <= consumed_at < expires_at
This remains foundational approval infrastructure only. It must not introduce MCP transport, credentials, connector dispatch, or an execution path.
Decision
The owner selected the fixed-lifetime option proposed on #23:
lifetime: exactly 10 minutes
source of time: PostgreSQL statement time at creation and consumption
mutability: expires_at is immutable at the application-role boundary
expiry response: the existing indistinguishable ErrApprovalGrantUnavailable
expiry evidence: no successful lifecycle event is appended
retention: the expired row remains for forensic inspection
renewal: outside this slice; a fresh action intent is required after expiry
Why this slice is safe and independently valuable
F5.9a (#250 / #251) already persists exact, single-use grants and consumes them with one conditional UPDATE ... RETURNING. The remaining gap is that an unconsumed grant has no expiry and can otherwise remain usable indefinitely.
The expiry predicate belongs in that same atomic update. Checking expiry before the update would create a time-of-check/time-of-use race; trusting a caller-supplied timestamp would make the lifetime bypassable.
Scope
Add an additive 0013 migration for approval-grant expiry.
Add immutable expires_at timestamptz NOT NULL with a database constraint requiring exactly approved_at + interval '10 minutes'.
Add an immutable evidence/lifecycle version marker so legacy rows remain distinguishable.
Backfill legacy rows for retention, but make every legacy unconsumed grant fail closed after migration; do not manufacture a false consumption event.
Mint approved_at and expires_at from one PostgreSQL statement timestamp.
Consume using one conditional UPDATE ... RETURNING whose predicate includes approved_at <= statement_timestamp() AND statement_timestamp() < expires_at.
Remove caller control over approval and consumption timestamps from the public approval-grant API.
Introduce a new lifecycle audit format and evidence-hash domain that binds the immutable expiry.
Preserve offline verification of existing policy format 1 and approval format 2 records.
Keep the application role limited to updating consumed_at; it must not update expiry or version metadata.
Update the operator/developer contract and session evidence.
Acceptance criteria
Every new grant has expires_at = approved_at + 10 minutes, enforced by PostgreSQL.
Consumption succeeds before expiry and atomically records the database statement time.
Consumption at or after expiry fails with ErrApprovalGrantUnavailable.
Unknown, foreign-workspace, mismatched, replayed, expired, and legacy-unconsumed grants remain indistinguishable to callers.
An expired refusal leaves consumed_at IS NULL, retains the row, and appends no successful approval-consumed event.
Two concurrent consumers before expiry still produce exactly one success.
Expiry and evidence-version columns cannot be updated by the application role.
New creation and consumption lifecycle entries share the same evidence digest, and mutating expires_at changes that digest.
Goal
Complete the next durable F5.9 security slice by giving every newly created approval grant one immutable, server-enforced absolute lifetime of exactly 10 minutes.
The usable interval is half-open:
approved_at <= consumed_at < expires_atThis remains foundational approval infrastructure only. It must not introduce MCP transport, credentials, connector dispatch, or an execution path.
Decision
The owner selected the fixed-lifetime option proposed on #23:
expires_atis immutable at the application-role boundaryErrApprovalGrantUnavailableWhy this slice is safe and independently valuable
F5.9a (#250 / #251) already persists exact, single-use grants and consumes them with one conditional
UPDATE ... RETURNING. The remaining gap is that an unconsumed grant has no expiry and can otherwise remain usable indefinitely.The expiry predicate belongs in that same atomic update. Checking expiry before the update would create a time-of-check/time-of-use race; trusting a caller-supplied timestamp would make the lifetime bypassable.
Scope
0013migration for approval-grant expiry.expires_at timestamptz NOT NULLwith a database constraint requiring exactlyapproved_at + interval '10 minutes'.approved_atandexpires_atfrom one PostgreSQL statement timestamp.UPDATE ... RETURNINGwhose predicate includesapproved_at <= statement_timestamp() AND statement_timestamp() < expires_at.consumed_at; it must not update expiry or version metadata.Acceptance criteria
expires_at = approved_at + 10 minutes, enforced by PostgreSQL.ErrApprovalGrantUnavailable.consumed_at IS NULL, retains the row, and appends no successful approval-consumed event.expires_atchanges that digest.Non-goals
elicitation/createtransport, URL-mode elicitation, or trusting elicitation content.Security, operability, and cost
Primary references
docs/EPICS.mdF5.9docs/adr/0005-ai-mcp-ardur-pdp.mdParent: #23
Depends on: #250