# Durable storage for the draft 3 free service

`profiles/free_store.py` implements private SQLite persistence for the two-origin free agreement service. It is a new draft 3 component; the published draft 2 store is unchanged. Wire validation, signatures, authentication, current authority and admission decisions remain the responsibility of the service and free-contract validators.

## Interface

`FreeStore(path, identity, fault_hook=None)` takes exactly `{origin, service_id, issuer}` as its identity. All three values are canonical HTTPS identifiers; service ID belongs to origin, while issuer can be a third-party provider. The identity and storage version are immutable settings and must match on restart.

`transaction()` yields a `sqlite3.Row` connection after `BEGIN IMMEDIATE`. Related writes, retry checks and final trusted guards must occur within that context. An exception rolls back; a successful commit uses rollback journaling, `synchronous=FULL`, foreign keys and recursive triggers. `connect()` provides the same settings and closes on context exit, but otherwise uses autocommit: it does not make multiple operations atomic.

The service must sample its trusted clock after obtaining the write lock and repeat authority/key/expiry checks after any later blocking guard. The store does not fetch authority, authenticate requests or call a clock. It takes no payment locks or network actions.

`fault_hook(stage)` is a trusted constructor-level test facility called at `before_commit` and `after_commit`; schema initialization does not invoke it. Pre-commit failure rolls back. Post-commit failure represents a lost response: the committed result survives, and a retry returns it. Production should omit this hook.

## Tables and invariants

The module's `SCHEMA` tuple is the source of truth for exact column names and order.

| Table | Material and constraints |
| --- | --- |
| settings | Storage version and immutable identity; update/delete/replacement blocked. |
| clients | Client ID, SHA-256 secret hash, role agent/gateway/admin, principal-ID bytes, public JWK/key ID, reporter ID and active flag. |
| documents | Exact body and media type keyed by SHA-256 digest; immutable. |
| offers | ID, service/client/principal, nonce and exact body; service/nonce unique; immutable. |
| agreements | One per offer; unique service/client acceptance idempotency key; exact acceptance bytes/JWS and receipt; current access state/version/time/reason; fixed access/use expiry. Owner and service must match the offer. |
| status_history | Original signed status by agreement and positive version; immutable. |
| status_attestations | Every emitted current-status JWS by exact-byte digest, agreement and lifecycle version; immutable. Multiple fresh statements can share one unchanged lifecycle version. |
| tokens | Token hash, agreement and expiry; binding cannot be updated. Deletion can discard a credential while retaining the agreement. |
| commands | Exact request and response status/header bytes/body keyed by client, route and idempotency key; immutable. |
| admissions | Unique decision and gateway/request identities, agreement, exact context, single-assignment terminal event. |
| records | Collector sequence/record ID, reporter namespace/event ID, exact payload, original intake JWS and client/agreement binding; immutable and unique per reporter/event. |
| decisions | Operation ID, optional agreement, exact operation/decision/authority/policy/observation bytes; immutable. |

Historical signatures and fixed agreement bindings cannot be overwritten, including through replacement inserts. Current access states are active, revoked or expired. State changes require increasing status versions; terminal state cannot reactivate. Revocation leaves the original use expiry unchanged.

The service must start at version 1, apply normative transitions and return original committed responses for duplicate acceptance/revocation. Version means a lifecycle transition, not a status poll. Retain the first statement for each version in status_history, and every newly emitted fresh statement in status_attestations keyed by SHA-256 of the exact ASCII compact JWS. The initial receipt can establish the accepted state; status statements are retained as the service emits them. The store does not verify that signed payloads match indexed columns or their digest: those are service transaction invariants. Opaque bytes are not interpreted as valid draft 3 contracts merely because they were stored.

## Retry helpers

All helpers take the caller's existing transaction connection:

- `put_document(db, body, media_type)` returns a digest. Exact retries reuse the row; another media type under the digest conflicts.
- `check_command(db, client_id, route, idempotency_key, body)` returns no row or the original response. Different exact bytes under the same key raise `FreeStoreConflict`, including whitespace-only JSON differences.
- `record_command(db, client_id, route, idempotency_key, body, response_status, response_headers, result)` stores a response or returns the original. It never replaces the original signature/receipt on retry.
- `check_record(db, namespace, event_id, payload)` compares exact reporter/event bytes.
- `record_record(db, record_id, namespace, event_id, payload, jws, client_id=None, agreement_id=None)` stores an intake or returns the original. Retrying cannot change the client/agreement association, collector record ID or retained intake signature.

The service derives namespace from authenticated reporter identity and verifies each submitted signature before deduplication. Original event signatures must remain available inside the signed intake materials when required by the wire contract. Storage does not infer semantic operation equivalence across different event IDs.

Helpers limit exact body/payload/header bytes to 1 MiB and response bodies to at most 1 MiB; wire validators may impose smaller limits. These helper checks do not validate direct SQL inputs. All check/write pairs must occur in one transaction. SQL uniqueness and immutable triggers provide the durable conflict boundary.

## Privacy and raw material access

The parent directory must be private and owned (0700 when created); the file must be private, owned, regular, not a symlink or hard link (0600 when created). Existing public permissions are rejected. Applications must control ancestor directories and prevent same-user replacement. This is access control, not encryption or protection against a compromised application/administrator.

Credential fields require lowercase 64-hex SHA-256 values. The service must actually hash high-entropy client secrets and access tokens: formatting cannot prove hashing or protect guessable passwords. Plaintext bearer tokens and client secrets must not be cached in command responses. The command cache is intended for repeatable signed agreement/revocation responses; token issuance needs separate handling that preserves hash-only storage. Private signing keys do not belong here.

`raw_materials(agreement_id)` returns local application input only: offer/acceptance bytes, acceptance JWS, receipt, the first status signatures per lifecycle version, every retained status attestation, and collector intake signatures. It excludes client tables, credential hashes, tokens and command responses. Results are labelled `local_raw_materials` with `portable_verification_established: false`.

This helper is not an authenticated HTTP export, ownership check, closed migration manifest or independent proof. The caller authorizes the reader and validates opaque contents; the store cannot redact secrets accidentally embedded in a supposedly public artifact.

No retention workflow, signed export closure, quota manager, encrypted backup, production migration or replication is implemented. Normal connections cannot delete immutable retained records. Future retention/migration work must preserve its audit and privacy semantics explicitly.

## Verification

**23 focused tests passed** using this command from the candidate directory:

```sh
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p test_free_store.py -v
```

Coverage includes restart with exact bytes/identity, private files, credential formats and exclusion from raw materials, command/report conflicts, concurrent writers, one agreement per offer, immutable signed history, terminal state, admission uniqueness, foreign keys, rollback, and actual subprocess exits immediately before/after commit. Fixtures are deliberately opaque and do not claim valid signatures.

These results establish the tested local storage behavior, not the two-origin HTTP lifecycle, independent client conformance, every filesystem's durability, paid settlement or protocol release-candidate status.
