2518 lines
105 KiB
Python
2518 lines
105 KiB
Python
#!/usr/bin/env python3
|
|
"""Apply an approved kb_stage proposal into canonical public.* rows.
|
|
|
|
Stage 2 of the KB apply pipeline: approve -> APPLY -> render -> surface.
|
|
|
|
Governance boundary
|
|
-------------------
|
|
This tool connects to Postgres as the narrow ``kb_apply`` login role, never as
|
|
superuser. ``kb_apply`` can insert reviewed claim/source/reasoning-tool rows and
|
|
write the strategy/evidence/edge tables required by supported contracts. It has
|
|
SELECT-only access to ``kb_stage.kb_proposals``; payload-bound SECURITY DEFINER
|
|
functions are the only way it can lock and finish an approved ledger row. It
|
|
holds no DELETE or DDL and no write access to unrelated tables (agents, budgets,
|
|
personas, ...). This enforces
|
|
"agents propose, but do not self-apply" at the database boundary rather than by
|
|
convention. The tool is an operator, invoked by a human/reviewer after approval;
|
|
it is deliberately not part of the agent runtime.
|
|
|
|
Flow
|
|
----
|
|
1. Read the proposal (must be ``status='approved'``).
|
|
2. Dispatch by ``proposal_type`` to build the canonical write from a strict
|
|
``apply_payload`` contract carried on the proposal payload.
|
|
3. Emit a single transaction: canonical writes, then a DO block that flips the
|
|
ledger to ``applied`` asserting rowcount=1 (the concurrent double-apply
|
|
guard), stamps ``applied_by_agent_id`` as a real FK, and verifies invariants
|
|
(RAISE on violation -> rollback), COMMIT.
|
|
|
|
Prerequisites (one-time, superuser): scripts/kb_apply_prereqs.sql bootstraps the
|
|
``kb-apply`` service-agent row, grants ``kb_apply`` SELECT on public.agents, and
|
|
ensures the one-active-strategy unique index. Run it before the first apply.
|
|
|
|
``--dry-run`` prints the exact SQL and does not connect for writes.
|
|
|
|
Contract (v2)
|
|
-------------
|
|
Freeform Leo eval packets are NOT applied directly. Each proposal must carry a
|
|
strict ``apply_payload`` object; a separate normalizer produces it. Shapes:
|
|
|
|
revise_strategy:
|
|
{"apply_payload": {
|
|
"agent_id": "<uuid>",
|
|
"strategy": {"diagnosis": str, "guiding_policy": str,
|
|
"proximate_objectives": [ ... ]},
|
|
"strategy_nodes": [{"node_type": "diagnosis|policy|objective",
|
|
"title": str, "body": str, "rank": int}, ...]}}
|
|
|
|
add_edge:
|
|
{"apply_payload": {"from_claim": "<uuid>", "to_claim": "<uuid>",
|
|
"edge_type": "<edge_type>", "weight": <0..1|null>}}
|
|
|
|
attach_evidence:
|
|
{"apply_payload": {"evidence": [{"claim_id": "<uuid>",
|
|
"source_id": "<uuid>",
|
|
"role": "<evidence_role>",
|
|
"weight": <0..1|null>}, ...]}}
|
|
|
|
approve_claim:
|
|
{"apply_payload": {
|
|
"agent_id": "<uuid|null>",
|
|
"claims": [{"id": "<uuid>", "type": "<claim type>",
|
|
"text": str, "status": "open", "confidence": <0..1|null>,
|
|
"tags": [str, ...], "created_by": "<uuid|null>"}, ...],
|
|
"sources": [{"id": "<uuid>", "source_type": "<source type>",
|
|
"url": str|null, "storage_path": str|null,
|
|
"excerpt": str|null, "hash": str,
|
|
"created_by": "<uuid|null>"}, ...],
|
|
"evidence": [{"claim_id": "<uuid>", "source_id": "<uuid>",
|
|
"role": "<evidence role>", "weight": <0..1|null>,
|
|
"created_by": "<uuid|null>"}, ...],
|
|
"edges": [{"from_claim": "<uuid>", "to_claim": "<uuid>",
|
|
"edge_type": "<edge type>", "weight": <0..1|null>,
|
|
"created_by": "<uuid|null>"}, ...],
|
|
"reasoning_tools": [{"id": "<uuid>", "agent_id": "<uuid|null>",
|
|
"name": str, "description": str,
|
|
"category": str|null}, ...]}}
|
|
|
|
``approve_claim`` is the only source-creation contract. It is an atomic,
|
|
insert-only canonical bundle: existing IDs must match the reviewed payload
|
|
exactly, source hashes cannot alias another ID, and every row is verified before
|
|
the approved proposal ledger can flip to ``applied``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import kb_apply_replay_receipt as replay_receipt
|
|
|
|
DEFAULT_SECRETS_FILE = "/home/teleo/.hermes/profiles/leoclean/secrets/kb-apply.env"
|
|
DEFAULT_CONTAINER = "teleo-pg"
|
|
DEFAULT_DB = "teleo"
|
|
DEFAULT_HOST = "127.0.0.1"
|
|
DEFAULT_ROLE = "kb_apply"
|
|
V3_CUTOVER_FUNCTION = "kb_stage.teleo_v3_enforce_apply_contract_cutover()"
|
|
V3_CUTOVER_FUNCTION_NAME = "teleo_v3_enforce_apply_contract_cutover"
|
|
V3_CUTOVER_TRIGGER = "teleo_v3_00_enforce_apply_contract_cutover"
|
|
V3_GATE_OWNER = "kb_gate_owner"
|
|
V3_PROTECTED_ROLES = ("kb_gate_owner", "kb_review", "kb_apply")
|
|
V3_TRIGGER_PREFIX = "teleo_v3_"
|
|
V3_CATALOG_DDL_ADVISORY_LOCK_KEY = 6072343533146485505
|
|
V3_CATALOG_LOCK_FUNCTION = "teleo_v3_acquire_protected_relation_locks"
|
|
V3_CATALOG_LOCK_FUNCTION_SOURCE_SHA256 = "897345944ba0f1b2a5c01b098fb17de6f881a122e906277086fa7889e76219e8"
|
|
V3_TRIGGER_FUNCTION_SOURCE_SHA256 = {
|
|
"teleo_v3_enforce_apply_contract_cutover": "3ab2d5e1d5d1d18b13579fe2c0cae487036c7b147cf7f1b6b9a3633274756c18",
|
|
"teleo_v3_guard_immutable_fields": "d243741e552fe8c232440ffa5f853cf261f4b86b6f32d76f375734a2235c2d13",
|
|
"teleo_v3_guard_linked_proposal_decision": "602ee8f81f4515b0237f09a503ecbfe4fce2dc4a799e13f833554103b0e83e32",
|
|
"teleo_v3_reject_delete": "46190723ab4520954858dbd4bb6c9230e68e41c68b5aee3b8c2719e1416de43c",
|
|
"teleo_v3_require_accepted_proposal": "ca398ef2f185488c0712bd5ebc22402ab9e41e5dfd2f5823b367e6a382e8a64f",
|
|
"teleo_v3_require_linked_insert": "d8c190bd2fc90159b9a4824015c10a73a701a263bba90b67f8ccce75e454e6f8",
|
|
"teleo_v3_validate_claim_edge": "6a7a3e9d1d5ac6e9490abc9b7b1f321bb2e0948aea2671415aad28101f4ee936",
|
|
"teleo_v3_validate_claim_evidence": "4c697c497699760214aca99f255c215e393319fd4482d4b36efac839280c3733",
|
|
"teleo_v3_validate_evidence_assessment": "860f939b4878abe8e2e604e03be9eeb6c23b4878af4b19f9ec42de4b821fac61",
|
|
}
|
|
|
|
# Handle of the service-agent row that performs canonical writes. Bootstrapped
|
|
# once by superuser (scripts/kb_apply_prereqs.sql); kb_apply holds SELECT-only on
|
|
# public.agents so it can resolve this into applied_by_agent_id, never INSERT.
|
|
SERVICE_AGENT_HANDLE = "kb-apply"
|
|
|
|
APPLYABLE_TYPES = ("revise_strategy", "add_edge", "attach_evidence", "approve_claim")
|
|
|
|
CLAIM_TYPES = {"empirical", "structural", "normative", "meta", "concept"}
|
|
SOURCE_TYPES = {"paper", "article", "transcript", "observation", "dataset", "post", "dm", "other"}
|
|
EVIDENCE_ROLES = {"grounds", "illustrates", "contradicts"}
|
|
EDGE_TYPES = {
|
|
"supports",
|
|
"challenges",
|
|
"requires",
|
|
"relates",
|
|
"contradicts",
|
|
"supersedes",
|
|
"derives_from",
|
|
"cites",
|
|
"causes",
|
|
"constrains",
|
|
"accelerates",
|
|
}
|
|
|
|
MAX_BUNDLE_ROWS = {
|
|
"claims": 100,
|
|
"sources": 500,
|
|
"evidence": 1000,
|
|
"edges": 1000,
|
|
"assessments": 100,
|
|
"reasoning_tools": 50,
|
|
}
|
|
|
|
|
|
def _trigger_argument_hex(arguments: tuple[str, ...]) -> str:
|
|
return b"".join(argument.encode("utf-8") + b"\0" for argument in arguments).hex()
|
|
|
|
|
|
def _v3_trigger_contract(
|
|
schema_name: str,
|
|
table_name: str,
|
|
trigger_name: str,
|
|
trigger_type: int,
|
|
function_name: str,
|
|
*,
|
|
update_columns: tuple[str, ...] = (),
|
|
arguments: tuple[str, ...] = (),
|
|
security_definer: bool = False,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"schema_name": schema_name,
|
|
"table_name": table_name,
|
|
"trigger_name": trigger_name,
|
|
"trigger_type": trigger_type,
|
|
"update_columns": sorted(update_columns),
|
|
"when_expression": None,
|
|
"argument_count": len(arguments),
|
|
"argument_hex": _trigger_argument_hex(arguments),
|
|
"function_schema": "kb_stage",
|
|
"function_name": function_name,
|
|
"function_identity_arguments": "",
|
|
"function_owner": V3_GATE_OWNER,
|
|
"function_security_definer": security_definer,
|
|
"function_config": ["search_path=pg_catalog, pg_temp"],
|
|
"function_source_sha256": V3_TRIGGER_FUNCTION_SOURCE_SHA256[function_name],
|
|
"function_owner_only_execute": True if security_definer else None,
|
|
}
|
|
|
|
|
|
# PostgreSQL pg_trigger.tgtype is a bitmask: ROW=1, BEFORE=2, INSERT=4,
|
|
# DELETE=8, UPDATE=16. Every V3 trigger is an ordinary row-level BEFORE trigger.
|
|
V3_EXPECTED_TRIGGER_CONTRACT = (
|
|
_v3_trigger_contract(
|
|
"kb_stage",
|
|
"kb_proposals",
|
|
V3_CUTOVER_TRIGGER,
|
|
23,
|
|
V3_CUTOVER_FUNCTION_NAME,
|
|
update_columns=("proposal_type", "payload", "status"),
|
|
security_definer=True,
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claims",
|
|
"teleo_v3_00_claim_require_linked_insert",
|
|
7,
|
|
"teleo_v3_require_linked_insert",
|
|
arguments=("accepted_by_proposal_id", "admission_state"),
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claims",
|
|
"teleo_v3_claim_require_accepted_proposal",
|
|
23,
|
|
"teleo_v3_require_accepted_proposal",
|
|
update_columns=("accepted_by_proposal_id",),
|
|
arguments=("accepted_by_proposal_id",),
|
|
security_definer=True,
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"sources",
|
|
"teleo_v3_00_source_require_linked_insert",
|
|
7,
|
|
"teleo_v3_require_linked_insert",
|
|
arguments=("accepted_by_proposal_id",),
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claims",
|
|
"teleo_v3_claim_immutable",
|
|
19,
|
|
"teleo_v3_guard_immutable_fields",
|
|
arguments=(
|
|
"id",
|
|
"owner_agent_id",
|
|
"type",
|
|
"text",
|
|
"proposition",
|
|
"body_markdown",
|
|
"scope",
|
|
"access_scope",
|
|
"status",
|
|
"admission_state",
|
|
"revision_criteria",
|
|
"revision_kind",
|
|
"supersedes_id",
|
|
"superseded_by",
|
|
"confidence",
|
|
"tags",
|
|
"created_by",
|
|
"created_at",
|
|
"updated_at",
|
|
"accepted_by_proposal_id",
|
|
),
|
|
),
|
|
_v3_trigger_contract("public", "claims", "teleo_v3_claim_reject_delete", 11, "teleo_v3_reject_delete"),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_evidence",
|
|
"teleo_v3_00_evidence_require_linked_insert",
|
|
7,
|
|
"teleo_v3_require_linked_insert",
|
|
arguments=("accepted_from_proposal_id",),
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"sources",
|
|
"teleo_v3_source_require_accepted_proposal",
|
|
23,
|
|
"teleo_v3_require_accepted_proposal",
|
|
update_columns=("accepted_by_proposal_id",),
|
|
arguments=("accepted_by_proposal_id",),
|
|
security_definer=True,
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_edges",
|
|
"teleo_v3_00_edge_require_linked_insert",
|
|
7,
|
|
"teleo_v3_require_linked_insert",
|
|
arguments=("accepted_by_proposal_id",),
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"sources",
|
|
"teleo_v3_source_immutable",
|
|
19,
|
|
"teleo_v3_guard_immutable_fields",
|
|
arguments=(
|
|
"id",
|
|
"source_type",
|
|
"url",
|
|
"storage_path",
|
|
"excerpt",
|
|
"hash",
|
|
"canonical_url",
|
|
"storage_uri",
|
|
"content_hash",
|
|
"access_scope",
|
|
"ingestion_origin",
|
|
"provenance_status",
|
|
"source_class",
|
|
"auto_promotion_policy",
|
|
"created_by",
|
|
"captured_at",
|
|
"created_at",
|
|
"accepted_by_proposal_id",
|
|
),
|
|
),
|
|
_v3_trigger_contract("public", "sources", "teleo_v3_source_reject_delete", 11, "teleo_v3_reject_delete"),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_evidence",
|
|
"teleo_v3_evidence_require_accepted_proposal",
|
|
23,
|
|
"teleo_v3_require_accepted_proposal",
|
|
update_columns=("accepted_from_proposal_id",),
|
|
arguments=("accepted_from_proposal_id",),
|
|
security_definer=True,
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_evidence",
|
|
"teleo_v3_evidence_validate_provenance",
|
|
23,
|
|
"teleo_v3_validate_claim_evidence",
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_evidence",
|
|
"teleo_v3_evidence_immutable",
|
|
19,
|
|
"teleo_v3_guard_immutable_fields",
|
|
arguments=(
|
|
"id",
|
|
"claim_id",
|
|
"source_id",
|
|
"role",
|
|
"weight",
|
|
"excerpt",
|
|
"locator_json",
|
|
"source_content_hash",
|
|
"rationale",
|
|
"polarity",
|
|
"created_by",
|
|
"created_at",
|
|
"accepted_from_proposal_id",
|
|
),
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_evidence",
|
|
"teleo_v3_evidence_reject_delete",
|
|
11,
|
|
"teleo_v3_reject_delete",
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_edges",
|
|
"teleo_v3_edge_require_accepted_proposal",
|
|
23,
|
|
"teleo_v3_require_accepted_proposal",
|
|
update_columns=("accepted_by_proposal_id",),
|
|
arguments=("accepted_by_proposal_id",),
|
|
security_definer=True,
|
|
),
|
|
_v3_trigger_contract(
|
|
"public", "claim_edges", "teleo_v3_edge_validate_endpoints", 23, "teleo_v3_validate_claim_edge"
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_edges",
|
|
"teleo_v3_edge_immutable",
|
|
19,
|
|
"teleo_v3_guard_immutable_fields",
|
|
arguments=(
|
|
"id",
|
|
"from_claim",
|
|
"to_claim",
|
|
"edge_type",
|
|
"weight",
|
|
"relation_type",
|
|
"rationale",
|
|
"scope_or_conditions",
|
|
"created_by",
|
|
"created_at",
|
|
"accepted_by_proposal_id",
|
|
),
|
|
),
|
|
_v3_trigger_contract("public", "claim_edges", "teleo_v3_edge_reject_delete", 11, "teleo_v3_reject_delete"),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_evidence_assessments",
|
|
"teleo_v3_assessment_require_accepted_proposal",
|
|
23,
|
|
"teleo_v3_require_accepted_proposal",
|
|
update_columns=("accepted_by_proposal_id",),
|
|
arguments=("accepted_by_proposal_id",),
|
|
security_definer=True,
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_evidence_assessments",
|
|
"teleo_v3_assessment_validate",
|
|
23,
|
|
"teleo_v3_validate_evidence_assessment",
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_evidence_assessments",
|
|
"teleo_v3_assessment_immutable",
|
|
19,
|
|
"teleo_v3_guard_immutable_fields",
|
|
arguments=(
|
|
"id",
|
|
"claim_id",
|
|
"support_tier",
|
|
"challenge_tier",
|
|
"overall_state",
|
|
"rationale",
|
|
"evidence_set_hash",
|
|
"accepted_by_proposal_id",
|
|
"supersedes_assessment_id",
|
|
"created_at",
|
|
),
|
|
),
|
|
_v3_trigger_contract(
|
|
"public",
|
|
"claim_evidence_assessments",
|
|
"teleo_v3_assessment_reject_delete",
|
|
11,
|
|
"teleo_v3_reject_delete",
|
|
),
|
|
_v3_trigger_contract(
|
|
"kb_stage",
|
|
"kb_proposals",
|
|
"teleo_v3_guard_linked_proposal_decision",
|
|
19,
|
|
"teleo_v3_guard_linked_proposal_decision",
|
|
update_columns=(
|
|
"proposal_type",
|
|
"status",
|
|
"rationale",
|
|
"payload",
|
|
"reviewed_by_handle",
|
|
"reviewed_by_agent_id",
|
|
"reviewed_at",
|
|
"review_note",
|
|
"applied_by_handle",
|
|
"applied_by_agent_id",
|
|
"applied_at",
|
|
),
|
|
),
|
|
)
|
|
|
|
V3_PROTECTED_RELATIONS = (
|
|
("kb_stage", "kb_proposals"),
|
|
("public", "claim_edges"),
|
|
("public", "claim_evidence"),
|
|
("public", "claim_evidence_assessments"),
|
|
("public", "claims"),
|
|
("public", "sources"),
|
|
)
|
|
|
|
|
|
def v3_expected_trigger_contract_json() -> str:
|
|
return json.dumps(V3_EXPECTED_TRIGGER_CONTRACT, separators=(",", ":"), sort_keys=True)
|
|
|
|
|
|
def build_v3_contract_state_query(
|
|
*,
|
|
into: str | None = None,
|
|
expected_contract_expression: str | None = None,
|
|
) -> str:
|
|
"""Return the exact catalog query shared by worker and transaction guards."""
|
|
protected_roles = ", ".join(sql_literal(role) for role in V3_PROTECTED_ROLES)
|
|
expected_contract = expected_contract_expression or sql_literal(v3_expected_trigger_contract_json())
|
|
into_clause = f"\n into {into}" if into is not None else ""
|
|
return f"""with expected_triggers as (
|
|
select expected.*
|
|
from pg_catalog.jsonb_to_recordset({expected_contract}::jsonb) as expected(
|
|
schema_name text,
|
|
table_name text,
|
|
trigger_name text,
|
|
trigger_type integer,
|
|
update_columns jsonb,
|
|
when_expression text,
|
|
argument_count integer,
|
|
argument_hex text,
|
|
function_schema text,
|
|
function_name text,
|
|
function_identity_arguments text,
|
|
function_owner text,
|
|
function_security_definer boolean,
|
|
function_config jsonb,
|
|
function_source_sha256 text,
|
|
function_owner_only_execute boolean
|
|
)
|
|
), expected_functions as (
|
|
select distinct function_schema, function_name, function_identity_arguments,
|
|
function_owner, function_security_definer, function_config,
|
|
function_source_sha256, function_owner_only_execute
|
|
from expected_triggers
|
|
), actual_v3_functions as (
|
|
select procedure.oid, namespace.nspname as function_schema,
|
|
procedure.proname as function_name,
|
|
pg_catalog.pg_get_function_identity_arguments(procedure.oid) as function_identity_arguments,
|
|
owner_role.rolname as function_owner,
|
|
procedure.prosecdef as function_security_definer,
|
|
pg_catalog.to_jsonb(procedure.proconfig) as function_config,
|
|
pg_catalog.encode(
|
|
pg_catalog.sha256(pg_catalog.convert_to(procedure.prosrc, 'UTF8')),
|
|
'hex'
|
|
) as function_source_sha256,
|
|
not exists (
|
|
select 1
|
|
from pg_catalog.aclexplode(
|
|
coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
|
|
) acl
|
|
where acl.privilege_type = 'EXECUTE'
|
|
and acl.grantee <> procedure.proowner
|
|
) as function_owner_only_execute,
|
|
language.lanname as function_language,
|
|
procedure.prokind as function_kind,
|
|
procedure.pronargs as function_argument_count,
|
|
procedure.prorettype
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
|
|
join pg_catalog.pg_language language on language.oid = procedure.prolang
|
|
where namespace.nspname = 'kb_stage'
|
|
and pg_catalog.left(procedure.proname, {len(V3_TRIGGER_PREFIX)}) = {sql_literal(V3_TRIGGER_PREFIX)}
|
|
and procedure.proname <> {sql_literal(V3_CATALOG_LOCK_FUNCTION)}
|
|
), actual_v3_catalog_lock_functions as (
|
|
select procedure.oid,
|
|
owner_role.rolname as function_owner,
|
|
procedure.prosecdef as function_security_definer,
|
|
pg_catalog.to_jsonb(procedure.proconfig) as function_config,
|
|
pg_catalog.encode(
|
|
pg_catalog.sha256(pg_catalog.convert_to(procedure.prosrc, 'UTF8')),
|
|
'hex'
|
|
) as function_source_sha256,
|
|
language.lanname as function_language,
|
|
procedure.prokind as function_kind,
|
|
procedure.pronargs as function_argument_count,
|
|
pg_catalog.pg_get_function_identity_arguments(procedure.oid) as function_identity_arguments,
|
|
procedure.prorettype,
|
|
exists (
|
|
select 1
|
|
from pg_catalog.aclexplode(
|
|
coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
|
|
) acl
|
|
join pg_catalog.pg_roles grantee on grantee.oid = acl.grantee
|
|
where acl.privilege_type = 'EXECUTE'
|
|
and grantee.rolname = 'kb_apply'
|
|
) as apply_execute,
|
|
not exists (
|
|
select 1
|
|
from pg_catalog.aclexplode(
|
|
coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
|
|
) acl
|
|
where acl.privilege_type = 'EXECUTE'
|
|
and acl.grantee <> procedure.proowner
|
|
and acl.grantee <> (
|
|
select role.oid from pg_catalog.pg_roles role where role.rolname = 'kb_apply'
|
|
)
|
|
) as execute_acl_exact
|
|
from pg_catalog.pg_proc procedure
|
|
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
|
join pg_catalog.pg_roles owner_role on owner_role.oid = procedure.proowner
|
|
join pg_catalog.pg_language language on language.oid = procedure.prolang
|
|
where namespace.nspname = 'kb_stage'
|
|
and procedure.proname = {sql_literal(V3_CATALOG_LOCK_FUNCTION)}
|
|
), actual_v3_triggers as (
|
|
select trigger_row.*, table_namespace.nspname as schema_name,
|
|
relation.relname as table_name,
|
|
function_namespace.nspname as function_schema,
|
|
procedure.proname as function_name,
|
|
pg_catalog.pg_get_function_identity_arguments(procedure.oid) as function_identity_arguments,
|
|
coalesce((
|
|
select pg_catalog.jsonb_agg(attribute.attname order by attribute.attname)
|
|
from pg_catalog.unnest(trigger_row.tgattr::smallint[]) update_column(attnum)
|
|
join pg_catalog.pg_attribute attribute
|
|
on attribute.attrelid = trigger_row.tgrelid
|
|
and attribute.attnum = update_column.attnum
|
|
and not attribute.attisdropped
|
|
), '[]'::jsonb) as update_columns,
|
|
pg_catalog.pg_get_expr(trigger_row.tgqual, trigger_row.tgrelid, true) as when_expression,
|
|
pg_catalog.encode(trigger_row.tgargs, 'hex') as argument_hex
|
|
from pg_catalog.pg_trigger trigger_row
|
|
join pg_catalog.pg_class relation on relation.oid = trigger_row.tgrelid
|
|
join pg_catalog.pg_namespace table_namespace on table_namespace.oid = relation.relnamespace
|
|
join pg_catalog.pg_proc procedure on procedure.oid = trigger_row.tgfoid
|
|
join pg_catalog.pg_namespace function_namespace on function_namespace.oid = procedure.pronamespace
|
|
where pg_catalog.left(trigger_row.tgname, {len(V3_TRIGGER_PREFIX)}) = {sql_literal(V3_TRIGGER_PREFIX)}
|
|
), function_contract as (
|
|
select
|
|
(select pg_catalog.count(*) from actual_v3_functions) =
|
|
(select pg_catalog.count(*) from expected_functions)
|
|
and not exists (
|
|
select 1
|
|
from expected_functions expected
|
|
where not exists (
|
|
select 1
|
|
from actual_v3_functions actual
|
|
where actual.function_schema = expected.function_schema
|
|
and actual.function_name = expected.function_name
|
|
and actual.function_identity_arguments = expected.function_identity_arguments
|
|
and actual.function_owner = expected.function_owner
|
|
and actual.function_security_definer = expected.function_security_definer
|
|
and actual.function_config = expected.function_config
|
|
and actual.function_source_sha256 = expected.function_source_sha256
|
|
and (
|
|
expected.function_owner_only_execute is null
|
|
or actual.function_owner_only_execute = expected.function_owner_only_execute
|
|
)
|
|
and actual.function_language = 'plpgsql'
|
|
and actual.function_kind = 'f'
|
|
and actual.function_argument_count = 0
|
|
and actual.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype
|
|
)
|
|
) as valid
|
|
), catalog_lock_function_contract as (
|
|
select pg_catalog.count(*) = 1
|
|
and pg_catalog.bool_and(
|
|
function_owner = {sql_literal(V3_GATE_OWNER)}
|
|
and function_security_definer
|
|
and function_config = '["search_path=pg_catalog, pg_temp"]'::jsonb
|
|
and function_source_sha256 = {sql_literal(V3_CATALOG_LOCK_FUNCTION_SOURCE_SHA256)}
|
|
and function_language = 'plpgsql'
|
|
and function_kind = 'f'
|
|
and function_argument_count = 0
|
|
and function_identity_arguments = ''
|
|
and prorettype = 'pg_catalog.void'::pg_catalog.regtype
|
|
and apply_execute
|
|
and execute_acl_exact
|
|
) as valid
|
|
from actual_v3_catalog_lock_functions
|
|
), trigger_contract as (
|
|
select
|
|
(select pg_catalog.count(*) from actual_v3_triggers) =
|
|
(select pg_catalog.count(*) from expected_triggers)
|
|
and not exists (
|
|
select 1
|
|
from expected_triggers expected
|
|
where not exists (
|
|
select 1
|
|
from actual_v3_triggers actual
|
|
where actual.schema_name = expected.schema_name
|
|
and actual.table_name = expected.table_name
|
|
and actual.tgname = expected.trigger_name
|
|
and not actual.tgisinternal
|
|
and actual.tgenabled = 'O'
|
|
and actual.tgtype = expected.trigger_type
|
|
and actual.update_columns = expected.update_columns
|
|
and actual.when_expression is not distinct from expected.when_expression
|
|
and actual.tgnargs = expected.argument_count
|
|
and actual.argument_hex = expected.argument_hex
|
|
and actual.tgconstraint = 0::pg_catalog.oid
|
|
and not actual.tgdeferrable
|
|
and not actual.tginitdeferred
|
|
and actual.tgconstrrelid = 0::pg_catalog.oid
|
|
and actual.tgparentid = 0::pg_catalog.oid
|
|
and actual.tgoldtable is null
|
|
and actual.tgnewtable is null
|
|
and actual.function_schema = expected.function_schema
|
|
and actual.function_name = expected.function_name
|
|
and actual.function_identity_arguments = expected.function_identity_arguments
|
|
and exists (
|
|
select 1
|
|
from actual_v3_functions function_row
|
|
where function_row.oid = actual.tgfoid
|
|
and function_row.function_schema = expected.function_schema
|
|
and function_row.function_name = expected.function_name
|
|
and function_row.function_identity_arguments = expected.function_identity_arguments
|
|
)
|
|
)
|
|
) as valid
|
|
), catalog_state as (
|
|
select
|
|
exists (select 1 from actual_v3_functions)
|
|
or exists (select 1 from actual_v3_catalog_lock_functions)
|
|
or exists (select 1 from actual_v3_triggers) as artifacts_present,
|
|
(select valid from function_contract) as functions_valid,
|
|
(select valid from catalog_lock_function_contract) as catalog_lock_function_valid,
|
|
(select valid from trigger_contract) as triggers_valid,
|
|
exists (
|
|
select 1
|
|
from pg_catalog.pg_roles owner_role
|
|
where owner_role.rolname = {sql_literal(V3_GATE_OWNER)}
|
|
and not owner_role.rolcanlogin
|
|
and not owner_role.rolinherit
|
|
and not owner_role.rolsuper
|
|
and not owner_role.rolcreatedb
|
|
and not owner_role.rolcreaterole
|
|
and not owner_role.rolreplication
|
|
and not owner_role.rolbypassrls
|
|
) as owner_role_valid,
|
|
not exists (
|
|
select 1
|
|
from pg_catalog.pg_auth_members membership
|
|
join pg_catalog.pg_roles member_role on member_role.oid = membership.member
|
|
join pg_catalog.pg_roles granted_role on granted_role.oid = membership.roleid
|
|
where member_role.rolname in ({protected_roles})
|
|
or granted_role.rolname in ({protected_roles})
|
|
) as memberships_valid
|
|
)
|
|
select case
|
|
when not artifacts_present then 'V2'
|
|
when functions_valid and catalog_lock_function_valid and triggers_valid
|
|
and owner_role_valid and memberships_valid then 'V3'
|
|
else 'INVALID'
|
|
end{into_clause}
|
|
from catalog_state"""
|
|
|
|
|
|
def _v3_contract_lock_sql() -> str:
|
|
statements: list[str] = []
|
|
for schema_name, table_name in V3_PROTECTED_RELATIONS:
|
|
if (schema_name, table_name) == ("kb_stage", "kb_proposals"):
|
|
statements.append(f"select kb_stage.{V3_CATALOG_LOCK_FUNCTION}();")
|
|
else:
|
|
statements.append(f"lock table {schema_name}.{table_name} in row exclusive mode;")
|
|
return "\n".join(statements)
|
|
|
|
|
|
def _v3_contract_transaction_guard_sql() -> str:
|
|
state_query = build_v3_contract_state_query(into="v_contract_state")
|
|
return f"""{_v3_contract_lock_sql()}
|
|
do $v3_contract_guard$
|
|
declare
|
|
v_contract_state text;
|
|
begin
|
|
{state_query};
|
|
if v_contract_state <> 'V3' then
|
|
raise exception using
|
|
errcode = '55000',
|
|
message = pg_catalog.format(
|
|
'apply_proposal: V3 catalog contract is %s; refusing mutating transaction',
|
|
v_contract_state
|
|
);
|
|
end if;
|
|
end
|
|
$v3_contract_guard$;"""
|
|
|
|
|
|
V3_CLAIM_TYPES = {"empirical", "predictive", "causal", "normative", "conceptual"}
|
|
V3_ACCESS_SCOPES = {"agent_private", "collective_internal", "public"}
|
|
V3_SOURCE_ORIGINS = {"operator_upload", "agent_fetch", "contributor", "system_import", "unknown"}
|
|
V3_PROVENANCE_STATES = {"verified", "unverified", "disputed"}
|
|
V3_SOURCE_CLASSES = {
|
|
"primary_record",
|
|
"peer_reviewed",
|
|
"institutional",
|
|
"journalistic",
|
|
"self_published",
|
|
"unknown",
|
|
}
|
|
V3_AUTO_PROMOTION_POLICIES = {"eligible", "human_required", "blocked"}
|
|
V3_EVIDENCE_POLARITIES = {"supports", "challenges", "illustrates"}
|
|
V3_RELATION_TYPES = {"supports", "challenges", "depends_on", "contradicts"}
|
|
V3_SUPPORT_TIERS = {"none", "weak", "moderate", "strong"}
|
|
V3_ASSESSMENT_STATES = {"insufficient", "support_dominant", "challenge_dominant", "mixed"}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# SQL helpers #
|
|
# --------------------------------------------------------------------------- #
|
|
def sql_literal(value: Any) -> str:
|
|
"""Render a Python value as an explicit PostgreSQL escape-string literal."""
|
|
if value is None:
|
|
return "null"
|
|
if isinstance(value, bool):
|
|
return "true" if value else "false"
|
|
if isinstance(value, (int, float)):
|
|
return repr(value)
|
|
escaped = str(value).replace("\\", "\\\\").replace("'", "''")
|
|
return "E'" + escaped + "'"
|
|
|
|
|
|
def _jsonb(value: Any) -> str:
|
|
return sql_literal(json.dumps(value, sort_keys=True)) + "::jsonb"
|
|
|
|
|
|
def deterministic_row_uuid(proposal_id: str, row_kind: str, *semantic_key: Any) -> str:
|
|
"""Derive a stable persisted row id from the proposal and semantic key.
|
|
|
|
Evidence and edge ids are not part of the reviewed v2 payload. Deriving
|
|
them here makes clone/production receipts and a pre-authorized rollback
|
|
packet bind the same exact ids instead of relying on database randomness.
|
|
"""
|
|
material = json.dumps(
|
|
["livingip", "kb-apply", str(proposal_id), row_kind, *semantic_key],
|
|
ensure_ascii=True,
|
|
separators=(",", ":"),
|
|
)
|
|
return str(uuid.uuid5(uuid.NAMESPACE_URL, material))
|
|
|
|
|
|
def _text_array(values: list[str]) -> str:
|
|
if not values:
|
|
return "'{}'::text[]"
|
|
return "array[" + ", ".join(sql_literal(value) for value in values) + "]::text[]"
|
|
|
|
|
|
def _uuid(value: Any, path: str, *, nullable: bool = False) -> str | None:
|
|
if value is None and nullable:
|
|
return None
|
|
try:
|
|
return str(uuid.UUID(str(value)))
|
|
except (TypeError, ValueError, AttributeError) as exc:
|
|
raise ValueError(f"{path} must be a UUID{' or null' if nullable else ''}") from exc
|
|
|
|
|
|
def _v3_uuid(value: Any, path: str, *, nullable: bool = False) -> str | None:
|
|
"""Require the reviewed V3 spelling to equal PostgreSQL's UUID text form."""
|
|
canonical = _uuid(value, path, nullable=nullable)
|
|
if canonical is None:
|
|
return None
|
|
if not isinstance(value, str) or value != canonical:
|
|
raise ValueError(f"{path} must be a canonical lowercase hyphenated UUID")
|
|
return canonical
|
|
|
|
|
|
def _weight(value: Any, path: str) -> Any:
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
raise ValueError(f"{path} must be a number from 0 to 1 or null")
|
|
if not 0 <= value <= 1:
|
|
raise ValueError(f"{path} must be between 0 and 1")
|
|
return value
|
|
|
|
|
|
def _required_text(value: Any, path: str) -> str:
|
|
if not isinstance(value, str) or not value.strip():
|
|
raise ValueError(f"{path} must be a non-empty string")
|
|
return value
|
|
|
|
|
|
def _optional_text(value: Any, path: str) -> str | None:
|
|
if value is None:
|
|
return None
|
|
if not isinstance(value, str):
|
|
raise ValueError(f"{path} must be a string or null")
|
|
return value
|
|
|
|
|
|
def _sha256(value: Any, path: str) -> str:
|
|
text = _required_text(value, path)
|
|
if len(text) != 64 or any(character not in "0123456789abcdef" for character in text):
|
|
raise ValueError(f"{path} must be a lowercase SHA-256 hex digest")
|
|
return text
|
|
|
|
|
|
def _utc_timestamp(value: Any, path: str) -> str:
|
|
try:
|
|
return replay_receipt.canonical_utc_timestamp(value, path=path)
|
|
except ValueError as exc:
|
|
raise ValueError(f"{path} must be a timezone-aware ISO-8601 timestamp") from exc
|
|
|
|
|
|
def _json_object(value: Any, path: str, *, nonempty: bool = False) -> dict[str, Any]:
|
|
if not isinstance(value, dict) or (nonempty and not value):
|
|
qualifier = "non-empty " if nonempty else ""
|
|
raise ValueError(f"{path} must be a {qualifier}object")
|
|
return value
|
|
|
|
|
|
def _rows(payload: dict[str, Any], key: str, *, required: bool = False) -> list[dict[str, Any]]:
|
|
value = payload.get(key)
|
|
if value is None:
|
|
value = []
|
|
if not isinstance(value, list) or any(not isinstance(row, dict) for row in value):
|
|
raise ValueError(f"approve_claim apply_payload.{key} must be a list of objects")
|
|
if required and not value:
|
|
raise ValueError(f"approve_claim apply_payload requires non-empty '{key}'")
|
|
if len(value) > MAX_BUNDLE_ROWS[key]:
|
|
raise ValueError(f"approve_claim apply_payload.{key} exceeds {MAX_BUNDLE_ROWS[key]} rows")
|
|
return value
|
|
|
|
|
|
def _dedupe(values: list[Any], path: str) -> None:
|
|
seen = set()
|
|
for value in values:
|
|
marker = json.dumps(value, sort_keys=True)
|
|
if marker in seen:
|
|
raise ValueError(f"{path} contains a duplicate: {value!r}")
|
|
seen.add(marker)
|
|
|
|
|
|
def _reject_unknown(mapping: dict[str, Any], allowed: set[str], path: str) -> None:
|
|
unknown = sorted(set(mapping) - allowed)
|
|
if unknown:
|
|
raise ValueError(f"{path} contains unknown fields: {unknown}")
|
|
|
|
|
|
def _v3_evidence_set_hash(claim_id: str, rows: list[dict[str, Any]]) -> str:
|
|
evidence = [
|
|
{
|
|
"source_id": row["source_id"],
|
|
"source_content_hash": row["source_content_hash"],
|
|
"polarity": row["polarity"],
|
|
"locator": row["locator_json"],
|
|
}
|
|
for row in rows
|
|
if row["claim_id"] == claim_id
|
|
]
|
|
evidence.sort(key=lambda row: json.dumps(row, separators=(",", ":"), sort_keys=True))
|
|
payload = json.dumps(
|
|
{"claim_id": claim_id, "evidence": evidence},
|
|
separators=(",", ":"),
|
|
sort_keys=True,
|
|
)
|
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _normalize_v3_approve_claim(apply_payload: dict[str, Any]) -> dict[str, Any]:
|
|
_reject_unknown(
|
|
apply_payload,
|
|
{
|
|
"contract_version",
|
|
"source_proposal_id",
|
|
"agent_id",
|
|
"claims",
|
|
"sources",
|
|
"evidence",
|
|
"edges",
|
|
"assessments",
|
|
"reasoning_tools",
|
|
"normalization_manifest",
|
|
},
|
|
"approve_claim apply_payload",
|
|
)
|
|
if apply_payload.get("contract_version") != 3:
|
|
raise ValueError("V3 approve_claim apply_payload.contract_version must equal 3")
|
|
for key in ("claims", "sources", "evidence", "edges", "assessments", "reasoning_tools"):
|
|
if key not in apply_payload:
|
|
raise ValueError(f"V3 approve_claim apply_payload requires explicit '{key}' collection")
|
|
if apply_payload.get("reasoning_tools") != []:
|
|
raise ValueError("V3 approve_claim does not admit reasoning_tools; use a separately reviewed capability")
|
|
if apply_payload.get("source_proposal_id") is not None:
|
|
_v3_uuid(apply_payload["source_proposal_id"], "approve_claim.source_proposal_id")
|
|
manifest = apply_payload.get("normalization_manifest")
|
|
if manifest is not None:
|
|
_json_object(manifest, "approve_claim.normalization_manifest")
|
|
|
|
agent_id = _v3_uuid(apply_payload.get("agent_id"), "approve_claim.agent_id", nullable=True)
|
|
raw_claims = _rows(apply_payload, "claims", required=True)
|
|
raw_sources = _rows(apply_payload, "sources", required=True)
|
|
raw_evidence = _rows(apply_payload, "evidence", required=True)
|
|
raw_edges = _rows(apply_payload, "edges")
|
|
raw_assessments = _rows(apply_payload, "assessments", required=True)
|
|
|
|
claims: list[dict[str, Any]] = []
|
|
for index, row in enumerate(raw_claims):
|
|
path = f"approve_claim.claims[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{
|
|
"id",
|
|
"owner_agent_id",
|
|
"type",
|
|
"proposition",
|
|
"body_markdown",
|
|
"scope",
|
|
"access_scope",
|
|
"status",
|
|
"revision_criteria",
|
|
"revision_kind",
|
|
"supersedes_id",
|
|
"tags",
|
|
"created_by",
|
|
},
|
|
path,
|
|
)
|
|
claim_type = row.get("type")
|
|
if claim_type not in V3_CLAIM_TYPES:
|
|
raise ValueError(f"{path}.type must be one of {sorted(V3_CLAIM_TYPES)}")
|
|
status = row.get("status", "active")
|
|
if status not in {"active", "contested"}:
|
|
raise ValueError(f"{path}.status must be active or contested for an original admission")
|
|
revision_kind = row.get("revision_kind", "original")
|
|
if revision_kind != "original" or row.get("supersedes_id") is not None:
|
|
raise ValueError("V3 minimum writer admits original claims only; revision transition is not yet proven")
|
|
access_scope = row.get("access_scope")
|
|
if access_scope not in V3_ACCESS_SCOPES:
|
|
raise ValueError(f"{path}.access_scope must be one of {sorted(V3_ACCESS_SCOPES)}")
|
|
owner_agent_id = _v3_uuid(row.get("owner_agent_id"), f"{path}.owner_agent_id", nullable=True)
|
|
if access_scope == "agent_private" and owner_agent_id is None:
|
|
raise ValueError(f"{path}.owner_agent_id is required for agent_private claims")
|
|
created_by = _v3_uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True)
|
|
if created_by is None:
|
|
raise ValueError(f"{path}.created_by or apply_payload.agent_id is required")
|
|
tags = row.get("tags") or []
|
|
if not isinstance(tags, list) or any(not isinstance(tag, str) or not tag.strip() for tag in tags):
|
|
raise ValueError(f"{path}.tags must be a list of non-empty strings")
|
|
claims.append(
|
|
{
|
|
"id": _v3_uuid(row.get("id"), f"{path}.id"),
|
|
"owner_agent_id": owner_agent_id,
|
|
"type": claim_type,
|
|
"proposition": _required_text(row.get("proposition"), f"{path}.proposition"),
|
|
"body_markdown": _required_text(row.get("body_markdown"), f"{path}.body_markdown"),
|
|
"scope": _required_text(row.get("scope"), f"{path}.scope"),
|
|
"access_scope": access_scope,
|
|
"status": status,
|
|
"revision_criteria": _required_text(row.get("revision_criteria"), f"{path}.revision_criteria"),
|
|
"revision_kind": revision_kind,
|
|
"supersedes_id": None,
|
|
"tags": tags,
|
|
"created_by": created_by,
|
|
}
|
|
)
|
|
|
|
sources: list[dict[str, Any]] = []
|
|
for index, row in enumerate(raw_sources):
|
|
path = f"approve_claim.sources[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{
|
|
"id",
|
|
"source_type",
|
|
"canonical_url",
|
|
"storage_uri",
|
|
"excerpt",
|
|
"content_hash",
|
|
"access_scope",
|
|
"ingestion_origin",
|
|
"provenance_status",
|
|
"source_class",
|
|
"auto_promotion_policy",
|
|
"captured_at",
|
|
"created_by",
|
|
},
|
|
path,
|
|
)
|
|
source_type = row.get("source_type")
|
|
if source_type not in SOURCE_TYPES:
|
|
raise ValueError(f"{path}.source_type must be one of {sorted(SOURCE_TYPES)}")
|
|
canonical_url = _optional_text(row.get("canonical_url"), f"{path}.canonical_url")
|
|
storage_uri = _optional_text(row.get("storage_uri"), f"{path}.storage_uri")
|
|
if not (canonical_url and canonical_url.strip()) and not (storage_uri and storage_uri.strip()):
|
|
raise ValueError(f"{path} requires canonical_url or storage_uri")
|
|
access_scope = row.get("access_scope")
|
|
if access_scope not in V3_ACCESS_SCOPES:
|
|
raise ValueError(f"{path}.access_scope must be one of {sorted(V3_ACCESS_SCOPES)}")
|
|
ingestion_origin = row.get("ingestion_origin")
|
|
if ingestion_origin not in V3_SOURCE_ORIGINS:
|
|
raise ValueError(f"{path}.ingestion_origin must be one of {sorted(V3_SOURCE_ORIGINS)}")
|
|
provenance_status = row.get("provenance_status")
|
|
if provenance_status not in V3_PROVENANCE_STATES:
|
|
raise ValueError(f"{path}.provenance_status must be one of {sorted(V3_PROVENANCE_STATES)}")
|
|
source_class = row.get("source_class")
|
|
if source_class not in V3_SOURCE_CLASSES:
|
|
raise ValueError(f"{path}.source_class must be one of {sorted(V3_SOURCE_CLASSES)}")
|
|
auto_promotion_policy = row.get("auto_promotion_policy")
|
|
if auto_promotion_policy not in V3_AUTO_PROMOTION_POLICIES:
|
|
raise ValueError(f"{path}.auto_promotion_policy must be one of {sorted(V3_AUTO_PROMOTION_POLICIES)}")
|
|
created_by = _v3_uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True)
|
|
if created_by is None:
|
|
raise ValueError(f"{path}.created_by or apply_payload.agent_id is required")
|
|
sources.append(
|
|
{
|
|
"id": _v3_uuid(row.get("id"), f"{path}.id"),
|
|
"source_type": source_type,
|
|
"canonical_url": canonical_url,
|
|
"storage_uri": storage_uri,
|
|
"excerpt": _optional_text(row.get("excerpt"), f"{path}.excerpt"),
|
|
"content_hash": _sha256(row.get("content_hash"), f"{path}.content_hash"),
|
|
"access_scope": access_scope,
|
|
"ingestion_origin": ingestion_origin,
|
|
"provenance_status": provenance_status,
|
|
"source_class": source_class,
|
|
"auto_promotion_policy": auto_promotion_policy,
|
|
"captured_at": _utc_timestamp(row.get("captured_at"), f"{path}.captured_at"),
|
|
"created_by": created_by,
|
|
}
|
|
)
|
|
|
|
claim_ids = {row["id"] for row in claims}
|
|
source_by_id = {row["id"]: row for row in sources}
|
|
evidence: list[dict[str, Any]] = []
|
|
polarity_to_role = {"supports": "grounds", "challenges": "contradicts", "illustrates": "illustrates"}
|
|
for index, row in enumerate(raw_evidence):
|
|
path = f"approve_claim.evidence[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{
|
|
"id",
|
|
"claim_id",
|
|
"source_id",
|
|
"polarity",
|
|
"excerpt",
|
|
"locator_json",
|
|
"source_content_hash",
|
|
"rationale",
|
|
"created_by",
|
|
},
|
|
path,
|
|
)
|
|
claim_id = _v3_uuid(row.get("claim_id"), f"{path}.claim_id")
|
|
source_id = _v3_uuid(row.get("source_id"), f"{path}.source_id")
|
|
if claim_id not in claim_ids or source_id not in source_by_id:
|
|
raise ValueError(f"{path} must link payload-owned claim and source rows")
|
|
polarity = row.get("polarity")
|
|
if polarity not in V3_EVIDENCE_POLARITIES:
|
|
raise ValueError(f"{path}.polarity must be one of {sorted(V3_EVIDENCE_POLARITIES)}")
|
|
source_content_hash = _sha256(row.get("source_content_hash"), f"{path}.source_content_hash")
|
|
if source_content_hash != source_by_id[source_id]["content_hash"]:
|
|
raise ValueError(f"{path}.source_content_hash does not match the payload source")
|
|
created_by = _v3_uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True)
|
|
if created_by is None:
|
|
raise ValueError(f"{path}.created_by or apply_payload.agent_id is required")
|
|
evidence.append(
|
|
{
|
|
"id": _v3_uuid(row.get("id"), f"{path}.id"),
|
|
"claim_id": claim_id,
|
|
"source_id": source_id,
|
|
"role": polarity_to_role[polarity],
|
|
"polarity": polarity,
|
|
"excerpt": _optional_text(row.get("excerpt"), f"{path}.excerpt"),
|
|
"locator_json": _json_object(row.get("locator_json"), f"{path}.locator_json", nonempty=True),
|
|
"source_content_hash": source_content_hash,
|
|
"rationale": _required_text(row.get("rationale"), f"{path}.rationale"),
|
|
"created_by": created_by,
|
|
}
|
|
)
|
|
|
|
edges: list[dict[str, Any]] = []
|
|
for index, row in enumerate(raw_edges):
|
|
path = f"approve_claim.edges[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{
|
|
"id",
|
|
"from_claim",
|
|
"to_claim",
|
|
"relation_type",
|
|
"rationale",
|
|
"scope_or_conditions",
|
|
"created_by",
|
|
},
|
|
path,
|
|
)
|
|
from_claim = _v3_uuid(row.get("from_claim"), f"{path}.from_claim")
|
|
to_claim = _v3_uuid(row.get("to_claim"), f"{path}.to_claim")
|
|
relation_type = row.get("relation_type")
|
|
if relation_type not in V3_RELATION_TYPES:
|
|
raise ValueError(f"{path}.relation_type must be one of {sorted(V3_RELATION_TYPES)}")
|
|
if from_claim == to_claim:
|
|
raise ValueError(f"{path} cannot be a self-edge")
|
|
if not ({from_claim, to_claim} & claim_ids):
|
|
raise ValueError(f"{path} must involve at least one payload-owned claim")
|
|
if relation_type == "contradicts" and from_claim > to_claim:
|
|
raise ValueError(f"{path} contradicts endpoints must use canonical UUID order")
|
|
created_by = _v3_uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True)
|
|
if created_by is None:
|
|
raise ValueError(f"{path}.created_by or apply_payload.agent_id is required")
|
|
edges.append(
|
|
{
|
|
"id": _v3_uuid(row.get("id"), f"{path}.id"),
|
|
"from_claim": from_claim,
|
|
"to_claim": to_claim,
|
|
"relation_type": relation_type,
|
|
"edge_type": "requires" if relation_type == "depends_on" else relation_type,
|
|
"rationale": _required_text(row.get("rationale"), f"{path}.rationale"),
|
|
"scope_or_conditions": _optional_text(row.get("scope_or_conditions"), f"{path}.scope_or_conditions"),
|
|
"created_by": created_by,
|
|
}
|
|
)
|
|
|
|
assessments: list[dict[str, Any]] = []
|
|
claim_type_by_id = {row["id"]: row["type"] for row in claims}
|
|
for index, row in enumerate(raw_assessments):
|
|
path = f"approve_claim.assessments[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{
|
|
"id",
|
|
"claim_id",
|
|
"support_tier",
|
|
"challenge_tier",
|
|
"overall_state",
|
|
"rationale",
|
|
"evidence_set_hash",
|
|
"supersedes_assessment_id",
|
|
},
|
|
path,
|
|
)
|
|
claim_id = _v3_uuid(row.get("claim_id"), f"{path}.claim_id")
|
|
if claim_id not in claim_ids:
|
|
raise ValueError(f"{path}.claim_id must identify a payload-owned claim")
|
|
support_tier = row.get("support_tier")
|
|
challenge_tier = row.get("challenge_tier")
|
|
if support_tier is not None and support_tier not in V3_SUPPORT_TIERS:
|
|
raise ValueError(f"{path}.support_tier must be null or one of {sorted(V3_SUPPORT_TIERS)}")
|
|
if challenge_tier is not None and challenge_tier not in V3_SUPPORT_TIERS:
|
|
raise ValueError(f"{path}.challenge_tier must be null or one of {sorted(V3_SUPPORT_TIERS)}")
|
|
if claim_type_by_id[claim_id] in {"empirical", "predictive", "causal"} and (
|
|
support_tier is None or challenge_tier is None
|
|
):
|
|
raise ValueError(f"{path} requires support_tier and challenge_tier for a truth-apt claim")
|
|
overall_state = row.get("overall_state")
|
|
if overall_state not in V3_ASSESSMENT_STATES:
|
|
raise ValueError(f"{path}.overall_state must be one of {sorted(V3_ASSESSMENT_STATES)}")
|
|
evidence_set_hash = _sha256(row.get("evidence_set_hash"), f"{path}.evidence_set_hash")
|
|
if evidence_set_hash != _v3_evidence_set_hash(claim_id, evidence):
|
|
raise ValueError(f"{path}.evidence_set_hash does not match the accepted evidence set")
|
|
supersedes_assessment_id = _v3_uuid(
|
|
row.get("supersedes_assessment_id"), f"{path}.supersedes_assessment_id", nullable=True
|
|
)
|
|
if supersedes_assessment_id is not None:
|
|
raise ValueError("V3 minimum writer admits first assessments only; assessment supersession is not proven")
|
|
assessments.append(
|
|
{
|
|
"id": _v3_uuid(row.get("id"), f"{path}.id"),
|
|
"claim_id": claim_id,
|
|
"support_tier": support_tier,
|
|
"challenge_tier": challenge_tier,
|
|
"overall_state": overall_state,
|
|
"rationale": _required_text(row.get("rationale"), f"{path}.rationale"),
|
|
"evidence_set_hash": evidence_set_hash,
|
|
"supersedes_assessment_id": None,
|
|
}
|
|
)
|
|
|
|
_dedupe([row["id"] for row in claims], "V3 approve_claim claims ids")
|
|
_dedupe([row["id"] for row in sources], "V3 approve_claim sources ids")
|
|
_dedupe([row["content_hash"] for row in sources], "V3 approve_claim source hashes")
|
|
_dedupe([row["id"] for row in evidence], "V3 approve_claim evidence ids")
|
|
_dedupe(
|
|
[[row["claim_id"], row["source_id"], row["polarity"]] for row in evidence],
|
|
"V3 approve_claim evidence keys",
|
|
)
|
|
_dedupe([row["id"] for row in edges], "V3 approve_claim edge ids")
|
|
_dedupe(
|
|
[[row["from_claim"], row["to_claim"], row["relation_type"]] for row in edges],
|
|
"V3 approve_claim edge keys",
|
|
)
|
|
_dedupe([row["id"] for row in assessments], "V3 approve_claim assessment ids")
|
|
_dedupe([row["claim_id"] for row in assessments], "V3 approve_claim assessment claim ids")
|
|
assessed_claims = {row["claim_id"] for row in assessments}
|
|
if assessed_claims != claim_ids:
|
|
raise ValueError("V3 approve_claim requires exactly one evidence assessment for every admitted claim")
|
|
|
|
return {
|
|
"agent_id": agent_id,
|
|
"claims": claims,
|
|
"sources": sources,
|
|
"evidence": evidence,
|
|
"edges": edges,
|
|
"assessments": assessments,
|
|
}
|
|
|
|
|
|
def _validate_approval_meta(approval: dict[str, Any], proposal_type: str, apply_payload: dict[str, Any]) -> None:
|
|
if not isinstance(approval, dict):
|
|
raise ValueError("approved proposal metadata is required")
|
|
if approval.get("proposal_type") != proposal_type:
|
|
raise ValueError("approved proposal_type does not match the apply builder")
|
|
payload = approval.get("payload")
|
|
if not isinstance(payload, dict) or payload.get("apply_payload") != apply_payload:
|
|
raise ValueError("approved proposal payload does not match the strict apply payload")
|
|
if not str(approval.get("reviewed_by_handle") or "").strip():
|
|
raise ValueError("approved proposal requires reviewed_by_handle")
|
|
if not str(approval.get("reviewed_at") or "").strip():
|
|
raise ValueError("approved proposal requires reviewed_at")
|
|
_utc_timestamp(approval["reviewed_at"], "approved proposal reviewed_at")
|
|
if not str(approval.get("review_note") or "").strip():
|
|
raise ValueError("approved proposal requires review_note")
|
|
_uuid(
|
|
approval.get("reviewed_by_agent_id"),
|
|
"approved proposal reviewed_by_agent_id",
|
|
nullable=True,
|
|
)
|
|
|
|
|
|
def _approval_guard_sql(proposal_id: str, approval: dict[str, Any]) -> str:
|
|
reviewed_at = _utc_timestamp(approval["reviewed_at"], "approved proposal reviewed_at")
|
|
return f"""select kb_stage.assert_approved_proposal(
|
|
{sql_literal(proposal_id)}::uuid,
|
|
{sql_literal(approval["proposal_type"])},
|
|
{_jsonb(approval["payload"])},
|
|
{sql_literal(approval["reviewed_by_handle"])},
|
|
{sql_literal(approval.get("reviewed_by_agent_id"))}::uuid,
|
|
{sql_literal(reviewed_at)}::timestamptz,
|
|
{sql_literal(approval["review_note"])}
|
|
);"""
|
|
|
|
|
|
def _ledger_and_verify(
|
|
proposal_id: str,
|
|
applied_by: str,
|
|
extra_checks: str,
|
|
approval: dict[str, Any],
|
|
) -> str:
|
|
"""Verify canonical rows, then finish the locked, payload-bound apply."""
|
|
reviewed_at = _utc_timestamp(approval["reviewed_at"], "approved proposal reviewed_at")
|
|
checks_sql = f"""do $verify$
|
|
begin
|
|
{extra_checks}
|
|
end
|
|
$verify$;"""
|
|
finish_sql = f"""select kb_stage.finish_approved_proposal(
|
|
{sql_literal(proposal_id)}::uuid,
|
|
{sql_literal(approval["proposal_type"])},
|
|
{_jsonb(approval["payload"])},
|
|
{sql_literal(approval["reviewed_by_handle"])},
|
|
{sql_literal(approval.get("reviewed_by_agent_id"))}::uuid,
|
|
{sql_literal(reviewed_at)}::timestamptz,
|
|
{sql_literal(approval["review_note"])},
|
|
{sql_literal(applied_by)}
|
|
);"""
|
|
return checks_sql + "\n" + finish_sql
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Per-type SQL builders (pure; unit-tested without a DB) #
|
|
# --------------------------------------------------------------------------- #
|
|
def build_revise_strategy_sql(
|
|
apply_payload: dict[str, Any],
|
|
proposal_id: str,
|
|
applied_by: str | None,
|
|
approval: dict[str, Any],
|
|
) -> str:
|
|
_validate_approval_meta(approval, "revise_strategy", apply_payload)
|
|
agent_id = apply_payload.get("agent_id")
|
|
strategy = apply_payload.get("strategy") or {}
|
|
nodes: list[dict[str, Any]] = apply_payload.get("strategy_nodes") or []
|
|
if not agent_id:
|
|
raise ValueError("revise_strategy apply_payload requires 'agent_id'")
|
|
for key in ("diagnosis", "guiding_policy", "proximate_objectives"):
|
|
if strategy.get(key) is None:
|
|
raise ValueError(f"revise_strategy apply_payload.strategy requires '{key}'")
|
|
if not nodes:
|
|
raise ValueError("revise_strategy apply_payload requires non-empty 'strategy_nodes'")
|
|
|
|
node_values = []
|
|
for n in nodes:
|
|
nt = n.get("node_type")
|
|
if nt not in ("diagnosis", "policy", "objective"):
|
|
raise ValueError(f"invalid strategy node_type: {nt!r}")
|
|
if not n.get("title") or n.get("body") is None:
|
|
raise ValueError("each strategy_node requires 'title' and 'body'")
|
|
node_values.append(
|
|
f"({sql_literal(agent_id)}::uuid, {sql_literal(nt)}, "
|
|
f"{sql_literal(n['title'])}, {sql_literal(n['body'])}, "
|
|
f"{int(n.get('rank', 1))})"
|
|
)
|
|
|
|
node_values_sql = ",\n ".join(node_values)
|
|
canonical = f"""
|
|
-- deactivate the current active strategy for this agent (one_active invariant)
|
|
update public.strategies
|
|
set active = false
|
|
where agent_id = {sql_literal(agent_id)}::uuid and active;
|
|
|
|
-- insert the new strategy as the next version, active
|
|
insert into public.strategies
|
|
(agent_id, diagnosis, guiding_policy, proximate_objectives, version, active)
|
|
select {sql_literal(agent_id)}::uuid,
|
|
{sql_literal(strategy["diagnosis"])},
|
|
{sql_literal(strategy["guiding_policy"])},
|
|
{_jsonb(strategy["proximate_objectives"])},
|
|
coalesce(max(version), 0) + 1,
|
|
true
|
|
from public.strategies
|
|
where agent_id = {sql_literal(agent_id)}::uuid;
|
|
|
|
-- retire the agent's current strategy nodes (shared NULL-agent nodes untouched)
|
|
update public.strategy_nodes
|
|
set status = 'retired', updated_at = now()
|
|
where agent_id = {sql_literal(agent_id)}::uuid and status <> 'retired';
|
|
|
|
-- insert the new node set
|
|
insert into public.strategy_nodes (agent_id, node_type, title, body, rank)
|
|
values
|
|
{node_values_sql};
|
|
""".rstrip()
|
|
|
|
checks = f""" if (select count(*) from public.strategies
|
|
where agent_id = {sql_literal(agent_id)}::uuid and active) <> 1 then
|
|
raise exception 'apply_proposal: expected exactly one active strategy for agent %', {sql_literal(agent_id)};
|
|
end if;"""
|
|
|
|
ledger = _ledger_and_verify(proposal_id, applied_by or SERVICE_AGENT_HANDLE, checks, approval)
|
|
return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval))
|
|
|
|
|
|
def build_add_edge_sql(
|
|
apply_payload: dict[str, Any],
|
|
proposal_id: str,
|
|
applied_by: str | None,
|
|
approval: dict[str, Any],
|
|
) -> str:
|
|
_validate_approval_meta(approval, "add_edge", apply_payload)
|
|
_reject_unknown(apply_payload, {"from_claim", "to_claim", "edge_type", "weight"}, "add_edge apply_payload")
|
|
f = _uuid(apply_payload.get("from_claim"), "add_edge.from_claim")
|
|
t = _uuid(apply_payload.get("to_claim"), "add_edge.to_claim")
|
|
et = apply_payload.get("edge_type")
|
|
if et not in EDGE_TYPES:
|
|
raise ValueError(f"add_edge.edge_type must be one of {sorted(EDGE_TYPES)}")
|
|
w = _weight(apply_payload.get("weight"), "add_edge.weight")
|
|
if f == t:
|
|
raise ValueError("add_edge from_claim and to_claim must differ")
|
|
row_id = deterministic_row_uuid(proposal_id, "claim_edge", f, t, et)
|
|
|
|
edge_lock_key = f"claim_edge:{f}:{t}:{et}"
|
|
canonical = f"""
|
|
-- serialize semantic edge creation because the historical table has no
|
|
-- (from_claim,to_claim,edge_type) uniqueness constraint.
|
|
select pg_advisory_xact_lock(hashtextextended({sql_literal(edge_lock_key)}, 0));
|
|
|
|
-- insert the edge unless an identical (from,to,type) edge already exists
|
|
insert into public.claim_edges (id, from_claim, to_claim, edge_type, weight)
|
|
select {sql_literal(row_id)}::uuid, {sql_literal(f)}::uuid, {sql_literal(t)}::uuid,
|
|
{sql_literal(et)}::edge_type, {sql_literal(w)}
|
|
where not exists (
|
|
select 1 from public.claim_edges
|
|
where from_claim = {sql_literal(f)}::uuid
|
|
and to_claim = {sql_literal(t)}::uuid
|
|
and edge_type = {sql_literal(et)}::edge_type);
|
|
""".rstrip()
|
|
|
|
checks = f""" -- Historical replay receipts can carry a pre-deterministic row id.
|
|
-- Require one exact semantic row; fresh applies still insert row_id above.
|
|
if (select count(*) from public.claim_edges
|
|
where from_claim = {sql_literal(f)}::uuid
|
|
and to_claim = {sql_literal(t)}::uuid
|
|
and edge_type = {sql_literal(et)}::edge_type) <> 1 then
|
|
raise exception 'apply_proposal: claim_edge row does not match strict apply payload';
|
|
end if;
|
|
if exists (select 1 from public.claim_edges
|
|
where from_claim = {sql_literal(f)}::uuid
|
|
and to_claim = {sql_literal(t)}::uuid
|
|
and edge_type = {sql_literal(et)}::edge_type
|
|
and weight is distinct from {sql_literal(w)}) then
|
|
raise exception 'apply_proposal: claim_edge row does not match strict apply payload';
|
|
end if;
|
|
if not exists (select 1 from public.claim_edges
|
|
where from_claim = {sql_literal(f)}::uuid
|
|
and to_claim = {sql_literal(t)}::uuid
|
|
and edge_type = {sql_literal(et)}::edge_type
|
|
and weight is not distinct from {sql_literal(w)}) then
|
|
raise exception 'apply_proposal: claim_edge row does not match strict apply payload';
|
|
end if;"""
|
|
|
|
ledger = _ledger_and_verify(proposal_id, applied_by or SERVICE_AGENT_HANDLE, checks, approval)
|
|
return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval))
|
|
|
|
|
|
def build_attach_evidence_sql(
|
|
apply_payload: dict[str, Any],
|
|
proposal_id: str,
|
|
applied_by: str | None,
|
|
approval: dict[str, Any],
|
|
) -> str:
|
|
_validate_approval_meta(approval, "attach_evidence", apply_payload)
|
|
_reject_unknown(apply_payload, {"evidence"}, "attach_evidence apply_payload")
|
|
evidence: list[dict[str, Any]] = apply_payload.get("evidence") or []
|
|
if not evidence:
|
|
raise ValueError("attach_evidence apply_payload requires non-empty 'evidence'")
|
|
|
|
statements = []
|
|
checks = []
|
|
for i, ev in enumerate(evidence):
|
|
_reject_unknown(
|
|
ev,
|
|
{"claim_id", "source_id", "role", "weight"},
|
|
f"attach_evidence.evidence[{i}]",
|
|
)
|
|
claim_id = _uuid(ev.get("claim_id"), f"attach_evidence.evidence[{i}].claim_id")
|
|
source_id = _uuid(ev.get("source_id"), f"attach_evidence.evidence[{i}].source_id")
|
|
role = ev.get("role") or "grounds"
|
|
if role not in EVIDENCE_ROLES:
|
|
raise ValueError(f"attach_evidence.evidence[{i}].role must be one of {sorted(EVIDENCE_ROLES)}")
|
|
weight = _weight(ev.get("weight"), f"attach_evidence.evidence[{i}].weight")
|
|
statements.append(
|
|
f"""insert into public.claim_evidence (claim_id, source_id, role, weight)
|
|
select {sql_literal(claim_id)}::uuid, {sql_literal(source_id)}::uuid,
|
|
{sql_literal(role)}::evidence_role, {sql_literal(weight)}
|
|
on conflict (claim_id, source_id, role) do nothing;"""
|
|
)
|
|
checks.append(
|
|
f""" -- Accept a legacy receipt id only when exactly one semantic row matches.
|
|
if (select count(*) from public.claim_evidence
|
|
where claim_id = {sql_literal(claim_id)}::uuid
|
|
and source_id = {sql_literal(source_id)}::uuid
|
|
and role = {sql_literal(role)}::evidence_role) <> 1 then
|
|
raise exception 'apply_proposal: evidence row % does not match strict apply payload', {i};
|
|
end if;
|
|
if exists (select 1 from public.claim_evidence
|
|
where claim_id = {sql_literal(claim_id)}::uuid
|
|
and source_id = {sql_literal(source_id)}::uuid
|
|
and role = {sql_literal(role)}::evidence_role
|
|
and weight is distinct from {sql_literal(weight)}) then
|
|
raise exception 'apply_proposal: evidence row % does not match strict apply payload', {i};
|
|
end if;
|
|
if not exists (select 1 from public.claim_evidence
|
|
where claim_id = {sql_literal(claim_id)}::uuid
|
|
and source_id = {sql_literal(source_id)}::uuid
|
|
and role = {sql_literal(role)}::evidence_role
|
|
and weight is not distinct from {sql_literal(weight)}) then
|
|
raise exception 'apply_proposal: evidence row % does not match strict apply payload', {i};
|
|
end if;"""
|
|
)
|
|
|
|
canonical = "\n".join(statements)
|
|
ledger = _ledger_and_verify(proposal_id, applied_by or SERVICE_AGENT_HANDLE, "\n".join(checks), approval)
|
|
return _wrap_txn(canonical, ledger, _approval_guard_sql(proposal_id, approval))
|
|
|
|
|
|
def build_approve_claim_v3_sql(
|
|
apply_payload: dict[str, Any],
|
|
proposal_id: str,
|
|
applied_by: str | None,
|
|
approval: dict[str, Any],
|
|
*,
|
|
fresh_owned: bool = False,
|
|
fresh_preflight_sha256: str | None = None,
|
|
) -> str:
|
|
"""Build an exact-row idempotent V3 claim/source/evidence transaction."""
|
|
_validate_approval_meta(approval, "approve_claim", apply_payload)
|
|
bundle = _normalize_v3_approve_claim(apply_payload)
|
|
if fresh_owned:
|
|
_sha256(fresh_preflight_sha256, "fresh-owned approve_claim preflight")
|
|
elif fresh_preflight_sha256 is not None:
|
|
raise ValueError("fresh preflight SHA-256 is only valid for fresh-owned approve_claim")
|
|
|
|
statements: list[str] = []
|
|
checks: list[str] = []
|
|
if fresh_owned:
|
|
statements.extend(
|
|
[
|
|
f"-- fresh-owned preflight SHA-256: {fresh_preflight_sha256}",
|
|
(
|
|
"select pg_advisory_xact_lock(hashtextextended("
|
|
f"'proposal-fresh-apply:' || {sql_literal(proposal_id)}, 0));"
|
|
),
|
|
]
|
|
)
|
|
|
|
for row in bundle["sources"]:
|
|
statements.append(
|
|
f"""do $source_conflict$
|
|
begin
|
|
if exists (select 1 from public.sources
|
|
where hash = {sql_literal(row["content_hash"])}
|
|
and id <> {sql_literal(row["id"])}::uuid) then
|
|
raise exception 'V3 approve_claim: source hash collision for source %', {sql_literal(row["id"])};
|
|
end if;
|
|
end
|
|
$source_conflict$;
|
|
insert into public.sources (
|
|
id, source_type, url, storage_path, excerpt, hash, created_by, captured_at,
|
|
canonical_url, storage_uri, content_hash, access_scope, ingestion_origin,
|
|
provenance_status, source_class, auto_promotion_policy, accepted_by_proposal_id
|
|
) values (
|
|
{sql_literal(row["id"])}::uuid, {sql_literal(row["source_type"])},
|
|
{sql_literal(row["canonical_url"])}, {sql_literal(row["storage_uri"])},
|
|
{sql_literal(row["excerpt"])}, {sql_literal(row["content_hash"])},
|
|
{sql_literal(row["created_by"])}::uuid, {sql_literal(row["captured_at"])}::timestamptz,
|
|
{sql_literal(row["canonical_url"])}, {sql_literal(row["storage_uri"])},
|
|
{sql_literal(row["content_hash"])}, {sql_literal(row["access_scope"])},
|
|
{sql_literal(row["ingestion_origin"])}, {sql_literal(row["provenance_status"])},
|
|
{sql_literal(row["source_class"])}, {sql_literal(row["auto_promotion_policy"])},
|
|
{sql_literal(proposal_id)}::uuid
|
|
)
|
|
on conflict do nothing;"""
|
|
)
|
|
checks.append(
|
|
f""" if not exists (select 1 from public.sources
|
|
where id = {sql_literal(row["id"])}::uuid
|
|
and source_type = {sql_literal(row["source_type"])}
|
|
and url is not distinct from {sql_literal(row["canonical_url"])}
|
|
and storage_path is not distinct from {sql_literal(row["storage_uri"])}
|
|
and excerpt is not distinct from {sql_literal(row["excerpt"])}
|
|
and hash = {sql_literal(row["content_hash"])}
|
|
and created_by is not distinct from {sql_literal(row["created_by"])}::uuid
|
|
and captured_at = {sql_literal(row["captured_at"])}::timestamptz
|
|
and canonical_url is not distinct from {sql_literal(row["canonical_url"])}
|
|
and storage_uri is not distinct from {sql_literal(row["storage_uri"])}
|
|
and content_hash = {sql_literal(row["content_hash"])}
|
|
and access_scope = {sql_literal(row["access_scope"])}
|
|
and ingestion_origin = {sql_literal(row["ingestion_origin"])}
|
|
and provenance_status = {sql_literal(row["provenance_status"])}
|
|
and source_class = {sql_literal(row["source_class"])}
|
|
and auto_promotion_policy = {sql_literal(row["auto_promotion_policy"])}
|
|
and accepted_by_proposal_id = {sql_literal(proposal_id)}::uuid) then
|
|
raise exception 'V3 approve_claim: source row does not match strict payload: %', {sql_literal(row["id"])};
|
|
end if;"""
|
|
)
|
|
|
|
for row in bundle["claims"]:
|
|
tags = _text_array(row["tags"])
|
|
statements.append(
|
|
f"""insert into public.claims (
|
|
id, owner_agent_id, type, text, proposition, body_markdown, scope, access_scope,
|
|
status, admission_state, revision_criteria, revision_kind, supersedes_id,
|
|
confidence, tags, created_by, superseded_by, accepted_by_proposal_id
|
|
) values (
|
|
{sql_literal(row["id"])}::uuid, {sql_literal(row["owner_agent_id"])}::uuid,
|
|
{sql_literal(row["type"])}, {sql_literal(row["proposition"])}, {sql_literal(row["proposition"])},
|
|
{sql_literal(row["body_markdown"])}, {sql_literal(row["scope"])}, {sql_literal(row["access_scope"])},
|
|
{sql_literal(row["status"])}, 'admitted', {sql_literal(row["revision_criteria"])},
|
|
{sql_literal(row["revision_kind"])}, null, null, {tags},
|
|
{sql_literal(row["created_by"])}::uuid, null, {sql_literal(proposal_id)}::uuid
|
|
)
|
|
on conflict do nothing;"""
|
|
)
|
|
checks.append(
|
|
f""" if not exists (select 1 from public.claims
|
|
where id = {sql_literal(row["id"])}::uuid
|
|
and owner_agent_id is not distinct from {sql_literal(row["owner_agent_id"])}::uuid
|
|
and type = {sql_literal(row["type"])}
|
|
and text = {sql_literal(row["proposition"])}
|
|
and proposition = {sql_literal(row["proposition"])}
|
|
and body_markdown = {sql_literal(row["body_markdown"])}
|
|
and scope = {sql_literal(row["scope"])}
|
|
and access_scope = {sql_literal(row["access_scope"])}
|
|
and status = {sql_literal(row["status"])}
|
|
and admission_state = 'admitted'
|
|
and revision_criteria = {sql_literal(row["revision_criteria"])}
|
|
and revision_kind = 'original'
|
|
and supersedes_id is null
|
|
and confidence is null
|
|
and tags = {tags}
|
|
and created_by is not distinct from {sql_literal(row["created_by"])}::uuid
|
|
and superseded_by is null
|
|
and accepted_by_proposal_id = {sql_literal(proposal_id)}::uuid) then
|
|
raise exception 'V3 approve_claim: claim row does not match strict payload: %', {sql_literal(row["id"])};
|
|
end if;"""
|
|
)
|
|
|
|
for row in bundle["evidence"]:
|
|
statements.append(
|
|
f"""insert into public.claim_evidence (
|
|
id, claim_id, source_id, role, weight, created_by, excerpt, locator_json,
|
|
source_content_hash, rationale, polarity, accepted_from_proposal_id
|
|
) values (
|
|
{sql_literal(row["id"])}::uuid, {sql_literal(row["claim_id"])}::uuid,
|
|
{sql_literal(row["source_id"])}::uuid, {sql_literal(row["role"])}::evidence_role,
|
|
null, {sql_literal(row["created_by"])}::uuid, {sql_literal(row["excerpt"])},
|
|
{_jsonb(row["locator_json"])}, {sql_literal(row["source_content_hash"])},
|
|
{sql_literal(row["rationale"])}, {sql_literal(row["polarity"])}, {sql_literal(proposal_id)}::uuid
|
|
)
|
|
on conflict do nothing;"""
|
|
)
|
|
checks.append(
|
|
f""" if not exists (select 1 from public.claim_evidence
|
|
where id = {sql_literal(row["id"])}::uuid
|
|
and claim_id = {sql_literal(row["claim_id"])}::uuid
|
|
and source_id = {sql_literal(row["source_id"])}::uuid
|
|
and role = {sql_literal(row["role"])}::evidence_role
|
|
and weight is null
|
|
and created_by is not distinct from {sql_literal(row["created_by"])}::uuid
|
|
and excerpt is not distinct from {sql_literal(row["excerpt"])}
|
|
and locator_json = {_jsonb(row["locator_json"])}
|
|
and source_content_hash = {sql_literal(row["source_content_hash"])}
|
|
and rationale = {sql_literal(row["rationale"])}
|
|
and polarity = {sql_literal(row["polarity"])}
|
|
and accepted_from_proposal_id = {sql_literal(proposal_id)}::uuid) then
|
|
raise exception 'V3 approve_claim: evidence row does not match strict payload: %', {sql_literal(row["id"])};
|
|
end if;"""
|
|
)
|
|
|
|
for row in bundle["edges"]:
|
|
statements.append(
|
|
f"""insert into public.claim_edges (
|
|
id, from_claim, to_claim, edge_type, weight, created_by, relation_type,
|
|
rationale, scope_or_conditions, accepted_by_proposal_id
|
|
) values (
|
|
{sql_literal(row["id"])}::uuid, {sql_literal(row["from_claim"])}::uuid,
|
|
{sql_literal(row["to_claim"])}::uuid, {sql_literal(row["edge_type"])}::edge_type,
|
|
null, {sql_literal(row["created_by"])}::uuid, {sql_literal(row["relation_type"])},
|
|
{sql_literal(row["rationale"])}, {sql_literal(row["scope_or_conditions"])},
|
|
{sql_literal(proposal_id)}::uuid
|
|
)
|
|
on conflict do nothing;"""
|
|
)
|
|
checks.append(
|
|
f""" if not exists (select 1 from public.claim_edges
|
|
where id = {sql_literal(row["id"])}::uuid
|
|
and from_claim = {sql_literal(row["from_claim"])}::uuid
|
|
and to_claim = {sql_literal(row["to_claim"])}::uuid
|
|
and edge_type = {sql_literal(row["edge_type"])}::edge_type
|
|
and weight is null
|
|
and created_by is not distinct from {sql_literal(row["created_by"])}::uuid
|
|
and relation_type = {sql_literal(row["relation_type"])}
|
|
and rationale = {sql_literal(row["rationale"])}
|
|
and scope_or_conditions is not distinct from {sql_literal(row["scope_or_conditions"])}
|
|
and accepted_by_proposal_id = {sql_literal(proposal_id)}::uuid) then
|
|
raise exception 'V3 approve_claim: edge row does not match strict payload: %', {sql_literal(row["id"])};
|
|
end if;"""
|
|
)
|
|
|
|
for row in bundle["assessments"]:
|
|
statements.append(
|
|
f"""insert into public.claim_evidence_assessments (
|
|
id, claim_id, support_tier, challenge_tier, overall_state, rationale,
|
|
evidence_set_hash, accepted_by_proposal_id, supersedes_assessment_id
|
|
) values (
|
|
{sql_literal(row["id"])}::uuid, {sql_literal(row["claim_id"])}::uuid,
|
|
{sql_literal(row["support_tier"])}, {sql_literal(row["challenge_tier"])},
|
|
{sql_literal(row["overall_state"])}, {sql_literal(row["rationale"])},
|
|
{sql_literal(row["evidence_set_hash"])}, {sql_literal(proposal_id)}::uuid,
|
|
{sql_literal(row["supersedes_assessment_id"])}::uuid
|
|
)
|
|
on conflict do nothing;"""
|
|
)
|
|
checks.append(
|
|
f""" if not exists (select 1 from public.claim_evidence_assessments
|
|
where id = {sql_literal(row["id"])}::uuid
|
|
and claim_id = {sql_literal(row["claim_id"])}::uuid
|
|
and support_tier is not distinct from {sql_literal(row["support_tier"])}
|
|
and challenge_tier is not distinct from {sql_literal(row["challenge_tier"])}
|
|
and overall_state = {sql_literal(row["overall_state"])}
|
|
and rationale = {sql_literal(row["rationale"])}
|
|
and evidence_set_hash = {sql_literal(row["evidence_set_hash"])}
|
|
and accepted_by_proposal_id = {sql_literal(proposal_id)}::uuid
|
|
and supersedes_assessment_id is not distinct from {sql_literal(row["supersedes_assessment_id"])}::uuid) then
|
|
raise exception 'V3 approve_claim: assessment row does not match strict payload: %', {sql_literal(row["id"])};
|
|
end if;"""
|
|
)
|
|
|
|
canonical = "\n\n".join(statements)
|
|
ledger = _ledger_and_verify(proposal_id, applied_by or SERVICE_AGENT_HANDLE, "\n".join(checks), approval)
|
|
return _wrap_txn(
|
|
canonical,
|
|
ledger,
|
|
_approval_guard_sql(proposal_id, approval),
|
|
serializable=True,
|
|
contract_guard_sql=_v3_contract_transaction_guard_sql(),
|
|
session_advisory_lock_key=V3_CATALOG_DDL_ADVISORY_LOCK_KEY,
|
|
)
|
|
|
|
|
|
def build_approve_claim_sql(
|
|
apply_payload: dict[str, Any],
|
|
proposal_id: str,
|
|
applied_by: str | None,
|
|
approval: dict[str, Any],
|
|
*,
|
|
fresh_owned: bool = False,
|
|
fresh_preflight_sha256: str | None = None,
|
|
) -> str:
|
|
"""Build one conflict-safe transaction for a reviewed canonical graph bundle."""
|
|
_validate_approval_meta(approval, "approve_claim", apply_payload)
|
|
if apply_payload.get("contract_version") == 3:
|
|
return build_approve_claim_v3_sql(
|
|
apply_payload,
|
|
proposal_id,
|
|
applied_by,
|
|
approval,
|
|
fresh_owned=fresh_owned,
|
|
fresh_preflight_sha256=fresh_preflight_sha256,
|
|
)
|
|
_reject_unknown(
|
|
apply_payload,
|
|
{
|
|
"contract_version",
|
|
"source_proposal_id",
|
|
"agent_id",
|
|
"claims",
|
|
"sources",
|
|
"evidence",
|
|
"edges",
|
|
"reasoning_tools",
|
|
"normalization_manifest",
|
|
},
|
|
"approve_claim apply_payload",
|
|
)
|
|
if apply_payload.get("contract_version") != 2:
|
|
raise ValueError("approve_claim apply_payload.contract_version must equal 2")
|
|
for key in ("claims", "sources", "evidence", "edges", "reasoning_tools"):
|
|
if key not in apply_payload:
|
|
raise ValueError(f"approve_claim apply_payload requires explicit '{key}' collection")
|
|
if apply_payload.get("source_proposal_id") is not None:
|
|
_uuid(apply_payload["source_proposal_id"], "approve_claim.source_proposal_id")
|
|
if apply_payload.get("normalization_manifest") is not None and not isinstance(
|
|
apply_payload["normalization_manifest"], dict
|
|
):
|
|
raise ValueError("approve_claim.normalization_manifest must be an object")
|
|
agent_id = _uuid(apply_payload.get("agent_id"), "approve_claim.agent_id", nullable=True)
|
|
raw_claims = _rows(apply_payload, "claims", required=True)
|
|
raw_sources = _rows(apply_payload, "sources")
|
|
raw_evidence = _rows(apply_payload, "evidence")
|
|
raw_edges = _rows(apply_payload, "edges")
|
|
raw_tools = _rows(apply_payload, "reasoning_tools")
|
|
|
|
claims: list[dict[str, Any]] = []
|
|
for index, row in enumerate(raw_claims):
|
|
path = f"approve_claim.claims[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{"id", "type", "text", "status", "confidence", "tags", "created_by", "superseded_by"},
|
|
path,
|
|
)
|
|
claim_type = row.get("type")
|
|
if claim_type not in CLAIM_TYPES:
|
|
raise ValueError(f"{path}.type must be one of {sorted(CLAIM_TYPES)}")
|
|
text = row.get("text")
|
|
if not isinstance(text, str) or not text.strip():
|
|
raise ValueError(f"{path}.text must be a non-empty string")
|
|
status = row.get("status", "open")
|
|
if status != "open":
|
|
raise ValueError(f"{path}.status must be 'open' for an insert-only approval")
|
|
tags = row.get("tags") or []
|
|
if not isinstance(tags, list) or any(not isinstance(tag, str) for tag in tags):
|
|
raise ValueError(f"{path}.tags must be a list of strings")
|
|
claims.append(
|
|
{
|
|
"id": _uuid(row.get("id"), f"{path}.id"),
|
|
"type": claim_type,
|
|
"text": text,
|
|
"status": status,
|
|
"confidence": _weight(row.get("confidence"), f"{path}.confidence"),
|
|
"tags": tags,
|
|
"created_by": _uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True),
|
|
"superseded_by": _uuid(row.get("superseded_by"), f"{path}.superseded_by", nullable=True),
|
|
}
|
|
)
|
|
|
|
sources: list[dict[str, Any]] = []
|
|
for index, row in enumerate(raw_sources):
|
|
path = f"approve_claim.sources[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{"id", "source_type", "url", "storage_path", "excerpt", "hash", "created_by"},
|
|
path,
|
|
)
|
|
source_type = row.get("source_type")
|
|
if source_type not in SOURCE_TYPES:
|
|
raise ValueError(f"{path}.source_type must be one of {sorted(SOURCE_TYPES)}")
|
|
source_hash = row.get("hash")
|
|
if not isinstance(source_hash, str) or not source_hash.strip():
|
|
raise ValueError(f"{path}.hash must be a non-empty string")
|
|
for key in ("url", "storage_path", "excerpt"):
|
|
if row.get(key) is not None and not isinstance(row.get(key), str):
|
|
raise ValueError(f"{path}.{key} must be a string or null")
|
|
sources.append(
|
|
{
|
|
"id": _uuid(row.get("id"), f"{path}.id"),
|
|
"source_type": source_type,
|
|
"url": row.get("url"),
|
|
"storage_path": row.get("storage_path"),
|
|
"excerpt": row.get("excerpt"),
|
|
"hash": source_hash,
|
|
"created_by": _uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True),
|
|
}
|
|
)
|
|
|
|
evidence: list[dict[str, Any]] = []
|
|
for index, row in enumerate(raw_evidence):
|
|
path = f"approve_claim.evidence[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{"claim_id", "source_id", "role", "weight", "created_by"},
|
|
path,
|
|
)
|
|
role = row.get("role", "grounds")
|
|
if role not in EVIDENCE_ROLES:
|
|
raise ValueError(f"{path}.role must be one of {sorted(EVIDENCE_ROLES)}")
|
|
evidence.append(
|
|
{
|
|
"claim_id": _uuid(row.get("claim_id"), f"{path}.claim_id"),
|
|
"source_id": _uuid(row.get("source_id"), f"{path}.source_id"),
|
|
"role": role,
|
|
"weight": _weight(row.get("weight"), f"{path}.weight"),
|
|
"created_by": _uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True),
|
|
}
|
|
)
|
|
|
|
edges: list[dict[str, Any]] = []
|
|
for index, row in enumerate(raw_edges):
|
|
path = f"approve_claim.edges[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{"from_claim", "to_claim", "edge_type", "weight", "created_by"},
|
|
path,
|
|
)
|
|
edge_type = row.get("edge_type")
|
|
if edge_type not in EDGE_TYPES:
|
|
raise ValueError(f"{path}.edge_type must be one of {sorted(EDGE_TYPES)}")
|
|
from_claim = _uuid(row.get("from_claim"), f"{path}.from_claim")
|
|
to_claim = _uuid(row.get("to_claim"), f"{path}.to_claim")
|
|
if from_claim == to_claim:
|
|
raise ValueError(f"{path} cannot be a self-edge")
|
|
edges.append(
|
|
{
|
|
"id": deterministic_row_uuid(proposal_id, "claim_edge", from_claim, to_claim, edge_type),
|
|
"from_claim": from_claim,
|
|
"to_claim": to_claim,
|
|
"edge_type": edge_type,
|
|
"weight": _weight(row.get("weight"), f"{path}.weight"),
|
|
"created_by": _uuid(row.get("created_by", agent_id), f"{path}.created_by", nullable=True),
|
|
}
|
|
)
|
|
|
|
reasoning_tools: list[dict[str, Any]] = []
|
|
for index, row in enumerate(raw_tools):
|
|
path = f"approve_claim.reasoning_tools[{index}]"
|
|
_reject_unknown(
|
|
row,
|
|
{"id", "agent_id", "name", "description", "category"},
|
|
path,
|
|
)
|
|
name = row.get("name")
|
|
description = row.get("description")
|
|
category = row.get("category")
|
|
if not isinstance(name, str) or not name.strip():
|
|
raise ValueError(f"{path}.name must be a non-empty string")
|
|
if not isinstance(description, str) or not description.strip():
|
|
raise ValueError(f"{path}.description must be a non-empty string")
|
|
if category is not None and not isinstance(category, str):
|
|
raise ValueError(f"{path}.category must be a string or null")
|
|
reasoning_tools.append(
|
|
{
|
|
"id": _uuid(row.get("id"), f"{path}.id"),
|
|
"agent_id": _uuid(row.get("agent_id", agent_id), f"{path}.agent_id", nullable=True),
|
|
"name": name,
|
|
"description": description,
|
|
"category": category,
|
|
}
|
|
)
|
|
|
|
_dedupe([row["id"] for row in claims], "approve_claim.claims ids")
|
|
_dedupe([row["id"] for row in sources], "approve_claim.sources ids")
|
|
_dedupe([row["hash"] for row in sources], "approve_claim.sources hashes")
|
|
_dedupe(
|
|
[[row["claim_id"], row["source_id"], row["role"]] for row in evidence],
|
|
"approve_claim.evidence keys",
|
|
)
|
|
_dedupe(
|
|
[[row["from_claim"], row["to_claim"], row["edge_type"]] for row in edges],
|
|
"approve_claim.edges keys",
|
|
)
|
|
_dedupe([row["id"] for row in reasoning_tools], "approve_claim.reasoning_tools ids")
|
|
|
|
if fresh_owned:
|
|
if (
|
|
not isinstance(fresh_preflight_sha256, str)
|
|
or len(fresh_preflight_sha256) != 64
|
|
or any(character not in "0123456789abcdef" for character in fresh_preflight_sha256)
|
|
):
|
|
raise ValueError("fresh-owned approve_claim requires an exact preflight SHA-256")
|
|
elif fresh_preflight_sha256 is not None:
|
|
raise ValueError("fresh preflight SHA-256 is only valid for fresh-owned approve_claim")
|
|
|
|
statements: list[str] = []
|
|
checks: list[str] = []
|
|
|
|
if fresh_owned:
|
|
statements.extend(
|
|
[
|
|
f"-- fresh-owned preflight SHA-256: {fresh_preflight_sha256}",
|
|
(
|
|
"select pg_advisory_xact_lock(hashtextextended("
|
|
f"'proposal-fresh-apply:' || {sql_literal(proposal_id)}, 0));"
|
|
),
|
|
]
|
|
)
|
|
|
|
if sources:
|
|
collision_checks = []
|
|
for row in sources:
|
|
collision_checks.append(
|
|
f""" if exists (select 1 from public.sources
|
|
where hash = {sql_literal(row["hash"])}
|
|
and id <> {sql_literal(row["id"])}::uuid) then
|
|
raise exception 'approve_claim: source hash collision for source %', {sql_literal(row["id"])};
|
|
end if;"""
|
|
)
|
|
statements.append("do $source_conflicts$\nbegin\n" + "\n".join(collision_checks) + "\nend\n$source_conflicts$;")
|
|
|
|
for row in claims:
|
|
tags = _text_array(row["tags"])
|
|
conflict_clause = ";" if fresh_owned else "\non conflict (id) do nothing;"
|
|
statements.append(
|
|
f"""insert into public.claims (id, type, text, status, confidence, tags, created_by, superseded_by)
|
|
select {sql_literal(row["id"])}::uuid, {sql_literal(row["type"])}, {sql_literal(row["text"])},
|
|
{sql_literal(row["status"])}, {sql_literal(row["confidence"])}, {tags},
|
|
{sql_literal(row["created_by"])}::uuid, {sql_literal(row["superseded_by"])}::uuid{conflict_clause}"""
|
|
)
|
|
checks.append(
|
|
f""" if not exists (select 1 from public.claims
|
|
where id = {sql_literal(row["id"])}::uuid
|
|
and type = {sql_literal(row["type"])}
|
|
and text = {sql_literal(row["text"])}
|
|
and status = {sql_literal(row["status"])}
|
|
and confidence is not distinct from {sql_literal(row["confidence"])}
|
|
and tags is not distinct from {tags}
|
|
and created_by is not distinct from {sql_literal(row["created_by"])}::uuid
|
|
and superseded_by is not distinct from {sql_literal(row["superseded_by"])}::uuid) then
|
|
raise exception 'approve_claim: claim row does not match strict apply payload: %', {sql_literal(row["id"])};
|
|
end if;"""
|
|
)
|
|
|
|
for row in sources:
|
|
conflict_clause = ";" if fresh_owned else "\non conflict do nothing;"
|
|
statements.append(
|
|
f"""insert into public.sources (id, source_type, url, storage_path, excerpt, hash, created_by)
|
|
select {sql_literal(row["id"])}::uuid, {sql_literal(row["source_type"])}, {sql_literal(row["url"])},
|
|
{sql_literal(row["storage_path"])}, {sql_literal(row["excerpt"])}, {sql_literal(row["hash"])},
|
|
{sql_literal(row["created_by"])}::uuid{conflict_clause}"""
|
|
)
|
|
checks.append(
|
|
f""" if exists (select 1 from public.sources
|
|
where hash = {sql_literal(row["hash"])}
|
|
and id <> {sql_literal(row["id"])}::uuid) then
|
|
raise exception 'approve_claim: source hash collision for source %', {sql_literal(row["id"])};
|
|
end if;
|
|
if not exists (select 1 from public.sources
|
|
where id = {sql_literal(row["id"])}::uuid
|
|
and source_type = {sql_literal(row["source_type"])}
|
|
and url is not distinct from {sql_literal(row["url"])}
|
|
and storage_path is not distinct from {sql_literal(row["storage_path"])}
|
|
and excerpt is not distinct from {sql_literal(row["excerpt"])}
|
|
and hash = {sql_literal(row["hash"])}
|
|
and created_by is not distinct from {sql_literal(row["created_by"])}::uuid) then
|
|
raise exception 'approve_claim: source row does not match strict apply payload: %', {sql_literal(row["id"])};
|
|
end if;"""
|
|
)
|
|
|
|
for row in reasoning_tools:
|
|
conflict_clause = ";" if fresh_owned else "\non conflict (id) do nothing;"
|
|
statements.append(
|
|
f"""insert into public.reasoning_tools (id, agent_id, name, description, category)
|
|
select {sql_literal(row["id"])}::uuid, {sql_literal(row["agent_id"])}::uuid,
|
|
{sql_literal(row["name"])}, {sql_literal(row["description"])}, {sql_literal(row["category"])}{conflict_clause}"""
|
|
)
|
|
checks.append(
|
|
f""" if not exists (select 1 from public.reasoning_tools
|
|
where id = {sql_literal(row["id"])}::uuid
|
|
and agent_id is not distinct from {sql_literal(row["agent_id"])}::uuid
|
|
and name = {sql_literal(row["name"])}
|
|
and description = {sql_literal(row["description"])}
|
|
and category is not distinct from {sql_literal(row["category"])}) then
|
|
raise exception 'approve_claim: reasoning tool row does not match strict apply payload: %', {sql_literal(row["id"])};
|
|
end if;"""
|
|
)
|
|
|
|
for row in evidence:
|
|
conflict_clause = ";" if fresh_owned else "\non conflict (claim_id, source_id, role) do nothing;"
|
|
statements.append(
|
|
f"""insert into public.claim_evidence (claim_id, source_id, role, weight, created_by)
|
|
select {sql_literal(row["claim_id"])}::uuid, {sql_literal(row["source_id"])}::uuid,
|
|
{sql_literal(row["role"])}::evidence_role, {sql_literal(row["weight"])},
|
|
{sql_literal(row["created_by"])}::uuid{conflict_clause}"""
|
|
)
|
|
checks.append(
|
|
f""" -- Accept a legacy receipt id only when exactly one semantic row matches.
|
|
if (select count(*) from public.claim_evidence
|
|
where claim_id = {sql_literal(row["claim_id"])}::uuid
|
|
and source_id = {sql_literal(row["source_id"])}::uuid
|
|
and role = {sql_literal(row["role"])}::evidence_role) <> 1 then
|
|
raise exception 'approve_claim: evidence row does not match strict apply payload';
|
|
end if;
|
|
if exists (select 1 from public.claim_evidence
|
|
where claim_id = {sql_literal(row["claim_id"])}::uuid
|
|
and source_id = {sql_literal(row["source_id"])}::uuid
|
|
and role = {sql_literal(row["role"])}::evidence_role
|
|
and (weight is distinct from {sql_literal(row["weight"])}
|
|
or created_by is distinct from {sql_literal(row["created_by"])}::uuid)) then
|
|
raise exception 'approve_claim: evidence row does not match strict apply payload';
|
|
end if;
|
|
if not exists (select 1 from public.claim_evidence
|
|
where claim_id = {sql_literal(row["claim_id"])}::uuid
|
|
and source_id = {sql_literal(row["source_id"])}::uuid
|
|
and role = {sql_literal(row["role"])}::evidence_role
|
|
and weight is not distinct from {sql_literal(row["weight"])}
|
|
and created_by is not distinct from {sql_literal(row["created_by"])}::uuid) then
|
|
raise exception 'approve_claim: evidence row does not match strict apply payload';
|
|
end if;"""
|
|
)
|
|
|
|
for row in edges:
|
|
edge_lock_key = f"claim_edge:{row['from_claim']}:{row['to_claim']}:{row['edge_type']}"
|
|
edge_insert_guard = (
|
|
";"
|
|
if fresh_owned
|
|
else f"""\n where not exists (
|
|
select 1 from public.claim_edges
|
|
where from_claim = {sql_literal(row["from_claim"])}::uuid
|
|
and to_claim = {sql_literal(row["to_claim"])}::uuid
|
|
and edge_type = {sql_literal(row["edge_type"])}::edge_type);"""
|
|
)
|
|
statements.append(
|
|
f"""select pg_advisory_xact_lock(hashtextextended({sql_literal(edge_lock_key)}, 0));
|
|
insert into public.claim_edges (id, from_claim, to_claim, edge_type, weight, created_by)
|
|
select {sql_literal(row["id"])}::uuid, {sql_literal(row["from_claim"])}::uuid, {sql_literal(row["to_claim"])}::uuid,
|
|
{sql_literal(row["edge_type"])}::edge_type, {sql_literal(row["weight"])},
|
|
{sql_literal(row["created_by"])}::uuid{edge_insert_guard}"""
|
|
)
|
|
checks.append(
|
|
f""" -- Accept a legacy receipt id only when exactly one semantic row matches.
|
|
if (select count(*) from public.claim_edges
|
|
where from_claim = {sql_literal(row["from_claim"])}::uuid
|
|
and to_claim = {sql_literal(row["to_claim"])}::uuid
|
|
and edge_type = {sql_literal(row["edge_type"])}::edge_type) <> 1 then
|
|
raise exception 'approve_claim: edge row does not match strict apply payload';
|
|
end if;
|
|
if exists (select 1 from public.claim_edges
|
|
where from_claim = {sql_literal(row["from_claim"])}::uuid
|
|
and to_claim = {sql_literal(row["to_claim"])}::uuid
|
|
and edge_type = {sql_literal(row["edge_type"])}::edge_type
|
|
and (weight is distinct from {sql_literal(row["weight"])}
|
|
or created_by is distinct from {sql_literal(row["created_by"])}::uuid)) then
|
|
raise exception 'approve_claim: edge row does not match strict apply payload';
|
|
end if;
|
|
if not exists (select 1 from public.claim_edges
|
|
where from_claim = {sql_literal(row["from_claim"])}::uuid
|
|
and to_claim = {sql_literal(row["to_claim"])}::uuid
|
|
and edge_type = {sql_literal(row["edge_type"])}::edge_type
|
|
and weight is not distinct from {sql_literal(row["weight"])}
|
|
and created_by is not distinct from {sql_literal(row["created_by"])}::uuid) then
|
|
raise exception 'approve_claim: edge row does not match strict apply payload';
|
|
end if;"""
|
|
)
|
|
|
|
canonical = "\n\n".join(statements)
|
|
ledger = _ledger_and_verify(proposal_id, applied_by or SERVICE_AGENT_HANDLE, "\n".join(checks), approval)
|
|
return _wrap_txn(
|
|
canonical,
|
|
ledger,
|
|
_approval_guard_sql(proposal_id, approval),
|
|
serializable=fresh_owned,
|
|
)
|
|
|
|
|
|
def _wrap_txn(
|
|
canonical_sql: str,
|
|
ledger_sql: str,
|
|
approval_guard_sql: str,
|
|
*,
|
|
serializable: bool = False,
|
|
contract_guard_sql: str = "",
|
|
session_advisory_lock_key: int | None = None,
|
|
) -> str:
|
|
session_lock_sql = ""
|
|
session_unlock_sql = ""
|
|
if session_advisory_lock_key is not None:
|
|
session_lock_sql = f"select pg_catalog.pg_advisory_lock_shared({session_advisory_lock_key});\n"
|
|
session_unlock_sql = f"select pg_catalog.pg_advisory_unlock_shared({session_advisory_lock_key});\n"
|
|
return (
|
|
session_lock_sql
|
|
+ "begin;\n"
|
|
+ ("set transaction isolation level serializable;\n" if serializable else "")
|
|
+ "set local timezone = 'UTC';\n"
|
|
+ "set local standard_conforming_strings = on;\n"
|
|
+ (f"{contract_guard_sql}\n" if contract_guard_sql else "")
|
|
+ f"{approval_guard_sql}\n"
|
|
+ f"{canonical_sql}\n"
|
|
+ f"{ledger_sql}\n"
|
|
+ "commit;\n"
|
|
+ session_unlock_sql
|
|
)
|
|
|
|
|
|
BUILDERS = {
|
|
"revise_strategy": build_revise_strategy_sql,
|
|
"add_edge": build_add_edge_sql,
|
|
"attach_evidence": build_attach_evidence_sql,
|
|
"approve_claim": build_approve_claim_sql,
|
|
}
|
|
|
|
|
|
def proposal_contract_version(proposal: dict[str, Any]) -> int:
|
|
"""Return the exact typed apply contract or reject malformed/hybrid shapes."""
|
|
proposal_type = proposal.get("proposal_type")
|
|
payload = proposal.get("payload")
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("proposal payload must be a JSON object")
|
|
apply_payload = payload.get("apply_payload")
|
|
if not isinstance(apply_payload, dict):
|
|
raise ValueError("proposal payload.apply_payload must be a JSON object")
|
|
|
|
raw_version = apply_payload.get("contract_version")
|
|
if proposal_type == "approve_claim":
|
|
if type(raw_version) is not int or raw_version not in {2, 3}:
|
|
raise ValueError("approve_claim apply_payload.contract_version must be integer 2 or 3")
|
|
return raw_version
|
|
if "contract_version" in apply_payload:
|
|
raise ValueError(f"proposal_type {proposal_type!r} is a V2 legacy contract and must omit contract_version")
|
|
return 2
|
|
|
|
|
|
def assert_expected_contract_version(proposal: dict[str, Any], expected: int | None) -> int:
|
|
actual = proposal_contract_version(proposal)
|
|
if expected is not None and actual != expected:
|
|
raise ValueError(f"proposal contract version {actual} does not match expected version {expected}")
|
|
if actual == 3 and proposal.get("proposal_type") != "approve_claim":
|
|
raise ValueError("V3 apply contract supports only proposal_type 'approve_claim'")
|
|
return actual
|
|
|
|
|
|
def build_apply_sql(
|
|
proposal: dict[str, Any],
|
|
applied_by: str | None,
|
|
*,
|
|
fresh_owned: bool = False,
|
|
fresh_preflight_sha256: str | None = None,
|
|
) -> str:
|
|
assert_expected_contract_version(proposal, None)
|
|
ptype = proposal["proposal_type"]
|
|
if ptype not in BUILDERS:
|
|
raise ValueError(f"apply_proposal cannot apply proposal_type {ptype!r}")
|
|
payload = proposal["payload"]
|
|
apply_payload = payload.get("apply_payload")
|
|
if apply_payload is None:
|
|
raise ValueError(
|
|
"proposal payload has no 'apply_payload' strict contract. "
|
|
"Run the normalizer to produce apply_payload before applying."
|
|
)
|
|
if fresh_owned and ptype != "approve_claim":
|
|
raise ValueError("fresh-owned apply is supported only for approve_claim")
|
|
if ptype == "approve_claim":
|
|
return build_approve_claim_sql(
|
|
apply_payload,
|
|
proposal["id"],
|
|
applied_by or SERVICE_AGENT_HANDLE,
|
|
proposal,
|
|
fresh_owned=fresh_owned,
|
|
fresh_preflight_sha256=fresh_preflight_sha256,
|
|
)
|
|
return BUILDERS[ptype](
|
|
apply_payload,
|
|
proposal["id"],
|
|
applied_by or SERVICE_AGENT_HANDLE,
|
|
proposal,
|
|
)
|
|
|
|
|
|
def assert_applyable(proposal: dict[str, Any]) -> None:
|
|
status = proposal.get("status")
|
|
if status == "applied":
|
|
raise SystemExit(f"proposal {proposal['id']} is already applied (idempotent no-op)")
|
|
if status != "approved":
|
|
raise SystemExit(f"proposal {proposal['id']} has status {status!r}; only 'approved' proposals apply")
|
|
|
|
|
|
def assert_receiptable(proposal: dict[str, Any]) -> None:
|
|
if proposal.get("status") != "applied":
|
|
raise SystemExit(
|
|
f"proposal {proposal['id']} has status {proposal.get('status')!r}; "
|
|
"--receipt-only requires an applied proposal"
|
|
)
|
|
replay_receipt.expected_row_counts(proposal)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Execution (docker exec as the kb_apply role) #
|
|
# --------------------------------------------------------------------------- #
|
|
def load_password(secrets_file: str) -> str:
|
|
path = Path(secrets_file)
|
|
if not path.is_file():
|
|
raise SystemExit(f"secrets file not found: {secrets_file}")
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, _, val = line.partition("=")
|
|
if key.strip() in ("KB_APPLY_PASSWORD", "KB_APPLY_DB_PASSWORD", "PGPASSWORD"):
|
|
return val.strip().strip('"').strip("'")
|
|
raise SystemExit(f"no KB_APPLY_PASSWORD/KB_APPLY_DB_PASSWORD/PGPASSWORD entry in {secrets_file}")
|
|
|
|
|
|
def run_psql(
|
|
args: argparse.Namespace,
|
|
sql: str,
|
|
password: str,
|
|
*,
|
|
redact_output_on_error: bool = False,
|
|
) -> str:
|
|
docker_binary = shutil.which("docker") or "docker"
|
|
command = [
|
|
docker_binary,
|
|
"exec",
|
|
"-e",
|
|
"PGPASSWORD",
|
|
"-i",
|
|
args.container,
|
|
"psql",
|
|
"-U",
|
|
args.role,
|
|
"-h",
|
|
args.host,
|
|
"-d",
|
|
args.db,
|
|
"-v",
|
|
"ON_ERROR_STOP=1",
|
|
"-At",
|
|
"-q",
|
|
]
|
|
session_sql = "set timezone = 'UTC';\n" + sql
|
|
result = subprocess.run(
|
|
command,
|
|
input=session_sql,
|
|
text=True,
|
|
capture_output=True,
|
|
env={"PGPASSWORD": password, "PATH": "/usr/bin:/bin:/usr/local/bin"},
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
if redact_output_on_error:
|
|
raise SystemExit(f"psql failed ({result.returncode}); private postflight output redacted")
|
|
raise SystemExit(f"psql failed ({result.returncode})\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}")
|
|
return result.stdout
|
|
|
|
|
|
def load_proposal(args: argparse.Namespace, password: str) -> dict[str, Any]:
|
|
sql = f"""select jsonb_build_object(
|
|
'id', id::text,
|
|
'proposal_type', proposal_type,
|
|
'status', status,
|
|
'payload', payload,
|
|
'reviewed_by_handle', reviewed_by_handle,
|
|
'reviewed_by_agent_id', reviewed_by_agent_id::text,
|
|
'reviewed_at', case when reviewed_at is null then null else
|
|
to_char(reviewed_at at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') end,
|
|
'review_note', review_note,
|
|
'applied_by_handle', applied_by_handle,
|
|
'applied_by_agent_id', applied_by_agent_id::text,
|
|
'applied_at', case when applied_at is null then null else
|
|
to_char(applied_at at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') end)::text
|
|
from kb_stage.kb_proposals
|
|
where id = {sql_literal(args.proposal_id)}::uuid;"""
|
|
out = run_psql(args, sql, password).strip()
|
|
if not out:
|
|
raise SystemExit(f"proposal not found: {args.proposal_id}")
|
|
return json.loads(out)
|
|
|
|
|
|
def capture_replay_receipt(
|
|
args: argparse.Namespace,
|
|
proposal: dict[str, Any],
|
|
password: str,
|
|
*,
|
|
apply_sql: str | None = None,
|
|
) -> tuple[Path, dict[str, Any]]:
|
|
"""Read exact postflight rows and atomically write a private replay receipt."""
|
|
postflight_sql = replay_receipt.build_postflight_sql(proposal)
|
|
raw = run_psql(
|
|
args,
|
|
postflight_sql,
|
|
password,
|
|
redact_output_on_error=True,
|
|
).strip()
|
|
if not raw:
|
|
raise RuntimeError("postflight query returned no row")
|
|
canonical_rows = json.loads(raw)
|
|
if not isinstance(canonical_rows, dict):
|
|
raise RuntimeError("postflight query did not return a JSON object")
|
|
apply_sql_source = "exact_executed_sql"
|
|
if apply_sql is None:
|
|
apply_sql = build_apply_sql_for_args(proposal, args, proposal.get("applied_by_handle"))
|
|
apply_sql_source = "reconstructed_current_engine"
|
|
receipt = replay_receipt.build_replay_receipt(
|
|
proposal,
|
|
canonical_rows,
|
|
apply_sql_sha256=replay_receipt.sha256_text(apply_sql),
|
|
apply_sql_source=apply_sql_source,
|
|
)
|
|
path = replay_receipt.resolve_receipt_path(
|
|
str(proposal["id"]),
|
|
receipt_out=args.receipt_out,
|
|
receipt_dir=args.receipt_dir,
|
|
)
|
|
replay_receipt.write_private_receipt(path, receipt)
|
|
return path, receipt
|
|
|
|
|
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
p.add_argument("proposal_id", help="UUID of the approved proposal to apply")
|
|
p.add_argument(
|
|
"--applied-by",
|
|
default=SERVICE_AGENT_HANDLE,
|
|
help="agent handle recorded as applied_by_handle and resolved to the "
|
|
"applied_by_agent_id FK (default: the kb-apply service agent)",
|
|
)
|
|
p.add_argument("--dry-run", action="store_true", help="print the SQL, do not execute")
|
|
p.add_argument("--secrets-file", default=DEFAULT_SECRETS_FILE)
|
|
p.add_argument("--container", default=DEFAULT_CONTAINER)
|
|
p.add_argument("--db", default=DEFAULT_DB)
|
|
p.add_argument("--host", default=DEFAULT_HOST)
|
|
p.add_argument("--role", default=DEFAULT_ROLE)
|
|
p.add_argument(
|
|
"--expected-contract-version",
|
|
type=int,
|
|
choices=(2, 3),
|
|
default=None,
|
|
help="reassert one exact proposal contract before any mutating SQL or receipt recovery",
|
|
)
|
|
p.add_argument(
|
|
"--fresh-preflight",
|
|
type=Path,
|
|
help=(
|
|
"exact proposal_apply_fresh_preflight JSON; makes approve_claim use strict fresh-owned inserts "
|
|
"bound to the timestamped preflight"
|
|
),
|
|
)
|
|
p.add_argument(
|
|
"--receipt-only",
|
|
action="store_true",
|
|
help="recover the private row-level replay receipt for an already applied proposal; no writes",
|
|
)
|
|
p.add_argument(
|
|
"--receipt-dir",
|
|
default=None,
|
|
help=f"private receipt directory (default: KB_APPLY_RECEIPT_DIR or {replay_receipt.DEFAULT_RECEIPT_DIR})",
|
|
)
|
|
p.add_argument(
|
|
"--receipt-out",
|
|
default=None,
|
|
help="exact private receipt path; overrides --receipt-dir",
|
|
)
|
|
args = p.parse_args(argv)
|
|
if args.receipt_only and args.dry_run:
|
|
p.error("--receipt-only and --dry-run are mutually exclusive")
|
|
return args
|
|
|
|
|
|
def build_apply_sql_for_args(
|
|
proposal: dict[str, Any],
|
|
args: argparse.Namespace,
|
|
applied_by: str | None,
|
|
) -> str:
|
|
fresh_preflight_path = getattr(args, "fresh_preflight", None)
|
|
if fresh_preflight_path is None:
|
|
return build_apply_sql(proposal, applied_by)
|
|
if not fresh_preflight_path.is_file():
|
|
raise SystemExit(f"fresh preflight file not found: {fresh_preflight_path}")
|
|
try:
|
|
preflight = json.loads(fresh_preflight_path.read_text(encoding="utf-8"))
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
raise SystemExit(f"fresh preflight is unreadable: {fresh_preflight_path}") from exc
|
|
import proposal_apply_lifecycle as lifecycle
|
|
|
|
try:
|
|
return lifecycle.build_fresh_owned_apply_sql(proposal, preflight, applied_by)
|
|
except ValueError as exc:
|
|
raise SystemExit(f"fresh preflight refused: {exc}") from exc
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(sys.argv[1:] if argv is None else argv)
|
|
|
|
if args.receipt_only:
|
|
password = load_password(args.secrets_file)
|
|
proposal = load_proposal(args, password)
|
|
assert_expected_contract_version(proposal, args.expected_contract_version)
|
|
assert_receiptable(proposal)
|
|
path, receipt = capture_replay_receipt(args, proposal, password)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"proposal_id": proposal["id"],
|
|
"receipt_path": str(path),
|
|
"replay_material_sha256": receipt["hashes"]["replay_material_sha256"],
|
|
"replay_ready": receipt["replay_ready"],
|
|
},
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
if args.dry_run:
|
|
# Dry-run still needs the proposal to build SQL; read it as kb_apply.
|
|
password = load_password(args.secrets_file)
|
|
proposal = load_proposal(args, password)
|
|
assert_expected_contract_version(proposal, args.expected_contract_version)
|
|
assert_applyable(proposal)
|
|
print(build_apply_sql_for_args(proposal, args, args.applied_by))
|
|
return 0
|
|
|
|
password = load_password(args.secrets_file)
|
|
proposal = load_proposal(args, password)
|
|
assert_expected_contract_version(proposal, args.expected_contract_version)
|
|
assert_applyable(proposal)
|
|
sql = build_apply_sql_for_args(proposal, args, args.applied_by)
|
|
run_psql(args, sql, password)
|
|
applied_proposal = load_proposal(args, password)
|
|
try:
|
|
path, receipt = capture_replay_receipt(
|
|
args,
|
|
applied_proposal,
|
|
password,
|
|
apply_sql=sql,
|
|
)
|
|
except Exception as exc:
|
|
raise SystemExit(
|
|
f"proposal {proposal['id']} committed, but private replay receipt capture failed: {exc}. "
|
|
f"Recover without reapplying via: {Path(__file__).name} {proposal['id']} --receipt-only"
|
|
) from exc
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"applied": True,
|
|
"proposal_id": proposal["id"],
|
|
"proposal_type": proposal["proposal_type"],
|
|
"receipt_path": str(path),
|
|
"replay_material_sha256": receipt["hashes"]["replay_material_sha256"],
|
|
"replay_ready": receipt["replay_ready"],
|
|
},
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|