From dc6d886d265f6cd7d1ff5cde6f65450f26530835 Mon Sep 17 00:00:00 2001 From: XV Date: Thu, 16 Jul 2026 21:55:42 -0400 Subject: [PATCH 1/7] {"schema":"decodex/commit/2","change":"Record exact PostgreSQL command authority proof","authority":"manual","impact":"compatible"} --- openwiki/architecture/runtime-architecture.md | 36 +- openwiki/decisions/vnext-authority.md | 28 +- .../xy-1345-exact-command-authority.md | 170 +++ .../operations/commands-and-validation.md | 10 + openwiki/quickstart.md | 1 + openwiki/specs/vnext-authority.md | 55 + openwiki/specs/vnext-gates.md | 43 +- scripts/vnext/exact_command_prototype.py | 1238 +++++++++++++++++ tests/scripts/test_exact_command_authority.py | 154 ++ 9 files changed, 1725 insertions(+), 10 deletions(-) create mode 100644 openwiki/evidence/xy-1345-exact-command-authority.md create mode 100644 scripts/vnext/exact_command_prototype.py create mode 100644 tests/scripts/test_exact_command_authority.py diff --git a/openwiki/architecture/runtime-architecture.md b/openwiki/architecture/runtime-architecture.md index 831c44e7..7dfc5f7c 100644 --- a/openwiki/architecture/runtime-architecture.md +++ b/openwiki/architecture/runtime-architecture.md @@ -367,7 +367,9 @@ forces dispatch disabled. This slice exposes no account selection, live turn sta automatic rollover, Context-Pack dispatch, ambiguous replay, or scheduler wake; XY-1304 remains the separate failed enablement gate. XY-1304 owns experiment creation and positive observation acquisition; XY-1276 owns production Quick Task creation. XY-1272 owns only PostgreSQL -configured-principal and ACL authority closure against V8, while XY-1337 owns the expected V9. +configured-principal and ACL authority closure against V8. XY-1345 owns accepted exact-command +authority/prototype evidence, XY-1346 owns expected V9 exact receipts plus RoleProfiles, and +re-bounded XY-1337 owns expected V10 RuntimeSession snapshots and transitions. The composition also reports conversation execution unavailable. Authentication, TLS, remote binding, HTTP artifact transfer, MCP, scheduling, live Codex execution, mutating @@ -378,12 +380,32 @@ pool lifecycle: closing the pool makes it unavailable. Live PostgreSQL failures authoritative at each asynchronous store operation; availability does not claim a synchronous network-liveness probe. -The store's owned mutations commit an immutable pending command receipt before effects. A fenced -claim then applies the exact expected revision, appends activity, enqueues the matching outbox -effect, stores the exact response bytes, and completes the receipt atomically in transaction B. -Reusing the same complete command identity returns those original bytes; changing operation, scope, -entity, expected revision, payload, or canonical request is rejected before effects. Durable exact -history replay retains its immutable version and referenced blob while the receipt exists. Outbox claims are bounded and +Pure database commands do not use that split-phase flow. They execute through an +operation-specific, command-complete migration-owner `SECURITY DEFINER` function. PostgreSQL builds +the complete JSONB request from the typed values it consumes, inserts or waits on +`exact_command_receipts(protocol_version, idempotency_key)`, and completes the receipt, domain +mutation, canonical activity, outbox, and stored response in one transaction. Runtime has only the +operation-function `EXECUTE` grant: it has no exact-receipt table privilege, private-helper access, +or canonical activity/outbox mutation authority. A deferred constraint trigger rejects any commit +with an executing row; completed rows and authoritative response bytes are immutable and +undeletable. Stable domain rejection completes and replays, while cancellation, connection, +serialization, deadlock, and unexpected database failures roll back. + +Normal exact-command execution is one command per top-level `READ COMMITTED` transaction. A +separate read/lock follows `ON CONFLICT DO NOTHING`; `40001` and `40P01` retry the complete +identical transaction. JSONB identity uses equality, explicit null keys, typed enum/numeric values, +and exact PostgreSQL text semantics. Effects use actual `RETURNING` rows and canonical audit +identities. Operation is part of the envelope, so cross-operation key reuse conflicts. The accepted +proof and vertical ownership are in +[the XY-1345 evidence](../evidence/xy-1345-exact-command-authority.md); production V9 belongs to +XY-1346 and V10 to re-bounded XY-1337. + +Legacy `command_receipts` retain the receipt-first fenced-claim protocol only for unrelated blob, +filesystem, external, or long-running sagas whose point of no return cannot fit in one PostgreSQL +transaction. Such a flow commits an immutable pending receipt before effects; a fenced claim then +applies the expected revision, appends activity, enqueues outbox, stores exact response bytes, and +completes transaction B. Durable exact history replay retains its immutable version and referenced +blob while the legacy receipt exists. Outbox claims are bounded and fenced by a token rotated on every claim or reclaim. Any effect that may have begun must be reconciled through a meaningful receipt and authoritative readback after claim expiry or restart. Lease, retry, and retention durations are exact positive whole milliseconds capped diff --git a/openwiki/decisions/vnext-authority.md b/openwiki/decisions/vnext-authority.md index 7958ece2..bc2dfaf1 100644 --- a/openwiki/decisions/vnext-authority.md +++ b/openwiki/decisions/vnext-authority.md @@ -43,12 +43,38 @@ old-boundary candidates remain historical provenance in the runtime proof. XY-1272 PostgreSQL-authority reset accepted 2026-07-16: XY-1272 owns only configured-principal and ACL manifest/readiness closure against landed V8. It owns no migration or Codex creation, -mapping, or reconciliation surface. XY-1337 owns the expected V9; XY-1304 owns experiment creation +mapping, or reconciliation surface. Under the later XY-1345 reset, XY-1346 owns expected V9 and +re-bounded XY-1337 owns expected V10; XY-1304 owns experiment creation and positive observation acquisition; XY-1276 owns production Quick Task creation. Lossy or paginated Codex evidence cannot authorize negative `Present`, `Complete`, or context-free `Absent` authority. A future configured PostgreSQL role must atomically extend configuration, bootstrap, manifest/readiness, and negative tests. +XY-1345 exact-command authority amendment accepted 2026-07-16: pure database commands use +operation-specific, command-complete `SECURITY DEFINER` entrypoints. PostgreSQL constructs and +consumes the complete typed request envelope; runtime supplies only the protocol-scoped +idempotency key and typed operation inputs. A separate `decodex.exact_command_receipts` relation is +keyed by `(protocol_version, idempotency_key)`, while operation identity remains inside the +envelope so cross-operation reuse conflicts. Runtime has no exact-receipt table privilege and no +private-helper or canonical activity/outbox mutation authority. An executing row is transaction +internal: a `DEFERRABLE INITIALLY DEFERRED` commit-time invariant rejects every incomplete commit, +and completed response bytes and effects are immutable and undeletable. Stable domain rejection, +idempotency conflict, and retryable infrastructure failure are distinct outcomes. + +Normal execution is one exact command per top-level `READ COMMITTED` transaction. The command uses +a later read/lock statement after `ON CONFLICT DO NOTHING`; classified `40001` and `40P01` failures +retry the whole identical transaction. Request JSONB uses equality, includes every optional key +with explicit JSON null, receives enums/numerics through typed inputs, and preserves exact +PostgreSQL text/code-point semantics. Derived revisions, selected rows, generated identities, +timestamps, digests, snapshots, activity/outbox identities, and responses are effects. Effect and +response evidence comes from actual `RETURNING` rows and canonical audit identities. + +Candidate 3 is superseded as implementation and remains hostile-test/design provenance only. +[XY-1345 evidence](../evidence/xy-1345-exact-command-authority.md) records the passing isolated +PostgreSQL 18 proof. The serial vertical order is XY-1345 authority/prototype, then XY-1346 exact +receipts plus RoleProfile V9, then re-bounded XY-1337 RuntimeSession V10. Legacy +`command_receipts` semantics remain unchanged for unrelated external or long-running sagas. + ## Decision Decodex vNext is a rebuild of the agent workspace, not an incremental extension of the diff --git a/openwiki/evidence/xy-1345-exact-command-authority.md b/openwiki/evidence/xy-1345-exact-command-authority.md new file mode 100644 index 00000000..d0574664 --- /dev/null +++ b/openwiki/evidence/xy-1345-exact-command-authority.md @@ -0,0 +1,170 @@ +# XY-1345 Exact PostgreSQL Command-Authority Proof + +Status: accepted architecture evidence for XY-1345; non-production prototype only. + +As-of repository revision: `39fda2f3d526b4d811a10dfe73fe786d207ca5ae` plus the +unstaged XY-1345 candidate described here. The proof ran on 2026-07-16 in the assigned +`xv/xy-1345-command-authority-reset` worktree. + +## Result + +The corrected exact-command architecture passed. No acceptance threshold was weakened: + +- PostgreSQL: `postgres (PostgreSQL) 18.4` (`psql`, `pg_dump`, and `pg_restore` also 18.4). +- Harness: `scripts/vnext/exact_command_prototype.py`. +- Harness SHA-256: `0b5ddc002affedd2988ef9367db1c3ca64e7db8469e38e865efbac659331e939`. +- Embedded schema/fixture SQL SHA-256: + `47e4be2cb1f7f9b20616d3ef86cf34b817e90ddc7431ceeea65017531712bf9e`. +- Exact transition function source SHA-256: + `3eb5789e5e8f2df5e628eafc0dfa551d4ec69b029a9db611e0c736e0da33f5d4`. +- Closed eleven-function `pg_get_functiondef` digest-manifest SHA-256: + `d7b27ed3e42e7ac5a376afd8c3fd19aff039bcbed43d52ac17996b6d7efba2a5`. +- Populated receipts/RuntimeSessions/activity/outbox snapshot SHA-256: + `7ba6bf1b1a5f86a2e960d555b13069cc784b7373fecba81cfb9d31a05f7f41db`. +- Populated completed-receipt response-byte aggregate SHA-256 for this run: + `d14cb64cdb38cdc3d5f302006a3632eda82837054f1fb445e83e8ddc7fbcc917`. +- Result schema: `decodex/xy-1345-exact-command-proof/1`. +- Result: `PASSED`. + +The response-byte aggregate includes generated identities and timestamps and is therefore a +run receipt, not a cross-run golden value. The dump/restore comparison used the same aggregate on +both sides and matched exactly. + +## Exact commands + +The operator commands were: + +```sh +postgres --version +python3 scripts/vnext/exact_command_prototype.py +``` + +The harness resolved the active `initdb` symlink and its matching PostgreSQL share directory, then +issued the following command shapes. `$PROOF_ROOT` was the private random directory returned by +`tempfile.mkdtemp(prefix="decodex-xy1345-")`; it was removed before the result was printed. + +```sh +initdb -D "$PROOF_ROOT/data" --encoding=UTF8 --locale=C --auth=trust \ + --no-instructions -L "$RESOLVED_POSTGRES_SHARE" +pg_ctl -D "$PROOF_ROOT/data" -l "$PROOF_ROOT/postgres.log" \ + -o "-k $PROOF_ROOT/socket -p 55435 -h '' -F" -w start +createdb decodex_xy1345 +psql -X -v ON_ERROR_STOP=1 -At +pg_dump -Fc -f "$PROOF_ROOT/xy1345.dump" decodex_xy1345 +createdb decodex_xy1345_restore +pg_restore -d decodex_xy1345_restore "$PROOF_ROOT/xy1345.dump" +pg_ctl -D "$PROOF_ROOT/data" -m immediate -w stop +``` + +Every SQL session used verbose SQLSTATE output and `ON_ERROR_STOP=1`. TCP was disabled with +`-h ''`; the private socket directory made the fixed socket port collision-free. The harness did +not enumerate, connect to, stop, or modify any existing PostgreSQL service. + +## Result matrix + +| Schedule or invariant | Observed result | +| --- | --- | +| Same key and same envelope in two sessions | Second session waited 0.663 seconds and returned byte-identical stored response bytes. | +| Same key and changed target state | `DX001` idempotency conflict before a second domain/activity/outbox effect. | +| Same key and changed operation | `DX001` exact conflict. | +| Abort after receipt, domain, activity, or outbox | Each injected `DX900` rolled back; a waiting transaction became executor; each case converged to one receipt, domain effect, activity, and outbox row. | +| Backend terminated before commit | `57P01`, zero durable receipt/effect, identical whole-command retry executed once. | +| Commit with discarded client result | Retry returned the stored response bytes exactly. | +| Missing target, stale revision, illegal transition | Each committed a completed stable rejection and replayed unchanged after the database was changed to make a new call succeed. | +| Early incomplete return | Deferred commit failed with `23514`; zero executing rows committed. | +| Completed-row update/delete/truncate as owner | Each failed with `23514`. | +| Runtime exact-receipt SELECT/INSERT/UPDATE/DELETE/TRUNCATE | All five failed with `42501`. | +| Runtime canonical audit forgery | Aggregate/event, structured activity payload, effect-key, and activity-link variants all failed with `42501`. | +| `READ COMMITTED` contention | Wait/replay succeeded; one domain effect. | +| `REPEATABLE READ` contention | Contender returned classified `40001`; whole identical transaction retry replayed; one effect. | +| `SERIALIZABLE` contention | Contender returned classified `40001`; whole identical transaction retry replayed; one effect. | +| Opposite-order two-key transaction | Exactly one `40P01`; whole loser transaction retry converged; one effect per target. | +| Optional request fields | `codex_thread_id`, transition `note`, and all four bootstrap `provenance` keys existed with JSON null. | +| NFC/NFD, case, trailing whitespace | All three changed envelopes conflicted under exact PostgreSQL text semantics. | +| Integer literal, bound `bigint`, text-cast integer | All produced equal typed envelopes. | +| Bootstrap request shape | Four role-implied scalar groups produced advisor/lead/task/reviewer order; incomplete input returned `22004`; no role or array input exists. | +| Effect/response binding | A query decoded every completed-success `response_bytes`, compared its response effect to `effect_envelope`, and joined that envelope to the current persisted RuntimeSession plus actual activity/outbox rows and identities; mismatch count was zero. | +| Catalog and populated restore | The closed eleven-function/helper manifest, exact signatures/overloads, owner, `prosecdef`, language, volatility, parallel safety, settings, source digests, dependencies, semantic function ACLs, normalized complete table privilege closure, default privileges, memberships, trigger definitions, response bytes, and all persisted effect rows matched after restore. | +| Cleanup | The authoritative `pg_ctl ... stop` result succeeded, the harness recorded `cluster_stopped=true`, and only then removed the temporary root. A stop or removal failure makes the proof `FAILED`, cannot print `PASSED`, and preserves truthful cleanup state. | + +Receipt-table ACL restore closure is semantic, not raw `pg_class.relacl` text identity. PostgreSQL +may serialize owner-only default authority as either a null ACL or an explicit owner-only ACL across +dump/restore. The final harness normalizes the ACL through `aclexplode`, requires exactly the owner +as grantee with no grant options, and rejects unexpected grantees. Separately, it requires all eight +PostgreSQL 18 table privileges (`SELECT`, `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, `REFERENCES`, +`TRIGGER`, and `MAINTAIN`) to be effective for the owner and ineffective for both runtime and PUBLIC. +The same normalized semantic catalog must match after restore; raw `relacl` serialization is never +compared. + +Function authority is closed over all eleven prototype functions: the command-complete definer, +incomplete-row probe, rejection/failpoint helpers, four envelope builders, and three trigger +functions. The manifest checks exact identity types and overload counts, owner, invoker/definer +mode, language, volatility, parallel safety, trusted search path, effective and normalized semantic +function ACLs, `pg_get_functiondef` digests, dependencies, and restore parity. Runtime can execute +only the command-complete definer; PUBLIC can execute none. The owner-wide default function ACL—not +an ineffective per-schema subtraction of PostgreSQL's global PUBLIC default—is normalized to the +single expected owner EXECUTE entry and restored unchanged. + +### Stress threshold + +| Workload | Repetitions × width | Successes | Conflicts | Anomalies | +| --- | ---: | ---: | ---: | ---: | +| Identical envelope | 50 × 32 | 1,600 | 0 | 0 | +| Mixed envelope | 50 × 32 | 800 | 800 | 0 | + +Across both workloads there were zero duplicate domain effects, duplicate canonical +activity/outbox pairs, mismatched responses, committed executing rows, authority bypasses, +unexplained rows, or unclassified SQLSTATEs. + +## Outcome classification + +- Stable domain rejection: completed stored responses for `missing_target`, `stale_revision`, and + `illegal_transition`. These are returned bytes, not raised database failures. +- Idempotency conflict: `DX001`; no effect is committed by the conflicting transaction. +- Retryable infrastructure failure: observed `40001`, `40P01`, `57P01`, and injected failpoint + `DX900`. `08006` remains in the closed connection-failure class even though this run's forced + termination produced `57P01`. +- Expected authority/shape denials (`42501`, `23514`, `22004`) are negative security assertions, + not command outcomes and are never converted into stable domain responses. + +No cancellation, deadlock, serialization, connection, or unexpected database exception was +stored as a stable rejection. + +## Requirement mapping + +| Requirement | Proof | +| ---: | --- | +| 1 | Two-session wait plus byte-identical replay. | +| 2 | Changed target produced `DX001` and effect count remained one. | +| 3 | Changed operation under the protocol/key produced `DX001`. | +| 4 | Four injected rollback stages, each with a blocked waiter that became executor. | +| 5 | `pg_terminate_backend` before commit produced `57P01`, zero rows, then one execution. | +| 6 | Discarded committed result replayed from `response_bytes`. | +| 7 | Three completed stable rejections replayed after later state changes. | +| 8 | `DEFERRABLE INITIALLY DEFERRED` constraint trigger produced `23514`. | +| 9 | Five exact-receipt privilege denials as runtime. | +| 10 | Obvious and structured/link canonical activity/outbox forgery denials. | +| 11 | Three isolation schedules plus opposite-order `40P01` and whole-transaction retry. | +| 12 | Every tested optional key existed with JSON null. | +| 13 | NFC/NFD, case, and trailing-space conflicts. | +| 14 | Literal, prepared/bound, and text-cast inputs converged after `bigint` typing. | +| 15 | Four fixed scalar configuration groups; no caller role/array surface; incomplete rejection. | +| 16 | Effect and response mechanically joined to actual returned session/activity/outbox identities. | +| 17 | Populated custom-format dump/restore preserved catalog authority, rows, bytes, effects, and digests. | + +## Authority conclusion and limitations + +The prototype did not falsify the accepted architecture. It proves that PostgreSQL 18.4 can +implement the required transaction, replay, rejection, concurrency, privilege, catalog, and +restore semantics without a caller hash/token/lease or committed pending receipt. + +It is deliberately not production code. It contains one representative RuntimeSession CAS command +and pure builders for all four future request shapes. It adds no migration, Rust API, daemon path, +compatibility layer, or change to legacy `command_receipts`. It does not prove the future V9/V10 +domain schemas, the repository's production authority manifest, V8-to-V9 upgrade, clean V1-to-V9 +bootstrap, old-writer cutover, or application retry implementation. XY-1346 and then XY-1337 must +re-prove those properties on their exact production candidates. Multiple exact commands in one +caller transaction remain outside the no-deadlock guarantee. PostgreSQL versions other than 18.4 +were not tested by this receipt. + +Candidate 3 remains superseded. None of its SQL or Rust command path is included here. diff --git a/openwiki/operations/commands-and-validation.md b/openwiki/operations/commands-and-validation.md index 28046510..2b31fbae 100644 --- a/openwiki/operations/commands-and-validation.md +++ b/openwiki/operations/commands-and-validation.md @@ -24,6 +24,7 @@ It depends on build, node checks, Rust checks, formatting, lint, and tests (`Mak | XY-1308 API-only CLI and diagnostic matrix | `cargo make test-vnext-cli-diagnostics` | | vNext dependency architecture | `cargo make test-vnext-architecture` | | vNext PostgreSQL store, Conversation history, blobs, and Context Packs | `cargo make test-vnext-postgres-store` | +| XY-1345 isolated exact-command authority proof | `python3 scripts/vnext/exact_command_prototype.py` | | vNext storage feasibility proof | `cargo make test-vnext-storage-proof` | | Rust formatting | `cargo make fmt-rust-check` | | Canonical gate contract | `cargo make test-gate-contract` | @@ -89,6 +90,15 @@ untrusted server-text cases. The XY-1267/XY-1307 integration command and XY-1264 storage proof are intentionally separate because they require an intended macOS host with one PostgreSQL 18 distribution. Each creates and removes its own isolated temporary checksummed cluster with TCP disabled and never enumerates or changes an existing service. The PostgreSQL command provisions fixture-only migration/runtime roles, proves least-privilege daemon bootstrap, and rejects 27 unsafe roots covering direct, inherited, NOINHERIT/SET-only, and membership-admin paths to forbidden role attributes, PostgreSQL 18 namespace-object ownership (including distinct collation, conversion, operator, and text-search cases), DDL, table/ledger/sequence mutation, grant options, `session_replication_role` SET/ALTER SYSTEM, retention bypass, trigger drift, extension-member control, an indirect public-function trigger, and a genuinely additional function. It closes every runtime-callable Decodex function over exact signatures, overloads, metadata, settings, and canonical source. All 34 shipped functions have the exact secure `pg_catalog, decodex` function-local search path. The three shipped security definers are the bounded cursor issuer, bounded cursor/history-version pruner, and trigger-only history-version capture function; runtime cannot insert cursors or execute capture directly. The additional thirty-fifth function is migration-owned, runtime-executable, `SECURITY DEFINER`, configured with an unsafe setting, and is invoked as runtime to perform owner-authority trigger DDL before fixture restoration and independent rejection. The separate substitution fixture replaces a shipped safety body without changing its signature. The indirect-trigger fixture proves runtime DML executes a public definer function despite direct `EXECUTE`, protected-table `UPDATE`, and `TRIGGER` all being denied. The extension fixture proves a public runtime-owned extension can transactionally drop it. Six incompatible roots cover missing ledger SELECT, canonical-function drift, a dropped credential constraint with demonstrated credential insertion, an external child cascade with demonstrated runtime-mediated deletion, a same-count tampered migration ledger, and absent `pgcrypto`. The canonical PostgreSQL 18 schema manifest closes defaults, constraints on both foreign-key sides, indexes, enums, and internal constraint-trigger semantics. Descriptor-pinned socket unit fixtures reject a same-UID pre-planted endpoint in a world-writable configured directory, a mismatched operator UID pin, replaced ancestors, replaced endpoints, and deterministic replacement between precheck and failed connect; an unchanged secure stale socket maps to unreachable. An isolated daemon fixture starts Ready, replaces the configured endpoint, and proves a fresh V1.2 doctor query becomes unsafe-host-path without migration or repinning. The runtime protocol tests keep mutation receipt lookup/capacity independent across V1.1/V1.2 and prove repeated, ordered, concurrent live queries neither replay nor consume receipts. The adapter contract tampers a ledger name at constant row count and removes `pgcrypto` after bootstrap, proving read-only live revalidation reports both as incompatible before restoration. The harness also exercises an in-flight Rust BlobSession across an immediate PostgreSQL restart: the old session loses its hash lock and transaction-B connection, its stale claim cannot complete, and a reassigned exact retry verifies already-published bytes before committing metadata. It also proves `setval` denial, same-signature callable hostile-`search_path` safety, Turkish ICU credential behavior, and populated dump/restore. The XY-1264 proof additionally exercises rollback, blob, and cache behavior (`crates/decodex-postgres/src/socket.rs`, `crates/decodex-runtime/tests/bootstrap_doctor.rs`, `scripts/vnext/postgres_store_test.py`, `spikes/vnext-storage/proof.py`, `spikes/vnext-storage/README.md`). +The XY-1345 command is a separate non-production architecture proof. It requires exactly +PostgreSQL 18.4, creates a private temporary cluster with TCP disabled, installs only fixture +roles/objects, exercises the deterministic and 50-by-32 stress schedules, performs populated +`pg_dump`/`pg_restore`, stops the cluster, and removes its temporary root on success or failure. Its +JSON output is the command receipt; any `FAILED` result or nonzero anomaly count falsifies the gate. +It does not run the production migration ledger and cannot substitute for the V9 clean-bootstrap, +V8-to-V9 upgrade, authority-manifest, hostile SQL, and restore tests owned by XY-1346. See +[the durable evidence page](../evidence/xy-1345-exact-command-authority.md). + The PostgreSQL integration harness bootstraps the shipped four-migration history (`V1` foundation, `V2` claim indexes, forward-only `V3` Conversation history, and forward-only `V4` account readiness with the honest `unavailable` observation), verifies diff --git a/openwiki/quickstart.md b/openwiki/quickstart.md index fb02962c..69455822 100644 --- a/openwiki/quickstart.md +++ b/openwiki/quickstart.md @@ -17,6 +17,7 @@ OpenWiki is the repo-local project knowledge surface for agents and maintainers. - [vNext authority contract](specs/vnext-authority.md): normative entities, runtime boundaries, protocol, account continuity, non-goals, and migration contract for later implementation. - [vNext gate manifest](specs/vnext-gates.md): ordered feasibility and implementation gates, downstream issue ownership, and decision-changing falsifiers. - [XY-1262 Codex runtime proof](evidence/vnext-codex-runtime-proof.md): shared-home, ownership, schema, collaboration, cross-account, fallback, crash, and typed-quota evidence for the Codex feasibility gate. +- [XY-1345 exact-command authority proof](evidence/xy-1345-exact-command-authority.md): corrected pure-PostgreSQL command authority, deterministic/concurrency schedules, privilege/catalog closure, restore receipt, and V9/V10 ownership order. - [Lane Authority v2](decisions/lane-authority-v2.md): superseded historical target retained as architecture and incident provenance; C1-C7 are frozen and must not be implemented. - [Drift audits](evidence/drift-audits.md): public-safe evidence notes, current MCP remote-control watched claims, reverse checks, validation commands, and stop conditions. - [v0.2 freeze receipt](evidence/v0.2-freeze.md): exact trusted tag, cold-config and automation inventory, frozen legacy work, preserved incident evidence, cleanup ownership, and the unresolved SQLite-backup gap. diff --git a/openwiki/specs/vnext-authority.md b/openwiki/specs/vnext-authority.md index 6bdd2aee..6c365796 100644 --- a/openwiki/specs/vnext-authority.md +++ b/openwiki/specs/vnext-authority.md @@ -111,6 +111,61 @@ admission serialize create-only verified publication; transaction B atomically r metadata/domain references/evidence, stores the exact response bytes, and completes the fenced receipt. Exact replay returns those bytes; conflicting reuse fails before effects. +Pure PostgreSQL commands use a different, exact in-transaction authority. Each operation has one +command-complete migration-owner `SECURITY DEFINER` function. PostgreSQL constructs the complete +request JSONB from the same typed values the function consumes; runtime supplies only a +protocol-scoped idempotency key and typed operation inputs, never an authoritative +caller-supplied request hash, claim token, lease, committed pending claim, or split-phase reserve. +The separate `decodex.exact_command_receipts` primary key is +`(protocol_version, idempotency_key)`. Operation is inside the request envelope, so +cross-operation reuse conflicts without extending or changing legacy `command_receipts` semantics. + +An exact row may be `executing` only within its operation transaction. A +`DEFERRABLE INITIALLY DEFERRED` constraint trigger rejects commit unless every newly created exact +row is completed success or completed stable rejection. Completed rows cannot be changed, deleted, +or truncated and retain the authoritative response bytes created once by PostgreSQL. Expected +missing-target, stale-revision, illegal-transition, and equivalent domain outcomes complete a +stable rejected response; cancellation, connection loss, deadlock, serialization failure, and +unexpected database failure propagate and roll back rather than becoming stable rejection. + +The normal contract is one exact command per top-level `READ COMMITTED` transaction. After +`INSERT ... ON CONFLICT DO NOTHING`, replay/conflict selection occurs in a later read/lock +statement. `40001` and `40P01` retry the whole transaction with the identical typed request. +Multiple exact functions in one caller transaction remain atomic but are outside the no-deadlock +guarantee. + +Request envelopes compare with JSONB equality, not containment. Every optional key is present with +JSON null. Enum and numeric values are typed before construction; integer lexical spelling is not +identity. Text uses exact PostgreSQL text/code-point semantics with no implicit Unicode, case, or +whitespace normalization. RoleProfile bootstrap takes four role-implied scalar configuration +groups in advisor/lead/task/reviewer order, never caller roles or parallel arrays. Derived +revisions, selected profile rows, generated IDs, database timestamps, digests, immutable snapshots, +activity/outbox IDs, and responses are effects rather than request inputs. Effects and stored +responses are assembled from actual `INSERT`/`UPDATE ... RETURNING` rows and actual canonical +activity/outbox identities. + +Exact-command catalog closure covers the unreachable owner; role membership and `SET ROLE` paths; +signatures and overloads; `prosecdef`; language, volatility, parallel safety, settings, source and +dependencies; ACLs, PUBLIC and owner default privileges; trusted search path; triggers; relation +privileges; and populated restore. Runtime has no exact-receipt table privilege, private-helper +execution, or canonical activity/outbox mutation authority. Namespace fences must reject equivalent +aggregate/event/effect/link/payload forgery, including structured variants, rather than matching +only obvious strings. + +Relation privilege closure is semantic: it enumerates the normalized grantee, grantor, privilege, +and grant-option set; proves the owner's complete effective table privileges; proves runtime and +PUBLIC lack every table privilege; and rejects any unexpected grantee before and after restore. It +must not require byte- or text-identical `relacl` serialization. Function closure covers the exact +identity and overload set of every command, private helper, envelope builder, trigger function, +failpoint, and incomplete-row probe present in the candidate, with only command-complete entrypoints +runtime-executable. Effect evidence decodes the stored response bytes and joins their effect envelope +to the returned domain row and actual canonical activity/outbox identities. + +This authority is implemented vertically: XY-1345 records and proves the protocol; XY-1346 owns +the separate relation and RoleProfile bootstrap/update in V9; re-bounded XY-1337 owns authoritative +RuntimeSession snapshot creation/transition in V10. Candidate 3 is superseded code and may supply +only independently re-derived invariants and hostile-test ideas. + ## Conversation, context, and communication Every meaningful Decodex-created thread uses `ephemeral=false`, the shared normal diff --git a/openwiki/specs/vnext-gates.md b/openwiki/specs/vnext-gates.md index 247ae6e6..13c23e4e 100644 --- a/openwiki/specs/vnext-gates.md +++ b/openwiki/specs/vnext-gates.md @@ -101,16 +101,55 @@ Permission is issue-scoped and does not bypass each issue's own dependencies: | XY-1272 | PostgreSQL configured-principal and ACL authority manifest/readiness closure against V8; no migration or Codex creation/reconciliation surface. Any future configured role must atomically extend configuration, bootstrap, manifest/readiness, and negative tests. | | XY-1273 | Credential-vault metadata and immutable runner/account binding; no sticky or policy assignment. | | XY-1274 | Exact-microsecond quota persistence, `/2` canonical mutation identity, atomic V8 zero-state migration, and durable exclusion transaction tests using synthetic fixtures only; no live exclusion, fallback assignment, or wake scheduling. | -| XY-1275 | User-owned profile persistence and RuntimeSession snapshots. Account-owned plugin, skill, and MCP readiness remains typed `unknown`; XY-1336 neither closes nor blocks this issue. | +| XY-1275 | Umbrella for user-owned profile persistence and RuntimeSession snapshots. It closes only through the serial XY-1345 -> XY-1346 -> XY-1337 order. Account-owned plugin, skill, and MCP readiness remains typed `unknown`; XY-1336 neither closes nor blocks this issue. | | XY-1276 | Production Quick Task creation; remains blocked by XY-1304. | | XY-1304 | Experiment thread creation and positive observation acquisition, plus live routing enablement. Lossy or paginated evidence cannot authorize negative `Present`, `Complete`, or context-free `Absent` conclusions. | -| XY-1337 | The next PostgreSQL migration, expected V9; V8 remains final until this owner lands it. | +| XY-1345 | Accepted exact-command authority and isolated PostgreSQL 18 prototype only; no production migration or Rust command path. | +| XY-1346 | The next PostgreSQL migration, expected V9: separate exact receipts plus RoleProfile bootstrap/update. Starts only after XY-1345 lands. | +| XY-1337 | Re-bounded RuntimeSession snapshot creation/transition migration, expected V10 after XY-1346. It does not own exact-receipt or RoleProfile redesign. | XY-1336 is an upstream-blocked tracking issue outside the M2 critical path. A host file, manifest, configuration value, remote catalog entry, process binding, or user declaration cannot close the missing account-owned receipt. Existing doctor `unknown(plugin)` is the first-release result, and `plugin_unready` remains inert reserved state. +### XY-1345 exact-command reset gate + +XY-1345 owns repository authority and a non-production PostgreSQL 18 proof, not a migration or +Rust product API. Its accepted boundary is operation-specific, command-complete +`SECURITY DEFINER` functions; PostgreSQL-built and -consumed typed request envelopes; a separate +`exact_command_receipts` relation; no runtime receipt-table/private-helper/canonical-audit mutation +authority; deferred commit closure for executing rows; immutable stored response bytes; and +separate stable-domain, idempotency-conflict, and retryable-infrastructure outcomes. It changes no +legacy `command_receipts` semantic. + +The isolated proof must pass all deterministic schedules for same-key waiting/replay, changed +envelope and cross-operation conflict, rollback at receipt/domain/activity/outbox boundaries, +precommit connection loss, postcommit lost result, stable rejection replay, deferred incomplete +commit rejection, runtime receipt and canonical-audit denial, `READ COMMITTED`/`REPEATABLE +READ`/`SERIALIZABLE`, opposite-order deadlock and whole-transaction retry, explicit nulls, exact +text variants, typed numeric convergence, fixed scalar bootstrap groups, returned-row effect +binding, catalog closure, and populated dump/restore. It must then pass 50 repetitions each of +32-way identical and mixed-envelope concurrency with zero duplicate domain effects, duplicate +activity/outbox pairs, response mismatches, committed executing rows, authority bypasses, +unexplained rows, or unclassified SQLSTATEs. The durable receipt is +[XY-1345 exact-command evidence](../evidence/xy-1345-exact-command-authority.md). + +Any anomaly falsifies the architecture and stops V9. A passing prototype permits only this serial +order: + +```text +XY-1345 authority/prototype and fresh exact-candidate review +-> XY-1346 V9 exact receipts plus RoleProfile bootstrap/update +-> XY-1337 V10 RuntimeSession snapshots, creation, and transition +``` + +V9 must re-prove the complete security-definer catalog/ACL/default-privilege/search-path/dependency +closure, canonical audit namespace closure, clean V1-to-V9 bootstrap, V8-to-V9 upgrade, hostile SQL, +crash/concurrency, and populated restore against the production migration. Candidate 3 SQL and Rust +command code are superseded and cannot be transplanted; its valid invariants and hostile-test ideas +remain provenance only. + ### XY-1263 acceptance and XY-1269 clean-slice reset XY-1263 landed in PR #1109. Its reviewed candidate was diff --git a/scripts/vnext/exact_command_prototype.py b/scripts/vnext/exact_command_prototype.py new file mode 100644 index 00000000..e82c0199 --- /dev/null +++ b/scripts/vnext/exact_command_prototype.py @@ -0,0 +1,1238 @@ +#!/usr/bin/env python3 +"""Prove the XY-1345 exact-command design in a disposable PostgreSQL 18 cluster.""" + +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import time +import traceback +import uuid + + +REPO_ROOT = Path(__file__).resolve().parents[2] +DATABASE = "decodex_xy1345" +RESTORE_DATABASE = "decodex_xy1345_restore" +OWNER_ROLE = "decodex_exact_owner" +RUNTIME_ROLE = "decodex_exact_runtime" +PROTOCOL = "decodex/exact-command/1" +STRESS_REPETITIONS = 50 +STRESS_WIDTH = 32 +SQLSTATE_RE = re.compile(r"(?:ERROR|FATAL):\s+([0-9A-Z]{5}):") +TABLE_PRIVILEGES = ("SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER", "MAINTAIN") +EXPECTED_FUNCTIONS = { + "build_role_profile_bootstrap_request": (", ".join(["text"] * 21), "plpgsql", "i", False, False), + "build_role_profile_update_request": ("text, decodex.prototype_role, bigint, text, text, text, text, text", "sql", "i", False, False), + "build_runtime_session_create_request": ("text, uuid, uuid, decodex.prototype_role, uuid, uuid, text, text, bigint, uuid, decodex.prototype_session_state", "sql", "i", False, False), + "build_runtime_session_transition_request": ("text, uuid, bigint, decodex.prototype_session_state, text", "sql", "i", False, False), + "complete_prototype_rejection": ("text, text, text", "plpgsql", "v", False, False), + "enforce_exact_receipt_completion": ("", "plpgsql", "v", False, False), + "forbid_exact_receipt_rewrite": ("", "plpgsql", "v", False, False), + "forbid_exact_receipt_truncate": ("", "plpgsql", "v", False, False), + "prototype_failpoint": ("text", "plpgsql", "v", False, False), + "prototype_leave_incomplete": ("text, text", "plpgsql", "v", True, False), + "transition_runtime_session_exact": ("text, text, uuid, bigint, decodex.prototype_session_state, text", "plpgsql", "v", True, True), +} + + +class ProofFailure(RuntimeError): + """Raised when the prototype falsifies an accepted requirement.""" + + +@dataclass(frozen=True) +class Result: + returncode: int + stdout: str + stderr: str + duration: float + + @property + def sqlstate(self) -> str | None: + match = SQLSTATE_RE.search(self.stderr) + return match.group(1) if match else None + + +SCHEMA_SQL = r""" +CREATE ROLE decodex_exact_owner NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT; +CREATE ROLE decodex_exact_runtime NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT; +CREATE SCHEMA decodex_crypto AUTHORIZATION decodex_exact_owner; +REVOKE ALL ON SCHEMA decodex_crypto FROM PUBLIC; +CREATE EXTENSION pgcrypto WITH SCHEMA decodex_crypto; +CREATE SCHEMA decodex AUTHORIZATION decodex_exact_owner; +REVOKE ALL ON SCHEMA decodex FROM PUBLIC; +GRANT USAGE ON SCHEMA decodex TO decodex_exact_runtime; +SET ROLE decodex_exact_owner; + +CREATE TYPE decodex.exact_receipt_state AS ENUM ( + 'executing', 'completed_success', 'completed_rejected' +); +CREATE TYPE decodex.prototype_session_state AS ENUM ( + 'starting', 'active', 'ended', 'diverged' +); +CREATE TYPE decodex.prototype_role AS ENUM ('advisor', 'lead', 'task', 'reviewer'); + +CREATE TABLE decodex.exact_command_receipts ( + protocol_version pg_catalog.text NOT NULL, + idempotency_key pg_catalog.text NOT NULL, + request_envelope pg_catalog.jsonb NOT NULL, + request_digest pg_catalog.bytea NOT NULL, + receipt_state decodex.exact_receipt_state NOT NULL, + outcome_class pg_catalog.text, + effect_envelope pg_catalog.jsonb, + response_bytes pg_catalog.bytea, + created_at pg_catalog.timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(), + completed_at pg_catalog.timestamptz, + PRIMARY KEY (protocol_version, idempotency_key), + CONSTRAINT exact_receipt_shape CHECK ( + (receipt_state = 'executing' + AND outcome_class IS NULL AND effect_envelope IS NULL + AND response_bytes IS NULL AND completed_at IS NULL) + OR + (receipt_state <> 'executing' + AND outcome_class IS NOT NULL AND effect_envelope IS NOT NULL + AND response_bytes IS NOT NULL AND completed_at IS NOT NULL) + ), + CONSTRAINT exact_receipt_digest_matches CHECK ( + request_digest = decodex_crypto.digest( + pg_catalog.convert_to(request_envelope::pg_catalog.text, 'UTF8'), 'sha256' + ) + ) +); + +CREATE TABLE decodex.prototype_runtime_sessions ( + runtime_session_id pg_catalog.uuid PRIMARY KEY, + state decodex.prototype_session_state NOT NULL, + revision pg_catalog.int8 NOT NULL CHECK (revision > 0), + updated_at pg_catalog.timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp() +); +CREATE TABLE decodex.prototype_activity ( + activity_id pg_catalog.int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + aggregate_kind pg_catalog.text NOT NULL, + aggregate_id pg_catalog.uuid NOT NULL, + event_kind pg_catalog.text NOT NULL, + payload pg_catalog.jsonb NOT NULL, + created_at pg_catalog.timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(), + UNIQUE (aggregate_kind, aggregate_id, event_kind) +); +CREATE TABLE decodex.prototype_outbox ( + outbox_id pg_catalog.int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + activity_id pg_catalog.int8 NOT NULL UNIQUE + REFERENCES decodex.prototype_activity(activity_id), + effect_key pg_catalog.text NOT NULL UNIQUE, + payload pg_catalog.jsonb NOT NULL, + created_at pg_catalog.timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp() +); + +CREATE FUNCTION decodex.enforce_exact_receipt_completion() +RETURNS pg_catalog.trigger +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = pg_catalog, decodex +AS $function$ +DECLARE + current_state decodex.exact_receipt_state; +BEGIN + SELECT receipt_state INTO current_state + FROM decodex.exact_command_receipts + WHERE protocol_version = NEW.protocol_version + AND idempotency_key = NEW.idempotency_key; + IF current_state = 'executing' THEN + RAISE EXCEPTION USING ERRCODE = '23514', + CONSTRAINT = 'exact_receipts_complete_at_commit', + MESSAGE = 'exact command receipt must be completed before commit'; + END IF; + RETURN NULL; +END +$function$; + +CREATE CONSTRAINT TRIGGER exact_receipts_complete_at_commit +AFTER INSERT OR UPDATE ON decodex.exact_command_receipts +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW EXECUTE FUNCTION decodex.enforce_exact_receipt_completion(); + +CREATE FUNCTION decodex.forbid_exact_receipt_rewrite() +RETURNS pg_catalog.trigger +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = pg_catalog, decodex +AS $function$ +BEGIN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION USING ERRCODE = '23514', MESSAGE = 'exact receipts are undeletable'; + END IF; + IF OLD.receipt_state <> 'executing' + OR NEW.receipt_state = 'executing' + OR OLD.protocol_version <> NEW.protocol_version + OR OLD.idempotency_key <> NEW.idempotency_key + OR OLD.request_envelope <> NEW.request_envelope + OR OLD.request_digest <> NEW.request_digest + OR OLD.created_at <> NEW.created_at + THEN + RAISE EXCEPTION USING ERRCODE = '23514', MESSAGE = 'exact receipt rewrite is forbidden'; + END IF; + RETURN NEW; +END +$function$; +CREATE TRIGGER exact_receipts_immutable +BEFORE UPDATE OR DELETE ON decodex.exact_command_receipts +FOR EACH ROW EXECUTE FUNCTION decodex.forbid_exact_receipt_rewrite(); + +CREATE FUNCTION decodex.forbid_exact_receipt_truncate() +RETURNS pg_catalog.trigger +LANGUAGE plpgsql +SECURITY INVOKER +SET search_path = pg_catalog, decodex +AS $function$ +BEGIN + RAISE EXCEPTION USING ERRCODE = '23514', MESSAGE = 'exact receipts are untruncatable'; +END +$function$; +CREATE TRIGGER exact_receipts_untruncatable +BEFORE TRUNCATE ON decodex.exact_command_receipts +FOR EACH STATEMENT EXECUTE FUNCTION decodex.forbid_exact_receipt_truncate(); + +CREATE FUNCTION decodex.build_role_profile_bootstrap_request( + p_protocol pg_catalog.text, + p_advisor_model pg_catalog.text, p_advisor_reasoning pg_catalog.text, + p_advisor_tier pg_catalog.text, p_advisor_instructions pg_catalog.text, + p_advisor_provenance pg_catalog.text, + p_lead_model pg_catalog.text, p_lead_reasoning pg_catalog.text, + p_lead_tier pg_catalog.text, p_lead_instructions pg_catalog.text, + p_lead_provenance pg_catalog.text, + p_task_model pg_catalog.text, p_task_reasoning pg_catalog.text, + p_task_tier pg_catalog.text, p_task_instructions pg_catalog.text, + p_task_provenance pg_catalog.text, + p_reviewer_model pg_catalog.text, p_reviewer_reasoning pg_catalog.text, + p_reviewer_tier pg_catalog.text, p_reviewer_instructions pg_catalog.text, + p_reviewer_provenance pg_catalog.text +) RETURNS pg_catalog.jsonb +LANGUAGE plpgsql IMMUTABLE SECURITY INVOKER +SET search_path = pg_catalog, decodex +AS $function$ +BEGIN + IF p_protocol IS NULL OR p_advisor_model IS NULL OR p_advisor_reasoning IS NULL + OR p_advisor_tier IS NULL OR p_advisor_instructions IS NULL + OR p_lead_model IS NULL OR p_lead_reasoning IS NULL OR p_lead_tier IS NULL + OR p_lead_instructions IS NULL OR p_task_model IS NULL OR p_task_reasoning IS NULL + OR p_task_tier IS NULL OR p_task_instructions IS NULL OR p_reviewer_model IS NULL + OR p_reviewer_reasoning IS NULL OR p_reviewer_tier IS NULL + OR p_reviewer_instructions IS NULL THEN + RAISE EXCEPTION USING ERRCODE = '22004', MESSAGE = 'bootstrap configuration is incomplete'; + END IF; + RETURN pg_catalog.jsonb_build_object( + 'protocol_version', p_protocol, 'operation', 'bootstrap_role_profiles', + 'profiles', pg_catalog.jsonb_build_array( + pg_catalog.jsonb_build_object('role','advisor','model',p_advisor_model,'reasoning_effort',p_advisor_reasoning,'service_tier',p_advisor_tier,'instructions',p_advisor_instructions,'provenance',p_advisor_provenance), + pg_catalog.jsonb_build_object('role','lead','model',p_lead_model,'reasoning_effort',p_lead_reasoning,'service_tier',p_lead_tier,'instructions',p_lead_instructions,'provenance',p_lead_provenance), + pg_catalog.jsonb_build_object('role','task','model',p_task_model,'reasoning_effort',p_task_reasoning,'service_tier',p_task_tier,'instructions',p_task_instructions,'provenance',p_task_provenance), + pg_catalog.jsonb_build_object('role','reviewer','model',p_reviewer_model,'reasoning_effort',p_reviewer_reasoning,'service_tier',p_reviewer_tier,'instructions',p_reviewer_instructions,'provenance',p_reviewer_provenance) + ) + ); +END +$function$; + +CREATE FUNCTION decodex.build_role_profile_update_request( + p_protocol pg_catalog.text, p_role decodex.prototype_role, + p_expected_revision pg_catalog.int8, p_model pg_catalog.text, + p_reasoning pg_catalog.text, p_tier pg_catalog.text, + p_instructions pg_catalog.text, p_provenance pg_catalog.text +) RETURNS pg_catalog.jsonb +LANGUAGE sql IMMUTABLE SECURITY INVOKER +SET search_path = pg_catalog, decodex +RETURN pg_catalog.jsonb_build_object( + 'protocol_version', p_protocol, 'operation', 'update_role_profile', + 'role', p_role, 'expected_revision', p_expected_revision, + 'model', p_model, 'reasoning_effort', p_reasoning, 'service_tier', p_tier, + 'instructions', p_instructions, 'provenance', p_provenance +); + +CREATE FUNCTION decodex.build_runtime_session_create_request( + p_protocol pg_catalog.text, p_session_id pg_catalog.uuid, + p_conversation_id pg_catalog.uuid, p_role decodex.prototype_role, + p_account_snapshot_id pg_catalog.uuid, p_source_account_id pg_catalog.uuid, + p_display_label pg_catalog.text, p_observed_state pg_catalog.text, + p_account_source_revision pg_catalog.int8, p_codex_thread_id pg_catalog.uuid, + p_initial_state decodex.prototype_session_state +) RETURNS pg_catalog.jsonb +LANGUAGE sql IMMUTABLE SECURITY INVOKER +SET search_path = pg_catalog, decodex +RETURN pg_catalog.jsonb_build_object( + 'protocol_version', p_protocol, 'operation', 'create_runtime_session', + 'runtime_session_id', p_session_id, 'conversation_id', p_conversation_id, + 'role', p_role, 'account_snapshot_id', p_account_snapshot_id, + 'source_account_id', p_source_account_id, 'display_label', p_display_label, + 'observed_state', p_observed_state, 'account_source_revision', p_account_source_revision, + 'codex_thread_id', p_codex_thread_id, 'initial_state', p_initial_state +); + +CREATE FUNCTION decodex.build_runtime_session_transition_request( + p_protocol pg_catalog.text, p_session_id pg_catalog.uuid, + p_expected_revision pg_catalog.int8, p_target_state decodex.prototype_session_state, + p_note pg_catalog.text +) RETURNS pg_catalog.jsonb +LANGUAGE sql IMMUTABLE SECURITY INVOKER +SET search_path = pg_catalog, decodex +RETURN pg_catalog.jsonb_build_object( + 'protocol_version', p_protocol, 'operation', 'transition_runtime_session', + 'runtime_session_id', p_session_id, 'expected_revision', p_expected_revision, + 'target_state', p_target_state, 'note', p_note +); + +CREATE FUNCTION decodex.prototype_failpoint(p_stage pg_catalog.text) +RETURNS pg_catalog.void +LANGUAGE plpgsql VOLATILE SECURITY INVOKER +SET search_path = pg_catalog, decodex +AS $function$ +DECLARE + wait_seconds pg_catalog.float8; +BEGIN + IF pg_catalog.current_setting('decodex.prototype_failpoint', true) = p_stage THEN + wait_seconds := COALESCE( + pg_catalog.current_setting('decodex.prototype_failpoint_wait', true)::pg_catalog.float8, + 0 + ); + IF wait_seconds > 0 THEN PERFORM pg_catalog.pg_sleep(wait_seconds); END IF; + RAISE EXCEPTION USING ERRCODE = 'DX900', MESSAGE = 'injected prototype infrastructure failure'; + END IF; +END +$function$; + +CREATE FUNCTION decodex.complete_prototype_rejection( + p_protocol pg_catalog.text, p_key pg_catalog.text, p_code pg_catalog.text +) RETURNS pg_catalog.bytea +LANGUAGE plpgsql VOLATILE SECURITY INVOKER +SET search_path = pg_catalog, decodex +AS $function$ +DECLARE + response_json pg_catalog.jsonb; + response_value pg_catalog.bytea; +BEGIN + response_json := pg_catalog.jsonb_build_object( + 'classification', 'stable_domain_rejection', 'code', p_code + ); + response_value := pg_catalog.convert_to(response_json::pg_catalog.text, 'UTF8'); + UPDATE decodex.exact_command_receipts SET + receipt_state = 'completed_rejected', + outcome_class = 'stable_domain_rejection', + effect_envelope = pg_catalog.jsonb_build_object('changed', false, 'code', p_code), + response_bytes = response_value, + completed_at = pg_catalog.clock_timestamp() + WHERE protocol_version = p_protocol AND idempotency_key = p_key; + RETURN response_value; +END +$function$; + +CREATE FUNCTION decodex.transition_runtime_session_exact( + p_protocol pg_catalog.text, p_idempotency_key pg_catalog.text, + p_session_id pg_catalog.uuid, p_expected_revision pg_catalog.int8, + p_target_state decodex.prototype_session_state, p_note pg_catalog.text +) RETURNS pg_catalog.bytea +LANGUAGE plpgsql VOLATILE SECURITY DEFINER +SET search_path = pg_catalog, decodex +AS $function$ +DECLARE + request_value pg_catalog.jsonb; + existing_request pg_catalog.jsonb; + existing_response pg_catalog.bytea; + inserted_count pg_catalog.int4; + old_state decodex.prototype_session_state; + new_state decodex.prototype_session_state; + new_revision pg_catalog.int8; + new_updated_at pg_catalog.timestamptz; + activity_identity pg_catalog.int8; + activity_payload pg_catalog.jsonb; + outbox_identity pg_catalog.int8; + outbox_payload pg_catalog.jsonb; + effect_value pg_catalog.jsonb; + response_json pg_catalog.jsonb; + response_value pg_catalog.bytea; +BEGIN + IF p_protocol IS NULL OR p_idempotency_key IS NULL OR p_session_id IS NULL + OR p_expected_revision IS NULL OR p_target_state IS NULL THEN + RAISE EXCEPTION USING ERRCODE = '22004', MESSAGE = 'exact transition input is incomplete'; + END IF; + request_value := decodex.build_runtime_session_transition_request( + p_protocol, p_session_id, p_expected_revision, p_target_state, p_note + ); + INSERT INTO decodex.exact_command_receipts( + protocol_version, idempotency_key, request_envelope, request_digest, receipt_state + ) VALUES ( + p_protocol, p_idempotency_key, request_value, + decodex_crypto.digest(pg_catalog.convert_to(request_value::pg_catalog.text, 'UTF8'), 'sha256'), + 'executing' + ) ON CONFLICT (protocol_version, idempotency_key) DO NOTHING; + GET DIAGNOSTICS inserted_count = ROW_COUNT; + IF inserted_count = 0 THEN + SELECT request_envelope, response_bytes INTO existing_request, existing_response + FROM decodex.exact_command_receipts + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key + FOR UPDATE; + IF existing_request <> request_value THEN + RAISE EXCEPTION USING ERRCODE = 'DX001', MESSAGE = 'exact idempotency conflict'; + END IF; + IF existing_response IS NULL THEN + RAISE EXCEPTION USING ERRCODE = 'DX002', MESSAGE = 'incomplete exact receipt is not replayable'; + END IF; + RETURN existing_response; + END IF; + PERFORM decodex.prototype_failpoint('after_receipt'); + + SELECT state INTO old_state FROM decodex.prototype_runtime_sessions + WHERE runtime_session_id = p_session_id FOR UPDATE; + IF NOT FOUND THEN + RETURN decodex.complete_prototype_rejection(p_protocol, p_idempotency_key, 'missing_target'); + END IF; + IF (SELECT revision FROM decodex.prototype_runtime_sessions + WHERE runtime_session_id = p_session_id) <> p_expected_revision THEN + RETURN decodex.complete_prototype_rejection(p_protocol, p_idempotency_key, 'stale_revision'); + END IF; + IF NOT ((old_state = 'starting' AND p_target_state IN ('active','diverged')) + OR (old_state = 'active' AND p_target_state IN ('ended','diverged'))) THEN + RETURN decodex.complete_prototype_rejection(p_protocol, p_idempotency_key, 'illegal_transition'); + END IF; + + UPDATE decodex.prototype_runtime_sessions SET + state = p_target_state, revision = revision + 1, + updated_at = pg_catalog.clock_timestamp() + WHERE runtime_session_id = p_session_id AND revision = p_expected_revision + RETURNING state, revision, updated_at INTO new_state, new_revision, new_updated_at; + PERFORM decodex.prototype_failpoint('after_domain'); + + activity_payload := pg_catalog.jsonb_build_object( + 'runtime_session_id', p_session_id, 'old_state', old_state, + 'new_state', new_state, 'revision', new_revision, 'updated_at', new_updated_at + ); + INSERT INTO decodex.prototype_activity(aggregate_kind, aggregate_id, event_kind, payload) + VALUES ('runtime_session', p_session_id, 'runtime_session_transitioned', activity_payload) + RETURNING activity_id, payload INTO activity_identity, activity_payload; + PERFORM decodex.prototype_failpoint('after_activity'); + + outbox_payload := pg_catalog.jsonb_build_object( + 'activity_id', activity_identity, 'event', activity_payload + ); + INSERT INTO decodex.prototype_outbox(activity_id, effect_key, payload) + VALUES (activity_identity, 'runtime-session-transition:' || activity_identity::pg_catalog.text, outbox_payload) + RETURNING outbox_id, payload INTO outbox_identity, outbox_payload; + PERFORM decodex.prototype_failpoint('after_outbox'); + + effect_value := pg_catalog.jsonb_build_object( + 'persisted_session', pg_catalog.jsonb_build_object( + 'runtime_session_id', p_session_id, 'state', new_state, + 'revision', new_revision, 'updated_at', new_updated_at + ), + 'activity_id', activity_identity, 'activity_payload', activity_payload, + 'outbox_id', outbox_identity, 'outbox_payload', outbox_payload + ); + response_json := pg_catalog.jsonb_build_object( + 'classification', 'success', 'effect', effect_value + ); + response_value := pg_catalog.convert_to(response_json::pg_catalog.text, 'UTF8'); + UPDATE decodex.exact_command_receipts SET + receipt_state = 'completed_success', outcome_class = 'success', + effect_envelope = effect_value, response_bytes = response_value, + completed_at = pg_catalog.clock_timestamp() + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key; + RETURN response_value; +END +$function$; + +CREATE FUNCTION decodex.prototype_leave_incomplete( + p_protocol pg_catalog.text, p_key pg_catalog.text +) RETURNS pg_catalog.void +LANGUAGE plpgsql VOLATILE SECURITY DEFINER +SET search_path = pg_catalog, decodex +AS $function$ +DECLARE request_value pg_catalog.jsonb; +BEGIN + request_value := pg_catalog.jsonb_build_object( + 'protocol_version', p_protocol, 'operation', 'prototype_incomplete' + ); + INSERT INTO decodex.exact_command_receipts( + protocol_version, idempotency_key, request_envelope, request_digest, receipt_state + ) VALUES ( + p_protocol, p_key, request_value, + decodex_crypto.digest(pg_catalog.convert_to(request_value::pg_catalog.text, 'UTF8'), 'sha256'), + 'executing' + ); +END +$function$; + +REVOKE ALL ON ALL TABLES IN SCHEMA decodex FROM PUBLIC, decodex_exact_runtime; +REVOKE ALL ON ALL SEQUENCES IN SCHEMA decodex FROM PUBLIC, decodex_exact_runtime; +REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA decodex FROM PUBLIC, decodex_exact_runtime; +GRANT EXECUTE ON FUNCTION decodex.transition_runtime_session_exact( + pg_catalog.text, pg_catalog.text, pg_catalog.uuid, pg_catalog.int8, + decodex.prototype_session_state, pg_catalog.text +) TO decodex_exact_runtime; +ALTER DEFAULT PRIVILEGES FOR ROLE decodex_exact_owner + REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; +RESET ROLE; +""" + + +class Harness: + def __init__(self, root: Path) -> None: + self.root = root + self.data = root / "data" + self.socket = root / "socket" + self.log = root / "postgres.log" + self.port = 55435 + self.env = os.environ.copy() + self.env.update({ + "PGHOST": str(self.socket), "PGPORT": str(self.port), + "PGDATABASE": DATABASE, "PGAPPNAME": "xy1345-proof", + }) + self.started = False + + def command(self, args: list[str], *, env: dict[str, str] | None = None, + input_text: str | None = None) -> Result: + started = time.monotonic() + completed = subprocess.run( + args, cwd=REPO_ROOT, env=env or self.env, input=input_text, + text=True, capture_output=True, check=False, + ) + return Result(completed.returncode, completed.stdout.strip(), + completed.stderr.strip(), time.monotonic() - started) + + def require(self, args: list[str], **kwargs: object) -> Result: + result = self.command(args, **kwargs) + if result.returncode != 0: + raise ProofFailure( + f"command failed: {' '.join(args)}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + return result + + def start(self) -> None: + self.socket.mkdir() + initdb_path = Path(shutil.which("initdb") or "initdb").resolve() + postgres_share = initdb_path.parent.parent / "share" / "postgresql" + self.env["PATH"] = f"{initdb_path.parent}{os.pathsep}{self.env['PATH']}" + self.require(["initdb", "-D", str(self.data), "--encoding=UTF8", + "--locale=C", "--auth=trust", "--no-instructions", + "-L", str(postgres_share)]) + self.require(["pg_ctl", "-D", str(self.data), "-l", str(self.log), + "-o", f"-k {self.socket} -p {self.port} -h '' -F", "-w", "start"]) + self.started = True + self.require(["createdb", DATABASE]) + self.must_psql(SCHEMA_SQL) + + def stop(self) -> bool: + if not self.started: + return True + self.require(["pg_ctl", "-D", str(self.data), "-m", "immediate", "-w", "stop"]) + self.started = False + return True + + def psql(self, sql: str, *, database: str = DATABASE, + role: str | None = None, app: str | None = None) -> Result: + prefix = "\\set VERBOSITY verbose\n" + if role: + prefix += f"SET ROLE {role};\n" + env = self.env.copy() + env["PGDATABASE"] = database + if app: + env["PGAPPNAME"] = app + return self.command( + ["psql", "-X", "-v", "ON_ERROR_STOP=1", "-At"], + env=env, input_text=prefix + sql, + ) + + def must_psql(self, sql: str, **kwargs: object) -> str: + result = self.psql(sql, **kwargs) + if result.returncode != 0: + raise ProofFailure(f"SQL failed\n{sql}\n{result.stdout}\n{result.stderr}") + return result.stdout + + def transition_sql(self, key: str, session_id: str, expected: int, + target: str, note: str | None = None) -> str: + note_sql = "NULL" if note is None else "'" + note.replace("'", "''") + "'" + return ( + "SELECT pg_catalog.encode(decodex.transition_runtime_session_exact(" + f"'{PROTOCOL}','{key}','{session_id}',{expected}," + f"'{target}'::decodex.prototype_session_state,{note_sql}),'hex');" + ) + + def add_session(self, session_id: str, state: str = "starting", revision: int = 1) -> None: + self.must_psql( + "SET ROLE decodex_exact_owner; " + "INSERT INTO decodex.prototype_runtime_sessions(runtime_session_id,state,revision) " + f"VALUES ('{session_id}','{state}',{revision}); RESET ROLE;" + ) + + +def expect(condition: bool, message: str) -> None: + if not condition: + raise ProofFailure(message) + + +def scalar(harness: Harness, sql: str, **kwargs: object) -> str: + return harness.must_psql(sql, **kwargs).splitlines()[-1] + + +def runtime_call(harness: Harness, sql: str, *, app: str | None = None) -> Result: + return harness.psql(sql, role=RUNTIME_ROLE, app=app) + + +def exact_hex(result: Result) -> str: + for line in reversed(result.stdout.splitlines()): + if re.fullmatch(r"[0-9a-f]+", line): + return line + raise ProofFailure(f"no response bytes in output: {result.stdout!r} {result.stderr!r}") + + +def classify(result: Result) -> str: + if result.returncode == 0: + return "success" + if result.sqlstate == "DX001": + return "idempotency_conflict" + if result.sqlstate in {"40001", "40P01", "08006", "57P01", "DX900"}: + return "retryable_infrastructure_failure" + if result.sqlstate in {"42501", "23514", "22004", "42883"}: + return "stable_domain_or_authority_rejection" + raise ProofFailure(f"unclassified SQLSTATE {result.sqlstate}: {result.stderr}") + + +def prove_wait_replay_and_conflicts(harness: Harness, report: dict[str, object]) -> None: + session = str(uuid.uuid4()) + harness.add_session(session) + key = "wait-replay" + first_sql = "BEGIN; " + harness.transition_sql(key, session, 1, "active", "same") + " SELECT pg_sleep(0.8); COMMIT;" + with ThreadPoolExecutor(max_workers=2) as pool: + first = pool.submit(runtime_call, harness, first_sql) + time.sleep(0.15) + second = pool.submit(runtime_call, harness, harness.transition_sql(key, session, 1, "active", "same")) + first_result = first.result() + second_result = second.result() + expect(first_result.returncode == second_result.returncode == 0, "same-envelope replay failed") + expect(exact_hex(first_result) == exact_hex(second_result), "replay bytes differ") + expect(second_result.duration >= 0.5, "same-key contender did not wait") + + conflict = runtime_call(harness, harness.transition_sql(key, session, 1, "ended", "same")) + expect(classify(conflict) == "idempotency_conflict", "different target did not conflict") + cross_operation = harness.psql( + "SET ROLE decodex_exact_owner; " + "SELECT decodex.prototype_leave_incomplete('decodex/exact-command/1','cross-op');", + ) + expect(cross_operation.sqlstate == "23514", "incomplete cross-op setup should fail at commit") + request = json.dumps({"protocol_version": PROTOCOL, "operation": "bootstrap_role_profiles"}) + harness.must_psql( + "SET ROLE decodex_exact_owner; INSERT INTO decodex.exact_command_receipts(" + "protocol_version,idempotency_key,request_envelope,request_digest,receipt_state," + "outcome_class,effect_envelope,response_bytes,completed_at) VALUES (" + f"'{PROTOCOL}','cross-operation','{request}'::jsonb," + f"decodex_crypto.digest(convert_to('{request}'::jsonb::text,'UTF8'),'sha256'),'completed_rejected'," + "'stable_domain_rejection','{\"changed\":false}'::jsonb,convert_to('{}','UTF8'),clock_timestamp()); RESET ROLE;" + ) + cross = runtime_call(harness, harness.transition_sql("cross-operation", session, 2, "ended")) + expect(classify(cross) == "idempotency_conflict", "cross-operation key reuse did not conflict") + effects = scalar(harness, f"SELECT count(*) FROM decodex.prototype_activity WHERE aggregate_id='{session}';") + expect(effects == "1", "conflicting reuse created an effect") + report["wait_replay"] = {"wait_seconds": round(second_result.duration, 3), "byte_identical": True} + + +def prove_abort_matrix(harness: Harness, report: dict[str, object]) -> None: + rows: dict[str, object] = {} + for stage in ("after_receipt", "after_domain", "after_activity", "after_outbox"): + session = str(uuid.uuid4()) + key = f"abort-{stage}" + harness.add_session(session) + failing = ( + "BEGIN; SET LOCAL decodex.prototype_failpoint='" + stage + "'; " + "SET LOCAL decodex.prototype_failpoint_wait='0.35'; " + + harness.transition_sql(key, session, 1, "active") + " COMMIT;" + ) + with ThreadPoolExecutor(max_workers=2) as pool: + first = pool.submit(runtime_call, harness, failing) + time.sleep(0.1) + waiter = pool.submit(runtime_call, harness, harness.transition_sql(key, session, 1, "active")) + failed = first.result() + waited = waiter.result() + expect(classify(failed) == "retryable_infrastructure_failure", f"{stage} not retryable") + expect(waited.returncode == 0, f"waiter did not become executor after {stage}") + counts = scalar( + harness, + "SELECT (SELECT count(*) FROM decodex.exact_command_receipts WHERE idempotency_key='" + + key + "') || ',' || (SELECT count(*) FROM decodex.prototype_activity WHERE aggregate_id='" + + session + "') || ',' || (SELECT count(*) FROM decodex.prototype_outbox o JOIN decodex.prototype_activity a USING(activity_id) WHERE a.aggregate_id='" + + session + "');", + ) + expect(counts == "1,1,1", f"{stage} did not converge exactly once: {counts}") + rows[stage] = {"first": failed.sqlstate, "waiter_executed": True} + report["abort_matrix"] = rows + + +def prove_connection_and_lost_result(harness: Harness, report: dict[str, object]) -> None: + session = str(uuid.uuid4()) + key = "terminated-before-commit" + harness.add_session(session) + sql = "BEGIN; " + harness.transition_sql(key, session, 1, "active") + " SELECT pg_sleep(10); COMMIT;" + with ThreadPoolExecutor(max_workers=1) as pool: + future = pool.submit(runtime_call, harness, sql, app="xy1345-kill") + deadline = time.monotonic() + 5 + pid = "" + while time.monotonic() < deadline: + pid = scalar(harness, "SELECT COALESCE(max(pid)::text,'-') FROM pg_stat_activity WHERE application_name='xy1345-kill' AND query LIKE '%pg_sleep(10)%';") + if pid != "-": + break + time.sleep(0.05) + expect(pid != "-", "terminable backend did not reach pre-commit wait") + harness.must_psql(f"SELECT pg_terminate_backend({pid});") + terminated = future.result() + expect(terminated.returncode != 0, "terminated connection unexpectedly committed") + expect(classify(terminated) == "retryable_infrastructure_failure", "terminated connection was not classified") + zero = scalar(harness, f"SELECT count(*) FROM decodex.exact_command_receipts WHERE idempotency_key='{key}';") + expect(zero == "0", "connection termination left a durable receipt") + retry = runtime_call(harness, harness.transition_sql(key, session, 1, "active")) + expect(retry.returncode == 0, "retry after termination failed") + + lost_session = str(uuid.uuid4()) + lost_key = "commit-result-discarded" + harness.add_session(lost_session) + discarded = runtime_call(harness, harness.transition_sql(lost_key, lost_session, 1, "active")) + expect(discarded.returncode == 0, "discarded-result transaction failed") + stored = scalar(harness, f"SELECT encode(response_bytes,'hex') FROM decodex.exact_command_receipts WHERE idempotency_key='{lost_key}';") + replayed = runtime_call(harness, harness.transition_sql(lost_key, lost_session, 1, "active")) + expect(exact_hex(replayed) == stored, "lost-result retry did not return stored bytes") + report["connection_loss"] = {"precommit_sqlstate": terminated.sqlstate, + "precommit_rows": 0, "retry_effects": 1, "postcommit_replay": "byte_identical"} + + +def prove_stable_rejections(harness: Harness, report: dict[str, object]) -> None: + cases: dict[str, str] = {} + + missing = str(uuid.uuid4()) + key = "reject-missing" + first = runtime_call(harness, harness.transition_sql(key, missing, 1, "active")) + expect(first.returncode == 0, "missing-target rejection did not commit") + harness.add_session(missing) + replay = runtime_call(harness, harness.transition_sql(key, missing, 1, "active")) + expect(exact_hex(first) == exact_hex(replay), "missing-target rejection changed") + cases["missing_target"] = "stable_domain_rejection" + + stale = str(uuid.uuid4()) + harness.add_session(stale, revision=2) + key = "reject-stale" + first = runtime_call(harness, harness.transition_sql(key, stale, 1, "active")) + harness.must_psql(f"SET ROLE {OWNER_ROLE}; UPDATE decodex.prototype_runtime_sessions SET revision=1 WHERE runtime_session_id='{stale}'; RESET ROLE;") + replay = runtime_call(harness, harness.transition_sql(key, stale, 1, "active")) + expect(exact_hex(first) == exact_hex(replay), "stale rejection changed") + cases["stale_revision"] = "stable_domain_rejection" + + illegal = str(uuid.uuid4()) + harness.add_session(illegal, state="active") + key = "reject-illegal" + first = runtime_call(harness, harness.transition_sql(key, illegal, 1, "starting")) + harness.must_psql(f"SET ROLE {OWNER_ROLE}; UPDATE decodex.prototype_runtime_sessions SET state='starting' WHERE runtime_session_id='{illegal}'; RESET ROLE;") + replay = runtime_call(harness, harness.transition_sql(key, illegal, 1, "starting")) + expect(exact_hex(first) == exact_hex(replay), "illegal-transition rejection changed") + cases["illegal_transition"] = "stable_domain_rejection" + report["stable_rejections"] = cases + + +def prove_deferred_and_privileges(harness: Harness, report: dict[str, object]) -> None: + incomplete = harness.psql( + f"BEGIN; SET ROLE {OWNER_ROLE}; SELECT decodex.prototype_leave_incomplete('{PROTOCOL}','incomplete'); COMMIT;" + ) + expect(incomplete.sqlstate == "23514", "deferred completion invariant did not reject commit") + expect(scalar(harness, "SELECT count(*) FROM decodex.exact_command_receipts WHERE idempotency_key='incomplete';") == "0", "incomplete receipt committed") + owner_immutable = { + "update_completed": "SET ROLE decodex_exact_owner; UPDATE decodex.exact_command_receipts SET response_bytes=response_bytes WHERE idempotency_key='wait-replay'", + "delete_completed": "SET ROLE decodex_exact_owner; DELETE FROM decodex.exact_command_receipts WHERE idempotency_key='wait-replay'", + "truncate_completed": "SET ROLE decodex_exact_owner; TRUNCATE decodex.exact_command_receipts", + } + for name, sql in owner_immutable.items(): + result = harness.psql(sql) + expect(result.sqlstate == "23514", f"owner could {name.replace('_', ' ')}") + + privilege_sql = { + "select": "SELECT * FROM decodex.exact_command_receipts", + "insert": "INSERT INTO decodex.exact_command_receipts(protocol_version,idempotency_key,request_envelope,request_digest,receipt_state) VALUES ('x','x','{}',decodex_crypto.digest(convert_to('{}','UTF8'),'sha256'),'executing')", + "update": "UPDATE decodex.exact_command_receipts SET outcome_class='x'", + "delete": "DELETE FROM decodex.exact_command_receipts", + "truncate": "TRUNCATE decodex.exact_command_receipts", + } + privileges: dict[str, str] = {} + for name, sql in privilege_sql.items(): + result = runtime_call(harness, sql) + expect(result.sqlstate == "42501", f"runtime exact receipt {name} was not denied") + privileges[name] = result.sqlstate or "" + + forgery_sql = { + "activity_kind": "INSERT INTO decodex.prototype_activity(aggregate_kind,aggregate_id,event_kind,payload) VALUES ('runtime_session',gen_random_uuid(),'runtime_session_transitioned','{}')", + "activity_structured": "INSERT INTO decodex.prototype_activity(aggregate_kind,aggregate_id,event_kind,payload) VALUES ('other',gen_random_uuid(),'other','{\"aggregate_kind\":\"runtime_session\",\"event_kind\":\"runtime_session_transitioned\"}')", + "outbox_effect": "INSERT INTO decodex.prototype_outbox(activity_id,effect_key,payload) VALUES (1,'runtime-session-transition:forged','{}')", + "outbox_link": "INSERT INTO decodex.prototype_outbox(activity_id,effect_key,payload) VALUES (1,'other','{\"activity_id\":1,\"event\":{\"aggregate_kind\":\"runtime_session\"}}')", + } + for name, sql in forgery_sql.items(): + result = runtime_call(harness, sql) + expect(result.sqlstate == "42501", f"runtime canonical forgery {name} was not denied") + privileges[name] = result.sqlstate or "" + + closure = scalar( + harness, + "SELECT (SELECT rolname FROM pg_roles r JOIN pg_proc p ON p.proowner=r.oid WHERE p.oid='decodex.transition_runtime_session_exact(text,text,uuid,bigint,decodex.prototype_session_state,text)'::regprocedure) || ',' || " + "(SELECT prosecdef::text FROM pg_proc WHERE oid='decodex.transition_runtime_session_exact(text,text,uuid,bigint,decodex.prototype_session_state,text)'::regprocedure) || ',' || " + "pg_has_role('decodex_exact_runtime','decodex_exact_owner','MEMBER')::text || ',' || " + "(SELECT count(*) FROM pg_proc WHERE pronamespace='decodex'::regnamespace AND proname='transition_runtime_session_exact');" + ) + expect(closure == f"{OWNER_ROLE},true,false,1", f"catalog closure mismatch: {closure}") + report["authority"] = {"runtime_denials": privileges, "definer_closure": closure, + "incomplete_commit_sqlstate": incomplete.sqlstate, + "completed_owner_rewrite_denials": {name: "23514" for name in owner_immutable}} + + +def prove_envelopes(harness: Harness, report: dict[str, object]) -> None: + u = [str(uuid.uuid4()) for _ in range(5)] + create = scalar( + harness, + "SELECT decodex.build_runtime_session_create_request(" + f"'{PROTOCOL}','{u[0]}','{u[1]}','task','{u[2]}','{u[3]}','label','available',7,NULL,'starting')::text;", + role=OWNER_ROLE, + ) + create_json = json.loads(create) + expect("codex_thread_id" in create_json and create_json["codex_thread_id"] is None, "optional create key is absent") + transition = scalar( + harness, + f"SELECT decodex.build_runtime_session_transition_request('{PROTOCOL}','{u[0]}',1,'active',NULL)::text;", + role=OWNER_ROLE, + ) + expect(json.loads(transition).get("note", "absent") is None, "optional transition key is absent") + + literal = scalar(harness, f"SELECT decodex.build_runtime_session_transition_request('{PROTOCOL}','{u[0]}',1,'active',NULL)::text;", role=OWNER_ROLE) + bound = scalar(harness, f"PREPARE typed(bigint) AS SELECT decodex.build_runtime_session_transition_request('{PROTOCOL}','{u[0]}',$1,'active',NULL)::text; EXECUTE typed(1);", role=OWNER_ROLE) + casted = scalar(harness, f"SELECT decodex.build_runtime_session_transition_request('{PROTOCOL}','{u[0]}',('1'::text)::bigint,'active',NULL)::text;", role=OWNER_ROLE) + expect(literal == bound == casted, "typed integer inputs did not converge") + + bootstrap_args = ",".join(["'m'","'medium'","'standard'","'instructions'","NULL"] * 4) + bootstrap = scalar(harness, f"SELECT decodex.build_role_profile_bootstrap_request('{PROTOCOL}',{bootstrap_args})::text;", role=OWNER_ROLE) + profiles = json.loads(bootstrap)["profiles"] + expect([item["role"] for item in profiles] == ["advisor","lead","task","reviewer"], "bootstrap role order is ambiguous") + expect(all("provenance" in item and item["provenance"] is None for item in profiles), "bootstrap optional null is absent") + incomplete = harness.psql( + f"SELECT decodex.build_role_profile_bootstrap_request('{PROTOCOL}',NULL,{','.join(["'x'"] * 19)});", + role=OWNER_ROLE, + ) + expect(incomplete.sqlstate == "22004", "incomplete bootstrap shape was accepted") + + base_session = str(uuid.uuid4()) + harness.add_session(base_session) + base = runtime_call(harness, harness.transition_sql("exact-text", base_session, 1, "active", "café")) + expect(base.returncode == 0, "exact-text baseline failed") + variants = ["café", "Café", "café "] + for value in variants: + result = runtime_call(harness, harness.transition_sql("exact-text", base_session, 1, "active", value)) + expect(classify(result) == "idempotency_conflict", f"text variant did not conflict: {value!r}") + report["envelopes"] = {"optional_keys_explicit_null": True, "typed_integer_convergence": True, "bootstrap_fixed_roles": True, "exact_text_conflicts": len(variants)} + + +def run_isolation_pair(harness: Harness, isolation: str, key: str, session: str) -> tuple[Result, Result]: + call = harness.transition_sql(key, session, 1, "active") + first_sql = f"BEGIN ISOLATION LEVEL {isolation}; {call} SELECT pg_sleep(0.6); COMMIT;" + second_sql = f"BEGIN ISOLATION LEVEL {isolation}; {call} COMMIT;" + with ThreadPoolExecutor(max_workers=2) as pool: + first = pool.submit(runtime_call, harness, first_sql) + time.sleep(0.12) + second = pool.submit(runtime_call, harness, second_sql) + return first.result(), second.result() + + +def prove_isolation_and_deadlock(harness: Harness, report: dict[str, object]) -> None: + results: dict[str, object] = {} + for isolation in ("READ COMMITTED", "REPEATABLE READ", "SERIALIZABLE"): + session = str(uuid.uuid4()) + key = "isolation-" + isolation.lower().replace(" ", "-") + harness.add_session(session) + first, second = run_isolation_pair(harness, isolation, key, session) + classes = [classify(first), classify(second)] + expect(classes[0] == "success", f"first {isolation} transaction failed") + expect(classes[1] in {"success", "retryable_infrastructure_failure"}, f"unexpected {isolation} outcome") + if second.returncode != 0: + expect(second.sqlstate == "40001", f"unexpected isolation SQLSTATE {second.sqlstate}") + retry = runtime_call(harness, f"BEGIN ISOLATION LEVEL {isolation}; {harness.transition_sql(key, session, 1, 'active')} COMMIT;") + expect(retry.returncode == 0, f"whole transaction retry failed for {isolation}") + effects = scalar(harness, f"SELECT count(*) FROM decodex.prototype_activity WHERE aggregate_id='{session}';") + expect(effects == "1", f"duplicate effect under {isolation}") + results[isolation] = {"initial": classes, "second_sqlstate": second.sqlstate, "effects": 1} + + a, b = str(uuid.uuid4()), str(uuid.uuid4()) + harness.add_session(a) + harness.add_session(b) + call_a = harness.transition_sql("deadlock-a", a, 1, "active") + call_b = harness.transition_sql("deadlock-b", b, 1, "active") + tx1 = "BEGIN; " + call_a + " SELECT pg_sleep(0.35); " + call_b + " COMMIT;" + tx2 = "BEGIN; " + call_b + " SELECT pg_sleep(0.35); " + call_a + " COMMIT;" + with ThreadPoolExecutor(max_workers=2) as pool: + one = pool.submit(runtime_call, harness, tx1) + two = pool.submit(runtime_call, harness, tx2) + deadlock_results = [one.result(), two.result()] + expect(sorted(r.sqlstate or "success" for r in deadlock_results) == ["40P01", "success"], "opposite-order schedule did not produce one classified deadlock") + loser_sql = tx1 if deadlock_results[0].returncode != 0 else tx2 + retry = runtime_call(harness, loser_sql) + expect(retry.returncode == 0, "deadlock whole-transaction retry failed") + expect(scalar(harness, f"SELECT count(*) FROM decodex.prototype_activity WHERE aggregate_id IN ('{a}','{b}');") == "2", "deadlock retry duplicated or lost effects") + results["opposite_order"] = {"sqlstate": "40P01", "retry": "converged", "effects": 2} + report["isolation"] = results + + +def stress_round(harness: Harness, repetition: int, mixed: bool) -> dict[str, object]: + session = str(uuid.uuid4()) + key = f"stress-{'mixed' if mixed else 'same'}-{repetition}" + harness.add_session(session) + requests = [] + for index in range(STRESS_WIDTH): + note = "alpha" if not mixed or index < STRESS_WIDTH // 2 else "beta" + requests.append(harness.transition_sql(key, session, 1, "active", note)) + with ThreadPoolExecutor(max_workers=STRESS_WIDTH) as pool: + observed = list(pool.map(lambda sql: runtime_call(harness, sql), requests)) + classes = [classify(item) for item in observed] + successes = [exact_hex(item) for item in observed if item.returncode == 0] + expect(successes and len(set(successes)) == 1, "stress responses mismatched") + if mixed: + expect(classes.count("success") == STRESS_WIDTH // 2, "mixed stress winner cardinality changed") + expect(classes.count("idempotency_conflict") == STRESS_WIDTH // 2, "mixed stress conflict cardinality changed") + else: + expect(classes == ["success"] * STRESS_WIDTH, "identical stress did not fully replay") + counts = scalar( + harness, + "SELECT (SELECT count(*) FROM decodex.prototype_activity WHERE aggregate_id='" + session + "') || ',' || " + "(SELECT count(*) FROM decodex.prototype_outbox o JOIN decodex.prototype_activity a USING(activity_id) WHERE a.aggregate_id='" + session + "') || ',' || " + "(SELECT count(*) FROM decodex.exact_command_receipts WHERE idempotency_key='" + key + "' AND receipt_state='executing');", + ) + expect(counts == "1,1,0", f"stress invariant mismatch: {counts}") + return {"success": classes.count("success"), "conflict": classes.count("idempotency_conflict")} + + +def prove_stress(harness: Harness, report: dict[str, object]) -> None: + identical = [stress_round(harness, i, False) for i in range(STRESS_REPETITIONS)] + mixed = [stress_round(harness, i, True) for i in range(STRESS_REPETITIONS)] + report["stress"] = { + "identical": {"repetitions": STRESS_REPETITIONS, "width": STRESS_WIDTH, + "successes": sum(item["success"] for item in identical), "anomalies": 0}, + "mixed": {"repetitions": STRESS_REPETITIONS, "width": STRESS_WIDTH, + "successes": sum(item["success"] for item in mixed), + "conflicts": sum(item["conflict"] for item in mixed), "anomalies": 0}, + } + + +def prove_effect_binding_and_restore(harness: Harness, report: dict[str, object]) -> dict[str, int]: + mismatch = int(scalar( + harness, + "WITH decoded AS (SELECT r.*, pg_catalog.convert_from(r.response_bytes,'UTF8')::pg_catalog.jsonb AS response " + "FROM decodex.exact_command_receipts r WHERE r.receipt_state='completed_success') " + "SELECT count(*) FROM decoded r " + "LEFT JOIN decodex.prototype_runtime_sessions s ON s.runtime_session_id=(r.effect_envelope->'persisted_session'->>'runtime_session_id')::pg_catalog.uuid " + "LEFT JOIN decodex.prototype_activity a ON a.activity_id=(r.effect_envelope->>'activity_id')::pg_catalog.int8 " + "LEFT JOIN decodex.prototype_outbox o ON o.outbox_id=(r.effect_envelope->>'outbox_id')::pg_catalog.int8 " + "WHERE r.response->>'classification' IS DISTINCT FROM 'success' " + "OR r.response->'effect' IS DISTINCT FROM r.effect_envelope " + "OR s.runtime_session_id IS NULL OR r.effect_envelope->'persisted_session' IS DISTINCT FROM pg_catalog.jsonb_build_object(" + "'runtime_session_id',s.runtime_session_id,'state',s.state,'revision',s.revision,'updated_at',s.updated_at) " + "OR a.activity_id IS NULL OR a.aggregate_kind IS DISTINCT FROM 'runtime_session' " + "OR a.aggregate_id IS DISTINCT FROM s.runtime_session_id OR a.event_kind IS DISTINCT FROM 'runtime_session_transitioned' " + "OR a.payload IS DISTINCT FROM r.effect_envelope->'activity_payload' " + "OR o.outbox_id IS NULL OR o.activity_id IS DISTINCT FROM a.activity_id " + "OR o.effect_key IS DISTINCT FROM 'runtime-session-transition:' || a.activity_id::pg_catalog.text " + "OR o.payload IS DISTINCT FROM r.effect_envelope->'outbox_payload';", + )) + expect(mismatch == 0, f"decoded response/effect rows have {mismatch} persisted binding mismatches") + catalog_sql = """ + WITH function_rows AS ( + SELECT pg_catalog.jsonb_build_object( + 'name', p.proname, + 'signature', p.oid::pg_catalog.regprocedure::pg_catalog.text, + 'identity_types', pg_catalog.oidvectortypes(p.proargtypes), + 'owner', owner_role.rolname, + 'prosecdef', p.prosecdef, + 'language', language.lanname, + 'volatility', p.provolatile, + 'parallel', p.proparallel, + 'settings', p.proconfig, + 'source_sha256', pg_catalog.encode(decodex_crypto.digest(pg_catalog.convert_to(p.prosrc,'UTF8'),'sha256'),'hex'), + 'definition_sha256', pg_catalog.encode(decodex_crypto.digest(pg_catalog.convert_to(pg_catalog.pg_get_functiondef(p.oid),'UTF8'),'sha256'),'hex'), + 'public_execute', pg_catalog.has_function_privilege('public', p.oid, 'EXECUTE'), + 'runtime_execute', pg_catalog.has_function_privilege('decodex_exact_runtime', p.oid, 'EXECUTE'), + 'owner_execute', pg_catalog.has_function_privilege('decodex_exact_owner', p.oid, 'EXECUTE'), + 'overloads', (SELECT pg_catalog.count(*) FROM pg_catalog.pg_proc q WHERE q.pronamespace=p.pronamespace AND q.proname=p.proname), + 'semantic_acl', COALESCE(( + SELECT pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object( + 'grantor', grantor_role.rolname, + 'grantee', COALESCE(grantee_role.rolname,'PUBLIC'), + 'privilege', privilege.privilege_type, + 'grantable', privilege.is_grantable + ) ORDER BY grantor_role.rolname,COALESCE(grantee_role.rolname,'PUBLIC'),privilege.privilege_type,privilege.is_grantable) + FROM pg_catalog.aclexplode(COALESCE(p.proacl,pg_catalog.acldefault('f',p.proowner))) privilege + JOIN pg_catalog.pg_roles grantor_role ON grantor_role.oid=privilege.grantor + LEFT JOIN pg_catalog.pg_roles grantee_role ON grantee_role.oid=privilege.grantee + ), '[]'::pg_catalog.jsonb), + 'dependencies', COALESCE(( + SELECT pg_catalog.jsonb_agg(pg_catalog.pg_describe_object(d.refclassid,d.refobjid,d.refobjsubid) || ':' || d.deptype::pg_catalog.text + ORDER BY pg_catalog.pg_describe_object(d.refclassid,d.refobjid,d.refobjsubid),d.deptype) + FROM pg_catalog.pg_depend d WHERE d.classid='pg_catalog.pg_proc'::pg_catalog.regclass AND d.objid=p.oid + ), '[]'::pg_catalog.jsonb) + ) AS item + FROM pg_catalog.pg_proc p + JOIN pg_catalog.pg_roles owner_role ON owner_role.oid=p.proowner + JOIN pg_catalog.pg_language language ON language.oid=p.prolang + WHERE p.pronamespace='decodex'::pg_catalog.regnamespace + ), receipt_relation AS ( + SELECT c.* FROM pg_catalog.pg_class c WHERE c.oid='decodex.exact_command_receipts'::pg_catalog.regclass + ) + SELECT pg_catalog.jsonb_build_object( + 'functions', (SELECT pg_catalog.jsonb_agg(item ORDER BY item->>'signature') FROM function_rows), + 'relation_owner', (SELECT r.rolname FROM receipt_relation c JOIN pg_catalog.pg_roles r ON r.oid=c.relowner), + 'relation_effective', pg_catalog.jsonb_build_object( + 'owner', pg_catalog.jsonb_build_object( + 'SELECT',pg_catalog.has_table_privilege('decodex_exact_owner','decodex.exact_command_receipts','SELECT'), + 'INSERT',pg_catalog.has_table_privilege('decodex_exact_owner','decodex.exact_command_receipts','INSERT'), + 'UPDATE',pg_catalog.has_table_privilege('decodex_exact_owner','decodex.exact_command_receipts','UPDATE'), + 'DELETE',pg_catalog.has_table_privilege('decodex_exact_owner','decodex.exact_command_receipts','DELETE'), + 'TRUNCATE',pg_catalog.has_table_privilege('decodex_exact_owner','decodex.exact_command_receipts','TRUNCATE'), + 'REFERENCES',pg_catalog.has_table_privilege('decodex_exact_owner','decodex.exact_command_receipts','REFERENCES'), + 'TRIGGER',pg_catalog.has_table_privilege('decodex_exact_owner','decodex.exact_command_receipts','TRIGGER'), + 'MAINTAIN',pg_catalog.has_table_privilege('decodex_exact_owner','decodex.exact_command_receipts','MAINTAIN')), + 'runtime', pg_catalog.jsonb_build_object( + 'SELECT',pg_catalog.has_table_privilege('decodex_exact_runtime','decodex.exact_command_receipts','SELECT'), + 'INSERT',pg_catalog.has_table_privilege('decodex_exact_runtime','decodex.exact_command_receipts','INSERT'), + 'UPDATE',pg_catalog.has_table_privilege('decodex_exact_runtime','decodex.exact_command_receipts','UPDATE'), + 'DELETE',pg_catalog.has_table_privilege('decodex_exact_runtime','decodex.exact_command_receipts','DELETE'), + 'TRUNCATE',pg_catalog.has_table_privilege('decodex_exact_runtime','decodex.exact_command_receipts','TRUNCATE'), + 'REFERENCES',pg_catalog.has_table_privilege('decodex_exact_runtime','decodex.exact_command_receipts','REFERENCES'), + 'TRIGGER',pg_catalog.has_table_privilege('decodex_exact_runtime','decodex.exact_command_receipts','TRIGGER'), + 'MAINTAIN',pg_catalog.has_table_privilege('decodex_exact_runtime','decodex.exact_command_receipts','MAINTAIN')), + 'public', pg_catalog.jsonb_build_object( + 'SELECT',pg_catalog.has_table_privilege('public','decodex.exact_command_receipts','SELECT'), + 'INSERT',pg_catalog.has_table_privilege('public','decodex.exact_command_receipts','INSERT'), + 'UPDATE',pg_catalog.has_table_privilege('public','decodex.exact_command_receipts','UPDATE'), + 'DELETE',pg_catalog.has_table_privilege('public','decodex.exact_command_receipts','DELETE'), + 'TRUNCATE',pg_catalog.has_table_privilege('public','decodex.exact_command_receipts','TRUNCATE'), + 'REFERENCES',pg_catalog.has_table_privilege('public','decodex.exact_command_receipts','REFERENCES'), + 'TRIGGER',pg_catalog.has_table_privilege('public','decodex.exact_command_receipts','TRIGGER'), + 'MAINTAIN',pg_catalog.has_table_privilege('public','decodex.exact_command_receipts','MAINTAIN')) + ), + 'relation_semantic_acl', (SELECT pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object( + 'grantor',grantor_role.rolname,'grantee',COALESCE(grantee_role.rolname,'PUBLIC'), + 'privilege',privilege.privilege_type,'grantable',privilege.is_grantable + ) ORDER BY grantor_role.rolname,COALESCE(grantee_role.rolname,'PUBLIC'),privilege.privilege_type,privilege.is_grantable) + FROM receipt_relation c + CROSS JOIN LATERAL pg_catalog.aclexplode(COALESCE(c.relacl,pg_catalog.acldefault('r',c.relowner))) privilege + JOIN pg_catalog.pg_roles grantor_role ON grantor_role.oid=privilege.grantor + LEFT JOIN pg_catalog.pg_roles grantee_role ON grantee_role.oid=privilege.grantee), + 'default_privileges', COALESCE((SELECT pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object( + 'role',owner_role.rolname,'schema',namespace.nspname,'type',d.defaclobjtype, + 'grantor',grantor_role.rolname,'grantee',COALESCE(grantee_role.rolname,'PUBLIC'), + 'privilege',privilege.privilege_type,'grantable',privilege.is_grantable + ) ORDER BY owner_role.rolname,namespace.nspname,d.defaclobjtype,grantor_role.rolname,COALESCE(grantee_role.rolname,'PUBLIC'),privilege.privilege_type) + FROM pg_catalog.pg_default_acl d + JOIN pg_catalog.pg_roles owner_role ON owner_role.oid=d.defaclrole + LEFT JOIN pg_catalog.pg_namespace namespace ON namespace.oid=d.defaclnamespace + CROSS JOIN LATERAL pg_catalog.aclexplode(d.defaclacl) privilege + JOIN pg_catalog.pg_roles grantor_role ON grantor_role.oid=privilege.grantor + LEFT JOIN pg_catalog.pg_roles grantee_role ON grantee_role.oid=privilege.grantee), '[]'::pg_catalog.jsonb), + 'role_memberships', COALESCE((SELECT pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object( + 'role',role_role.rolname,'member',member_role.rolname,'admin',membership.admin_option + ) ORDER BY role_role.rolname,member_role.rolname) + FROM pg_catalog.pg_auth_members membership + JOIN pg_catalog.pg_roles role_role ON role_role.oid=membership.roleid + JOIN pg_catalog.pg_roles member_role ON member_role.oid=membership.member + WHERE role_role.rolname IN ('decodex_exact_owner','decodex_exact_runtime') + OR member_role.rolname IN ('decodex_exact_owner','decodex_exact_runtime')), '[]'::pg_catalog.jsonb), + 'triggers', (SELECT pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object( + 'name',t.tgname,'constraint',t.tgconstraint<>0,'deferrable',t.tgdeferrable, + 'initially_deferred',t.tginitdeferred,'enabled',t.tgenabled, + 'function',t.tgfoid::pg_catalog.regprocedure::pg_catalog.text, + 'definition',pg_catalog.pg_get_triggerdef(t.oid,true) + ) ORDER BY t.tgname) FROM pg_catalog.pg_trigger t + WHERE t.tgrelid='decodex.exact_command_receipts'::pg_catalog.regclass AND NOT t.tgisinternal) + )::pg_catalog.text; + """ + catalog_before_text = scalar(harness, catalog_sql) + catalog_before = json.loads(catalog_before_text) + functions = {item["name"]: item for item in catalog_before["functions"]} + expect(set(functions) == set(EXPECTED_FUNCTIONS), f"function identity set drifted: {sorted(functions)}") + for name, (identity_types, language, volatility, prosecdef, runtime_execute) in EXPECTED_FUNCTIONS.items(): + item = functions[name] + expect(item["identity_types"] == identity_types, f"{name} signature drifted: {item['identity_types']}") + expected_regprocedure = f"decodex.{name}({identity_types.replace(', ', ',')})" + expect(item["signature"] == expected_regprocedure, f"{name} regprocedure identity drifted: {item['signature']}") + expect(item["owner"] == OWNER_ROLE and item["overloads"] == 1, f"{name} owner/overload closure failed") + expect(item["language"] == language and item["volatility"] == volatility, f"{name} language/volatility drifted") + expect(item["parallel"] == "u" and item["prosecdef"] is prosecdef, f"{name} execution metadata drifted") + expect(item["settings"] == ["search_path=pg_catalog, decodex"], f"{name} trusted search path drifted") + expect(item["owner_execute"] and not item["public_execute"] and item["runtime_execute"] is runtime_execute, + f"{name} effective execute closure failed") + expected_grantees = {OWNER_ROLE} | ({RUNTIME_ROLE} if runtime_execute else set()) + expect({entry["grantee"] for entry in item["semantic_acl"]} == expected_grantees, + f"{name} has unexpected function grantees: {item['semantic_acl']}") + expect(all(entry["grantor"] == OWNER_ROLE and entry["privilege"] == "EXECUTE" and not entry["grantable"] + for entry in item["semantic_acl"]), f"{name} semantic function ACL drifted") + expect(re.fullmatch(r"[0-9a-f]{64}", item["definition_sha256"]) is not None and item["dependencies"], + f"{name} source/dependency closure is incomplete") + expect(catalog_before["relation_owner"] == OWNER_ROLE, "receipt relation owner drifted") + expect(all(catalog_before["relation_effective"]["owner"].get(privilege) is True for privilege in TABLE_PRIVILEGES), + "receipt owner-effective privilege closure failed") + for role in ("runtime", "public"): + expect(all(catalog_before["relation_effective"][role].get(privilege) is False for privilege in TABLE_PRIVILEGES), + f"receipt {role} effective privilege closure failed") + relation_acl = catalog_before["relation_semantic_acl"] + expect({entry["privilege"] for entry in relation_acl} == set(TABLE_PRIVILEGES), + f"receipt semantic privilege set drifted: {relation_acl}") + expect(all(entry["grantor"] == OWNER_ROLE and entry["grantee"] == OWNER_ROLE and not entry["grantable"] + for entry in relation_acl), f"receipt relation has an unexpected grantee/grant option: {relation_acl}") + expect(catalog_before["default_privileges"] == [{"role": OWNER_ROLE, "schema": None, "type": "f", + "grantor": OWNER_ROLE, "grantee": OWNER_ROLE, "privilege": "EXECUTE", "grantable": False}], + f"default function privilege closure drifted: {catalog_before['default_privileges']}") + expect(catalog_before["role_memberships"] == [], f"owner/runtime role membership path exists: {catalog_before['role_memberships']}") + triggers = {item["name"]: item for item in catalog_before["triggers"]} + expect(set(triggers) == {"exact_receipts_complete_at_commit", "exact_receipts_immutable", "exact_receipts_untruncatable"}, + f"receipt trigger identity set drifted: {sorted(triggers)}") + expect(triggers["exact_receipts_complete_at_commit"]["function"] == "decodex.enforce_exact_receipt_completion()" + and triggers["exact_receipts_complete_at_commit"]["constraint"] + and triggers["exact_receipts_complete_at_commit"]["deferrable"] + and triggers["exact_receipts_complete_at_commit"]["initially_deferred"], "deferred completion trigger closure failed") + expect(triggers["exact_receipts_immutable"]["function"] == "decodex.forbid_exact_receipt_rewrite()" + and not triggers["exact_receipts_immutable"]["constraint"], "immutable trigger closure failed") + expect(triggers["exact_receipts_untruncatable"]["function"] == "decodex.forbid_exact_receipt_truncate()" + and not triggers["exact_receipts_untruncatable"]["constraint"], "truncate trigger closure failed") + expect(all(item["enabled"] == "O" and item["definition"] for item in triggers.values()), "receipt trigger enablement/definition drifted") + + before = scalar( + harness, + "SELECT count(*)||','||encode(decodex_crypto.digest(string_agg(encode(response_bytes,'hex'),'' ORDER BY protocol_version,idempotency_key),'sha256'),'hex') " + "FROM decodex.exact_command_receipts WHERE receipt_state<>'executing';", + ) + dump = harness.root / "xy1345.dump" + harness.require(["pg_dump", "-Fc", "-f", str(dump), DATABASE]) + harness.require(["createdb", RESTORE_DATABASE]) + harness.require(["pg_restore", "-d", RESTORE_DATABASE, str(dump)]) + after = scalar( + harness, + "SELECT count(*)||','||encode(decodex_crypto.digest(string_agg(encode(response_bytes,'hex'),'' ORDER BY protocol_version,idempotency_key),'sha256'),'hex') " + "FROM decodex.exact_command_receipts WHERE receipt_state<>'executing';", + database=RESTORE_DATABASE, + ) + expect(before == after, "restore changed receipt count or response bytes") + data_sql = """ + SELECT pg_catalog.jsonb_build_object( + 'receipts', (SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.to_jsonb(r) ORDER BY protocol_version,idempotency_key),'[]'::pg_catalog.jsonb) FROM decodex.exact_command_receipts r), + 'sessions', (SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.to_jsonb(s) ORDER BY runtime_session_id),'[]'::pg_catalog.jsonb) FROM decodex.prototype_runtime_sessions s), + 'activity', (SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.to_jsonb(a) ORDER BY activity_id),'[]'::pg_catalog.jsonb) FROM decodex.prototype_activity a), + 'outbox', (SELECT COALESCE(pg_catalog.jsonb_agg(pg_catalog.to_jsonb(o) ORDER BY outbox_id),'[]'::pg_catalog.jsonb) FROM decodex.prototype_outbox o) + )::pg_catalog.text; + """ + data_before_text = scalar(harness, data_sql) + data_after_text = scalar(harness, data_sql, database=RESTORE_DATABASE) + expect(data_before_text == data_after_text, "restore changed persisted receipts, sessions, activity, or outbox rows") + catalog_after_text = scalar(harness, catalog_sql, database=RESTORE_DATABASE) + catalog_after = json.loads(catalog_after_text) + drift = {key: [catalog_before.get(key), catalog_after.get(key)] + for key in catalog_before if catalog_before.get(key) != catalog_after.get(key)} + expect(not drift, f"restore changed catalog/source/dependency/ACL closure: {drift}") + duplicate_domain = int(scalar(harness, "SELECT count(*) FROM (SELECT aggregate_id,event_kind FROM decodex.prototype_activity GROUP BY aggregate_id,event_kind HAVING count(*)>1) duplicate;")) + duplicate_pairs = int(scalar(harness, "SELECT count(*) FROM (SELECT activity_id FROM decodex.prototype_outbox GROUP BY activity_id HAVING count(*)>1) duplicate;")) + unexplained = int(scalar(harness, + "SELECT (SELECT count(*) FROM decodex.prototype_activity a LEFT JOIN decodex.exact_command_receipts r ON (r.effect_envelope->>'activity_id')::bigint=a.activity_id WHERE r.protocol_version IS NULL) + " + "(SELECT count(*) FROM decodex.prototype_outbox o LEFT JOIN decodex.exact_command_receipts r ON (r.effect_envelope->>'outbox_id')::bigint=o.outbox_id WHERE r.protocol_version IS NULL);")) + authority_bypasses = sum(1 for role in ("runtime", "public") for privilege in TABLE_PRIVILEGES + if catalog_before["relation_effective"][role][privilege]) + sum(1 for item in functions.values() + if item["public_execute"] or item["runtime_execute"] != (item["name"] == "transition_runtime_session_exact")) + len(catalog_before["role_memberships"]) + anomalies = {"duplicate_domain_effects": duplicate_domain, "duplicate_activity_outbox_pairs": duplicate_pairs, + "mismatched_responses": mismatch, "authority_bypasses": authority_bypasses, "unexplained_rows": unexplained} + expect(not any(anomalies.values()), f"final persisted anomaly counters are nonzero: {anomalies}") + report["effect_restore"] = {"binding_mismatches": mismatch, + "receipt_bytes_digest": before.split(",", 1)[1], "restored_equal": True, + "persisted_data_sha256": hashlib.sha256(data_before_text.encode("utf-8")).hexdigest(), + "catalog_source_acl_dependency_restore_equal": True, + "attested_function_count": len(functions), + "function_definition_manifest_sha256": hashlib.sha256(json.dumps( + {name: item["definition_sha256"] for name, item in sorted(functions.items())}, + sort_keys=True, separators=(",", ":"), + ).encode("utf-8")).hexdigest(), + "function_definition_sha256": {name: item["definition_sha256"] for name, item in sorted(functions.items())}, + "transition_function_source_sha256": functions["transition_runtime_session_exact"]["source_sha256"]} + return anomalies + + +def build_report(harness: Harness) -> dict[str, object]: + version = harness.require(["postgres", "--version"]).stdout + expect(version == "postgres (PostgreSQL) 18.4", f"exact PostgreSQL version changed: {version}") + report: dict[str, object] = { + "schema": "decodex/xy-1345-exact-command-proof/1", + "postgres": version, + "protocol": PROTOCOL, + "harness_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + "schema_sql_sha256": hashlib.sha256(SCHEMA_SQL.encode("utf-8")).hexdigest(), + "classification": { + "stable_domain_rejection": ["missing_target", "stale_revision", "illegal_transition"], + "idempotency_conflict": ["DX001"], + "retryable_infrastructure_failure": ["40001", "40P01", "08006", "57P01", "DX900"], + }, + } + prove_wait_replay_and_conflicts(harness, report) + prove_abort_matrix(harness, report) + prove_connection_and_lost_result(harness, report) + prove_stable_rejections(harness, report) + prove_deferred_and_privileges(harness, report) + prove_envelopes(harness, report) + prove_isolation_and_deadlock(harness, report) + prove_stress(harness, report) + anomalies = prove_effect_binding_and_restore(harness, report) + committed_executing = int(scalar(harness, "SELECT count(*) FROM decodex.exact_command_receipts WHERE receipt_state='executing';")) + expect(committed_executing == 0, f"{committed_executing} committed executing receipts remain") + report["final_anomalies"] = {**anomalies, "committed_executing_rows": committed_executing, + "unclassified_sqlstates": 0} + return report + + +def main() -> int: + root = Path(tempfile.mkdtemp(prefix="decodex-xy1345-")).resolve() + harness = Harness(root) + report: dict[str, object] | None = None + error: Exception | None = None + cluster_stopped = not harness.started + try: + harness.start() + report = build_report(harness) + except Exception as exc: # cleanup must also cover architectural falsifiers + error = exc + finally: + try: + cluster_stopped = harness.stop() + except Exception as cleanup_exc: + cluster_stopped = False + if error is None: + error = cleanup_exc + else: + error = ProofFailure(f"proof failed: {error}; PostgreSQL shutdown also failed: {cleanup_exc}") + if cluster_stopped: + try: + shutil.rmtree(root) + except Exception as cleanup_exc: + if error is None: + error = ProofFailure(f"temporary proof root removal failed: {cleanup_exc}") + cleaned = not root.exists() + if not cleaned and error is None: + error = ProofFailure("temporary proof root was not removed") + if error is not None: + print(json.dumps({"result": "FAILED", "error": str(error), + "traceback": "".join(traceback.format_exception(error)), + "cleanup": {"cluster_stopped": cluster_stopped, "temporary_root_removed": cleaned}}, indent=2), file=sys.stderr) + return 1 + assert report is not None + report["cleanup"] = {"cluster_stopped": cluster_stopped, "temporary_root_removed": cleaned, + "tcp_disabled": True, "existing_services_enumerated_or_modified": False} + report["result"] = "PASSED" + print(json.dumps(report, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/scripts/test_exact_command_authority.py b/tests/scripts/test_exact_command_authority.py new file mode 100644 index 00000000..46687bef --- /dev/null +++ b/tests/scripts/test_exact_command_authority.py @@ -0,0 +1,154 @@ +"""Static regressions for the accepted XY-1345 command-authority boundary.""" + +from pathlib import Path +import re +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +HARNESS = ROOT / "scripts/vnext/exact_command_prototype.py" +NORMATIVE_FILES = ( + ROOT / "openwiki/decisions/vnext-authority.md", + ROOT / "openwiki/specs/vnext-authority.md", + ROOT / "openwiki/specs/vnext-gates.md", + ROOT / "openwiki/architecture/runtime-architecture.md", +) + + +class ExactCommandAuthorityTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.harness = HARNESS.read_text(encoding="utf-8") + cls.authority = "\n".join( + path.read_text(encoding="utf-8") for path in NORMATIVE_FILES + ) + + def test_prototype_uses_a_separate_deferred_exact_receipt_authority(self) -> None: + self.assertIn("CREATE TABLE decodex.exact_command_receipts", self.harness) + self.assertIn("DEFERRABLE INITIALLY DEFERRED", self.harness) + self.assertIn("PRIMARY KEY (protocol_version, idempotency_key)", self.harness) + self.assertNotIn("ALTER TABLE decodex.command_receipts", self.harness) + + def test_complete_command_signature_has_no_split_phase_authority(self) -> None: + match = re.search( + r"CREATE FUNCTION decodex\.transition_runtime_session_exact\((.*?)\) " + r"RETURNS pg_catalog\.bytea", + self.harness, + re.DOTALL, + ) + self.assertIsNotNone(match) + signature = match.group(1) + for forbidden in ( + "request_hash", + "request_digest", + "claim_token", + "claim_owner", + "lease", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, signature) + self.assertIn("p_idempotency_key", signature) + self.assertIn("p_target_state", signature) + + def test_envelope_equality_and_explicit_optional_nulls_are_owned_by_postgres(self) -> None: + self.assertIn("existing_request <> request_value", self.harness) + self.assertNotIn("existing_request @>", self.harness) + self.assertIn("'codex_thread_id', p_codex_thread_id", self.harness) + self.assertIn("'target_state', p_target_state, 'note', p_note", self.harness) + for builder in ( + "build_role_profile_bootstrap_request", + "build_role_profile_update_request", + "build_runtime_session_create_request", + "build_runtime_session_transition_request", + ): + with self.subTest(builder=builder): + self.assertIn(f"CREATE FUNCTION decodex.{builder}", self.harness) + + def test_restore_compares_receipt_acl_semantics_not_relacl_serialization(self) -> None: + self.assertNotIn("relacl::pg_catalog.text", self.harness) + self.assertIn("pg_catalog.aclexplode(COALESCE(c.relacl", self.harness) + relation_acl = self.harness.split("'relation_effective'", 1)[1].split( + "'default_privileges'", 1 + )[0] + for role in ("decodex_exact_owner", "decodex_exact_runtime", "public"): + with self.subTest(role=role): + self.assertIn(role, relation_acl) + for privilege in ( + "SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", + "REFERENCES", "TRIGGER", "MAINTAIN", + ): + with self.subTest(privilege=privilege): + self.assertIn("has_table_privilege", relation_acl) + self.assertIn(privilege, relation_acl) + self.assertIn("relation_semantic_acl", relation_acl) + self.assertIn("unexpected grantee/grant option", self.harness) + self.assertIn( + "ALTER DEFAULT PRIVILEGES FOR ROLE decodex_exact_owner\n" + "\tREVOKE EXECUTE ON FUNCTIONS FROM PUBLIC", + self.harness, + ) + self.assertNotIn( + "ALTER DEFAULT PRIVILEGES FOR ROLE decodex_exact_owner IN SCHEMA", + self.harness, + ) + + def test_catalog_closes_every_prototype_function_and_trigger(self) -> None: + expected = { + "enforce_exact_receipt_completion", + "forbid_exact_receipt_rewrite", + "forbid_exact_receipt_truncate", + "build_role_profile_bootstrap_request", + "build_role_profile_update_request", + "build_runtime_session_create_request", + "build_runtime_session_transition_request", + "prototype_failpoint", + "complete_prototype_rejection", + "transition_runtime_session_exact", + "prototype_leave_incomplete", + } + manifest = self.harness.split("EXPECTED_FUNCTIONS = {", 1)[1].split("}\n", 1)[0] + for function in expected: + with self.subTest(function=function): + self.assertIn(f'"{function}"', manifest) + self.assertEqual(manifest.count('"): (') + manifest.count('": ('), len(expected)) + for assertion in ( + "function identity set drifted", + "effective execute closure failed", + "semantic function ACL drifted", + "source/dependency closure is incomplete", + "receipt trigger identity set drifted", + ): + self.assertIn(assertion, self.harness) + self.assertIn("pg_catalog.pg_get_functiondef(p.oid)", self.harness) + self.assertIn("function_definition_manifest_sha256", self.harness) + + def test_effect_mismatches_decode_response_and_join_every_persisted_effect(self) -> None: + effect_proof = self.harness.split("def prove_effect_binding_and_restore", 1)[1] + self.assertIn("pg_catalog.convert_from(r.response_bytes,'UTF8')", effect_proof) + self.assertIn("LEFT JOIN decodex.prototype_runtime_sessions", effect_proof) + self.assertIn("r.response->'effect' IS DISTINCT FROM r.effect_envelope", effect_proof) + self.assertIn('"mismatched_responses": mismatch', effect_proof) + + def test_cleanup_requires_authoritative_cluster_shutdown(self) -> None: + stop = self.harness.split("\tdef stop(self)", 1)[1].split("\n\tdef psql", 1)[0] + self.assertIn("self.require", stop) + self.assertNotIn("self.command", stop) + self.assertIn('"cluster_stopped": cluster_stopped', self.harness) + self.assertIn("if not cleaned and error is None", self.harness) + + def test_normative_authority_rejects_candidate_era_protocol(self) -> None: + for required in ( + "exact_command_receipts", + "DEFERRABLE INITIALLY DEFERRED", + "READ COMMITTED", + "XY-1345", + "XY-1346", + "Candidate 3", + ): + with self.subTest(required=required): + self.assertIn(required, self.authority) + self.assertIn("caller-supplied request hash", self.authority) + + +if __name__ == "__main__": + unittest.main() From 6373f901bd6c55fcf67abfe433130dd50b7e9382 Mon Sep 17 00:00:00 2001 From: XV Date: Thu, 16 Jul 2026 22:15:07 -0400 Subject: [PATCH 2/7] {"schema":"decodex/commit/2","change":"Remove redundant proof shutdown initializer","authority":"manual","impact":"compatible"} --- openwiki/evidence/xy-1345-exact-command-authority.md | 8 ++++---- scripts/vnext/exact_command_prototype.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openwiki/evidence/xy-1345-exact-command-authority.md b/openwiki/evidence/xy-1345-exact-command-authority.md index d0574664..8f06d4e7 100644 --- a/openwiki/evidence/xy-1345-exact-command-authority.md +++ b/openwiki/evidence/xy-1345-exact-command-authority.md @@ -12,7 +12,7 @@ The corrected exact-command architecture passed. No acceptance threshold was wea - PostgreSQL: `postgres (PostgreSQL) 18.4` (`psql`, `pg_dump`, and `pg_restore` also 18.4). - Harness: `scripts/vnext/exact_command_prototype.py`. -- Harness SHA-256: `0b5ddc002affedd2988ef9367db1c3ca64e7db8469e38e865efbac659331e939`. +- Harness SHA-256: `bf56eee949ed5450b07a296fbb761cc4ffd9afc1ec8cc2a2057b96749daa7e68`. - Embedded schema/fixture SQL SHA-256: `47e4be2cb1f7f9b20616d3ef86cf34b817e90ddc7431ceeea65017531712bf9e`. - Exact transition function source SHA-256: @@ -20,9 +20,9 @@ The corrected exact-command architecture passed. No acceptance threshold was wea - Closed eleven-function `pg_get_functiondef` digest-manifest SHA-256: `d7b27ed3e42e7ac5a376afd8c3fd19aff039bcbed43d52ac17996b6d7efba2a5`. - Populated receipts/RuntimeSessions/activity/outbox snapshot SHA-256: - `7ba6bf1b1a5f86a2e960d555b13069cc784b7373fecba81cfb9d31a05f7f41db`. + `98cd96b0cff2102dbcd31dced06998f094951d0e583cd9d34b60deaf76ea2fde`. - Populated completed-receipt response-byte aggregate SHA-256 for this run: - `d14cb64cdb38cdc3d5f302006a3632eda82837054f1fb445e83e8ddc7fbcc917`. + `8795c1792590a77037aaffd89e058a49e347d59b564962ed0723a1ecaef7679d`. - Result schema: `decodex/xy-1345-exact-command-proof/1`. - Result: `PASSED`. @@ -64,7 +64,7 @@ not enumerate, connect to, stop, or modify any existing PostgreSQL service. | Schedule or invariant | Observed result | | --- | --- | -| Same key and same envelope in two sessions | Second session waited 0.663 seconds and returned byte-identical stored response bytes. | +| Same key and same envelope in two sessions | Second session waited 0.662 seconds and returned byte-identical stored response bytes. | | Same key and changed target state | `DX001` idempotency conflict before a second domain/activity/outbox effect. | | Same key and changed operation | `DX001` exact conflict. | | Abort after receipt, domain, activity, or outbox | Each injected `DX900` rolled back; a waiting transaction became executor; each case converged to one receipt, domain effect, activity, and outbox row. | diff --git a/scripts/vnext/exact_command_prototype.py b/scripts/vnext/exact_command_prototype.py index e82c0199..dd0e5873 100644 --- a/scripts/vnext/exact_command_prototype.py +++ b/scripts/vnext/exact_command_prototype.py @@ -1197,7 +1197,6 @@ def main() -> int: harness = Harness(root) report: dict[str, object] | None = None error: Exception | None = None - cluster_stopped = not harness.started try: harness.start() report = build_report(harness) From 117c42090a664fe28cda768cd9c51ca4ddc10424 Mon Sep 17 00:00:00 2001 From: XV Date: Fri, 17 Jul 2026 00:13:10 -0400 Subject: [PATCH 3/7] {"schema":"decodex/commit/2","change":"XY-1346 adds exact RoleProfile bootstrap and update commands with immutable revisions, receipts, retry semantics, authority manifests, focused tests, final-gate coverage, and aligned OpenWiki authority.","authority":"manual","impact":"compatible"} --- .../migrations/V9__exact_role_profiles.sql | 816 ++++++++++++++++++ crates/decodex-postgres/src/authority.rs | 207 ++++- crates/decodex-postgres/src/error.rs | 4 +- crates/decodex-postgres/src/lib.rs | 46 +- crates/decodex-postgres/src/migrations.rs | 11 +- crates/decodex-postgres/src/role_profiles.rs | 435 ++++++++++ .../decodex-postgres/tests/postgres_store.rs | 12 + .../tests/postgres_store/role_profiles.rs | 744 ++++++++++++++++ openwiki/architecture/runtime-architecture.md | 36 +- openwiki/decisions/vnext-authority.md | 3 +- .../operations/commands-and-validation.md | 11 +- openwiki/quickstart.md | 13 +- openwiki/specs/vnext-authority.md | 13 +- openwiki/specs/vnext-gates.md | 2 +- scripts/vnext/postgres_store_test.py | 160 +++- .../test_exact_role_profile_authority.py | 90 ++ 16 files changed, 2555 insertions(+), 48 deletions(-) create mode 100644 crates/decodex-postgres/migrations/V9__exact_role_profiles.sql create mode 100644 crates/decodex-postgres/src/role_profiles.rs create mode 100644 crates/decodex-postgres/tests/postgres_store/role_profiles.rs create mode 100644 tests/scripts/test_exact_role_profile_authority.py diff --git a/crates/decodex-postgres/migrations/V9__exact_role_profiles.sql b/crates/decodex-postgres/migrations/V9__exact_role_profiles.sql new file mode 100644 index 00000000..7d69a149 --- /dev/null +++ b/crates/decodex-postgres/migrations/V9__exact_role_profiles.sql @@ -0,0 +1,816 @@ +-- XY-1346 exact in-transaction receipts and immutable global RoleProfiles. +CREATE TYPE decodex.exact_receipt_state AS ENUM ( + 'executing', 'completed_success', 'completed_rejected' +); +CREATE TYPE decodex.role_profile_role AS ENUM ('advisor', 'lead', 'task', 'reviewer'); + +CREATE TABLE decodex.exact_command_receipts ( + protocol_version text NOT NULL, + idempotency_key text NOT NULL, + request_envelope jsonb NOT NULL, + request_digest bytea NOT NULL, + receipt_state decodex.exact_receipt_state NOT NULL, + outcome_class text, + effect_envelope jsonb, + response_bytes bytea, + created_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(), + completed_at timestamptz, + CONSTRAINT exact_command_receipts_pkey PRIMARY KEY (protocol_version, idempotency_key), + CONSTRAINT exact_command_receipts_identity_bounded CHECK ( + pg_catalog.octet_length(protocol_version) BETWEEN 1 AND 64 + AND protocol_version COLLATE pg_catalog."C" ~ '^[a-z0-9][a-z0-9._/-]{0,63}$' + AND pg_catalog.octet_length(idempotency_key) BETWEEN 1 AND 256 + AND NOT decodex.has_credential_material(idempotency_key) + ), + CONSTRAINT exact_command_receipts_request_digest CHECK ( + request_digest = public.digest( + pg_catalog.convert_to(request_envelope::text, 'UTF8'), 'sha256' + ) + ), + CONSTRAINT exact_command_receipts_no_credentials CHECK ( + NOT decodex.has_credential_material(request_envelope) + AND (effect_envelope IS NULL OR NOT decodex.has_credential_material(effect_envelope)) + ), + CONSTRAINT exact_command_receipts_shape CHECK ( + (receipt_state = 'executing' + AND outcome_class IS NULL AND effect_envelope IS NULL + AND response_bytes IS NULL AND completed_at IS NULL) + OR + (receipt_state = 'completed_success' AND outcome_class = 'success' + AND effect_envelope IS NOT NULL AND response_bytes IS NOT NULL + AND completed_at IS NOT NULL) + OR + (receipt_state = 'completed_rejected' AND outcome_class = 'stable_domain_rejection' + AND effect_envelope IS NOT NULL AND response_bytes IS NOT NULL + AND completed_at IS NOT NULL) + ), + CONSTRAINT exact_command_receipts_response_authority CHECK ( + response_bytes IS NULL OR ( + pg_catalog.convert_from(response_bytes, 'UTF8')::jsonb->>'classification' + IS NOT DISTINCT FROM outcome_class + AND pg_catalog.convert_from(response_bytes, 'UTF8')::jsonb->'effect' + IS NOT DISTINCT FROM effect_envelope + ) + ), + CONSTRAINT exact_command_receipts_finite_times CHECK ( + pg_catalog.isfinite(created_at) + AND (completed_at IS NULL OR pg_catalog.isfinite(completed_at)) + AND (completed_at IS NULL OR completed_at >= created_at) + ) +); + +CREATE TABLE decodex.role_profiles ( + role decodex.role_profile_role PRIMARY KEY, + current_revision bigint NOT NULL CHECK (current_revision > 0), + created_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(), + updated_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(), + CONSTRAINT role_profiles_finite_times CHECK ( + pg_catalog.isfinite(created_at) AND pg_catalog.isfinite(updated_at) + AND updated_at >= created_at + ) +); + +CREATE TABLE decodex.role_profile_revisions ( + role decodex.role_profile_role NOT NULL, + revision bigint NOT NULL CHECK (revision > 0), + model text NOT NULL, + reasoning_effort text NOT NULL, + service_tier text NOT NULL, + instructions text NOT NULL, + provenance text, + created_at timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp(), + CONSTRAINT role_profile_revisions_pkey PRIMARY KEY (role, revision), + CONSTRAINT role_profile_revisions_profile_fk FOREIGN KEY (role) + REFERENCES decodex.role_profiles(role) ON DELETE RESTRICT, + CONSTRAINT role_profile_revisions_configuration CHECK ( + pg_catalog.octet_length(model) BETWEEN 1 AND 128 + AND pg_catalog.octet_length(reasoning_effort) BETWEEN 1 AND 32 + AND reasoning_effort COLLATE pg_catalog."C" ~ '^[a-z][a-z0-9_-]{0,31}$' + AND pg_catalog.octet_length(service_tier) BETWEEN 1 AND 32 + AND service_tier COLLATE pg_catalog."C" ~ '^[a-z][a-z0-9_-]{0,31}$' + AND pg_catalog.octet_length(instructions) BETWEEN 1 AND 65536 + AND (provenance IS NULL OR pg_catalog.octet_length(provenance) BETWEEN 1 AND 4096) + AND model COLLATE pg_catalog."C" !~ '[[:cntrl:]]' + AND reasoning_effort COLLATE pg_catalog."C" !~ '[[:cntrl:]]' + AND service_tier COLLATE pg_catalog."C" !~ '[[:cntrl:]]' + AND instructions COLLATE pg_catalog."C" !~ U&'[\0080-\009F]' + AND (provenance IS NULL OR provenance COLLATE pg_catalog."C" !~ U&'[\0080-\009F]') + AND NOT decodex.has_credential_material(model) + AND NOT decodex.has_credential_material(reasoning_effort) + AND NOT decodex.has_credential_material(service_tier) + AND NOT decodex.has_credential_material(instructions) + AND (provenance IS NULL OR NOT decodex.has_credential_material(provenance)) + ), + CONSTRAINT role_profile_revisions_finite_time CHECK (pg_catalog.isfinite(created_at)) +); + +ALTER TABLE decodex.role_profiles + ADD CONSTRAINT role_profiles_current_revision_fk + FOREIGN KEY (role, current_revision) + REFERENCES decodex.role_profile_revisions(role, revision) + DEFERRABLE INITIALLY DEFERRED; + +CREATE FUNCTION decodex.enforce_exact_receipt_completion() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +DECLARE current_state decodex.exact_receipt_state; +BEGIN + SELECT receipt_state INTO current_state + FROM decodex.exact_command_receipts + WHERE protocol_version = NEW.protocol_version + AND idempotency_key = NEW.idempotency_key; + + IF current_state = 'executing' THEN + RAISE EXCEPTION 'exact command receipt must be completed before commit' + USING ERRCODE = '23514', CONSTRAINT = 'exact_receipts_complete_at_commit'; + END IF; + + RETURN NULL; +END +$$; + +CREATE CONSTRAINT TRIGGER exact_receipts_complete_at_commit +AFTER INSERT OR UPDATE ON decodex.exact_command_receipts +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW EXECUTE FUNCTION decodex.enforce_exact_receipt_completion(); + +CREATE FUNCTION decodex.forbid_exact_receipt_rewrite() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION 'exact command receipts cannot be deleted' + USING ERRCODE = '23514', CONSTRAINT = 'exact_receipts_immutable'; + END IF; + + IF OLD.receipt_state <> 'executing' + OR NEW.receipt_state = 'executing' + OR OLD.protocol_version <> NEW.protocol_version + OR OLD.idempotency_key <> NEW.idempotency_key + OR OLD.request_envelope <> NEW.request_envelope + OR OLD.request_digest <> NEW.request_digest + OR OLD.created_at <> NEW.created_at + THEN + RAISE EXCEPTION 'exact command receipt rewrite is forbidden' + USING ERRCODE = '23514', CONSTRAINT = 'exact_receipts_immutable'; + END IF; + + RETURN NEW; +END +$$; + +CREATE TRIGGER exact_receipts_immutable +BEFORE UPDATE OR DELETE ON decodex.exact_command_receipts +FOR EACH ROW EXECUTE FUNCTION decodex.forbid_exact_receipt_rewrite(); + +CREATE FUNCTION decodex.forbid_exact_receipt_truncate() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +BEGIN + RAISE EXCEPTION 'exact command receipts cannot be truncated' + USING ERRCODE = '23514', CONSTRAINT = 'exact_receipts_untruncatable'; +END +$$; + +CREATE TRIGGER exact_receipts_untruncatable +BEFORE TRUNCATE ON decodex.exact_command_receipts +FOR EACH STATEMENT EXECUTE FUNCTION decodex.forbid_exact_receipt_truncate(); + +CREATE FUNCTION decodex.enforce_complete_role_profile_set() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +DECLARE profile_count bigint; +BEGIN + SELECT pg_catalog.count(*) INTO profile_count FROM decodex.role_profiles; + + IF profile_count NOT IN (0, 4) + OR (profile_count = 4 AND EXISTS ( + SELECT expected.role + FROM (VALUES + ('advisor'::decodex.role_profile_role), ('lead'::decodex.role_profile_role), + ('task'::decodex.role_profile_role), ('reviewer'::decodex.role_profile_role) + ) AS expected(role) + EXCEPT SELECT role FROM decodex.role_profiles + )) + THEN + RAISE EXCEPTION 'RoleProfiles must contain exactly the four global roles' + USING ERRCODE = '23514', CONSTRAINT = 'role_profiles_exact_global_set'; + END IF; + + RETURN NULL; +END +$$; + +CREATE CONSTRAINT TRIGGER role_profiles_exact_global_set +AFTER INSERT OR UPDATE OR DELETE ON decodex.role_profiles +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW EXECUTE FUNCTION decodex.enforce_complete_role_profile_set(); + +CREATE FUNCTION decodex.forbid_role_profile_identity_rewrite() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION 'RoleProfile identities cannot be deleted' + USING ERRCODE = '23514', CONSTRAINT = 'role_profiles_identity_immutable'; + END IF; + + IF NEW.role <> OLD.role OR NEW.created_at <> OLD.created_at + OR NEW.current_revision <> OLD.current_revision + 1 + OR NEW.updated_at < OLD.updated_at + THEN + RAISE EXCEPTION 'RoleProfile identity or current revision rewrite is forbidden' + USING ERRCODE = '23514', CONSTRAINT = 'role_profiles_identity_immutable'; + END IF; + + RETURN NEW; +END +$$; + +CREATE TRIGGER role_profiles_identity_immutable +BEFORE UPDATE OR DELETE ON decodex.role_profiles +FOR EACH ROW EXECUTE FUNCTION decodex.forbid_role_profile_identity_rewrite(); + +CREATE FUNCTION decodex.forbid_role_profile_revision_mutation() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +BEGIN + RAISE EXCEPTION 'RoleProfile revisions are immutable' + USING ERRCODE = '23514', CONSTRAINT = 'role_profile_revisions_immutable'; +END +$$; + +CREATE TRIGGER role_profile_revisions_immutable +BEFORE UPDATE OR DELETE ON decodex.role_profile_revisions +FOR EACH ROW EXECUTE FUNCTION decodex.forbid_role_profile_revision_mutation(); + +CREATE FUNCTION decodex.forbid_role_profile_truncate() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +BEGIN + RAISE EXCEPTION 'RoleProfile authority cannot be truncated' + USING ERRCODE = '23514', CONSTRAINT = 'role_profiles_untruncatable'; +END +$$; + +CREATE TRIGGER role_profiles_untruncatable +BEFORE TRUNCATE ON decodex.role_profiles +FOR EACH STATEMENT EXECUTE FUNCTION decodex.forbid_role_profile_truncate(); +CREATE TRIGGER role_profile_revisions_untruncatable +BEFORE TRUNCATE ON decodex.role_profile_revisions +FOR EACH STATEMENT EXECUTE FUNCTION decodex.forbid_role_profile_truncate(); + +CREATE FUNCTION decodex.enforce_role_profile_event_namespace() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +DECLARE owner_name name; +DECLARE linked_role_profile boolean := false; +BEGIN + SELECT pg_catalog.pg_get_userbyid(class.relowner) INTO owner_name + FROM pg_catalog.pg_class AS class + WHERE class.oid = TG_RELID; + + IF TG_TABLE_NAME = 'activity' THEN + linked_role_profile := NEW.aggregate_kind = 'role_profile' + OR NEW.event_kind IN ('role_profile_bootstrapped', 'role_profile_updated') + OR NEW.payload->>'aggregate_kind' = 'role_profile' + OR NEW.payload->>'event_kind' IN ('role_profile_bootstrapped', 'role_profile_updated') + OR NEW.payload->>'kind' = 'role_profile' + OR NEW.payload OPERATOR(pg_catalog.?&) ARRAY[ + 'role', 'model', 'reasoning_effort', 'service_tier', 'instructions', 'revision' + ]; + IF TG_OP = 'UPDATE' THEN + linked_role_profile := linked_role_profile + OR OLD.aggregate_kind = 'role_profile' + OR OLD.event_kind IN ('role_profile_bootstrapped', 'role_profile_updated') + OR OLD.payload->>'aggregate_kind' = 'role_profile' + OR OLD.payload->>'event_kind' IN ('role_profile_bootstrapped', 'role_profile_updated') + OR OLD.payload->>'kind' = 'role_profile' + OR OLD.payload OPERATOR(pg_catalog.?&) ARRAY[ + 'role', 'model', 'reasoning_effort', 'service_tier', 'instructions', 'revision' + ]; + END IF; + ELSE + linked_role_profile := NEW.aggregate_kind = 'role_profile' + OR NEW.payload->>'aggregate_kind' = 'role_profile' + OR NEW.payload->>'event_kind' IN ('role_profile_bootstrapped', 'role_profile_updated') + OR NEW.payload->'payload'->>'kind' = 'role_profile' + OR EXISTS ( + SELECT 1 FROM decodex.activity AS activity + WHERE activity.sequence = (NEW.payload->>'activity_sequence')::bigint + AND activity.aggregate_kind = 'role_profile' + ); + IF TG_OP = 'UPDATE' THEN + linked_role_profile := linked_role_profile + OR OLD.aggregate_kind = 'role_profile' + OR OLD.payload->>'aggregate_kind' = 'role_profile' + OR OLD.payload->>'event_kind' IN ('role_profile_bootstrapped', 'role_profile_updated') + OR OLD.payload->'payload'->>'kind' = 'role_profile'; + END IF; + END IF; + + IF linked_role_profile AND current_user::name <> owner_name THEN + IF TG_TABLE_NAME = 'activity' OR TG_OP = 'INSERT' THEN + RAISE EXCEPTION 'RoleProfile activity/outbox namespace is command-owned' + USING ERRCODE = '42501', CONSTRAINT = 'role_profile_event_namespace'; + ELSIF NEW.id IS DISTINCT FROM OLD.id + OR NEW.effect_key IS DISTINCT FROM OLD.effect_key + OR NEW.aggregate_kind IS DISTINCT FROM OLD.aggregate_kind + OR NEW.aggregate_id IS DISTINCT FROM OLD.aggregate_id + OR NEW.aggregate_revision IS DISTINCT FROM OLD.aggregate_revision + OR NEW.payload IS DISTINCT FROM OLD.payload + OR NEW.created_at IS DISTINCT FROM OLD.created_at + THEN + RAISE EXCEPTION 'RoleProfile outbox authority fields are command-owned' + USING ERRCODE = '42501', CONSTRAINT = 'role_profile_event_namespace'; + END IF; + END IF; + + RETURN NEW; +EXCEPTION + WHEN invalid_text_representation THEN + RAISE EXCEPTION 'RoleProfile activity/outbox link is malformed' + USING ERRCODE = '42501', CONSTRAINT = 'role_profile_event_namespace'; +END +$$; + +CREATE TRIGGER activity_role_profile_namespace +BEFORE INSERT OR UPDATE ON decodex.activity +FOR EACH ROW EXECUTE FUNCTION decodex.enforce_role_profile_event_namespace(); +CREATE TRIGGER outbox_role_profile_namespace +BEFORE INSERT OR UPDATE ON decodex.outbox +FOR EACH ROW EXECUTE FUNCTION decodex.enforce_role_profile_event_namespace(); + +CREATE FUNCTION decodex.is_role_profile_configuration( + p_model text, p_reasoning_effort text, p_service_tier text, + p_instructions text, p_provenance text +) RETURNS boolean +LANGUAGE sql +IMMUTABLE +SET search_path = pg_catalog, decodex +AS $$ +SELECT p_model IS NOT NULL AND pg_catalog.octet_length(p_model) BETWEEN 1 AND 128 + AND p_reasoning_effort IS NOT NULL + AND pg_catalog.octet_length(p_reasoning_effort) BETWEEN 1 AND 32 + AND p_reasoning_effort COLLATE pg_catalog."C" ~ '^[a-z][a-z0-9_-]{0,31}$' + AND p_service_tier IS NOT NULL + AND pg_catalog.octet_length(p_service_tier) BETWEEN 1 AND 32 + AND p_service_tier COLLATE pg_catalog."C" ~ '^[a-z][a-z0-9_-]{0,31}$' + AND p_instructions IS NOT NULL + AND pg_catalog.octet_length(p_instructions) BETWEEN 1 AND 65536 + AND (p_provenance IS NULL OR pg_catalog.octet_length(p_provenance) BETWEEN 1 AND 4096) + AND p_model COLLATE pg_catalog."C" !~ '[[:cntrl:]]' + AND p_instructions COLLATE pg_catalog."C" !~ U&'[\0080-\009F]' + AND (p_provenance IS NULL OR p_provenance COLLATE pg_catalog."C" !~ U&'[\0080-\009F]') + AND NOT decodex.has_credential_material(p_model) + AND NOT decodex.has_credential_material(p_reasoning_effort) + AND NOT decodex.has_credential_material(p_service_tier) + AND NOT decodex.has_credential_material(p_instructions) + AND (p_provenance IS NULL OR NOT decodex.has_credential_material(p_provenance)) +$$; + +CREATE FUNCTION decodex.build_role_profile_bootstrap_request( + p_protocol text, + p_advisor_model text, p_advisor_reasoning_effort text, + p_advisor_service_tier text, p_advisor_instructions text, p_advisor_provenance text, + p_lead_model text, p_lead_reasoning_effort text, + p_lead_service_tier text, p_lead_instructions text, p_lead_provenance text, + p_task_model text, p_task_reasoning_effort text, + p_task_service_tier text, p_task_instructions text, p_task_provenance text, + p_reviewer_model text, p_reviewer_reasoning_effort text, + p_reviewer_service_tier text, p_reviewer_instructions text, p_reviewer_provenance text +) RETURNS jsonb +LANGUAGE plpgsql +IMMUTABLE +SET search_path = pg_catalog, decodex +AS $$ +BEGIN + IF p_protocol IS NULL OR p_advisor_model IS NULL OR p_advisor_reasoning_effort IS NULL + OR p_advisor_service_tier IS NULL OR p_advisor_instructions IS NULL + OR p_lead_model IS NULL OR p_lead_reasoning_effort IS NULL + OR p_lead_service_tier IS NULL OR p_lead_instructions IS NULL + OR p_task_model IS NULL OR p_task_reasoning_effort IS NULL + OR p_task_service_tier IS NULL OR p_task_instructions IS NULL + OR p_reviewer_model IS NULL OR p_reviewer_reasoning_effort IS NULL + OR p_reviewer_service_tier IS NULL OR p_reviewer_instructions IS NULL + THEN + RAISE EXCEPTION 'RoleProfile bootstrap request is incomplete' USING ERRCODE = '22004'; + END IF; + + RETURN pg_catalog.jsonb_build_object( + 'protocol_version', p_protocol, 'operation', 'bootstrap_role_profiles', + 'profiles', pg_catalog.jsonb_build_array( + pg_catalog.jsonb_build_object('role', 'advisor', 'model', p_advisor_model, + 'reasoning_effort', p_advisor_reasoning_effort, 'service_tier', p_advisor_service_tier, + 'instructions', p_advisor_instructions, 'provenance', p_advisor_provenance), + pg_catalog.jsonb_build_object('role', 'lead', 'model', p_lead_model, + 'reasoning_effort', p_lead_reasoning_effort, 'service_tier', p_lead_service_tier, + 'instructions', p_lead_instructions, 'provenance', p_lead_provenance), + pg_catalog.jsonb_build_object('role', 'task', 'model', p_task_model, + 'reasoning_effort', p_task_reasoning_effort, 'service_tier', p_task_service_tier, + 'instructions', p_task_instructions, 'provenance', p_task_provenance), + pg_catalog.jsonb_build_object('role', 'reviewer', 'model', p_reviewer_model, + 'reasoning_effort', p_reviewer_reasoning_effort, 'service_tier', p_reviewer_service_tier, + 'instructions', p_reviewer_instructions, 'provenance', p_reviewer_provenance) + ) + ); +END +$$; + +CREATE FUNCTION decodex.build_role_profile_update_request( + p_protocol text, p_role decodex.role_profile_role, p_expected_revision bigint, + p_model text, p_reasoning_effort text, p_service_tier text, + p_instructions text, p_provenance text +) RETURNS jsonb +LANGUAGE sql +IMMUTABLE +SET search_path = pg_catalog, decodex +AS $$ +SELECT pg_catalog.jsonb_build_object( + 'protocol_version', p_protocol, 'operation', 'update_role_profile', + 'role', p_role, 'expected_revision', p_expected_revision, + 'model', p_model, 'reasoning_effort', p_reasoning_effort, + 'service_tier', p_service_tier, 'instructions', p_instructions, + 'provenance', p_provenance +) +$$; + +CREATE FUNCTION decodex.complete_exact_role_profile_rejection( + p_protocol text, p_idempotency_key text, p_code text +) RETURNS bytea +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +DECLARE effect_value jsonb; +DECLARE response_value bytea; +BEGIN + effect_value := pg_catalog.jsonb_build_object('changed', false, 'code', p_code); + response_value := pg_catalog.convert_to(pg_catalog.jsonb_build_object( + 'classification', 'stable_domain_rejection', 'code', p_code, + 'effect', effect_value + )::text, 'UTF8'); + + UPDATE decodex.exact_command_receipts + SET receipt_state = 'completed_rejected', outcome_class = 'stable_domain_rejection', + effect_envelope = effect_value, response_bytes = response_value, + completed_at = pg_catalog.clock_timestamp() + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key; + + RETURN response_value; +END +$$; + +CREATE FUNCTION decodex.bootstrap_role_profiles_exact( + p_protocol text, p_idempotency_key text, + p_advisor_model text, p_advisor_reasoning_effort text, + p_advisor_service_tier text, p_advisor_instructions text, p_advisor_provenance text, + p_lead_model text, p_lead_reasoning_effort text, + p_lead_service_tier text, p_lead_instructions text, p_lead_provenance text, + p_task_model text, p_task_reasoning_effort text, + p_task_service_tier text, p_task_instructions text, p_task_provenance text, + p_reviewer_model text, p_reviewer_reasoning_effort text, + p_reviewer_service_tier text, p_reviewer_instructions text, p_reviewer_provenance text +) RETURNS bytea +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = pg_catalog, decodex +AS $$ +DECLARE request_value jsonb; +DECLARE existing_request jsonb; +DECLARE existing_response bytea; +DECLARE inserted_count integer; +DECLARE profiles_value jsonb; +DECLARE profile_value jsonb; +DECLARE activity_sequence bigint; +DECLARE activity_payload jsonb; +DECLARE outbox_id bigint; +DECLARE outbox_payload jsonb; +DECLARE effects_value jsonb := '[]'::jsonb; +DECLARE effect_value jsonb; +DECLARE response_value bytea; +BEGIN + IF p_protocol IS NULL OR p_idempotency_key IS NULL THEN + RAISE EXCEPTION 'exact RoleProfile command identity is incomplete' USING ERRCODE = '22004'; + END IF; + IF pg_catalog.octet_length(p_protocol) NOT BETWEEN 1 AND 64 + OR p_protocol COLLATE pg_catalog."C" !~ '^[a-z0-9][a-z0-9._/-]{0,63}$' + OR pg_catalog.octet_length(p_idempotency_key) NOT BETWEEN 1 AND 256 + OR decodex.has_credential_material(p_idempotency_key) + THEN + RAISE EXCEPTION 'exact RoleProfile command identity is invalid' USING ERRCODE = '22023'; + END IF; + + request_value := decodex.build_role_profile_bootstrap_request( + p_protocol, + p_advisor_model, p_advisor_reasoning_effort, p_advisor_service_tier, + p_advisor_instructions, p_advisor_provenance, + p_lead_model, p_lead_reasoning_effort, p_lead_service_tier, + p_lead_instructions, p_lead_provenance, + p_task_model, p_task_reasoning_effort, p_task_service_tier, + p_task_instructions, p_task_provenance, + p_reviewer_model, p_reviewer_reasoning_effort, p_reviewer_service_tier, + p_reviewer_instructions, p_reviewer_provenance + ); + + INSERT INTO decodex.exact_command_receipts( + protocol_version, idempotency_key, request_envelope, request_digest, receipt_state + ) VALUES ( + p_protocol, p_idempotency_key, request_value, + public.digest(pg_catalog.convert_to(request_value::text, 'UTF8'), 'sha256'), 'executing' + ) ON CONFLICT (protocol_version, idempotency_key) DO NOTHING; + GET DIAGNOSTICS inserted_count = ROW_COUNT; + + IF inserted_count = 0 THEN + SELECT request_envelope, response_bytes INTO existing_request, existing_response + FROM decodex.exact_command_receipts + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key + FOR UPDATE; + IF existing_request <> request_value THEN + RAISE EXCEPTION 'exact idempotency conflict' USING ERRCODE = 'DX001'; + END IF; + IF existing_response IS NULL THEN + RAISE EXCEPTION 'incomplete exact receipt is not replayable' USING ERRCODE = 'DX002'; + END IF; + RETURN existing_response; + END IF; + + IF NOT decodex.is_role_profile_configuration(p_advisor_model, p_advisor_reasoning_effort, + p_advisor_service_tier, p_advisor_instructions, p_advisor_provenance) + OR NOT decodex.is_role_profile_configuration(p_lead_model, p_lead_reasoning_effort, + p_lead_service_tier, p_lead_instructions, p_lead_provenance) + OR NOT decodex.is_role_profile_configuration(p_task_model, p_task_reasoning_effort, + p_task_service_tier, p_task_instructions, p_task_provenance) + OR NOT decodex.is_role_profile_configuration(p_reviewer_model, p_reviewer_reasoning_effort, + p_reviewer_service_tier, p_reviewer_instructions, p_reviewer_provenance) + THEN + RETURN decodex.complete_exact_role_profile_rejection( + p_protocol, p_idempotency_key, 'invalid_profile' + ); + END IF; + + LOCK TABLE decodex.role_profiles IN SHARE ROW EXCLUSIVE MODE; + IF EXISTS (SELECT 1 FROM decodex.role_profiles) THEN + RETURN decodex.complete_exact_role_profile_rejection( + p_protocol, p_idempotency_key, 'already_bootstrapped' + ); + END IF; + + WITH inputs(role, model, reasoning_effort, service_tier, instructions, provenance) AS ( + VALUES + ('advisor'::decodex.role_profile_role, p_advisor_model, + p_advisor_reasoning_effort, p_advisor_service_tier, + p_advisor_instructions, p_advisor_provenance), + ('lead'::decodex.role_profile_role, p_lead_model, + p_lead_reasoning_effort, p_lead_service_tier, + p_lead_instructions, p_lead_provenance), + ('task'::decodex.role_profile_role, p_task_model, + p_task_reasoning_effort, p_task_service_tier, + p_task_instructions, p_task_provenance), + ('reviewer'::decodex.role_profile_role, p_reviewer_model, + p_reviewer_reasoning_effort, p_reviewer_service_tier, + p_reviewer_instructions, p_reviewer_provenance) + ), inserted_profiles AS ( + INSERT INTO decodex.role_profiles(role, current_revision) + SELECT role, 1 FROM inputs ORDER BY role + RETURNING role, current_revision, created_at, updated_at + ), inserted_revisions AS ( + INSERT INTO decodex.role_profile_revisions( + role, revision, model, reasoning_effort, service_tier, instructions, provenance + ) + SELECT input.role, profile.current_revision, input.model, input.reasoning_effort, + input.service_tier, input.instructions, input.provenance + FROM inputs AS input JOIN inserted_profiles AS profile USING (role) + RETURNING role, revision, model, reasoning_effort, service_tier, + instructions, provenance, created_at + ) + SELECT pg_catalog.jsonb_agg(pg_catalog.jsonb_build_object( + 'role', revision.role, 'revision', revision.revision, + 'model', revision.model, 'reasoning_effort', revision.reasoning_effort, + 'service_tier', revision.service_tier, 'instructions', revision.instructions, + 'provenance', revision.provenance, 'created_at', revision.created_at, + 'updated_at', profile.updated_at + ) ORDER BY revision.role) + INTO profiles_value + FROM inserted_revisions AS revision JOIN inserted_profiles AS profile USING (role); + + FOR profile_value IN SELECT value FROM pg_catalog.jsonb_array_elements(profiles_value) + LOOP + activity_payload := profile_value || pg_catalog.jsonb_build_object('kind', 'role_profile'); + INSERT INTO decodex.activity( + aggregate_kind, aggregate_id, revision, event_kind, correlation_key, payload + ) VALUES ( + 'role_profile', profile_value->>'role', (profile_value->>'revision')::bigint, + 'role_profile_bootstrapped', p_idempotency_key, activity_payload + ) RETURNING sequence, payload INTO activity_sequence, activity_payload; + + outbox_payload := pg_catalog.jsonb_build_object( + 'activity_sequence', activity_sequence, + 'event_kind', 'role_profile_bootstrapped', 'aggregate_kind', 'role_profile', + 'aggregate_id', profile_value->>'role', + 'revision', (profile_value->>'revision')::bigint, 'payload', activity_payload + ); + INSERT INTO decodex.outbox( + effect_key, aggregate_kind, aggregate_id, aggregate_revision, payload + ) VALUES ( + 'activity/' || activity_sequence::text, 'role_profile', profile_value->>'role', + (profile_value->>'revision')::bigint, outbox_payload + ) RETURNING id, payload INTO outbox_id, outbox_payload; + + effects_value := effects_value || pg_catalog.jsonb_build_array( + pg_catalog.jsonb_build_object( + 'profile', profile_value, 'activity_sequence', activity_sequence, + 'activity_payload', activity_payload, 'outbox_id', outbox_id, + 'outbox_payload', outbox_payload + ) + ); + END LOOP; + + effect_value := pg_catalog.jsonb_build_object('profiles', effects_value); + response_value := pg_catalog.convert_to(pg_catalog.jsonb_build_object( + 'classification', 'success', 'effect', effect_value + )::text, 'UTF8'); + UPDATE decodex.exact_command_receipts + SET receipt_state = 'completed_success', outcome_class = 'success', + effect_envelope = effect_value, response_bytes = response_value, + completed_at = pg_catalog.clock_timestamp() + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key; + + RETURN response_value; +END +$$; + +CREATE FUNCTION decodex.update_role_profile_exact( + p_protocol text, p_idempotency_key text, + p_role decodex.role_profile_role, p_expected_revision bigint, + p_model text, p_reasoning_effort text, p_service_tier text, + p_instructions text, p_provenance text +) RETURNS bytea +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = pg_catalog, decodex +AS $$ +DECLARE request_value jsonb; +DECLARE existing_request jsonb; +DECLARE existing_response bytea; +DECLARE inserted_count integer; +DECLARE actual_revision bigint; +DECLARE profile_value jsonb; +DECLARE activity_sequence bigint; +DECLARE activity_payload jsonb; +DECLARE outbox_id bigint; +DECLARE outbox_payload jsonb; +DECLARE effect_value jsonb; +DECLARE response_value bytea; +BEGIN + IF p_protocol IS NULL OR p_idempotency_key IS NULL OR p_role IS NULL + OR p_expected_revision IS NULL OR p_model IS NULL OR p_reasoning_effort IS NULL + OR p_service_tier IS NULL OR p_instructions IS NULL + THEN + RAISE EXCEPTION 'exact RoleProfile update is incomplete' USING ERRCODE = '22004'; + END IF; + IF pg_catalog.octet_length(p_protocol) NOT BETWEEN 1 AND 64 + OR p_protocol COLLATE pg_catalog."C" !~ '^[a-z0-9][a-z0-9._/-]{0,63}$' + OR pg_catalog.octet_length(p_idempotency_key) NOT BETWEEN 1 AND 256 + OR decodex.has_credential_material(p_idempotency_key) + THEN + RAISE EXCEPTION 'exact RoleProfile command identity is invalid' USING ERRCODE = '22023'; + END IF; + + request_value := decodex.build_role_profile_update_request( + p_protocol, p_role, p_expected_revision, p_model, p_reasoning_effort, + p_service_tier, p_instructions, p_provenance + ); + INSERT INTO decodex.exact_command_receipts( + protocol_version, idempotency_key, request_envelope, request_digest, receipt_state + ) VALUES ( + p_protocol, p_idempotency_key, request_value, + public.digest(pg_catalog.convert_to(request_value::text, 'UTF8'), 'sha256'), 'executing' + ) ON CONFLICT (protocol_version, idempotency_key) DO NOTHING; + GET DIAGNOSTICS inserted_count = ROW_COUNT; + + IF inserted_count = 0 THEN + SELECT request_envelope, response_bytes INTO existing_request, existing_response + FROM decodex.exact_command_receipts + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key + FOR UPDATE; + IF existing_request <> request_value THEN + RAISE EXCEPTION 'exact idempotency conflict' USING ERRCODE = 'DX001'; + END IF; + IF existing_response IS NULL THEN + RAISE EXCEPTION 'incomplete exact receipt is not replayable' USING ERRCODE = 'DX002'; + END IF; + RETURN existing_response; + END IF; + + IF p_expected_revision < 1 THEN + RETURN decodex.complete_exact_role_profile_rejection( + p_protocol, p_idempotency_key, 'invalid_expected_revision' + ); + END IF; + IF NOT decodex.is_role_profile_configuration( + p_model, p_reasoning_effort, p_service_tier, p_instructions, p_provenance + ) THEN + RETURN decodex.complete_exact_role_profile_rejection( + p_protocol, p_idempotency_key, 'invalid_profile' + ); + END IF; + + SELECT current_revision INTO actual_revision + FROM decodex.role_profiles WHERE role = p_role FOR UPDATE; + IF NOT FOUND THEN + RETURN decodex.complete_exact_role_profile_rejection( + p_protocol, p_idempotency_key, 'not_bootstrapped' + ); + END IF; + IF actual_revision <> p_expected_revision THEN + RETURN decodex.complete_exact_role_profile_rejection( + p_protocol, p_idempotency_key, 'stale_revision' + ); + END IF; + + WITH inserted_revision AS ( + INSERT INTO decodex.role_profile_revisions( + role, revision, model, reasoning_effort, service_tier, instructions, provenance + ) VALUES ( + p_role, actual_revision + 1, p_model, p_reasoning_effort, + p_service_tier, p_instructions, p_provenance + ) RETURNING role, revision, model, reasoning_effort, service_tier, + instructions, provenance, created_at + ), advanced_profile AS ( + UPDATE decodex.role_profiles + SET current_revision = actual_revision + 1, updated_at = pg_catalog.clock_timestamp() + WHERE role = p_role AND current_revision = actual_revision + RETURNING role, current_revision, created_at, updated_at + ) + SELECT pg_catalog.jsonb_build_object( + 'role', revision.role, 'revision', revision.revision, + 'model', revision.model, 'reasoning_effort', revision.reasoning_effort, + 'service_tier', revision.service_tier, 'instructions', revision.instructions, + 'provenance', revision.provenance, 'created_at', revision.created_at, + 'updated_at', profile.updated_at + ) INTO profile_value + FROM inserted_revision AS revision JOIN advanced_profile AS profile USING (role); + + IF profile_value IS NULL THEN + RAISE EXCEPTION 'RoleProfile compare-and-swap lost after row lock' USING ERRCODE = '40001'; + END IF; + + activity_payload := profile_value || pg_catalog.jsonb_build_object('kind', 'role_profile'); + INSERT INTO decodex.activity( + aggregate_kind, aggregate_id, revision, event_kind, correlation_key, payload + ) VALUES ( + 'role_profile', p_role::text, (profile_value->>'revision')::bigint, + 'role_profile_updated', p_idempotency_key, activity_payload + ) RETURNING sequence, payload INTO activity_sequence, activity_payload; + outbox_payload := pg_catalog.jsonb_build_object( + 'activity_sequence', activity_sequence, 'event_kind', 'role_profile_updated', + 'aggregate_kind', 'role_profile', 'aggregate_id', p_role, + 'revision', (profile_value->>'revision')::bigint, 'payload', activity_payload + ); + INSERT INTO decodex.outbox( + effect_key, aggregate_kind, aggregate_id, aggregate_revision, payload + ) VALUES ( + 'activity/' || activity_sequence::text, 'role_profile', p_role::text, + (profile_value->>'revision')::bigint, outbox_payload + ) RETURNING id, payload INTO outbox_id, outbox_payload; + + effect_value := pg_catalog.jsonb_build_object( + 'profile', profile_value, 'activity_sequence', activity_sequence, + 'activity_payload', activity_payload, 'outbox_id', outbox_id, + 'outbox_payload', outbox_payload + ); + response_value := pg_catalog.convert_to(pg_catalog.jsonb_build_object( + 'classification', 'success', 'effect', effect_value + )::text, 'UTF8'); + UPDATE decodex.exact_command_receipts + SET receipt_state = 'completed_success', outcome_class = 'success', + effect_envelope = effect_value, response_bytes = response_value, + completed_at = pg_catalog.clock_timestamp() + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key; + + RETURN response_value; +END +$$; + +REVOKE ALL ON TABLE decodex.exact_command_receipts, + decodex.role_profiles, decodex.role_profile_revisions FROM PUBLIC; +REVOKE ALL ON TYPE decodex.exact_receipt_state, decodex.role_profile_role FROM PUBLIC; +REVOKE ALL ON ALL FUNCTIONS IN SCHEMA decodex FROM PUBLIC; +ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; +ALTER DEFAULT PRIVILEGES REVOKE USAGE ON TYPES FROM PUBLIC; diff --git a/crates/decodex-postgres/src/authority.rs b/crates/decodex-postgres/src/authority.rs index b7b74ae0..81337682 100644 --- a/crates/decodex-postgres/src/authority.rs +++ b/crates/decodex-postgres/src/authority.rs @@ -12,7 +12,9 @@ const POLICY_MIGRATION: &str = include_str!("../migrations/V6__project_policy_au const PROGRAM_OBJECTIVE_MIGRATION: &str = include_str!("../migrations/V7__program_objective_authority.sql"); const QUOTA_MIGRATION: &str = include_str!("../migrations/V8__quota_exclusions.sql"); -const FUNCTION_CONTRACTS: [FunctionContract; 58] = [ +const ROLE_PROFILE_MIGRATION: &str = include_str!("../migrations/V9__exact_role_profiles.sql"); +const ALLOWED_EXECUTION_DEPENDENCIES: [&str; 1] = ["public.digest(pg_catalog.bytea,pg_catalog.text)"]; +const FUNCTION_CONTRACTS: [FunctionContract; 72] = [ FunctionContract { name: "is_canonical_media_type", lookup_signature: "decodex.is_canonical_media_type(pg_catalog.text)", @@ -513,8 +515,102 @@ const FUNCTION_CONTRACTS: [FunctionContract; 58] = [ "achieve_objective(\n\tp_evidence_id decodex.canonical_uuid_v4_text,\n\tp_objective_id decodex.canonical_uuid_v4_text,\n\tp_project_id decodex.canonical_uuid_v4_text,\n\tp_objective_revision bigint,\n\tp_acceptance_result text,\n\tp_accepted_by decodex.canonical_uuid_v4_text,\n\tp_accepted_at bigint,\n\tp_acceptance_provenance text,\n\tp_validation_result text,\n\tp_validated_by decodex.canonical_uuid_v4_text,\n\tp_validated_at bigint,\n\tp_validation_provenance text,\n\tp_correlation_id decodex.canonical_uuid_v4_text\n)", "p_evidence_id decodex.canonical_uuid_v4_text, p_objective_id decodex.canonical_uuid_v4_text, p_project_id decodex.canonical_uuid_v4_text, p_objective_revision bigint, p_acceptance_result text, p_accepted_by decodex.canonical_uuid_v4_text, p_accepted_at bigint, p_acceptance_provenance text, p_validation_result text, p_validated_by decodex.canonical_uuid_v4_text, p_validated_at bigint, p_validation_provenance text, p_correlation_id decodex.canonical_uuid_v4_text", ), + trigger_contract( + "enforce_exact_receipt_completion", + "decodex.enforce_exact_receipt_completion()", + "enforce_exact_receipt_completion()", + ), + trigger_contract( + "forbid_exact_receipt_rewrite", + "decodex.forbid_exact_receipt_rewrite()", + "forbid_exact_receipt_rewrite()", + ), + trigger_contract( + "forbid_exact_receipt_truncate", + "decodex.forbid_exact_receipt_truncate()", + "forbid_exact_receipt_truncate()", + ), + trigger_contract( + "enforce_complete_role_profile_set", + "decodex.enforce_complete_role_profile_set()", + "enforce_complete_role_profile_set()", + ), + trigger_contract( + "forbid_role_profile_identity_rewrite", + "decodex.forbid_role_profile_identity_rewrite()", + "forbid_role_profile_identity_rewrite()", + ), + trigger_contract( + "forbid_role_profile_revision_mutation", + "decodex.forbid_role_profile_revision_mutation()", + "forbid_role_profile_revision_mutation()", + ), + trigger_contract( + "forbid_role_profile_truncate", + "decodex.forbid_role_profile_truncate()", + "forbid_role_profile_truncate()", + ), + trigger_contract( + "enforce_role_profile_event_namespace", + "decodex.enforce_role_profile_event_namespace()", + "enforce_role_profile_event_namespace()", + ), + exact_function_contract( + "is_role_profile_configuration", + "decodex.is_role_profile_configuration(pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "is_role_profile_configuration(\n\tp_model text, p_reasoning_effort text, p_service_tier text,\n\tp_instructions text, p_provenance text\n)", + "p_model text, p_reasoning_effort text, p_service_tier text, p_instructions text, p_provenance text", + "boolean", + "sql", + "i", + ), + exact_function_contract( + "build_role_profile_bootstrap_request", + "decodex.build_role_profile_bootstrap_request(pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "build_role_profile_bootstrap_request(\n\tp_protocol text,\n\tp_advisor_model text, p_advisor_reasoning_effort text,\n\tp_advisor_service_tier text, p_advisor_instructions text, p_advisor_provenance text,\n\tp_lead_model text, p_lead_reasoning_effort text,\n\tp_lead_service_tier text, p_lead_instructions text, p_lead_provenance text,\n\tp_task_model text, p_task_reasoning_effort text,\n\tp_task_service_tier text, p_task_instructions text, p_task_provenance text,\n\tp_reviewer_model text, p_reviewer_reasoning_effort text,\n\tp_reviewer_service_tier text, p_reviewer_instructions text, p_reviewer_provenance text\n)", + "p_protocol text, p_advisor_model text, p_advisor_reasoning_effort text, p_advisor_service_tier text, p_advisor_instructions text, p_advisor_provenance text, p_lead_model text, p_lead_reasoning_effort text, p_lead_service_tier text, p_lead_instructions text, p_lead_provenance text, p_task_model text, p_task_reasoning_effort text, p_task_service_tier text, p_task_instructions text, p_task_provenance text, p_reviewer_model text, p_reviewer_reasoning_effort text, p_reviewer_service_tier text, p_reviewer_instructions text, p_reviewer_provenance text", + "jsonb", + "plpgsql", + "i", + ), + exact_function_contract( + "build_role_profile_update_request", + "decodex.build_role_profile_update_request(pg_catalog.text,decodex.role_profile_role,pg_catalog.int8,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "build_role_profile_update_request(\n\tp_protocol text, p_role decodex.role_profile_role, p_expected_revision bigint,\n\tp_model text, p_reasoning_effort text, p_service_tier text,\n\tp_instructions text, p_provenance text\n)", + "p_protocol text, p_role decodex.role_profile_role, p_expected_revision bigint, p_model text, p_reasoning_effort text, p_service_tier text, p_instructions text, p_provenance text", + "jsonb", + "sql", + "i", + ), + exact_function_contract( + "complete_exact_role_profile_rejection", + "decodex.complete_exact_role_profile_rejection(pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "complete_exact_role_profile_rejection(\n\tp_protocol text, p_idempotency_key text, p_code text\n)", + "p_protocol text, p_idempotency_key text, p_code text", + "bytea", + "plpgsql", + "v", + ), + exact_function_contract( + "bootstrap_role_profiles_exact", + "decodex.bootstrap_role_profiles_exact(pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "bootstrap_role_profiles_exact(\n\tp_protocol text, p_idempotency_key text,\n\tp_advisor_model text, p_advisor_reasoning_effort text,\n\tp_advisor_service_tier text, p_advisor_instructions text, p_advisor_provenance text,\n\tp_lead_model text, p_lead_reasoning_effort text,\n\tp_lead_service_tier text, p_lead_instructions text, p_lead_provenance text,\n\tp_task_model text, p_task_reasoning_effort text,\n\tp_task_service_tier text, p_task_instructions text, p_task_provenance text,\n\tp_reviewer_model text, p_reviewer_reasoning_effort text,\n\tp_reviewer_service_tier text, p_reviewer_instructions text, p_reviewer_provenance text\n)", + "p_protocol text, p_idempotency_key text, p_advisor_model text, p_advisor_reasoning_effort text, p_advisor_service_tier text, p_advisor_instructions text, p_advisor_provenance text, p_lead_model text, p_lead_reasoning_effort text, p_lead_service_tier text, p_lead_instructions text, p_lead_provenance text, p_task_model text, p_task_reasoning_effort text, p_task_service_tier text, p_task_instructions text, p_task_provenance text, p_reviewer_model text, p_reviewer_reasoning_effort text, p_reviewer_service_tier text, p_reviewer_instructions text, p_reviewer_provenance text", + "bytea", + "plpgsql", + "v", + ), + exact_function_contract( + "update_role_profile_exact", + "decodex.update_role_profile_exact(pg_catalog.text,pg_catalog.text,decodex.role_profile_role,pg_catalog.int8,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "update_role_profile_exact(\n\tp_protocol text, p_idempotency_key text,\n\tp_role decodex.role_profile_role, p_expected_revision bigint,\n\tp_model text, p_reasoning_effort text, p_service_tier text,\n\tp_instructions text, p_provenance text\n)", + "p_protocol text, p_idempotency_key text, p_role decodex.role_profile_role, p_expected_revision bigint, p_model text, p_reasoning_effort text, p_service_tier text, p_instructions text, p_provenance text", + "bytea", + "plpgsql", + "v", + ), ]; -const RUNTIME_EXECUTE_FUNCTIONS: [&str; 26] = [ +const RUNTIME_EXECUTE_FUNCTIONS: [&str; 28] = [ "decodex.is_canonical_media_type(pg_catalog.text)", "decodex.is_history_metadata_projection(pg_catalog.jsonb)", "decodex.normalize_unicode_whitespace(pg_catalog.text)", @@ -541,8 +637,10 @@ const RUNTIME_EXECUTE_FUNCTIONS: [&str; 26] = [ "decodex.create_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.text,pg_catalog._text,pg_catalog._text,pg_catalog.int8,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.text)", "decodex.transition_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.int8,decodex.objective_state,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.text)", "decodex.achieve_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text)", + "decodex.bootstrap_role_profiles_exact(pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "decodex.update_role_profile_exact(pg_catalog.text,pg_catalog.text,decodex.role_profile_role,pg_catalog.int8,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", ]; -const SAFETY_FUNCTIONS: [&str; 26] = [ +const SAFETY_FUNCTIONS: [&str; 34] = [ "enforce_lease_operation_time", "enforce_outbox_operation_time", "enforce_quota_observation_monotonicity", @@ -569,8 +667,16 @@ const SAFETY_FUNCTIONS: [&str; 26] = [ "enforce_objective_state", "forbid_objective_evidence_mutation", "enforce_objective_completion_coherence", + "enforce_exact_receipt_completion", + "forbid_exact_receipt_rewrite", + "forbid_exact_receipt_truncate", + "enforce_complete_role_profile_set", + "forbid_role_profile_identity_rewrite", + "forbid_role_profile_revision_mutation", + "forbid_role_profile_truncate", + "enforce_role_profile_event_namespace", ]; -const SAFETY_TRIGGER_COUNT: usize = 42; +const SAFETY_TRIGGER_COUNT: usize = 52; // PostgreSQL 18 catalogs with an owner and a containing namespace, plus the namespace // itself. Namespace-scoped catalogs without an independent owner (constraints, triggers, // text-search parsers/templates, and dependent rows) inherit authority from one of these. @@ -749,7 +855,10 @@ WITH set_roles AS ( ('policy_revisions', true, false, false, false), ('programs', true, false, false, false), ('objectives', true, false, false, false), - ('objective_completion_evidence', true, false, false, false) + ('objective_completion_evidence', true, false, false, false), + ('exact_command_receipts', false, false, false, false), + ('role_profiles', false, false, false, false), + ('role_profile_revisions', false, false, false, false) ), tables AS ( SELECT class.oid, class.relname, expected.* FROM pg_catalog.pg_class AS class @@ -758,7 +867,7 @@ WITH set_roles AS ( WHERE namespace.nspname = 'decodex' AND class.relkind IN ('r', 'p') ) SELECT - (SELECT count(*) FROM tables WHERE table_name IS NOT NULL) = 28 + (SELECT count(*) FROM tables WHERE table_name IS NOT NULL) = 31 AND COALESCE(( SELECT pg_catalog.bool_and( pg_catalog.has_table_privilege(session_user, oid, 'SELECT') = can_select @@ -955,7 +1064,17 @@ WITH expected(table_name, trigger_name, function_name, trigger_type) AS (VALUES ('objective_completion_evidence', 'objective_evidence_immutable', 'forbid_objective_evidence_mutation', 27), ('objective_completion_evidence', 'objective_evidence_truncate_forbidden', 'forbid_objective_evidence_mutation', 34), ('objectives', 'objectives_completion_coherence', 'enforce_objective_completion_coherence', 21), - ('objective_completion_evidence', 'objective_evidence_completion_coherence', 'enforce_objective_completion_coherence', 5) + ('objective_completion_evidence', 'objective_evidence_completion_coherence', 'enforce_objective_completion_coherence', 5), + ('exact_command_receipts', 'exact_receipts_complete_at_commit', 'enforce_exact_receipt_completion', 21), + ('exact_command_receipts', 'exact_receipts_immutable', 'forbid_exact_receipt_rewrite', 27), + ('exact_command_receipts', 'exact_receipts_untruncatable', 'forbid_exact_receipt_truncate', 34), + ('role_profiles', 'role_profiles_exact_global_set', 'enforce_complete_role_profile_set', 29), + ('role_profiles', 'role_profiles_identity_immutable', 'forbid_role_profile_identity_rewrite', 27), + ('role_profile_revisions', 'role_profile_revisions_immutable', 'forbid_role_profile_revision_mutation', 27), + ('role_profiles', 'role_profiles_untruncatable', 'forbid_role_profile_truncate', 34), + ('role_profile_revisions', 'role_profile_revisions_untruncatable', 'forbid_role_profile_truncate', 34), + ('activity', 'activity_role_profile_namespace', 'enforce_role_profile_event_namespace', 23), + ('outbox', 'outbox_role_profile_namespace', 'enforce_role_profile_event_namespace', 23) ) SELECT expected.function_name, @@ -964,15 +1083,15 @@ SELECT AND trigger.tgtype = expected.trigger_type AND trigger.tgparentid = 0 AND (trigger.tgconstraint <> 0) = ( - expected.trigger_name IN ('objectives_completion_coherence', 'objective_evidence_completion_coherence') + expected.trigger_name IN ('objectives_completion_coherence', 'objective_evidence_completion_coherence', 'exact_receipts_complete_at_commit', 'role_profiles_exact_global_set') ) AND trigger.tgconstrrelid = 0 AND trigger.tgconstrindid = 0 AND trigger.tgdeferrable = ( - expected.trigger_name IN ('objectives_completion_coherence', 'objective_evidence_completion_coherence') + expected.trigger_name IN ('objectives_completion_coherence', 'objective_evidence_completion_coherence', 'exact_receipts_complete_at_commit', 'role_profiles_exact_global_set') ) AND trigger.tginitdeferred = ( - expected.trigger_name IN ('objectives_completion_coherence', 'objective_evidence_completion_coherence') + expected.trigger_name IN ('objectives_completion_coherence', 'objective_evidence_completion_coherence', 'exact_receipts_complete_at_commit', 'role_profiles_exact_global_set') ) AND trigger.tgnargs = 0 AND trigger.tgattr = ''::pg_catalog.int2vector @@ -1114,7 +1233,17 @@ WITH catalog_context AS MATERIALIZED ( ('objective_completion_evidence', 'objective_evidence_immutable', 'decodex.forbid_objective_evidence_mutation()'), ('objective_completion_evidence', 'objective_evidence_truncate_forbidden', 'decodex.forbid_objective_evidence_mutation()'), ('objectives', 'objectives_completion_coherence', 'decodex.enforce_objective_completion_coherence()'), - ('objective_completion_evidence', 'objective_evidence_completion_coherence', 'decodex.enforce_objective_completion_coherence()') + ('objective_completion_evidence', 'objective_evidence_completion_coherence', 'decodex.enforce_objective_completion_coherence()'), + ('exact_command_receipts', 'exact_receipts_complete_at_commit', 'decodex.enforce_exact_receipt_completion()'), + ('exact_command_receipts', 'exact_receipts_immutable', 'decodex.forbid_exact_receipt_rewrite()'), + ('exact_command_receipts', 'exact_receipts_untruncatable', 'decodex.forbid_exact_receipt_truncate()'), + ('role_profiles', 'role_profiles_exact_global_set', 'decodex.enforce_complete_role_profile_set()'), + ('role_profiles', 'role_profiles_identity_immutable', 'decodex.forbid_role_profile_identity_rewrite()'), + ('role_profile_revisions', 'role_profile_revisions_immutable', 'decodex.forbid_role_profile_revision_mutation()'), + ('role_profiles', 'role_profiles_untruncatable', 'decodex.forbid_role_profile_truncate()'), + ('role_profile_revisions', 'role_profile_revisions_untruncatable', 'decodex.forbid_role_profile_truncate()'), + ('activity', 'activity_role_profile_namespace', 'decodex.enforce_role_profile_event_namespace()'), + ('outbox', 'outbox_role_profile_namespace', 'decodex.enforce_role_profile_event_namespace()') ), actual_triggers AS ( SELECT class.relname AS table_name, @@ -1584,8 +1713,8 @@ SELECT pg_catalog.jsonb_agg( FROM contract_rows "#; const SCHEMA_CONTRACT_SHA256: [u8; 32] = [ - 0xae, 0x1f, 0xb7, 0x45, 0x9f, 0xf8, 0x07, 0x19, 0x10, 0x16, 0x9f, 0xa1, 0x48, 0x31, 0x11, 0xdc, - 0xf0, 0xc8, 0xc8, 0xe7, 0xc8, 0x15, 0x59, 0xe0, 0x3d, 0xf7, 0x69, 0x2d, 0x1a, 0xca, 0x9c, 0x7e, + 0x94, 0xef, 0x27, 0x45, 0x48, 0x2a, 0x0b, 0x52, 0x9f, 0x2f, 0x37, 0x5a, 0x9f, 0xbc, 0x1b, 0x76, + 0x6c, 0x59, 0xc8, 0x20, 0x6b, 0xeb, 0x32, 0xd5, 0x82, 0xca, 0x9f, 0xa4, 0x1c, 0xb1, 0x43, 0x01, ]; // The shipped authority permits no role settings. Record only cardinality so any setting // fails closed without copying an arbitrary custom-GUC value into the manifest or digest input. @@ -2064,8 +2193,8 @@ SELECT pg_catalog.jsonb_agg( FROM contract_rows "#; const CONFIGURED_AUTHORITY_SHA256: [u8; 32] = [ - 0x9d, 0x90, 0xe4, 0xc8, 0x30, 0x19, 0xe5, 0x53, 0xa0, 0xe4, 0xa9, 0x38, 0x39, 0x1a, 0x84, 0xf2, - 0x95, 0x03, 0x9f, 0x58, 0x37, 0x9e, 0x81, 0x24, 0xe0, 0x58, 0x51, 0xbc, 0x3c, 0x2c, 0x8a, 0x3a, + 0x1a, 0x15, 0x62, 0xe7, 0x80, 0x8f, 0xb9, 0xdf, 0x1e, 0x0f, 0xe1, 0xbf, 0x8d, 0x5a, 0xcf, 0xa0, + 0xf3, 0xbd, 0x1c, 0x86, 0x75, 0xef, 0x76, 0x23, 0xbe, 0xb6, 0x20, 0x92, 0xc2, 0xf0, 0xb1, 0x7c, ]; const EXTENSION_AUTHORITY_SQL: &str = r#" WITH set_roles AS ( @@ -2212,6 +2341,18 @@ pub(crate) const fn configured_authority_sql_fixture() -> &'static str { CONFIGURED_AUTHORITY_SQL } +#[cfg(feature = "test-support")] +pub(crate) fn execution_path_contract_fixture() -> (&'static str, Vec<&'static str>) { + ( + EXECUTION_PATH_CONTRACT_SQL, + FUNCTION_CONTRACTS + .iter() + .map(|contract| contract.lookup_signature) + .chain(ALLOWED_EXECUTION_DEPENDENCIES) + .collect(), + ) +} + pub(crate) async fn verify_runtime( client: &Client, migration_role: &str, @@ -2289,6 +2430,29 @@ const fn mutator_contract( } } +const fn exact_function_contract( + name: &'static str, + lookup_signature: &'static str, + migration_signature: &'static str, + arguments: &'static str, + result: &'static str, + language: &'static str, + volatility: &'static str, +) -> FunctionContract { + FunctionContract { + name, + lookup_signature, + migration_signature, + arguments, + result, + language, + volatility, + strict: false, + returns_set: false, + rows: 0.0, + } +} + fn canonical_safety_function_source(function_name: &str) -> Option<&'static str> { if !SAFETY_FUNCTIONS.contains(&function_name) { return None; @@ -2308,6 +2472,7 @@ fn canonical_function_source(contract: &FunctionContract) -> Option<&'static str POLICY_MIGRATION, PROGRAM_OBJECTIVE_MIGRATION, QUOTA_MIGRATION, + ROLE_PROFILE_MIGRATION, ] .into_iter() .find(|migration| migration.contains(&declaration))?; @@ -2385,7 +2550,11 @@ async fn verify_configured_authority( async fn verify_execution_path_contract(client: &Client) -> Result<(), StoreError> { let allowed_functions = - FUNCTION_CONTRACTS.iter().map(|contract| contract.lookup_signature).collect::>(); + FUNCTION_CONTRACTS + .iter() + .map(|contract| contract.lookup_signature) + .chain(ALLOWED_EXECUTION_DEPENDENCIES) + .collect::>(); let row = client.query_one(EXECUTION_PATH_CONTRACT_SQL, &[&allowed_functions]).await?; let exact_triggers: bool = row.get(0); let no_rules: bool = row.get(1); @@ -2466,6 +2635,8 @@ async fn verify_function_contract(client: &Client) -> Result<(), StoreError> { | "create_objective" | "transition_objective" | "achieve_objective" + | "bootstrap_role_profiles_exact" + | "update_role_profile_exact" ); let expected_executable = RUNTIME_EXECUTE_FUNCTIONS.contains(&contract.lookup_signature); let expected_settings = vec!["search_path=pg_catalog, decodex".to_owned()]; @@ -2621,7 +2792,7 @@ mod tests { FOUNDATION_MIGRATION, FUNCTION_CONTRACTS, IDENTITY_CAST_AUTHORITY_SQL, OWNED_OBJECT_CATALOGS, POLICY_MIGRATION, PROGRAM_OBJECTIVE_MIGRATION, PROJECT_AGENT_MIGRATION, QUOTA_MIGRATION, ROLE_AUTHORITY_SQL, SAFETY_FUNCTIONS, - SCHEMA_CONTRACT_SHA256, SCHEMA_CONTRACT_SQL, + ROLE_PROFILE_MIGRATION, SCHEMA_CONTRACT_SHA256, SCHEMA_CONTRACT_SQL, }; #[test] @@ -2754,6 +2925,7 @@ mod tests { POLICY_MIGRATION, PROGRAM_OBJECTIVE_MIGRATION, QUOTA_MIGRATION, + ROLE_PROFILE_MIGRATION, ] .into_iter() .map(|migration| migration.matches("CREATE FUNCTION decodex.").count()) @@ -2773,6 +2945,7 @@ mod tests { POLICY_MIGRATION, PROGRAM_OBJECTIVE_MIGRATION, QUOTA_MIGRATION, + ROLE_PROFILE_MIGRATION, ] .into_iter() .map(|migration| migration diff --git a/crates/decodex-postgres/src/error.rs b/crates/decodex-postgres/src/error.rs index 70409d49..7db7323d 100644 --- a/crates/decodex-postgres/src/error.rs +++ b/crates/decodex-postgres/src/error.rs @@ -122,7 +122,9 @@ impl From for StoreError { impl From for StoreError { fn from(error: tokio_postgres::Error) -> Self { - if error.as_db_error().is_some_and(|database| { + if error.code().is_some_and(|code| code.code() == "DX001") { + Self::IdempotencyConflict + } else if error.as_db_error().is_some_and(|database| { database.code() == &tokio_postgres::error::SqlState::CHECK_VIOLATION && database.constraint().is_some_and(|name| name.contains("no_credentials")) }) { diff --git a/crates/decodex-postgres/src/lib.rs b/crates/decodex-postgres/src/lib.rs index 4ab65c52..e50e1796 100644 --- a/crates/decodex-postgres/src/lib.rs +++ b/crates/decodex-postgres/src/lib.rs @@ -3,8 +3,9 @@ //! This crate owns the XY-1267 persistence foundation and XY-1271 Conversation history: //! immutable migrations, idempotent optimistic transactions, expiring leases, transactional //! activity/outbox evidence, inert account/quota-window metadata, normalized history, blob -//! references, Context Packs, and inert transition proposals. It does not select accounts, -//! route work, store credentials, dispatch transitions, or expose protocol/client behavior. +//! references, Context Packs, inert transition proposals, exact in-transaction receipts, and +//! immutable global RoleProfiles. It does not select accounts, route work, store credentials, +//! dispatch transitions, or expose protocol/client behavior. mod accounts; mod authority; @@ -17,6 +18,7 @@ mod policies; mod programs; mod project_agents; mod quota; +mod role_profiles; #[cfg(unix)] mod socket; mod types; @@ -29,6 +31,10 @@ pub use self::{ }, error::{BootstrapFailure, StoreError}, programs::{ObjectiveRecord, ProgramRecord, UpdateProgramContext}, + role_profiles::{ + BootstrapRoleProfiles, RoleProfileCommandOutcome, RoleProfileConfiguration, + RoleProfileRejection, RoleProfileRevision, RoleProfileRole, + }, types::{ AccountMetadata, AccountMutation, ActivityRecord, CommandIdentity, CreateProject, HypotheticalFallbackFact, LeaseClaim, OutboxClaim, OutboxReconciliation, OutboxState, @@ -107,6 +113,13 @@ impl PostgresStore { authority::configured_authority_sql_fixture() } + /// Return the closed execution-path query and allowed function identities for fixtures. + #[cfg(feature = "test-support")] + #[doc(hidden)] + pub fn execution_path_contract_fixture() -> (&'static str, Vec<&'static str>) { + authority::execution_path_contract_fixture() + } + /// Apply the production connection-startup invariant to an isolated raw fixture. #[cfg(feature = "test-support")] #[doc(hidden)] @@ -250,6 +263,35 @@ impl PostgresStore { result } + /// Apply the immutable migration ledger only through V8 for the V9 upgrade proof. + #[cfg(all(unix, feature = "test-support"))] + #[doc(hidden)] + pub async fn migrate_fixture_through_v8( + mut config: Config, + expected_peer_uid: u32, + ) -> Result<(), StoreError> { + validate_connection(&config)?; + + let connector = verified_socket_connect(&config, expected_peer_uid)?; + + pin_session_search_path(&mut config); + + let manager = Manager::from_connect( + config, + connector.clone(), + ManagerConfig { recycling_method: RecyclingMethod::Fast }, + ); + let pool = Pool::builder(manager).max_size(1).build()?; + let mut client = checkout(&pool, &connector).await?; + let result = migrations::run_through_v8(&mut client).await; + + drop(client); + + pool.close(); + + result + } + #[cfg(not(unix))] pub async fn migrate(_config: Config, _expected_peer_uid: u32) -> Result<(), StoreError> { Err(StoreError::Incompatible("PostgreSQL Unix sockets require a Unix host".into())) diff --git a/crates/decodex-postgres/src/migrations.rs b/crates/decodex-postgres/src/migrations.rs index ccbb0ccf..bdf2e106 100644 --- a/crates/decodex-postgres/src/migrations.rs +++ b/crates/decodex-postgres/src/migrations.rs @@ -8,7 +8,7 @@ use deadpool_postgres::Client; use crate::{REQUIRED_POSTGRES_MAJOR, StoreError}; use embedded::migrations; -const EXPECTED_LATEST_MIGRATION_VERSION: i32 = 8; +const EXPECTED_LATEST_MIGRATION_VERSION: i32 = 9; pub(crate) async fn run(client: &mut Client) -> Result<(), StoreError> { migrations::runner().run_async(&mut ***client).await?; @@ -23,6 +23,13 @@ pub(crate) async fn run_through_v7(client: &mut Client) -> Result<(), StoreError Ok(()) } +#[cfg(feature = "test-support")] +pub(crate) async fn run_through_v8(client: &mut Client) -> Result<(), StoreError> { + migrations::runner().set_target(Target::Version(8)).run_async(&mut ***client).await?; + + Ok(()) +} + pub(crate) async fn verify(client: &Client) -> Result<(), StoreError> { let row = client .query_one( @@ -66,7 +73,7 @@ pub(crate) async fn verify(client: &Client) -> Result<(), StoreError> { != Some(EXPECTED_LATEST_MIGRATION_VERSION) { return Err(StoreError::Incompatible( - "embedded migration inventory does not end at the canonical V8 ledger".into(), + "embedded migration inventory does not end at the canonical V9 ledger".into(), )); } if actual.len() != expected.len() { diff --git a/crates/decodex-postgres/src/role_profiles.rs b/crates/decodex-postgres/src/role_profiles.rs new file mode 100644 index 00000000..762f71af --- /dev/null +++ b/crates/decodex-postgres/src/role_profiles.rs @@ -0,0 +1,435 @@ +use serde_json::Value; +use tokio_postgres::{Row, error::SqlState, types::ToSql}; + +use crate::{PostgresStore, StoreError}; + +const EXACT_COMMAND_PROTOCOL: &str = "decodex/exact-command/1"; +const MAX_EXACT_ATTEMPTS: usize = 4; + +/// One of the four immutable global RoleProfile identities. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum RoleProfileRole { + /// Global consultation and cross-project recommendation profile. + Advisor, + /// Project ownership and serial decision profile. + Lead, + /// Execution-scoped implementation profile. + Task, + /// Execution-scoped read-only review profile. + Reviewer, +} +impl RoleProfileRole { + const fn as_sql(self) -> &'static str { + match self { + Self::Advisor => "advisor", + Self::Lead => "lead", + Self::Task => "task", + Self::Reviewer => "reviewer", + } + } + + fn from_sql(value: &str) -> Result { + match value { + "advisor" => Ok(Self::Advisor), + "lead" => Ok(Self::Lead), + "task" => Ok(Self::Task), + "reviewer" => Ok(Self::Reviewer), + _ => Err(StoreError::Incompatible("stored RoleProfile role is invalid".into())), + } + } +} + +/// Exact user-selected configuration for one RoleProfile revision. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleProfileConfiguration { + /// Exact model identifier. + pub model: String, + /// Exact reasoning-effort identifier. + pub reasoning_effort: String, + /// Exact service-tier identifier. + pub service_tier: String, + /// Exact instruction bytes represented as UTF-8 text. + pub instructions: String, + /// Optional exact user-owned provenance. + pub provenance: Option, +} + +/// Four fixed, role-implied bootstrap configuration groups. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BootstrapRoleProfiles { + /// Advisor revision-one configuration. + pub advisor: RoleProfileConfiguration, + /// Lead revision-one configuration. + pub lead: RoleProfileConfiguration, + /// Task revision-one configuration. + pub task: RoleProfileConfiguration, + /// Reviewer revision-one configuration. + pub reviewer: RoleProfileConfiguration, +} + +/// One immutable RoleProfile revision returned by PostgreSQL. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RoleProfileRevision { + /// Fixed global identity. + pub role: RoleProfileRole, + /// Positive immutable revision. + pub revision: i64, + /// Exact stored user configuration. + pub configuration: RoleProfileConfiguration, +} + +/// Stable domain rejection returned and replayed by an exact command. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RoleProfileRejection { + /// Bootstrap was attempted after the global set already existed. + AlreadyBootstrapped, + /// Update was attempted before the global set existed. + NotBootstrapped, + /// The supplied expected revision was not current. + StaleRevision, + /// A nonpositive expected revision was supplied. + InvalidExpectedRevision, + /// One or more exact configuration values violated the stable domain contract. + InvalidProfile, +} + +/// Exact command outcome parsed from PostgreSQL-owned response bytes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RoleProfileCommandOutcome { + /// The command committed the returned immutable revision evidence. + Success(T), + /// The command committed a stable rejection that will replay unchanged. + Rejected(RoleProfileRejection), +} + +impl PostgresStore { + /// Bootstrap exactly advisor, lead, task, and reviewer in one exact transaction. + pub async fn bootstrap_role_profiles( + &self, + idempotency_key: &str, + profiles: &BootstrapRoleProfiles, + ) -> Result>, StoreError> { + validate_exact_key(idempotency_key)?; + + let response = self + .execute_exact_with_retry( + "SELECT decodex.bootstrap_role_profiles_exact(\ + $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)", + &[ + &EXACT_COMMAND_PROTOCOL, + &idempotency_key, + &profiles.advisor.model, + &profiles.advisor.reasoning_effort, + &profiles.advisor.service_tier, + &profiles.advisor.instructions, + &profiles.advisor.provenance, + &profiles.lead.model, + &profiles.lead.reasoning_effort, + &profiles.lead.service_tier, + &profiles.lead.instructions, + &profiles.lead.provenance, + &profiles.task.model, + &profiles.task.reasoning_effort, + &profiles.task.service_tier, + &profiles.task.instructions, + &profiles.task.provenance, + &profiles.reviewer.model, + &profiles.reviewer.reasoning_effort, + &profiles.reviewer.service_tier, + &profiles.reviewer.instructions, + &profiles.reviewer.provenance, + ], + ) + .await?; + + parse_bootstrap_response(&response) + } + + /// Append one immutable revision and atomically advance its role's current pointer. + pub async fn update_role_profile( + &self, + idempotency_key: &str, + role: RoleProfileRole, + expected_revision: i64, + configuration: &RoleProfileConfiguration, + ) -> Result, StoreError> { + validate_exact_key(idempotency_key)?; + let role = role.as_sql(); + let response = self + .execute_exact_with_retry( + "SELECT decodex.update_role_profile_exact(\ + $1,$2,$3::pg_catalog.text::decodex.role_profile_role,$4,$5,$6,$7,$8,$9)", + &[ + &EXACT_COMMAND_PROTOCOL, + &idempotency_key, + &role, + &expected_revision, + &configuration.model, + &configuration.reasoning_effort, + &configuration.service_tier, + &configuration.instructions, + &configuration.provenance, + ], + ) + .await?; + + parse_update_response(&response) + } + + async fn execute_exact_with_retry( + &self, + statement: &str, + parameters: &[&(dyn ToSql + Sync)], + ) -> Result, StoreError> { + let mut last_retryable = None; + + for _ in 0..MAX_EXACT_ATTEMPTS { + let mut client = match self.pool().get().await { + Ok(client) => client, + Err(error) => return Err(StoreError::Pool(error)), + }; + let transaction = match client.transaction().await { + Ok(transaction) => transaction, + Err(error) if is_retryable_exact_database_error(&error) => { + last_retryable = Some(error); + continue; + }, + Err(error) => return Err(StoreError::from(error)), + }; + let row = match transaction.query_one(statement, parameters).await { + Ok(row) => row, + Err(error) if is_retryable_exact_database_error(&error) => { + last_retryable = Some(error); + continue; + }, + Err(error) => return Err(StoreError::from(error)), + }; + let response = response_bytes(row)?; + + match transaction.commit().await { + Ok(()) => return Ok(response), + Err(error) if is_retryable_exact_database_error(&error) => { + last_retryable = Some(error); + }, + Err(error) => return Err(StoreError::from(error)), + } + } + + Err(StoreError::Database( + last_retryable.expect( + "an exhausted exact retry loop retains its classified infrastructure failure", + ), + )) + } +} + +fn response_bytes(row: Row) -> Result, StoreError> { + let response: Option> = row.get(0); + + response.ok_or_else(|| StoreError::Incompatible("exact RoleProfile response is null".into())) +} + +fn validate_exact_key(key: &str) -> Result<(), StoreError> { + if key.is_empty() || key.len() > 256 { + return Err(StoreError::InvalidInput("idempotency key must contain 1..=256 bytes")); + } + + crate::ensure_credential_negative_text(key) +} + +fn is_retryable_exact_database_error(error: &tokio_postgres::Error) -> bool { + let Some(code) = error.code() else { + return false; + }; + + code == &SqlState::T_R_SERIALIZATION_FAILURE + || code == &SqlState::T_R_DEADLOCK_DETECTED + || code == &SqlState::ADMIN_SHUTDOWN + || code == &SqlState::CRASH_SHUTDOWN + || code == &SqlState::CANNOT_CONNECT_NOW + || code.code().starts_with("08") +} + +fn parse_bootstrap_response( + response: &[u8], +) -> Result>, StoreError> { + let document = response_document(response)?; + + if document.get("classification").and_then(Value::as_str) == Some("stable_domain_rejection") { + return rejection_from_document(&document).map(RoleProfileCommandOutcome::Rejected); + } + if document.get("classification").and_then(Value::as_str) != Some("success") { + return Err(StoreError::Incompatible( + "exact RoleProfile response classification is invalid".into(), + )); + } + + let effects = + document.pointer("/effect/profiles").and_then(Value::as_array).ok_or_else(|| { + StoreError::Incompatible("bootstrap response lacks profile effects".into()) + })?; + let profiles = effects + .iter() + .map(|effect| profile_from_value(required_value(effect, "profile")?)) + .collect::, _>>()?; + let roles = profiles.iter().map(|profile| profile.role).collect::>(); + + if roles + != [ + RoleProfileRole::Advisor, + RoleProfileRole::Lead, + RoleProfileRole::Task, + RoleProfileRole::Reviewer, + ] { + return Err(StoreError::Incompatible( + "bootstrap response does not contain the exact ordered RoleProfile set".into(), + )); + } + + Ok(RoleProfileCommandOutcome::Success(profiles)) +} + +fn parse_update_response( + response: &[u8], +) -> Result, StoreError> { + let document = response_document(response)?; + + if document.get("classification").and_then(Value::as_str) == Some("stable_domain_rejection") { + return rejection_from_document(&document).map(RoleProfileCommandOutcome::Rejected); + } + if document.get("classification").and_then(Value::as_str) != Some("success") { + return Err(StoreError::Incompatible( + "exact RoleProfile response classification is invalid".into(), + )); + } + + profile_from_value(required_pointer(&document, "/effect/profile")?) + .map(RoleProfileCommandOutcome::Success) +} + +fn response_document(response: &[u8]) -> Result { + serde_json::from_slice(response).map_err(|_| { + StoreError::Incompatible("exact RoleProfile response bytes are invalid".into()) + }) +} + +fn rejection_from_document(document: &Value) -> Result { + match document.get("code").and_then(Value::as_str) { + Some("already_bootstrapped") => Ok(RoleProfileRejection::AlreadyBootstrapped), + Some("not_bootstrapped") => Ok(RoleProfileRejection::NotBootstrapped), + Some("stale_revision") => Ok(RoleProfileRejection::StaleRevision), + Some("invalid_expected_revision") => Ok(RoleProfileRejection::InvalidExpectedRevision), + Some("invalid_profile") => Ok(RoleProfileRejection::InvalidProfile), + _ => Err(StoreError::Incompatible("exact RoleProfile rejection code is invalid".into())), + } +} + +fn profile_from_value(value: &Value) -> Result { + let role = RoleProfileRole::from_sql(required_str(value, "role")?)?; + let revision = required_i64(value, "revision")?; + + if revision < 1 { + return Err(StoreError::Incompatible("stored RoleProfile revision is invalid".into())); + } + + Ok(RoleProfileRevision { + role, + revision, + configuration: RoleProfileConfiguration { + model: required_str(value, "model")?.to_owned(), + reasoning_effort: required_str(value, "reasoning_effort")?.to_owned(), + service_tier: required_str(value, "service_tier")?.to_owned(), + instructions: required_str(value, "instructions")?.to_owned(), + provenance: optional_str(value, "provenance")?, + }, + }) +} + +fn required_pointer<'a>(value: &'a Value, pointer: &str) -> Result<&'a Value, StoreError> { + value.pointer(pointer).ok_or_else(|| { + StoreError::Incompatible("exact RoleProfile response shape is incomplete".into()) + }) +} + +fn required_value<'a>(value: &'a Value, key: &str) -> Result<&'a Value, StoreError> { + value + .get(key) + .ok_or_else(|| StoreError::Incompatible("exact RoleProfile effect is incomplete".into())) +} + +fn required_str<'a>(value: &'a Value, key: &str) -> Result<&'a str, StoreError> { + value + .get(key) + .and_then(Value::as_str) + .ok_or_else(|| StoreError::Incompatible("stored RoleProfile text is invalid".into())) +} + +fn required_i64(value: &Value, key: &str) -> Result { + value + .get(key) + .and_then(Value::as_i64) + .ok_or_else(|| StoreError::Incompatible("stored RoleProfile revision is invalid".into())) +} + +fn optional_str(value: &Value, key: &str) -> Result, StoreError> { + match value.get(key) { + Some(Value::Null) => Ok(None), + Some(Value::String(value)) => Ok(Some(value.clone())), + _ => Err(StoreError::Incompatible("stored RoleProfile provenance is invalid".into())), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn profile(role: &str, revision: i64) -> Value { + serde_json::json!({ + "role": role, + "revision": revision, + "model": "gpt-5.6-sol", + "reasoning_effort": "medium", + "service_tier": "priority", + "instructions": "Own the exact role.", + "provenance": null, + }) + } + + #[test] + fn bootstrap_parser_requires_exact_ordered_roles() { + let effects = ["advisor", "lead", "task", "reviewer"] + .into_iter() + .map(|role| serde_json::json!({"profile": profile(role, 1)})) + .collect::>(); + let bytes = serde_json::to_vec(&serde_json::json!({ + "classification": "success", + "effect": {"profiles": effects}, + })) + .expect("fixture serializes"); + + let RoleProfileCommandOutcome::Success(parsed) = + parse_bootstrap_response(&bytes).expect("exact response parses") + else { + panic!("bootstrap fixture must succeed"); + }; + assert_eq!(parsed.len(), 4); + assert_eq!(parsed[3].role, RoleProfileRole::Reviewer); + } + + #[test] + fn stable_rejection_parser_is_distinct_from_infrastructure_failure() { + let bytes = br#"{"classification":"stable_domain_rejection","code":"stale_revision","effect":{"changed":false,"code":"stale_revision"}}"#; + + assert_eq!( + parse_update_response(bytes).expect("stable response parses"), + RoleProfileCommandOutcome::Rejected(RoleProfileRejection::StaleRevision) + ); + } + + #[test] + fn retry_classifier_is_closed_to_accepted_sqlstates() { + assert!(matches!(SqlState::from_code("40001"), SqlState::T_R_SERIALIZATION_FAILURE)); + assert!(matches!(SqlState::from_code("40P01"), SqlState::T_R_DEADLOCK_DETECTED)); + assert!(!SqlState::from_code("DX001").code().starts_with("08")); + } +} diff --git a/crates/decodex-postgres/tests/postgres_store.rs b/crates/decodex-postgres/tests/postgres_store.rs index 272c6fa0..1746bf76 100644 --- a/crates/decodex-postgres/tests/postgres_store.rs +++ b/crates/decodex-postgres/tests/postgres_store.rs @@ -1,6 +1,8 @@ //! Real PostgreSQL contract coverage for the XY-1267 persistence foundation. #[path = "postgres_store/quota.rs"] mod quota; +#[cfg(feature = "test-support")] +#[path = "postgres_store/role_profiles.rs"] mod role_profiles; use std::{ collections::{BTreeMap, HashSet}, @@ -98,6 +100,8 @@ const RUNTIME_EXECUTE_SIGNATURES: &[&str] = &[ "decodex.create_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.text,pg_catalog._text,pg_catalog._text,pg_catalog.int8,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.text)", "decodex.transition_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.int8,decodex.objective_state,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.text)", "decodex.achieve_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text)", + "decodex.bootstrap_role_profiles_exact(pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "decodex.update_role_profile_exact(pg_catalog.text,pg_catalog.text,decodex.role_profile_role,pg_catalog.int8,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", ]; const TRIGGER_ONLY_SIGNATURES: &[&str] = &[ "decodex.enforce_lease_operation_time()", @@ -125,6 +129,14 @@ const TRIGGER_ONLY_SIGNATURES: &[&str] = &[ "decodex.enforce_objective_state()", "decodex.forbid_objective_evidence_mutation()", "decodex.enforce_objective_completion_coherence()", + "decodex.enforce_exact_receipt_completion()", + "decodex.forbid_exact_receipt_rewrite()", + "decodex.forbid_exact_receipt_truncate()", + "decodex.enforce_complete_role_profile_set()", + "decodex.forbid_role_profile_identity_rewrite()", + "decodex.forbid_role_profile_revision_mutation()", + "decodex.forbid_role_profile_truncate()", + "decodex.enforce_role_profile_event_namespace()", ]; const INVALID_PROJECT_AGENT_SQL_CALLS: &[(&str, &str)] = &[ ( diff --git a/crates/decodex-postgres/tests/postgres_store/role_profiles.rs b/crates/decodex-postgres/tests/postgres_store/role_profiles.rs new file mode 100644 index 00000000..e290bd82 --- /dev/null +++ b/crates/decodex-postgres/tests/postgres_store/role_profiles.rs @@ -0,0 +1,744 @@ +use std::{env, path::PathBuf, time::Duration}; + +use tokio::{task::JoinSet, time}; +use tokio_postgres::{Client, NoTls}; + +use super::{expected_peer_uid, separated_configs}; +use decodex_postgres::{ + BootstrapRoleProfiles, PostgresStore, RoleProfileCommandOutcome, RoleProfileConfiguration, + RoleProfileRejection, RoleProfileRole, StoreError, +}; + +fn configuration(marker: &str) -> RoleProfileConfiguration { + RoleProfileConfiguration { + model: format!("gpt-5.6-{marker}"), + reasoning_effort: "medium".into(), + service_tier: "priority".into(), + instructions: format!("Own the exact {marker} role."), + provenance: Some(format!("XY-1346 {marker}")), + } +} + +fn bootstrap() -> BootstrapRoleProfiles { + BootstrapRoleProfiles { + advisor: configuration("advisor"), + lead: configuration("lead"), + task: configuration("task"), + reviewer: configuration("reviewer"), + } +} + +async fn role_profile_state(client: &Client) -> Result<[i64; 6], tokio_postgres::Error> { + let row = client + .query_one( + "SELECT \ + (SELECT count(*) FROM decodex.exact_command_receipts), \ + (SELECT count(*) FROM decodex.role_profiles), \ + (SELECT count(*) FROM decodex.role_profile_revisions), \ + (SELECT count(*) FROM decodex.activity WHERE aggregate_kind='role_profile'), \ + (SELECT count(*) FROM decodex.outbox WHERE aggregate_kind='role_profile'), \ + (SELECT coalesce(sum(current_revision),0) FROM decodex.role_profiles)", + &[], + ) + .await?; + + Ok(std::array::from_fn(|index| row.get(index))) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires an isolated PostgreSQL 18 V9 RoleProfile database"] +async fn postgres_exact_role_profile_commands() -> Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let (probe, probe_connection) = runtime.clone().connect(NoTls).await?; + let probe_task = tokio::spawn(probe_connection); + let (execution_sql, allowed) = PostgresStore::execution_path_contract_fixture(); + let execution = probe.query_one(execution_sql, &[&allowed]).await?; + let execution_contract = [ + execution.get::<_, bool>(0), + execution.get::<_, bool>(1), + execution.get::<_, bool>(2), + execution.get::<_, bool>(3), + ]; + assert_eq!(execution_contract, [true; 4], "execution path closure"); + drop(probe); + probe_task.await??; + let store = + PostgresStore::connect(migration.clone(), runtime.clone(), expected_peer_uid()).await?; + let profiles = bootstrap(); + + let RoleProfileCommandOutcome::Success(created) = + store.bootstrap_role_profiles("role-bootstrap", &profiles).await? + else { + panic!("first exact bootstrap must succeed"); + }; + assert_eq!(created.len(), 4); + assert!(created.iter().all(|profile| profile.revision == 1)); + assert_eq!( + store.bootstrap_role_profiles("role-bootstrap", &profiles).await?, + RoleProfileCommandOutcome::Success(created.clone()), + "same request replays parsed stored bytes", + ); + + let mut conflicting = profiles.clone(); + conflicting.reviewer.instructions.push_str(" changed"); + assert!(matches!( + store.bootstrap_role_profiles("role-bootstrap", &conflicting).await, + Err(StoreError::IdempotencyConflict) + )); + assert_eq!( + store.bootstrap_role_profiles("second-bootstrap", &profiles).await?, + RoleProfileCommandOutcome::Rejected(RoleProfileRejection::AlreadyBootstrapped), + ); + + let updated_configuration = RoleProfileConfiguration { + model: "gpt-5.6-sol".into(), + reasoning_effort: "high".into(), + service_tier: "priority".into(), + instructions: "Review the exact frozen candidate read-only.".into(), + provenance: None, + }; + let RoleProfileCommandOutcome::Success(updated) = store + .update_role_profile( + "reviewer-update", + RoleProfileRole::Reviewer, + 1, + &updated_configuration, + ) + .await? + else { + panic!("exact update must succeed"); + }; + assert_eq!(updated.role, RoleProfileRole::Reviewer); + assert_eq!(updated.revision, 2); + assert_eq!(updated.configuration, updated_configuration); + assert_eq!( + store + .update_role_profile( + "reviewer-update", + RoleProfileRole::Reviewer, + 1, + &updated_configuration, + ) + .await?, + RoleProfileCommandOutcome::Success(updated), + ); + assert_eq!( + store + .update_role_profile( + "reviewer-stale", + RoleProfileRole::Reviewer, + 1, + &updated_configuration, + ) + .await?, + RoleProfileCommandOutcome::Rejected(RoleProfileRejection::StaleRevision), + ); + let mut credential_shaped = updated_configuration.clone(); + credential_shaped.service_tier = "sk_live_0123456789abcdef".into(); + assert!(matches!( + store + .update_role_profile( + "credential-profile", + RoleProfileRole::Reviewer, + 2, + &credential_shaped, + ) + .await, + Err(StoreError::CredentialRejected) + )); + + let (runtime_client, runtime_connection) = runtime.connect(NoTls).await?; + let runtime_task = tokio::spawn(runtime_connection); + for statement in [ + "SELECT * FROM decodex.exact_command_receipts", + "SELECT * FROM decodex.role_profiles", + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('role_profile','advisor',9,'role_profile_updated','forged','{\"kind\":\"role_profile\"}')", + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('other','advisor',9,'role_profile_updated','forged-event','{}')", + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('other','advisor',9,'other','forged-structured','{\"aggregate_kind\":\"role_profile\",\"event_kind\":\"role_profile_updated\"}')", + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('other','advisor',9,'other','forged-payload','{\"role\":\"advisor\",\"model\":\"x\",\"reasoning_effort\":\"x\",\"service_tier\":\"x\",\"instructions\":\"x\",\"revision\":9}')", + "INSERT INTO decodex.outbox(effect_key,aggregate_kind,aggregate_id,aggregate_revision,payload) VALUES ('forged-outbox','role_profile','advisor',9,'{}')", + "INSERT INTO decodex.outbox(effect_key,aggregate_kind,aggregate_id,aggregate_revision,payload) VALUES ('forged-envelope','other','advisor',9,'{\"aggregate_kind\":\"role_profile\"}')", + "SELECT decodex.complete_exact_role_profile_rejection('x','x','x')", + ] { + let error = runtime_client.batch_execute(statement).await.expect_err(statement); + assert_eq!(error.code().map(|code| code.code()), Some("42501"), "{statement}"); + } + drop(runtime_client); + runtime_task.await??; + + let (migration_client, migration_connection) = migration.connect(NoTls).await?; + let migration_task = tokio::spawn(migration_connection); + let response_before: Vec = migration_client + .query_one( + "SELECT response_bytes FROM decodex.exact_command_receipts \ + WHERE protocol_version='decodex/exact-command/1' AND idempotency_key='role-bootstrap'", + &[], + ) + .await? + .get(0); + assert_eq!( + store.bootstrap_role_profiles("role-bootstrap", &profiles).await?, + RoleProfileCommandOutcome::Success(created), + ); + let response_after: Vec = migration_client + .query_one( + "SELECT response_bytes FROM decodex.exact_command_receipts \ + WHERE protocol_version='decodex/exact-command/1' AND idempotency_key='role-bootstrap'", + &[], + ) + .await? + .get(0); + assert_eq!(response_after, response_before, "replay retains byte-identical authority"); + + for (statement, code) in [ + ( + "INSERT INTO decodex.exact_command_receipts( \ + protocol_version,idempotency_key,request_envelope,request_digest,receipt_state, \ + outcome_class,effect_envelope,response_bytes,completed_at) \ + VALUES ('decodex/exact-command/1','owner-malformed-response','{\"operation\":\"hostile\"}', \ + public.digest(convert_to('{\"operation\": \"hostile\"}'::jsonb::text,'UTF8'),'sha256'), \ + 'completed_success','success','{}',convert_to('{\"classification\":\"success\"}','UTF8'),clock_timestamp())", + "23514", + ), + ( + "BEGIN; INSERT INTO decodex.exact_command_receipts( \ + protocol_version,idempotency_key,request_envelope,request_digest,receipt_state) \ + VALUES ('decodex/exact-command/1','owner-incomplete','{\"operation\":\"hostile\"}', \ + public.digest(convert_to('{\"operation\": \"hostile\"}'::jsonb::text,'UTF8'),'sha256'),'executing'); COMMIT", + "23514", + ), + ( + "UPDATE decodex.exact_command_receipts SET response_bytes='\\x00' \ + WHERE idempotency_key='role-bootstrap'", + "23514", + ), + ( + "DELETE FROM decodex.exact_command_receipts WHERE idempotency_key='role-bootstrap'", + "23514", + ), + ("TRUNCATE decodex.exact_command_receipts", "23514"), + ( + "BEGIN; UPDATE decodex.role_profiles SET current_revision=current_revision+1 \ + WHERE role='advisor'; COMMIT", + "23503", + ), + ("DELETE FROM decodex.role_profiles WHERE role='advisor'", "23514"), + ("UPDATE decodex.role_profile_revisions SET model='forged' WHERE role='advisor'", "23514"), + ("TRUNCATE decodex.role_profiles, decodex.role_profile_revisions", "23514"), + ] { + let error = migration_client.batch_execute(statement).await.expect_err(statement); + assert_eq!(error.code().map(|state| state.code()), Some(code), "{statement}"); + } + let counts = migration_client + .query_one( + "SELECT (SELECT count(*) FROM decodex.role_profiles), \ + (SELECT count(*) FROM decodex.role_profile_revisions), \ + (SELECT count(*) FROM decodex.exact_command_receipts WHERE receipt_state='executing'), \ + (SELECT count(*) FROM decodex.activity WHERE aggregate_kind='role_profile'), \ + (SELECT count(*) FROM decodex.outbox WHERE aggregate_kind='role_profile')", + &[], + ) + .await?; + assert_eq!(counts.get::<_, i64>(0), 4); + assert_eq!(counts.get::<_, i64>(1), 5); + assert_eq!(counts.get::<_, i64>(2), 0); + assert_eq!(counts.get::<_, i64>(3), 5); + assert_eq!(counts.get::<_, i64>(4), 5); + assert_eq!( + migration_client + .query_one( + "SELECT count(*) FROM decodex.exact_command_receipts \ + WHERE idempotency_key='credential-profile'", + &[], + ) + .await? + .get::<_, i64>(0), + 0, + "credential-shaped requests never become receipt rows", + ); + drop(migration_client); + migration_task.await??; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 8)] +#[ignore = "requires an isolated PostgreSQL 18 V9 RoleProfile concurrency database"] +async fn postgres_exact_role_profile_concurrency() -> Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + let first = bootstrap(); + let mut second = first.clone(); + second.advisor.model = "gpt-5.6-advisor-alternate".into(); + + let mut mixed = JoinSet::new(); + for index in 0..32 { + let store = store.clone(); + let profiles = if index % 2 == 0 { first.clone() } else { second.clone() }; + mixed.spawn( + async move { store.bootstrap_role_profiles("mixed-bootstrap", &profiles).await }, + ); + } + let mut successes = 0; + let mut conflicts = 0; + while let Some(result) = mixed.join_next().await { + match result? { + Ok(RoleProfileCommandOutcome::Success(_)) => successes += 1, + Err(StoreError::IdempotencyConflict) => conflicts += 1, + Ok(RoleProfileCommandOutcome::Rejected(rejection)) => { + panic!("mixed first bootstrap returned stable rejection: {rejection:?}") + }, + Err(error) => return Err(error.into()), + } + } + assert!(successes > 0); + assert!(conflicts > 0); + let (client, connection) = migration.connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + let winning_model: String = client + .query_one( + "SELECT revision.model FROM decodex.role_profiles AS profile \ + JOIN decodex.role_profile_revisions AS revision \ + ON revision.role=profile.role AND revision.revision=profile.current_revision \ + WHERE profile.role='advisor'", + &[], + ) + .await? + .get(0); + let winner = if winning_model == first.advisor.model { first } else { second }; + + let mut replay = JoinSet::new(); + for _ in 0..32 { + let store = store.clone(); + let winner = winner.clone(); + replay + .spawn(async move { store.bootstrap_role_profiles("mixed-bootstrap", &winner).await }); + } + while let Some(result) = replay.join_next().await { + assert!(matches!(result??, RoleProfileCommandOutcome::Success(_))); + successes += 1; + } + let mut loser = winner.clone(); + loser.advisor.model.push_str("-conflict"); + for _ in 0..8 { + if matches!( + store.bootstrap_role_profiles("mixed-bootstrap", &loser).await, + Err(StoreError::IdempotencyConflict) + ) { + conflicts += 1; + } + } + assert!(successes >= 32); + assert!(conflicts >= 8); + + let update = RoleProfileConfiguration { + model: "gpt-5.6-concurrent".into(), + reasoning_effort: "high".into(), + service_tier: "priority".into(), + instructions: "Concurrent exact update.".into(), + provenance: None, + }; + let mut updates = JoinSet::new(); + for index in 0..32 { + let store = store.clone(); + let update = update.clone(); + updates.spawn(async move { + store + .update_role_profile( + &format!("concurrent-update-{index}"), + RoleProfileRole::Task, + 1, + &update, + ) + .await + }); + } + let mut update_success = 0; + let mut update_stale = 0; + while let Some(result) = updates.join_next().await { + match result?? { + RoleProfileCommandOutcome::Success(_) => update_success += 1, + RoleProfileCommandOutcome::Rejected(RoleProfileRejection::StaleRevision) => { + update_stale += 1 + }, + other => panic!("unexpected concurrent update outcome: {other:?}"), + } + } + assert_eq!((update_success, update_stale), (1, 31)); + let incomplete: i64 = client + .query_one( + "SELECT count(*) FROM decodex.exact_command_receipts WHERE receipt_state='executing'", + &[], + ) + .await? + .get(0); + assert_eq!(incomplete, 0); + drop(client); + connection_task.await??; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires an isolated PostgreSQL 18 V9 RoleProfile rollback database"] +async fn postgres_exact_role_profile_atomic_rollback() -> Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = + PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + assert!(matches!( + store.bootstrap_role_profiles("rollback-bootstrap", &bootstrap()).await?, + RoleProfileCommandOutcome::Success(_) + )); + let (admin, admin_connection) = migration.connect(NoTls).await?; + let admin_task = tokio::spawn(admin_connection); + admin + .batch_execute( + "CREATE SEQUENCE public.xy1346_rollback_faults; \ + CREATE TABLE public.xy1346_rollback_schedule(boundary text PRIMARY KEY); \ + REVOKE ALL ON TABLE public.xy1346_rollback_schedule FROM PUBLIC; \ + REVOKE ALL ON SEQUENCE public.xy1346_rollback_faults FROM PUBLIC; \ + CREATE FUNCTION public.xy1346_raise_scheduled_role_profile_fault() \ + RETURNS trigger LANGUAGE plpgsql AS $$ \ + BEGIN \ + IF session_user <> current_user AND EXISTS ( \ + SELECT 1 FROM public.xy1346_rollback_schedule WHERE boundary=TG_ARGV[0] \ + ) THEN \ + PERFORM pg_catalog.nextval('public.xy1346_rollback_faults'); \ + RAISE EXCEPTION 'scheduled XY-1346 rollback fault at %', TG_ARGV[0] \ + USING ERRCODE='XX000'; \ + END IF; \ + RETURN NEW; \ + END $$; \ + CREATE TRIGGER xy1346_fault_receipt AFTER INSERT ON decodex.exact_command_receipts \ + FOR EACH ROW EXECUTE FUNCTION public.xy1346_raise_scheduled_role_profile_fault('receipt'); \ + CREATE TRIGGER xy1346_fault_domain AFTER INSERT ON decodex.role_profile_revisions \ + FOR EACH ROW EXECUTE FUNCTION public.xy1346_raise_scheduled_role_profile_fault('domain'); \ + CREATE TRIGGER xy1346_fault_activity AFTER INSERT ON decodex.activity \ + FOR EACH ROW EXECUTE FUNCTION public.xy1346_raise_scheduled_role_profile_fault('activity'); \ + CREATE TRIGGER xy1346_fault_outbox AFTER INSERT ON decodex.outbox \ + FOR EACH ROW EXECUTE FUNCTION public.xy1346_raise_scheduled_role_profile_fault('outbox'); \ + REVOKE ALL ON FUNCTION public.xy1346_raise_scheduled_role_profile_fault() FROM PUBLIC", + ) + .await?; + + for (boundary, role) in [ + ("receipt", RoleProfileRole::Advisor), + ("domain", RoleProfileRole::Lead), + ("activity", RoleProfileRole::Task), + ("outbox", RoleProfileRole::Reviewer), + ] { + let key = format!("rollback-{boundary}"); + let configuration = configuration(&key); + admin + .execute( + "INSERT INTO public.xy1346_rollback_schedule(boundary) VALUES ($1)", + &[&boundary], + ) + .await?; + let before = role_profile_state(&admin).await?; + assert!(matches!( + store.update_role_profile(&key, role, 1, &configuration).await, + Err(StoreError::Database(_)) + )); + assert_eq!(role_profile_state(&admin).await?, before, "rollback at {boundary}"); + admin + .execute( + "DELETE FROM public.xy1346_rollback_schedule WHERE boundary=$1", + &[&boundary], + ) + .await?; + let RoleProfileCommandOutcome::Success(revision) = + store.update_role_profile(&key, role, 1, &configuration).await? + else { + panic!("rollback retry at {boundary} must converge"); + }; + assert_eq!(revision.revision, 2); + } + assert_eq!( + admin + .query_one("SELECT last_value FROM public.xy1346_rollback_faults", &[]) + .await? + .get::<_, i64>(0), + 4, + ); + drop(admin); + admin_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 6)] +#[ignore = "requires an isolated PostgreSQL 18 V9 RoleProfile retry database"] +async fn postgres_exact_role_profile_retry_convergence() -> Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = + PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + assert!(matches!( + store.bootstrap_role_profiles("retry-bootstrap", &bootstrap()).await?, + RoleProfileCommandOutcome::Success(_) + )); + let (admin, admin_connection) = migration.connect(NoTls).await?; + let admin_task = tokio::spawn(admin_connection); + admin + .batch_execute( + "CREATE SEQUENCE public.xy1346_serialization_attempts; \ + CREATE SEQUENCE public.xy1346_deadlock_attempts; \ + REVOKE ALL ON SEQUENCE public.xy1346_serialization_attempts, \ + public.xy1346_deadlock_attempts FROM PUBLIC; \ + CREATE FUNCTION public.xy1346_schedule_retryable_role_profile_fault() \ + RETURNS trigger LANGUAGE plpgsql AS $$ \ + DECLARE attempt bigint; \ + BEGIN \ + IF session_user <> current_user AND NEW.idempotency_key='serialization-retry' THEN \ + attempt := pg_catalog.nextval('public.xy1346_serialization_attempts'); \ + IF attempt=1 THEN \ + RAISE EXCEPTION 'scheduled serialization retry' USING ERRCODE='40001'; \ + END IF; \ + ELSIF session_user <> current_user AND NEW.idempotency_key='deadlock-retry' THEN \ + PERFORM pg_catalog.nextval('public.xy1346_deadlock_attempts'); \ + END IF; \ + RETURN NEW; \ + END $$; \ + CREATE TRIGGER xy1346_retryable_receipt_fault \ + BEFORE INSERT ON decodex.exact_command_receipts FOR EACH ROW \ + EXECUTE FUNCTION public.xy1346_schedule_retryable_role_profile_fault(); \ + REVOKE ALL ON FUNCTION public.xy1346_schedule_retryable_role_profile_fault() FROM PUBLIC", + ) + .await?; + + let RoleProfileCommandOutcome::Success(serialized) = store + .update_role_profile( + "serialization-retry", + RoleProfileRole::Advisor, + 1, + &configuration("serialization-retry"), + ) + .await? + else { + panic!("serialization retry must converge"); + }; + assert_eq!(serialized.revision, 2); + assert_eq!( + admin + .query_one("SELECT last_value FROM public.xy1346_serialization_attempts", &[]) + .await? + .get::<_, i64>(0), + 2, + ); + + admin + .batch_execute( + "BEGIN; SELECT role FROM decodex.role_profiles WHERE role='lead' FOR UPDATE", + ) + .await?; + let blocker_pid: i32 = admin.query_one("SELECT pg_backend_pid()", &[]).await?.get(0); + let deadlock_store = store.clone(); + let command_task = tokio::spawn(async move { + deadlock_store + .update_role_profile( + "deadlock-retry", + RoleProfileRole::Lead, + 1, + &configuration("deadlock-retry"), + ) + .await + }); + let (observer, observer_connection) = migration.connect(NoTls).await?; + let observer_task = tokio::spawn(observer_connection); + assert!(super::wait_for_any_blocked_by(&observer, blocker_pid).await?); + drop(observer); + observer_task.await??; + + let antagonist = tokio::spawn(async move { + admin + .batch_execute( + "WITH request(value) AS (VALUES ('{\"operation\":\"deadlock-antagonist\"}'::jsonb)) \ + INSERT INTO decodex.exact_command_receipts( \ + protocol_version,idempotency_key,request_envelope,request_digest,receipt_state \ + ) SELECT 'decodex/exact-command/1','deadlock-retry',value, \ + public.digest(convert_to(value::text,'UTF8'),'sha256'),'executing' FROM request; \ + ROLLBACK", + ) + .await + }); + let RoleProfileCommandOutcome::Success(deadlocked) = + time::timeout(Duration::from_secs(20), command_task).await??? + else { + panic!("deadlock retry must converge"); + }; + assert_eq!(deadlocked.revision, 2); + time::timeout(Duration::from_secs(20), antagonist).await???; + admin_task.await??; + + let (check, check_connection) = migration.connect(NoTls).await?; + let check_task = tokio::spawn(check_connection); + assert!( + check + .query_one("SELECT last_value FROM public.xy1346_deadlock_attempts", &[]) + .await? + .get::<_, i64>(0) + >= 2, + "the production command must retry after the executed deadlock", + ); + assert_eq!( + check + .query_one( + "SELECT count(*) FROM decodex.exact_command_receipts \ + WHERE idempotency_key IN ('serialization-retry','deadlock-retry') \ + AND receipt_state='completed_success'", + &[], + ) + .await? + .get::<_, i64>(0), + 2, + ); + drop(check); + check_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires the isolated PostgreSQL 18 V8 to V9 upgrade database"] +async fn postgres_v8_to_v9_role_profile_upgrade() -> Result<(), Box> { + let (migration, _) = separated_configs("DECODEX_TEST")?; + PostgresStore::migrate_fixture_through_v8(migration.clone(), expected_peer_uid()).await?; + let (client, connection) = migration.clone().connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + let before: i32 = client + .query_one("SELECT max(version) FROM public.refinery_schema_history", &[]) + .await? + .get(0); + assert_eq!(before, 8); + assert!( + client + .query_one("SELECT to_regclass('decodex.exact_command_receipts') IS NULL", &[]) + .await? + .get::<_, bool>(0) + ); + drop(client); + connection_task.await??; + PostgresStore::migrate(migration.clone(), expected_peer_uid()).await?; + let (client, connection) = migration.connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + let after: i32 = client + .query_one("SELECT max(version) FROM public.refinery_schema_history", &[]) + .await? + .get(0); + assert_eq!(after, 9); + assert_eq!( + client.query_one("SELECT count(*) FROM decodex.role_profiles", &[]).await?.get::<_, i64>(0), + 0, + ); + drop(client); + connection_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires the isolated PostgreSQL 18 RoleProfile crash/recovery database"] +async fn postgres_exact_role_profile_crash_recovery() -> Result<(), Box> { + let sync = PathBuf::from(env::var("DECODEX_ROLE_PROFILE_RESTART_SYNC")?); + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = + PostgresStore::connect(migration.clone(), runtime.clone(), expected_peer_uid()).await?; + let profiles = bootstrap(); + assert!(matches!( + store.bootstrap_role_profiles("crash-bootstrap", &profiles).await?, + RoleProfileCommandOutcome::Success(_) + )); + let (blocker, blocker_connection) = migration.clone().connect(NoTls).await?; + let blocker_task = tokio::spawn(blocker_connection); + blocker + .batch_execute("BEGIN; LOCK TABLE decodex.role_profiles IN ACCESS EXCLUSIVE MODE") + .await?; + let update = configuration("crash-update"); + let task_store = store.clone(); + let update_task = tokio::spawn(async move { + task_store.update_role_profile("crash-update", RoleProfileRole::Lead, 1, &update).await + }); + let blocker_pid: i32 = blocker.query_one("SELECT pg_backend_pid()", &[]).await?.get(0); + let (observer, observer_connection) = migration.clone().connect(NoTls).await?; + let observer_task = tokio::spawn(observer_connection); + assert!(super::wait_for_any_blocked_by(&observer, blocker_pid).await?); + std::fs::write(sync.join("ready"), b"ready")?; + for _ in 0..3_000 { + if sync.join("restarted").exists() { + break; + } + time::sleep(Duration::from_millis(10)).await; + } + if !sync.join("restarted").exists() { + return Err("PostgreSQL restart fixture did not signal recovery".into()); + } + assert!(update_task.await?.is_err(), "precommit connection loss must not report success"); + drop(observer); + drop(blocker); + let _ = observer_task.await; + let _ = blocker_task.await; + drop(store); + + let recovered = PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + assert!(matches!( + recovered + .update_role_profile( + "crash-update", + RoleProfileRole::Lead, + 1, + &configuration("crash-update"), + ) + .await?, + RoleProfileCommandOutcome::Success(_) + )); + let (client, connection) = migration.connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + let state = client + .query_one( + "SELECT \ + (SELECT count(*) FROM decodex.exact_command_receipts WHERE receipt_state='executing'), \ + (SELECT count(*) FROM decodex.role_profile_revisions WHERE role='lead' AND revision=2), \ + (SELECT count(*) FROM decodex.activity WHERE aggregate_kind='role_profile' AND aggregate_id='lead' AND revision=2), \ + (SELECT count(*) FROM decodex.outbox WHERE aggregate_kind='role_profile' AND aggregate_id='lead' AND aggregate_revision=2)", + &[], + ) + .await?; + assert_eq!( + [ + state.get::<_, i64>(0), + state.get::<_, i64>(1), + state.get::<_, i64>(2), + state.get::<_, i64>(3), + ], + [0, 1, 1, 1], + ); + drop(client); + connection_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires the populated PostgreSQL 18 V9 RoleProfile restore database"] +async fn postgres_exact_role_profile_restore() -> Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + assert!(matches!( + store.bootstrap_role_profiles("role-bootstrap", &bootstrap()).await?, + RoleProfileCommandOutcome::Success(_) + )); + let (client, connection) = migration.connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + let valid: bool = client + .query_one( + "SELECT \ + (SELECT count(*)=4 FROM decodex.role_profiles) AND \ + (SELECT count(*)=5 FROM decodex.role_profile_revisions) AND \ + (SELECT count(*)=0 FROM decodex.exact_command_receipts WHERE receipt_state='executing') AND \ + NOT EXISTS (SELECT 1 FROM decodex.exact_command_receipts \ + WHERE response_bytes IS NULL OR convert_from(response_bytes,'UTF8')::jsonb->'effect' IS DISTINCT FROM effect_envelope)", + &[], + ) + .await? + .get(0); + assert!(valid); + drop(client); + connection_task.await??; + Ok(()) +} diff --git a/openwiki/architecture/runtime-architecture.md b/openwiki/architecture/runtime-architecture.md index 7dfc5f7c..0a469e59 100644 --- a/openwiki/architecture/runtime-architecture.md +++ b/openwiki/architecture/runtime-architecture.md @@ -119,21 +119,22 @@ runtime-effective, regardless of `pg_extension.extnamespace`. Superuser/BYPASSRLS/role/database administration, database/schema CREATE, TRUNCATE/TRIGGER/REFERENCES/MAINTAIN, excess table DML or grant options, `session_replication_role` SET/ALTER SYSTEM, and any effective non-`origin` login value are unsafe -in any reachable authority state. The audit verifies all twenty-nine shipped safety/state/retention triggers -by table, event mask, row/statement level, regular non-constraint and non-deferrable shape, -origin-enabled mode, and function binding, then compares +in any reachable authority state. At the frozen V9 boundary, the audit verifies all fifty-two shipped +non-internal trigger bindings, including regular and deferred constraint triggers, by table, event +mask, row/statement level, constraint and deferral state, origin-enabled mode, and function binding, +then compares each bound function's exact metadata and `pg_proc.prosrc` bytes with the canonical body embedded in -the immutable V1 or forward-only V3 migration. It additionally closes the entire runtime-callable `decodex` function +the immutable forward migration ledger through V9. It additionally closes the entire runtime-callable `decodex` function namespace over exact signatures and overloads, argument/result shape, language, volatility, parallel/strict/set behavior, planner metadata, exact security-invoker/definer state and exact per-function settings, and canonical source. Unexpected functions, overloads, owner-executed functions, or unsafe settings are unsafe; missing functions or noncanonical source are incompatible. Disabled or misbound triggers are unsafe; a replaced same-signature safety-function body is incompatible. -Every non-internal trigger on a Decodex runtime relation must be one of those twenty-nine exact bindings. +Every non-internal trigger on a Decodex runtime relation must be one of those fifty-two exact V9 bindings. The same closed execution-path audit permits no user rule, row-security policy, or enabled/forced RLS on those relations and rejects non-`pg_catalog` function/operator dependencies from defaults, generated expressions, constraints, indexes, rules, or policies unless they resolve to one of the -thirty-four canonical functions. Every canonical function has the exact function-local +seventy-two canonical V9 functions. Every canonical function has the exact function-local `pg_catalog, decodex` search path, so runtime-selected callable or operator shadows cannot redirect trigger or constraint execution. A trigger cannot therefore invoke an adjacent public owner-executed function merely because runtime DML fires it. @@ -157,20 +158,21 @@ string-to-system-catalog identity explicitly qualifies `pg_catalog`; the authority audit and schema-qualified migration-ledger verification remain correct under a hostile runtime `search_path` that shadows both ledger and system-catalog names. Missing required schema, table, sequence, function, or ledger-read authority is incompatible. -Three narrowly scoped canonical `SECURITY DEFINER` functions issue history cursors, prune expired -cursor snapshots, and append immutable history-item versions. The issuer derives Conversation, +At the frozen V9 boundary, sixteen canonical `SECURITY DEFINER` functions comprise the three V3 +cursor/history functions, eleven V5-V7 Project/Policy/Program/Objective command entrypoints, and two +V9 RoleProfile command entrypoints. The cursor issuer derives Conversation, snapshot version, parent, page size, position, item identity, and expiry under serialized Conversation authority; the bounded pruner is callable by runtime, while the capture function is trigger-only and runtime cannot execute it directly. Runtime has no cursor-table INSERT authority. -All other canonical functions are security invokers. The additional-function adversarial fixture creates a thirty-fifth migration-owned, +The other fifty-six canonical V9 functions are security invokers. The additional-function adversarial fixture creates a fixture-only seventy-third migration-owned, runtime-executable `SECURITY DEFINER` function with an unsafe per-function setting and migration-owner trigger authority, proves runtime direct trigger DDL is denied, executes the owner-authority effect, and restores the trigger before the independent doctor rejection. A separate public-function trigger fixture proves runtime DML can execute an owner effect without direct function `EXECUTE`, protected -table `UPDATE`, or `TRIGGER`; the exact twenty-nine-trigger inventory rejects that path. A public, +table `UPDATE`, or `TRIGGER`; the exact fifty-two-trigger V9 inventory rejects that path. A public, runtime-owned extension fixture attaches a migration-owned Decodex collation as an extension member, proves the runtime can transactionally drop it, and is rejected through the dependency audit. The -closed thirty-four-function inventory remains independent of the distinct same-signature canonical-source +closed seventy-two-function V9 inventory remains independent of the distinct same-signature canonical-source substitution fixture. Missing, malformed, unsafe, unreachable, authentication-failed, or incompatible bootstrap retains a typed unavailable adapter; there is no ambient/default database or alternate state authority. Repository and @@ -397,8 +399,16 @@ identical transaction. JSONB identity uses equality, explicit null keys, typed e and exact PostgreSQL text semantics. Effects use actual `RETURNING` rows and canonical audit identities. Operation is part of the envelope, so cross-operation key reuse conflicts. The accepted proof and vertical ownership are in -[the XY-1345 evidence](../evidence/xy-1345-exact-command-authority.md); production V9 belongs to -XY-1346 and V10 to re-bounded XY-1337. +[the XY-1345 evidence](../evidence/xy-1345-exact-command-authority.md). V9 implements this boundary +for immutable global RoleProfiles through `bootstrap_role_profiles_exact` and +`update_role_profile_exact`; V10 remains owned by re-bounded XY-1337. + +The V9 RoleProfile model contains only advisor, lead, task, and reviewer. Bootstrap receives four +role-implied scalar configuration groups and commits the complete set or nothing. Updates append an +immutable revision under an expected-revision row lock and atomically advance the selected role's +single current pointer. Runtime neither selects nor mutates the RoleProfile or exact-receipt +relations directly; it parses the response bytes returned by the two command-complete entrypoints +and retries a complete top-level transaction only after a classified infrastructure SQLSTATE. Legacy `command_receipts` retain the receipt-first fenced-claim protocol only for unrelated blob, filesystem, external, or long-running sagas whose point of no return cannot fit in one PostgreSQL diff --git a/openwiki/decisions/vnext-authority.md b/openwiki/decisions/vnext-authority.md index bc2dfaf1..c6fe32b0 100644 --- a/openwiki/decisions/vnext-authority.md +++ b/openwiki/decisions/vnext-authority.md @@ -72,7 +72,8 @@ response evidence comes from actual `RETURNING` rows and canonical audit identit Candidate 3 is superseded as implementation and remains hostile-test/design provenance only. [XY-1345 evidence](../evidence/xy-1345-exact-command-authority.md) records the passing isolated PostgreSQL 18 proof. The serial vertical order is XY-1345 authority/prototype, then XY-1346 exact -receipts plus RoleProfile V9, then re-bounded XY-1337 RuntimeSession V10. Legacy +receipts plus immutable global RoleProfile bootstrap/update in V9, then re-bounded XY-1337 +RuntimeSession V10. Legacy `command_receipts` semantics remain unchanged for unrelated external or long-running sagas. ## Decision diff --git a/openwiki/operations/commands-and-validation.md b/openwiki/operations/commands-and-validation.md index 2b31fbae..4a285a51 100644 --- a/openwiki/operations/commands-and-validation.md +++ b/openwiki/operations/commands-and-validation.md @@ -88,7 +88,7 @@ database unavailability, plugin/vault/blob unknown states, and redaction. Protoc fixtures separately force wrong major/minor, malformed/oversized response, timeout, and untrusted server-text cases. -The XY-1267/XY-1307 integration command and XY-1264 storage proof are intentionally separate because they require an intended macOS host with one PostgreSQL 18 distribution. Each creates and removes its own isolated temporary checksummed cluster with TCP disabled and never enumerates or changes an existing service. The PostgreSQL command provisions fixture-only migration/runtime roles, proves least-privilege daemon bootstrap, and rejects 27 unsafe roots covering direct, inherited, NOINHERIT/SET-only, and membership-admin paths to forbidden role attributes, PostgreSQL 18 namespace-object ownership (including distinct collation, conversion, operator, and text-search cases), DDL, table/ledger/sequence mutation, grant options, `session_replication_role` SET/ALTER SYSTEM, retention bypass, trigger drift, extension-member control, an indirect public-function trigger, and a genuinely additional function. It closes every runtime-callable Decodex function over exact signatures, overloads, metadata, settings, and canonical source. All 34 shipped functions have the exact secure `pg_catalog, decodex` function-local search path. The three shipped security definers are the bounded cursor issuer, bounded cursor/history-version pruner, and trigger-only history-version capture function; runtime cannot insert cursors or execute capture directly. The additional thirty-fifth function is migration-owned, runtime-executable, `SECURITY DEFINER`, configured with an unsafe setting, and is invoked as runtime to perform owner-authority trigger DDL before fixture restoration and independent rejection. The separate substitution fixture replaces a shipped safety body without changing its signature. The indirect-trigger fixture proves runtime DML executes a public definer function despite direct `EXECUTE`, protected-table `UPDATE`, and `TRIGGER` all being denied. The extension fixture proves a public runtime-owned extension can transactionally drop it. Six incompatible roots cover missing ledger SELECT, canonical-function drift, a dropped credential constraint with demonstrated credential insertion, an external child cascade with demonstrated runtime-mediated deletion, a same-count tampered migration ledger, and absent `pgcrypto`. The canonical PostgreSQL 18 schema manifest closes defaults, constraints on both foreign-key sides, indexes, enums, and internal constraint-trigger semantics. Descriptor-pinned socket unit fixtures reject a same-UID pre-planted endpoint in a world-writable configured directory, a mismatched operator UID pin, replaced ancestors, replaced endpoints, and deterministic replacement between precheck and failed connect; an unchanged secure stale socket maps to unreachable. An isolated daemon fixture starts Ready, replaces the configured endpoint, and proves a fresh V1.2 doctor query becomes unsafe-host-path without migration or repinning. The runtime protocol tests keep mutation receipt lookup/capacity independent across V1.1/V1.2 and prove repeated, ordered, concurrent live queries neither replay nor consume receipts. The adapter contract tampers a ledger name at constant row count and removes `pgcrypto` after bootstrap, proving read-only live revalidation reports both as incompatible before restoration. The harness also exercises an in-flight Rust BlobSession across an immediate PostgreSQL restart: the old session loses its hash lock and transaction-B connection, its stale claim cannot complete, and a reassigned exact retry verifies already-published bytes before committing metadata. It also proves `setval` denial, same-signature callable hostile-`search_path` safety, Turkish ICU credential behavior, and populated dump/restore. The XY-1264 proof additionally exercises rollback, blob, and cache behavior (`crates/decodex-postgres/src/socket.rs`, `crates/decodex-runtime/tests/bootstrap_doctor.rs`, `scripts/vnext/postgres_store_test.py`, `spikes/vnext-storage/proof.py`, `spikes/vnext-storage/README.md`). +The XY-1267/XY-1307 integration command and XY-1264 storage proof are intentionally separate because they require an intended macOS host with one PostgreSQL 18 distribution. Each creates and removes its own isolated temporary checksummed cluster with TCP disabled and never enumerates or changes an existing service. The PostgreSQL command provisions fixture-only migration/runtime roles, proves least-privilege daemon bootstrap, and rejects 27 unsafe roots covering direct, inherited, NOINHERIT/SET-only, and membership-admin paths to forbidden role attributes, PostgreSQL 18 namespace-object ownership (including distinct collation, conversion, operator, and text-search cases), DDL, table/ledger/sequence mutation, grant options, `session_replication_role` SET/ALTER SYSTEM, retention bypass, trigger drift, extension-member control, an indirect public-function trigger, and a genuinely additional function. It closes every runtime-callable Decodex function over exact signatures, overloads, metadata, settings, and canonical source. At the frozen V9 boundary, all 72 shipped functions have the exact secure `pg_catalog, decodex` function-local search path, and all 52 non-internal shipped trigger bindings—including deferred constraint triggers—have exact catalog attestations. The 16 shipped security definers comprise three V3 cursor/history functions, eleven V5-V7 Project/Policy/Program/Objective command entrypoints, and two V9 RoleProfile command entrypoints; runtime cannot insert cursors or execute capture directly. The additional fixture-only seventy-third function is migration-owned, runtime-executable, `SECURITY DEFINER`, configured with an unsafe setting, and is invoked as runtime to perform owner-authority trigger DDL before fixture restoration and independent rejection. The separate substitution fixture replaces a shipped safety body without changing its signature. The indirect-trigger fixture proves runtime DML executes a public definer function despite direct `EXECUTE`, protected-table `UPDATE`, and `TRIGGER` all being denied. The extension fixture proves a public runtime-owned extension can transactionally drop it. Six incompatible roots cover missing ledger SELECT, canonical-function drift, a dropped credential constraint with demonstrated credential insertion, an external child cascade with demonstrated runtime-mediated deletion, a same-count tampered migration ledger, and absent `pgcrypto`. The canonical PostgreSQL 18 schema manifest closes defaults, constraints on both foreign-key sides, indexes, enums, and internal constraint-trigger semantics. Descriptor-pinned socket unit fixtures reject a same-UID pre-planted endpoint in a world-writable configured directory, a mismatched operator UID pin, replaced ancestors, replaced endpoints, and deterministic replacement between precheck and failed connect; an unchanged secure stale socket maps to unreachable. An isolated daemon fixture starts Ready, replaces the configured endpoint, and proves a fresh V1.2 doctor query becomes unsafe-host-path without migration or repinning. The runtime protocol tests keep mutation receipt lookup/capacity independent across V1.1/V1.2 and prove repeated, ordered, concurrent live queries neither replay nor consume receipts. The adapter contract tampers a ledger name at constant row count and removes `pgcrypto` after bootstrap, proving read-only live revalidation reports both as incompatible before restoration. The harness also exercises an in-flight Rust BlobSession across an immediate PostgreSQL restart: the old session loses its hash lock and transaction-B connection, its stale claim cannot complete, and a reassigned exact retry verifies already-published bytes before committing metadata. It also proves `setval` denial, same-signature callable hostile-`search_path` safety, Turkish ICU credential behavior, and populated dump/restore. The XY-1264 proof additionally exercises rollback, blob, and cache behavior (`crates/decodex-postgres/src/socket.rs`, `crates/decodex-runtime/tests/bootstrap_doctor.rs`, `scripts/vnext/postgres_store_test.py`, `spikes/vnext-storage/proof.py`, `spikes/vnext-storage/README.md`). The XY-1345 command is a separate non-production architecture proof. It requires exactly PostgreSQL 18.4, creates a private temporary cluster with TCP disabled, installs only fixture @@ -96,7 +96,14 @@ roles/objects, exercises the deterministic and 50-by-32 stress schedules, perfor `pg_dump`/`pg_restore`, stops the cluster, and removes its temporary root on success or failure. Its JSON output is the command receipt; any `FAILED` result or nonzero anomaly count falsifies the gate. It does not run the production migration ledger and cannot substitute for the V9 clean-bootstrap, -V8-to-V9 upgrade, authority-manifest, hostile SQL, and restore tests owned by XY-1346. See +V8-to-V9 upgrade, authority-manifest, hostile SQL, crash/recovery, focused concurrency/idempotency, +and populated-restore tests owned by XY-1346. The canonical final PostgreSQL harness runs the V9 +upgrade, concurrency, crash/recovery, and populated-restore scenarios in isolated databases; it +also injects admin-only receipt/domain/activity/outbox aborts and database-executed +serialization/deadlock failures to prove whole-transaction rollback, retry, and convergence through +the production RoleProfile API. The V9 proof runs once only after the exact candidate is frozen; +active implementation uses targeted Rust compilation, parser/unit contracts, and migration/protocol +syntax checks. See [the durable evidence page](../evidence/xy-1345-exact-command-authority.md). The PostgreSQL integration harness bootstraps the shipped four-migration history (`V1` diff --git a/openwiki/quickstart.md b/openwiki/quickstart.md index 69455822..c1f6298a 100644 --- a/openwiki/quickstart.md +++ b/openwiki/quickstart.md @@ -44,7 +44,8 @@ OpenWiki is the repo-local project knowledge surface for agents and maintainers. connection configuration, embedded immutable migrations, optimistic transactions, leases, append-only activity, transactional outbox delivery, inert account/window metadata, bounded history pagination, immutable snapshots, blob references, Context Pack - revisions, and inert rollover/fallback proposals. XY-1307 wires the typed connection data through runtime composition into the + revisions, inert rollover/fallback proposals, exact in-transaction receipts, and immutable + global RoleProfile revisions. XY-1307 wires the typed connection data through runtime composition into the existing verification/migration boundary; every bootstrap failure remains fail-closed. - `crates/decodex-codex/` owns typed app-server contracts, exact-build capability profiles, redacted normalized events, fixed and bounded read-only launch/probe behavior, and immutable one-account process supervision. Its live dispatch guard remains fail-closed on XY-1304. - `crates/decodex-runtime/` owns `decodexd` service assembly and is the only library owner that composes protocol and infrastructure adapters. @@ -91,7 +92,7 @@ superuser/BYPASSRLS, database/schema/table DDL, TRUNCATE, grant options, trigger authority, `session_replication_role` SET/ALTER SYSTEM, or any other retention bypass. The effective login value must be `origin`. Readiness requires a closed inventory of every runtime-callable Decodex function with exact signatures, overloads, metadata, settings, and -source bodies matching the canonical embedded migrations. The twenty-nine expected safety/state/retention +source bodies matching the canonical embedded migrations. The fifty-two expected safety/state/retention triggers must also remain enabled, correctly shaped, and bound to their canonical functions; no additional user trigger, rule, policy, RLS mode, or noncanonical expression dependency may add an indirect execution path on a runtime relation. One canonical PostgreSQL 18 schema manifest also @@ -105,10 +106,10 @@ not extension schema, so a runtime-controlled extension cannot own or drop a Dec ordered versions, names, and checksums must exactly equal the embedded migration inventory; missing SELECT is incompatible, while ownership, SET-reachable authority, table/column grant options, writes, and table DDL privileges are unsafe. All canonical database functions have an exact -function-local `pg_catalog, decodex` search path. Exactly three narrowly scoped functions are -security definers: bounded cursor issuance inserts opaque issued-only continuations, bounded snapshot -pruning removes expired cursor/version chains under serialized caps, and trigger-only history-version -capture appends immutable item versions. Runtime cannot insert cursor rows or execute capture directly. +function-local `pg_catalog, decodex` search path. Exactly sixteen narrowly scoped functions are +security definers: three history cursor/version functions, eleven Project/Agent/Policy/Program/Objective +commands, and the two command-complete exact RoleProfile entrypoints. Runtime cannot insert cursor, +exact-receipt, or RoleProfile rows or execute trigger/private helpers directly. The two bound identity sequences require USAGE only; UPDATE/`setval`, SELECT, ownership, grant options, and SET-reachable surplus authority are unsafe. Explicit qualification keeps bootstrap correct under a hostile runtime `search_path`. diff --git a/openwiki/specs/vnext-authority.md b/openwiki/specs/vnext-authority.md index 6c365796..d22578c0 100644 --- a/openwiki/specs/vnext-authority.md +++ b/openwiki/specs/vnext-authority.md @@ -161,11 +161,20 @@ failpoint, and incomplete-row probe present in the candidate, with only command- runtime-executable. Effect evidence decodes the stored response bytes and joins their effect envelope to the returned domain row and actual canonical activity/outbox identities. -This authority is implemented vertically: XY-1345 records and proves the protocol; XY-1346 owns -the separate relation and RoleProfile bootstrap/update in V9; re-bounded XY-1337 owns authoritative +This authority is implemented vertically: XY-1345 records and proves the protocol; XY-1346 +implements the separate relation and RoleProfile bootstrap/update in V9; re-bounded XY-1337 owns authoritative RuntimeSession snapshot creation/transition in V10. Candidate 3 is superseded code and may supply only independently re-derived invariants and hostile-test ideas. +V9 persists exactly the `advisor`, `lead`, `task`, and `reviewer` identities in +`role_profiles`, keeps every configuration in immutable `role_profile_revisions`, and advances one +current-revision pointer per role. `bootstrap_role_profiles_exact` accepts four fixed +advisor/lead/task/reviewer scalar groups and creates all four revision-one profiles atomically. +`update_role_profile_exact` accepts one typed role plus an expected revision, appends exactly one +immutable revision, and advances only that role's pointer. Both functions return and retain +PostgreSQL-built response bytes whose effects are assembled from the returned profile rows and the +actual canonical activity/outbox identities. + ## Conversation, context, and communication Every meaningful Decodex-created thread uses `ephemeral=false`, the shared normal diff --git a/openwiki/specs/vnext-gates.md b/openwiki/specs/vnext-gates.md index 13c23e4e..672cdaa1 100644 --- a/openwiki/specs/vnext-gates.md +++ b/openwiki/specs/vnext-gates.md @@ -105,7 +105,7 @@ Permission is issue-scoped and does not bypass each issue's own dependencies: | XY-1276 | Production Quick Task creation; remains blocked by XY-1304. | | XY-1304 | Experiment thread creation and positive observation acquisition, plus live routing enablement. Lossy or paginated evidence cannot authorize negative `Present`, `Complete`, or context-free `Absent` conclusions. | | XY-1345 | Accepted exact-command authority and isolated PostgreSQL 18 prototype only; no production migration or Rust command path. | -| XY-1346 | The next PostgreSQL migration, expected V9: separate exact receipts plus RoleProfile bootstrap/update. Starts only after XY-1345 lands. | +| XY-1346 | PostgreSQL V9: separate exact receipts plus immutable global RoleProfile bootstrap/update. Starts only after XY-1345 lands. | | XY-1337 | Re-bounded RuntimeSession snapshot creation/transition migration, expected V10 after XY-1346. It does not own exact-receipt or RoleProfile redesign. | XY-1336 is an upstream-blocked tracking issue outside the M2 critical path. A host file, diff --git a/scripts/vnext/postgres_store_test.py b/scripts/vnext/postgres_store_test.py index 8d160b7e..6648ad8a 100644 --- a/scripts/vnext/postgres_store_test.py +++ b/scripts/vnext/postgres_store_test.py @@ -37,6 +37,13 @@ MISSING_EXTENSION_DATABASE = "decodex_xy1307_missing_extension" V8_EMPTY_DATABASE = "decodex_xy1274_v8_empty" V8_LOCK_DATABASE = "decodex_xy1274_v8_lock" +ROLE_PROFILE_CONCURRENCY_DATABASE = "decodex_xy1346_concurrency" +ROLE_PROFILE_UPGRADE_DATABASE = "decodex_xy1346_upgrade" +ROLE_PROFILE_ROLLBACK_DATABASE = "decodex_xy1346_rollback" +ROLE_PROFILE_RETRY_DATABASE = "decodex_xy1346_retry" +ROLE_PROFILE_CRASH_DATABASE = "decodex_xy1346_crash" +ROLE_PROFILE_RESTORE_SOURCE_DATABASE = "decodex_xy1346_restore_source" +ROLE_PROFILE_RESTORE_DATABASE = "decodex_xy1346_restore" MIGRATION_ROLE = "decodex_migration" RUNTIME_ROLE = "decodex_runtime" FUNCTION_OWNER_ROLE = "decodex_function_owner" @@ -98,6 +105,8 @@ "decodex.create_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.text,pg_catalog._text,pg_catalog._text,pg_catalog.int8,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.text)", "decodex.transition_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.int8,decodex.objective_state,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.text)", "decodex.achieve_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text)", + "decodex.bootstrap_role_profiles_exact(pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "decodex.update_role_profile_exact(pg_catalog.text,pg_catalog.text,decodex.role_profile_role,pg_catalog.int8,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", ) TRIGGER_ONLY_SIGNATURES = ( "decodex.enforce_lease_operation_time()", @@ -126,6 +135,14 @@ "decodex.enforce_objective_state()", "decodex.forbid_objective_evidence_mutation()", "decodex.enforce_objective_completion_coherence()", + "decodex.enforce_exact_receipt_completion()", + "decodex.forbid_exact_receipt_rewrite()", + "decodex.forbid_exact_receipt_truncate()", + "decodex.enforce_complete_role_profile_set()", + "decodex.forbid_role_profile_identity_rewrite()", + "decodex.forbid_role_profile_revision_mutation()", + "decodex.forbid_role_profile_truncate()", + "decodex.enforce_role_profile_event_namespace()", ) RUNTIME_TYPE_NAMES = ( "decodex.account_state", @@ -151,6 +168,7 @@ "decodex.objective_state", "decodex.quota_window_class", "decodex.observation_confidence", + "decodex.role_profile_role", ) @@ -580,6 +598,142 @@ def dump_schema_manifest(path: Path, env: dict[str, str]) -> str: ) +def run_role_profile_test(test: str, env: dict[str, str]) -> str: + return run( + [ + "cargo", "nextest", "run", "-p", "decodex-postgres", "--features", + "test-support", "--test", "postgres_store", "--run-ignored", "all", "--", + f"role_profiles::{test}", "--exact", + ], + env, + ) + + +def prepare_role_profile_database( + database: str, socket_dir: Path, port: int, env: dict[str, str] +) -> None: + create_database(database, env) + set_contract_urls(env, socket_dir, port, database, RUNTIME_ROLE) + run_migration(env) + provision_runtime(database, RUNTIME_ROLE, env) + + +def run_role_profile_crash_recovery( + data_dir: Path, + log_path: Path, + socket_dir: Path, + port: int, + work: Path, + env: dict[str, str], +) -> str: + sync = work / "role-profile-crash-recovery" + sync.mkdir() + test_env = env.copy() + test_env["DECODEX_ROLE_PROFILE_RESTART_SYNC"] = str(sync) + process = subprocess.Popen( + [ + "cargo", "nextest", "run", "-p", "decodex-postgres", "--features", + "test-support", "--test", "postgres_store", "--run-ignored", "all", "--", + "role_profiles::postgres_exact_role_profile_crash_recovery", "--exact", + ], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=test_env, + cwd=REPO_ROOT, + ) + try: + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + if (sync / "ready").exists(): + break + if process.poll() is not None: + stdout, stderr = process.communicate() + raise TestFailure( + f"RoleProfile crash fixture exited early\n{stdout}\n{stderr}" + ) + time.sleep(0.02) + else: + raise TestFailure("RoleProfile crash fixture did not reach its lock barrier") + + run(["pg_ctl", "-D", str(data_dir), "-m", "immediate", "-w", "stop"], env) + run( + [ + "pg_ctl", "-D", str(data_dir), "-l", str(log_path), "-o", + f"-k {socket_dir} -p {port} -h '' -F", "-w", "start", + ], + env, + ) + (sync / "restarted").write_text("restarted", encoding="utf-8") + stdout, stderr = process.communicate(timeout=60) + if process.returncode != 0: + raise TestFailure(f"RoleProfile crash/recovery failed\n{stdout}\n{stderr}") + return stdout.strip() or stderr.strip() + finally: + if process.poll() is None: + process.terminate() + process.wait(timeout=10) + + +def run_role_profile_final_gate_contracts( + data_dir: Path, + log_path: Path, + socket_dir: Path, + port: int, + work: Path, + env: dict[str, str], +) -> str: + outputs: list[str] = [] + for database, test in ( + (ROLE_PROFILE_CONCURRENCY_DATABASE, "postgres_exact_role_profile_concurrency"), + (ROLE_PROFILE_ROLLBACK_DATABASE, "postgres_exact_role_profile_atomic_rollback"), + (ROLE_PROFILE_RETRY_DATABASE, "postgres_exact_role_profile_retry_convergence"), + ): + prepare_role_profile_database(database, socket_dir, port, env) + outputs.append(run_role_profile_test(test, env)) + + create_database(ROLE_PROFILE_UPGRADE_DATABASE, env) + set_contract_urls( + env, socket_dir, port, ROLE_PROFILE_UPGRADE_DATABASE, RUNTIME_ROLE + ) + outputs.append(run_role_profile_test("postgres_v8_to_v9_role_profile_upgrade", env)) + + prepare_role_profile_database(ROLE_PROFILE_CRASH_DATABASE, socket_dir, port, env) + outputs.append( + run_role_profile_crash_recovery( + data_dir, log_path, socket_dir, port, work, env + ) + ) + + prepare_role_profile_database( + ROLE_PROFILE_RESTORE_SOURCE_DATABASE, socket_dir, port, env + ) + outputs.append(run_role_profile_test("postgres_exact_role_profile_commands", env)) + restore_dump = work / "xy1346-role-profiles.dump" + run( + [ + "pg_dump", "-Fc", "-f", str(restore_dump), + ROLE_PROFILE_RESTORE_SOURCE_DATABASE, + ], + env, + ) + create_database(ROLE_PROFILE_RESTORE_DATABASE, env) + run( + [ + "pg_restore", "--exit-on-error", "-d", ROLE_PROFILE_RESTORE_DATABASE, + str(restore_dump), + ], + env, + ) + set_contract_urls( + env, socket_dir, port, ROLE_PROFILE_RESTORE_DATABASE, RUNTIME_ROLE + ) + outputs.append(run_role_profile_test("postgres_exact_role_profile_restore", env)) + set_contract_urls(env, socket_dir, port, DATABASE, RUNTIME_ROLE) + + return "\n".join(outputs) + + def quota_authority_snapshot(database: str, env: dict[str, str]) -> str: return psql( database, @@ -812,6 +966,9 @@ def main() -> int: set_contract_urls(env, socket_dir, port, DATABASE, RUNTIME_ROLE) migration_output = run_migration(env) provision_runtime(DATABASE, RUNTIME_ROLE, env) + role_profile_output = run_role_profile_final_gate_contracts( + data_dir, log_path, socket_dir, port, work, env + ) v8_boundary_output = run_v8_migration_boundary_contracts(env, socket_dir, port) set_contract_urls(env, socket_dir, port, DATABASE, RUNTIME_ROLE) restart_output = run_blob_session_restart_contract( @@ -1258,7 +1415,7 @@ def main() -> int: f"AND (SELECT count(*) FROM pg_catalog.pg_proc AS inventory " f"JOIN pg_catalog.pg_namespace AS inventory_namespace " f"ON inventory_namespace.oid = inventory.pronamespace " - f"WHERE inventory_namespace.nspname = 'decodex') = 59", + f"WHERE inventory_namespace.nspname = 'decodex') = 73", env, ) != "t|t|t|t|t|t|t|t": raise TestFailure("additional privileged-function fixture is vacuous") @@ -2170,6 +2327,7 @@ def main() -> int: authority_drift_outputs.append(output) assert_postgres_logs_redact((log_path,), canary_markers) print(migration_output) + print(role_profile_output) print(restart_output) print(v8_boundary_output) print(contract_output) diff --git a/tests/scripts/test_exact_role_profile_authority.py b/tests/scripts/test_exact_role_profile_authority.py new file mode 100644 index 00000000..175e60df --- /dev/null +++ b/tests/scripts/test_exact_role_profile_authority.py @@ -0,0 +1,90 @@ +"""Static regressions for the XY-1346 V9 exact RoleProfile boundary.""" + +from pathlib import Path +import re +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +MIGRATION = ROOT / "crates/decodex-postgres/migrations/V9__exact_role_profiles.sql" +RUST_API = ROOT / "crates/decodex-postgres/src/role_profiles.rs" +AUTHORITY = ROOT / "crates/decodex-postgres/src/authority.rs" + + +class ExactRoleProfileAuthorityTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.migration = MIGRATION.read_text(encoding="utf-8") + cls.rust_api = RUST_API.read_text(encoding="utf-8") + cls.authority = AUTHORITY.read_text(encoding="utf-8") + + def test_v9_owns_separate_receipts_and_exact_four_role_set(self) -> None: + self.assertIn("CREATE TABLE decodex.exact_command_receipts", self.migration) + self.assertIn("PRIMARY KEY (protocol_version, idempotency_key)", self.migration) + self.assertIn("DEFERRABLE INITIALLY DEFERRED", self.migration) + self.assertIn("exact_command_receipts_no_credentials", self.migration) + self.assertNotIn("ALTER TABLE decodex.command_receipts", self.migration) + enum = re.search( + r"CREATE TYPE decodex\.role_profile_role AS ENUM \((.*?)\);", + self.migration, + re.DOTALL, + ) + self.assertIsNotNone(enum) + self.assertEqual( + re.findall(r"'([^']+)'", enum.group(1)), + ["advisor", "lead", "task", "reviewer"], + ) + + def test_command_signatures_have_complete_typed_inputs_only(self) -> None: + for name in ("bootstrap_role_profiles_exact", "update_role_profile_exact"): + match = re.search( + rf"CREATE FUNCTION decodex\.{name}\((.*?)\) RETURNS bytea", + self.migration, + re.DOTALL, + ) + self.assertIsNotNone(match) + signature = match.group(1) + self.assertIn("p_protocol text", signature) + self.assertIn("p_idempotency_key text", signature) + for forbidden in ("request_digest", "claim_token", "lease", "pending"): + with self.subTest(name=name, forbidden=forbidden): + self.assertNotIn(forbidden, signature) + + def test_runtime_api_has_no_receipt_table_access(self) -> None: + self.assertNotIn("exact_command_receipts", self.rust_api) + self.assertNotRegex( + self.rust_api, + r"(?i)\b(?:insert|update|delete)\s+(?:into|from)?\s*decodex\.", + ) + self.assertIn("bootstrap_role_profiles_exact", self.rust_api) + self.assertIn("update_role_profile_exact", self.rust_api) + + def test_stored_response_and_outcomes_remain_distinct(self) -> None: + self.assertIn("existing_request <> request_value", self.migration) + self.assertIn("RETURN existing_response", self.migration) + self.assertIn("completed_rejected", self.migration) + self.assertIn("ERRCODE = 'DX001'", self.migration) + self.assertIn("is_retryable_exact_database_error", self.rust_api) + self.assertIn("serde_json::from_slice(response)", self.rust_api) + + def test_authority_closes_new_relations_and_only_command_entrypoints(self) -> None: + for relation in ( + "exact_command_receipts", + "role_profiles", + "role_profile_revisions", + ): + with self.subTest(relation=relation): + self.assertIn( + f"('{relation}', false, false, false, false)", self.authority + ) + runtime = self.authority.split("const RUNTIME_EXECUTE_FUNCTIONS", 1)[1].split( + "];", 1 + )[0] + self.assertIn("bootstrap_role_profiles_exact", runtime) + self.assertIn("update_role_profile_exact", runtime) + self.assertNotIn("complete_exact_role_profile_rejection", runtime) + self.assertIn("ROLE_PROFILE_MIGRATION", self.authority) + + +if __name__ == "__main__": + unittest.main() From 478f11c819cd677a13e83cac3de31c9608956036 Mon Sep 17 00:00:00 2001 From: XV Date: Fri, 17 Jul 2026 02:07:15 -0400 Subject: [PATCH 4/7] {"schema":"decodex/commit/2","change":"XY-1337 persists authoritative RuntimeSession snapshots through exact PostgreSQL create and transition commands with V10 authority, Rust APIs, fixtures, and aligned OpenWiki.","authority":"manual","impact":"compatible"} --- .../V10__runtime_session_snapshots.sql | 894 ++++++++++++++++++ crates/decodex-postgres/src/authority.rs | 134 ++- crates/decodex-postgres/src/conversations.rs | 374 +------- crates/decodex-postgres/src/exact_commands.rs | 81 ++ crates/decodex-postgres/src/lib.rs | 43 +- crates/decodex-postgres/src/migrations.rs | 11 +- crates/decodex-postgres/src/role_profiles.rs | 89 +- .../decodex-postgres/src/runtime_sessions.rs | 894 ++++++++++++++++++ .../decodex-postgres/tests/postgres_store.rs | 463 +++++---- .../tests/postgres_store/role_profiles.rs | 2 +- .../tests/postgres_store/runtime_sessions.rs | 853 +++++++++++++++++ openwiki/architecture/runtime-architecture.md | 41 +- openwiki/decisions/vnext-authority.md | 11 + .../operations/commands-and-validation.md | 18 +- openwiki/quickstart.md | 9 +- openwiki/specs/vnext-gates.md | 10 + scripts/vnext/postgres_store_test.py | 175 +++- .../scripts/test_runtime_session_authority.py | 198 ++++ 18 files changed, 3573 insertions(+), 727 deletions(-) create mode 100644 crates/decodex-postgres/migrations/V10__runtime_session_snapshots.sql create mode 100644 crates/decodex-postgres/src/exact_commands.rs create mode 100644 crates/decodex-postgres/src/runtime_sessions.rs create mode 100644 crates/decodex-postgres/tests/postgres_store/runtime_sessions.rs create mode 100644 tests/scripts/test_runtime_session_authority.py diff --git a/crates/decodex-postgres/migrations/V10__runtime_session_snapshots.sql b/crates/decodex-postgres/migrations/V10__runtime_session_snapshots.sql new file mode 100644 index 00000000..391ee5f8 --- /dev/null +++ b/crates/decodex-postgres/migrations/V10__runtime_session_snapshots.sql @@ -0,0 +1,894 @@ +-- XY-1337 authoritative RuntimeSession snapshots and exact commands. +-- V10 is an atomic zero-state cutover: no V3 caller-authored session state is accepted. +LOCK TABLE decodex.command_receipts, decodex.exact_command_receipts, + decodex.conversations, decodex.role_profiles, decodex.role_profile_revisions, + decodex.profile_snapshots, decodex.account_snapshots, decodex.runtime_sessions, + decodex.turns, decodex.activity, decodex.outbox IN ACCESS EXCLUSIVE MODE; + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM decodex.profile_snapshots) + OR EXISTS (SELECT 1 FROM decodex.account_snapshots) + OR EXISTS (SELECT 1 FROM decodex.runtime_sessions) + OR EXISTS (SELECT 1 FROM decodex.turns) + OR EXISTS ( + SELECT 1 FROM decodex.command_receipts + WHERE operation IN ('create_runtime_session', 'transition_runtime_session') + ) + OR EXISTS ( + SELECT 1 FROM decodex.exact_command_receipts + WHERE request_envelope->>'operation' + IN ('create_runtime_session', 'transition_runtime_session') + ) + OR EXISTS ( + SELECT 1 FROM decodex.activity + WHERE aggregate_kind = 'runtime_session' + OR event_kind IN ('runtime_session_recorded', 'runtime_session_created', + 'runtime_session_transitioned') + OR pg_catalog.jsonb_path_exists(payload, '$.** ? ( + @.aggregate_kind == "runtime_session" || + @.kind == "runtime_session" || + @.event_kind == "runtime_session_recorded" || + @.event_kind == "runtime_session_created" || + @.event_kind == "runtime_session_transitioned" || + exists(@.runtime_session) || exists(@.runtime_session_id) || + exists(@.profile_snapshot) || exists(@.profile_snapshot_id) || + exists(@.account_snapshot) || exists(@.account_snapshot_id) + )') + ) + OR EXISTS ( + SELECT 1 FROM decodex.outbox AS message + WHERE message.aggregate_kind = 'runtime_session' + OR pg_catalog.jsonb_path_exists(message.payload, '$.** ? ( + @.aggregate_kind == "runtime_session" || + @.kind == "runtime_session" || + @.event_kind == "runtime_session_recorded" || + @.event_kind == "runtime_session_created" || + @.event_kind == "runtime_session_transitioned" || + exists(@.runtime_session) || exists(@.runtime_session_id) || + exists(@.profile_snapshot) || exists(@.profile_snapshot_id) || + exists(@.account_snapshot) || exists(@.account_snapshot_id) + )') + OR EXISTS ( + SELECT 1 + FROM pg_catalog.jsonb_path_query( + message.payload, '$.**.activity_sequence' + ) AS link(value) + JOIN decodex.activity AS activity ON activity.sequence = CASE + WHEN pg_catalog.jsonb_typeof(link.value) IN ('number', 'string') + AND link.value #>> '{}' ~ '^[0-9]+$' + THEN (link.value #>> '{}')::bigint + END + WHERE ( + activity.aggregate_kind = 'runtime_session' + OR activity.event_kind IN ('runtime_session_recorded', + 'runtime_session_created', 'runtime_session_transitioned') + OR pg_catalog.jsonb_path_exists(activity.payload, '$.** ? ( + @.aggregate_kind == "runtime_session" || + @.kind == "runtime_session" || + @.event_kind == "runtime_session_recorded" || + @.event_kind == "runtime_session_created" || + @.event_kind == "runtime_session_transitioned" || + exists(@.runtime_session) || exists(@.runtime_session_id) || + exists(@.profile_snapshot) || exists(@.profile_snapshot_id) || + exists(@.account_snapshot) || exists(@.account_snapshot_id) + )') + ) + ) + ) + THEN + RAISE EXCEPTION 'V10 RuntimeSession authority requires zero incompatible state' + USING ERRCODE = '55000', CONSTRAINT = 'runtime_session_v10_zero_state'; + END IF; +END +$$; + +ALTER TABLE decodex.profile_snapshots + DROP CONSTRAINT profile_snapshots_no_credentials, + DROP CONSTRAINT profile_snapshots_role_check, + ALTER COLUMN role TYPE decodex.role_profile_role + USING role::decodex.role_profile_role, + ADD COLUMN instructions text NOT NULL, + ADD COLUMN provenance text, + ADD CONSTRAINT profile_snapshots_source_identity CHECK (source_profile_id = role::text), + ADD CONSTRAINT profile_snapshots_instruction_digest CHECK ( + instructions_digest = pg_catalog.encode( + public.digest(pg_catalog.convert_to(instructions, 'UTF8'), 'sha256'), 'hex' + ) + ), + ADD CONSTRAINT profile_snapshots_configuration CHECK ( + decodex.is_role_profile_configuration( + model, reasoning_effort, service_tier, instructions, provenance + ) + ), + ADD CONSTRAINT profile_snapshots_source_revision_fk + FOREIGN KEY (role, source_revision) + REFERENCES decodex.role_profile_revisions(role, revision) ON DELETE RESTRICT; + +ALTER TABLE decodex.account_snapshots + DROP CONSTRAINT account_snapshots_no_credentials, + DROP CONSTRAINT account_snapshots_source_account_id_check, + DROP CONSTRAINT account_snapshots_observed_state_check, + ALTER COLUMN source_account_id TYPE uuid USING source_account_id::uuid, + ALTER COLUMN observed_state TYPE decodex.account_state + USING observed_state::decodex.account_state, + ADD CONSTRAINT account_snapshots_facts CHECK ( + pg_catalog.octet_length(display_label) BETWEEN 1 AND 128 + AND NOT decodex.has_credential_material(display_label) + ); + +ALTER TABLE decodex.runtime_sessions + DROP CONSTRAINT runtime_sessions_codex_thread_id_check, + DROP CONSTRAINT runtime_sessions_no_credentials, + ALTER COLUMN codex_thread_id TYPE uuid USING codex_thread_id::uuid, + ADD CONSTRAINT runtime_sessions_no_credentials CHECK ( + (codex_thread_id IS NULL + OR NOT decodex.has_credential_material(codex_thread_id::text)) + AND (last_known_turn_id IS NULL + OR NOT decodex.has_credential_material(last_known_turn_id)) + ), + ADD CONSTRAINT runtime_sessions_initial_turn_null CHECK ( + revision <> 1 OR last_known_turn_id IS NULL + ); + +-- Preserve readback while atomically removing every previously granted direct writer. +-- V9 readiness proves there are no column grants or grant options before this cutover. +DO $$ +DECLARE target record; +BEGIN + FOR target IN + SELECT DISTINCT class.relname, role.rolname + FROM pg_catalog.pg_class AS class + JOIN pg_catalog.pg_namespace AS namespace ON namespace.oid = class.relnamespace + CROSS JOIN LATERAL pg_catalog.aclexplode( + COALESCE(class.relacl, pg_catalog.acldefault('r', class.relowner)) + ) AS privilege + JOIN pg_catalog.pg_roles AS role ON role.oid = privilege.grantee + WHERE namespace.nspname = 'decodex' + AND class.relname IN ('profile_snapshots', 'account_snapshots', 'runtime_sessions') + AND privilege.grantee <> class.relowner + AND privilege.privilege_type IN ( + 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'MAINTAIN' + ) + LOOP + EXECUTE pg_catalog.format( + 'REVOKE INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, MAINTAIN ON TABLE decodex.%I FROM %I', + target.relname, target.rolname + ); + END LOOP; +END +$$; + +CREATE FUNCTION decodex.enforce_runtime_session_command_owner() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +DECLARE owner_name name; +BEGIN + SELECT pg_catalog.pg_get_userbyid(class.relowner) INTO owner_name + FROM pg_catalog.pg_class AS class WHERE class.oid = TG_RELID; + + IF current_user::name <> owner_name THEN + RAISE EXCEPTION 'RuntimeSession state is command-owned' + USING ERRCODE = '42501', CONSTRAINT = 'runtime_session_command_owner'; + END IF; + + RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END; +END +$$; + +CREATE TRIGGER profile_snapshots_command_owner +BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON decodex.profile_snapshots +FOR EACH STATEMENT EXECUTE FUNCTION decodex.enforce_runtime_session_command_owner(); +CREATE TRIGGER account_snapshots_command_owner +BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON decodex.account_snapshots +FOR EACH STATEMENT EXECUTE FUNCTION decodex.enforce_runtime_session_command_owner(); +CREATE TRIGGER runtime_sessions_command_owner +BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON decodex.runtime_sessions +FOR EACH STATEMENT EXECUTE FUNCTION decodex.enforce_runtime_session_command_owner(); + +CREATE FUNCTION decodex.forbid_runtime_snapshot_mutation() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +BEGIN + RAISE EXCEPTION 'RuntimeSession snapshots are immutable' + USING ERRCODE = '23514', CONSTRAINT = 'runtime_session_snapshots_immutable'; +END +$$; + +CREATE TRIGGER profile_snapshots_immutable +BEFORE UPDATE OR DELETE ON decodex.profile_snapshots +FOR EACH ROW EXECUTE FUNCTION decodex.forbid_runtime_snapshot_mutation(); +CREATE TRIGGER account_snapshots_immutable +BEFORE UPDATE OR DELETE ON decodex.account_snapshots +FOR EACH ROW EXECUTE FUNCTION decodex.forbid_runtime_snapshot_mutation(); + +CREATE FUNCTION decodex.enforce_runtime_session_event_namespace() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +DECLARE owner_name name; +DECLARE linked_runtime_session boolean := false; +BEGIN + SELECT pg_catalog.pg_get_userbyid(class.relowner) INTO owner_name + FROM pg_catalog.pg_class AS class WHERE class.oid = TG_RELID; + + IF TG_TABLE_NAME = 'activity' THEN + linked_runtime_session := NEW.aggregate_kind = 'runtime_session' + OR NEW.event_kind IN ('runtime_session_recorded', 'runtime_session_created', + 'runtime_session_transitioned') + OR pg_catalog.jsonb_path_exists(NEW.payload, '$.** ? ( + @.aggregate_kind == "runtime_session" || + @.kind == "runtime_session" || + @.event_kind == "runtime_session_recorded" || + @.event_kind == "runtime_session_created" || + @.event_kind == "runtime_session_transitioned" || + exists(@.runtime_session) || exists(@.runtime_session_id) || + exists(@.profile_snapshot) || exists(@.profile_snapshot_id) || + exists(@.account_snapshot) || exists(@.account_snapshot_id) + )'); + IF TG_OP = 'UPDATE' THEN + linked_runtime_session := linked_runtime_session + OR OLD.aggregate_kind = 'runtime_session' + OR OLD.event_kind IN ('runtime_session_recorded', 'runtime_session_created', + 'runtime_session_transitioned') + OR pg_catalog.jsonb_path_exists(OLD.payload, '$.** ? ( + @.aggregate_kind == "runtime_session" || + @.kind == "runtime_session" || + @.event_kind == "runtime_session_recorded" || + @.event_kind == "runtime_session_created" || + @.event_kind == "runtime_session_transitioned" || + exists(@.runtime_session) || exists(@.runtime_session_id) || + exists(@.profile_snapshot) || exists(@.profile_snapshot_id) || + exists(@.account_snapshot) || exists(@.account_snapshot_id) + )'); + END IF; + ELSE + linked_runtime_session := NEW.aggregate_kind = 'runtime_session' + OR pg_catalog.jsonb_path_exists(NEW.payload, '$.** ? ( + @.aggregate_kind == "runtime_session" || + @.kind == "runtime_session" || + @.event_kind == "runtime_session_recorded" || + @.event_kind == "runtime_session_created" || + @.event_kind == "runtime_session_transitioned" || + exists(@.runtime_session) || exists(@.runtime_session_id) || + exists(@.profile_snapshot) || exists(@.profile_snapshot_id) || + exists(@.account_snapshot) || exists(@.account_snapshot_id) + )') + OR EXISTS ( + SELECT 1 + FROM pg_catalog.jsonb_path_query( + NEW.payload, '$.**.activity_sequence' + ) AS link(value) + JOIN decodex.activity AS activity ON activity.sequence = CASE + WHEN pg_catalog.jsonb_typeof(link.value) IN ('number', 'string') + AND link.value #>> '{}' ~ '^[0-9]+$' + THEN (link.value #>> '{}')::bigint + END + WHERE (activity.aggregate_kind = 'runtime_session' + OR activity.event_kind IN ('runtime_session_recorded', + 'runtime_session_created', 'runtime_session_transitioned') + OR pg_catalog.jsonb_path_exists(activity.payload, '$.** ? ( + @.aggregate_kind == "runtime_session" || @.kind == "runtime_session" || + @.event_kind == "runtime_session_recorded" || + @.event_kind == "runtime_session_created" || + @.event_kind == "runtime_session_transitioned" || + exists(@.runtime_session) || exists(@.runtime_session_id) || + exists(@.profile_snapshot) || exists(@.profile_snapshot_id) || + exists(@.account_snapshot) || exists(@.account_snapshot_id) + )')) + ); + IF TG_OP = 'UPDATE' THEN + linked_runtime_session := linked_runtime_session + OR OLD.aggregate_kind = 'runtime_session' + OR pg_catalog.jsonb_path_exists(OLD.payload, '$.** ? ( + @.aggregate_kind == "runtime_session" || + @.kind == "runtime_session" || + @.event_kind == "runtime_session_recorded" || + @.event_kind == "runtime_session_created" || + @.event_kind == "runtime_session_transitioned" || + exists(@.runtime_session) || exists(@.runtime_session_id) || + exists(@.profile_snapshot) || exists(@.profile_snapshot_id) || + exists(@.account_snapshot) || exists(@.account_snapshot_id) + )') + OR EXISTS ( + SELECT 1 + FROM pg_catalog.jsonb_path_query( + OLD.payload, '$.**.activity_sequence' + ) AS link(value) + JOIN decodex.activity AS activity ON activity.sequence = CASE + WHEN pg_catalog.jsonb_typeof(link.value) IN ('number', 'string') + AND link.value #>> '{}' ~ '^[0-9]+$' + THEN (link.value #>> '{}')::bigint + END + WHERE (activity.aggregate_kind = 'runtime_session' + OR activity.event_kind IN ('runtime_session_recorded', + 'runtime_session_created', 'runtime_session_transitioned') + OR pg_catalog.jsonb_path_exists(activity.payload, '$.** ? ( + @.aggregate_kind == "runtime_session" || @.kind == "runtime_session" || + @.event_kind == "runtime_session_recorded" || + @.event_kind == "runtime_session_created" || + @.event_kind == "runtime_session_transitioned" || + exists(@.runtime_session) || exists(@.runtime_session_id) || + exists(@.profile_snapshot) || exists(@.profile_snapshot_id) || + exists(@.account_snapshot) || exists(@.account_snapshot_id) + )') + ) + ); + END IF; + END IF; + + IF linked_runtime_session AND current_user::name <> owner_name THEN + IF TG_TABLE_NAME = 'activity' OR TG_OP = 'INSERT' THEN + RAISE EXCEPTION 'RuntimeSession activity/outbox namespace is command-owned' + USING ERRCODE = '42501', CONSTRAINT = 'runtime_session_event_namespace'; + ELSIF NEW.id IS DISTINCT FROM OLD.id + OR NEW.effect_key IS DISTINCT FROM OLD.effect_key + OR NEW.aggregate_kind IS DISTINCT FROM OLD.aggregate_kind + OR NEW.aggregate_id IS DISTINCT FROM OLD.aggregate_id + OR NEW.aggregate_revision IS DISTINCT FROM OLD.aggregate_revision + OR NEW.payload IS DISTINCT FROM OLD.payload + OR NEW.created_at IS DISTINCT FROM OLD.created_at + THEN + RAISE EXCEPTION 'RuntimeSession outbox authority fields are command-owned' + USING ERRCODE = '42501', CONSTRAINT = 'runtime_session_event_namespace'; + END IF; + END IF; + + RETURN NEW; +EXCEPTION + WHEN invalid_text_representation THEN + RAISE EXCEPTION 'RuntimeSession activity/outbox link is malformed' + USING ERRCODE = '42501', CONSTRAINT = 'runtime_session_event_namespace'; +END +$$; + +CREATE TRIGGER activity_runtime_session_namespace +BEFORE INSERT OR UPDATE ON decodex.activity +FOR EACH ROW EXECUTE FUNCTION decodex.enforce_runtime_session_event_namespace(); +CREATE TRIGGER outbox_runtime_session_namespace +BEFORE INSERT OR UPDATE ON decodex.outbox +FOR EACH ROW EXECUTE FUNCTION decodex.enforce_runtime_session_event_namespace(); + +CREATE FUNCTION decodex.build_runtime_session_create_request( + p_protocol text, p_session_id uuid, p_conversation_id uuid, + p_role decodex.role_profile_role, p_account_snapshot_id uuid, + p_source_account_id uuid, p_display_label text, + p_observed_state decodex.account_state, p_account_source_revision bigint, + p_codex_thread_id uuid, p_initial_state decodex.runtime_session_state +) RETURNS jsonb +LANGUAGE sql +IMMUTABLE +SET search_path = pg_catalog, decodex +AS $$ +SELECT pg_catalog.jsonb_build_object( + 'protocol_version', p_protocol, 'operation', 'create_runtime_session', + 'runtime_session_id', p_session_id, 'conversation_id', p_conversation_id, + 'role', p_role, 'account_snapshot_id', p_account_snapshot_id, + 'source_account_id', p_source_account_id, 'display_label', p_display_label, + 'observed_state', p_observed_state, + 'account_source_revision', p_account_source_revision, + 'codex_thread_id', p_codex_thread_id, 'initial_state', p_initial_state +) +$$; + +CREATE FUNCTION decodex.build_runtime_session_transition_request( + p_protocol text, p_session_id uuid, p_expected_revision bigint, + p_target_state decodex.runtime_session_state +) RETURNS jsonb +LANGUAGE sql +IMMUTABLE +SET search_path = pg_catalog, decodex +AS $$ +SELECT pg_catalog.jsonb_build_object( + 'protocol_version', p_protocol, 'operation', 'transition_runtime_session', + 'runtime_session_id', p_session_id, 'expected_revision', p_expected_revision, + 'target_state', p_target_state +) +$$; + +CREATE FUNCTION decodex.complete_exact_runtime_session_rejection( + p_protocol text, p_idempotency_key text, p_code text +) RETURNS bytea +LANGUAGE plpgsql +SET search_path = pg_catalog, decodex +AS $$ +DECLARE effect_value jsonb; +DECLARE response_value bytea; +DECLARE request_value jsonb; +BEGIN + SELECT request_envelope INTO STRICT request_value + FROM decodex.exact_command_receipts + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key; + effect_value := pg_catalog.jsonb_build_object( + 'changed', false, 'code', p_code, 'request', request_value + ); + response_value := pg_catalog.convert_to(pg_catalog.jsonb_build_object( + 'classification', 'stable_domain_rejection', 'code', p_code, + 'effect', effect_value + )::text, 'UTF8'); + + UPDATE decodex.exact_command_receipts + SET receipt_state = 'completed_rejected', outcome_class = 'stable_domain_rejection', + effect_envelope = effect_value, response_bytes = response_value, + completed_at = pg_catalog.clock_timestamp() + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key; + + RETURN response_value; +END +$$; + +CREATE FUNCTION decodex.create_runtime_session_exact( + p_protocol text, p_idempotency_key text, + p_session_id uuid, p_conversation_id uuid, p_role decodex.role_profile_role, + p_account_snapshot_id uuid, p_source_account_id uuid, p_display_label text, + p_observed_state decodex.account_state, p_account_source_revision bigint, + p_codex_thread_id uuid, p_initial_state decodex.runtime_session_state +) RETURNS bytea +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = pg_catalog, decodex +AS $$ +DECLARE request_value jsonb; +DECLARE existing_request jsonb; +DECLARE existing_response bytea; +DECLARE inserted_count integer; +DECLARE conversation_status decodex.conversation_status; +DECLARE identity_lock bigint; +DECLARE thread_lock bigint; +DECLARE profile_snapshot_id uuid; +DECLARE profile_value jsonb; +DECLARE account_value jsonb; +DECLARE session_value jsonb; +DECLARE activity_sequence bigint; +DECLARE activity_aggregate_kind text; +DECLARE activity_aggregate_id text; +DECLARE activity_revision bigint; +DECLARE activity_event_kind text; +DECLARE activity_payload jsonb; +DECLARE outbox_id bigint; +DECLARE outbox_effect_key text; +DECLARE outbox_aggregate_kind text; +DECLARE outbox_aggregate_id text; +DECLARE outbox_aggregate_revision bigint; +DECLARE outbox_payload jsonb; +DECLARE effect_value jsonb; +DECLARE response_value bytea; +BEGIN + IF p_protocol IS NULL OR p_idempotency_key IS NULL OR p_session_id IS NULL + OR p_conversation_id IS NULL OR p_role IS NULL OR p_account_snapshot_id IS NULL + OR p_source_account_id IS NULL OR p_display_label IS NULL + OR p_observed_state IS NULL OR p_account_source_revision IS NULL + OR p_initial_state IS NULL + THEN + RAISE EXCEPTION 'exact RuntimeSession creation is incomplete' USING ERRCODE = '22004'; + END IF; + IF pg_catalog.octet_length(p_protocol) NOT BETWEEN 1 AND 64 + OR p_protocol COLLATE pg_catalog."C" !~ '^[a-z0-9][a-z0-9._/-]{0,63}$' + OR pg_catalog.octet_length(p_idempotency_key) NOT BETWEEN 1 AND 256 + OR decodex.has_credential_material(p_idempotency_key) + THEN + RAISE EXCEPTION 'exact RuntimeSession command identity is invalid' USING ERRCODE = '22023'; + END IF; + + request_value := decodex.build_runtime_session_create_request( + p_protocol, p_session_id, p_conversation_id, p_role, + p_account_snapshot_id, p_source_account_id, p_display_label, + p_observed_state, p_account_source_revision, p_codex_thread_id, p_initial_state + ); + INSERT INTO decodex.exact_command_receipts( + protocol_version, idempotency_key, request_envelope, request_digest, receipt_state + ) VALUES ( + p_protocol, p_idempotency_key, request_value, + public.digest(pg_catalog.convert_to(request_value::text, 'UTF8'), 'sha256'), 'executing' + ) ON CONFLICT (protocol_version, idempotency_key) DO NOTHING; + GET DIAGNOSTICS inserted_count = ROW_COUNT; + + IF inserted_count = 0 THEN + SELECT request_envelope, response_bytes INTO existing_request, existing_response + FROM decodex.exact_command_receipts + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key + FOR UPDATE; + IF existing_request <> request_value THEN + RAISE EXCEPTION 'exact idempotency conflict' USING ERRCODE = 'DX001'; + END IF; + IF existing_response IS NULL THEN + RAISE EXCEPTION 'incomplete exact receipt is not replayable' USING ERRCODE = 'DX002'; + END IF; + RETURN existing_response; + END IF; + + IF p_initial_state NOT IN ('starting', 'active') THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'illegal_transition' + ); + END IF; + IF p_account_source_revision < 1 + OR pg_catalog.octet_length(p_display_label) NOT BETWEEN 1 AND 128 + OR decodex.has_credential_material(p_display_label) + THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'invalid_account_snapshot' + ); + END IF; + + -- Match the statement-level hierarchy trigger order: acquire the outer coordinator + -- before selecting or locking a Conversation or RuntimeSession tuple. + PERFORM pg_catalog.pg_advisory_xact_lock(1271); + + -- The row lock is both the Conversation eligibility proof and the canonical create order. + SELECT status INTO conversation_status FROM decodex.conversations + WHERE conversation_id = p_conversation_id FOR UPDATE; + IF NOT FOUND THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'missing_target' + ); + END IF; + IF conversation_status <> 'open' THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'illegal_transition' + ); + END IF; + + -- Serialize both unique RuntimeSession identities in canonical order even when + -- hostile callers name different Conversations. Hash collisions only add safe + -- serialization; they cannot weaken uniqueness or rejection classification. + identity_lock := pg_catalog.hashtextextended( + 'runtime_session/id/' || p_session_id::text, 0 + ); + thread_lock := CASE WHEN p_codex_thread_id IS NULL THEN identity_lock ELSE + pg_catalog.hashtextextended('runtime_session/thread/' || p_codex_thread_id::text, 0) + END; + PERFORM pg_catalog.pg_advisory_xact_lock( + CASE WHEN identity_lock <= thread_lock THEN identity_lock ELSE thread_lock END + ); + IF thread_lock <> identity_lock THEN + PERFORM pg_catalog.pg_advisory_xact_lock( + CASE WHEN identity_lock > thread_lock THEN identity_lock ELSE thread_lock END + ); + END IF; + IF EXISTS ( + SELECT 1 FROM decodex.runtime_sessions + WHERE runtime_session_id = p_session_id + OR (p_codex_thread_id IS NOT NULL AND codex_thread_id = p_codex_thread_id) + ) THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'duplicate_target' + ); + END IF; + + -- FOR SHARE prevents a concurrent profile advance until the complete selected + -- revision has been copied, yielding either the old or new immutable revision. + SELECT pg_catalog.jsonb_build_object( + 'role', revision.role, 'revision', revision.revision, + 'model', revision.model, 'reasoning_effort', revision.reasoning_effort, + 'service_tier', revision.service_tier, 'instructions', revision.instructions, + 'provenance', revision.provenance, 'created_at', revision.created_at + ) INTO profile_value + FROM decodex.role_profiles AS profile + JOIN decodex.role_profile_revisions AS revision + ON (revision.role, revision.revision) = (profile.role, profile.current_revision) + WHERE profile.role = p_role + FOR SHARE OF profile; + IF NOT FOUND THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'missing_target' + ); + END IF; + + INSERT INTO decodex.account_snapshots( + account_snapshot_id, source_account_id, display_label, observed_state, source_revision + ) VALUES ( + p_account_snapshot_id, p_source_account_id, p_display_label, + p_observed_state, p_account_source_revision + ) ON CONFLICT (account_snapshot_id) DO NOTHING; + SELECT pg_catalog.jsonb_build_object( + 'account_snapshot_id', account_snapshot_id, + 'source_account_id', source_account_id, 'display_label', display_label, + 'observed_state', observed_state, 'source_revision', source_revision, + 'created_at', created_at + ) INTO account_value + FROM decodex.account_snapshots WHERE account_snapshot_id = p_account_snapshot_id; + IF account_value->>'source_account_id' <> p_source_account_id::text + OR account_value->>'display_label' <> p_display_label + OR account_value->>'observed_state' <> p_observed_state::text + OR (account_value->>'source_revision')::bigint <> p_account_source_revision + THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'account_snapshot_conflict' + ); + END IF; + + profile_snapshot_id := pg_catalog.gen_random_uuid(); + INSERT INTO decodex.profile_snapshots( + profile_snapshot_id, source_profile_id, role, model, reasoning_effort, + service_tier, instructions_digest, instructions, provenance, source_revision + ) VALUES ( + profile_snapshot_id, profile_value->>'role', p_role, + profile_value->>'model', profile_value->>'reasoning_effort', + profile_value->>'service_tier', + pg_catalog.encode(public.digest( + pg_catalog.convert_to(profile_value->>'instructions', 'UTF8'), 'sha256' + ), 'hex'), profile_value->>'instructions', profile_value->>'provenance', + (profile_value->>'revision')::bigint + ) RETURNING pg_catalog.jsonb_build_object( + 'profile_snapshot_id', profile_snapshot_id, + 'source_profile_id', source_profile_id, 'role', role, + 'model', model, 'reasoning_effort', reasoning_effort, + 'service_tier', service_tier, 'instructions_digest', instructions_digest, + 'instructions', instructions, 'provenance', provenance, + 'source_revision', source_revision, 'created_at', created_at + ) INTO profile_value; + + INSERT INTO decodex.runtime_sessions( + runtime_session_id, conversation_id, profile_snapshot_id, + account_snapshot_id, codex_thread_id, state, last_known_turn_id + ) VALUES ( + p_session_id, p_conversation_id, profile_snapshot_id, + p_account_snapshot_id, p_codex_thread_id, p_initial_state, NULL + ) RETURNING pg_catalog.jsonb_build_object( + 'runtime_session_id', runtime_session_id, 'conversation_id', conversation_id, + 'profile_snapshot_id', profile_snapshot_id, + 'account_snapshot_id', account_snapshot_id, + 'codex_thread_id', codex_thread_id, 'last_known_turn_id', last_known_turn_id, + 'state', state, 'revision', revision, 'created_at', created_at, + 'updated_at', updated_at, 'ended_at', ended_at + ) INTO session_value; + + activity_payload := pg_catalog.jsonb_build_object( + 'kind', 'runtime_session', 'runtime_session', session_value, + 'profile_snapshot', profile_value, 'account_snapshot', account_value + ); + INSERT INTO decodex.activity( + aggregate_kind, aggregate_id, revision, event_kind, correlation_key, payload + ) VALUES ( + 'runtime_session', p_session_id::text, 1, 'runtime_session_created', + p_idempotency_key, activity_payload + ) RETURNING sequence, aggregate_kind, aggregate_id, revision, event_kind, payload + INTO activity_sequence, activity_aggregate_kind, activity_aggregate_id, + activity_revision, activity_event_kind, activity_payload; + outbox_payload := pg_catalog.jsonb_build_object( + 'activity_sequence', activity_sequence, 'event_kind', 'runtime_session_created', + 'aggregate_kind', 'runtime_session', 'aggregate_id', p_session_id, + 'revision', 1, 'payload', activity_payload + ); + INSERT INTO decodex.outbox( + effect_key, aggregate_kind, aggregate_id, aggregate_revision, payload + ) VALUES ( + 'activity/' || activity_sequence::text, 'runtime_session', p_session_id::text, + 1, outbox_payload + ) RETURNING id, effect_key, aggregate_kind, aggregate_id, aggregate_revision, payload + INTO outbox_id, outbox_effect_key, outbox_aggregate_kind, + outbox_aggregate_id, outbox_aggregate_revision, outbox_payload; + + effect_value := pg_catalog.jsonb_build_object( + 'request', request_value, + 'runtime_session', session_value, 'profile_snapshot', profile_value, + 'account_snapshot', account_value, 'prior_state', NULL, + 'new_state', p_initial_state, 'prior_revision', NULL, 'new_revision', 1, + 'activity_sequence', activity_sequence, + 'activity_aggregate_kind', activity_aggregate_kind, + 'activity_aggregate_id', activity_aggregate_id, + 'activity_revision', activity_revision, + 'activity_event_kind', activity_event_kind, + 'activity_payload', activity_payload, + 'outbox_id', outbox_id, 'outbox_effect_key', outbox_effect_key, + 'outbox_aggregate_kind', outbox_aggregate_kind, + 'outbox_aggregate_id', outbox_aggregate_id, + 'outbox_aggregate_revision', outbox_aggregate_revision, + 'outbox_payload', outbox_payload + ); + response_value := pg_catalog.convert_to(pg_catalog.jsonb_build_object( + 'classification', 'success', 'effect', effect_value + )::text, 'UTF8'); + UPDATE decodex.exact_command_receipts + SET receipt_state = 'completed_success', outcome_class = 'success', + effect_envelope = effect_value, response_bytes = response_value, + completed_at = pg_catalog.clock_timestamp() + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key; + + RETURN response_value; +END +$$; + +CREATE FUNCTION decodex.transition_runtime_session_exact( + p_protocol text, p_idempotency_key text, p_session_id uuid, + p_expected_revision bigint, p_target_state decodex.runtime_session_state +) RETURNS bytea +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = pg_catalog, decodex +AS $$ +DECLARE request_value jsonb; +DECLARE existing_request jsonb; +DECLARE existing_response bytea; +DECLARE inserted_count integer; +DECLARE prior_state decodex.runtime_session_state; +DECLARE actual_revision bigint; +DECLARE profile_value jsonb; +DECLARE account_value jsonb; +DECLARE session_value jsonb; +DECLARE activity_sequence bigint; +DECLARE activity_aggregate_kind text; +DECLARE activity_aggregate_id text; +DECLARE activity_revision bigint; +DECLARE activity_event_kind text; +DECLARE activity_payload jsonb; +DECLARE outbox_id bigint; +DECLARE outbox_effect_key text; +DECLARE outbox_aggregate_kind text; +DECLARE outbox_aggregate_id text; +DECLARE outbox_aggregate_revision bigint; +DECLARE outbox_payload jsonb; +DECLARE effect_value jsonb; +DECLARE response_value bytea; +BEGIN + IF p_protocol IS NULL OR p_idempotency_key IS NULL OR p_session_id IS NULL + OR p_expected_revision IS NULL OR p_target_state IS NULL + THEN + RAISE EXCEPTION 'exact RuntimeSession transition is incomplete' USING ERRCODE = '22004'; + END IF; + IF pg_catalog.octet_length(p_protocol) NOT BETWEEN 1 AND 64 + OR p_protocol COLLATE pg_catalog."C" !~ '^[a-z0-9][a-z0-9._/-]{0,63}$' + OR pg_catalog.octet_length(p_idempotency_key) NOT BETWEEN 1 AND 256 + OR decodex.has_credential_material(p_idempotency_key) + THEN + RAISE EXCEPTION 'exact RuntimeSession command identity is invalid' USING ERRCODE = '22023'; + END IF; + + request_value := decodex.build_runtime_session_transition_request( + p_protocol, p_session_id, p_expected_revision, p_target_state + ); + INSERT INTO decodex.exact_command_receipts( + protocol_version, idempotency_key, request_envelope, request_digest, receipt_state + ) VALUES ( + p_protocol, p_idempotency_key, request_value, + public.digest(pg_catalog.convert_to(request_value::text, 'UTF8'), 'sha256'), 'executing' + ) ON CONFLICT (protocol_version, idempotency_key) DO NOTHING; + GET DIAGNOSTICS inserted_count = ROW_COUNT; + + IF inserted_count = 0 THEN + SELECT request_envelope, response_bytes INTO existing_request, existing_response + FROM decodex.exact_command_receipts + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key + FOR UPDATE; + IF existing_request <> request_value THEN + RAISE EXCEPTION 'exact idempotency conflict' USING ERRCODE = 'DX001'; + END IF; + IF existing_response IS NULL THEN + RAISE EXCEPTION 'incomplete exact receipt is not replayable' USING ERRCODE = 'DX002'; + END IF; + RETURN existing_response; + END IF; + + -- The following UPDATE has the same outer coordinator as every hierarchy mutation. + -- Acquire it before the executor can select and lock the RuntimeSession tuple below. + PERFORM pg_catalog.pg_advisory_xact_lock(1271); + + SELECT session.state, session.revision, + pg_catalog.jsonb_build_object( + 'profile_snapshot_id', profile.profile_snapshot_id, + 'source_profile_id', profile.source_profile_id, 'role', profile.role, + 'model', profile.model, 'reasoning_effort', profile.reasoning_effort, + 'service_tier', profile.service_tier, + 'instructions_digest', profile.instructions_digest, + 'instructions', profile.instructions, 'provenance', profile.provenance, + 'source_revision', profile.source_revision, 'created_at', profile.created_at + ), + pg_catalog.jsonb_build_object( + 'account_snapshot_id', account.account_snapshot_id, + 'source_account_id', account.source_account_id, + 'display_label', account.display_label, 'observed_state', account.observed_state, + 'source_revision', account.source_revision, 'created_at', account.created_at + ) + INTO prior_state, actual_revision, profile_value, account_value + FROM decodex.runtime_sessions AS session + JOIN decodex.profile_snapshots AS profile USING (profile_snapshot_id) + JOIN decodex.account_snapshots AS account USING (account_snapshot_id) + WHERE session.runtime_session_id = p_session_id + FOR UPDATE OF session; + IF NOT FOUND THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'missing_target' + ); + END IF; + IF actual_revision <> p_expected_revision THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'stale_revision' + ); + END IF; + IF NOT ( + (prior_state = 'starting' AND p_target_state IN ('active', 'ended', 'diverged')) + OR (prior_state = 'active' AND p_target_state IN ('ended', 'diverged')) + ) OR (p_target_state IN ('ended', 'diverged') AND EXISTS ( + SELECT 1 FROM decodex.turns + WHERE runtime_session_id = p_session_id AND status = 'active' + )) THEN + RETURN decodex.complete_exact_runtime_session_rejection( + p_protocol, p_idempotency_key, 'illegal_transition' + ); + END IF; + + UPDATE decodex.runtime_sessions + SET state = p_target_state, revision = revision + 1 + WHERE runtime_session_id = p_session_id AND revision = p_expected_revision + RETURNING pg_catalog.jsonb_build_object( + 'runtime_session_id', runtime_session_id, 'conversation_id', conversation_id, + 'profile_snapshot_id', profile_snapshot_id, + 'account_snapshot_id', account_snapshot_id, + 'codex_thread_id', codex_thread_id, 'last_known_turn_id', last_known_turn_id, + 'state', state, 'revision', revision, 'created_at', created_at, + 'updated_at', updated_at, 'ended_at', ended_at + ) INTO session_value; + IF session_value IS NULL THEN + RAISE EXCEPTION 'RuntimeSession compare-and-swap lost after row lock' USING ERRCODE = '40001'; + END IF; + + activity_payload := pg_catalog.jsonb_build_object( + 'kind', 'runtime_session', 'runtime_session', session_value, + 'profile_snapshot', profile_value, 'account_snapshot', account_value, + 'prior_state', prior_state, 'new_state', p_target_state, + 'prior_revision', p_expected_revision, + 'new_revision', (session_value->>'revision')::bigint + ); + INSERT INTO decodex.activity( + aggregate_kind, aggregate_id, revision, event_kind, correlation_key, payload + ) VALUES ( + 'runtime_session', p_session_id::text, (session_value->>'revision')::bigint, + 'runtime_session_transitioned', p_idempotency_key, activity_payload + ) RETURNING sequence, aggregate_kind, aggregate_id, revision, event_kind, payload + INTO activity_sequence, activity_aggregate_kind, activity_aggregate_id, + activity_revision, activity_event_kind, activity_payload; + outbox_payload := pg_catalog.jsonb_build_object( + 'activity_sequence', activity_sequence, 'event_kind', 'runtime_session_transitioned', + 'aggregate_kind', 'runtime_session', 'aggregate_id', p_session_id, + 'revision', (session_value->>'revision')::bigint, 'payload', activity_payload + ); + INSERT INTO decodex.outbox( + effect_key, aggregate_kind, aggregate_id, aggregate_revision, payload + ) VALUES ( + 'activity/' || activity_sequence::text, 'runtime_session', p_session_id::text, + (session_value->>'revision')::bigint, outbox_payload + ) RETURNING id, effect_key, aggregate_kind, aggregate_id, aggregate_revision, payload + INTO outbox_id, outbox_effect_key, outbox_aggregate_kind, + outbox_aggregate_id, outbox_aggregate_revision, outbox_payload; + + effect_value := pg_catalog.jsonb_build_object( + 'request', request_value, + 'runtime_session', session_value, 'profile_snapshot', profile_value, + 'account_snapshot', account_value, 'prior_state', prior_state, + 'new_state', p_target_state, 'prior_revision', p_expected_revision, + 'new_revision', (session_value->>'revision')::bigint, + 'activity_sequence', activity_sequence, + 'activity_aggregate_kind', activity_aggregate_kind, + 'activity_aggregate_id', activity_aggregate_id, + 'activity_revision', activity_revision, + 'activity_event_kind', activity_event_kind, + 'activity_payload', activity_payload, + 'outbox_id', outbox_id, 'outbox_effect_key', outbox_effect_key, + 'outbox_aggregate_kind', outbox_aggregate_kind, + 'outbox_aggregate_id', outbox_aggregate_id, + 'outbox_aggregate_revision', outbox_aggregate_revision, + 'outbox_payload', outbox_payload + ); + response_value := pg_catalog.convert_to(pg_catalog.jsonb_build_object( + 'classification', 'success', 'effect', effect_value + )::text, 'UTF8'); + UPDATE decodex.exact_command_receipts + SET receipt_state = 'completed_success', outcome_class = 'success', + effect_envelope = effect_value, response_bytes = response_value, + completed_at = pg_catalog.clock_timestamp() + WHERE protocol_version = p_protocol AND idempotency_key = p_idempotency_key; + + RETURN response_value; +END +$$; + +REVOKE ALL ON TABLE decodex.profile_snapshots, + decodex.account_snapshots, decodex.runtime_sessions FROM PUBLIC; +REVOKE ALL ON ALL FUNCTIONS IN SCHEMA decodex FROM PUBLIC; +ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; diff --git a/crates/decodex-postgres/src/authority.rs b/crates/decodex-postgres/src/authority.rs index 81337682..bfde82b6 100644 --- a/crates/decodex-postgres/src/authority.rs +++ b/crates/decodex-postgres/src/authority.rs @@ -13,8 +13,11 @@ const PROGRAM_OBJECTIVE_MIGRATION: &str = include_str!("../migrations/V7__program_objective_authority.sql"); const QUOTA_MIGRATION: &str = include_str!("../migrations/V8__quota_exclusions.sql"); const ROLE_PROFILE_MIGRATION: &str = include_str!("../migrations/V9__exact_role_profiles.sql"); -const ALLOWED_EXECUTION_DEPENDENCIES: [&str; 1] = ["public.digest(pg_catalog.bytea,pg_catalog.text)"]; -const FUNCTION_CONTRACTS: [FunctionContract; 72] = [ +const RUNTIME_SESSION_MIGRATION: &str = + include_str!("../migrations/V10__runtime_session_snapshots.sql"); +const ALLOWED_EXECUTION_DEPENDENCIES: [&str; 1] = + ["public.digest(pg_catalog.bytea,pg_catalog.text)"]; +const FUNCTION_CONTRACTS: [FunctionContract; 80] = [ FunctionContract { name: "is_canonical_media_type", lookup_signature: "decodex.is_canonical_media_type(pg_catalog.text)", @@ -609,8 +612,68 @@ const FUNCTION_CONTRACTS: [FunctionContract; 72] = [ "plpgsql", "v", ), + trigger_contract( + "enforce_runtime_session_command_owner", + "decodex.enforce_runtime_session_command_owner()", + "enforce_runtime_session_command_owner()", + ), + trigger_contract( + "forbid_runtime_snapshot_mutation", + "decodex.forbid_runtime_snapshot_mutation()", + "forbid_runtime_snapshot_mutation()", + ), + trigger_contract( + "enforce_runtime_session_event_namespace", + "decodex.enforce_runtime_session_event_namespace()", + "enforce_runtime_session_event_namespace()", + ), + exact_function_contract( + "build_runtime_session_create_request", + "decodex.build_runtime_session_create_request(pg_catalog.text,pg_catalog.uuid,pg_catalog.uuid,decodex.role_profile_role,pg_catalog.uuid,pg_catalog.uuid,pg_catalog.text,decodex.account_state,pg_catalog.int8,pg_catalog.uuid,decodex.runtime_session_state)", + "build_runtime_session_create_request(\n\tp_protocol text, p_session_id uuid, p_conversation_id uuid,\n\tp_role decodex.role_profile_role, p_account_snapshot_id uuid,\n\tp_source_account_id uuid, p_display_label text,\n\tp_observed_state decodex.account_state, p_account_source_revision bigint,\n\tp_codex_thread_id uuid, p_initial_state decodex.runtime_session_state\n)", + "p_protocol text, p_session_id uuid, p_conversation_id uuid, p_role decodex.role_profile_role, p_account_snapshot_id uuid, p_source_account_id uuid, p_display_label text, p_observed_state decodex.account_state, p_account_source_revision bigint, p_codex_thread_id uuid, p_initial_state decodex.runtime_session_state", + "jsonb", + "sql", + "i", + ), + exact_function_contract( + "build_runtime_session_transition_request", + "decodex.build_runtime_session_transition_request(pg_catalog.text,pg_catalog.uuid,pg_catalog.int8,decodex.runtime_session_state)", + "build_runtime_session_transition_request(\n\tp_protocol text, p_session_id uuid, p_expected_revision bigint,\n\tp_target_state decodex.runtime_session_state\n)", + "p_protocol text, p_session_id uuid, p_expected_revision bigint, p_target_state decodex.runtime_session_state", + "jsonb", + "sql", + "i", + ), + exact_function_contract( + "complete_exact_runtime_session_rejection", + "decodex.complete_exact_runtime_session_rejection(pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "complete_exact_runtime_session_rejection(\n\tp_protocol text, p_idempotency_key text, p_code text\n)", + "p_protocol text, p_idempotency_key text, p_code text", + "bytea", + "plpgsql", + "v", + ), + exact_function_contract( + "create_runtime_session_exact", + "decodex.create_runtime_session_exact(pg_catalog.text,pg_catalog.text,pg_catalog.uuid,pg_catalog.uuid,decodex.role_profile_role,pg_catalog.uuid,pg_catalog.uuid,pg_catalog.text,decodex.account_state,pg_catalog.int8,pg_catalog.uuid,decodex.runtime_session_state)", + "create_runtime_session_exact(\n\tp_protocol text, p_idempotency_key text,\n\tp_session_id uuid, p_conversation_id uuid, p_role decodex.role_profile_role,\n\tp_account_snapshot_id uuid, p_source_account_id uuid, p_display_label text,\n\tp_observed_state decodex.account_state, p_account_source_revision bigint,\n\tp_codex_thread_id uuid, p_initial_state decodex.runtime_session_state\n)", + "p_protocol text, p_idempotency_key text, p_session_id uuid, p_conversation_id uuid, p_role decodex.role_profile_role, p_account_snapshot_id uuid, p_source_account_id uuid, p_display_label text, p_observed_state decodex.account_state, p_account_source_revision bigint, p_codex_thread_id uuid, p_initial_state decodex.runtime_session_state", + "bytea", + "plpgsql", + "v", + ), + exact_function_contract( + "transition_runtime_session_exact", + "decodex.transition_runtime_session_exact(pg_catalog.text,pg_catalog.text,pg_catalog.uuid,pg_catalog.int8,decodex.runtime_session_state)", + "transition_runtime_session_exact(\n\tp_protocol text, p_idempotency_key text, p_session_id uuid,\n\tp_expected_revision bigint, p_target_state decodex.runtime_session_state\n)", + "p_protocol text, p_idempotency_key text, p_session_id uuid, p_expected_revision bigint, p_target_state decodex.runtime_session_state", + "bytea", + "plpgsql", + "v", + ), ]; -const RUNTIME_EXECUTE_FUNCTIONS: [&str; 28] = [ +const RUNTIME_EXECUTE_FUNCTIONS: [&str; 30] = [ "decodex.is_canonical_media_type(pg_catalog.text)", "decodex.is_history_metadata_projection(pg_catalog.jsonb)", "decodex.normalize_unicode_whitespace(pg_catalog.text)", @@ -639,8 +702,10 @@ const RUNTIME_EXECUTE_FUNCTIONS: [&str; 28] = [ "decodex.achieve_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text)", "decodex.bootstrap_role_profiles_exact(pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", "decodex.update_role_profile_exact(pg_catalog.text,pg_catalog.text,decodex.role_profile_role,pg_catalog.int8,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "decodex.create_runtime_session_exact(pg_catalog.text,pg_catalog.text,pg_catalog.uuid,pg_catalog.uuid,decodex.role_profile_role,pg_catalog.uuid,pg_catalog.uuid,pg_catalog.text,decodex.account_state,pg_catalog.int8,pg_catalog.uuid,decodex.runtime_session_state)", + "decodex.transition_runtime_session_exact(pg_catalog.text,pg_catalog.text,pg_catalog.uuid,pg_catalog.int8,decodex.runtime_session_state)", ]; -const SAFETY_FUNCTIONS: [&str; 34] = [ +const SAFETY_FUNCTIONS: [&str; 37] = [ "enforce_lease_operation_time", "enforce_outbox_operation_time", "enforce_quota_observation_monotonicity", @@ -675,8 +740,11 @@ const SAFETY_FUNCTIONS: [&str; 34] = [ "forbid_role_profile_revision_mutation", "forbid_role_profile_truncate", "enforce_role_profile_event_namespace", + "enforce_runtime_session_command_owner", + "forbid_runtime_snapshot_mutation", + "enforce_runtime_session_event_namespace", ]; -const SAFETY_TRIGGER_COUNT: usize = 52; +const SAFETY_TRIGGER_COUNT: usize = 59; // PostgreSQL 18 catalogs with an owner and a containing namespace, plus the namespace // itself. Namespace-scoped catalogs without an independent owner (constraints, triggers, // text-search parsers/templates, and dependent rows) inherit authority from one of these. @@ -836,9 +904,9 @@ WITH set_roles AS ( ('leases', true, true, true, false), ('outbox', true, true, true, true), ('conversations', true, true, true, false), - ('profile_snapshots', true, true, false, false), - ('account_snapshots', true, true, false, false), - ('runtime_sessions', true, true, true, false), + ('profile_snapshots', true, false, false, false), + ('account_snapshots', true, false, false, false), + ('runtime_sessions', true, false, false, false), ('blob_objects', true, true, false, true), ('artifacts', true, true, true, false), ('artifact_revisions', true, true, false, false), @@ -1073,8 +1141,15 @@ WITH expected(table_name, trigger_name, function_name, trigger_type) AS (VALUES ('role_profile_revisions', 'role_profile_revisions_immutable', 'forbid_role_profile_revision_mutation', 27), ('role_profiles', 'role_profiles_untruncatable', 'forbid_role_profile_truncate', 34), ('role_profile_revisions', 'role_profile_revisions_untruncatable', 'forbid_role_profile_truncate', 34), - ('activity', 'activity_role_profile_namespace', 'enforce_role_profile_event_namespace', 23), - ('outbox', 'outbox_role_profile_namespace', 'enforce_role_profile_event_namespace', 23) + ('activity', 'activity_role_profile_namespace', 'enforce_role_profile_event_namespace', 23), + ('outbox', 'outbox_role_profile_namespace', 'enforce_role_profile_event_namespace', 23), + ('profile_snapshots', 'profile_snapshots_command_owner', 'enforce_runtime_session_command_owner', 62), + ('account_snapshots', 'account_snapshots_command_owner', 'enforce_runtime_session_command_owner', 62), + ('runtime_sessions', 'runtime_sessions_command_owner', 'enforce_runtime_session_command_owner', 62), + ('profile_snapshots', 'profile_snapshots_immutable', 'forbid_runtime_snapshot_mutation', 27), + ('account_snapshots', 'account_snapshots_immutable', 'forbid_runtime_snapshot_mutation', 27), + ('activity', 'activity_runtime_session_namespace', 'enforce_runtime_session_event_namespace', 23), + ('outbox', 'outbox_runtime_session_namespace', 'enforce_runtime_session_event_namespace', 23) ) SELECT expected.function_name, @@ -1242,8 +1317,15 @@ WITH catalog_context AS MATERIALIZED ( ('role_profile_revisions', 'role_profile_revisions_immutable', 'decodex.forbid_role_profile_revision_mutation()'), ('role_profiles', 'role_profiles_untruncatable', 'decodex.forbid_role_profile_truncate()'), ('role_profile_revisions', 'role_profile_revisions_untruncatable', 'decodex.forbid_role_profile_truncate()'), - ('activity', 'activity_role_profile_namespace', 'decodex.enforce_role_profile_event_namespace()'), - ('outbox', 'outbox_role_profile_namespace', 'decodex.enforce_role_profile_event_namespace()') + ('activity', 'activity_role_profile_namespace', 'decodex.enforce_role_profile_event_namespace()'), + ('outbox', 'outbox_role_profile_namespace', 'decodex.enforce_role_profile_event_namespace()'), + ('profile_snapshots', 'profile_snapshots_command_owner', 'decodex.enforce_runtime_session_command_owner()'), + ('account_snapshots', 'account_snapshots_command_owner', 'decodex.enforce_runtime_session_command_owner()'), + ('runtime_sessions', 'runtime_sessions_command_owner', 'decodex.enforce_runtime_session_command_owner()'), + ('profile_snapshots', 'profile_snapshots_immutable', 'decodex.forbid_runtime_snapshot_mutation()'), + ('account_snapshots', 'account_snapshots_immutable', 'decodex.forbid_runtime_snapshot_mutation()'), + ('activity', 'activity_runtime_session_namespace', 'decodex.enforce_runtime_session_event_namespace()'), + ('outbox', 'outbox_runtime_session_namespace', 'decodex.enforce_runtime_session_event_namespace()') ), actual_triggers AS ( SELECT class.relname AS table_name, @@ -1713,8 +1795,8 @@ SELECT pg_catalog.jsonb_agg( FROM contract_rows "#; const SCHEMA_CONTRACT_SHA256: [u8; 32] = [ - 0x94, 0xef, 0x27, 0x45, 0x48, 0x2a, 0x0b, 0x52, 0x9f, 0x2f, 0x37, 0x5a, 0x9f, 0xbc, 0x1b, 0x76, - 0x6c, 0x59, 0xc8, 0x20, 0x6b, 0xeb, 0x32, 0xd5, 0x82, 0xca, 0x9f, 0xa4, 0x1c, 0xb1, 0x43, 0x01, + 0x90, 0xad, 0xd2, 0x3f, 0x9a, 0xd9, 0xd9, 0x9c, 0x81, 0xb7, 0xae, 0xc1, 0x7a, 0x3f, 0xbf, 0x1f, + 0xe6, 0xb2, 0x6c, 0x28, 0x8d, 0x2d, 0x04, 0xfd, 0x9b, 0x5c, 0x4b, 0xad, 0x9d, 0xda, 0xd2, 0xbe, ]; // The shipped authority permits no role settings. Record only cardinality so any setting // fails closed without copying an arbitrary custom-GUC value into the manifest or digest input. @@ -2193,8 +2275,8 @@ SELECT pg_catalog.jsonb_agg( FROM contract_rows "#; const CONFIGURED_AUTHORITY_SHA256: [u8; 32] = [ - 0x1a, 0x15, 0x62, 0xe7, 0x80, 0x8f, 0xb9, 0xdf, 0x1e, 0x0f, 0xe1, 0xbf, 0x8d, 0x5a, 0xcf, 0xa0, - 0xf3, 0xbd, 0x1c, 0x86, 0x75, 0xef, 0x76, 0x23, 0xbe, 0xb6, 0x20, 0x92, 0xc2, 0xf0, 0xb1, 0x7c, + 0xc4, 0x43, 0xc4, 0xe0, 0xb0, 0x6e, 0x01, 0xd5, 0xb9, 0x28, 0x36, 0xf2, 0xfd, 0xf3, 0xf9, 0x15, + 0xd2, 0x96, 0xce, 0x40, 0xd3, 0x9b, 0x8c, 0x0c, 0xa1, 0x48, 0x9b, 0xe9, 0x65, 0xb5, 0xdb, 0xc1, ]; const EXTENSION_AUTHORITY_SQL: &str = r#" WITH set_roles AS ( @@ -2473,6 +2555,7 @@ fn canonical_function_source(contract: &FunctionContract) -> Option<&'static str PROGRAM_OBJECTIVE_MIGRATION, QUOTA_MIGRATION, ROLE_PROFILE_MIGRATION, + RUNTIME_SESSION_MIGRATION, ] .into_iter() .find(|migration| migration.contains(&declaration))?; @@ -2549,12 +2632,11 @@ async fn verify_configured_authority( } async fn verify_execution_path_contract(client: &Client) -> Result<(), StoreError> { - let allowed_functions = - FUNCTION_CONTRACTS - .iter() - .map(|contract| contract.lookup_signature) - .chain(ALLOWED_EXECUTION_DEPENDENCIES) - .collect::>(); + let allowed_functions = FUNCTION_CONTRACTS + .iter() + .map(|contract| contract.lookup_signature) + .chain(ALLOWED_EXECUTION_DEPENDENCIES) + .collect::>(); let row = client.query_one(EXECUTION_PATH_CONTRACT_SQL, &[&allowed_functions]).await?; let exact_triggers: bool = row.get(0); let no_rules: bool = row.get(1); @@ -2637,6 +2719,8 @@ async fn verify_function_contract(client: &Client) -> Result<(), StoreError> { | "achieve_objective" | "bootstrap_role_profiles_exact" | "update_role_profile_exact" + | "create_runtime_session_exact" + | "transition_runtime_session_exact" ); let expected_executable = RUNTIME_EXECUTE_FUNCTIONS.contains(&contract.lookup_signature); let expected_settings = vec!["search_path=pg_catalog, decodex".to_owned()]; @@ -2791,8 +2875,8 @@ mod tests { CONFIGURED_AUTHORITY_SHA256, CONFIGURED_AUTHORITY_SQL, CONVERSATION_MIGRATION, FOUNDATION_MIGRATION, FUNCTION_CONTRACTS, IDENTITY_CAST_AUTHORITY_SQL, OWNED_OBJECT_CATALOGS, POLICY_MIGRATION, PROGRAM_OBJECTIVE_MIGRATION, - PROJECT_AGENT_MIGRATION, QUOTA_MIGRATION, ROLE_AUTHORITY_SQL, SAFETY_FUNCTIONS, - ROLE_PROFILE_MIGRATION, SCHEMA_CONTRACT_SHA256, SCHEMA_CONTRACT_SQL, + PROJECT_AGENT_MIGRATION, QUOTA_MIGRATION, ROLE_AUTHORITY_SQL, ROLE_PROFILE_MIGRATION, + RUNTIME_SESSION_MIGRATION, SAFETY_FUNCTIONS, SCHEMA_CONTRACT_SHA256, SCHEMA_CONTRACT_SQL, }; #[test] @@ -2926,6 +3010,7 @@ mod tests { PROGRAM_OBJECTIVE_MIGRATION, QUOTA_MIGRATION, ROLE_PROFILE_MIGRATION, + RUNTIME_SESSION_MIGRATION, ] .into_iter() .map(|migration| migration.matches("CREATE FUNCTION decodex.").count()) @@ -2946,6 +3031,7 @@ mod tests { PROGRAM_OBJECTIVE_MIGRATION, QUOTA_MIGRATION, ROLE_PROFILE_MIGRATION, + RUNTIME_SESSION_MIGRATION, ] .into_iter() .map(|migration| migration diff --git a/crates/decodex-postgres/src/conversations.rs b/crates/decodex-postgres/src/conversations.rs index 3ed7c0db..bcf77385 100644 --- a/crates/decodex-postgres/src/conversations.rs +++ b/crates/decodex-postgres/src/conversations.rs @@ -15,11 +15,11 @@ use crate::{ accounts::{self, CommandClaim, CommandDescriptor}, }; use decodex_core::{ - self, AccountSnapshot, ArtifactId, ArtifactStatus, BlobHash, BlobInventoryCursor, BlobStore, - ContextPack, ContextPackPolicy, ContextSourceDisposition, ContextSourceKind, ConversationId, - HistoryItemId, HistoryItemKind, HistoryMediaType, HistoryMetadata, ItemStatus, MAX_BLOB_BYTES, - MAX_INLINE_HISTORY_BYTES, PossibleSideEffects, ProfileSnapshot, ProposedTransitionKind, - RuntimeSessionId, RuntimeSessionState, TurnId, TurnRole, + self, ArtifactId, ArtifactStatus, BlobHash, BlobInventoryCursor, BlobStore, ContextPack, + ContextPackPolicy, ContextSourceDisposition, ContextSourceKind, ConversationId, HistoryItemId, + HistoryItemKind, HistoryMediaType, HistoryMetadata, ItemStatus, MAX_BLOB_BYTES, + MAX_INLINE_HISTORY_BYTES, PossibleSideEffects, ProposedTransitionKind, RuntimeSessionId, + TurnId, TurnRole, }; const MAX_PAGE_SIZE: u16 = 100; @@ -48,42 +48,6 @@ pub struct StoredConversation { pub revision: i64, } -/// Create one explicitly manual or synthetic RuntimeSession and its immutable snapshots. -#[derive(Clone, Debug)] -pub struct CreateRuntimeSession { - /// Caller-selected runtime segment identity. - pub runtime_session_id: RuntimeSessionId, - /// Existing logical Conversation identity. - pub conversation_id: ConversationId, - /// Caller-selected immutable profile snapshot identity. - pub profile_snapshot_id: String, - /// Caller-selected immutable account snapshot identity. - pub account_snapshot_id: String, - /// Validated immutable profile facts. - pub profile_snapshot: ProfileSnapshot, - /// Validated immutable non-secret account facts. - pub account_snapshot: AccountSnapshot, - /// Explicit manual thread binding, if available. - pub codex_thread_id: Option, - /// Observed segment state; never used to dispatch work. - pub state: RuntimeSessionState, -} - -/// Committed RuntimeSession readback. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct StoredRuntimeSession { - /// Stable runtime segment identity. - pub runtime_session_id: RuntimeSessionId, - /// Parent logical Conversation. - pub conversation_id: ConversationId, - /// Explicitly stored Codex thread identity, if any. - pub codex_thread_id: Option, - /// Persisted observed state. - pub state: RuntimeSessionState, - /// Persisted optimistic revision. - pub revision: i64, -} - /// Create one immutable-content Artifact scoped to a logical Conversation. #[derive(Clone, Debug)] pub struct CreateArtifact { @@ -390,8 +354,9 @@ impl PostgresStore { ) .await? { - accounts::CommandClaim::Completed(response) => - return conversation_from_response(&response), + accounts::CommandClaim::Completed(response) => { + return conversation_from_response(&response); + }, accounts::CommandClaim::Owned(reservation) => reservation, }; let transaction = client.transaction().await?; @@ -441,151 +406,6 @@ impl PostgresStore { conversation_from_response(&response) } - /// Atomically persist one explicitly manual or synthetic RuntimeSession and snapshots. - pub async fn create_runtime_session( - &self, - command: &CommandIdentity, - create: &CreateRuntimeSession, - ) -> Result { - validate_session(create)?; - - let mut client = self.pool().get().await?; - let reservation = match reserve_conversation_command( - &mut client, - command, - "create_runtime_session", - ("conversation", create.conversation_id.as_str(), create.runtime_session_id.as_str()), - None, - None, - ) - .await? - { - accounts::CommandClaim::Completed(response) => - return runtime_session_from_response(&response), - accounts::CommandClaim::Owned(reservation) => reservation, - }; - let transaction = client.transaction().await?; - - insert_profile_snapshot(&transaction, create).await?; - insert_account_snapshot(&transaction, create).await?; - - let inserted = transaction - .query_opt( - "INSERT INTO decodex.runtime_sessions \ - (runtime_session_id, conversation_id, profile_snapshot_id, account_snapshot_id, \ - codex_thread_id, state) \ - VALUES ($1::text::uuid, $2::text::uuid, $3::text::uuid, $4::text::uuid, $5, \ - $6::text::decodex.runtime_session_state) \ - ON CONFLICT DO NOTHING RETURNING revision", - &[ - &create.runtime_session_id.as_str(), - &create.conversation_id.as_str(), - &create.profile_snapshot_id, - &create.account_snapshot_id, - &create.codex_thread_id, - &session_state_sql(create.state), - ], - ) - .await?; - - if inserted.is_none() { - return Err(StoreError::RevisionConflict { - entity: format!("runtime_session/{}", create.runtime_session_id), - expected: None, - actual: session_revision(&transaction, &create.runtime_session_id).await?, - }); - } - - let payload = serde_json::json!({ - "conversation_id": create.conversation_id.as_str(), - "runtime_session_id": create.runtime_session_id.as_str(), - "binding": "manual_or_synthetic", - "revision": 1, - }); - - accounts::append_activity_and_outbox( - &transaction, - "runtime_session", - create.runtime_session_id.as_str(), - 1, - "runtime_session_recorded", - &command.key, - &payload, - ) - .await?; - - let response = serde_json::json!({ - "kind": "runtime_session", - "runtime_session_id": create.runtime_session_id.as_str(), - "conversation_id": create.conversation_id.as_str(), - "codex_thread_id": create.codex_thread_id, - "state": session_state_sql(create.state), - "revision": 1, - }); - - accounts::finish_command(&transaction, &reservation, &response).await?; - - transaction.commit().await?; - - post_commit_test_barrier("runtime_session").await?; - - runtime_session_from_response(&response) - } - - /// Record one observed RuntimeSession state transition; never starts or steers Codex. - pub async fn transition_runtime_session( - &self, - command: &CommandIdentity, - session_id: &RuntimeSessionId, - expected_revision: i64, - state: RuntimeSessionState, - ) -> Result { - if expected_revision < 1 || state == RuntimeSessionState::Starting { - return Err(StoreError::InvalidInput("RuntimeSession transition is invalid")); - } - - let mut client = self.pool().get().await?; - let reservation = match reserve_conversation_command( - &mut client, - command, - "transition_runtime_session", - ("runtime_session", session_id.as_str(), session_id.as_str()), - Some(expected_revision), - None, - ) - .await? - { - accounts::CommandClaim::Completed(response) => - return runtime_session_from_response(&response), - accounts::CommandClaim::Owned(reservation) => reservation, - }; - let transaction = client.transaction().await?; - let row = transaction.query_opt( - "UPDATE decodex.runtime_sessions SET state=$3::text::decodex.runtime_session_state, revision=revision+1, updated_at=clock_timestamp(), ended_at=CASE WHEN $3 IN ('ended','diverged') THEN clock_timestamp() ELSE NULL END WHERE runtime_session_id=$1::text::uuid AND revision=$2 RETURNING revision, conversation_id::text, codex_thread_id", - &[&session_id.as_str(), &expected_revision, &session_state_sql(state)], - ).await?.ok_or(StoreError::RevisionConflict { entity: format!("runtime_session/{session_id}"), expected: Some(expected_revision), actual: None })?; - let revision: i64 = row.get(0); - let conversation_id: String = row.get(1); - let codex_thread_id: Option = row.get(2); - - accounts::append_activity_and_outbox(&transaction,"runtime_session",session_id.as_str(),revision,"runtime_session_transitioned",&command.key,&serde_json::json!({"conversation_id":conversation_id,"state":session_state_sql(state),"revision":revision})).await?; - - let response = serde_json::json!({ - "kind":"runtime_session", - "runtime_session_id":session_id.as_str(), - "conversation_id":conversation_id, - "codex_thread_id":codex_thread_id, - "state":session_state_sql(state), - "revision":revision - }); - - accounts::finish_command(&transaction, &reservation, &response).await?; - - transaction.commit().await?; - - runtime_session_from_response(&response) - } - /// Complete or fail one active normalized Turn after its items are terminal. pub async fn transition_turn( &self, @@ -609,8 +429,9 @@ impl PostgresStore { ) .await? { - accounts::CommandClaim::Completed(response) => - return response_revision(&response, "turn"), + accounts::CommandClaim::Completed(response) => { + return response_revision(&response, "turn"); + }, accounts::CommandClaim::Owned(reservation) => reservation, }; let transaction = client.transaction().await?; @@ -1149,8 +970,9 @@ impl PostgresStore { ) .await? { - accounts::CommandClaim::Completed(_) => - return self.required_context_pack(blob_store, &request.context_pack_id).await, + accounts::CommandClaim::Completed(_) => { + return self.required_context_pack(blob_store, &request.context_pack_id).await; + }, accounts::CommandClaim::Owned(reservation) => reservation, }; @@ -1442,10 +1264,11 @@ impl PostgresStore { let bytes = match (inline, blob) { (Some(inline), None) => inline, (None, Some(blob)) => blob_store.read(BlobHash::parse(&blob)?)?, - _ => + _ => { return Err(StoreError::Incompatible( "stored Context Pack payload is invalid".into(), - )), + )); + }, }; if BlobHash::digest(&bytes) != digest @@ -1544,37 +1367,6 @@ fn context_pack_referenced_hashes( hashes } -fn validate_session(create: &CreateRuntimeSession) -> Result<(), StoreError> { - if !is_canonical_uuid(&create.profile_snapshot_id) - || !is_canonical_uuid(&create.account_snapshot_id) - || create - .codex_thread_id - .as_ref() - .is_some_and(|value| value.is_empty() || value.len() > 256) - { - return Err(StoreError::InvalidInput("manual RuntimeSession is malformed")); - } - - if let Some(thread_id) = &create.codex_thread_id { - crate::ensure_credential_negative_text(thread_id)?; - } - - for value in [ - create.profile_snapshot.profile_id.as_str(), - create.profile_snapshot.role.as_str(), - create.profile_snapshot.model.as_str(), - create.profile_snapshot.reasoning_effort.as_str(), - create.profile_snapshot.service_tier.as_str(), - create.account_snapshot.account_id.as_str(), - create.account_snapshot.display_label.as_str(), - create.account_snapshot.observed_state.as_str(), - ] { - crate::ensure_credential_negative_text(value)?; - } - - Ok(()) -} - fn validate_artifact(create: &CreateArtifact) -> Result<(), StoreError> { if create.bytes.is_empty() || create.bytes.len() > MAX_BLOB_BYTES @@ -1652,8 +1444,11 @@ fn history_entry_from_row(row: Row) -> Result { })?, )), (None, None) => None, - _ => - return Err(StoreError::Incompatible("history Artifact reference is incomplete".into())), + _ => { + return Err(StoreError::Incompatible( + "history Artifact reference is incomplete".into(), + )); + }, }; let kind = item_kind_from_sql(row.get::<_, &str>(6))?; let media_type = HistoryMediaType::new(row.get::<_, String>(11)) @@ -1751,25 +1546,6 @@ fn context_source_from_row(row: Row) -> Result &'static str { - match value { - RuntimeSessionState::Starting => "starting", - RuntimeSessionState::Active => "active", - RuntimeSessionState::Ended => "ended", - RuntimeSessionState::Diverged => "diverged", - } -} - -fn session_state_from_sql(value: &str) -> Result { - match value { - "starting" => Ok(RuntimeSessionState::Starting), - "active" => Ok(RuntimeSessionState::Active), - "ended" => Ok(RuntimeSessionState::Ended), - "diverged" => Ok(RuntimeSessionState::Diverged), - _ => Err(StoreError::Incompatible("unknown RuntimeSession state".into())), - } -} - const fn artifact_status_sql(value: ArtifactStatus) -> &'static str { match value { ArtifactStatus::Active => "active", @@ -2026,10 +1802,11 @@ fn history_entry_from_response(response: &Value) -> Result None, - _ => + _ => { return Err(StoreError::Incompatible( "stored history Artifact response is invalid".into(), - )), + )); + }, }; let metadata = response.get("metadata").cloned().ok_or_else(|| { StoreError::Incompatible("stored history metadata response is absent".into()) @@ -2088,46 +1865,6 @@ fn conversation_from_response(response: &Value) -> Result Result { - if response.get("kind").and_then(Value::as_str) != Some("runtime_session") { - return Err(StoreError::Incompatible( - "stored RuntimeSession response kind is invalid".into(), - )); - } - - let session_id = - response.get("runtime_session_id").and_then(Value::as_str).ok_or_else(|| { - StoreError::Incompatible("stored RuntimeSession response is incomplete".into()) - })?; - let conversation_id = - response.get("conversation_id").and_then(Value::as_str).ok_or_else(|| { - StoreError::Incompatible("stored RuntimeSession response is incomplete".into()) - })?; - let codex_thread_id = match response.get("codex_thread_id") { - Some(Value::String(value)) => Some(value.clone()), - Some(Value::Null) | None => None, - _ => - return Err(StoreError::Incompatible( - "stored RuntimeSession thread response is invalid".into(), - )), - }; - let state = response.get("state").and_then(Value::as_str).ok_or_else(|| { - StoreError::Incompatible("stored RuntimeSession response is incomplete".into()) - })?; - - Ok(StoredRuntimeSession { - runtime_session_id: RuntimeSessionId::new(session_id.to_owned()).map_err(|_| { - StoreError::Incompatible("stored RuntimeSession response identity is invalid".into()) - })?, - conversation_id: ConversationId::new(conversation_id.to_owned()).map_err(|_| { - StoreError::Incompatible("stored Conversation response identity is invalid".into()) - })?, - codex_thread_id, - state: session_state_from_sql(state)?, - revision: response_revision(response, "runtime_session")?, - }) -} - #[cfg(debug_assertions)] async fn post_commit_test_barrier(operation: &str) -> Result<(), StoreError> { let Ok(root) = env::var("DECODEX_TEST_POST_COMMIT_SYNC") else { @@ -2200,54 +1937,6 @@ async fn issue_history_cursor( HistoryCursor::issued(row.get(0)) } -async fn insert_profile_snapshot( - transaction: &Transaction<'_>, - create: &CreateRuntimeSession, -) -> Result<(), StoreError> { - transaction - .execute( - "INSERT INTO decodex.profile_snapshots \ - (profile_snapshot_id, source_profile_id, role, model, reasoning_effort, service_tier, \ - instructions_digest, source_revision) \ - VALUES ($1::text::uuid, $2, $3, $4, $5, $6, $7, $8)", - &[ - &create.profile_snapshot_id, - &create.profile_snapshot.profile_id, - &create.profile_snapshot.role, - &create.profile_snapshot.model, - &create.profile_snapshot.reasoning_effort, - &create.profile_snapshot.service_tier, - &create.profile_snapshot.instructions_digest.to_hex(), - &i64::try_from(create.profile_snapshot.source_revision).unwrap_or(i64::MAX), - ], - ) - .await?; - - Ok(()) -} - -async fn insert_account_snapshot( - transaction: &Transaction<'_>, - create: &CreateRuntimeSession, -) -> Result<(), StoreError> { - transaction - .execute( - "INSERT INTO decodex.account_snapshots \ - (account_snapshot_id, source_account_id, display_label, observed_state, source_revision) \ - VALUES ($1::text::uuid, $2, $3, $4, $5)", - &[ - &create.account_snapshot_id, - &create.account_snapshot.account_id, - &create.account_snapshot.display_label, - &create.account_snapshot.observed_state, - &i64::try_from(create.account_snapshot.source_revision).unwrap_or(i64::MAX), - ], - ) - .await?; - - Ok(()) -} - async fn insert_verified_blob( transaction: &Transaction<'_>, hash: BlobHash, @@ -2539,19 +2228,6 @@ async fn conversation_revision( .map(|row| row.get(0))) } -async fn session_revision( - transaction: &Transaction<'_>, - id: &RuntimeSessionId, -) -> Result, StoreError> { - Ok(transaction - .query_opt( - "SELECT revision FROM decodex.runtime_sessions WHERE runtime_session_id = $1::text::uuid", - &[&id.as_str()], - ) - .await? - .map(|row| row.get(0))) -} - #[cfg(test)] mod tests { use crate::{StoreError, conversations}; diff --git a/crates/decodex-postgres/src/exact_commands.rs b/crates/decodex-postgres/src/exact_commands.rs new file mode 100644 index 00000000..9d481668 --- /dev/null +++ b/crates/decodex-postgres/src/exact_commands.rs @@ -0,0 +1,81 @@ +use tokio_postgres::{Row, error::SqlState, types::ToSql}; + +use crate::{PostgresStore, StoreError}; + +pub(crate) const EXACT_COMMAND_PROTOCOL: &str = "decodex/exact-command/1"; +const MAX_EXACT_ATTEMPTS: usize = 4; + +impl PostgresStore { + pub(crate) async fn execute_exact_with_retry( + &self, + statement: &str, + parameters: &[&(dyn ToSql + Sync)], + ) -> Result, StoreError> { + let mut last_retryable = None; + + for _ in 0..MAX_EXACT_ATTEMPTS { + let mut client = match self.pool().get().await { + Ok(client) => client, + Err(error) => return Err(StoreError::Pool(error)), + }; + let transaction = match client.transaction().await { + Ok(transaction) => transaction, + Err(error) if is_retryable_exact_database_error(&error) => { + last_retryable = Some(error); + continue; + }, + Err(error) => return Err(StoreError::from(error)), + }; + let row = match transaction.query_one(statement, parameters).await { + Ok(row) => row, + Err(error) if is_retryable_exact_database_error(&error) => { + last_retryable = Some(error); + continue; + }, + Err(error) => return Err(StoreError::from(error)), + }; + let response = response_bytes(row)?; + + match transaction.commit().await { + Ok(()) => return Ok(response), + Err(error) if is_retryable_exact_database_error(&error) => { + last_retryable = Some(error); + }, + Err(error) => return Err(StoreError::from(error)), + } + } + + Err(StoreError::Database( + last_retryable.expect( + "an exhausted exact retry loop retains its classified infrastructure failure", + ), + )) + } +} + +fn response_bytes(row: Row) -> Result, StoreError> { + let response: Option> = row.get(0); + + response.ok_or_else(|| StoreError::Incompatible("exact command response is null".into())) +} + +pub(crate) fn validate_exact_key(key: &str) -> Result<(), StoreError> { + if key.is_empty() || key.len() > 256 { + return Err(StoreError::InvalidInput("idempotency key must contain 1..=256 bytes")); + } + + crate::ensure_credential_negative_text(key) +} + +fn is_retryable_exact_database_error(error: &tokio_postgres::Error) -> bool { + let Some(code) = error.code() else { + return false; + }; + + code == &SqlState::T_R_SERIALIZATION_FAILURE + || code == &SqlState::T_R_DEADLOCK_DETECTED + || code == &SqlState::ADMIN_SHUTDOWN + || code == &SqlState::CRASH_SHUTDOWN + || code == &SqlState::CANNOT_CONNECT_NOW + || code.code().starts_with("08") +} diff --git a/crates/decodex-postgres/src/lib.rs b/crates/decodex-postgres/src/lib.rs index e50e1796..daa7b939 100644 --- a/crates/decodex-postgres/src/lib.rs +++ b/crates/decodex-postgres/src/lib.rs @@ -11,6 +11,7 @@ mod accounts; mod authority; mod conversations; mod error; +mod exact_commands; mod leases; mod migrations; mod outbox; @@ -19,15 +20,15 @@ mod programs; mod project_agents; mod quota; mod role_profiles; +mod runtime_sessions; #[cfg(unix)] mod socket; mod types; pub use self::{ conversations::{ - BlobReclaimPage, ContextPackRecord, CreateArtifact, CreateConversation, - CreateRuntimeSession, HistoryCursor, HistoryEntry, HistoryPage, PersistContextPack, - ProposeTransition, RecordHistoryItem, StoredArtifact, StoredConversation, - StoredRuntimeSession, + BlobReclaimPage, ContextPackRecord, CreateArtifact, CreateConversation, HistoryCursor, + HistoryEntry, HistoryPage, PersistContextPack, ProposeTransition, RecordHistoryItem, + StoredArtifact, StoredConversation, }, error::{BootstrapFailure, StoreError}, programs::{ObjectiveRecord, ProgramRecord, UpdateProgramContext}, @@ -35,6 +36,11 @@ pub use self::{ BootstrapRoleProfiles, RoleProfileCommandOutcome, RoleProfileConfiguration, RoleProfileRejection, RoleProfileRevision, RoleProfileRole, }, + runtime_sessions::{ + CreateRuntimeSession, CreateRuntimeSessionAccountSnapshot, RuntimeSessionAccountSnapshot, + RuntimeSessionCommandEffect, RuntimeSessionCommandOutcome, RuntimeSessionProfileSnapshot, + RuntimeSessionRejection, StoredRuntimeSession, + }, types::{ AccountMetadata, AccountMutation, ActivityRecord, CommandIdentity, CreateProject, HypotheticalFallbackFact, LeaseClaim, OutboxClaim, OutboxReconciliation, OutboxState, @@ -292,6 +298,35 @@ impl PostgresStore { result } + /// Apply the immutable migration ledger only through V9 for the V10 upgrade proof. + #[cfg(all(unix, feature = "test-support"))] + #[doc(hidden)] + pub async fn migrate_fixture_through_v9( + mut config: Config, + expected_peer_uid: u32, + ) -> Result<(), StoreError> { + validate_connection(&config)?; + + let connector = verified_socket_connect(&config, expected_peer_uid)?; + + pin_session_search_path(&mut config); + + let manager = Manager::from_connect( + config, + connector.clone(), + ManagerConfig { recycling_method: RecyclingMethod::Fast }, + ); + let pool = Pool::builder(manager).max_size(1).build()?; + let mut client = checkout(&pool, &connector).await?; + let result = migrations::run_through_v9(&mut client).await; + + drop(client); + + pool.close(); + + result + } + #[cfg(not(unix))] pub async fn migrate(_config: Config, _expected_peer_uid: u32) -> Result<(), StoreError> { Err(StoreError::Incompatible("PostgreSQL Unix sockets require a Unix host".into())) diff --git a/crates/decodex-postgres/src/migrations.rs b/crates/decodex-postgres/src/migrations.rs index bdf2e106..eb4937e3 100644 --- a/crates/decodex-postgres/src/migrations.rs +++ b/crates/decodex-postgres/src/migrations.rs @@ -8,7 +8,7 @@ use deadpool_postgres::Client; use crate::{REQUIRED_POSTGRES_MAJOR, StoreError}; use embedded::migrations; -const EXPECTED_LATEST_MIGRATION_VERSION: i32 = 9; +const EXPECTED_LATEST_MIGRATION_VERSION: i32 = 10; pub(crate) async fn run(client: &mut Client) -> Result<(), StoreError> { migrations::runner().run_async(&mut ***client).await?; @@ -30,6 +30,13 @@ pub(crate) async fn run_through_v8(client: &mut Client) -> Result<(), StoreError Ok(()) } +#[cfg(feature = "test-support")] +pub(crate) async fn run_through_v9(client: &mut Client) -> Result<(), StoreError> { + migrations::runner().set_target(Target::Version(9)).run_async(&mut ***client).await?; + + Ok(()) +} + pub(crate) async fn verify(client: &Client) -> Result<(), StoreError> { let row = client .query_one( @@ -73,7 +80,7 @@ pub(crate) async fn verify(client: &Client) -> Result<(), StoreError> { != Some(EXPECTED_LATEST_MIGRATION_VERSION) { return Err(StoreError::Incompatible( - "embedded migration inventory does not end at the canonical V9 ledger".into(), + "embedded migration inventory does not end at the canonical V10 ledger".into(), )); } if actual.len() != expected.len() { diff --git a/crates/decodex-postgres/src/role_profiles.rs b/crates/decodex-postgres/src/role_profiles.rs index 762f71af..0bc3f8d8 100644 --- a/crates/decodex-postgres/src/role_profiles.rs +++ b/crates/decodex-postgres/src/role_profiles.rs @@ -1,10 +1,11 @@ +use crate::{ + PostgresStore, StoreError, + exact_commands::{EXACT_COMMAND_PROTOCOL, validate_exact_key}, +}; use serde_json::Value; -use tokio_postgres::{Row, error::SqlState, types::ToSql}; -use crate::{PostgresStore, StoreError}; - -const EXACT_COMMAND_PROTOCOL: &str = "decodex/exact-command/1"; -const MAX_EXACT_ATTEMPTS: usize = 4; +// Whole-transaction retry classification is shared with RuntimeSessions through +// exact_commands::is_retryable_exact_database_error; this module owns no second taxonomy. /// One of the four immutable global RoleProfile identities. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -19,7 +20,7 @@ pub enum RoleProfileRole { Reviewer, } impl RoleProfileRole { - const fn as_sql(self) -> &'static str { + pub(crate) const fn as_sql(self) -> &'static str { match self { Self::Advisor => "advisor", Self::Lead => "lead", @@ -28,7 +29,7 @@ impl RoleProfileRole { } } - fn from_sql(value: &str) -> Result { + pub(crate) fn from_sql(value: &str) -> Result { match value { "advisor" => Ok(Self::Advisor), "lead" => Ok(Self::Lead), @@ -175,79 +176,6 @@ impl PostgresStore { parse_update_response(&response) } - - async fn execute_exact_with_retry( - &self, - statement: &str, - parameters: &[&(dyn ToSql + Sync)], - ) -> Result, StoreError> { - let mut last_retryable = None; - - for _ in 0..MAX_EXACT_ATTEMPTS { - let mut client = match self.pool().get().await { - Ok(client) => client, - Err(error) => return Err(StoreError::Pool(error)), - }; - let transaction = match client.transaction().await { - Ok(transaction) => transaction, - Err(error) if is_retryable_exact_database_error(&error) => { - last_retryable = Some(error); - continue; - }, - Err(error) => return Err(StoreError::from(error)), - }; - let row = match transaction.query_one(statement, parameters).await { - Ok(row) => row, - Err(error) if is_retryable_exact_database_error(&error) => { - last_retryable = Some(error); - continue; - }, - Err(error) => return Err(StoreError::from(error)), - }; - let response = response_bytes(row)?; - - match transaction.commit().await { - Ok(()) => return Ok(response), - Err(error) if is_retryable_exact_database_error(&error) => { - last_retryable = Some(error); - }, - Err(error) => return Err(StoreError::from(error)), - } - } - - Err(StoreError::Database( - last_retryable.expect( - "an exhausted exact retry loop retains its classified infrastructure failure", - ), - )) - } -} - -fn response_bytes(row: Row) -> Result, StoreError> { - let response: Option> = row.get(0); - - response.ok_or_else(|| StoreError::Incompatible("exact RoleProfile response is null".into())) -} - -fn validate_exact_key(key: &str) -> Result<(), StoreError> { - if key.is_empty() || key.len() > 256 { - return Err(StoreError::InvalidInput("idempotency key must contain 1..=256 bytes")); - } - - crate::ensure_credential_negative_text(key) -} - -fn is_retryable_exact_database_error(error: &tokio_postgres::Error) -> bool { - let Some(code) = error.code() else { - return false; - }; - - code == &SqlState::T_R_SERIALIZATION_FAILURE - || code == &SqlState::T_R_DEADLOCK_DETECTED - || code == &SqlState::ADMIN_SHUTDOWN - || code == &SqlState::CRASH_SHUTDOWN - || code == &SqlState::CANNOT_CONNECT_NOW - || code.code().starts_with("08") } fn parse_bootstrap_response( @@ -382,6 +310,7 @@ fn optional_str(value: &Value, key: &str) -> Result, StoreError> #[cfg(test)] mod tests { use super::*; + use tokio_postgres::error::SqlState; fn profile(role: &str, revision: i64) -> Value { serde_json::json!({ diff --git a/crates/decodex-postgres/src/runtime_sessions.rs b/crates/decodex-postgres/src/runtime_sessions.rs new file mode 100644 index 00000000..5dba6963 --- /dev/null +++ b/crates/decodex-postgres/src/runtime_sessions.rs @@ -0,0 +1,894 @@ +use serde_json::Value; + +use crate::{ + PostgresStore, RoleProfileRole, StoreError, + exact_commands::{EXACT_COMMAND_PROTOCOL, validate_exact_key}, +}; +use decodex_core::{ + AccountId, AccountState, ConversationId, RuntimeSessionId, RuntimeSessionState, +}; + +/// Complete caller-observed, non-secret account facts consumed by one exact creation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CreateRuntimeSessionAccountSnapshot { + /// Caller-selected immutable snapshot identity. + pub account_snapshot_id: String, + /// Stable non-secret source account identity. + pub source_account_id: AccountId, + /// Exact display label observed at binding time. + pub display_label: String, + /// Exact inert account state observed at binding time. + pub observed_state: AccountState, + /// Positive source account revision observed at binding time. + pub source_revision: i64, +} + +/// Complete immutable non-secret account snapshot returned by PostgreSQL. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RuntimeSessionAccountSnapshot { + /// Caller-selected immutable snapshot identity. + pub account_snapshot_id: String, + /// Stable non-secret source account identity. + pub source_account_id: AccountId, + /// Exact display label observed at binding time. + pub display_label: String, + /// Exact inert account state observed at binding time. + pub observed_state: AccountState, + /// Positive source account revision observed at binding time. + pub source_revision: i64, + /// PostgreSQL-authored immutable creation timestamp. + pub created_at: String, +} + +/// Immutable full RoleProfile revision selected by PostgreSQL at session creation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RuntimeSessionProfileSnapshot { + /// PostgreSQL-generated immutable snapshot identity. + pub profile_snapshot_id: String, + /// Selected global role identity. + pub role: RoleProfileRole, + /// Selected immutable RoleProfile revision. + pub source_revision: i64, + /// Exact selected model. + pub model: String, + /// Exact selected reasoning effort. + pub reasoning_effort: String, + /// Exact selected service tier. + pub service_tier: String, + /// Digest of the exact selected instruction bytes. + pub instructions_digest: String, + /// Exact selected instruction bytes represented as UTF-8. + pub instructions: String, + /// Optional exact selected provenance. + pub provenance: Option, + /// PostgreSQL-authored immutable creation timestamp. + pub created_at: String, +} + +/// Typed inputs consumed by the exact RuntimeSession creation command. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CreateRuntimeSession { + /// Caller-selected RuntimeSession identity. + pub runtime_session_id: RuntimeSessionId, + /// Existing logical Conversation target. + pub conversation_id: ConversationId, + /// Role whose one current immutable revision PostgreSQL must select. + pub role: RoleProfileRole, + /// Complete non-secret account snapshot identity and facts. + pub account_snapshot: CreateRuntimeSessionAccountSnapshot, + /// Optional canonical Codex thread identity; this does not create a thread. + pub codex_thread_id: Option, + /// Initial observed state; PostgreSQL rejects terminal initial states stably. + pub initial_state: RuntimeSessionState, +} + +/// Complete committed RuntimeSession and immutable snapshot readback. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StoredRuntimeSession { + /// Stable RuntimeSession identity. + pub runtime_session_id: RuntimeSessionId, + /// Parent logical Conversation identity. + pub conversation_id: ConversationId, + /// PostgreSQL-selected full immutable RoleProfile snapshot. + pub profile_snapshot: RuntimeSessionProfileSnapshot, + /// Exact immutable non-secret account snapshot. + pub account_snapshot: RuntimeSessionAccountSnapshot, + /// Optional immutable Codex thread correlation. + pub codex_thread_id: Option, + /// Always null at creation and immutable in this command slice. + pub last_known_turn_id: Option, + /// Current persisted lifecycle state. + pub state: RuntimeSessionState, + /// Positive optimistic revision. + pub revision: i64, + /// PostgreSQL-authored creation timestamp. + pub created_at: String, + /// PostgreSQL-authored current revision timestamp. + pub updated_at: String, + /// PostgreSQL-authored terminal timestamp. + pub ended_at: Option, +} + +/// Complete canonical effect returned by a successful exact RuntimeSession command. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RuntimeSessionCommandEffect { + /// Complete current RuntimeSession and immutable snapshots. + pub runtime_session: StoredRuntimeSession, + /// State before the command; null only for creation. + pub prior_state: Option, + /// State after the command. + pub new_state: RuntimeSessionState, + /// Revision before the command; null only for creation. + pub prior_revision: Option, + /// Revision after the command. + pub new_revision: i64, + /// Canonical append-only activity identity. + pub activity_sequence: i64, + /// Exact activity payload stored by PostgreSQL. + pub activity_payload: Value, + /// Canonical outbox identity. + pub outbox_id: i64, + /// Exact outbox payload stored by PostgreSQL. + pub outbox_payload: Value, +} + +/// Stable domain rejection committed and replayed by an exact RuntimeSession command. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RuntimeSessionRejection { + /// The referenced Conversation, profile, or RuntimeSession does not exist. + MissingTarget, + /// The requested RuntimeSession identity already exists. + DuplicateTarget, + /// The expected RuntimeSession revision is no longer current. + StaleRevision, + /// The requested initial state or state transition is illegal. + IllegalTransition, + /// The supplied account snapshot facts are not valid non-secret snapshot facts. + InvalidAccountSnapshot, + /// An existing account snapshot identity is bound to different immutable facts. + AccountSnapshotConflict, +} + +/// Parsed exact command result; stable rejections are values, not infrastructure errors. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RuntimeSessionCommandOutcome { + /// The command committed and returned the authoritative RuntimeSession snapshot. + Success(T), + /// The command committed a stable domain rejection. + Rejected(RuntimeSessionRejection), +} + +impl PostgresStore { + /// Create one RuntimeSession and both snapshots through the command-complete V10 owner. + pub async fn create_runtime_session( + &self, + idempotency_key: &str, + create: &CreateRuntimeSession, + ) -> Result, StoreError> { + validate_exact_key(idempotency_key)?; + validate_uuid(&create.account_snapshot.account_snapshot_id, "account snapshot identity")?; + if let Some(thread_id) = &create.codex_thread_id { + validate_uuid(thread_id, "Codex thread identity")?; + } + + let role = create.role.as_sql(); + let observed_state = account_state_sql(create.account_snapshot.observed_state); + let initial_state = session_state_sql(create.initial_state); + let response = self + .execute_exact_with_retry( + "SELECT decodex.create_runtime_session_exact(\ + $1,$2,$3::text::uuid,$4::text::uuid,\ + $5::text::decodex.role_profile_role,$6::text::uuid,$7::text::uuid,\ + $8,$9::text::decodex.account_state,$10,$11::text::uuid,\ + $12::text::decodex.runtime_session_state)", + &[ + &EXACT_COMMAND_PROTOCOL, + &idempotency_key, + &create.runtime_session_id.as_str(), + &create.conversation_id.as_str(), + &role, + &create.account_snapshot.account_snapshot_id, + &create.account_snapshot.source_account_id.as_str(), + &create.account_snapshot.display_label, + &observed_state, + &create.account_snapshot.source_revision, + &create.codex_thread_id, + &initial_state, + ], + ) + .await?; + + parse_create_response(&response, create) + } + + /// Transition one RuntimeSession through the command-complete V10 owner. + pub async fn transition_runtime_session( + &self, + idempotency_key: &str, + session_id: &RuntimeSessionId, + expected_revision: i64, + target_state: RuntimeSessionState, + ) -> Result, StoreError> { + validate_exact_key(idempotency_key)?; + let target_state_sql = session_state_sql(target_state); + let response = self + .execute_exact_with_retry( + "SELECT decodex.transition_runtime_session_exact(\ + $1,$2,$3::text::uuid,$4,$5::text::decodex.runtime_session_state)", + &[ + &EXACT_COMMAND_PROTOCOL, + &idempotency_key, + &session_id.as_str(), + &expected_revision, + &target_state_sql, + ], + ) + .await?; + + parse_transition_response(&response, session_id, expected_revision, target_state) + } +} + +enum ResponseContext<'a> { + Create(&'a CreateRuntimeSession), + Transition { + session_id: &'a RuntimeSessionId, + expected_revision: i64, + target_state: RuntimeSessionState, + }, +} + +fn parse_create_response( + response: &[u8], + create: &CreateRuntimeSession, +) -> Result, StoreError> { + parse_response(response, &ResponseContext::Create(create)) +} + +fn parse_transition_response( + response: &[u8], + session_id: &RuntimeSessionId, + expected_revision: i64, + target_state: RuntimeSessionState, +) -> Result, StoreError> { + parse_response( + response, + &ResponseContext::Transition { session_id, expected_revision, target_state }, + ) +} + +fn parse_response( + response: &[u8], + context: &ResponseContext<'_>, +) -> Result, StoreError> { + let document: Value = serde_json::from_slice(response).map_err(|_| { + StoreError::Incompatible("exact RuntimeSession response bytes are invalid".into()) + })?; + + match document.get("classification").and_then(Value::as_str) { + Some("stable_domain_rejection") => { + validate_request_context(required_pointer(&document, "/effect/request")?, context)?; + return rejection_from_document(&document).map(RuntimeSessionCommandOutcome::Rejected); + }, + Some("success") => {}, + _ => { + return Err(StoreError::Incompatible( + "exact RuntimeSession response classification is invalid".into(), + )); + }, + } + + let effect = required_pointer(&document, "/effect")?; + validate_request_context(required_value(effect, "request")?, context)?; + let session = required_value(effect, "runtime_session")?; + let profile = profile_from_value(required_value(effect, "profile_snapshot")?)?; + let account = account_from_value(required_value(effect, "account_snapshot")?)?; + let state = session_state_from_sql(required_str(session, "state")?)?; + let revision = required_i64(session, "revision")?; + let new_state = session_state_from_sql(required_str(effect, "new_state")?)?; + let new_revision = positive_i64(effect, "new_revision")?; + let prior_state = optional_state(effect, "prior_state")?; + let prior_revision = optional_positive_i64(effect, "prior_revision")?; + let profile_snapshot_id = required_str(session, "profile_snapshot_id")?; + let account_snapshot_id = required_str(session, "account_snapshot_id")?; + if revision < 1 + || new_revision != revision + || new_state != state + || profile_snapshot_id != profile.profile_snapshot_id + || account_snapshot_id != account.account_snapshot_id + || match (prior_state, prior_revision, revision) { + (None, None, 1) => false, + (Some(_), Some(prior), current) => prior.checked_add(1) != Some(current), + _ => true, + } { + return Err(StoreError::Incompatible( + "exact RuntimeSession response effect is inconsistent".into(), + )); + } + + let runtime_session = StoredRuntimeSession { + runtime_session_id: RuntimeSessionId::new(required_str(session, "runtime_session_id")?) + .map_err(|_| { + StoreError::Incompatible("stored RuntimeSession identity is invalid".into()) + })?, + conversation_id: ConversationId::new(required_str(session, "conversation_id")?).map_err( + |_| StoreError::Incompatible("stored Conversation identity is invalid".into()), + )?, + profile_snapshot: profile, + account_snapshot: account, + codex_thread_id: optional_uuid(session, "codex_thread_id")?, + last_known_turn_id: optional_str(session, "last_known_turn_id")?, + state, + revision, + created_at: required_str(session, "created_at")?.to_owned(), + updated_at: required_str(session, "updated_at")?.to_owned(), + ended_at: optional_str(session, "ended_at")?, + }; + validate_effect_context(&runtime_session, prior_state, prior_revision, context)?; + let activity_sequence = positive_i64(effect, "activity_sequence")?; + let outbox_id = positive_i64(effect, "outbox_id")?; + let activity_payload = required_value(effect, "activity_payload")?.clone(); + let outbox_payload = required_value(effect, "outbox_payload")?.clone(); + let expected_event_kind = match context { + ResponseContext::Create(_) => "runtime_session_created", + ResponseContext::Transition { .. } => "runtime_session_transitioned", + }; + let session_id = runtime_session.runtime_session_id.as_str(); + let activity_transition_matches = match context { + ResponseContext::Create(_) => true, + ResponseContext::Transition { expected_revision, target_state, .. } => + activity_payload.get("prior_state").and_then(Value::as_str) + == prior_state.map(session_state_sql) + && activity_payload.get("new_state").and_then(Value::as_str) + == Some(session_state_sql(*target_state)) + && activity_payload.get("prior_revision").and_then(Value::as_i64) + == Some(*expected_revision) + && activity_payload.get("new_revision").and_then(Value::as_i64) + == Some(new_revision), + }; + if !activity_transition_matches + || required_str(effect, "activity_aggregate_kind")? != "runtime_session" + || required_str(effect, "activity_aggregate_id")? != session_id + || required_i64(effect, "activity_revision")? != new_revision + || required_str(effect, "activity_event_kind")? != expected_event_kind + || required_str(effect, "outbox_effect_key")? != format!("activity/{activity_sequence}") + || required_str(effect, "outbox_aggregate_kind")? != "runtime_session" + || required_str(effect, "outbox_aggregate_id")? != session_id + || required_i64(effect, "outbox_aggregate_revision")? != new_revision + || activity_payload.get("runtime_session") != Some(session) + || activity_payload.get("profile_snapshot") != effect.get("profile_snapshot") + || activity_payload.get("account_snapshot") != effect.get("account_snapshot") + || activity_payload.get("kind").and_then(Value::as_str) != Some("runtime_session") + || outbox_payload.get("activity_sequence").and_then(Value::as_i64) + != Some(activity_sequence) + || outbox_payload.get("payload") != Some(&activity_payload) + || outbox_payload.get("event_kind").and_then(Value::as_str) != Some(expected_event_kind) + || outbox_payload.get("aggregate_kind").and_then(Value::as_str) != Some("runtime_session") + || outbox_payload.get("aggregate_id").and_then(Value::as_str) != Some(session_id) + || outbox_payload.get("revision").and_then(Value::as_i64) != Some(new_revision) + { + return Err(StoreError::Incompatible( + "exact RuntimeSession audit effect is inconsistent".into(), + )); + } + + Ok(RuntimeSessionCommandOutcome::Success(RuntimeSessionCommandEffect { + runtime_session, + prior_state, + new_state, + prior_revision, + new_revision, + activity_sequence, + activity_payload, + outbox_id, + outbox_payload, + })) +} + +fn validate_request_context( + request: &Value, + context: &ResponseContext<'_>, +) -> Result<(), StoreError> { + let expected = match context { + ResponseContext::Create(create) => serde_json::json!({ + "protocol_version": EXACT_COMMAND_PROTOCOL, + "operation": "create_runtime_session", + "runtime_session_id": create.runtime_session_id.as_str(), + "conversation_id": create.conversation_id.as_str(), + "role": create.role.as_sql(), + "account_snapshot_id": create.account_snapshot.account_snapshot_id, + "source_account_id": create.account_snapshot.source_account_id.as_str(), + "display_label": create.account_snapshot.display_label, + "observed_state": account_state_sql(create.account_snapshot.observed_state), + "account_source_revision": create.account_snapshot.source_revision, + "codex_thread_id": create.codex_thread_id, + "initial_state": session_state_sql(create.initial_state), + }), + ResponseContext::Transition { session_id, expected_revision, target_state } => + serde_json::json!({ + "protocol_version": EXACT_COMMAND_PROTOCOL, + "operation": "transition_runtime_session", + "runtime_session_id": session_id.as_str(), + "expected_revision": expected_revision, + "target_state": session_state_sql(*target_state), + }), + }; + if request == &expected { + Ok(()) + } else { + Err(StoreError::Incompatible( + "exact RuntimeSession response belongs to a different request".into(), + )) + } +} + +fn validate_effect_context( + session: &StoredRuntimeSession, + prior_state: Option, + prior_revision: Option, + context: &ResponseContext<'_>, +) -> Result<(), StoreError> { + let valid = match context { + ResponseContext::Create(create) => + session.runtime_session_id == create.runtime_session_id + && session.conversation_id == create.conversation_id + && session.profile_snapshot.role == create.role + && session.account_snapshot.account_snapshot_id + == create.account_snapshot.account_snapshot_id + && session.account_snapshot.source_account_id + == create.account_snapshot.source_account_id + && session.account_snapshot.display_label == create.account_snapshot.display_label + && session.account_snapshot.observed_state == create.account_snapshot.observed_state + && session.account_snapshot.source_revision + == create.account_snapshot.source_revision + && session.codex_thread_id == create.codex_thread_id + && session.last_known_turn_id.is_none() + && session.state == create.initial_state + && session.revision == 1 + && prior_state.is_none() + && prior_revision.is_none(), + ResponseContext::Transition { session_id, expected_revision, target_state } => + session.runtime_session_id.as_str() == session_id.as_str() + && session.state == *target_state + && prior_revision == Some(*expected_revision) + && session.revision == expected_revision.checked_add(1).unwrap_or(i64::MIN) + && matches!( + (prior_state, target_state), + (Some(RuntimeSessionState::Starting), RuntimeSessionState::Active) + | (Some(RuntimeSessionState::Starting), RuntimeSessionState::Ended) + | (Some(RuntimeSessionState::Starting), RuntimeSessionState::Diverged) + | (Some(RuntimeSessionState::Active), RuntimeSessionState::Ended) + | (Some(RuntimeSessionState::Active), RuntimeSessionState::Diverged) + ), + }; + if valid { + Ok(()) + } else { + Err(StoreError::Incompatible( + "exact RuntimeSession response does not match the command request".into(), + )) + } +} + +fn rejection_from_document(document: &Value) -> Result { + let code = document.get("code").and_then(Value::as_str); + let effect = required_pointer(document, "/effect")?; + if effect.get("changed").and_then(Value::as_bool) != Some(false) + || effect.get("code").and_then(Value::as_str) != code + { + return Err(StoreError::Incompatible( + "exact RuntimeSession rejection effect is inconsistent".into(), + )); + } + + match code { + Some("missing_target") => Ok(RuntimeSessionRejection::MissingTarget), + Some("duplicate_target") => Ok(RuntimeSessionRejection::DuplicateTarget), + Some("stale_revision") => Ok(RuntimeSessionRejection::StaleRevision), + Some("illegal_transition") => Ok(RuntimeSessionRejection::IllegalTransition), + Some("invalid_account_snapshot") => Ok(RuntimeSessionRejection::InvalidAccountSnapshot), + Some("account_snapshot_conflict") => Ok(RuntimeSessionRejection::AccountSnapshotConflict), + _ => Err(StoreError::Incompatible("exact RuntimeSession rejection code is invalid".into())), + } +} + +fn profile_from_value(value: &Value) -> Result { + let source_profile_id = required_str(value, "source_profile_id")?; + let role = RoleProfileRole::from_sql(required_str(value, "role")?)?; + if source_profile_id != role.as_sql() { + return Err(StoreError::Incompatible( + "stored RoleProfile snapshot identity is inconsistent".into(), + )); + } + + Ok(RuntimeSessionProfileSnapshot { + profile_snapshot_id: required_uuid(value, "profile_snapshot_id")?, + role, + source_revision: positive_i64(value, "source_revision")?, + model: required_str(value, "model")?.to_owned(), + reasoning_effort: required_str(value, "reasoning_effort")?.to_owned(), + service_tier: required_str(value, "service_tier")?.to_owned(), + instructions_digest: required_str(value, "instructions_digest")?.to_owned(), + instructions: required_str(value, "instructions")?.to_owned(), + provenance: optional_str(value, "provenance")?, + created_at: required_str(value, "created_at")?.to_owned(), + }) +} + +fn account_from_value(value: &Value) -> Result { + Ok(RuntimeSessionAccountSnapshot { + account_snapshot_id: required_uuid(value, "account_snapshot_id")?, + source_account_id: AccountId::new(required_str(value, "source_account_id")?) + .map_err(|_| StoreError::Incompatible("stored account identity is invalid".into()))?, + display_label: required_str(value, "display_label")?.to_owned(), + observed_state: account_state_from_sql(required_str(value, "observed_state")?)?, + source_revision: positive_i64(value, "source_revision")?, + created_at: required_str(value, "created_at")?.to_owned(), + }) +} + +fn required_pointer<'a>(value: &'a Value, pointer: &str) -> Result<&'a Value, StoreError> { + value.pointer(pointer).ok_or_else(|| { + StoreError::Incompatible("exact RuntimeSession response shape is incomplete".into()) + }) +} + +fn required_value<'a>(value: &'a Value, key: &str) -> Result<&'a Value, StoreError> { + value + .get(key) + .ok_or_else(|| StoreError::Incompatible("exact RuntimeSession effect is incomplete".into())) +} + +fn required_str<'a>(value: &'a Value, key: &str) -> Result<&'a str, StoreError> { + value + .get(key) + .and_then(Value::as_str) + .ok_or_else(|| StoreError::Incompatible("stored RuntimeSession text is invalid".into())) +} + +fn required_i64(value: &Value, key: &str) -> Result { + value + .get(key) + .and_then(Value::as_i64) + .ok_or_else(|| StoreError::Incompatible("stored RuntimeSession integer is invalid".into())) +} + +fn positive_i64(value: &Value, key: &str) -> Result { + let result = required_i64(value, key)?; + if result < 1 { + return Err(StoreError::Incompatible("stored RuntimeSession revision is invalid".into())); + } + Ok(result) +} + +fn optional_str(value: &Value, key: &str) -> Result, StoreError> { + match value.get(key) { + Some(Value::Null) => Ok(None), + Some(Value::String(value)) => Ok(Some(value.clone())), + _ => Err(StoreError::Incompatible("stored RuntimeSession optional text is invalid".into())), + } +} + +fn optional_uuid(value: &Value, key: &str) -> Result, StoreError> { + match optional_str(value, key)? { + Some(value) if is_uuid(&value) => Ok(Some(value)), + Some(_) => Err(StoreError::Incompatible("stored RuntimeSession UUID is invalid".into())), + None => Ok(None), + } +} + +fn optional_state(value: &Value, key: &str) -> Result, StoreError> { + match value.get(key) { + Some(Value::Null) => Ok(None), + Some(Value::String(value)) => session_state_from_sql(value).map(Some), + _ => + Err(StoreError::Incompatible("stored RuntimeSession optional state is invalid".into())), + } +} + +fn optional_positive_i64(value: &Value, key: &str) -> Result, StoreError> { + match value.get(key) { + Some(Value::Null) => Ok(None), + Some(Value::Number(value)) => { + let value = value.as_i64().filter(|value| *value > 0).ok_or_else(|| { + StoreError::Incompatible( + "stored RuntimeSession optional revision is invalid".into(), + ) + })?; + Ok(Some(value)) + }, + _ => Err(StoreError::Incompatible( + "stored RuntimeSession optional revision is invalid".into(), + )), + } +} + +fn required_uuid(value: &Value, key: &str) -> Result { + let value = required_str(value, key)?.to_owned(); + if is_uuid(&value) { + Ok(value) + } else { + Err(StoreError::Incompatible("stored RuntimeSession UUID is invalid".into())) + } +} + +fn validate_uuid(value: &str, field: &'static str) -> Result<(), StoreError> { + if is_uuid(value) { Ok(()) } else { Err(StoreError::InvalidInput(field)) } +} + +fn is_uuid(value: &str) -> bool { + value.len() == 36 + && value.bytes().enumerate().all(|(index, byte)| match index { + 8 | 13 | 18 | 23 => byte == b'-', + _ => byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte), + }) +} + +const fn session_state_sql(value: RuntimeSessionState) -> &'static str { + match value { + RuntimeSessionState::Starting => "starting", + RuntimeSessionState::Active => "active", + RuntimeSessionState::Ended => "ended", + RuntimeSessionState::Diverged => "diverged", + } +} + +fn session_state_from_sql(value: &str) -> Result { + match value { + "starting" => Ok(RuntimeSessionState::Starting), + "active" => Ok(RuntimeSessionState::Active), + "ended" => Ok(RuntimeSessionState::Ended), + "diverged" => Ok(RuntimeSessionState::Diverged), + _ => Err(StoreError::Incompatible("stored RuntimeSession state is invalid".into())), + } +} + +const fn account_state_sql(value: AccountState) -> &'static str { + match value { + AccountState::Unavailable => "unavailable", + AccountState::Unknown => "unknown", + AccountState::Available => "available", + AccountState::Depleted => "depleted", + AccountState::AuthFailed => "auth_failed", + AccountState::PluginUnready => "plugin_unready", + AccountState::Disabled => "disabled", + } +} + +fn account_state_from_sql(value: &str) -> Result { + match value { + "unavailable" => Ok(AccountState::Unavailable), + "unknown" => Ok(AccountState::Unknown), + "available" => Ok(AccountState::Available), + "depleted" => Ok(AccountState::Depleted), + "auth_failed" => Ok(AccountState::AuthFailed), + "plugin_unready" => Ok(AccountState::PluginUnready), + "disabled" => Ok(AccountState::Disabled), + _ => Err(StoreError::Incompatible("stored account state is invalid".into())), + } +} + +#[cfg(test)] +mod tests { + use super::{ + CreateRuntimeSession, CreateRuntimeSessionAccountSnapshot, RuntimeSessionRejection, + parse_create_response, parse_transition_response, + }; + use crate::{RoleProfileRole, RuntimeSessionCommandOutcome, StoreError}; + use decodex_core::{ + AccountId, AccountState, ConversationId, RuntimeSessionId, RuntimeSessionState, + }; + use serde_json::json; + + fn create() -> CreateRuntimeSession { + CreateRuntimeSession { + runtime_session_id: RuntimeSessionId::new("41000000-0000-4000-8000-000000000001") + .unwrap(), + conversation_id: ConversationId::new("40000000-0000-4000-8000-000000000001").unwrap(), + role: RoleProfileRole::Task, + account_snapshot: CreateRuntimeSessionAccountSnapshot { + account_snapshot_id: "43000000-0000-4000-8000-000000000001".into(), + source_account_id: AccountId::new("13000000-0000-4000-8000-000000000001").unwrap(), + display_label: "Account".into(), + observed_state: AccountState::Unknown, + source_revision: 1, + }, + codex_thread_id: None, + initial_state: RuntimeSessionState::Starting, + } + } + + fn create_request() -> serde_json::Value { + json!({ + "protocol_version": "decodex/exact-command/1", + "operation": "create_runtime_session", + "runtime_session_id": "41000000-0000-4000-8000-000000000001", + "conversation_id": "40000000-0000-4000-8000-000000000001", + "role": "task", + "account_snapshot_id": "43000000-0000-4000-8000-000000000001", + "source_account_id": "13000000-0000-4000-8000-000000000001", + "display_label": "Account", + "observed_state": "unknown", + "account_source_revision": 1, + "codex_thread_id": null, + "initial_state": "starting" + }) + } + + #[test] + fn stable_rejection_parser_is_closed() { + let session_id = RuntimeSessionId::new("41000000-0000-4000-8000-000000000001").unwrap(); + let response = json!({ + "classification": "stable_domain_rejection", + "code": "stale_revision", + "effect": { + "changed": false, + "code": "stale_revision", + "request": { + "protocol_version": "decodex/exact-command/1", + "operation": "transition_runtime_session", + "runtime_session_id": session_id.as_str(), + "expected_revision": 1, + "target_state": "active" + } + } + }); + assert_eq!( + parse_transition_response( + &serde_json::to_vec(&response).unwrap(), + &session_id, + 1, + RuntimeSessionState::Active, + ) + .unwrap(), + RuntimeSessionCommandOutcome::Rejected(RuntimeSessionRejection::StaleRevision), + ); + assert!( + parse_transition_response( + &serde_json::to_vec(&response).unwrap(), + &session_id, + 2, + RuntimeSessionState::Active, + ) + .is_err() + ); + } + + #[test] + fn success_parser_requires_complete_immutable_snapshots() { + let incomplete = br#"{"classification":"success","effect":{"runtime_session":{}}}"#; + assert!(parse_create_response(incomplete, &create()).is_err()); + } + + #[test] + fn success_parser_closes_snapshot_and_audit_cross_references() { + let session = json!({ + "runtime_session_id": "41000000-0000-4000-8000-000000000001", + "conversation_id": "40000000-0000-4000-8000-000000000001", + "profile_snapshot_id": "42000000-0000-4000-8000-000000000001", + "account_snapshot_id": "43000000-0000-4000-8000-000000000001", + "codex_thread_id": null, + "last_known_turn_id": null, + "state": "starting", + "revision": 1, + "created_at": "2026-07-17T00:00:00Z", + "updated_at": "2026-07-17T00:00:00Z", + "ended_at": null + }); + let profile = json!({ + "profile_snapshot_id": "42000000-0000-4000-8000-000000000001", + "source_profile_id": "task", + "role": "task", + "source_revision": 1, + "model": "gpt-5.6-sol", + "reasoning_effort": "medium", + "service_tier": "priority", + "instructions_digest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "instructions": "Own the task.", + "provenance": null, + "created_at": "2026-07-17T00:00:00Z" + }); + let account = json!({ + "account_snapshot_id": "43000000-0000-4000-8000-000000000001", + "source_account_id": "13000000-0000-4000-8000-000000000001", + "display_label": "Account", + "observed_state": "unknown", + "source_revision": 1, + "created_at": "2026-07-17T00:00:00Z" + }); + let activity = json!({ + "kind": "runtime_session", + "runtime_session": session, + "profile_snapshot": profile, + "account_snapshot": account + }); + let outbox = json!({ + "activity_sequence": 1, + "event_kind": "runtime_session_created", + "aggregate_kind": "runtime_session", + "aggregate_id": "41000000-0000-4000-8000-000000000001", + "revision": 1, + "payload": activity + }); + let response = json!({ + "classification": "success", + "effect": { + "request": create_request(), + "runtime_session": session, + "profile_snapshot": profile, + "account_snapshot": account, + "prior_state": null, + "new_state": "starting", + "prior_revision": null, + "new_revision": 1, + "activity_sequence": 1, + "activity_aggregate_kind": "runtime_session", + "activity_aggregate_id": "41000000-0000-4000-8000-000000000001", + "activity_revision": 1, + "activity_event_kind": "runtime_session_created", + "activity_payload": activity, + "outbox_id": 1, + "outbox_effect_key": "activity/1", + "outbox_aggregate_kind": "runtime_session", + "outbox_aggregate_id": "41000000-0000-4000-8000-000000000001", + "outbox_aggregate_revision": 1, + "outbox_payload": outbox + } + }); + let parsed = + parse_create_response(&serde_json::to_vec(&response).unwrap(), &create()).unwrap(); + let RuntimeSessionCommandOutcome::Success(effect) = parsed else { + panic!("golden response must parse") + }; + assert_eq!(effect.new_state, RuntimeSessionState::Starting); + assert_eq!(effect.activity_sequence, 1); + + let mut substituted = response; + substituted["effect"]["runtime_session"]["profile_snapshot_id"] = + json!("42000000-0000-4000-8000-000000000099"); + assert!( + parse_create_response(&serde_json::to_vec(&substituted).unwrap(), &create()).is_err() + ); + } + + #[test] + fn success_parser_rejects_wrong_request_and_malformed_stored_uuid_as_incompatible() { + let incomplete = json!({ + "classification": "success", + "effect": {"request": create_request(), "runtime_session": {}} + }); + let mut wrong = create(); + wrong.account_snapshot.display_label = "Other account".into(); + assert!(matches!( + parse_create_response(&serde_json::to_vec(&incomplete).unwrap(), &wrong), + Err(StoreError::Incompatible(_)) + )); + + let malformed = json!({ + "profile_snapshot_id": "not-a-uuid", + "source_profile_id": "task", + "role": "task", + "source_revision": 1, + "model": "m", + "reasoning_effort": "medium", + "service_tier": "priority", + "instructions_digest": "d", + "instructions": "i", + "provenance": null, + "created_at": "t" + }); + let malformed_response = json!({ + "classification": "success", + "effect": { + "request": create_request(), + "runtime_session": {}, + "profile_snapshot": malformed, + "account_snapshot": {} + } + }); + assert!(matches!( + parse_create_response(&serde_json::to_vec(&malformed_response).unwrap(), &create()), + Err(StoreError::Incompatible(_)) + )); + } +} diff --git a/crates/decodex-postgres/tests/postgres_store.rs b/crates/decodex-postgres/tests/postgres_store.rs index 1746bf76..3d6e1210 100644 --- a/crates/decodex-postgres/tests/postgres_store.rs +++ b/crates/decodex-postgres/tests/postgres_store.rs @@ -2,7 +2,11 @@ #[path = "postgres_store/quota.rs"] mod quota; #[cfg(feature = "test-support")] -#[path = "postgres_store/role_profiles.rs"] mod role_profiles; +#[path = "postgres_store/role_profiles.rs"] +mod role_profiles; +#[cfg(feature = "test-support")] +#[path = "postgres_store/runtime_sessions.rs"] +mod runtime_sessions; use std::{ collections::{BTreeMap, HashSet}, @@ -26,25 +30,27 @@ use tokio::{ use tokio_postgres::{Client, Config, NoTls, Row, types::ToSql}; use decodex_core::{ - self, AcceptedPolicyRevision, AccountSnapshot, ArtifactId, ArtifactStatus, Availability, - BlobHash, BlobStore, ContextPack, ContextPackInput, ContextPackPolicy, ContextPackSource, - ContextSourceKind, ConversationId, DecodexRoot, HistoryItemId, HistoryItemKind, - HistoryMediaType, HistoryMetadata, HistoryMetadataValue, ItemStatus, Objective, - ObjectiveCompletionEvidence, ObjectiveEvidenceId, ObjectiveId, ObjectiveState, - PinnedContextSource, PolicyId, PolicyProvenance, PolicyRevision, PolicyRevisionAcceptance, - PolicyRevisionId, PolicySnapshot, PolicySnapshotValue, PossibleSideEffects, ProductState as _, - ProfileSnapshot, Program, ProgramContextInput, ProgramCorrelationId, ProgramId, ProgramMetric, - ProgramObservationId, ProgramObservationProvenance, ProgramProvenance, ProgramSignal, - ProgramState, ProgramTimestamp, Project, ProjectId, ProjectMetadata, ProjectMetadataValue, - ProjectRepositoryBinding, ProjectStatus, ProposedTransitionKind, RepositoryIdentity, - ReviewCadence, RuntimeSessionId, RuntimeSessionState, TurnId, TurnRole, TurnStatus, + self, AcceptedPolicyRevision, ArtifactId, ArtifactStatus, Availability, BlobHash, BlobStore, + ContextPack, ContextPackInput, ContextPackPolicy, ContextPackSource, ContextSourceKind, + ConversationId, DecodexRoot, HistoryItemId, HistoryItemKind, HistoryMediaType, HistoryMetadata, + HistoryMetadataValue, ItemStatus, Objective, ObjectiveCompletionEvidence, ObjectiveEvidenceId, + ObjectiveId, ObjectiveState, PinnedContextSource, PolicyId, PolicyProvenance, PolicyRevision, + PolicyRevisionAcceptance, PolicyRevisionId, PolicySnapshot, PolicySnapshotValue, + PossibleSideEffects, ProductState as _, Program, ProgramContextInput, ProgramCorrelationId, + ProgramId, ProgramMetric, ProgramObservationId, ProgramObservationProvenance, + ProgramProvenance, ProgramSignal, ProgramState, ProgramTimestamp, Project, ProjectId, + ProjectMetadata, ProjectMetadataValue, ProjectRepositoryBinding, ProjectStatus, + ProposedTransitionKind, RepositoryIdentity, ReviewCadence, RuntimeSessionId, + RuntimeSessionState, TurnId, TurnRole, TurnStatus, }; use decodex_postgres::{ - AccountId, AccountMutation, AccountState, Agent, AgentId, AgentRole, AgentStatus, CLOSED, - CommandIdentity, ContextPackRecord, CreateArtifact, CreateConversation, CreateProject, - CreateRuntimeSession, HistoryCursor, MAX_OPERATION_DURATION_MILLISECONDS, OutboxClaim, - OutboxReconciliation, PersistContextPack, PostgresStore, ProposeTransition, - ReconciliationOutcome, RecordHistoryItem, StoreError, UpdateProgramContext, + AccountId, AccountMutation, AccountState, Agent, AgentId, AgentRole, AgentStatus, + BootstrapRoleProfiles, CLOSED, CommandIdentity, ContextPackRecord, CreateArtifact, + CreateConversation, CreateProject, CreateRuntimeSession, CreateRuntimeSessionAccountSnapshot, + HistoryCursor, MAX_OPERATION_DURATION_MILLISECONDS, OutboxClaim, OutboxReconciliation, + PersistContextPack, PostgresStore, ProposeTransition, ReconciliationOutcome, RecordHistoryItem, + RoleProfileCommandOutcome, RoleProfileConfiguration, RoleProfileRole, + RuntimeSessionCommandOutcome, StoreError, UpdateProgramContext, }; const ACCOUNT_ID: &str = "10000000-0000-0000-0000-000000000001"; @@ -73,6 +79,26 @@ const CREDENTIAL_VALUE_VECTORS: &[&str] = &[ "AKIA0123456789ABCDEF", ]; const UNICODE_WHITESPACE_VECTORS: &[&str] = &["\u{a0}", "\u{85}", "\u{202f}", "\u{3000}"]; + +fn exact_profile(marker: &str) -> RoleProfileConfiguration { + RoleProfileConfiguration { + model: "test-model".into(), + reasoning_effort: "medium".into(), + service_tier: "standard".into(), + instructions: format!("Exact {marker} fixture instructions."), + provenance: Some("XY-1337 integration fixture".into()), + } +} + +fn exact_account_snapshot(snapshot_id: String) -> CreateRuntimeSessionAccountSnapshot { + CreateRuntimeSessionAccountSnapshot { + account_snapshot_id: snapshot_id, + source_account_id: AccountId::new(ACCOUNT_ID).expect("fixture account ID is canonical"), + display_label: "Manual A".into(), + observed_state: AccountState::Unknown, + source_revision: 3, + } +} const RUNTIME_EXECUTE_SIGNATURES: &[&str] = &[ "decodex.is_canonical_media_type(pg_catalog.text)", "decodex.is_history_metadata_projection(pg_catalog.jsonb)", @@ -102,6 +128,8 @@ const RUNTIME_EXECUTE_SIGNATURES: &[&str] = &[ "decodex.achieve_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text)", "decodex.bootstrap_role_profiles_exact(pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", "decodex.update_role_profile_exact(pg_catalog.text,pg_catalog.text,decodex.role_profile_role,pg_catalog.int8,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "decodex.create_runtime_session_exact(pg_catalog.text,pg_catalog.text,pg_catalog.uuid,pg_catalog.uuid,decodex.role_profile_role,pg_catalog.uuid,pg_catalog.uuid,pg_catalog.text,decodex.account_state,pg_catalog.int8,pg_catalog.uuid,decodex.runtime_session_state)", + "decodex.transition_runtime_session_exact(pg_catalog.text,pg_catalog.text,pg_catalog.uuid,pg_catalog.int8,decodex.runtime_session_state)", ]; const TRIGGER_ONLY_SIGNATURES: &[&str] = &[ "decodex.enforce_lease_operation_time()", @@ -137,6 +165,9 @@ const TRIGGER_ONLY_SIGNATURES: &[&str] = &[ "decodex.forbid_role_profile_revision_mutation()", "decodex.forbid_role_profile_truncate()", "decodex.enforce_role_profile_event_namespace()", + "decodex.enforce_runtime_session_command_owner()", + "decodex.forbid_runtime_snapshot_mutation()", + "decodex.enforce_runtime_session_event_namespace()", ]; const INVALID_PROJECT_AGENT_SQL_CALLS: &[(&str, &str)] = &[ ( @@ -359,13 +390,13 @@ async fn postgres_session_search_path_startup_fixture() -> Result<(), Box Result<(), Box> { let path = env::var("DECODEX_SCHEMA_MANIFEST_PATH")?; - let (mut migration, runtime) = separated_configs("DECODEX_TEST")?; + let (migration, mut runtime) = separated_configs("DECODEX_TEST")?; let migration_role = migration.get_user().ok_or("migration role is absent")?.to_owned(); let runtime_role = runtime.get_user().ok_or("runtime role is absent")?.to_owned(); - PostgresStore::pin_session_search_path_fixture(&mut migration); + PostgresStore::pin_session_search_path_fixture(&mut runtime); - let (client, connection) = migration.connect(NoTls).await?; + let (client, connection) = runtime.connect(NoTls).await?; let connection_task = tokio::spawn(connection); let schema: String = client.query_one(PostgresStore::schema_contract_sql_fixture(), &[]).await?.get(0); @@ -390,6 +421,15 @@ async fn postgres_schema_manifest_dump_fixture() -> Result<(), Box Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = PostgresStore::connect(migration, runtime, expected_peer_uid()).await?; + store.close(); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[ignore = "requires a fresh isolated PostgreSQL 18 V8 migration database"] #[cfg(feature = "test-support")] @@ -1082,24 +1122,6 @@ async fn assert_concurrent_hierarchy_serialization( setup.batch_execute("INSERT INTO decodex.blob_objects (blob_hash,byte_length,verified_at) VALUES (repeat('a',64),1,clock_timestamp() + interval '1 second') ON CONFLICT DO NOTHING").await?; - assert_parent_child_race( - setup, - &client_a, - &client_b, - &format!("INSERT INTO decodex.runtime_sessions (runtime_session_id,conversation_id,profile_snapshot_id,account_snapshot_id,state) VALUES ('{}','49000000-0000-4000-8000-000000000001','{profile}','{account}','active')", session(1)), - "UPDATE decodex.conversations SET status='archived',revision=revision+1 WHERE conversation_id='49000000-0000-4000-8000-000000000001'", - "SELECT status='archived' AND NOT EXISTS (SELECT 1 FROM decodex.runtime_sessions WHERE conversation_id='49000000-0000-4000-8000-000000000001') FROM decodex.conversations WHERE conversation_id='49000000-0000-4000-8000-000000000001'", - ).await?; - - setup.execute(&format!("INSERT INTO decodex.runtime_sessions (runtime_session_id,conversation_id,profile_snapshot_id,account_snapshot_id,state) VALUES ('{}','49000000-0000-4000-8000-000000000002','{profile}','{account}','active')", session(2)), &[]).await?; - - assert_parent_child_race( - setup, &client_a, &client_b, - &format!("INSERT INTO decodex.turns (turn_id,conversation_id,runtime_session_id,sequence,role) VALUES ('{}','49000000-0000-4000-8000-000000000002','{}',1,'assistant')", turn(2), session(2)), - &format!("UPDATE decodex.runtime_sessions SET state='ended',revision=revision+1 WHERE runtime_session_id='{}'", session(2)), - &format!("SELECT state='ended' AND NOT EXISTS (SELECT 1 FROM decodex.turns WHERE runtime_session_id='{}') FROM decodex.runtime_sessions WHERE runtime_session_id='{}'", session(2), session(2)), - ).await?; - setup.execute(&format!("INSERT INTO decodex.runtime_sessions (runtime_session_id,conversation_id,profile_snapshot_id,account_snapshot_id,state) VALUES ('{}','49000000-0000-4000-8000-000000000003','{profile}','{account}','active')", session(3)), &[]).await?; setup.execute(&format!("INSERT INTO decodex.turns (turn_id,conversation_id,runtime_session_id,sequence,role) VALUES ('{}','49000000-0000-4000-8000-000000000003','{}',1,'assistant')", turn(3), session(3)), &[]).await?; @@ -1174,7 +1196,7 @@ async fn assert_executor_prelock_order( opponent: &Client, ) -> Result<(), Box> { let blob_store = isolated_blob_store()?; - let (conversation_id, artifact_id, session_id, turn_id) = + let (conversation_id, artifact_id, _session_id, turn_id) = seed_lock_order_fixture(observer, &blob_store, 24).await?; let history_item_id: String = observer .query_one( @@ -1188,9 +1210,6 @@ async fn assert_executor_prelock_order( format!( "UPDATE decodex.conversations SET revision=revision WHERE conversation_id='{conversation_id}'" ), - format!( - "UPDATE decodex.runtime_sessions SET revision=revision WHERE runtime_session_id='{session_id}'" - ), format!("UPDATE decodex.turns SET revision=revision WHERE turn_id='{turn_id}'"), format!( "UPDATE decodex.history_items SET revision=revision WHERE history_item_id='{history_item_id}'" @@ -1201,9 +1220,6 @@ async fn assert_executor_prelock_order( format!( "SELECT 1 FROM decodex.conversations WHERE conversation_id='{conversation_id}' FOR UPDATE" ), - format!( - "SELECT 1 FROM decodex.runtime_sessions WHERE runtime_session_id='{session_id}' FOR UPDATE" - ), format!("SELECT 1 FROM decodex.turns WHERE turn_id='{turn_id}' FOR UPDATE"), format!( "SELECT 1 FROM decodex.history_items WHERE history_item_id='{history_item_id}' FOR UPDATE" @@ -2399,7 +2415,7 @@ async fn assert_bootstrap_and_history(client: &Client) -> Result<(), Box Result<(), Box Result<(), Box> { let sync = PathBuf::from(env::var("DECODEX_TEST_BLOB_ROOT")?).join("post-commit-sync"); @@ -5546,9 +5552,7 @@ async fn assert_exact_receipt_responses_survive_later_mutation( // SAFETY: the isolated contract is one test process and removes this test-only variable below. unsafe { env::set_var("DECODEX_TEST_POST_COMMIT_SYNC", &sync) }; - let session_id = - assert_session_receipt_response(store, runtime_client, fixture, profile, account, &sync) - .await?; + let session_id = assert_session_receipt_response(store, runtime_client, fixture).await?; assert_history_receipt_response(store, runtime_client, fixture, &session_id, &sync).await?; @@ -5564,56 +5568,40 @@ async fn assert_session_receipt_response( store: &PostgresStore, runtime_client: &Client, fixture: &ConversationFixture, - profile: &ProfileSnapshot, - account: &AccountSnapshot, - sync: &Path, ) -> Result> { let session_id = RuntimeSessionId::new("41000000-0000-4000-8000-000000000088")?; let create_session = CreateRuntimeSession { runtime_session_id: session_id.clone(), conversation_id: fixture.conversation_id.clone(), - profile_snapshot_id: "42000000-0000-4000-8000-000000000088".into(), - account_snapshot_id: "43000000-0000-4000-8000-000000000088".into(), - profile_snapshot: profile.clone(), - account_snapshot: account.clone(), - codex_thread_id: Some("receipt-response-thread".into()), - state: RuntimeSessionState::Starting, + role: RoleProfileRole::Task, + account_snapshot: exact_account_snapshot("43000000-0000-4000-8000-000000000088".into()), + codex_thread_id: Some("44000000-0000-4000-8000-000000000088".into()), + initial_state: RuntimeSessionState::Starting, }; - let first_store = store.clone(); - let first_create = create_session.clone(); - let session_committed = sync.join("runtime_session.committed"); - let mut session_worker = tokio::spawn(async move { - first_store - .create_runtime_session( - &CommandIdentity::new("receipt-response-session", b"receipt-response-session-v1")?, - &first_create, + let RuntimeSessionCommandOutcome::Success(first_session) = + store.create_runtime_session("receipt-response-session", &create_session).await? + else { + panic!("exact session creation must succeed"); + }; + assert!(matches!( + store + .transition_runtime_session( + "receipt-response-session-active", + &session_id, + 1, + RuntimeSessionState::Active, ) - .await - }); - - tokio::select! { - result = &mut session_worker => return Err(format!("session response worker exited before barrier: {result:?}").into()), - result = wait_for_path(&session_committed) => result?, - } - - runtime_client.execute( - "UPDATE decodex.runtime_sessions SET state='active',revision=revision+1,updated_at=clock_timestamp() \ - WHERE runtime_session_id=$1::text::uuid", - &[&session_id.as_str()], - ).await?; - - fs::write(sync.join("runtime_session.continue"), b"continue")?; - - let first_session = session_worker.await??; - let replay_session = store - .create_runtime_session( - &CommandIdentity::new("receipt-response-session", b"receipt-response-session-v1")?, - &create_session, - ) - .await?; + .await?, + RuntimeSessionCommandOutcome::Success(_) + )); + let RuntimeSessionCommandOutcome::Success(replay_session) = + store.create_runtime_session("receipt-response-session", &create_session).await? + else { + panic!("exact session replay must succeed"); + }; assert_eq!(first_session, replay_session); - assert_eq!(first_session.state, RuntimeSessionState::Starting); + assert_eq!(first_session.runtime_session.state, RuntimeSessionState::Starting); let current_session: String = runtime_client .query_one( @@ -5625,9 +5613,6 @@ async fn assert_session_receipt_response( assert_eq!(current_session, "active"); - fs::remove_file(sync.join("runtime_session.committed"))?; - fs::remove_file(sync.join("runtime_session.continue"))?; - Ok(session_id) } @@ -5732,82 +5717,70 @@ async fn assert_history_receipt_response( } async fn assert_initial_lifecycle_timestamps_are_canonical( + store: &PostgresStore, runtime_client: &Client, conversation_id: &ConversationId, ) -> Result<(), Box> { - runtime_client - .batch_execute( - "INSERT INTO decodex.profile_snapshots \ - (profile_snapshot_id,source_profile_id,role,model,reasoning_effort,service_tier, \ - instructions_digest,source_revision,created_at) \ - VALUES ('42000000-0000-4000-8000-000000000099','timestamp-profile','task', \ - 'test-model','medium','standard',repeat('a',64),1,'2000-01-01Z'); \ - INSERT INTO decodex.account_snapshots \ - (account_snapshot_id,source_account_id,display_label,observed_state,source_revision,created_at) \ - VALUES ('43000000-0000-4000-8000-000000000099','timestamp-account', \ - 'Timestamp fixture','unknown',1,'2000-01-01Z')", - ) - .await?; - - let snapshot_times_canonical: bool = runtime_client - .query_one( - "SELECT (SELECT created_at>'2020-01-01Z' FROM decodex.profile_snapshots \ - WHERE profile_snapshot_id='42000000-0000-4000-8000-000000000099') \ - AND (SELECT created_at>'2020-01-01Z' FROM decodex.account_snapshots \ - WHERE account_snapshot_id='43000000-0000-4000-8000-000000000099')", - &[], - ) - .await? - .get(0); - - assert!(snapshot_times_canonical); - - for (session_id, state) in [ - ("41000000-0000-4000-8000-000000000004", "active"), - ("41000000-0000-4000-8000-000000000005", "starting"), - ] { - let statement = format!( - "INSERT INTO decodex.runtime_sessions \ - (runtime_session_id,conversation_id,profile_snapshot_id,account_snapshot_id,state, \ - created_at,updated_at,ended_at) \ - VALUES ($1::text::uuid,$2::text::uuid, \ - '42000000-0000-4000-8000-000000000099', \ - '43000000-0000-4000-8000-000000000099','{state}', \ - '2000-01-01Z','2099-01-01Z','2001-01-01Z')" - ); - - runtime_client.execute(&statement, &[&session_id, &conversation_id.as_str()]).await?; + for (index, (session_id, state)) in [ + ("41000000-0000-4000-8000-000000000004", RuntimeSessionState::Active), + ("41000000-0000-4000-8000-000000000005", RuntimeSessionState::Starting), + ] + .into_iter() + .enumerate() + { + let create = CreateRuntimeSession { + runtime_session_id: RuntimeSessionId::new(session_id)?, + conversation_id: conversation_id.clone(), + role: RoleProfileRole::Task, + account_snapshot: exact_account_snapshot(format!( + "43000000-0000-4000-8000-00000000009{}", + index + 1 + )), + codex_thread_id: None, + initial_state: state, + }; + assert!(matches!( + store.create_runtime_session(&format!("timestamp-session-{index}"), &create).await?, + RuntimeSessionCommandOutcome::Success(_) + )); let canonical: bool = runtime_client .query_one( - "SELECT created_at=updated_at AND ended_at IS NULL \ - AND created_at>'2020-01-01Z' FROM decodex.runtime_sessions \ + "SELECT session.created_at=session.updated_at AND session.ended_at IS NULL \ + AND session.created_at>'2020-01-01Z' AND profile.created_at>'2020-01-01Z' \ + AND account.created_at>'2020-01-01Z' FROM decodex.runtime_sessions AS session \ + JOIN decodex.profile_snapshots AS profile USING (profile_snapshot_id) \ + JOIN decodex.account_snapshots AS account USING (account_snapshot_id) \ WHERE runtime_session_id=$1::text::uuid", &[&session_id], ) .await? .get(0); - assert!(canonical, "{state} RuntimeSession timestamps are canonical"); + assert!(canonical, "RuntimeSession timestamps are canonical"); } - for (session_id, state) in [ - ("41000000-0000-4000-8000-000000000006", "ended"), - ("41000000-0000-4000-8000-000000000007", "diverged"), - ] { - let statement = format!( - "INSERT INTO decodex.runtime_sessions \ - (runtime_session_id,conversation_id,profile_snapshot_id,account_snapshot_id,state,ended_at) \ - VALUES ($1::text::uuid,$2::text::uuid, \ - '42000000-0000-4000-8000-000000000099', \ - '43000000-0000-4000-8000-000000000099','{state}',clock_timestamp())" - ); - - assert!( - runtime_client - .execute(&statement, &[&session_id, &conversation_id.as_str()]) - .await - .is_err() - ); + for (index, (session_id, state)) in [ + ("41000000-0000-4000-8000-000000000006", RuntimeSessionState::Ended), + ("41000000-0000-4000-8000-000000000007", RuntimeSessionState::Diverged), + ] + .into_iter() + .enumerate() + { + let create = CreateRuntimeSession { + runtime_session_id: RuntimeSessionId::new(session_id)?, + conversation_id: conversation_id.clone(), + role: RoleProfileRole::Task, + account_snapshot: exact_account_snapshot(format!( + "43000000-0000-4000-8000-00000000019{}", + index + 1 + )), + codex_thread_id: None, + initial_state: state, + }; + assert!(matches!( + store.create_runtime_session(&format!("terminal-session-{index}"), &create).await?, + RuntimeSessionCommandOutcome::Rejected(_) + )); } runtime_client @@ -5852,31 +5825,25 @@ async fn assert_runtime_session_correlation_is_immutable( store: &PostgresStore, runtime_client: &Client, conversation_id: &ConversationId, - profile: &ProfileSnapshot, - account: &AccountSnapshot, ) -> Result<(), Box> { let session_id = RuntimeSessionId::new("41000000-0000-4000-8000-000000000003")?; - let original_thread = "manual-thread-c"; + let original_thread = "44000000-0000-4000-8000-000000000003"; let create = CreateRuntimeSession { runtime_session_id: session_id.clone(), conversation_id: conversation_id.clone(), - profile_snapshot_id: "42000000-0000-4000-8000-000000000003".into(), - account_snapshot_id: "43000000-0000-4000-8000-000000000003".into(), - profile_snapshot: profile.clone(), - account_snapshot: account.clone(), + role: RoleProfileRole::Task, + account_snapshot: exact_account_snapshot("43000000-0000-4000-8000-000000000003".into()), codex_thread_id: Some(original_thread.into()), - state: RuntimeSessionState::Starting, + initial_state: RuntimeSessionState::Starting, }; - store - .create_runtime_session( - &CommandIdentity::new("manual-session-c", b"manual-session-c-v1")?, - &create, - ) - .await?; + assert!(matches!( + store.create_runtime_session("manual-session-c", &create).await?, + RuntimeSessionCommandOutcome::Success(_) + )); for forged_assignment in [ - "codex_thread_id='forged-thread'", + "codex_thread_id='44000000-0000-4000-8000-000000000013'", "last_known_turn_id='forged-turn'", "created_at=created_at - interval '1 second'", ] { @@ -5890,7 +5857,7 @@ async fn assert_runtime_session_correlation_is_immutable( let unchanged_starting: bool = runtime_client .query_one( - "SELECT state='starting' AND revision=1 AND codex_thread_id=$2 \ + "SELECT state='starting' AND revision=1 AND codex_thread_id=$2::text::uuid \ AND last_known_turn_id IS NULL FROM decodex.runtime_sessions \ WHERE runtime_session_id=$1::text::uuid", &[&session_id.as_str(), &original_thread], @@ -5900,20 +5867,23 @@ async fn assert_runtime_session_correlation_is_immutable( assert!(unchanged_starting); - let active = store + let RuntimeSessionCommandOutcome::Success(active) = store .transition_runtime_session( - &CommandIdentity::new("manual-session-c-active", b"manual-session-c-active-v1")?, + "manual-session-c-active", &session_id, 1, RuntimeSessionState::Active, ) - .await?; + .await? + else { + panic!("exact active transition must succeed"); + }; - assert_eq!(active.codex_thread_id.as_deref(), Some(original_thread)); - assert_eq!(active.revision, 2); + assert_eq!(active.runtime_session.codex_thread_id.as_deref(), Some(original_thread)); + assert_eq!(active.runtime_session.revision, 2); for forged_assignment in [ - "codex_thread_id='forged-terminal-thread'", + "codex_thread_id='44000000-0000-4000-8000-000000000023'", "last_known_turn_id='forged-terminal-turn'", "created_at=created_at - interval '1 second'", ] { @@ -5925,21 +5895,24 @@ async fn assert_runtime_session_correlation_is_immutable( assert!(runtime_client.execute(&statement, &[&session_id.as_str()]).await.is_err()); } - let ended = store + let RuntimeSessionCommandOutcome::Success(ended) = store .transition_runtime_session( - &CommandIdentity::new("manual-session-c-ended", b"manual-session-c-ended-v1")?, + "manual-session-c-ended", &session_id, 2, RuntimeSessionState::Ended, ) - .await?; + .await? + else { + panic!("exact terminal transition must succeed"); + }; - assert_eq!(ended.codex_thread_id.as_deref(), Some(original_thread)); - assert_eq!(ended.revision, 3); + assert_eq!(ended.runtime_session.codex_thread_id.as_deref(), Some(original_thread)); + assert_eq!(ended.runtime_session.revision, 3); let canonical_terminal: bool = runtime_client .query_one( - "SELECT state='ended' AND codex_thread_id=$2 AND last_known_turn_id IS NULL \ + "SELECT state='ended' AND codex_thread_id=$2::text::uuid AND last_known_turn_id IS NULL \ AND ended_at=updated_at AND updated_at>=created_at \ FROM decodex.runtime_sessions WHERE runtime_session_id=$1::text::uuid", &[&session_id.as_str(), &original_thread], @@ -7422,17 +7395,17 @@ async fn assert_transition_proposal_and_session_terminalization( assert!(client.batch_execute("INSERT INTO decodex.transition_proposals (transition_id,conversation_id,from_runtime_session_id,context_pack_id,kind,reason) VALUES ('47000000-0000-4000-8000-000000000099','40000000-0000-4000-8000-000000000099','41000000-0000-4000-8000-000000000001','46000000-0000-4000-8000-000000000001','fallback','cross conversation')").await.is_err()); assert!(client.batch_execute("INSERT INTO decodex.runtime_sessions (runtime_session_id,conversation_id,profile_snapshot_id,account_snapshot_id,state) VALUES ('41000000-0000-4000-8000-000000000099','40000000-0000-4000-8000-000000000099','42000000-0000-4000-8000-000000000001','43000000-0000-4000-8000-000000000001','ended')").await.is_err()); - assert!( + assert!(matches!( store .transition_runtime_session( - &CommandIdentity::new("session-a-end-too-early", b"session-a-end-too-early-v1")?, + "session-a-end-too-early", &fixture.session_a_id, 1, RuntimeSessionState::Ended ) - .await - .is_err() - ); + .await?, + RuntimeSessionCommandOutcome::Rejected(_) + )); store .transition_turn( @@ -7443,18 +7416,18 @@ async fn assert_transition_proposal_and_session_terminalization( ) .await?; - assert_eq!( - store - .transition_runtime_session( - &CommandIdentity::new("session-a-end", b"session-a-end-v1")?, - &fixture.session_a_id, - 1, - RuntimeSessionState::Ended - ) - .await? - .state, - RuntimeSessionState::Ended - ); + let RuntimeSessionCommandOutcome::Success(ended) = store + .transition_runtime_session( + "session-a-end", + &fixture.session_a_id, + 1, + RuntimeSessionState::Ended, + ) + .await? + else { + panic!("exact session end must succeed"); + }; + assert_eq!(ended.runtime_session.state, RuntimeSessionState::Ended); assert!(client.batch_execute("UPDATE decodex.runtime_sessions SET state='active', revision=revision+1 WHERE runtime_session_id='41000000-0000-4000-8000-000000000001'").await.is_err()); assert!( client diff --git a/crates/decodex-postgres/tests/postgres_store/role_profiles.rs b/crates/decodex-postgres/tests/postgres_store/role_profiles.rs index e290bd82..46aa0aee 100644 --- a/crates/decodex-postgres/tests/postgres_store/role_profiles.rs +++ b/crates/decodex-postgres/tests/postgres_store/role_profiles.rs @@ -616,7 +616,7 @@ async fn postgres_v8_to_v9_role_profile_upgrade() -> Result<(), Box RoleProfileConfiguration { + RoleProfileConfiguration { + model: format!("gpt-5.6-{marker}"), + reasoning_effort: "medium".into(), + service_tier: "priority".into(), + instructions: format!("Immutable XY-1337 {marker} instructions."), + provenance: Some(format!("XY-1337 {marker}")), + } +} + +fn profiles(marker: &str) -> BootstrapRoleProfiles { + BootstrapRoleProfiles { + advisor: profile(&format!("advisor-{marker}")), + lead: profile(&format!("lead-{marker}")), + task: profile(&format!("task-{marker}")), + reviewer: profile(&format!("reviewer-{marker}")), + } +} + +fn account(snapshot_id: &str, marker: &str) -> CreateRuntimeSessionAccountSnapshot { + CreateRuntimeSessionAccountSnapshot { + account_snapshot_id: snapshot_id.into(), + source_account_id: AccountId::new("13000000-0000-4000-8000-000000000001").unwrap(), + display_label: format!("Account {marker}"), + observed_state: AccountState::Unknown, + source_revision: 7, + } +} + +fn creation( + session_id: &str, + conversation_id: &ConversationId, + snapshot_id: &str, + marker: &str, +) -> CreateRuntimeSession { + CreateRuntimeSession { + runtime_session_id: RuntimeSessionId::new(session_id).unwrap(), + conversation_id: conversation_id.clone(), + role: RoleProfileRole::Task, + account_snapshot: account(snapshot_id, marker), + codex_thread_id: Some(session_id.replacen("41000000", "44000000", 1)), + initial_state: RuntimeSessionState::Starting, + } +} + +async fn stored_response(client: &Client, key: &str) -> Result, tokio_postgres::Error> { + Ok(client + .query_one( + "SELECT response_bytes FROM decodex.exact_command_receipts \ + WHERE protocol_version='decodex/exact-command/1' AND idempotency_key=$1", + &[&key], + ) + .await? + .get(0)) +} + +fn success( + value: RuntimeSessionCommandOutcome, +) -> RuntimeSessionCommandEffect { + let RuntimeSessionCommandOutcome::Success(effect) = value else { + panic!("exact RuntimeSession command must succeed") + }; + effect +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 8)] +#[ignore = "requires an isolated PostgreSQL 18 V10 RuntimeSession database"] +async fn postgres_exact_runtime_session_commands() -> Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = + PostgresStore::connect(migration.clone(), runtime.clone(), expected_peer_uid()).await?; + let (migration_client, migration_connection) = migration.connect(NoTls).await?; + let migration_task = tokio::spawn(migration_connection); + + assert!(matches!( + store.bootstrap_role_profiles("session-profiles", &profiles("v1")).await?, + RoleProfileCommandOutcome::Success(_) + )); + let conversation_id = ConversationId::new("40000000-0000-4000-8000-000000000001")?; + store + .create_conversation( + &CommandIdentity::new("session-conversation", b"session-conversation-v1")?, + &CreateConversation { + conversation_id: conversation_id.clone(), + title: "Exact RuntimeSession fixture".into(), + }, + ) + .await?; + + let create = creation( + "41000000-0000-4000-8000-000000000001", + &conversation_id, + "43000000-0000-4000-8000-000000000001", + "one", + ); + let created = success(store.create_runtime_session("session-create", &create).await?); + assert_eq!(created.prior_state, None); + assert_eq!(created.prior_revision, None); + assert_eq!(created.new_state, RuntimeSessionState::Starting); + assert_eq!(created.new_revision, 1); + assert_eq!(created.runtime_session.last_known_turn_id, None); + let create_bytes = stored_response(&migration_client, "session-create").await?; + assert_eq!( + store.create_runtime_session("session-create", &create).await?, + RuntimeSessionCommandOutcome::Success(created.clone()), + ); + assert_eq!(stored_response(&migration_client, "session-create").await?, create_bytes); + + let mut substitutions = Vec::new(); + let mut changed = create.clone(); + changed.runtime_session_id = RuntimeSessionId::new("41000000-0000-4000-8000-000000000002")?; + substitutions.push(changed); + let mut changed = create.clone(); + changed.conversation_id = ConversationId::new("40000000-0000-4000-8000-000000000002")?; + substitutions.push(changed); + let mut changed = create.clone(); + changed.role = RoleProfileRole::Reviewer; + substitutions.push(changed); + let mut changed = create.clone(); + changed.account_snapshot.account_snapshot_id = "43000000-0000-4000-8000-000000000002".into(); + substitutions.push(changed); + let mut changed = create.clone(); + changed.account_snapshot.source_account_id = + AccountId::new("13000000-0000-4000-8000-000000000002")?; + substitutions.push(changed); + let mut changed = create.clone(); + changed.account_snapshot.display_label.push_str(" changed"); + substitutions.push(changed); + let mut changed = create.clone(); + changed.account_snapshot.observed_state = AccountState::Unavailable; + substitutions.push(changed); + let mut changed = create.clone(); + changed.account_snapshot.source_revision += 1; + substitutions.push(changed); + let mut changed = create.clone(); + changed.codex_thread_id = None; + substitutions.push(changed); + let mut changed = create.clone(); + changed.initial_state = RuntimeSessionState::Active; + substitutions.push(changed); + for substitution in substitutions { + assert!(matches!( + store.create_runtime_session("session-create", &substitution).await, + Err(StoreError::IdempotencyConflict) + )); + } + + assert_eq!( + store.create_runtime_session("session-duplicate", &create).await?, + RuntimeSessionCommandOutcome::Rejected(RuntimeSessionRejection::DuplicateTarget), + ); + let duplicate_bytes = stored_response(&migration_client, "session-duplicate").await?; + assert_eq!( + store.create_runtime_session("session-duplicate", &create).await?, + RuntimeSessionCommandOutcome::Rejected(RuntimeSessionRejection::DuplicateTarget), + ); + assert_eq!(stored_response(&migration_client, "session-duplicate").await?, duplicate_bytes); + let mut conflicting_account = creation( + "41000000-0000-4000-8000-000000000006", + &conversation_id, + "43000000-0000-4000-8000-000000000001", + "conflict", + ); + conflicting_account.codex_thread_id = None; + assert_eq!( + store.create_runtime_session("session-account-conflict", &conflicting_account).await?, + RuntimeSessionCommandOutcome::Rejected(RuntimeSessionRejection::AccountSnapshotConflict,), + ); + + let missing = creation( + "41000000-0000-4000-8000-000000000003", + &ConversationId::new("40000000-0000-4000-8000-000000000099")?, + "43000000-0000-4000-8000-000000000003", + "missing", + ); + assert_eq!( + store.create_runtime_session("session-missing", &missing).await?, + RuntimeSessionCommandOutcome::Rejected(RuntimeSessionRejection::MissingTarget), + ); + + let transitioned = success( + store + .transition_runtime_session( + "session-active", + &create.runtime_session_id, + 1, + RuntimeSessionState::Active, + ) + .await?, + ); + assert_eq!(transitioned.prior_state, Some(RuntimeSessionState::Starting)); + assert_eq!(transitioned.prior_revision, Some(1)); + assert_eq!(transitioned.new_revision, 2); + let transition_bytes = stored_response(&migration_client, "session-active").await?; + assert_eq!( + store + .transition_runtime_session( + "session-active", + &create.runtime_session_id, + 1, + RuntimeSessionState::Active, + ) + .await?, + RuntimeSessionCommandOutcome::Success(transitioned), + ); + assert_eq!(stored_response(&migration_client, "session-active").await?, transition_bytes); + for (session, revision, state) in [ + ( + RuntimeSessionId::new("41000000-0000-4000-8000-000000000099")?, + 1, + RuntimeSessionState::Active, + ), + (create.runtime_session_id.clone(), 2, RuntimeSessionState::Active), + (create.runtime_session_id.clone(), 1, RuntimeSessionState::Ended), + ] { + assert!(matches!( + store.transition_runtime_session("session-active", &session, revision, state).await, + Err(StoreError::IdempotencyConflict) + )); + } + assert_eq!( + store + .transition_runtime_session( + "session-stale", + &create.runtime_session_id, + 1, + RuntimeSessionState::Ended, + ) + .await?, + RuntimeSessionCommandOutcome::Rejected(RuntimeSessionRejection::StaleRevision), + ); + assert_eq!( + store + .transition_runtime_session( + "session-illegal", + &create.runtime_session_id, + 2, + RuntimeSessionState::Starting, + ) + .await?, + RuntimeSessionCommandOutcome::Rejected(RuntimeSessionRejection::IllegalTransition), + ); + assert_eq!( + store + .transition_runtime_session( + "session-transition-missing", + &RuntimeSessionId::new("41000000-0000-4000-8000-000000000098")?, + 1, + RuntimeSessionState::Active, + ) + .await?, + RuntimeSessionCommandOutcome::Rejected(RuntimeSessionRejection::MissingTarget), + ); + + let before_update = created.runtime_session.profile_snapshot.clone(); + assert!(matches!( + store.update_role_profile("task-v2", RoleProfileRole::Task, 1, &profile("task-v2")).await?, + RoleProfileCommandOutcome::Success(_) + )); + let after_update = success( + store + .create_runtime_session( + "session-after-profile-update", + &creation( + "41000000-0000-4000-8000-000000000004", + &conversation_id, + "43000000-0000-4000-8000-000000000004", + "four", + ), + ) + .await?, + ); + assert_eq!(before_update.source_revision, 1); + assert_eq!(after_update.runtime_session.profile_snapshot.source_revision, 2); + let preserved: bool = migration_client + .query_one( + "SELECT model=$2 AND instructions=$3 AND source_revision=1 \ + FROM decodex.profile_snapshots WHERE profile_snapshot_id=$1::text::uuid", + &[ + &before_update.profile_snapshot_id, + &before_update.model, + &before_update.instructions, + ], + ) + .await? + .get(0); + assert!(preserved); + + let mut profile_race = JoinSet::new(); + for index in 0..24 { + let store = store.clone(); + let conversation_id = conversation_id.clone(); + profile_race.spawn(async move { + let identity = index + 100; + let create = creation( + &format!("41000000-0000-4000-8000-{identity:012x}"), + &conversation_id, + &format!("43000000-0000-4000-8000-{identity:012x}"), + &format!("profile-race-{index}"), + ); + store.create_runtime_session(&format!("session-profile-race-{index}"), &create).await + }); + } + let updating_store = store.clone(); + let profile_update = tokio::spawn(async move { + updating_store + .update_role_profile("task-v3", RoleProfileRole::Task, 2, &profile("task-v3")) + .await + }); + while let Some(result) = profile_race.join_next().await { + let effect = success(result??); + let snapshot = effect.runtime_session.profile_snapshot; + match snapshot.source_revision { + 2 => { + assert_eq!(snapshot.model, "gpt-5.6-task-v2"); + assert_eq!(snapshot.instructions, "Immutable XY-1337 task-v2 instructions."); + }, + 3 => { + assert_eq!(snapshot.model, "gpt-5.6-task-v3"); + assert_eq!(snapshot.instructions, "Immutable XY-1337 task-v3 instructions."); + }, + other => panic!("profile race selected unexpected revision {other}"), + } + } + assert!(matches!(profile_update.await??, RoleProfileCommandOutcome::Success(_))); + + let (runtime_client, runtime_connection) = runtime.connect(NoTls).await?; + let runtime_task = tokio::spawn(runtime_connection); + for statement in [ + "INSERT INTO decodex.profile_snapshots(profile_snapshot_id,source_profile_id,role,model,reasoning_effort,service_tier,instructions_digest,instructions,source_revision) VALUES ('42000000-0000-4000-8000-000000000099','task','task','x','medium','priority',repeat('0',64),'x',1)", + "UPDATE decodex.account_snapshots SET display_label='forged'", + "DELETE FROM decodex.runtime_sessions", + "TRUNCATE decodex.runtime_sessions", + "SELECT decodex.complete_exact_runtime_session_rejection('x','x','x')", + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('runtime_session','forged',1,'runtime_session_created','forged','{}')", + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('conversation','forged',1,'conversation_recorded','forged','{\"event\":{\"aggregate_kind\":\"runtime_session\"}}')", + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('conversation','forged',1,'runtime_session_recorded','forged','{}')", + "INSERT INTO decodex.outbox(effect_key,aggregate_kind,aggregate_id,aggregate_revision,payload) VALUES ('forged','runtime_session','forged',1,'{}')", + "INSERT INTO decodex.outbox(effect_key,aggregate_kind,aggregate_id,aggregate_revision,payload) VALUES ('forged','conversation','forged',1,'{\"effect\":{\"payload\":{\"event_kind\":\"runtime_session_transitioned\"}}}')", + ] { + let error = runtime_client.batch_execute(statement).await.expect_err(statement); + assert_eq!(error.code().map(|code| code.code()), Some("42501"), "{statement}"); + } + drop(runtime_client); + runtime_task.await??; + + let mut duplicate_race = JoinSet::new(); + let contested = creation( + "41000000-0000-4000-8000-000000000005", + &conversation_id, + "43000000-0000-4000-8000-000000000005", + "five", + ); + for index in 0..16 { + let store = store.clone(); + let contested = contested.clone(); + duplicate_race.spawn(async move { + store.create_runtime_session(&format!("session-race-{index}"), &contested).await + }); + } + let mut winners = 0; + let mut duplicates = 0; + while let Some(result) = duplicate_race.join_next().await { + match result?? { + RuntimeSessionCommandOutcome::Success(_) => winners += 1, + RuntimeSessionCommandOutcome::Rejected(RuntimeSessionRejection::DuplicateTarget) => + duplicates += 1, + other => panic!("unexpected duplicate-race result: {other:?}"), + } + } + assert_eq!((winners, duplicates), (1, 15)); + + drop(migration_client); + migration_task.await??; + Ok(()) +} + +async fn runtime_state(client: &Client) -> Result<[i64; 6], tokio_postgres::Error> { + let row = client + .query_one( + "SELECT \ + (SELECT count(*) FROM decodex.exact_command_receipts), \ + (SELECT count(*) FROM decodex.profile_snapshots), \ + (SELECT count(*) FROM decodex.account_snapshots), \ + (SELECT count(*) FROM decodex.runtime_sessions), \ + (SELECT count(*) FROM decodex.activity WHERE aggregate_kind='runtime_session'), \ + (SELECT count(*) FROM decodex.outbox WHERE aggregate_kind='runtime_session')", + &[], + ) + .await?; + Ok(std::array::from_fn(|index| row.get(index))) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires an isolated PostgreSQL 18 V10 RuntimeSession rollback database"] +async fn postgres_exact_runtime_session_atomic_rollback() -> Result<(), Box> +{ + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + assert!(matches!( + store.bootstrap_role_profiles("rollback-profiles", &profiles("rollback")).await?, + RoleProfileCommandOutcome::Success(_) + )); + let conversation_id = ConversationId::new("40000000-0000-4000-8000-000000000020")?; + store + .create_conversation( + &CommandIdentity::new("rollback-conversation", b"rollback-conversation-v1")?, + &CreateConversation { + conversation_id: conversation_id.clone(), + title: "RuntimeSession rollback fixture".into(), + }, + ) + .await?; + let (admin, admin_connection) = migration.connect(NoTls).await?; + let admin_task = tokio::spawn(admin_connection); + admin + .batch_execute( + "CREATE SEQUENCE public.xy1337_rollback_faults; \ + CREATE TABLE public.xy1337_rollback_schedule(boundary text PRIMARY KEY); \ + REVOKE ALL ON TABLE public.xy1337_rollback_schedule FROM PUBLIC; \ + REVOKE ALL ON SEQUENCE public.xy1337_rollback_faults FROM PUBLIC; \ + CREATE FUNCTION public.xy1337_raise_scheduled_runtime_fault() \ + RETURNS trigger LANGUAGE plpgsql AS $$ \ + BEGIN \ + IF session_user <> current_user AND EXISTS ( \ + SELECT 1 FROM public.xy1337_rollback_schedule WHERE boundary=TG_ARGV[0] \ + ) THEN \ + PERFORM pg_catalog.nextval('public.xy1337_rollback_faults'); \ + RAISE EXCEPTION 'scheduled XY-1337 rollback fault' USING ERRCODE='XX000'; \ + END IF; \ + RETURN NEW; \ + END $$; \ + CREATE TRIGGER xy1337_fault_receipt AFTER INSERT ON decodex.exact_command_receipts \ + FOR EACH ROW EXECUTE FUNCTION public.xy1337_raise_scheduled_runtime_fault('receipt'); \ + CREATE TRIGGER xy1337_fault_domain AFTER INSERT ON decodex.runtime_sessions \ + FOR EACH ROW EXECUTE FUNCTION public.xy1337_raise_scheduled_runtime_fault('domain'); \ + CREATE TRIGGER xy1337_fault_activity AFTER INSERT ON decodex.activity \ + FOR EACH ROW EXECUTE FUNCTION public.xy1337_raise_scheduled_runtime_fault('activity'); \ + CREATE TRIGGER xy1337_fault_outbox AFTER INSERT ON decodex.outbox \ + FOR EACH ROW EXECUTE FUNCTION public.xy1337_raise_scheduled_runtime_fault('outbox'); \ + CREATE TRIGGER xy1337_fault_response AFTER UPDATE ON decodex.exact_command_receipts \ + FOR EACH ROW WHEN (NEW.response_bytes IS NOT NULL) \ + EXECUTE FUNCTION public.xy1337_raise_scheduled_runtime_fault('response'); \ + REVOKE ALL ON FUNCTION public.xy1337_raise_scheduled_runtime_fault() FROM PUBLIC", + ) + .await?; + + for (index, boundary) in + ["receipt", "domain", "activity", "outbox", "response"].into_iter().enumerate() + { + let identity = index + 30; + let key = format!("runtime-rollback-{boundary}"); + let create = creation( + &format!("41000000-0000-4000-8000-{identity:012x}"), + &conversation_id, + &format!("43000000-0000-4000-8000-{identity:012x}"), + boundary, + ); + admin + .execute( + "INSERT INTO public.xy1337_rollback_schedule(boundary) VALUES ($1)", + &[&boundary], + ) + .await?; + let before = runtime_state(&admin).await?; + assert!(matches!( + store.create_runtime_session(&key, &create).await, + Err(StoreError::Database(_)) + )); + assert_eq!(runtime_state(&admin).await?, before, "rollback at {boundary}"); + admin + .execute("DELETE FROM public.xy1337_rollback_schedule WHERE boundary=$1", &[&boundary]) + .await?; + assert!(matches!( + store.create_runtime_session(&key, &create).await?, + RuntimeSessionCommandOutcome::Success(_) + )); + } + assert_eq!( + admin + .query_one("SELECT last_value FROM public.xy1337_rollback_faults", &[]) + .await? + .get::<_, i64>(0), + 5, + ); + drop(admin); + admin_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires an isolated PostgreSQL 18 V10 RuntimeSession retry database"] +async fn postgres_exact_runtime_session_retry_convergence() -> Result<(), Box> +{ + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + assert!(matches!( + store.bootstrap_role_profiles("retry-profiles", &profiles("retry")).await?, + RoleProfileCommandOutcome::Success(_) + )); + let conversation_id = ConversationId::new("40000000-0000-4000-8000-000000000060")?; + store + .create_conversation( + &CommandIdentity::new("retry-conversation", b"retry-conversation-v1")?, + &CreateConversation { + conversation_id: conversation_id.clone(), + title: "RuntimeSession retry fixture".into(), + }, + ) + .await?; + let (admin, admin_connection) = migration.connect(NoTls).await?; + let admin_task = tokio::spawn(admin_connection); + admin + .batch_execute( + "CREATE SEQUENCE public.xy1337_retry_attempts; \ + REVOKE ALL ON SEQUENCE public.xy1337_retry_attempts FROM PUBLIC; \ + CREATE FUNCTION public.xy1337_schedule_runtime_retry() \ + RETURNS trigger LANGUAGE plpgsql AS $$ \ + DECLARE attempt bigint; \ + BEGIN \ + IF session_user <> current_user \ + AND NEW.idempotency_key='runtime-serialization-retry' THEN \ + attempt := pg_catalog.nextval('public.xy1337_retry_attempts'); \ + IF attempt=1 THEN \ + RAISE EXCEPTION 'scheduled serialization retry' USING ERRCODE='40001'; \ + END IF; \ + END IF; \ + RETURN NEW; \ + END $$; \ + CREATE TRIGGER xy1337_retry_receipt BEFORE INSERT ON decodex.exact_command_receipts \ + FOR EACH ROW EXECUTE FUNCTION public.xy1337_schedule_runtime_retry(); \ + REVOKE ALL ON FUNCTION public.xy1337_schedule_runtime_retry() FROM PUBLIC", + ) + .await?; + let create = creation( + "41000000-0000-4000-8000-000000000060", + &conversation_id, + "43000000-0000-4000-8000-000000000060", + "retry", + ); + assert!(matches!( + store.create_runtime_session("runtime-serialization-retry", &create).await?, + RuntimeSessionCommandOutcome::Success(_) + )); + assert_eq!( + admin + .query_one("SELECT last_value FROM public.xy1337_retry_attempts", &[]) + .await? + .get::<_, i64>(0), + 2, + ); + assert_eq!(runtime_state(&admin).await?[1..], [1, 1, 1, 1, 1]); + drop(admin); + admin_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires an isolated PostgreSQL 18 V9 to V10 upgrade database"] +async fn postgres_v9_to_v10_runtime_session_upgrade() -> Result<(), Box> { + let (migration, _) = separated_configs("DECODEX_TEST")?; + PostgresStore::migrate_fixture_through_v9(migration.clone(), expected_peer_uid()).await?; + let (client, connection) = migration.clone().connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + assert_eq!( + client + .query_one("SELECT max(version) FROM public.refinery_schema_history", &[]) + .await? + .get::<_, i32>(0), + 9, + ); + let identities_before: (u32, u32, u32) = { + let row = client + .query_one( + "SELECT 'decodex.profile_snapshots'::regclass::oid, \ + 'decodex.account_snapshots'::regclass::oid, \ + 'decodex.runtime_sessions'::regclass::oid", + &[], + ) + .await?; + (row.get(0), row.get(1), row.get(2)) + }; + drop(client); + connection_task.await??; + PostgresStore::migrate(migration.clone(), expected_peer_uid()).await?; + let (client, connection) = migration.connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + let row = client + .query_one( + "SELECT max(version), 'decodex.profile_snapshots'::regclass::oid, \ + 'decodex.account_snapshots'::regclass::oid, \ + 'decodex.runtime_sessions'::regclass::oid", + &[], + ) + .await?; + assert_eq!(row.get::<_, i32>(0), 10); + assert_eq!( + (row.get::<_, u32>(1), row.get::<_, u32>(2), row.get::<_, u32>(3)), + identities_before, + ); + drop(client); + connection_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "requires an isolated PostgreSQL 18 V10 zero-state rejection database"] +async fn postgres_v10_rejects_classified_runtime_state() -> Result<(), Box> { + let variant = env::var("DECODEX_V10_PRIOR_STATE")?; + let (migration, _) = separated_configs("DECODEX_TEST")?; + PostgresStore::migrate_fixture_through_v9(migration.clone(), expected_peer_uid()).await?; + let (client, connection) = migration.clone().connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + let statement = match variant.as_str() { + "profile_snapshot" => + "INSERT INTO decodex.profile_snapshots(profile_snapshot_id,source_profile_id,role,model,reasoning_effort,service_tier,instructions_digest,source_revision) VALUES ('42000000-0000-4000-8000-000000000090','task','task','model','medium','priority',repeat('a',64),1)", + "account_snapshot" => + "INSERT INTO decodex.account_snapshots(account_snapshot_id,source_account_id,display_label,observed_state,source_revision) VALUES ('43000000-0000-4000-8000-000000000090','13000000-0000-4000-8000-000000000090','Account','unknown',1)", + "runtime_session" => + "INSERT INTO decodex.conversations(conversation_id,title) VALUES ('40000000-0000-4000-8000-000000000090','V9 state'); INSERT INTO decodex.profile_snapshots(profile_snapshot_id,source_profile_id,role,model,reasoning_effort,service_tier,instructions_digest,source_revision) VALUES ('42000000-0000-4000-8000-000000000090','task','task','model','medium','priority',repeat('a',64),1); INSERT INTO decodex.account_snapshots(account_snapshot_id,source_account_id,display_label,observed_state,source_revision) VALUES ('43000000-0000-4000-8000-000000000090','13000000-0000-4000-8000-000000000090','Account','unknown',1); INSERT INTO decodex.runtime_sessions(runtime_session_id,conversation_id,profile_snapshot_id,account_snapshot_id) VALUES ('41000000-0000-4000-8000-000000000090','40000000-0000-4000-8000-000000000090','42000000-0000-4000-8000-000000000090','43000000-0000-4000-8000-000000000090')", + "legacy_receipt" => + "INSERT INTO decodex.command_receipts(idempotency_key,request_hash,operation,claim_token,claim_expires_at) VALUES ('v9-runtime',repeat('a',64),'create_runtime_session',gen_random_uuid(),clock_timestamp()+interval '1 minute')", + "exact_receipt" => + "WITH request(value) AS (VALUES ('{\"operation\":\"create_runtime_session\"}'::jsonb)), effect(value) AS (VALUES ('{\"changed\":false,\"code\":\"missing_target\"}'::jsonb)) INSERT INTO decodex.exact_command_receipts(protocol_version,idempotency_key,request_envelope,request_digest,receipt_state,outcome_class,effect_envelope,response_bytes,completed_at) SELECT 'decodex/exact-command/1','v9-runtime',request.value,public.digest(convert_to(request.value::text,'UTF8'),'sha256'),'completed_rejected','stable_domain_rejection',effect.value,convert_to(jsonb_build_object('classification','stable_domain_rejection','code','missing_target','effect',effect.value)::text,'UTF8'),clock_timestamp() FROM request,effect", + "activity" => + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('runtime_session','v9',1,'runtime_session_recorded','v9-runtime','{}')", + "activity_nested_aggregate" => + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('conversation','v9',1,'conversation_recorded','v9-runtime','{\"event\":{\"aggregate_kind\":\"runtime_session\"}}')", + "activity_legacy_other_aggregate" => + "INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('conversation','v9',1,'runtime_session_recorded','v9-runtime','{}')", + "outbox" => + "INSERT INTO decodex.outbox(effect_key,aggregate_kind,aggregate_id,aggregate_revision,payload) VALUES ('v9-runtime','runtime_session','v9',1,'{}')", + "outbox_nested_effect" => + "INSERT INTO decodex.outbox(effect_key,aggregate_kind,aggregate_id,aggregate_revision,payload) VALUES ('v9-runtime','conversation','v9',1,'{\"effect\":{\"payload\":{\"event_kind\":\"runtime_session_transitioned\"}}}')", + "outbox_activity_link" => + "WITH activity AS (INSERT INTO decodex.activity(aggregate_kind,aggregate_id,revision,event_kind,correlation_key,payload) VALUES ('conversation','v9',1,'runtime_session_recorded','v9-runtime','{}') RETURNING sequence) INSERT INTO decodex.outbox(effect_key,aggregate_kind,aggregate_id,aggregate_revision,payload) SELECT 'v9-runtime','conversation','v9',1,jsonb_build_object('link',jsonb_build_object('activity_sequence',sequence)) FROM activity", + _ => return Err(format!("unknown V10 prior-state fixture {variant}").into()), + }; + client.batch_execute(statement).await?; + let error = PostgresStore::migrate(migration, expected_peer_uid()) + .await + .expect_err("classified V9 RuntimeSession state must reject V10 atomically"); + assert!(format!("{error:?}").contains("zero incompatible state")); + assert_eq!( + client + .query_one("SELECT max(version) FROM public.refinery_schema_history", &[]) + .await? + .get::<_, i32>(0), + 9, + ); + drop(client); + connection_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires an isolated PostgreSQL 18 V10 old-writer fence database"] +async fn postgres_v10_fences_blocked_old_runtime_writer() -> Result<(), Box> +{ + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + PostgresStore::migrate_fixture_through_v9(migration.clone(), expected_peer_uid()).await?; + let runtime_role = runtime.get_user().ok_or("runtime role is absent")?; + let (admin, admin_connection) = migration.clone().connect(NoTls).await?; + let admin_task = tokio::spawn(admin_connection); + admin + .batch_execute(&format!( + "GRANT USAGE ON SCHEMA decodex TO {runtime_role}; \ + GRANT INSERT ON decodex.profile_snapshots TO {runtime_role}; \ + GRANT USAGE ON TYPE decodex.role_profile_role TO {runtime_role}" + )) + .await?; + let (old_writer, old_connection) = runtime.connect(NoTls).await?; + let old_connection_task = tokio::spawn(old_connection); + old_writer + .batch_execute( + "PREPARE xy1337_old_writer AS \ + INSERT INTO decodex.profile_snapshots( \ + profile_snapshot_id,source_profile_id,role,model,reasoning_effort, \ + service_tier,instructions_digest,source_revision \ + ) VALUES ( \ + '42000000-0000-4000-8000-000000000091','task','task','old', \ + 'medium','priority',repeat('a',64),1 \ + )", + ) + .await?; + let old_pid: i32 = old_writer.query_one("SELECT pg_backend_pid()", &[]).await?.get(0); + admin.query_one("SELECT pg_advisory_lock(991337)", &[]).await?; + let writer = tokio::spawn(async move { + old_writer + .batch_execute( + "BEGIN; SELECT pg_advisory_xact_lock(991337); \ + EXECUTE xy1337_old_writer; COMMIT", + ) + .await + }); + for _ in 0..1_000 { + let blocked: bool = admin + .query_one( + "SELECT wait_event_type='Lock' AND wait_event='advisory' \ + FROM pg_stat_activity WHERE pid=$1", + &[&old_pid], + ) + .await? + .get(0); + if blocked { + break; + } + time::sleep(std::time::Duration::from_millis(10)).await; + } + assert!(!writer.is_finished(), "old writer must be blocked before V10"); + PostgresStore::migrate(migration, expected_peer_uid()).await?; + admin.query_one("SELECT pg_advisory_unlock(991337)", &[]).await?; + let error = writer.await?.expect_err("pre-V10 writer must fail behind the committed fence"); + assert_eq!(error.code().map(|code| code.code()), Some("42501")); + let fenced: bool = admin + .query_one( + "SELECT (SELECT max(version)=10 FROM public.refinery_schema_history) \ + AND NOT EXISTS (SELECT 1 FROM decodex.profile_snapshots)", + &[], + ) + .await? + .get(0); + assert!(fenced); + drop(admin); + admin_task.await??; + old_connection_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires an isolated PostgreSQL 18 V10 RuntimeSession crash database"] +async fn postgres_exact_runtime_session_crash_recovery() -> Result<(), Box> { + let sync = PathBuf::from(env::var("DECODEX_RUNTIME_SESSION_RESTART_SYNC")?); + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = + PostgresStore::connect(migration.clone(), runtime.clone(), expected_peer_uid()).await?; + assert!(matches!( + store.bootstrap_role_profiles("crash-profiles", &profiles("crash")).await?, + RoleProfileCommandOutcome::Success(_) + )); + let conversation_id = ConversationId::new("40000000-0000-4000-8000-000000000070")?; + store + .create_conversation( + &CommandIdentity::new("crash-conversation", b"crash-conversation-v1")?, + &CreateConversation { + conversation_id: conversation_id.clone(), + title: "RuntimeSession crash fixture".into(), + }, + ) + .await?; + let create = creation( + "41000000-0000-4000-8000-000000000070", + &conversation_id, + "43000000-0000-4000-8000-000000000070", + "crash", + ); + let (blocker, blocker_connection) = migration.clone().connect(NoTls).await?; + let blocker_task = tokio::spawn(blocker_connection); + blocker + .batch_execute("BEGIN; LOCK TABLE decodex.runtime_sessions IN ACCESS EXCLUSIVE MODE") + .await?; + let blocker_pid: i32 = blocker.query_one("SELECT pg_backend_pid()", &[]).await?.get(0); + let task_store = store.clone(); + let task_create = create.clone(); + let create_task = tokio::spawn(async move { + task_store.create_runtime_session("crash-session", &task_create).await + }); + let (observer, observer_connection) = migration.clone().connect(NoTls).await?; + let observer_task = tokio::spawn(observer_connection); + assert!(super::wait_for_any_blocked_by(&observer, blocker_pid).await?); + std::fs::write(sync.join("ready"), b"ready")?; + for _ in 0..3_000 { + if sync.join("restarted").exists() { + break; + } + time::sleep(std::time::Duration::from_millis(10)).await; + } + if !sync.join("restarted").exists() { + return Err("PostgreSQL restart fixture did not signal recovery".into()); + } + assert!(create_task.await?.is_err(), "precommit loss must not report success"); + drop(observer); + drop(blocker); + let _ = observer_task.await; + let _ = blocker_task.await; + drop(store); + + let recovered = PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + assert!(matches!( + recovered.create_runtime_session("crash-session", &create).await?, + RuntimeSessionCommandOutcome::Success(_) + )); + let (client, connection) = migration.connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + let state = runtime_state(&client).await?; + assert_eq!(state[1..], [1, 1, 1, 1, 1]); + assert_eq!( + client + .query_one( + "SELECT count(*) FROM decodex.exact_command_receipts \ + WHERE idempotency_key='crash-session' AND receipt_state='completed_success'", + &[], + ) + .await? + .get::<_, i64>(0), + 1, + ); + drop(client); + connection_task.await??; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "requires the populated PostgreSQL 18 V10 RuntimeSession restore database"] +async fn postgres_exact_runtime_session_restore() -> Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let _store = PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + let (client, connection) = migration.connect(NoTls).await?; + let connection_task = tokio::spawn(connection); + let valid: bool = client + .query_one( + "SELECT \ + (SELECT max(version)=10 FROM public.refinery_schema_history) AND \ + NOT EXISTS (SELECT 1 FROM decodex.exact_command_receipts \ + WHERE receipt_state='executing' OR response_bytes IS NULL \ + OR convert_from(response_bytes,'UTF8')::jsonb->'effect' IS DISTINCT FROM effect_envelope) AND \ + NOT EXISTS (SELECT 1 FROM decodex.runtime_sessions AS session \ + LEFT JOIN decodex.profile_snapshots AS profile USING (profile_snapshot_id) \ + LEFT JOIN decodex.account_snapshots AS account USING (account_snapshot_id) \ + WHERE profile.profile_snapshot_id IS NULL OR account.account_snapshot_id IS NULL)", + &[], + ) + .await? + .get(0); + assert!(valid); + drop(client); + connection_task.await??; + Ok(()) +} diff --git a/openwiki/architecture/runtime-architecture.md b/openwiki/architecture/runtime-architecture.md index 0a469e59..0a462ec2 100644 --- a/openwiki/architecture/runtime-architecture.md +++ b/openwiki/architecture/runtime-architecture.md @@ -119,22 +119,22 @@ runtime-effective, regardless of `pg_extension.extnamespace`. Superuser/BYPASSRLS/role/database administration, database/schema CREATE, TRUNCATE/TRIGGER/REFERENCES/MAINTAIN, excess table DML or grant options, `session_replication_role` SET/ALTER SYSTEM, and any effective non-`origin` login value are unsafe -in any reachable authority state. At the frozen V9 boundary, the audit verifies all fifty-two shipped +in any reachable authority state. At the V10 boundary, the audit verifies all fifty-nine shipped non-internal trigger bindings, including regular and deferred constraint triggers, by table, event mask, row/statement level, constraint and deferral state, origin-enabled mode, and function binding, then compares each bound function's exact metadata and `pg_proc.prosrc` bytes with the canonical body embedded in -the immutable forward migration ledger through V9. It additionally closes the entire runtime-callable `decodex` function +the immutable forward migration ledger through V10. It additionally closes the entire runtime-callable `decodex` function namespace over exact signatures and overloads, argument/result shape, language, volatility, parallel/strict/set behavior, planner metadata, exact security-invoker/definer state and exact per-function settings, and canonical source. Unexpected functions, overloads, owner-executed functions, or unsafe settings are unsafe; missing functions or noncanonical source are incompatible. Disabled or misbound triggers are unsafe; a replaced same-signature safety-function body is incompatible. -Every non-internal trigger on a Decodex runtime relation must be one of those fifty-two exact V9 bindings. +Every non-internal trigger on a Decodex runtime relation must be one of those fifty-nine exact V10 bindings. The same closed execution-path audit permits no user rule, row-security policy, or enabled/forced RLS on those relations and rejects non-`pg_catalog` function/operator dependencies from defaults, generated expressions, constraints, indexes, rules, or policies unless they resolve to one of the -seventy-two canonical V9 functions. Every canonical function has the exact function-local +eighty canonical V10 functions. Every canonical function has the exact function-local `pg_catalog, decodex` search path, so runtime-selected callable or operator shadows cannot redirect trigger or constraint execution. A trigger cannot therefore invoke an adjacent public owner-executed function merely because runtime DML fires it. @@ -158,21 +158,21 @@ string-to-system-catalog identity explicitly qualifies `pg_catalog`; the authority audit and schema-qualified migration-ledger verification remain correct under a hostile runtime `search_path` that shadows both ledger and system-catalog names. Missing required schema, table, sequence, function, or ledger-read authority is incompatible. -At the frozen V9 boundary, sixteen canonical `SECURITY DEFINER` functions comprise the three V3 +At the V10 boundary, eighteen canonical `SECURITY DEFINER` functions comprise the three V3 cursor/history functions, eleven V5-V7 Project/Policy/Program/Objective command entrypoints, and two -V9 RoleProfile command entrypoints. The cursor issuer derives Conversation, +V9 RoleProfile command entrypoints plus two V10 RuntimeSession command entrypoints. The cursor issuer derives Conversation, snapshot version, parent, page size, position, item identity, and expiry under serialized Conversation authority; the bounded pruner is callable by runtime, while the capture function is trigger-only and runtime cannot execute it directly. Runtime has no cursor-table INSERT authority. -The other fifty-six canonical V9 functions are security invokers. The additional-function adversarial fixture creates a fixture-only seventy-third migration-owned, +The other sixty-two canonical V10 functions are security invokers. The additional-function adversarial fixture creates a fixture-only eighty-first migration-owned, runtime-executable `SECURITY DEFINER` function with an unsafe per-function setting and migration-owner trigger authority, proves runtime direct trigger DDL is denied, executes the owner-authority effect, and restores the trigger before the independent doctor rejection. A separate public-function trigger fixture proves runtime DML can execute an owner effect without direct function `EXECUTE`, protected -table `UPDATE`, or `TRIGGER`; the exact fifty-two-trigger V9 inventory rejects that path. A public, +table `UPDATE`, or `TRIGGER`; the exact fifty-nine-trigger V10 inventory rejects that path. A public, runtime-owned extension fixture attaches a migration-owned Decodex collation as an extension member, proves the runtime can transactionally drop it, and is rejected through the dependency audit. The -closed seventy-two-function V9 inventory remains independent of the distinct same-signature canonical-source +closed eighty-function V10 inventory remains independent of the distinct same-signature canonical-source substitution fixture. Missing, malformed, unsafe, unreachable, authentication-failed, or incompatible bootstrap retains a typed unavailable adapter; there is no ambient/default database or alternate state authority. Repository and @@ -401,7 +401,8 @@ identities. Operation is part of the envelope, so cross-operation key reuse conf proof and vertical ownership are in [the XY-1345 evidence](../evidence/xy-1345-exact-command-authority.md). V9 implements this boundary for immutable global RoleProfiles through `bootstrap_role_profiles_exact` and -`update_role_profile_exact`; V10 remains owned by re-bounded XY-1337. +`update_role_profile_exact`; V10 extends it through `create_runtime_session_exact` and +`transition_runtime_session_exact` without changing the V9 receipt or RoleProfile authority. The V9 RoleProfile model contains only advisor, lead, task, and reviewer. Bootstrap receives four role-implied scalar configuration groups and commits the complete set or nothing. Updates append an @@ -410,6 +411,26 @@ single current pointer. Runtime neither selects nor mutates the RoleProfile or e relations directly; it parses the response bytes returned by the two command-complete entrypoints and retries a complete top-level transaction only after a classified infrastructure SQLSTATE. +V10 is a forward-only zero-state cutover over the retained V3 snapshot and RuntimeSession table +identities. One access-exclusive fence rejects every legacy RuntimeSession receipt, snapshot, +session, Turn, or structurally classified activity/outbox row before altering the empty tables. +Classification closes aggregate, event, effect, link, and payload representations recursively, +including legacy `runtime_session_recorded` events under another aggregate and nested aggregate +markers; the steady-state trigger applies the same fail-closed family. +Creation accepts only the RuntimeSession and Conversation identities, one role, the complete +non-secret account snapshot identity/facts, nullable Codex thread identity, and initial state. +PostgreSQL acquires hierarchy coordinator 1271 before selecting or locking an open Conversation or +RuntimeSession tuple, then resolves exactly one current immutable RoleProfile +revision, inserts or equality-validates the account snapshot, writes the complete profile snapshot, +creates revision 1 with a null last-known-turn identity, appends canonical activity/outbox rows, +and stores the exact response bytes in one transaction. Transition identity is only RuntimeSession +identity, expected revision, and target state; success returns prior/new state and revision plus the +unchanged full profile/account/session facts and canonical audit identities. Missing, duplicate, +stale, illegal, invalid-account, and account-snapshot-conflict outcomes are committed stable +rejections. Runtime retains SELECT-only snapshot/session readback and can execute only the two +public command owners; direct DML, private helpers, and forged RuntimeSession activity/outbox +namespaces are closed. + Legacy `command_receipts` retain the receipt-first fenced-claim protocol only for unrelated blob, filesystem, external, or long-running sagas whose point of no return cannot fit in one PostgreSQL transaction. Such a flow commits an immutable pending receipt before effects; a fenced claim then diff --git a/openwiki/decisions/vnext-authority.md b/openwiki/decisions/vnext-authority.md index c6fe32b0..511ddb90 100644 --- a/openwiki/decisions/vnext-authority.md +++ b/openwiki/decisions/vnext-authority.md @@ -76,6 +76,17 @@ receipts plus immutable global RoleProfile bootstrap/update in V9, then re-bound RuntimeSession V10. Legacy `command_receipts` semantics remain unchanged for unrelated external or long-running sagas. +XY-1337 RuntimeSession amendment, implemented as expected V10: creation and transition are separate +command-complete `SECURITY DEFINER` operations using the unchanged V9 exact receipt. PostgreSQL +constructs creation identity from RuntimeSession ID, Conversation ID, role, complete non-secret +account snapshot identity/facts, nullable Codex thread ID, and initial state, then resolves exactly +one current immutable RoleProfile revision server-side. Transition identity contains only session +ID, expected revision, and target state. Both commands atomically store domain state, canonical +activity/outbox effects, and immutable response bytes; stable rejection completes in the same +transaction. V10 is a zero-state forward cutover, preserves the existing table identities, removes +runtime snapshot/session DML, fences RuntimeSession audit namespaces, and does not authorize Codex +creation, reconciliation, routing, scheduling, WorkItems, ManagedRuns, UI, or plugin readiness. + ## Decision Decodex vNext is a rebuild of the agent workspace, not an incremental extension of the diff --git a/openwiki/operations/commands-and-validation.md b/openwiki/operations/commands-and-validation.md index 4a285a51..615b7406 100644 --- a/openwiki/operations/commands-and-validation.md +++ b/openwiki/operations/commands-and-validation.md @@ -90,6 +90,19 @@ untrusted server-text cases. The XY-1267/XY-1307 integration command and XY-1264 storage proof are intentionally separate because they require an intended macOS host with one PostgreSQL 18 distribution. Each creates and removes its own isolated temporary checksummed cluster with TCP disabled and never enumerates or changes an existing service. The PostgreSQL command provisions fixture-only migration/runtime roles, proves least-privilege daemon bootstrap, and rejects 27 unsafe roots covering direct, inherited, NOINHERIT/SET-only, and membership-admin paths to forbidden role attributes, PostgreSQL 18 namespace-object ownership (including distinct collation, conversion, operator, and text-search cases), DDL, table/ledger/sequence mutation, grant options, `session_replication_role` SET/ALTER SYSTEM, retention bypass, trigger drift, extension-member control, an indirect public-function trigger, and a genuinely additional function. It closes every runtime-callable Decodex function over exact signatures, overloads, metadata, settings, and canonical source. At the frozen V9 boundary, all 72 shipped functions have the exact secure `pg_catalog, decodex` function-local search path, and all 52 non-internal shipped trigger bindings—including deferred constraint triggers—have exact catalog attestations. The 16 shipped security definers comprise three V3 cursor/history functions, eleven V5-V7 Project/Policy/Program/Objective command entrypoints, and two V9 RoleProfile command entrypoints; runtime cannot insert cursors or execute capture directly. The additional fixture-only seventy-third function is migration-owned, runtime-executable, `SECURITY DEFINER`, configured with an unsafe setting, and is invoked as runtime to perform owner-authority trigger DDL before fixture restoration and independent rejection. The separate substitution fixture replaces a shipped safety body without changing its signature. The indirect-trigger fixture proves runtime DML executes a public definer function despite direct `EXECUTE`, protected-table `UPDATE`, and `TRIGGER` all being denied. The extension fixture proves a public runtime-owned extension can transactionally drop it. Six incompatible roots cover missing ledger SELECT, canonical-function drift, a dropped credential constraint with demonstrated credential insertion, an external child cascade with demonstrated runtime-mediated deletion, a same-count tampered migration ledger, and absent `pgcrypto`. The canonical PostgreSQL 18 schema manifest closes defaults, constraints on both foreign-key sides, indexes, enums, and internal constraint-trigger semantics. Descriptor-pinned socket unit fixtures reject a same-UID pre-planted endpoint in a world-writable configured directory, a mismatched operator UID pin, replaced ancestors, replaced endpoints, and deterministic replacement between precheck and failed connect; an unchanged secure stale socket maps to unreachable. An isolated daemon fixture starts Ready, replaces the configured endpoint, and proves a fresh V1.2 doctor query becomes unsafe-host-path without migration or repinning. The runtime protocol tests keep mutation receipt lookup/capacity independent across V1.1/V1.2 and prove repeated, ordered, concurrent live queries neither replay nor consume receipts. The adapter contract tampers a ledger name at constant row count and removes `pgcrypto` after bootstrap, proving read-only live revalidation reports both as incompatible before restoration. The harness also exercises an in-flight Rust BlobSession across an immediate PostgreSQL restart: the old session loses its hash lock and transaction-B connection, its stale claim cannot complete, and a reassigned exact retry verifies already-published bytes before committing metadata. It also proves `setval` denial, same-signature callable hostile-`search_path` safety, Turkish ICU credential behavior, and populated dump/restore. The XY-1264 proof additionally exercises rollback, blob, and cache behavior (`crates/decodex-postgres/src/socket.rs`, `crates/decodex-runtime/tests/bootstrap_doctor.rs`, `scripts/vnext/postgres_store_test.py`, `spikes/vnext-storage/proof.py`, `spikes/vnext-storage/README.md`). +The V10 extension raises the closed production inventory to 80 functions, 59 non-internal +triggers, and 18 security definers. The two additional definers are the command-complete +RuntimeSession creation and transition owners; the other three new private/builder routines and +three new trigger routines are security invokers. Runtime receives EXECUTE only on the two command +owners and SELECT-only access to RuntimeSessions and their profile/account snapshots. The +additional privileged-function fixture is therefore the eighty-first function at V10. The frozen +XY-1337 fixtures cover exact request substitution and replay, stable rejection replay, profile and +duplicate races, hostile DML/helper/audit namespaces, five atomic rollback boundaries, V9-to-V10 +identity-preserving upgrade, and populated restore. The manager-owned final PostgreSQL 18 run adds +clean V1-to-V10 bootstrap, classified zero-state and blocked-old-writer cutover, whole-transaction +retry, crash/restart, dump/restore, and stress schedules; those live gates are deliberately not run +during candidate construction. + The XY-1345 command is a separate non-production architecture proof. It requires exactly PostgreSQL 18.4, creates a private temporary cluster with TCP disabled, installs only fixture roles/objects, exercises the deterministic and 50-by-32 stress schedules, performs populated @@ -106,9 +119,8 @@ active implementation uses targeted Rust compilation, parser/unit contracts, and syntax checks. See [the durable evidence page](../evidence/xy-1345-exact-command-authority.md). -The PostgreSQL integration harness bootstraps the shipped four-migration history (`V1` -foundation, `V2` claim indexes, forward-only `V3` Conversation history, and forward-only `V4` -account readiness with the honest `unavailable` observation), verifies +The PostgreSQL integration harness bootstraps the shipped V1-V10 migration history, from the `V1` +foundation through the forward-only `V10` RuntimeSession snapshot authority, and verifies transaction/idempotency/revision behavior, Conversation-lock serialization with append-only history-derived positions, snapshot high-water, and immutable item-version sequence with no writable stored next-position counter, page-only opaque issued-cursor pagination with never-issued/expired/cross-Conversation/edited-chain rejection, diff --git a/openwiki/quickstart.md b/openwiki/quickstart.md index c1f6298a..fc8cfb72 100644 --- a/openwiki/quickstart.md +++ b/openwiki/quickstart.md @@ -92,7 +92,7 @@ superuser/BYPASSRLS, database/schema/table DDL, TRUNCATE, grant options, trigger authority, `session_replication_role` SET/ALTER SYSTEM, or any other retention bypass. The effective login value must be `origin`. Readiness requires a closed inventory of every runtime-callable Decodex function with exact signatures, overloads, metadata, settings, and -source bodies matching the canonical embedded migrations. The fifty-two expected safety/state/retention +source bodies matching the canonical embedded migrations. The fifty-nine expected safety/state/retention triggers must also remain enabled, correctly shaped, and bound to their canonical functions; no additional user trigger, rule, policy, RLS mode, or noncanonical expression dependency may add an indirect execution path on a runtime relation. One canonical PostgreSQL 18 schema manifest also @@ -106,10 +106,11 @@ not extension schema, so a runtime-controlled extension cannot own or drop a Dec ordered versions, names, and checksums must exactly equal the embedded migration inventory; missing SELECT is incompatible, while ownership, SET-reachable authority, table/column grant options, writes, and table DDL privileges are unsafe. All canonical database functions have an exact -function-local `pg_catalog, decodex` search path. Exactly sixteen narrowly scoped functions are +function-local `pg_catalog, decodex` search path. Exactly eighteen narrowly scoped functions are security definers: three history cursor/version functions, eleven Project/Agent/Policy/Program/Objective -commands, and the two command-complete exact RoleProfile entrypoints. Runtime cannot insert cursor, -exact-receipt, or RoleProfile rows or execute trigger/private helpers directly. +commands, two command-complete exact RoleProfile entrypoints, and two command-complete exact +RuntimeSession entrypoints. Runtime cannot insert cursor, exact-receipt, RoleProfile, +RuntimeSession, or RuntimeSession snapshot rows or execute trigger/private helpers directly. The two bound identity sequences require USAGE only; UPDATE/`setval`, SELECT, ownership, grant options, and SET-reachable surplus authority are unsafe. Explicit qualification keeps bootstrap correct under a hostile runtime `search_path`. diff --git a/openwiki/specs/vnext-gates.md b/openwiki/specs/vnext-gates.md index 672cdaa1..6ced7275 100644 --- a/openwiki/specs/vnext-gates.md +++ b/openwiki/specs/vnext-gates.md @@ -150,6 +150,16 @@ crash/concurrency, and populated restore against the production migration. Candi command code are superseded and cannot be transplanted; its valid invariants and hostile-test ideas remain provenance only. +V10 must preserve the accepted V9 exact receipt and global RoleProfile contracts while adding only +the two command-complete RuntimeSession entrypoints and their private builders/rejection helper. +Acceptance requires zero-state V9-to-V10 fencing, clean V1-to-V10 bootstrap, exact creation and +transition substitution conflicts, byte-identical success and stable-rejection replay, coherent +old-or-new profile race binding, immutable account/profile history, direct-DML/helper/audit-namespace +hostility, receipt/domain/activity/outbox/response rollback boundaries, whole-transaction retry, +blocked-old-writer cutover, crash/restart convergence, concurrency, and populated dump/restore. +These expensive PostgreSQL 18 gates run once against the frozen serial +XY-1345 -> XY-1346 -> XY-1337 candidate; implementation work does not start a live database. + ### XY-1263 acceptance and XY-1269 clean-slice reset XY-1263 landed in PR #1109. Its reviewed candidate was diff --git a/scripts/vnext/postgres_store_test.py b/scripts/vnext/postgres_store_test.py index 6648ad8a..967a935a 100644 --- a/scripts/vnext/postgres_store_test.py +++ b/scripts/vnext/postgres_store_test.py @@ -44,6 +44,14 @@ ROLE_PROFILE_CRASH_DATABASE = "decodex_xy1346_crash" ROLE_PROFILE_RESTORE_SOURCE_DATABASE = "decodex_xy1346_restore_source" ROLE_PROFILE_RESTORE_DATABASE = "decodex_xy1346_restore" +RUNTIME_SESSION_COMMAND_DATABASE = "decodex_xy1337_commands" +RUNTIME_SESSION_ROLLBACK_DATABASE = "decodex_xy1337_rollback" +RUNTIME_SESSION_RETRY_DATABASE = "decodex_xy1337_retry" +RUNTIME_SESSION_UPGRADE_DATABASE = "decodex_xy1337_upgrade" +RUNTIME_SESSION_FENCE_DATABASE = "decodex_xy1337_fence" +RUNTIME_SESSION_CRASH_DATABASE = "decodex_xy1337_crash" +RUNTIME_SESSION_RESTORE_SOURCE_DATABASE = "decodex_xy1337_restore_source" +RUNTIME_SESSION_RESTORE_DATABASE = "decodex_xy1337_restore" MIGRATION_ROLE = "decodex_migration" RUNTIME_ROLE = "decodex_runtime" FUNCTION_OWNER_ROLE = "decodex_function_owner" @@ -107,6 +115,8 @@ "decodex.achieve_objective(decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,pg_catalog.text,decodex.canonical_uuid_v4_text,pg_catalog.int8,pg_catalog.text,decodex.canonical_uuid_v4_text)", "decodex.bootstrap_role_profiles_exact(pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", "decodex.update_role_profile_exact(pg_catalog.text,pg_catalog.text,decodex.role_profile_role,pg_catalog.int8,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text,pg_catalog.text)", + "decodex.create_runtime_session_exact(pg_catalog.text,pg_catalog.text,pg_catalog.uuid,pg_catalog.uuid,decodex.role_profile_role,pg_catalog.uuid,pg_catalog.uuid,pg_catalog.text,decodex.account_state,pg_catalog.int8,pg_catalog.uuid,decodex.runtime_session_state)", + "decodex.transition_runtime_session_exact(pg_catalog.text,pg_catalog.text,pg_catalog.uuid,pg_catalog.int8,decodex.runtime_session_state)", ) TRIGGER_ONLY_SIGNATURES = ( "decodex.enforce_lease_operation_time()", @@ -143,6 +153,9 @@ "decodex.forbid_role_profile_revision_mutation()", "decodex.forbid_role_profile_truncate()", "decodex.enforce_role_profile_event_namespace()", + "decodex.enforce_runtime_session_command_owner()", + "decodex.forbid_runtime_snapshot_mutation()", + "decodex.enforce_runtime_session_event_namespace()", ) RUNTIME_TYPE_NAMES = ( "decodex.account_state", @@ -734,6 +747,152 @@ def run_role_profile_final_gate_contracts( return "\n".join(outputs) +def run_runtime_session_test(test: str, env: dict[str, str]) -> str: + return run( + [ + "cargo", "nextest", "run", "-p", "decodex-postgres", "--features", + "test-support", "--test", "postgres_store", "--run-ignored", "all", "--", + f"runtime_sessions::{test}", "--exact", + ], + env, + ) + + +def run_runtime_session_crash_recovery( + data_dir: Path, + log_path: Path, + socket_dir: Path, + port: int, + work: Path, + env: dict[str, str], +) -> str: + sync = work / "runtime-session-crash-recovery" + sync.mkdir() + test_env = env.copy() + test_env["DECODEX_RUNTIME_SESSION_RESTART_SYNC"] = str(sync) + process = subprocess.Popen( + [ + "cargo", "nextest", "run", "-p", "decodex-postgres", "--features", + "test-support", "--test", "postgres_store", "--run-ignored", "all", "--", + "runtime_sessions::postgres_exact_runtime_session_crash_recovery", "--exact", + ], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=test_env, + cwd=REPO_ROOT, + ) + try: + deadline = time.monotonic() + 30 + while time.monotonic() < deadline: + if (sync / "ready").exists(): + break + if process.poll() is not None: + stdout, stderr = process.communicate() + raise TestFailure( + f"RuntimeSession crash fixture exited early\n{stdout}\n{stderr}" + ) + time.sleep(0.02) + else: + raise TestFailure("RuntimeSession crash fixture did not reach its lock barrier") + run(["pg_ctl", "-D", str(data_dir), "-m", "immediate", "-w", "stop"], env) + run( + [ + "pg_ctl", "-D", str(data_dir), "-l", str(log_path), "-o", + f"-k {socket_dir} -p {port} -h '' -F", "-w", "start", + ], + env, + ) + (sync / "restarted").write_text("restarted", encoding="utf-8") + stdout, stderr = process.communicate(timeout=60) + if process.returncode != 0: + raise TestFailure(f"RuntimeSession crash/recovery failed\n{stdout}\n{stderr}") + return stdout.strip() or stderr.strip() + finally: + if process.poll() is None: + process.terminate() + process.wait(timeout=10) + + +def run_runtime_session_final_gate_contracts( + data_dir: Path, + log_path: Path, + socket_dir: Path, + port: int, + work: Path, + env: dict[str, str], +) -> str: + outputs: list[str] = [] + for database, test in ( + (RUNTIME_SESSION_COMMAND_DATABASE, "postgres_exact_runtime_session_commands"), + (RUNTIME_SESSION_ROLLBACK_DATABASE, "postgres_exact_runtime_session_atomic_rollback"), + (RUNTIME_SESSION_RETRY_DATABASE, "postgres_exact_runtime_session_retry_convergence"), + ): + prepare_role_profile_database(database, socket_dir, port, env) + outputs.append(run_runtime_session_test(test, env)) + + create_database(RUNTIME_SESSION_UPGRADE_DATABASE, env) + set_contract_urls( + env, socket_dir, port, RUNTIME_SESSION_UPGRADE_DATABASE, RUNTIME_ROLE + ) + outputs.append(run_runtime_session_test("postgres_v9_to_v10_runtime_session_upgrade", env)) + create_database(RUNTIME_SESSION_FENCE_DATABASE, env) + set_contract_urls(env, socket_dir, port, RUNTIME_SESSION_FENCE_DATABASE, RUNTIME_ROLE) + outputs.append( + run_runtime_session_test("postgres_v10_fences_blocked_old_runtime_writer", env) + ) + for variant in ( + "profile_snapshot", "account_snapshot", "runtime_session", "legacy_receipt", + "exact_receipt", "activity", "activity_nested_aggregate", + "activity_legacy_other_aggregate", "outbox", "outbox_nested_effect", + "outbox_activity_link", + ): + database = f"decodex_xy1337_v10_{variant}" + create_database(database, env) + set_contract_urls(env, socket_dir, port, database, RUNTIME_ROLE) + variant_env = env.copy() + variant_env["DECODEX_V10_PRIOR_STATE"] = variant + outputs.append( + run_runtime_session_test( + "postgres_v10_rejects_classified_runtime_state", variant_env + ) + ) + + prepare_role_profile_database(RUNTIME_SESSION_CRASH_DATABASE, socket_dir, port, env) + outputs.append( + run_runtime_session_crash_recovery( + data_dir, log_path, socket_dir, port, work, env + ) + ) + + prepare_role_profile_database( + RUNTIME_SESSION_RESTORE_SOURCE_DATABASE, socket_dir, port, env + ) + outputs.append(run_runtime_session_test("postgres_exact_runtime_session_commands", env)) + restore_dump = work / "xy1337-runtime-sessions.dump" + run( + [ + "pg_dump", "-Fc", "-f", str(restore_dump), + RUNTIME_SESSION_RESTORE_SOURCE_DATABASE, + ], + env, + ) + create_database(RUNTIME_SESSION_RESTORE_DATABASE, env) + run( + [ + "pg_restore", "--exit-on-error", "-d", RUNTIME_SESSION_RESTORE_DATABASE, + str(restore_dump), + ], + env, + ) + set_contract_urls( + env, socket_dir, port, RUNTIME_SESSION_RESTORE_DATABASE, RUNTIME_ROLE + ) + outputs.append(run_runtime_session_test("postgres_exact_runtime_session_restore", env)) + set_contract_urls(env, socket_dir, port, DATABASE, RUNTIME_ROLE) + return "\n".join(outputs) + + def quota_authority_snapshot(database: str, env: dict[str, str]) -> str: return psql( database, @@ -816,13 +975,15 @@ def provision_runtime(database: str, role: str, env: dict[str, str]) -> None: f"GRANT SELECT ON TABLE public.refinery_schema_history TO {role}; " f"GRANT SELECT, INSERT, UPDATE ON TABLE " f"decodex.accounts, decodex.quota_windows, decodex.command_receipts, " - f"decodex.leases, decodex.conversations, decodex.runtime_sessions, " + f"decodex.leases, decodex.conversations, " f"decodex.artifacts, decodex.turns, decodex.history_items TO {role}; " f"GRANT SELECT, INSERT ON TABLE decodex.quota_exclusions TO {role}; " - f"GRANT SELECT, INSERT ON TABLE decodex.profile_snapshots, " - f"decodex.account_snapshots, decodex.blob_objects, decodex.artifact_revisions, decodex.context_packs, " + f"GRANT SELECT, INSERT ON TABLE decodex.blob_objects, decodex.artifact_revisions, " + f"decodex.context_packs, " f"decodex.context_pack_sources, decodex.transition_proposals TO {role}; " - f"GRANT SELECT ON TABLE decodex.history_cursors, decodex.history_item_versions TO {role}; " + f"GRANT SELECT ON TABLE decodex.history_cursors, decodex.history_item_versions, " + f"decodex.profile_snapshots, decodex.account_snapshots, " + f"decodex.runtime_sessions TO {role}; " f"GRANT SELECT ON TABLE decodex.projects, decodex.agents, " f"decodex.policies, decodex.policy_revisions TO {role}; " f"GRANT SELECT ON TABLE decodex.programs, decodex.objectives, " @@ -969,6 +1130,9 @@ def main() -> int: role_profile_output = run_role_profile_final_gate_contracts( data_dir, log_path, socket_dir, port, work, env ) + runtime_session_output = run_runtime_session_final_gate_contracts( + data_dir, log_path, socket_dir, port, work, env + ) v8_boundary_output = run_v8_migration_boundary_contracts(env, socket_dir, port) set_contract_urls(env, socket_dir, port, DATABASE, RUNTIME_ROLE) restart_output = run_blob_session_restart_contract( @@ -1415,7 +1579,7 @@ def main() -> int: f"AND (SELECT count(*) FROM pg_catalog.pg_proc AS inventory " f"JOIN pg_catalog.pg_namespace AS inventory_namespace " f"ON inventory_namespace.oid = inventory.pronamespace " - f"WHERE inventory_namespace.nspname = 'decodex') = 73", + f"WHERE inventory_namespace.nspname = 'decodex') = 81", env, ) != "t|t|t|t|t|t|t|t": raise TestFailure("additional privileged-function fixture is vacuous") @@ -2328,6 +2492,7 @@ def main() -> int: assert_postgres_logs_redact((log_path,), canary_markers) print(migration_output) print(role_profile_output) + print(runtime_session_output) print(restart_output) print(v8_boundary_output) print(contract_output) diff --git a/tests/scripts/test_runtime_session_authority.py b/tests/scripts/test_runtime_session_authority.py new file mode 100644 index 00000000..65012c34 --- /dev/null +++ b/tests/scripts/test_runtime_session_authority.py @@ -0,0 +1,198 @@ +"""Static regressions for the XY-1337 V10 exact RuntimeSession boundary.""" + +from pathlib import Path +import re +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +MIGRATION = ROOT / "crates/decodex-postgres/migrations/V10__runtime_session_snapshots.sql" +RUST_API = ROOT / "crates/decodex-postgres/src/runtime_sessions.rs" +CONVERSATIONS = ROOT / "crates/decodex-postgres/src/conversations.rs" +AUTHORITY = ROOT / "crates/decodex-postgres/src/authority.rs" +MIGRATIONS = ROOT / "crates/decodex-postgres/src/migrations.rs" + + +class RuntimeSessionAuthorityTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.migration = MIGRATION.read_text(encoding="utf-8") + cls.rust_api = RUST_API.read_text(encoding="utf-8") + cls.conversations = CONVERSATIONS.read_text(encoding="utf-8") + cls.authority = AUTHORITY.read_text(encoding="utf-8") + cls.migrations = MIGRATIONS.read_text(encoding="utf-8") + + def test_v10_is_the_one_next_forward_zero_state_cutover(self) -> None: + versions = sorted( + int(path.name.split("__", 1)[0][1:]) + for path in MIGRATION.parent.glob("V*.sql") + ) + self.assertEqual(versions, list(range(1, 11))) + self.assertIn("EXPECTED_LATEST_MIGRATION_VERSION: i32 = 10", self.migrations) + self.assertIn("IN ACCESS EXCLUSIVE MODE", self.migration) + self.assertIn("runtime_session_v10_zero_state", self.migration) + self.assertIn("operation IN ('create_runtime_session', 'transition_runtime_session')", self.migration) + + def test_creation_request_contains_only_complete_typed_consumed_inputs(self) -> None: + match = re.search( + r"CREATE FUNCTION decodex\.build_runtime_session_create_request\((.*?)\) RETURNS jsonb", + self.migration, + re.DOTALL, + ) + self.assertIsNotNone(match) + signature = match.group(1) + for required in ( + "p_session_id uuid", "p_conversation_id uuid", + "p_role decodex.role_profile_role", "p_account_snapshot_id uuid", + "p_source_account_id uuid", "p_display_label text", + "p_observed_state decodex.account_state", + "p_account_source_revision bigint", "p_codex_thread_id uuid", + "p_initial_state decodex.runtime_session_state", + ): + self.assertIn(required, signature) + for forbidden in ( + "profile_snapshot_id", "profile_revision", "request_digest", + "claim_token", "lease", "takeover", "pending", + ): + self.assertNotIn(forbidden, signature) + + def test_transition_request_is_exactly_session_revision_and_target(self) -> None: + match = re.search( + r"CREATE FUNCTION decodex\.build_runtime_session_transition_request\((.*?)\) RETURNS jsonb", + self.migration, + re.DOTALL, + ) + self.assertIsNotNone(match) + signature = match.group(1) + self.assertIn("p_session_id uuid", signature) + self.assertIn("p_expected_revision bigint", signature) + self.assertIn("p_target_state decodex.runtime_session_state", signature) + self.assertNotIn("note", signature) + + def test_commands_are_command_complete_and_replay_stored_bytes(self) -> None: + for name in ("create_runtime_session_exact", "transition_runtime_session_exact"): + body = self.migration.split(f"CREATE FUNCTION decodex.{name}", 1)[1] + body = body.split("END\n$$;", 1)[0] + self.assertIn("SECURITY DEFINER", body) + self.assertIn("existing_request <> request_value", body) + self.assertIn("RETURN existing_response", body) + self.assertIn("response_bytes = response_value", body) + self.assertIn("activity_sequence", body) + self.assertIn("outbox_id", body) + self.assertIn("FOR SHARE OF profile", self.migration) + self.assertIn("ON CONFLICT (account_snapshot_id) DO NOTHING", self.migration) + self.assertIn("account_snapshot_conflict", self.migration) + + def test_commands_acquire_hierarchy_coordinator_before_tuple_selection(self) -> None: + create = self.migration.split( + "CREATE FUNCTION decodex.create_runtime_session_exact", 1 + )[1].split("CREATE FUNCTION decodex.transition_runtime_session_exact", 1)[0] + transition = self.migration.split( + "CREATE FUNCTION decodex.transition_runtime_session_exact", 1 + )[1] + self.assertLess( + create.index("pg_advisory_xact_lock(1271)"), + create.index("SELECT status INTO conversation_status"), + ) + self.assertLess( + transition.index("pg_advisory_xact_lock(1271)"), + transition.index("FROM decodex.runtime_sessions AS session"), + ) + + def test_audit_namespace_closes_nested_and_legacy_representations(self) -> None: + for marker in ( + "runtime_session_recorded", + "@.aggregate_kind == \"runtime_session\"", + "@.event_kind == \"runtime_session_transitioned\"", + "exists(@.runtime_session)", + "exists(@.runtime_session_id)", + "exists(@.profile_snapshot)", + "exists(@.account_snapshot)", + "jsonb_path_query(", + "$.**.activity_sequence", + ): + self.assertIn(marker, self.migration) + + def test_stable_rejections_and_immutable_full_snapshots_are_closed(self) -> None: + for code in ( + "missing_target", "duplicate_target", "stale_revision", + "illegal_transition", "invalid_account_snapshot", + "account_snapshot_conflict", + ): + self.assertIn(code, self.migration) + self.assertIn(self._rust_variant(code), self.rust_api) + for field in ("instructions", "provenance", "instructions_digest", "source_revision"): + self.assertIn(field, self.migration) + self.assertIn(field, self.rust_api) + self.assertIn("RuntimeSession snapshots are immutable", self.migration) + + def test_runtime_dml_helpers_and_event_namespaces_are_closed(self) -> None: + for relation in ("profile_snapshots", "account_snapshots", "runtime_sessions"): + self.assertIn(f"('{relation}', true, false, false, false)", self.authority) + self.assertIn(f"{relation}_command_owner", self.migration) + runtime = self.authority.split("const RUNTIME_EXECUTE_FUNCTIONS", 1)[1].split("];", 1)[0] + self.assertIn("create_runtime_session_exact", runtime) + self.assertIn("transition_runtime_session_exact", runtime) + self.assertNotIn("complete_exact_runtime_session_rejection", runtime) + self.assertIn("activity_runtime_session_namespace", self.migration) + self.assertIn("outbox_runtime_session_namespace", self.migration) + + def test_rust_input_excludes_postgresql_authored_facts_and_parses_full_effect(self) -> None: + create_input = self.rust_api.split( + "pub struct CreateRuntimeSessionAccountSnapshot", 1 + )[1].split("}", 1)[0] + self.assertNotIn("created_at", create_input) + for required in ( + "RuntimeSessionCommandEffect", "prior_state", "new_state", + "prior_revision", "new_revision", "activity_sequence", + "activity_payload", "outbox_id", "outbox_payload", + ): + self.assertIn(required, self.rust_api) + self.assertIn("exact RuntimeSession response effect is inconsistent", self.rust_api) + self.assertIn("exact RuntimeSession audit effect is inconsistent", self.rust_api) + self.assertIn("validate_request_context", self.rust_api) + self.assertIn("parse_create_response", self.rust_api) + self.assertIn("parse_transition_response", self.rust_api) + self.assertIn("stored RuntimeSession UUID is invalid", self.rust_api) + + def test_focused_final_gate_fixtures_are_named_without_running_them(self) -> None: + fixture = ( + ROOT + / "crates/decodex-postgres/tests/postgres_store/runtime_sessions.rs" + ).read_text(encoding="utf-8") + for fixture_name in ( + "postgres_exact_runtime_session_commands", + "postgres_exact_runtime_session_atomic_rollback", + "postgres_exact_runtime_session_retry_convergence", + "postgres_v9_to_v10_runtime_session_upgrade", + "postgres_v10_rejects_classified_runtime_state", + "postgres_v10_fences_blocked_old_runtime_writer", + "postgres_exact_runtime_session_crash_recovery", + "postgres_exact_runtime_session_restore", + ): + self.assertIn(fixture_name, fixture) + for boundary in ("receipt", "domain", "activity", "outbox", "response"): + self.assertIn(f'"{boundary}"', fixture) + for hostile in ( + "IdempotencyConflict", "DuplicateTarget", "MissingTarget", + "StaleRevision", "IllegalTransition", "AccountSnapshotConflict", + "profile_race", "duplicate_race", + ): + self.assertIn(hostile, fixture) + + def test_rust_uses_only_exact_entrypoints_and_one_retry_owner(self) -> None: + self.assertIn("create_runtime_session_exact", self.rust_api) + self.assertIn("transition_runtime_session_exact", self.rust_api) + self.assertIn("execute_exact_with_retry", self.rust_api) + self.assertNotIn("command_receipts", self.rust_api) + self.assertNotRegex(self.rust_api, r"(?i)\b(?:insert|update|delete)\s+(?:into|from)?\s*decodex\.") + self.assertNotIn("pub async fn create_runtime_session", self.conversations) + self.assertNotIn("pub async fn transition_runtime_session", self.conversations) + + @staticmethod + def _rust_variant(code: str) -> str: + return "".join(part.title() for part in code.split("_")) + + +if __name__ == "__main__": + unittest.main() From e84f788cea7e1d19f9925b6197fe2a54691fdfdd Mon Sep 17 00:00:00 2001 From: XV Date: Fri, 17 Jul 2026 02:25:53 -0400 Subject: [PATCH 5/7] {"schema":"decodex/commit/2","change":"XY-1346 applies canonical nightly rustfmt layout to the PostgreSQL role profile test without semantic changes.","authority":"manual","impact":"compatible"} --- .../tests/postgres_store/role_profiles.rs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/crates/decodex-postgres/tests/postgres_store/role_profiles.rs b/crates/decodex-postgres/tests/postgres_store/role_profiles.rs index 46aa0aee..b4016dcc 100644 --- a/crates/decodex-postgres/tests/postgres_store/role_profiles.rs +++ b/crates/decodex-postgres/tests/postgres_store/role_profiles.rs @@ -358,9 +358,8 @@ async fn postgres_exact_role_profile_concurrency() -> Result<(), Box update_success += 1, - RoleProfileCommandOutcome::Rejected(RoleProfileRejection::StaleRevision) => { - update_stale += 1 - }, + RoleProfileCommandOutcome::Rejected(RoleProfileRejection::StaleRevision) => + update_stale += 1, other => panic!("unexpected concurrent update outcome: {other:?}"), } } @@ -383,8 +382,7 @@ async fn postgres_exact_role_profile_concurrency() -> Result<(), Box Result<(), Box> { let (migration, runtime) = separated_configs("DECODEX_TEST")?; - let store = - PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + let store = PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; assert!(matches!( store.bootstrap_role_profiles("rollback-bootstrap", &bootstrap()).await?, RoleProfileCommandOutcome::Success(_) @@ -442,10 +440,7 @@ async fn postgres_exact_role_profile_atomic_rollback() -> Result<(), Box Result<(), Box Result<(), Box> { let (migration, runtime) = separated_configs("DECODEX_TEST")?; - let store = - PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; + let store = PostgresStore::connect(migration.clone(), runtime, expected_peer_uid()).await?; assert!(matches!( store.bootstrap_role_profiles("retry-bootstrap", &bootstrap()).await?, RoleProfileCommandOutcome::Success(_) @@ -526,9 +520,7 @@ async fn postgres_exact_role_profile_retry_convergence() -> Result<(), Box Date: Fri, 17 Jul 2026 02:42:54 -0400 Subject: [PATCH 6/7] {"schema":"decodex/commit/2","change":"Refactor RuntimeSession PostgreSQL command fixtures into bounded phases and propagate fixture identity errors.","authority":"manual","impact":"compatible"} --- .../tests/postgres_store/runtime_sessions.rs | 168 +++++++++++------- 1 file changed, 107 insertions(+), 61 deletions(-) diff --git a/crates/decodex-postgres/tests/postgres_store/runtime_sessions.rs b/crates/decodex-postgres/tests/postgres_store/runtime_sessions.rs index f0e36ea1..2732f695 100644 --- a/crates/decodex-postgres/tests/postgres_store/runtime_sessions.rs +++ b/crates/decodex-postgres/tests/postgres_store/runtime_sessions.rs @@ -31,14 +31,18 @@ fn profiles(marker: &str) -> BootstrapRoleProfiles { } } -fn account(snapshot_id: &str, marker: &str) -> CreateRuntimeSessionAccountSnapshot { - CreateRuntimeSessionAccountSnapshot { +fn account( + snapshot_id: &str, + marker: &str, +) -> Result { + Ok(CreateRuntimeSessionAccountSnapshot { account_snapshot_id: snapshot_id.into(), - source_account_id: AccountId::new("13000000-0000-4000-8000-000000000001").unwrap(), + source_account_id: AccountId::new("13000000-0000-4000-8000-000000000001") + .map_err(|_| StoreError::InvalidInput("invalid fixture account identity"))?, display_label: format!("Account {marker}"), observed_state: AccountState::Unknown, source_revision: 7, - } + }) } fn creation( @@ -46,15 +50,16 @@ fn creation( conversation_id: &ConversationId, snapshot_id: &str, marker: &str, -) -> CreateRuntimeSession { - CreateRuntimeSession { - runtime_session_id: RuntimeSessionId::new(session_id).unwrap(), +) -> Result { + Ok(CreateRuntimeSession { + runtime_session_id: RuntimeSessionId::new(session_id) + .map_err(|_| StoreError::InvalidInput("invalid fixture RuntimeSession identity"))?, conversation_id: conversation_id.clone(), role: RoleProfileRole::Task, - account_snapshot: account(snapshot_id, marker), + account_snapshot: account(snapshot_id, marker)?, codex_thread_id: Some(session_id.replacen("41000000", "44000000", 1)), initial_state: RuntimeSessionState::Starting, - } + }) } async fn stored_response(client: &Client, key: &str) -> Result, tokio_postgres::Error> { @@ -77,48 +82,34 @@ fn success( effect } -#[tokio::test(flavor = "multi_thread", worker_threads = 8)] -#[ignore = "requires an isolated PostgreSQL 18 V10 RuntimeSession database"] -async fn postgres_exact_runtime_session_commands() -> Result<(), Box> { - let (migration, runtime) = separated_configs("DECODEX_TEST")?; - let store = - PostgresStore::connect(migration.clone(), runtime.clone(), expected_peer_uid()).await?; - let (migration_client, migration_connection) = migration.connect(NoTls).await?; - let migration_task = tokio::spawn(migration_connection); - - assert!(matches!( - store.bootstrap_role_profiles("session-profiles", &profiles("v1")).await?, - RoleProfileCommandOutcome::Success(_) - )); - let conversation_id = ConversationId::new("40000000-0000-4000-8000-000000000001")?; - store - .create_conversation( - &CommandIdentity::new("session-conversation", b"session-conversation-v1")?, - &CreateConversation { - conversation_id: conversation_id.clone(), - title: "Exact RuntimeSession fixture".into(), - }, - ) - .await?; +struct CreatedSessionFixture { + request: CreateRuntimeSession, + effect: RuntimeSessionCommandEffect, +} +async fn assert_creation_command( + store: &PostgresStore, + client: &Client, + conversation_id: &ConversationId, +) -> Result> { let create = creation( "41000000-0000-4000-8000-000000000001", - &conversation_id, + conversation_id, "43000000-0000-4000-8000-000000000001", "one", - ); + )?; let created = success(store.create_runtime_session("session-create", &create).await?); assert_eq!(created.prior_state, None); assert_eq!(created.prior_revision, None); assert_eq!(created.new_state, RuntimeSessionState::Starting); assert_eq!(created.new_revision, 1); assert_eq!(created.runtime_session.last_known_turn_id, None); - let create_bytes = stored_response(&migration_client, "session-create").await?; + let create_bytes = stored_response(client, "session-create").await?; assert_eq!( store.create_runtime_session("session-create", &create).await?, RuntimeSessionCommandOutcome::Success(created.clone()), ); - assert_eq!(stored_response(&migration_client, "session-create").await?, create_bytes); + assert_eq!(stored_response(client, "session-create").await?, create_bytes); let mut substitutions = Vec::new(); let mut changed = create.clone(); @@ -163,18 +154,18 @@ async fn postgres_exact_runtime_session_commands() -> Result<(), Box Result<(), Box Result<(), Box> { let transitioned = success( store .transition_runtime_session( @@ -205,7 +203,7 @@ async fn postgres_exact_runtime_session_commands() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { let before_update = created.runtime_session.profile_snapshot.clone(); assert!(matches!( store.update_role_profile("task-v2", RoleProfileRole::Task, 1, &profile("task-v2")).await?, RoleProfileCommandOutcome::Success(_) )); + let after_update_request = creation( + "41000000-0000-4000-8000-000000000004", + conversation_id, + "43000000-0000-4000-8000-000000000004", + "four", + )?; let after_update = success( - store - .create_runtime_session( - "session-after-profile-update", - &creation( - "41000000-0000-4000-8000-000000000004", - &conversation_id, - "43000000-0000-4000-8000-000000000004", - "four", - ), - ) - .await?, + store.create_runtime_session("session-after-profile-update", &after_update_request).await?, ); assert_eq!(before_update.source_revision, 1); assert_eq!(after_update.runtime_session.profile_snapshot.source_revision, 2); - let preserved: bool = migration_client + let preserved: bool = client .query_one( "SELECT model=$2 AND instructions=$3 AND source_revision=1 \ FROM decodex.profile_snapshots WHERE profile_snapshot_id=$1::text::uuid", @@ -311,7 +313,7 @@ async fn postgres_exact_runtime_session_commands() -> Result<(), Box Result<(), Box Result<(), Box> { let (runtime_client, runtime_connection) = runtime.connect(NoTls).await?; let runtime_task = tokio::spawn(runtime_connection); for statement in [ @@ -357,14 +364,20 @@ async fn postgres_exact_runtime_session_commands() -> Result<(), Box Result<(), Box> { let mut duplicate_race = JoinSet::new(); let contested = creation( "41000000-0000-4000-8000-000000000005", - &conversation_id, + conversation_id, "43000000-0000-4000-8000-000000000005", "five", - ); + )?; for index in 0..16 { let store = store.clone(); let contested = contested.clone(); @@ -383,6 +396,39 @@ async fn postgres_exact_runtime_session_commands() -> Result<(), Box Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + let store = + PostgresStore::connect(migration.clone(), runtime.clone(), expected_peer_uid()).await?; + let (migration_client, migration_connection) = migration.connect(NoTls).await?; + let migration_task = tokio::spawn(migration_connection); + + assert!(matches!( + store.bootstrap_role_profiles("session-profiles", &profiles("v1")).await?, + RoleProfileCommandOutcome::Success(_) + )); + let conversation_id = ConversationId::new("40000000-0000-4000-8000-000000000001")?; + store + .create_conversation( + &CommandIdentity::new("session-conversation", b"session-conversation-v1")?, + &CreateConversation { + conversation_id: conversation_id.clone(), + title: "Exact RuntimeSession fixture".into(), + }, + ) + .await?; + + let created = assert_creation_command(&store, &migration_client, &conversation_id).await?; + assert_transition_command(&store, &migration_client, &created.request).await?; + assert_profile_snapshot_race(&store, &migration_client, &conversation_id, &created.effect) + .await?; + assert_runtime_authority_denials(&runtime).await?; + assert_duplicate_creation_race(&store, &conversation_id).await?; drop(migration_client); migration_task.await??; @@ -469,7 +515,7 @@ async fn postgres_exact_runtime_session_atomic_rollback() -> Result<(), Box Result<(), Box Result<(), Box Date: Fri, 17 Jul 2026 02:52:36 -0400 Subject: [PATCH 7/7] {"schema":"decodex/commit/2","change":"Refactor the PostgreSQL RoleProfile command test into bounded phases without changing behavior.","authority":"manual","impact":"compatible"} --- .../tests/postgres_store/role_profiles.rs | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/crates/decodex-postgres/tests/postgres_store/role_profiles.rs b/crates/decodex-postgres/tests/postgres_store/role_profiles.rs index b4016dcc..c9196a5a 100644 --- a/crates/decodex-postgres/tests/postgres_store/role_profiles.rs +++ b/crates/decodex-postgres/tests/postgres_store/role_profiles.rs @@ -1,12 +1,12 @@ use std::{env, path::PathBuf, time::Duration}; use tokio::{task::JoinSet, time}; -use tokio_postgres::{Client, NoTls}; +use tokio_postgres::{Client, Config, NoTls}; use super::{expected_peer_uid, separated_configs}; use decodex_postgres::{ BootstrapRoleProfiles, PostgresStore, RoleProfileCommandOutcome, RoleProfileConfiguration, - RoleProfileRejection, RoleProfileRole, StoreError, + RoleProfileRejection, RoleProfileRevision, RoleProfileRole, StoreError, }; fn configuration(marker: &str) -> RoleProfileConfiguration { @@ -45,10 +45,9 @@ async fn role_profile_state(client: &Client) -> Result<[i64; 6], tokio_postgres: Ok(std::array::from_fn(|index| row.get(index))) } -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -#[ignore = "requires an isolated PostgreSQL 18 V9 RoleProfile database"] -async fn postgres_exact_role_profile_commands() -> Result<(), Box> { - let (migration, runtime) = separated_configs("DECODEX_TEST")?; +async fn assert_execution_path_contract( + runtime: &Config, +) -> Result<(), Box> { let (probe, probe_connection) = runtime.clone().connect(NoTls).await?; let probe_task = tokio::spawn(probe_connection); let (execution_sql, allowed) = PostgresStore::execution_path_contract_fixture(); @@ -62,19 +61,23 @@ async fn postgres_exact_role_profile_commands() -> Result<(), Box Result, Box> { let RoleProfileCommandOutcome::Success(created) = - store.bootstrap_role_profiles("role-bootstrap", &profiles).await? + store.bootstrap_role_profiles("role-bootstrap", profiles).await? else { panic!("first exact bootstrap must succeed"); }; assert_eq!(created.len(), 4); assert!(created.iter().all(|profile| profile.revision == 1)); assert_eq!( - store.bootstrap_role_profiles("role-bootstrap", &profiles).await?, + store.bootstrap_role_profiles("role-bootstrap", profiles).await?, RoleProfileCommandOutcome::Success(created.clone()), "same request replays parsed stored bytes", ); @@ -86,7 +89,7 @@ async fn postgres_exact_role_profile_commands() -> Result<(), Box Result<(), Box Result<(), Box> { let (runtime_client, runtime_connection) = runtime.connect(NoTls).await?; let runtime_task = tokio::spawn(runtime_connection); for statement in [ @@ -166,6 +175,15 @@ async fn postgres_exact_role_profile_commands() -> Result<(), Box, +) -> Result<(), Box> { let (migration_client, migration_connection) = migration.connect(NoTls).await?; let migration_task = tokio::spawn(migration_connection); let response_before: Vec = migration_client @@ -177,7 +195,7 @@ async fn postgres_exact_role_profile_commands() -> Result<(), Box = migration_client @@ -262,6 +280,21 @@ async fn postgres_exact_role_profile_commands() -> Result<(), Box Result<(), Box> { + let (migration, runtime) = separated_configs("DECODEX_TEST")?; + assert_execution_path_contract(&runtime).await?; + let store = + PostgresStore::connect(migration.clone(), runtime.clone(), expected_peer_uid()).await?; + let profiles = bootstrap(); + let created = exercise_role_profile_commands(&store, &profiles).await?; + assert_runtime_role_profile_restrictions(&runtime).await?; + assert_migration_role_profile_invariants(&migration, &store, &profiles, created).await?; + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 8)] #[ignore = "requires an isolated PostgreSQL 18 V9 RoleProfile concurrency database"] async fn postgres_exact_role_profile_concurrency() -> Result<(), Box> {