task-7-report.md 12 KB

Task 7 Report — Trusted rule generation and publication gates

Date: 2026-07-24 Branch: codex/data-rule-execution-m3a-m5

Outcome

Task 7 establishes a fail-closed path from an AI-authored natural-language candidate to an immutable published RuleVersion. Neither caller-supplied compile evidence nor caller-supplied test evidence can advance lifecycle state.

The delivered chain is:

  1. /interpret resolves distinct server-owned input/output schema snapshots, a required input sample, and an optional golden output before invoking the authoring model.
  2. A ready rule candidate is persisted with its actor, source hash, candidate hash, RuleSpec hash, model hash, prompt hash, context hash, and all bounded repair attempts.
  3. The server issues a short-lived HMAC-SHA256 generation receipt over that closed claim set.
  4. RuleVersion creation locks the generation row, verifies every claim against canonical database state, consumes the receipt exactly once, and creates a draft RuleVersion plus an immutable validation profile.
  5. Logical validation compiles a closed, validation-only Polars plan against the separately pinned input and output schema snapshots.
  6. Logical testing executes that exact plan and any golden comparison in the same isolated Polars worker against pinned MinIO artifacts. Digest, schema, resource, row, expiry, and ownership checks are enforced by ArtifactStore and the bounded worker.
  7. Successful evidence advances RuleVersion draft -> validated and logical plan compiled -> tested.
  8. Publication requires exact successful compile and test evidence and advances both objects to published.
  9. Deployment-bound physical plans independently pass compiled -> tested -> published. They must reference a published RuleVersion and its successful logical test evidence.

The public catalog returns published rules only. DataFlow release continues to consume published plans only, so Task 7 does not weaken the Task 4–6 execution boundary.

Receipt and concurrency boundary

GenerationReceiptSigner signs a compact, closed JSON claim set containing:

  • receipt version;
  • generation run ID;
  • actor UID;
  • source text hash;
  • candidate hash;
  • RuleSpec hash;
  • model hash;
  • prompt hash;
  • validation context hash;
  • issued-at and expiry timestamps.

Receipts use the dedicated required RULE_GENERATION_RECEIPT_SECRET and never fall back to the Flask session secret. Issued-at, expiry, maximum lifetime, and clock skew are checked cryptographically and against current database time during consumption.

Draft creation performs SELECT ... FOR UPDATE on the generation record and a compare-and-set consumption update. A new consumption is rejected if it is expired, tampered, belongs to another actor, has a different source/RuleSpec/candidate or validation context, has already been consumed, or is linked to another version.

An exact same-actor receipt replay after an unknown commit outcome returns the already-linked canonical draft even after the short-lived receipt expires. This replay path still requires the same signed receipt hash and every canonical claim to match; expiry is bypassed only after the consumed row is proven exact. New consumption uses PostgreSQL clock_timestamp() rather than the transaction-start timestamp, so a long-running transaction cannot extend a receipt's usable lifetime.

Publication locks the rule/plan/profile/schema or plan/component/rule/binding/schema rows in one transaction and re-reads the current compiler, RuleSpec, plan, schema, binding, data source, object, dialect, evidence, and audit chain. Same-actor retries after an unknown commit outcome return the canonical published result; a different actor cannot use that replay path. Real PostgreSQL concurrency tests use independent threads, connections, and sessions: same-actor contenders return one canonical result and one audit, while a different actor is rejected after the winning commit.

AI repair boundary

The authoring agent now has a deterministic maximum of two repair attempts. Each attempt persists:

  • attempt number and outcome;
  • candidate hash;
  • deterministic error code;
  • model, prompt, and context hashes.

The prompt hash is calculated from the actual full prompt sent for that attempt, including the output JSON schema and accumulated repair messages. The final generation receipt therefore binds the final repair prompt, not the initial prompt.

Only malformed JSON and closed candidate-contract errors are repairable. Ambiguity and low confidence return clarification requirements immediately. Permission failures, destructive scope, lifecycle failures, and execution failures are never sent into the automatic repair loop.

Logical and physical evidence

Logical compile evidence binds the RuleVersion, compiler version, exact plan hash, schema hashes, capabilities, and actor. Logical test evidence additionally binds the exact trusted run ID and server-generated attestation:

  • input artifact digest;
  • output artifact digest;
  • output schema hash;
  • violation digest;
  • bounded row and violation counts.

All four evidence writers use INSERT ... RETURNING. On a uniqueness conflict they load the canonical database row, compare the complete security-relevant content, and return the real existing evidence ID; divergent replay fails closed.

The logical runner supports assert with both reject and quarantine. Quarantined rows are removed from the primary output, counted separately as rows_quarantined, and are not misreported as rejected. The compiler version was advanced to dataops-polars-1.43.0 for this result-contract change.

Physical SQL preflight validates the exact SQLGlot-bound plan and executes only EXPLAIN <compiled INSERT ... SELECT ...> through the governed data-source manager. It never uses ANALYZE and never executes the DML. The resulting evidence contains dialect, statement digest, and EXPLAIN digest. Real PostgreSQL and MySQL tests assert that destination row counts remain zero.

Physical Polars preflight stages the latest trusted, ready, unexpired input artifact and executes the exact bound plan in the isolated worker. Both physical backends are rejected on current schema, binding, dialect, plan-hash, RuleVersion, or logical-evidence drift.

Logical dry-run supports a distinct output schema and optional golden Parquet result. The server stages both artifacts and passes their local paths and digests to one spawned worker governed by the plan's single RLIMIT/RSS/timeout budget. That worker verifies the staged golden digest and compares exact schema, row count, row order, null positions, and field values. The Flask process does not materialize either Parquet result. Concurrent large-golden tests fail closed inside the same resource boundary. The real cross-schema acceptance reads name/mobile, derives name_copy, removes one quarantined row, and compares the output to the pinned golden artifact.

Forward-only migration

Migration 20260723_200 is forward-only and adds:

  • corrected RuleVersion and execution-plan lifecycle constraints;
  • receipt hash, consumption timestamp, validation context, model hash, and prompt hash on generation runs;
  • one-time receipt/version uniqueness constraints;
  • persisted generation attempts;
  • immutable validation profiles;
  • logical plans and logical compile/test evidence;
  • physical compile/test evidence binding fields;
  • validation failures and publication audits;
  • the published-rule catalog index.

The migration refuses to run when revision 190 contains any RuleVersion or execution plan, including draft and validated rows, because no pre-Task-7 row can possess the required generation, compile, test, and publication provenance. Operators must export and remove all legacy rows, apply migration 200, and then rebuild each rule through the Task 7 chain. Physical evidence carries an explicit legacy_untrusted marker, and publication/Runner/DataFlow gates require it to be false. Downgrade is deliberately rejected because deleting publication and generation evidence would break audit and replay guarantees.

API surface

The following server-owned operations are available:

  • POST /api/rules/interpret;
  • POST /api/rules/rule-versions;
  • POST /api/rules/rule-versions/<id>/validate;
  • POST /api/rules/rule-versions/<id>/test;
  • POST /api/rules/rule-versions/<id>/publish;
  • GET /api/rules/rule-versions/<id>/evidence;
  • GET /api/rules/catalog;
  • POST /api/rules/execution-plans/<id>/validate;
  • POST /api/rules/execution-plans/<id>/test;
  • POST /api/rules/execution-plans/<id>/publish.

Request bodies and catalog query parameters use closed shapes. Compile/test endpoints do not accept caller evidence. Editor permissions cover validation/testing while publication keeps the existing administrative permission boundary. The documented canonical catalog path is implemented; the prior nested path remains a compatibility alias.

/interpret preflights the dedicated receipt signer before resolving context or invoking the model, so missing configuration cannot incur model cost. Local and production environment templates and deployment runbooks document generation and coordinated rotation of RULE_GENERATION_RECEIPT_SECRET without containing a real secret.

DataFlow release no longer trusts RuleVersion status alone. Its production asset loader requires the exact published logical plan, successful compile evidence, successful test evidence, matching plan/schema/capability hashes, and the matching publication audit. Revoked or drifted logical evidence makes the referenced rule unavailable and the release fails closed.

Acceptance evidence

Fail-first evidence included:

  • publication rejected without exact compile and test evidence;
  • invalid, expired, replayed, actor-mismatched, source-mismatched, and RuleSpec-mismatched receipts;
  • concurrent receipt consumption;
  • caller-forged test evidence;
  • schema, binding, dialect, and plan drift;
  • bounded repair exhaustion and no-repair ambiguity;
  • initial quarantine compilation failure before support was added.

Final verification:

  • focused second-round trust-boundary suite: 39 passed;
  • Golden negative tests use real file SHA-256 and equal-row content drift to exercise worker-side row comparison;
  • focused data-rule/API/migration suite from the initial delivery: 183 passed;
  • Runner plus real PostgreSQL/MySQL/Polars regression: 126 passed;
  • real PostgreSQL + MinIO cross-schema receipt/logical lifecycle: 1 passed;
  • real PhysicalPlanPublicationService PostgreSQL/MySQL lifecycle, drift, evidence replay, and PostgreSQL concurrency: 2 passed;
  • full repository suite: 632 passed, 29 skipped, 59 subtests passed;
  • changed-file Ruff: All checks passed!;
  • git diff --check: passed;
  • empty local PostgreSQL full Alembic rebuild: base through 20260723_200 (head);
  • real revision-190 draft/validated/published upgrade guards: 3 passed.
  • rebuilt local Docker backend: container reported healthy, and GET /api/system/health returned HTTP/application code 200 with database and Neo4j healthy.

Skipped tests are environment-gated integration suites. The required real PostgreSQL, MySQL, and MinIO acceptance tests above were run explicitly against the local Docker services and passed.

Residual scope

Task 7 records publication preflight run IDs and exact evidence in dedicated logical/physical evidence tables. It does not fabricate production rule_runs for logical validation because those rows require a real deployment and component binding. Production execution evidence remains owned by the Task 6 Runner path after a physical plan is published.

Task 7 now supports separate input/output snapshots, cross-schema transformations, deployable secret templates, and a documented rotation procedure. The remaining scope is production operational rollout: multi-replica load and failure testing, monitoring/alert thresholds, and production canary/rollback evidence. Those operational gates are outside this local Docker acceptance and must be closed before a production environment is declared ready.