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:
parent
3485415b82
commit
d40e842052
6 changed files with 271 additions and 6 deletions
|
|
@ -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
|
||||
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
|
||||
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.
|
||||
|
||||
`go test -race ./...` checks the portable producer vectors, typed fact pairing,
|
||||
|
|
|
|||
|
|
@ -103,11 +103,11 @@ def utc_now(clock: Callable[[], datetime] | None = None) -> datetime:
|
|||
now = datetime.now(timezone.utc)
|
||||
if now.tzinfo is None:
|
||||
now = now.replace(tzinfo=timezone.utc)
|
||||
return now.astimezone(timezone.utc).replace(microsecond=0)
|
||||
return now.astimezone(timezone.utc)
|
||||
|
||||
|
||||
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:
|
||||
|
|
@ -768,7 +768,6 @@ class Engine:
|
|||
):
|
||||
raise Unprocessable("decision_id must be a non-empty string")
|
||||
conn = self._conn()
|
||||
now = iso(self.now())
|
||||
idempotent = False
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
|
|
@ -794,7 +793,11 @@ class Engine:
|
|||
if row["status"] != "approved":
|
||||
conn.rollback()
|
||||
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()
|
||||
raise Conflict("cannot consume outside validity window")
|
||||
if row["human_control"]:
|
||||
|
|
@ -819,6 +822,7 @@ class Engine:
|
|||
"use",
|
||||
approval_id,
|
||||
actor=row["actor"],
|
||||
observed_at=consumed_at,
|
||||
extra={
|
||||
"binding_digest": row["binding_digest"],
|
||||
"binding": json.loads(row["binding_json"]),
|
||||
|
|
@ -911,13 +915,14 @@ class Engine:
|
|||
*,
|
||||
actor: str | None,
|
||||
extra: dict[str, Any] | None = None,
|
||||
observed_at: datetime | None = None,
|
||||
) -> str:
|
||||
if self.fail_outbox:
|
||||
raise StoreUnavailable("outbox insert failed")
|
||||
if event_class not in EVENT_CLASSES:
|
||||
raise Unprocessable(f"unknown event class {event_class}")
|
||||
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"
|
||||
details = {"class": event_class, **(extra or {})}
|
||||
if approval_id:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`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`
|
||||
|
||||
`GET /v1/approvals/{id}/claim` serves the **approval-claim** envelope defined in
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
130
tests/test_timestamp_precision.py
Normal file
130
tests/test_timestamp_precision.py
Normal 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) == []
|
||||
65
workplans/APPROVAL-WP-0006-fractional-consumption-time.md
Normal file
65
workplans/APPROVAL-WP-0006-fractional-consumption-time.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue