- Normalize rich proposals into atomic reviewed canonical graph bundles with exact row verification. - Harden reviewer/apply roles, upgrade ACLs, and prove generic plus Helmer lifecycles in isolated PostgreSQL. - Publish repo-native VPS/GCP/Cory skills and live-readonly GCP inventory without changing the live VPS runtime. `.agents/skills/crabbox/SKILL.md` `.agents/skills/teleo-gcp-parity-ops/SKILL.md` `.agents/skills/teleo-kb-db-change-workflow/SKILL.md` `.agents/skills/teleo-leo-onboarding/SKILL.md` `.agents/skills/teleo-vps-runtime-ops/SKILL.md` `.agents/skills/working-leo-cory-outcomes/SKILL.md` `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json` `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md` `docs/reports/leo-working-state-20260709/current-truth-index.md` `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.json` `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md` `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.json` `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.md` `docs/reports/leo-working-state-20260709/gcp-parallel-delegation-current.json` `docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json` `docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json` `docs/reports/leo-working-state-20260709/skill-pack-manifest.json` `docs/reports/leo-working-state-20260709/skill-pack-readme.md` `docs/reports/leo-working-state-20260709/working-leo-current-state-20260709.md` `docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md` `docs/reports/leo-working-state-20260709/working-leo-execution-plan-current.md` `scripts/apply_proposal.py` `scripts/apply_worker.py` `scripts/approve_proposal.py` `scripts/kb_apply_prereqs.sql` `scripts/kb_proposal_normalize.py` `scripts/probe_gcp_db_parity.py` `scripts/run_approve_claim_clone_canary.py` `scripts/run_approve_claim_isolated_container_canary.sh` `tests/test_apply_proposal.py` `tests/test_apply_worker.py` `tests/test_approve_proposal.py` `tests/test_kb_apply_prereqs.py` `tests/test_kb_proposal_normalize.py` `tests/test_kb_proposal_routes.py` `tests/test_probe_gcp_db_parity.py` `tests/test_repo_skill_pack.py`
934 lines
39 KiB
Python
934 lines
39 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 json
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
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"
|
|
|
|
# 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,
|
|
"reasoning_tools": 50,
|
|
}
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 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 _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 _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 _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 _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")
|
|
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:
|
|
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(approval['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."""
|
|
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(approval['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)
|
|
f = apply_payload.get("from_claim")
|
|
t = apply_payload.get("to_claim")
|
|
et = apply_payload.get("edge_type")
|
|
w = apply_payload.get("weight")
|
|
if not (f and t and et):
|
|
raise ValueError("add_edge apply_payload requires 'from_claim', 'to_claim', 'edge_type'")
|
|
if f == t:
|
|
raise ValueError("add_edge from_claim and to_claim must differ")
|
|
|
|
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 (from_claim, to_claim, edge_type, weight)
|
|
select {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""" 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)
|
|
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):
|
|
claim_id = ev.get("claim_id")
|
|
source_id = ev.get("source_id")
|
|
role = ev.get("role") or "grounds"
|
|
weight = ev.get("weight")
|
|
if not claim_id:
|
|
raise ValueError(f"evidence[{i}] requires 'claim_id'")
|
|
if not source_id:
|
|
raise ValueError(
|
|
f"evidence[{i}] requires an existing 'source_id'. "
|
|
"kb_apply cannot create public.sources; mint the source upstream first."
|
|
)
|
|
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""" 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_sql(
|
|
apply_payload: dict[str, Any],
|
|
proposal_id: str,
|
|
applied_by: str | None,
|
|
approval: dict[str, Any],
|
|
) -> str:
|
|
"""Build one conflict-safe transaction for a reviewed canonical graph bundle."""
|
|
_validate_approval_meta(approval, "approve_claim", apply_payload)
|
|
_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(
|
|
{
|
|
"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")
|
|
|
|
statements: list[str] = []
|
|
checks: list[str] = []
|
|
|
|
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"])
|
|
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
|
|
on conflict (id) do nothing;"""
|
|
)
|
|
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:
|
|
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
|
|
on conflict do nothing;"""
|
|
)
|
|
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:
|
|
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'])}
|
|
on conflict (id) do nothing;"""
|
|
)
|
|
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:
|
|
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
|
|
on conflict (claim_id, source_id, role) do nothing;"""
|
|
)
|
|
checks.append(
|
|
f""" 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']}"
|
|
)
|
|
statements.append(
|
|
f"""select pg_advisory_xact_lock(hashtextextended({sql_literal(edge_lock_key)}, 0));
|
|
insert into public.claim_edges (from_claim, to_claim, edge_type, weight, created_by)
|
|
select {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
|
|
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);"""
|
|
)
|
|
checks.append(
|
|
f""" 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))
|
|
|
|
|
|
def _wrap_txn(canonical_sql: str, ledger_sql: str, approval_guard_sql: str) -> str:
|
|
return (
|
|
"begin;\n"
|
|
"set local standard_conforming_strings = on;\n"
|
|
f"{approval_guard_sql}\n"
|
|
f"{canonical_sql}\n"
|
|
f"{ledger_sql}\n"
|
|
"commit;\n"
|
|
)
|
|
|
|
|
|
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 build_apply_sql(proposal: dict[str, Any], applied_by: str | None) -> str:
|
|
ptype = proposal["proposal_type"]
|
|
if ptype not in BUILDERS:
|
|
raise ValueError(f"apply_proposal cannot apply proposal_type {ptype!r}")
|
|
payload = proposal.get("payload") or {}
|
|
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."
|
|
)
|
|
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"
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# 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) -> str:
|
|
command = [
|
|
"docker", "exec", "-e", "PGPASSWORD", "-i", args.container,
|
|
"psql", "-U", args.role, "-h", args.host, "-d", args.db,
|
|
"-v", "ON_ERROR_STOP=1", "-At", "-q",
|
|
]
|
|
result = subprocess.run(
|
|
command, input=sql, text=True, capture_output=True,
|
|
env={"PGPASSWORD": password, "PATH": "/usr/bin:/bin:/usr/local/bin"},
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
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', reviewed_at::text,
|
|
'review_note', review_note)::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 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)
|
|
return p.parse_args(argv)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(sys.argv[1:] if argv is None else argv)
|
|
|
|
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_applyable(proposal)
|
|
print(build_apply_sql(proposal, args.applied_by))
|
|
return 0
|
|
|
|
password = load_password(args.secrets_file)
|
|
proposal = load_proposal(args, password)
|
|
assert_applyable(proposal)
|
|
sql = build_apply_sql(proposal, args.applied_by)
|
|
run_psql(args, sql, password)
|
|
print(f"applied proposal {proposal['id']} ({proposal['proposal_type']})")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|