Preserve fractional approval expiry through atomic consumption

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 17:35:53 +02:00
parent 3485415b82
commit d40e842052
6 changed files with 271 additions and 6 deletions

View file

@ -36,7 +36,8 @@ Canonical float/string formatting follows Python source, including negative
floating zero, exponent switching/padding, HTML and U+2028/U+2029. Duplicate floating zero, exponent switching/padding, HTML and U+2028/U+2029. Duplicate
object keys, nonfinite/overflow numbers, invalid UTF-8 and lone surrogates refuse. object keys, nonfinite/overflow numbers, invalid UTF-8 and lone surrogates refuse.
Inputs are bounded to 1 MiB and 128 container levels. Timestamp precision is at Inputs are bounded to 1 MiB and 128 container levels. Timestamp precision is at
most nanoseconds with strict zoned RFC3339; current producer emits whole seconds. most nanoseconds with strict zoned RFC3339; the precision-repaired Python
producer preserves microseconds, while historical whole-second strings remain valid.
These explicit parser/resource limits fail closed, never normalize authority. These explicit parser/resource limits fail closed, never normalize authority.
`go test -race ./...` checks the portable producer vectors, typed fact pairing, `go test -race ./...` checks the portable producer vectors, typed fact pairing,

View file

@ -103,11 +103,11 @@ def utc_now(clock: Callable[[], datetime] | None = None) -> datetime:
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
if now.tzinfo is None: if now.tzinfo is None:
now = now.replace(tzinfo=timezone.utc) now = now.replace(tzinfo=timezone.utc)
return now.astimezone(timezone.utc).replace(microsecond=0) return now.astimezone(timezone.utc)
def iso(ts: datetime) -> str: def iso(ts: datetime) -> str:
return ts.astimezone(timezone.utc).replace(microsecond=0).isoformat() return ts.astimezone(timezone.utc).isoformat()
def parse_iso(value: str) -> datetime: def parse_iso(value: str) -> datetime:
@ -768,7 +768,6 @@ class Engine:
): ):
raise Unprocessable("decision_id must be a non-empty string") raise Unprocessable("decision_id must be a non-empty string")
conn = self._conn() conn = self._conn()
now = iso(self.now())
idempotent = False idempotent = False
try: try:
conn.execute("BEGIN IMMEDIATE") conn.execute("BEGIN IMMEDIATE")
@ -794,7 +793,11 @@ class Engine:
if row["status"] != "approved": if row["status"] != "approved":
conn.rollback() conn.rollback()
raise Conflict(f"cannot consume from status {row['status']}") raise Conflict(f"cannot consume from status {row['status']}")
if self.now() < parse_iso(row["not_before"]) or self.now() >= parse_iso(row["expires_at"]): # Lock acquisition can wait past a validity boundary. One post-lock
# instant is authoritative for both the check and its durable proof.
consumed_at = self.now()
now = iso(consumed_at)
if consumed_at < parse_iso(row["not_before"]) or consumed_at >= parse_iso(row["expires_at"]):
conn.rollback() conn.rollback()
raise Conflict("cannot consume outside validity window") raise Conflict("cannot consume outside validity window")
if row["human_control"]: if row["human_control"]:
@ -819,6 +822,7 @@ class Engine:
"use", "use",
approval_id, approval_id,
actor=row["actor"], actor=row["actor"],
observed_at=consumed_at,
extra={ extra={
"binding_digest": row["binding_digest"], "binding_digest": row["binding_digest"],
"binding": json.loads(row["binding_json"]), "binding": json.loads(row["binding_json"]),
@ -911,13 +915,14 @@ class Engine:
*, *,
actor: str | None, actor: str | None,
extra: dict[str, Any] | None = None, extra: dict[str, Any] | None = None,
observed_at: datetime | None = None,
) -> str: ) -> str:
if self.fail_outbox: if self.fail_outbox:
raise StoreUnavailable("outbox insert failed") raise StoreUnavailable("outbox insert failed")
if event_class not in EVENT_CLASSES: if event_class not in EVENT_CLASSES:
raise Unprocessable(f"unknown event class {event_class}") raise Unprocessable(f"unknown event class {event_class}")
event_id = str(uuid4()) event_id = str(uuid4())
created = iso(self.now()) created = iso(observed_at if observed_at is not None else self.now())
resource = f"approval:{approval_id}" if approval_id else "approval-engine:heartbeat" resource = f"approval:{approval_id}" if approval_id else "approval-engine:heartbeat"
details = {"class": event_class, **(extra or {})} details = {"class": event_class, **(extra or {})}
if approval_id: if approval_id:

View file

@ -42,6 +42,24 @@ after consumption, the approval remains spent and a retry needs a new approval.
The response is mutation evidence, not a permission decision. It contains no The response is mutation evidence, not a permission decision. It contains no
`effect`, `allow`, `deny`, or decision result. `effect`, `allow`, `deny`, or decision result.
## Authoritative time and fractional precision
New source preserves the actual aware UTC clock's microseconds rather than
rounding down to whole seconds. The first consume samples one instant after
`BEGIN IMMEDIATE` acquires its write lock, then uses that instant for the
not-before/inclusive and expiry/exclusive check, persisted `consumed_at` and
`updated_at`, and use-event occurrence/creation time. Lock wait cannot borrow
an earlier validity instant or stamp evidence before actual admission.
Existing whole-second ISO strings remain valid and are never backfilled with
invented fractions. Idempotent responses and object readback retain the original
recorded timestamp/decision exactly, even after expiry; they do not renew
execution authority. The store schema, API keys, CAS/digest correspondence and
no-unconsume rule are unchanged. Claim observation/freshness and other new
source timestamps also retain actual clock precision. Consumers must parse
aware ISO/RFC3339 timestamps including fractional seconds instead of assuming
a fixed-width whole-second string. No clock skew or early-use tolerance is added.
## The claim response is not an `ActionAuthorization` ## The claim response is not an `ActionAuthorization`
`GET /v1/approvals/{id}/claim` serves the **approval-claim** envelope defined in `GET /v1/approvals/{id}/claim` serves the **approval-claim** envelope defined in

View file

@ -0,0 +1,46 @@
{
"reviewer": "approval-engine-owner (agent approval-engine, codex)",
"decision": "approved",
"comment": "INTENT Purpose (valid right now), The Claim Shape and Consumption Ordering: approved for bounded Approval source time precision repair and Approval-owned fact/temporal correspondence in exact Access R3 source. Work APPROVAL-WP-0006-T01/T02. Repair patch SHA256 sha256:74eb0ff67d14508c2a2cee14d055790321ee171a96d5290ad6d037ce8f61dbcd against 3485415b823faf4f3b682079195e186666b0ed2b; store.py SHA256 sha256:866b275ed2d844d0aa9090ce9899ce955c6bfe32a493c2645c86e72efa343e86. Access R3 full patch SHA256 sha256:46bfeb706d3a88bf6054309cc9303eb360a91e57dda3b0fcbb7b80d551a75db6, delta sha256:17ecf399c50c07845c545b31cad4022200eb20dfb760595aa76a271e9cee7521; all 31 frozen source hashes matched. This review does not assent to Access policy/other-owner behavior, native credentials/trust, deployment or effects.",
"reviewed_repair_patch_sha256": "sha256:74eb0ff67d14508c2a2cee14d055790321ee171a96d5290ad6d037ce8f61dbcd",
"reviewed_access_full_r3_sha256": "sha256:46bfeb706d3a88bf6054309cc9303eb360a91e57dda3b0fcbb7b80d551a75db6",
"reviewed_access_r2_to_r3_sha256": "sha256:17ecf399c50c07845c545b31cad4022200eb20dfb760595aa76a271e9cee7521",
"approval_time_findings": [
"utc_now and iso preserve actual aware microseconds; no schema or binding algorithm change",
"First consume samples one authoritative time only after BEGINIMMEDIATE obtains the lock; identical instant checks validity, persists receipt/update and creates use outbox timestamp",
"Expiry remains exclusive; actual clock .750 refuses expiry .500; post-lock expiry cannot borrow pre-lock admission",
"Whole-second old rows, readback and idempotent original decision/timestamp are preserved without fabricated precision; no unconsume, retry authority or tolerance",
"No Access NotBefore backdating, caller wait/sleep acceptance or consumer timing waiver is needed"
],
"access_fact_temporal_findings": [
"Signed claim-free DENY normalization receipt supplies existing producer-evaluated native act and approval-free digest before issue, never permission; requester consumer independently authenticates it",
"Actual retained registry normalization is source-owned and number-preserving; final join compares the approved act/PDP digest and refuses drift instead of refreshing old context",
"Original and newly fetched facts are validated separately; no mutation of original claim/context or either request digest",
"Exact immutable operation deadline must fit caller, original/fetched freshness and approval validity; decision is precise post-evaluation NotBefore to exact operation deadline",
"Entire remaining interval must cover declared backendTTL plus 5-second cleanup reserve and source maximum 120; this review does not approve that policy or native budget",
"Default Approval freshness 30 cannot satisfy minimum 30+5; fixture freshness 120 is explicit synthetic configuration, not a producer default flip or native grant"
],
"validation": {
"repair_focused_passed": 89,
"repair_full_passed": 298,
"repair_full_failed_preexisting": 1,
"baseline_counterexamples_failed": 5,
"access_independent_command": "go test -race -count=1 ./internal/decision ./cmd/access-engine -run TestTrusted",
"access_independent_result": "passed"
},
"retained_failures": [
"coordinator-counterexample.json documents canonical expired fractional approval consumed",
"Initial repair expiry test attempted already-expired issuance; repaired engine correctly refused; fixture corrected to issue before expiry then advance",
"Existing deployment pin/historical release evidence mismatch remains unchanged"
],
"prior_evidence": "WP0005 closure and6079edR1review remain exact historical evidence; this is a new precision task and new exactR3 review",
"remaining_gates": [
"Coordinator independent source integration/publication/synchronization at exact repair and Access/Secrets successor source pins",
"Actual immediate strict CAS/whole-path remeasurement must use unchanged precise signed NotBefore and no sleep/waiver",
"Native issuer/profile, CA/source/read credential/store tenant admission and schema/image/config rollout remain existing WP0002T01",
"Actual use event/audit receiver reconciliation remains WP0002T04",
"Served PEP original decision/request CAS before effect, live one-shot journal, deadlines and confirmed cleanup remain WP0002T05 and existing Secrets/Platform work"
],
"native_accepted": false,
"authority_promotion": false
}

View file

@ -0,0 +1,130 @@
"""Actual source clock, locking and original immutable receipt precision."""
from datetime import timedelta, timezone
import sqlite3
import threading
from concurrent.futures import ThreadPoolExecutor
import pytest
from approval_engine.errors import Conflict, StoreUnavailable
from approval_engine.store import Engine, iso, utc_now
from tests.conftest import FROZEN, binding
DIGEST = "sha256:" + "ab" * 32
def issue(engine, start, end):
obj = engine.create(binding(), {"not_before": start.isoformat(), "expires_at": end.isoformat()})
return engine.add_entry(obj.id, "human", principal_type="human")
def uses(engine):
return [row for row in engine.undrained() if row["class"] == "use"]
def test_clock_and_iso_preserve_aware_precision_and_legacy_seconds():
now = FROZEN.replace(microsecond=750123)
offset = now.astimezone(timezone(timedelta(hours=2)))
assert utc_now(lambda: offset) == now
assert iso(offset) == "2026-08-29T12:00:00.750123+00:00"
assert iso(FROZEN) == "2026-08-29T12:00:00+00:00"
def test_fractional_expiry_refuses_actual_canonical_counterexample():
engine = Engine(clock=lambda: FROZEN)
obj = issue(engine, FROZEN-timedelta(seconds=1), FROZEN.replace(microsecond=500000))
engine.clock = lambda: FROZEN.replace(microsecond=750000)
assert engine.claim(obj.id)["valid_now"] is False
with pytest.raises(Conflict):
engine.consume(obj.id, DIGEST, decision_id="decision:original")
assert uses(engine) == []
def test_immediate_fractional_receipt_restart_and_outbox_use_one_instant(tmp_path):
current = [FROZEN.replace(microsecond=750123)]
path = tmp_path/"approval.sqlite"
engine = Engine(path, clock=lambda: current[0])
obj = issue(engine, FROZEN.replace(microsecond=700000), FROZEN+timedelta(seconds=1))
samples = []
def clock():
value = current[0] + timedelta(microseconds=len(samples))
samples.append(value)
return value
engine.clock = clock
result = engine.consume(obj.id, DIGEST, decision_id="decision:original")
assert samples == [current[0]]
assert result["consumed_at"] == current[0].isoformat()
assert result["consumed_at"] >= FROZEN.replace(microsecond=700001).isoformat()
event = uses(engine)[0]
assert event["created_at"] == event["payload"]["observed_at"] == result["consumed_at"]
engine.close()
engine = Engine(path, clock=lambda: FROZEN+timedelta(days=1), auto_migrate=False)
assert engine.get(obj.id).as_dict()["consumption"]["consumed_at"] == result["consumed_at"]
retry = engine.consume(obj.id, DIGEST, decision_id="decision:new")
assert retry == {**result, "idempotent": True}
assert uses(engine) == [event]
engine.close()
@pytest.mark.parametrize("after,accepted", [(450000, True), (750000, False)])
def test_consume_samples_after_real_sqlite_lock_wait(tmp_path, after, accepted):
current = [FROZEN.replace(microsecond=250000)]
path = tmp_path/"approval.sqlite"
engine = Engine(path, clock=lambda: current[0])
obj = issue(engine, FROZEN-timedelta(seconds=1), FROZEN.replace(microsecond=500000))
entered = threading.Event()
initialized = threading.Event()
start = threading.Event()
samples = []
def consume():
conn = engine._conn()
conn.set_trace_callback(lambda statement: entered.set() if statement == "BEGIN IMMEDIATE" else None)
initialized.set()
assert start.wait(2)
def clock():
samples.append(current[0])
return current[0]
engine.clock = clock
try:
return engine.consume(obj.id, DIGEST, decision_id="decision:original")
finally:
engine.close()
with ThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(consume)
assert initialized.wait(2)
lock = sqlite3.connect(path)
lock.execute("BEGIN IMMEDIATE")
start.set()
assert entered.wait(2)
current[0] = FROZEN.replace(microsecond=after)
lock.commit()
lock.close()
if accepted:
assert future.result(timeout=2)["consumed_at"] == current[0].isoformat()
else:
with pytest.raises(Conflict):
future.result(timeout=2)
assert samples == [current[0]]
assert len(uses(engine)) == int(accepted)
engine.close()
def test_old_second_precision_receipt_and_payload_are_never_backfilled(tmp_path):
engine = Engine(tmp_path/"old.sqlite", clock=lambda: FROZEN)
obj = issue(engine, FROZEN-timedelta(seconds=1), FROZEN+timedelta(seconds=1))
original = engine.consume(obj.id, DIGEST, decision_id="decision:old")
event = uses(engine)[0]
assert original["consumed_at"] == "2026-08-29T12:00:00+00:00"
engine.clock = lambda: FROZEN + timedelta(days=1, microseconds=123456)
assert engine.consume(obj.id, DIGEST, decision_id="decision:new") == {**original, "idempotent": True}
assert uses(engine) == [event]
def test_fractional_use_outbox_failure_rolls_back(engine):
engine.clock = lambda: FROZEN.replace(microsecond=123456)
obj = issue(engine, FROZEN, FROZEN+timedelta(seconds=1))
engine.fail_outbox = True
with pytest.raises(StoreUnavailable):
engine.consume(obj.id, DIGEST)
assert engine.get(obj.id).consumed_at is None
assert uses(engine) == []

View file

@ -0,0 +1,65 @@
---
id: APPROVAL-WP-0006
type: workplan
title: "Preserve fractional authoritative approval and consumption time"
domain: infotech
repo: approval-engine
status: active
flavor: implementation
owner: codex
topic_slug: netkingdom
created: "2026-10-16"
updated: "2026-10-16"
depends_on: []
---
Explicit CUST-WP-0080 precision defect demand: actual clock .750 was rounded to
.000, accepting consume beyond a .500 expiry and recording evidence earlier than
the actual signed decision. INTENT basis: valid right now, exact-bound atomic
consumption and faithful source evidence. Fix source time, not Access backdating
or a consumer waiver. This does not promote native issuer/deployment authority.
## Preserve clock precision and one post-lock consumption instant
```task
id: APPROVAL-WP-0006-T01
status: done
priority: high
```
Preserve aware UTC microseconds through clock/ISO formatting. After acquisition
of the SQLite consume lock, sample one time for validity, stored consumption
receipt/update and use-event occurrence. Preserve old second-precision rows,
original idempotent receipt/decision and local transactional atomicity.
Seven added regressions: baseline five failed/two passed; repaired 89 focused
checks pass. Full source suite: 298 passed, one pre-existing deployment pin vs
historical release-evidence mismatch retained. One initial test attempted issue
after expiry and was corrected to issue before expiry, then advance the clock;
the repaired source correctly refused that expired setup. Source change needs no
schema migration and preserves old rows/idempotent timestamps exactly.
## Review temporal source successor and hand off independently pinned repair
```task
id: APPROVAL-WP-0006-T02
status: progress
priority: high
```
Supply exact patch/test/source review evidence for coordinator integration and
Access/Secrets immediate strict CAS remeasurement. Review only Approval facts,
normalized preapproval correspondence and temporal bounds on the exact successor
source. Existing WP0005 closure/R1 review remain historical; native issuer,
audit receiver, served-PEP/CAS/effect and cleanup gates remain WP0002 T01/T04/T05
and the existing Access/Secrets/Platform records. No native gate is closed here.
Source owner review and handoff are prepared in
`docs/reviews/2026-10-16-fractional-consumption-temporal-approval-owner-review.json`.
Standalone repair patch is frozen separately from the Access R3 fact review;
89 focused source checks and 298 full checks pass, with one known image-evidence
mismatch. Access precise NotBefore remains unchanged and original claim freshness
is never renewed. Exact R3 normalized-preapproval/temporal source review covers
Approval facts only; native freshness120 configuration is not granted. Independent
integration/publication and actual immediate strict CAS successor remeasurement
remain pending coordinator/consumer evidence, not a reason to backdate or waive.