#!/usr/bin/env python3 """Run a networkless PostgreSQL least-privilege lifecycle for Leo. The canary provisions the real guarded proposal/apply prerequisites and the real Observatory read role in a disposable PostgreSQL container. It then connects as the read principal, proves required retrieval, attempts forbidden database and role operations, compares catalog/data fingerprints, and verifies that the container and temporary work directory were removed. """ from __future__ import annotations import argparse import hashlib import json import os import secrets import shutil import subprocess import tempfile import time import uuid from datetime import datetime, timezone from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] APPLY_PREREQS_SQL = ROOT / "scripts" / "kb_apply_prereqs.sql" READ_ROLE_SQL = ROOT / "ops" / "observatory_read_role.sql" DATABASE = "teleo_canonical" IAM_USER = "sa-observatory-read-adapter@teleo-501523.iam" PARTICIPANT_HANDLE = "m3taversal" DEFAULT_IMAGE = "postgres:16-alpine" CANARY_LABEL = "leo-negative-permissions" BOOTSTRAP_SQL = r""" create role kb_gate_owner nologin inherit; create role kb_review login inherit; create role kb_apply login inherit; create schema kb_stage; create table public.agents ( id uuid primary key, handle text not null unique, kind text not null ); create table public.strategies ( id uuid primary key, agent_id uuid not null, active boolean not null default true ); create table public.strategy_nodes (id uuid primary key); create table public.claim_evidence ( id uuid primary key, claim_id uuid not null, source_id uuid not null, role text not null ); create table public.claim_edges (id uuid primary key); create table public.claims ( id uuid primary key, text text not null ); create table public.sources ( id uuid primary key, hash text not null ); create table public.reasoning_tools (id uuid primary key); create table public.private_notes ( id uuid primary key, note text not null ); insert into public.agents (id, handle, kind) values ('11111111-1111-1111-1111-111111111111', '__PARTICIPANT_HANDLE__', 'human'); create table kb_stage.kb_proposals ( id uuid primary key, proposal_type text not null, status text not null, payload jsonb not null, reviewed_by_handle text, reviewed_by_agent_id uuid, reviewed_at timestamptz, review_note text, applied_by_handle text, applied_by_agent_id uuid, applied_at timestamptz, updated_at timestamptz not null default now() ); create table kb_stage.kb_review_principals ( db_role name primary key, reviewed_by_handle text not null, reviewed_by_agent_id uuid not null references public.agents(id), active boolean not null default true, created_at timestamptz not null default now() ); create table kb_stage.kb_proposal_approvals ( proposal_id uuid primary key references kb_stage.kb_proposals(id), proposal_type text not null, payload jsonb not null, reviewed_by_handle text not null, reviewed_by_agent_id uuid not null references public.agents(id), reviewed_by_db_role name not null, reviewed_at timestamptz not null, review_note text not null ); insert into public.claims (id, text) values ( '22222222-2222-2222-2222-222222222222', 'Canonical Leo knowledge remains review-gated.' ); insert into public.sources (id, hash) values ('33333333-3333-3333-3333-333333333333', 'fixture-source-hash'); insert into public.private_notes (id, note) values ('44444444-4444-4444-4444-444444444444', 'not allowlisted'); insert into kb_stage.kb_proposals ( id, proposal_type, status, payload ) values ( '55555555-5555-5555-5555-555555555555', 'revise_strategy', 'pending_review', '{"apply_payload":{"agent_id":"11111111-1111-1111-1111-111111111111"}}' ); """ class CanaryError(RuntimeError): """A setup or verification step failed.""" def utc_now() -> str: return datetime.now(timezone.utc).isoformat() def canonical_sha256(value: Any) -> str: encoded = json.dumps(value, sort_keys=True, separators=(",", ":")).encode() return hashlib.sha256(encoded).hexdigest() def sql_literal(value: str) -> str: return "'" + value.replace("'", "''") + "'" def sql_identifier(value: str) -> str: return '"' + value.replace('"', '""') + '"' def run( command: list[str], *, input_text: str | None = None, check: bool = True, timeout: int = 60, ) -> subprocess.CompletedProcess[str]: completed = subprocess.run( command, input=input_text, text=True, capture_output=True, check=False, timeout=timeout, ) if check and completed.returncode != 0: stderr = completed.stderr.strip()[-2000:] raise CanaryError(f"command failed with exit {completed.returncode}: {stderr}") return completed def admin_psql(container: str, sql: str, *, exec_env: dict[str, str] | None = None) -> str: command = ["docker", "exec"] for name, value in sorted((exec_env or {}).items()): command.extend(["-e", f"{name}={value}"]) command.extend( [ "-i", container, "psql", "-X", "--set", "ON_ERROR_STOP=1", "-U", "postgres", "-d", DATABASE, "-At", ] ) return run(command, input_text=sql).stdout def user_psql( container: str, password: str, sql_commands: list[str], ) -> subprocess.CompletedProcess[str]: command = [ "docker", "exec", "-e", f"PGPASSWORD={password}", container, "psql", "-X", "--set", "ON_ERROR_STOP=1", "--set", "VERBOSITY=verbose", "-h", "127.0.0.1", "-U", IAM_USER, "-d", DATABASE, "-At", ] for sql in sql_commands: command.extend(["-c", sql]) return run(command, check=False) def parse_last_json(output: str) -> dict[str, Any]: for line in reversed(output.splitlines()): line = line.strip() if not line.startswith("{"): continue parsed = json.loads(line) if isinstance(parsed, dict): return parsed raise CanaryError("expected a JSON object in psql output") def sanitized_denial(completed: subprocess.CompletedProcess[str]) -> str: lines = [line.strip() for line in completed.stderr.splitlines() if line.strip()] for line in lines: if "ERROR:" in line or "FATAL:" in line: return line[:500] return (lines[-1] if lines else "no database denial was returned")[:500] def allowed_operation( operation_id: str, category: str, completed: subprocess.CompletedProcess[str], ) -> dict[str, Any]: return { "id": operation_id, "category": category, "expected": "allowed", "passed": completed.returncode == 0, "exit_code": completed.returncode, "readback": [line for line in completed.stdout.splitlines() if line], "error": sanitized_denial(completed) if completed.returncode != 0 else None, } def denied_operation( operation_id: str, category: str, completed: subprocess.CompletedProcess[str], expected_fragments: tuple[str, ...], ) -> dict[str, Any]: denial = sanitized_denial(completed) lowered = denial.lower() matched = next((fragment for fragment in expected_fragments if fragment.lower() in lowered), None) return { "id": operation_id, "category": category, "expected": "denied", "passed": completed.returncode != 0 and matched is not None, "exit_code": completed.returncode, "matched_denial": matched, "denial": denial, } def fingerprint(container: str) -> dict[str, Any]: principal = sql_literal(IAM_USER) query = f""" select jsonb_build_object( 'database', current_database(), 'participant_handle', ( select handle from public.agents where id = '11111111-1111-1111-1111-111111111111' ), 'principal', ( select jsonb_build_object( 'rolname', rolname, 'rolinherit', rolinherit, 'rolsuper', rolsuper, 'rolcreatedb', rolcreatedb, 'rolcreaterole', rolcreaterole, 'rolreplication', rolreplication, 'rolbypassrls', rolbypassrls ) from pg_catalog.pg_roles where rolname = {principal} ), 'memberships', coalesce(( select jsonb_agg( jsonb_build_object( 'role', granted.rolname, 'admin_option', membership.admin_option ) order by granted.rolname ) from pg_catalog.pg_auth_members membership join pg_catalog.pg_roles granted on granted.oid = membership.roleid join pg_catalog.pg_roles member on member.oid = membership.member where member.rolname = {principal} ), '[]'::jsonb), 'role_database_settings', coalesce(( select jsonb_agg(setting order by setting) from pg_catalog.pg_db_role_setting role_setting cross join lateral unnest(role_setting.setconfig) setting where role_setting.setrole = (select oid from pg_catalog.pg_roles where rolname = {principal}) and role_setting.setdatabase = (select oid from pg_catalog.pg_database where datname = current_database()) ), '[]'::jsonb), 'effective_privileges', jsonb_build_object( 'claims_select', pg_catalog.has_table_privilege({principal}, 'public.claims', 'SELECT'), 'claims_insert', pg_catalog.has_table_privilege({principal}, 'public.claims', 'INSERT'), 'proposals_select', pg_catalog.has_table_privilege({principal}, 'kb_stage.kb_proposals', 'SELECT'), 'proposals_insert', pg_catalog.has_table_privilege({principal}, 'kb_stage.kb_proposals', 'INSERT'), 'private_notes_select', pg_catalog.has_table_privilege({principal}, 'public.private_notes', 'SELECT'), 'public_schema_create', pg_catalog.has_schema_privilege({principal}, 'public', 'CREATE'), 'kb_stage_schema_create', pg_catalog.has_schema_privilege({principal}, 'kb_stage', 'CREATE'), 'approve_execute', pg_catalog.has_function_privilege( {principal}, 'kb_stage.approve_strict_proposal(uuid,text,jsonb,text,text)', 'EXECUTE' ), 'assert_execute', pg_catalog.has_function_privilege( {principal}, 'kb_stage.assert_approved_proposal(uuid,text,jsonb,text,uuid,timestamptz,text)', 'EXECUTE' ), 'finish_execute', pg_catalog.has_function_privilege( {principal}, 'kb_stage.finish_approved_proposal(uuid,text,jsonb,text,uuid,timestamptz,text,text)', 'EXECUTE' ) ), 'row_counts', jsonb_build_object( 'public.claims', (select count(*) from public.claims), 'public.sources', (select count(*) from public.sources), 'public.claim_evidence', (select count(*) from public.claim_evidence), 'public.claim_edges', (select count(*) from public.claim_edges), 'kb_stage.kb_proposals', (select count(*) from kb_stage.kb_proposals) ) )::text; """ return parse_last_json(admin_psql(container, query)) def wait_until_ready(container: str) -> None: ready_streak = 0 deadline = time.monotonic() + 30 while time.monotonic() < deadline: ready = run( ["docker", "exec", container, "pg_isready", "-U", "postgres", "-d", DATABASE], check=False, ) if ready.returncode == 0: ready_streak += 1 if ready_streak >= 2: return else: ready_streak = 0 time.sleep(0.25) raise CanaryError("disposable PostgreSQL did not become stably ready") def operation_suite(container: str, password: str) -> list[dict[str, Any]]: read_only_off = "set default_transaction_read_only = off" fake_payload = "'{\"apply_payload\":{}}'::jsonb" proposal_id = "'55555555-5555-5555-5555-555555555555'::uuid" reviewer_id = "'11111111-1111-1111-1111-111111111111'::uuid" reviewer_handle = sql_literal(PARTICIPANT_HANDLE) operations = [ allowed_operation( "session_read_only_defaults", "session", user_psql( container, password, [ "select jsonb_build_object(" "'default_transaction_read_only', current_setting('default_transaction_read_only'), " "'transaction_read_only', current_setting('transaction_read_only'))::text" ], ), ), allowed_operation( "canonical_retrieval", "retrieval", user_psql( container, password, [ "select jsonb_build_object('claim_count', count(*), 'sample_text', min(text))::text " "from public.claims" ], ), ), allowed_operation( "proposal_ledger_retrieval", "retrieval", user_psql( container, password, [ "select jsonb_build_object('proposal_count', count(*), 'statuses', " "jsonb_agg(status order by status))::text from kb_stage.kb_proposals" ], ), ), denied_operation( "canonical_insert_default_read_only", "canonical_dml", user_psql( container, password, ["insert into public.claims (id, text) values ('66666666-6666-6666-6666-666666666666', 'forbidden')"], ), ("read-only transaction",), ), denied_operation( "canonical_insert_acl_after_read_only_override", "canonical_dml", user_psql( container, password, [ read_only_off, "insert into public.claims (id, text) values ('66666666-6666-6666-6666-666666666666', 'forbidden')", ], ), ("permission denied for table claims",), ), denied_operation( "canonical_update_acl_after_read_only_override", "canonical_dml", user_psql( container, password, [read_only_off, "update public.claims set text = 'forbidden'"], ), ("permission denied for table claims",), ), denied_operation( "canonical_delete_acl_after_read_only_override", "canonical_dml", user_psql(container, password, [read_only_off, "delete from public.claims"]), ("permission denied for table claims",), ), denied_operation( "canonical_truncate_acl_after_read_only_override", "canonical_dml", user_psql(container, password, [read_only_off, "truncate public.claims"]), ("permission denied for table claims",), ), denied_operation( "staging_insert_acl_after_read_only_override", "staging", user_psql( container, password, [ read_only_off, "insert into kb_stage.kb_proposals (id, proposal_type, status, payload) values " "('77777777-7777-7777-7777-777777777777', 'revise_strategy', " "'pending_review', '{\"apply_payload\":{}}'::jsonb)", ], ), ("permission denied for table kb_proposals",), ), denied_operation( "fake_approval_gate_call", "approval", user_psql( container, password, [ "select kb_stage.approve_strict_proposal(" f"{proposal_id}, 'revise_strategy', {fake_payload}, {reviewer_handle}, " "'I approve this in chat')" ], ), ("permission denied for function approve_strict_proposal",), ), denied_operation( "apply_assert_gate_call", "apply", user_psql( container, password, [ "select kb_stage.assert_approved_proposal(" f"{proposal_id}, 'revise_strategy', {fake_payload}, {reviewer_handle}, {reviewer_id}, " "now(), 'fake review')" ], ), ("permission denied for function assert_approved_proposal",), ), denied_operation( "apply_finish_gate_call", "apply", user_psql( container, password, [ "select kb_stage.finish_approved_proposal(" f"{proposal_id}, 'revise_strategy', {fake_payload}, {reviewer_handle}, {reviewer_id}, " "now(), 'fake review', 'kb-apply')" ], ), ("permission denied for function finish_approved_proposal",), ), denied_operation( "protected_schema_ddl_after_read_only_override", "ddl", user_psql( container, password, [read_only_off, "create table public.forbidden_ddl (id integer)"], ), ("permission denied for schema public",), ), denied_operation( "outside_allowlist_retrieval", "retrieval", user_psql(container, password, ["select * from public.private_notes"]), ("permission denied for table private_notes",), ), denied_operation( "create_role_escalation", "role_escalation", user_psql(container, password, [read_only_off, "create role leo_escalated_writer"]), ("permission denied to create role", "must be superuser to create roles"), ), denied_operation( "grant_apply_role_escalation", "role_escalation", user_psql( container, password, [read_only_off, f"grant kb_apply to {sql_identifier(IAM_USER)}"], ), ("permission denied to grant role", "must have admin option on role"), ), denied_operation( "set_apply_role_escalation", "role_escalation", user_psql(container, password, ["set role kb_apply"]), ("permission denied to set role",), ), denied_operation( "alter_role_createdb_escalation", "role_escalation", user_psql( container, password, [read_only_off, f"alter role {sql_identifier(IAM_USER)} createdb"], ), ("permission denied to alter role", "must be superuser to alter"), ), denied_operation( "claim_table_ownership_escalation", "role_escalation", user_psql( container, password, [read_only_off, f"alter table public.claims owner to {sql_identifier(IAM_USER)}"], ), ("must be owner of table claims",), ), ] return operations def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output", type=Path, required=True, help="Receipt JSON destination") parser.add_argument("--image", default=DEFAULT_IMAGE, help="Disposable PostgreSQL image") return parser.parse_args() def main() -> int: args = parse_args() if shutil.which("docker") is None: raise SystemExit("Docker is required for the negative-permissions canary") run_id = uuid.uuid4().hex[:12] container = f"leo-negative-permissions-{run_id}" temp_dir = Path(tempfile.mkdtemp(prefix=f"{container}-")) password = secrets.token_urlsafe(24) started_at = utc_now() container_started = False execution_passed = False receipt: dict[str, Any] = { "artifact": "leo_negative_permissions_canary", "schema": "livingip.leo-negative-permissions.v1", "run_id": run_id, "started_at_utc": started_at, "required_tier": "T2_runtime", "current_tier": "T0_spec", "scope": { "database": DATABASE, "principal": IAM_USER, "container_network": "none", "production_contacted": False, "canonical_production_rows_contacted": False, }, "identity_policy": { "participant_handle": PARTICIPANT_HANDLE, "reviewer_handle": PARTICIPANT_HANDLE, "exact_handle_required": True, }, "operations": [], "passed": False, } try: repo_sha = run(["git", "rev-parse", "HEAD"], timeout=15).stdout.strip() docker_server = run(["docker", "version", "--format", "{{.Server.Version}}"], timeout=15).stdout.strip() image_id = run( ["docker", "image", "inspect", "--format", "{{.Id}}", args.image], timeout=15, ).stdout.strip() receipt["runtime"] = { "repo_git_sha": repo_sha, "postgres_image": args.image, "postgres_image_id": image_id, "docker_server_version": docker_server, } run( [ "docker", "run", "--detach", "--rm", "--name", container, "--network", "none", "--label", f"livingip.canary={CANARY_LABEL}", "--label", f"livingip.canary.instance={container}", "--tmpfs", "/var/lib/postgresql/data:rw,nosuid,nodev,size=512m", "--env", f"POSTGRES_PASSWORD={secrets.token_urlsafe(24)}", "--env", f"POSTGRES_DB={DATABASE}", args.image, ], timeout=30, ) container_started = True wait_until_ready(container) inspect = run( [ "docker", "inspect", "--format", "{{json .HostConfig.NetworkMode}}|{{json .Mounts}}|" '{{index .Config.Labels "livingip.canary"}}|' '{{index .Config.Labels "livingip.canary.instance"}}', container, ], ).stdout.strip() network_mode, mounts, canary_label, instance_label = inspect.split("|", 3) isolation = { "network_mode": json.loads(network_mode), "persistent_mounts": json.loads(mounts), "canary_label": canary_label, "instance_label": instance_label, "temporary_data_mount": "tmpfs:/var/lib/postgresql/data", } receipt["isolation"] = isolation if isolation != { "network_mode": "none", "persistent_mounts": [], "canary_label": CANARY_LABEL, "instance_label": container, "temporary_data_mount": "tmpfs:/var/lib/postgresql/data", }: raise CanaryError(f"disposable container isolation mismatch: {isolation}") admin_psql(container, BOOTSTRAP_SQL.replace("__PARTICIPANT_HANDLE__", PARTICIPANT_HANDLE)) admin_psql(container, APPLY_PREREQS_SQL.read_text(encoding="utf-8")) admin_psql( container, f"create role {sql_identifier(IAM_USER)} login inherit password {sql_literal(password)};", ) migration_output = admin_psql( container, READ_ROLE_SQL.read_text(encoding="utf-8"), exec_env={"OBSERVATORY_DB_IAM_USER": IAM_USER}, ) receipt["role_provisioning"] = parse_last_json(migration_output) before = fingerprint(container) operations = operation_suite(container, password) after = fingerprint(container) receipt["operations"] = operations receipt["fingerprint"] = { "before": before, "after": after, "before_sha256": canonical_sha256(before), "after_sha256": canonical_sha256(after), "unchanged": before == after, } receipt["summary"] = { "allowed_expected": sum(operation["expected"] == "allowed" for operation in operations), "allowed_passed": sum( operation["expected"] == "allowed" and operation["passed"] for operation in operations ), "denied_expected": sum(operation["expected"] == "denied" for operation in operations), "denied_passed": sum(operation["expected"] == "denied" and operation["passed"] for operation in operations), } execution_passed = all(operation["passed"] for operation in operations) and before == after except Exception as exc: receipt["execution_error"] = str(exc)[:2000] finally: remove = run(["docker", "rm", "--force", container], check=False, timeout=30) if temp_dir.exists(): shutil.rmtree(temp_dir) exact_name = run( [ "docker", "container", "ls", "--all", "--filter", f"name=^/{container}$", "--format", "{{.Names}}", ], check=False, timeout=15, ) label_scope = run( [ "docker", "container", "ls", "--all", "--filter", f"label=livingip.canary={CANARY_LABEL}", "--filter", f"label=livingip.canary.instance={container}", "--format", "{{.Names}}", ], check=False, timeout=15, ) cleanup = { "container_started": container_started, "remove_exit_code": remove.returncode, "exact_name_readback_exit_code": exact_name.returncode, "exact_name_orphans": [line for line in exact_name.stdout.splitlines() if line], "label_readback_exit_code": label_scope.returncode, "label_scoped_orphans": [line for line in label_scope.stdout.splitlines() if line], "temporary_workdir_exists": temp_dir.exists(), } cleanup["passed"] = ( (not container_started or remove.returncode == 0) and exact_name.returncode == 0 and not cleanup["exact_name_orphans"] and label_scope.returncode == 0 and not cleanup["label_scoped_orphans"] and not cleanup["temporary_workdir_exists"] ) receipt["cleanup"] = cleanup receipt["finished_at_utc"] = utc_now() receipt["current_tier"] = "T2_runtime" if execution_passed and cleanup["passed"] else "T0_spec" receipt["passed"] = execution_passed and cleanup["passed"] args.output.parent.mkdir(parents=True, exist_ok=True) temporary_output = args.output.with_name(f".{args.output.name}.{run_id}.tmp") temporary_output.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") os.replace(temporary_output, args.output) print( json.dumps( { "artifact": receipt["artifact"], "passed": receipt["passed"], "current_tier": receipt["current_tier"], "operations": receipt.get("summary"), "cleanup": receipt["cleanup"], "output": str(args.output), }, sort_keys=True, ) ) return 0 if receipt["passed"] else 1 if __name__ == "__main__": raise SystemExit(main())