Merge pull request #72 from living-ip/codex/working-leo-db-path-20260709
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
Add guarded canonical claim application
This commit is contained in:
commit
771679061f
37 changed files with 22266 additions and 388 deletions
|
|
@ -1,3 +1,8 @@
|
|||
---
|
||||
name: crabbox
|
||||
description: Use for remote Linux verification, isolated CI proof, Crabbox lease cleanup, and PR evidence without production deploys or secret forwarding.
|
||||
---
|
||||
|
||||
# Crabbox
|
||||
|
||||
Use Crabbox for remote Linux verification and PR proof only.
|
||||
|
|
|
|||
|
|
@ -1,85 +1,88 @@
|
|||
---
|
||||
name: teleo-gcp-parity-ops
|
||||
description: Use for Teleo/Leo GCP parity, Cloud SQL/cutover readiness, account selection, non-interactive auth blockers, and separating GCP from VPS Telegram proof.
|
||||
description: Use for Teleo GCP control-plane inventory, Cloud SQL read-only parity, authenticated Chrome routing, VM/runtime comparison, and exact GCP access gates without collapsing VPS proof into GCP proof.
|
||||
---
|
||||
|
||||
# Teleo GCP Parity Ops
|
||||
|
||||
## Job
|
||||
## Working Target
|
||||
|
||||
Handle GCP parity as its own lane, with precise auth/account/project facts and no overclaiming from VPS or local proof.
|
||||
Prove the real GCP resource, database, runtime, and Cory-answer paths separately.
|
||||
Control-plane inventory alone is not database or Leo parity.
|
||||
|
||||
## Trigger Phrases
|
||||
## Current Verified Inventory
|
||||
|
||||
- "GCP parity"
|
||||
- "move Leo to GCP"
|
||||
- "Cloud SQL Teleo"
|
||||
- "teleo-501523"
|
||||
- "GCP blocker"
|
||||
- "VPS then GCP"
|
||||
- Project: `teleo-501523` (`Teleo`).
|
||||
- Authenticated Console account: redacted `b***@livingip.xyz` via `authuser=5`.
|
||||
- VM: `teleo-prod-1`, running in `europe-west6-a`.
|
||||
- Cloud SQL: exactly one instance, `teleo-pgvector-standby`.
|
||||
- Database engine: PostgreSQL `16.14`, Enterprise.
|
||||
- Region/zone: `europe-west6` / `europe-west6-c`, single-zone.
|
||||
- Network: private-only `teleo-staging-net`; public IP disabled.
|
||||
- Connection name: `teleo-501523:europe-west6:teleo-pgvector-standby`.
|
||||
- Cloud SQL Studio loads, but the observed session exposes no enabled existing
|
||||
IAM DB principal and selects built-in password authentication.
|
||||
|
||||
## Current Known Facts
|
||||
Read these before refreshing:
|
||||
|
||||
- Project: `teleo-501523`
|
||||
- Expected project-access account: `billy@livingip.xyz`
|
||||
- Retained blocker: selected account cannot refresh access token non-interactively.
|
||||
- Other locally available accounts refresh but lack access to `teleo-501523`.
|
||||
- Current probe artifact: `docs/reports/leo-working-state-20260709/gcp-db-parity-current-probe-current.json`
|
||||
- Prior auth-capable blocker artifact: `docs/reports/leo-working-state-20260709/gcp-db-parity-current-blocker-20260709.json`
|
||||
- `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.json`
|
||||
- `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.json`
|
||||
- `docs/reports/leo-working-state-20260709/gcp-parallel-delegation-current.json`
|
||||
|
||||
The current probe may report `blocked_on_network_dns_before_auth_refresh` when the
|
||||
runner cannot resolve `oauth2.googleapis.com`; that is an environment/network
|
||||
gate before the prior Google account reauth or project-permission checks.
|
||||
## Browser Rule
|
||||
|
||||
Always refresh if cheap before making a current claim.
|
||||
Use authenticated Chrome/Computer Use for Console-only facts when CLI auth is
|
||||
stale. Never use AppleScript, `osascript`, JXA, `open`, System Events, or a
|
||||
focus-stealing fallback. Do not inspect cookies, tokens, saved passwords, or
|
||||
secret values.
|
||||
|
||||
## Status Split
|
||||
## Required Parity Rows
|
||||
|
||||
Never claim GCP complete from:
|
||||
Track each independently:
|
||||
|
||||
- VPS service health,
|
||||
- Telegram group canaries,
|
||||
- local DB rehearsals,
|
||||
- GCP account selection alone.
|
||||
1. control-plane project/VM/Cloud SQL inventory;
|
||||
2. database name, principal, transaction-read-only proof, schema, counts, and
|
||||
selected row identities;
|
||||
3. VM process/service/repo/profile/model/bot configuration;
|
||||
4. no-post `DC-01` through `DC-06` replies and score;
|
||||
5. cleanup and no-mutation readback.
|
||||
|
||||
GCP parity needs its own:
|
||||
## Cloud SQL Read-Only Query
|
||||
|
||||
- auth readback,
|
||||
- project access readback,
|
||||
- Cloud SQL or target DB readback,
|
||||
- runtime/config parity readback,
|
||||
- Telegram or operator path proof if the target is GCP-hosted Leo.
|
||||
Only after Studio is already authenticated with an existing principal:
|
||||
|
||||
## Safe Actions
|
||||
|
||||
Allowed:
|
||||
|
||||
- `gcloud config list` / account list readbacks,
|
||||
- `scripts/probe_gcp_db_parity.py`,
|
||||
- non-secret auth status checks,
|
||||
- project access probes,
|
||||
- read-only SQL or service discovery where auth works,
|
||||
- blocker artifact updates.
|
||||
|
||||
Stop for:
|
||||
|
||||
- password/OTP/KYC,
|
||||
- raw secret requests,
|
||||
- irreversible cloud changes,
|
||||
- production cutover without explicit authorization.
|
||||
|
||||
## Blocker Format
|
||||
|
||||
If auth is still blocked, retain:
|
||||
|
||||
```json
|
||||
{
|
||||
"current_canary": "gcloud account can read teleo-501523 and target DB metadata",
|
||||
"attempted_routes": [],
|
||||
"exact_gate": "",
|
||||
"clear_CTA": "",
|
||||
"next_non_user_action": ""
|
||||
}
|
||||
```sql
|
||||
begin transaction read only;
|
||||
select current_database(), current_user,
|
||||
current_setting('transaction_read_only');
|
||||
select count(*) from public.claims;
|
||||
select count(*) from public.sources;
|
||||
select count(*) from public.claim_evidence;
|
||||
select count(*) from public.claim_edges;
|
||||
select count(*) from kb_stage.kb_proposals;
|
||||
rollback;
|
||||
```
|
||||
|
||||
The CTA must name the exact account and what the operator should do, not a vague "reauth needed."
|
||||
Do not submit a raw password, create a user, change IAM, enable a flag, open
|
||||
browser SSH that creates a key, or mutate infrastructure under this read-only
|
||||
skill.
|
||||
|
||||
## Exact Current Gate
|
||||
|
||||
Database parity is unproven because Studio requires a database credential and
|
||||
did not expose an enabled existing IAM principal. The clear CTA is:
|
||||
|
||||
```text
|
||||
Authenticate Cloud SQL Studio with an already-existing authorized database
|
||||
principal without sharing the credential, then tell Codex: GCP Studio authenticated.
|
||||
```
|
||||
|
||||
Otherwise route IAM/database-user enablement to a separately authorized write
|
||||
window. Once authenticated, run only the read-only transaction above, retain
|
||||
sanitized results, and close the agent-created tab.
|
||||
|
||||
## Claim Ceiling
|
||||
|
||||
Current proof covers the GCP project, VM, Cloud SQL instance, health, version,
|
||||
and private routing surface. It does not cover DB contents, VPS-vs-GCP schema or
|
||||
row parity, application connectivity, GCP Leo behavior, or cutover.
|
||||
|
|
|
|||
|
|
@ -1,79 +1,103 @@
|
|||
---
|
||||
name: teleo-kb-db-change-workflow
|
||||
description: Use for Teleo KB proposal normalization, approved-to-canonical DB change workflows, clone rehearsals, apply packets, rollback plans, and row-level proof without silently mutating production.
|
||||
description: Use for Teleo canonical Postgres changes, proposal normalization, separate review and apply authority, isolated approve_claim canaries, row-level proof, rollback, and production-apply gating.
|
||||
---
|
||||
|
||||
# Teleo KB DB Change Workflow
|
||||
|
||||
## Job
|
||||
## Working Target
|
||||
|
||||
Turn approved KB proposal intent into a strict, reviewable, rehearsed, row-level DB change contract.
|
||||
|
||||
## Trigger Phrases
|
||||
|
||||
- "manipulate the knowledge base"
|
||||
- "approved proposal apply"
|
||||
- "canonical DB rows"
|
||||
- "kb_stage to public"
|
||||
- "claim/source contract"
|
||||
- "rollback rehearsal"
|
||||
- "production apply packet"
|
||||
Move one reviewed knowledge change from `kb_stage.kb_proposals` to exact
|
||||
canonical rows without confusing approval with application or silently changing
|
||||
production.
|
||||
|
||||
## State Model
|
||||
|
||||
Use these states exactly:
|
||||
- `pending_review`: staged, not approved, canonical rows unchanged.
|
||||
- `approved`: reviewed intent exists, but `applied_at` may still be null.
|
||||
- `applied`: the guarded transaction finished and canonical postflight matches.
|
||||
- `packet_ready_not_executed`: rehearsed artifacts exist; production is unchanged.
|
||||
|
||||
- `proposed`: draft or staged proposal text exists.
|
||||
- `pending_review`: proposal exists but is not approved.
|
||||
- `approved`: reviewer approved intent; canonical DB may still be unchanged.
|
||||
- `applied`: canonical row mutation happened and was read back.
|
||||
- `not_applied`: no canonical mutation exists.
|
||||
- `packet_ready_not_executed`: SQL packet and rehearsal proof exist, but production was not changed.
|
||||
Never infer `applied` from chat text, a packet, a clone, or `status=approved`.
|
||||
|
||||
## Current Table Contract
|
||||
## Canonical Contract
|
||||
|
||||
Core canonical tables used in July 9 work:
|
||||
The strict `approve_claim` v2 payload may create:
|
||||
|
||||
- `public.claims`
|
||||
- `public.sources`
|
||||
- `public.claim_edges`
|
||||
- `public.claim_evidence`
|
||||
- `public.claim_edges`
|
||||
- `public.reasoning_tools`
|
||||
|
||||
Staging table:
|
||||
The lifecycle is split across:
|
||||
|
||||
- `kb_stage.kb_proposals`
|
||||
- `scripts/kb_proposal_normalize.py`: fail-closed rich intent to strict payload.
|
||||
- `scripts/approve_proposal.py`: `kb_review` login approves the exact type/payload.
|
||||
- `scripts/apply_proposal.py`: operator-only `kb_apply` login writes and verifies.
|
||||
- `scripts/kb_apply_prereqs.sql`: roles, immutable approval row, gate functions,
|
||||
ownership, indexes, and exact ACL matrix.
|
||||
- `scripts/run_approve_claim_isolated_container_canary.sh`: disposable runtime
|
||||
proof plus read-only live count/service endpoints.
|
||||
|
||||
## Required Workflow
|
||||
## Authority Invariants
|
||||
|
||||
1. Fetch proposal and status from live DB.
|
||||
2. Normalize proposal payload into planned rows.
|
||||
3. Preserve reviewer/caveat text as source/evidence/proposal body context.
|
||||
4. Reject or block unsupported edge types, unknown targets, and schema gaps unless a reviewed mapping overlay exists.
|
||||
5. Generate rollback-only SQL first.
|
||||
6. Run live rollback rehearsal in a transaction.
|
||||
7. Run disposable clone commit rehearsal.
|
||||
8. Generate production packet only after clone proof passes.
|
||||
9. Do not execute production commit unless explicitly authorized.
|
||||
10. If production commit is authorized later, run preflight, commit, postflight, and service stability readback.
|
||||
1. `kb_review` and `kb_apply` are separate `NOINHERIT` login roles with no role
|
||||
memberships.
|
||||
2. `kb_gate_owner` is `NOLOGIN` and owns the immutable approval table and three
|
||||
`SECURITY DEFINER` gate functions.
|
||||
3. The legacy three-argument approval overload must be absent.
|
||||
4. Review binds DB role, reviewer principal, type, full payload, timestamp, and
|
||||
note in one immutable row.
|
||||
5. Apply locks and compares that snapshot before writes, verifies every
|
||||
payload-controlled field and exact table deltas, then finishes the ledger.
|
||||
6. Existing semantic evidence/edge rows must match approved weights and owners;
|
||||
a mismatch rolls back.
|
||||
7. `kb_apply` remains a trusted operator-only canonical writer. This design does
|
||||
not claim resistance to a compromised apply credential.
|
||||
|
||||
## Current Important Proposals
|
||||
## Operator Path
|
||||
|
||||
- `00957f6c-9883-4015-95a4-6b09367efb0e`: strict `add_edge` canary, applied.
|
||||
- `8dfedb3f-3aa4-4200-970f-4c0016f6869f`: live Telegram staged proposal, pending review at the retained snapshot.
|
||||
- `14fa5ecc-ac7a-41c1-807d-a2e85b936617`: approved rich proposal, mapped subset packet-ready, not production-applied.
|
||||
- `ac036c9d-20a0-4ffe-881f-57d6b7bacf22`: approved rich proposal, mapped subset packet-ready, not production-applied.
|
||||
- `a64df080-8502-42e2-98f4-9bbdecb8da73`: approved Helmer 7 Powers proposal, plan generated for reasoning tool plus claims/sources/edges; continue with rehearsal/packet proof.
|
||||
|
||||
## Claim Ceiling
|
||||
|
||||
Clone proof is not production apply. A packet file is not a DB change. Production apply requires explicit authority and postflight row readback.
|
||||
|
||||
## Validation
|
||||
|
||||
Run focused tests after planner/packet changes:
|
||||
1. Read the proposal and canonical target rows.
|
||||
2. Normalize rich intent; reject malformed, duplicate, lossy, or unsupported
|
||||
candidates instead of guessing.
|
||||
3. Run focused tests:
|
||||
|
||||
```bash
|
||||
uv run --extra dev ruff check scripts/kb_rich_proposal_creation_plan.py scripts/kb_rich_proposal_apply_packet.py tests/test_kb_rich_proposal_creation_plan.py tests/test_kb_rich_proposal_apply_packet.py
|
||||
uv run --extra dev pytest tests/test_kb_rich_proposal_creation_plan.py tests/test_kb_rich_proposal_apply_packet.py
|
||||
.venv/bin/python -m pytest -q \
|
||||
tests/test_kb_proposal_normalize.py \
|
||||
tests/test_approve_proposal.py \
|
||||
tests/test_apply_proposal.py \
|
||||
tests/test_kb_apply_prereqs.py
|
||||
```
|
||||
|
||||
4. Run both disposable canaries from the repo root:
|
||||
|
||||
```bash
|
||||
scripts/run_approve_claim_isolated_container_canary.sh \
|
||||
--output docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json
|
||||
|
||||
scripts/run_approve_claim_isolated_container_canary.sh \
|
||||
--normalization-json docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json \
|
||||
--output docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json
|
||||
```
|
||||
|
||||
5. Require `status=pass`, every check true, expected deltas equal measured
|
||||
deltas, source hashes matching current files, zero live count deltas, stable
|
||||
service endpoints, and independent clone/container/workdir cleanup.
|
||||
6. Treat production deployment, permission migration, worker enablement, and a
|
||||
production proposal apply as separate authorization windows.
|
||||
|
||||
## Current Proof And Claim Ceiling
|
||||
|
||||
The retained generic and Helmer v3 receipts each pass `37/37`. Generic proves
|
||||
`2/2/2/1/1`; Helmer proves `5/13/17/7/1`. Live read-only endpoint counts stayed
|
||||
`1837/4145/4670/4916/17/26`, and the gateway stayed on PID `2999690` with zero
|
||||
restarts during those runs.
|
||||
|
||||
This proves current-source behavior in disposable PostgreSQL and unchanged live
|
||||
count/service endpoints. It does not prove byte-for-byte live content, code
|
||||
deployment, the production permission migration, or Helmer application.
|
||||
|
||||
Read `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md`
|
||||
and both JSON receipts before making a current claim.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ Orient the worker before action. Build a current, proof-linked understanding of
|
|||
- Leo is the operator-facing agent expected to answer in Telegram, remember operator context, reason from canonical KB state, stage concrete KB changes, and support approved changes becoming canonical DB rows with proof.
|
||||
- The immediate July 9 issue is not generic bot liveness. It is Cory/m3taversal's expectation that approved KB changes move beyond proposal state when appropriate.
|
||||
- The VPS is the currently proven Telegram-visible Leo surface.
|
||||
- GCP parity is a separate lane and is not proven current until the GCP blocker clears.
|
||||
- GCP is a separate lane. Project, VM, and private Cloud SQL inventory are now
|
||||
current; DB contents, runtime parity, Cory answers, and cutover are not.
|
||||
|
||||
## Read First
|
||||
|
||||
|
|
@ -34,6 +35,9 @@ From the repo root, read:
|
|||
2. `docs/reports/leo-working-state-20260709/operator-surface-map.md`
|
||||
3. `docs/reports/leo-working-state-20260709/working-leo-definition-20260709.md`
|
||||
4. `docs/reports/leo-working-state-20260709/cory-expected-working-leo-outcomes-20260709.md`
|
||||
5. `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md`
|
||||
6. `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md`
|
||||
7. `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.md`
|
||||
|
||||
## Required Status Split
|
||||
|
||||
|
|
@ -55,6 +59,9 @@ Do not collapse GCP demo readiness into Telegram completion. Do not collapse pro
|
|||
- Do not production-apply DB packets unless explicitly authorized.
|
||||
- Do not expose secret contents.
|
||||
- Do not treat an old summary as current if a fresh readback is cheap.
|
||||
- Do not call GCP blocked on CLI login when authenticated Chrome can answer the
|
||||
control-plane question. Conversely, do not submit a raw DB password or create
|
||||
IAM/users under a read-only lane.
|
||||
|
||||
## Output Format
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ Navigate and verify the VPS safely, with exact readbacks and no surprise live-ru
|
|||
- Service: `leoclean-gateway.service`
|
||||
- DB container: `teleo-pg`
|
||||
- DB: `teleo`
|
||||
- Last retained count endpoints: claims `1837`, sources `4145`, evidence `4670`,
|
||||
edges `4916`, reasoning tools `17`, proposals `26`. Treat these as stale until
|
||||
refreshed.
|
||||
- Profile reports: `/home/teleo/.hermes/profiles/leoclean/kb_stage/reports/`
|
||||
- Deploy/source area: `/opt/teleo-eval/workspaces/deploy-infra`
|
||||
|
||||
|
|
@ -38,6 +41,7 @@ systemctl show leoclean-gateway.service -p ActiveState -p SubState -p MainPID -p
|
|||
```
|
||||
|
||||
Before claiming DB state, use `docker exec teleo-pg psql -U postgres -d teleo` and query exact tables/rows.
|
||||
Wrap verification queries in `begin transaction read only; ... rollback;`.
|
||||
|
||||
Before claiming cleanup, query disposable DBs:
|
||||
|
||||
|
|
@ -76,6 +80,19 @@ For DB rehearsals:
|
|||
7. Verify production counts stayed unchanged.
|
||||
8. Verify the disposable DB no longer exists.
|
||||
|
||||
For the guarded claim-bundle lifecycle, prefer the retained isolated wrapper:
|
||||
|
||||
```bash
|
||||
scripts/run_approve_claim_isolated_container_canary.sh \
|
||||
--output docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json
|
||||
```
|
||||
|
||||
It must use a disposable unexposed PostgreSQL container, bind its receipt to
|
||||
current source hashes, compare exact payload projections and table deltas, read
|
||||
live count/service endpoints without writing them, and independently prove the
|
||||
container/workdir are absent afterward. Do not install the candidate code into
|
||||
the live Leo deploy merely to run this canary.
|
||||
|
||||
## Blocker Format
|
||||
|
||||
Do not say "blocked" without:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: working-leo-cory-outcomes
|
||||
description: Use when defining, testing, or repairing Leo against Cory/m3taversal's expected outcomes: Telegram memory, critical reasoning, canonical KB truth, proposed-vs-approved-vs-applied state, and guarded DB manipulation.
|
||||
description: "Use when defining, testing, or repairing Leo against Cory/m3taversal's expected outcomes: Telegram memory, critical reasoning, canonical KB truth, proposed-vs-approved-vs-applied state, and guarded DB manipulation."
|
||||
---
|
||||
|
||||
# Working Leo / Cory Outcomes
|
||||
|
|
@ -32,6 +32,9 @@ A working Leo is a Telegram-facing agent that:
|
|||
8. Retains Cory's caveats and review notes in source/evidence/proposal rows.
|
||||
9. Produces before/after table-level proof and service stability readback.
|
||||
10. Survives an intentional `leoclean-gateway.service` restart: active before, active after, canonical KB counts unchanged, and a no-post handler smoke still answers.
|
||||
11. For a reviewed graph bundle, produces exact payload-controlled row
|
||||
projections, exact table deltas, source-byte binding, and cleanup proof in a
|
||||
disposable runtime before any production permission or apply window.
|
||||
|
||||
## Current Benchmark Cases
|
||||
|
||||
|
|
@ -41,6 +44,27 @@ A working Leo is a Telegram-facing agent that:
|
|||
- Staged write canary: proposal `8dfedb3f-3aa4-4200-970f-4c0016f6869f`, status `pending_review`, with no new public canonical rows after the test timestamp.
|
||||
- Open-ended triage canary: `WL-LIVE-TG-CORY-OPEN-20260709`, where Leo inferred the likely failure mode from "agents not working / same state as last night" and explained the proposed, approved, and applied split without exact IDs.
|
||||
- Old rich proposal packets: `14fa5ecc...`, `ac036c9d...`, and `a64df080...` are not to be silently production-applied.
|
||||
- Guarded apply proof: both generic and real Helmer v3 receipts pass `37/37` in
|
||||
disposable PostgreSQL; production Helmer remains unapplied.
|
||||
|
||||
## Cory Direct Questions
|
||||
|
||||
For vague or no-context questions, answer directly and then name the one action
|
||||
that would change the proof:
|
||||
|
||||
- "Did the DB change?": split applied rows, approved-but-unapplied proposals,
|
||||
pending rows, and canceled rows. Say `Approved is not the same as applied`.
|
||||
- "Is Helmer in Leo?": say `no, not canonical` until the exact claims, sources,
|
||||
evidence, edges, reasoning tool, and applied ledger read back from production.
|
||||
- "Did the decision matrix approve it?": verify matrix tables first; reviewer
|
||||
approval is not a matrix vote.
|
||||
- "Are document pointers the blocker?": answer `not just pointer mismatch` and
|
||||
separate files, source refs, canonical source rows, review, and apply.
|
||||
- "Can I demo KB mutation?": split staging-demo truth from canonical-apply truth.
|
||||
- "Did editing SOUL.md change identity?": no canonical identity change without
|
||||
row IDs plus render/sync postflight.
|
||||
|
||||
End no-context answers with exactly one `Next Cory-style follow-up:` line.
|
||||
|
||||
## Required Answer Discipline
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,88 @@
|
|||
# Approved Claim Bundle Isolation Canary
|
||||
|
||||
Status: `pass`
|
||||
|
||||
## Working Target
|
||||
|
||||
A Cory-approved strict bundle must move through a separate reviewer credential,
|
||||
an immutable approval snapshot, and a narrow apply credential before exact
|
||||
claim/source/evidence/edge/reasoning-tool rows become canonical. A stale payload,
|
||||
replay, hash collision, or direct privilege bypass must leave no partial rows.
|
||||
|
||||
## Final Runtime Proof
|
||||
|
||||
- Generic bundle: `37/37` checks passed. Its exact payload-controlled projection
|
||||
and exact table deltas were `2` claims, `2` sources, `2` evidence links, `1`
|
||||
edge, and `1` reasoning tool.
|
||||
- Real Helmer bundle: `37/37` checks passed. Its exact payload-controlled
|
||||
projection and exact table deltas were `5` claims, `13` sources, `17`
|
||||
evidence links, `7` edges, and `1` reasoning tool.
|
||||
- Helmer source proposal: `a64df080-8502-42e2-98f4-9bbdecb8da73`; strict child
|
||||
proposal in the final isolated run: `87344d36-5005-5d88-a04c-067aeceda3c3`.
|
||||
- `kb_review` approved the exact pending type and full JSON payload as mapped
|
||||
reviewer `m3ta`; the immutable approval row matched the proposal ledger.
|
||||
- `kb_apply` could not directly update proposal status/payload, immutable
|
||||
approval rows, or existing `claim_evidence` rows.
|
||||
- Mutating the approved ledger after SQL generation made the stale apply fail
|
||||
before canonical writes; restoring the ledger left the immutable approval
|
||||
unchanged.
|
||||
- Replay was refused. A different source ID reusing an existing source hash was
|
||||
refused, and its proposal/approval remained intact with zero partial rows.
|
||||
- Every payload-controlled persisted field, including weights, roles, owners,
|
||||
tags, hashes, and relationships, matched the reviewed payload. Exact table
|
||||
deltas also exclude unexpected extra rows under unrelated IDs.
|
||||
- The migration readback proves one expected signature per gate function, no
|
||||
legacy overload, no protected-role membership, no object owned by a login
|
||||
role, and the exact reviewer/apply execute split.
|
||||
|
||||
## Isolation And Cleanup
|
||||
|
||||
- Both runs used a new unexposed `postgres:16` container built from a read-only
|
||||
schema/reference copy of live `teleo`; no cluster-wide role was created in the
|
||||
production `teleo-pg` instance.
|
||||
- The wrapper issued read-only queries to live `teleo-pg`; endpoint counts were
|
||||
identical before and after each final run, with zero deltas for all six tables:
|
||||
`claims=1837`, `sources=4145`, `claim_evidence=4670`, `claim_edges=4916`,
|
||||
`reasoning_tools=17`, `kb_proposals=26`. This is endpoint-count evidence, not
|
||||
a claim that every live row byte was compared.
|
||||
- `leoclean-gateway.service` stayed `active/running`, `MainPID=2999690`,
|
||||
`NRestarts=0`.
|
||||
- Inner clone databases, outer Postgres containers, temporary workdirs, and the
|
||||
remote source bundle were removed. Independent name/path readbacks found no
|
||||
residue after cleanup.
|
||||
- Both receipts retain repo-relative SHA-256/size bindings for the apply,
|
||||
approval, prerequisite SQL, inner canary, and isolation wrapper source files;
|
||||
a local recomputation matched every retained hash.
|
||||
- No Telegram message, production canonical apply, production permission
|
||||
migration, service restart, or runtime-profile write occurred.
|
||||
|
||||
## Reproduce
|
||||
|
||||
Generic bundle:
|
||||
|
||||
```bash
|
||||
scripts/run_approve_claim_isolated_container_canary.sh \
|
||||
--output docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json
|
||||
```
|
||||
|
||||
Real normalized Helmer bundle:
|
||||
|
||||
```bash
|
||||
scripts/run_approve_claim_isolated_container_canary.sh \
|
||||
--normalization-json docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json \
|
||||
--output docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json
|
||||
```
|
||||
|
||||
## Evidence And Claim Ceiling
|
||||
|
||||
- Generic receipt: `approve-claim-clone-canary-current.json`
|
||||
- Helmer receipt: `helmer-approve-claim-clone-canary-current.json`
|
||||
- Lossless normalization: `helmer-approve-claim-normalization-current.json`
|
||||
- Inner harness: `scripts/run_approve_claim_clone_canary.py`
|
||||
- Isolation wrapper: `scripts/run_approve_claim_isolated_container_canary.sh`
|
||||
|
||||
This proves the repo-owned review/apply lifecycle at isolated runtime tier. It
|
||||
does not mean the new code, reviewer/apply roles, or permission migration are
|
||||
deployed on VPS production, and it does not mean Helmer is canonical there.
|
||||
`kb_apply` remains an operator-only trusted canonical writer; the proof covers
|
||||
the supplied tool and privilege matrix, not a compromised apply credential.
|
||||
|
|
@ -55,6 +55,12 @@ Use this file before making status claims. Prefer fresh VPS/GCP readbacks when c
|
|||
|
||||
- Strict apply plan: `docs/reports/leo-working-state-20260709/kb-apply-canary-plan-current.md`
|
||||
- Strict apply execute: `docs/reports/leo-working-state-20260709/kb-apply-canary-execute-current.md`
|
||||
- Approved claim-bundle isolation canary: `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md`
|
||||
- Generic approved claim-bundle isolation receipt: `docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json`
|
||||
- Helmer approved claim-bundle isolation receipt: `docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json`
|
||||
- Lossless Helmer strict normalization: `docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json`
|
||||
- Approved claim-bundle lifecycle harness: `scripts/run_approve_claim_clone_canary.py`
|
||||
- Approved claim-bundle isolated-container wrapper: `scripts/run_approve_claim_isolated_container_canary.sh`
|
||||
- Claim/source preview: `docs/reports/leo-working-state-20260709/claim-source-contract-preview-current.md`
|
||||
- Mapped rich proposal plan: `docs/reports/leo-working-state-20260709/rich-proposal-creation-plan-mapped-current.md`
|
||||
- Mapped production apply packet, not executed in production: `docs/reports/leo-working-state-20260709/production-apply-packet-current.md`
|
||||
|
|
@ -113,6 +119,10 @@ Use this file before making status claims. Prefer fresh VPS/GCP readbacks when c
|
|||
|
||||
- Current GCP parity probe: `docs/reports/leo-working-state-20260709/gcp-db-parity-current-probe-current.md`
|
||||
- Current GCP parity probe JSON: `docs/reports/leo-working-state-20260709/gcp-db-parity-current-probe-current.json`
|
||||
- Current GCP Cloud SQL live inventory: `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md`
|
||||
- Current GCP Cloud SQL live inventory JSON: `docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.json`
|
||||
- Current GCP Cloud SQL Studio parity gate: `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.md`
|
||||
- Current GCP Cloud SQL Studio parity gate JSON: `docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.json`
|
||||
- Prior auth-capable GCP blocker: `docs/reports/leo-working-state-20260709/gcp-db-parity-current-blocker-20260709.json`
|
||||
|
||||
## Images
|
||||
|
|
@ -148,6 +158,7 @@ Use this file before making status claims. Prefer fresh VPS/GCP readbacks when c
|
|||
- The public Argus KB diagnostics HTTP probe passes from live `curl` payloads captured at `2026-07-10T02:47:53Z`: `3` approved proposals are visible (`14fa5ecc`, `ac036c9d`, `a64df080`), all are `approved_needs_apply_payload`, `worker_applyable_count=0`, and the endpoint is read-only. This supports Leo's expected answer that the approved KB work is not yet directly applyable/canonical, but it is not Docker/Postgres row-count or gateway-service proof.
|
||||
- The highest-risk direct-claim cases are now green at handler level: `DC-02` recognizes approved Helmer staging row `a64df080-8502-42e2-98f4-9bbdecb8da73` as approved/unapplied, `DC-03` says the decision-matrix path is not shipped while matrix tables are absent, `DC-04` avoids a single-cause document pointer diagnosis, `DC-05` separates staging demo from authorized canonical apply, and `DC-06` separates `SOUL.md` runtime edits from canonical DB identity.
|
||||
- A strict canonical `add_edge` apply canary is live-proven.
|
||||
- The repo-owned normal apply path now supports an atomic, insert-only `approve_claim` bundle behind separate `kb_review` and `kb_apply` credentials, a DB-role-to-reviewer principal mapping, and an immutable approval snapshot. Final source-bound isolated-container receipts pass `37/37` for both the generic bundle (`2` claims, `2` sources, `2` evidence rows, `1` edge, `1` reasoning tool) and the real losslessly normalized Helmer bundle (`5/13/17/7/1`). They prove exact payload-controlled projections and table deltas, reviewer/apply separation, denied direct proposal/evidence mutation, stale-payload refusal before canonical writes, replay refusal, source-hash rollback, exact agent/external-claim seeding, hardened role/function/ACL state, and independent inner/outer cleanup. Read-only live count endpoints stayed `1837/4145/4670/4916/17/26` and the gateway stayed PID `2999690`/`NRestarts=0`; this is not a byte-for-byte live-row comparison. The code and production permission migration are not deployed, and Helmer remains unapplied on production.
|
||||
- Helmer 7 Powers is full-coverage packet and ledger clone-proven, not production-applied.
|
||||
- Mapped rich approved proposal packet is clone/rehearsal-proven for the supported subset, not production-applied.
|
||||
- The strategy-anchor companion write for mapped rich proposal `14fa5ecc` is clone-proven and reversible, not production-applied.
|
||||
|
|
@ -170,4 +181,4 @@ Use this file before making status claims. Prefer fresh VPS/GCP readbacks when c
|
|||
- The live VPS runtime behavior should not be changed while the DB apply path is stabilized.
|
||||
- Latest cleanup readback found no leftover rehearsal/clone/staging database names on the VPS Postgres container and did not mutate production DB or post to Telegram.
|
||||
- A live Telegram regression canary should be rerun after any production apply; no production apply has been authorized or executed.
|
||||
- GCP parity remains blocked by account auth/access facts until a fresh readback proves otherwise.
|
||||
- GCP control-plane Chrome readback now proves project `teleo-501523`, running VM `teleo-prod-1` in `europe-west6-a`, and one available private-only PostgreSQL `16.14` Cloud SQL instance, `teleo-pgvector-standby`, in `europe-west6-c`. Cloud SQL Studio is reachable but exposes no enabled existing IAM database principal and requires built-in password authentication; no credential was submitted and no SQL ran. Database-row/runtime/DC-01-through-DC-06 parity therefore remains unproven. The exact next gate is an already-existing authenticated Studio principal or a separately authorized IAM/database-user enablement window.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
{
|
||||
"schema": "teleo.gcpCloudSqlLiveReadonly.v1",
|
||||
"captured_at_utc": "2026-07-10T06:52:27Z",
|
||||
"source_thread_id": "019f47af-f2cc-7c10-b928-a9283afa2621",
|
||||
"current_canary": "Open the authenticated Teleo Cloud SQL instances page with authuser=5 and retain the complete sanitized current inventory plus instance routing metadata.",
|
||||
"required_tier": "T3_live_readonly",
|
||||
"current_tier": "T3_live_readonly",
|
||||
"status": "done_verified",
|
||||
"strongest_claim_allowed": "Project teleo-501523 currently exposes exactly one Cloud SQL instance in the authenticated Console: teleo-pgvector-standby. It is runnable/available, PostgreSQL 16.14 Enterprise, single-zone in europe-west6-c, and reachable only through private connectivity on teleo-staging-net. This does not prove database contents, schema parity, row counts, application connectivity, or production cutover.",
|
||||
"capture": {
|
||||
"surface": "authenticated Google Cloud Console DOM readback",
|
||||
"account": "b***@livingip.xyz",
|
||||
"project_id": "teleo-501523",
|
||||
"project_display_name": "Teleo",
|
||||
"list_url": "https://console.cloud.google.com/sql/instances?authuser=5&project=teleo-501523",
|
||||
"overview_url": "https://console.cloud.google.com/sql/instances/teleo-pgvector-standby/overview?authuser=5&project=teleo-501523",
|
||||
"cli_authentication_used": false,
|
||||
"secret_values_captured": false
|
||||
},
|
||||
"inventory": {
|
||||
"instance_count": 1,
|
||||
"zero_instances": false,
|
||||
"instances": [
|
||||
{
|
||||
"instance_id": "teleo-pgvector-standby",
|
||||
"status_ui": "Available; Runnable",
|
||||
"engine": "PostgreSQL",
|
||||
"major_version": "16",
|
||||
"database_version": "PostgreSQL 16.14",
|
||||
"cloud_sql_edition": "Enterprise",
|
||||
"instance_role": "Primary instance",
|
||||
"region": "europe-west6",
|
||||
"zone": "europe-west6-c",
|
||||
"availability": "Single zone",
|
||||
"high_availability": false,
|
||||
"machine": {
|
||||
"vcpus": 1,
|
||||
"memory_mb_ui": 628.74
|
||||
},
|
||||
"storage": {
|
||||
"type": "SSD",
|
||||
"provisioned_gb": 10,
|
||||
"used_gb_ui": 2,
|
||||
"automatic_storage_increase": true
|
||||
},
|
||||
"connectivity": {
|
||||
"connection_name": "teleo-501523:europe-west6:teleo-pgvector-standby",
|
||||
"private_ip_connectivity": true,
|
||||
"private_service_access": true,
|
||||
"associated_network": "projects/teleo-501523/global/networks/teleo-staging-net",
|
||||
"vpc_network": "teleo-staging-net",
|
||||
"allocated_ip_range": "Automatically assigned IP range",
|
||||
"internal_ip": "10.61.0.3",
|
||||
"public_ip_connectivity": false,
|
||||
"public_ip": null,
|
||||
"default_tcp_port": 5432,
|
||||
"write_endpoint": null
|
||||
},
|
||||
"database_authentication": {
|
||||
"cloudsql_iam_authentication": true,
|
||||
"application_or_operator_principal_verified": false
|
||||
},
|
||||
"resilience_and_backup": {
|
||||
"automated_backups": true,
|
||||
"backup_tier": "Standard",
|
||||
"point_in_time_recovery": true,
|
||||
"deletion_prevention": true,
|
||||
"latest_visible_backup_status": "Backup finished",
|
||||
"latest_visible_backup_date_local": "2026-07-10"
|
||||
},
|
||||
"recent_visible_operations_sanitized": [
|
||||
"Backup finished on 2026-07-10",
|
||||
"Backup finished on 2026-07-09",
|
||||
"Two imports succeeded on 2026-07-08",
|
||||
"Database creation completed on 2026-07-08"
|
||||
],
|
||||
"observed_health_issues": [
|
||||
"Auditing not enabled",
|
||||
"No password policy",
|
||||
"No user password policy"
|
||||
],
|
||||
"labels": [],
|
||||
"maintenance": {
|
||||
"window": "Sunday 06:00-07:00 CEST",
|
||||
"scheduled_now": false,
|
||||
"version_current": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"db_parity_route": {
|
||||
"target_instance": "teleo-pgvector-standby",
|
||||
"required_network_surface": "A runtime or connector with private reachability to teleo-staging-net",
|
||||
"private_endpoint": "10.61.0.3:5432",
|
||||
"cloud_sql_connection_name": "teleo-501523:europe-west6:teleo-pgvector-standby",
|
||||
"public_route_available": false,
|
||||
"iam_database_authentication_enabled": true,
|
||||
"database_name_proven_by_this_pass": false,
|
||||
"next_readonly_owner_action": "From a verified teleo-staging-net-connected runtime, authenticate with an authorized database principal and run read-only database/schema/count parity queries against this instance. Do not attempt a public-IP connection because public connectivity is disabled."
|
||||
},
|
||||
"not_proven": [
|
||||
"database names",
|
||||
"database contents or row counts",
|
||||
"schema parity with the VPS database",
|
||||
"successful application-level connection from teleo-prod-1",
|
||||
"production traffic cutover",
|
||||
"high availability or cross-zone failover"
|
||||
],
|
||||
"actions": [
|
||||
"Opened the exact Cloud SQL instances URL in the authorized authuser=5 Chrome session",
|
||||
"Read the complete live instance treegrid and verified one and only one instance row",
|
||||
"Opened the instance Overview through read-only navigation",
|
||||
"Read status, engine/version, region/zone, machine, storage, connectivity, backup, IAM-authentication, and maintenance metadata",
|
||||
"Did not activate Edit, Connect, Restart, Stop, Import, Export, Migrate, Clone, Delete, or failover controls"
|
||||
],
|
||||
"cleanup_readback": {
|
||||
"browser_tab_finalized": true,
|
||||
"browser_tab_disposition": "agent-created evidence tab closed; no handoff tab kept",
|
||||
"gcp_writes": 0,
|
||||
"database_writes": 0,
|
||||
"service_restarts": 0,
|
||||
"deploys": 0,
|
||||
"iam_changes": 0,
|
||||
"credential_or_key_creation": 0,
|
||||
"telegram_sends": 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Teleo Cloud SQL T3 live-readonly inventory
|
||||
|
||||
Captured: `2026-07-10T06:52:27Z`
|
||||
|
||||
## Outcome
|
||||
|
||||
The authenticated `authuser=5` Cloud Console session proves that project `teleo-501523` currently has exactly one Cloud SQL instance.
|
||||
|
||||
- Required tier: `T3_live_readonly`
|
||||
- Current tier: `T3_live_readonly`
|
||||
- Status: `done_verified`
|
||||
- Account: `b***@livingip.xyz`
|
||||
- Project: `Teleo` / `teleo-501523`
|
||||
|
||||
## Current inventory
|
||||
|
||||
| Instance | Status | Engine | Region / zone | Availability | Connectivity |
|
||||
|---|---|---|---|---|---|
|
||||
| `teleo-pgvector-standby` | Available / runnable | PostgreSQL `16.14`, Enterprise | `europe-west6` / `europe-west6-c` | Single-zone; not highly available | Private IP and PSA enabled; public IP disabled |
|
||||
|
||||
Instance count: `1`.
|
||||
|
||||
## DB parity routing metadata
|
||||
|
||||
- Cloud SQL connection name: `teleo-501523:europe-west6:teleo-pgvector-standby`
|
||||
- VPC: `projects/teleo-501523/global/networks/teleo-staging-net`
|
||||
- Private endpoint: `10.61.0.3:5432`
|
||||
- Private Service Access: enabled
|
||||
- Public IP connectivity: disabled
|
||||
- Cloud SQL IAM database authentication: enabled
|
||||
- Write endpoint: none shown
|
||||
|
||||
The DB parity worker must run from a host or connector with private reachability to `teleo-staging-net`, authenticate with an authorized database principal, and issue read-only schema/count queries. A public-IP route is unavailable.
|
||||
|
||||
## Other current facts
|
||||
|
||||
- Machine: `1 vCPU`, `628.74 MB`
|
||||
- Storage: `10 GB` SSD, `2 GB` shown used; automatic storage increase enabled
|
||||
- Automated backups and point-in-time recovery: enabled
|
||||
- Deletion prevention: enabled
|
||||
- Latest visible backup: finished on `2026-07-10`
|
||||
- Two sanitized import operations are shown as successful on `2026-07-08`
|
||||
- Maintenance: Sunday `06:00-07:00 CEST`; no maintenance currently scheduled
|
||||
- Observed health issues: auditing not enabled, no password policy, no user password policy
|
||||
|
||||
## Claim ceiling
|
||||
|
||||
This proves the current Cloud SQL control-plane inventory and private routing surface. It does not prove database names, contents, row counts, schema parity, an application connection from `teleo-prod-1`, production traffic cutover, or failover capability.
|
||||
|
||||
## Actions and mutation boundary
|
||||
|
||||
The exact live list and instance Overview URLs were read through Chrome. No Edit, Connect, Restart, Stop, Import, Export, Migrate, Clone, Delete, or failover control was activated. GCP writes, DB writes, IAM changes, key creation, service restarts, deploys, and Telegram sends were all zero.
|
||||
|
||||
Cleanup readback: the agent-created Cloud SQL evidence tab was closed and no browser handoff tab was retained.
|
||||
|
||||
Machine-readable evidence: `gcp-cloud-sql-t3-live-readonly-current.json`.
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
{
|
||||
"schema": "teleo.gcpCloudSqlStudioParityGate.v1",
|
||||
"captured_at_utc": "2026-07-10T06:57:22.085Z",
|
||||
"source_thread_id": "019f47af-f2cc-7c10-b928-a9283afa2621",
|
||||
"current_canary": "Open Cloud SQL Studio for teleo-pgvector-standby with authuser=5, authenticate only with an already-authorized principal, execute a transaction-level read-only schema/count parity query, and roll it back.",
|
||||
"required_tier": "T3_live_readonly",
|
||||
"current_tier": "T0_spec",
|
||||
"supporting_gate_evidence_tier": "T3_live_readonly",
|
||||
"status": "blocked_with_artifact",
|
||||
"strongest_claim_allowed": "The authenticated Cloud SQL Studio surface is live and reachable for teleo-pgvector-standby, but it did not expose an existing IAM database principal. The visible login flow required built-in database authentication with a password credential. No database session or SQL readback was established, so database contents and VPS parity remain unproven.",
|
||||
"control_plane_evidence_source": "/Users/user/Documents/Codex/2026-07-10/leo-gcp-control-plane/outputs/gcp-cloud-sql-t3-live-readonly-current.json",
|
||||
"target": {
|
||||
"project": "teleo-501523",
|
||||
"instance": "teleo-pgvector-standby",
|
||||
"connection_name": "teleo-501523:europe-west6:teleo-pgvector-standby",
|
||||
"engine": "PostgreSQL 16.14",
|
||||
"private_endpoint": "10.61.0.3:5432",
|
||||
"network": "teleo-staging-net",
|
||||
"instance_iam_db_authentication_flag": "on",
|
||||
"console_account": "b***@livingip.xyz"
|
||||
},
|
||||
"live_studio_readback": {
|
||||
"studio_url_sanitized": "https://console.cloud.google.com/sql/instances/teleo-pgvector-standby/studio?authuser=5&project=teleo-501523",
|
||||
"studio_loaded": true,
|
||||
"login_dialog_visible": true,
|
||||
"database_selector_visible": true,
|
||||
"user_selector_visible": true,
|
||||
"iam_database_authentication_option_visible": true,
|
||||
"iam_database_authentication_option_enabled": false,
|
||||
"built_in_database_authentication_selected": true,
|
||||
"password_credential_required": true,
|
||||
"password_value_retained": false,
|
||||
"authenticate_clicked": false
|
||||
},
|
||||
"sql_execution": {
|
||||
"database_session_established": false,
|
||||
"begin_transaction_read_only_executed": false,
|
||||
"current_database_read": false,
|
||||
"current_user_read": false,
|
||||
"transaction_read_only_read": false,
|
||||
"catalog_or_table_selects_executed": false,
|
||||
"rollback_executed": false,
|
||||
"query_timestamp_utc": null
|
||||
},
|
||||
"parity_fields": {
|
||||
"database_name": null,
|
||||
"schemas": null,
|
||||
"public.claims": null,
|
||||
"public.sources": null,
|
||||
"public.claim_evidence": null,
|
||||
"public.claim_edges": null,
|
||||
"kb_stage.kb_proposals": null,
|
||||
"proposal_ids_and_statuses": null,
|
||||
"selected_row_identities": null,
|
||||
"gcp_vs_vps": null,
|
||||
"ahead_behind_equal": null
|
||||
},
|
||||
"actions": [
|
||||
"Opened the authenticated instance Overview URL with authuser=5 in an agent-created Chrome tab.",
|
||||
"Discovered the exact Cloud SQL Studio href from the live instance navigation.",
|
||||
"Opened Cloud SQL Studio and read the visible database-login state.",
|
||||
"Stopped before authentication because the IAM option was disabled and the selected built-in flow required a password credential.",
|
||||
"Did not open browser SSH because that path may create an ephemeral key.",
|
||||
"Finalized Chrome and closed the agent-created tab."
|
||||
],
|
||||
"exact_gate": "Cloud SQL Studio exposes no enabled existing IAM database principal for this console session. Its selected built-in authentication path requires a raw database password, which this lane is forbidden to inspect, submit, recover, or replace.",
|
||||
"why_autonomous_repair_stops": "Authenticating would require handling a raw password, or creating/granting a database or IAM principal. Both are explicit stop conditions, and the latter would mutate GCP or the database.",
|
||||
"clear_CTA": "Using an already-existing authorized database principal, have an operator complete the Cloud SQL Studio database login without sharing credentials with Codex, then tell Codex the Studio editor is authenticated. Otherwise route IAM/user enablement to a separately authorized write lane.",
|
||||
"next_non_user_action": "Once Studio is already authenticated with an existing principal, run only BEGIN TRANSACTION READ ONLY; identity/read-only checks; catalog/table/count SELECTs; and ROLLBACK, then retain the sanitized results.",
|
||||
"cleanup_readback": {
|
||||
"browser_finalized": true,
|
||||
"agent_created_tab_closed": true,
|
||||
"database_session_open": false,
|
||||
"transaction_open": false,
|
||||
"gcp_writes": 0,
|
||||
"database_writes": 0,
|
||||
"iam_changes": 0,
|
||||
"credential_or_key_creation": 0,
|
||||
"browser_ssh_opened": false,
|
||||
"service_restarts": 0,
|
||||
"deploys": 0,
|
||||
"telegram_sends": 0
|
||||
},
|
||||
"secret_values_included": false
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Cloud SQL Studio parity gate
|
||||
|
||||
Captured UTC: `2026-07-10T06:57:22.085Z`
|
||||
|
||||
## Outcome
|
||||
|
||||
Cloud SQL Studio opened successfully for `teleo-pgvector-standby` under the authenticated `b***@livingip.xyz` Console session. The database login dialog did not provide an enabled existing IAM principal: IAM database authentication was disabled in the dialog, built-in authentication was selected, and that path required a password credential.
|
||||
|
||||
The lane stopped before authentication. No database session was established and no SQL ran.
|
||||
|
||||
- Required tier: `T3_live_readonly`
|
||||
- Current database-parity tier: `T0_spec`
|
||||
- Live gate evidence: `T3_live_readonly`
|
||||
- Status: `blocked_with_artifact`
|
||||
|
||||
## Target verified from the live control-plane evidence
|
||||
|
||||
- Project: `teleo-501523`
|
||||
- Instance: `teleo-pgvector-standby`
|
||||
- Connection: `teleo-501523:europe-west6:teleo-pgvector-standby`
|
||||
- Engine: PostgreSQL 16.14
|
||||
- Endpoint: private-only `10.61.0.3:5432`
|
||||
- Network: `teleo-staging-net`
|
||||
- Instance flag: `cloudsql.iam_authentication=on`
|
||||
|
||||
## Exact gate
|
||||
|
||||
The Studio login surface exposed no enabled existing IAM database principal for this Console session. The selected built-in authentication route requires a raw database password. Using that password, creating a database user, changing IAM, or granting access is outside this lane's hard ceiling.
|
||||
|
||||
An operator can complete Studio login independently with an already-existing authorized principal, without sharing credentials with Codex, and then tell Codex the SQL editor is authenticated. Otherwise, IAM/user enablement must be handled by a separately authorized write lane.
|
||||
|
||||
## Not proven
|
||||
|
||||
No current database name, principal identity, transaction-read-only result, schema presence, table counts, proposal IDs/statuses, selected row identities, or GCP-versus-VPS comparison was read.
|
||||
|
||||
The planned `BEGIN TRANSACTION READ ONLY` query was not executed and therefore is not counted as evidence.
|
||||
|
||||
## Cleanup
|
||||
|
||||
- `Authenticate` was not clicked.
|
||||
- No browser SSH was opened.
|
||||
- Chrome was finalized and the agent-created Studio tab was closed.
|
||||
- No database session or transaction remains open.
|
||||
- No GCP, database, IAM, credential, key, service, deploy, or Telegram mutation occurred.
|
||||
|
||||
Machine-readable evidence: `gcp-cloudsql-studio-parity-gate-current.json`.
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"schema": "working-leo.gcp-parallel-delegation.v1",
|
||||
"source_thread_id": "019f47af-f2cc-7c10-b928-a9283afa2621",
|
||||
"last_updated_utc": "2026-07-10T15:47:17Z",
|
||||
"source_goal": "Make Leo on VPS meet the working-Leo definition, then prove the equivalent GCP path without weakening the VPS standard.",
|
||||
"dispatched_repo_head": "339565585dc417501488a38135998283988482d1",
|
||||
"parallelism_expected": 10,
|
||||
"parallelism_actual": 10,
|
||||
"ship_owner": {
|
||||
"thread_id": "019f4a53-b16c-7653-8026-bba381b853bf",
|
||||
"working_target": "Authenticated live GCP project, Compute Engine, Cloud SQL, and Leo surface inventory",
|
||||
"required_tier": "T3_live_readonly",
|
||||
"status": "completed_t3_control_plane_inventory"
|
||||
},
|
||||
"visible_threads": [
|
||||
{
|
||||
"thread_id": "019f4a53-b16c-7653-8026-bba381b853bf",
|
||||
"lane_class": "parallel_canary",
|
||||
"role": "GCP control-plane ship owner",
|
||||
"working_target": "Authenticated live GCP resource inventory",
|
||||
"required_tier": "T3_live_readonly",
|
||||
"owned_surface": "GCP project and resource discovery; read-only",
|
||||
"forbidden_surface": "Shared repo writes, Telegram sends, DB writes, restarts, deploys, credentials, IAM, infrastructure mutation",
|
||||
"status": "completed_t3_project_vm_cloudsql_inventory"
|
||||
},
|
||||
{
|
||||
"thread_id": "019f4a53-9702-7a10-a894-354af22d9cee",
|
||||
"lane_class": "parallel_canary",
|
||||
"role": "GCP Cloud SQL parity owner",
|
||||
"working_target": "Read-only canonical row and schema parity against VPS",
|
||||
"required_tier": "T3_live_readonly",
|
||||
"owned_surface": "Cloud SQL discovery and read-only SQL",
|
||||
"forbidden_surface": "Shared repo writes, DB mutation, Telegram sends, restarts, deploys, credentials, IAM, infrastructure mutation",
|
||||
"status": "completed_live_studio_gate_waiting_existing_db_principal"
|
||||
},
|
||||
{
|
||||
"thread_id": "019f4a53-c10a-7781-b336-401d526d2a48",
|
||||
"lane_class": "parallel_canary",
|
||||
"role": "GCP Leo runtime parity owner",
|
||||
"working_target": "Read-only VM, process, service, repo, profile, model, DB, and bot surface map",
|
||||
"required_tier": "T3_live_readonly",
|
||||
"owned_surface": "Compute Engine and Leo runtime inspection; read-only",
|
||||
"forbidden_surface": "Shared repo writes, Telegram sends, profile writes, restarts, deploys, credentials, IAM, infrastructure mutation",
|
||||
"status": "completed_runtime_contract_waiting_iap_access"
|
||||
},
|
||||
{
|
||||
"thread_id": "019f4a53-cf0a-7e92-a7d0-16fde32224de",
|
||||
"lane_class": "parallel_canary",
|
||||
"role": "GCP no-post Cory canary owner",
|
||||
"working_target": "Real GCP DC-01 through DC-06 runtime replies, score, unchanged DB counts, and cleanup",
|
||||
"required_tier": "T2_runtime_plus_T3_live_readonly",
|
||||
"owned_surface": "No-post handler execution and scoring against the discovered GCP runtime",
|
||||
"forbidden_surface": "Shared repo writes, Telegram sends, DB apply, profile writes, restarts, deploys",
|
||||
"status": "completed_before_runtime_canary_waiting_iap_access"
|
||||
}
|
||||
],
|
||||
"support_agents": [
|
||||
{
|
||||
"agent_id": "019f4a53-d792-75e1-93c0-ad3c90868176",
|
||||
"nickname": "Dewey",
|
||||
"role": "gcloud DNS and auth anomaly diagnosis",
|
||||
"owned_surface": "Local gcloud runtime and network diagnosis; read-only",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"agent_id": "019f4a53-e042-72e3-923f-bff6e067175e",
|
||||
"nickname": "Fermat",
|
||||
"role": "Cloud SQL parity query contract",
|
||||
"owned_surface": "Retained artifacts, schema, and read-only query design",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"agent_id": "019f4a53-e68a-7e41-a7e6-be6b1f378b41",
|
||||
"nickname": "Planck",
|
||||
"role": "GCP probe script defect audit",
|
||||
"owned_surface": "scripts/probe_gcp_db_parity.py and tests; read-only",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"agent_id": "019f4a53-ec08-7b02-945e-3aa3d3f275a5",
|
||||
"nickname": "Banach",
|
||||
"role": "Cory-standard GCP acceptance and cutover sequence",
|
||||
"owned_surface": "Working-Leo acceptance gates and execution order; read-only",
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
"review_agents": [
|
||||
{
|
||||
"agent_id": "019f4a5b-552e-7873-84ab-a274eec7261f",
|
||||
"nickname": "Bacon",
|
||||
"role": "DB apply authority and race review",
|
||||
"status": "completed_findings_integrated"
|
||||
},
|
||||
{
|
||||
"agent_id": "019f4a5b-5ca4-7a80-88e1-20c9a8d147aa",
|
||||
"nickname": "Meitner",
|
||||
"role": "Rich normalization and lifecycle review",
|
||||
"status": "completed_findings_integrated"
|
||||
}
|
||||
],
|
||||
"root_owned_running_rows": [
|
||||
{
|
||||
"working_target": "Approved strict claim bundles become canonical rows with conflict-safe row-level proof in a disposable DB clone",
|
||||
"owned_files": [
|
||||
"scripts/apply_proposal.py",
|
||||
"scripts/apply_worker.py",
|
||||
"scripts/kb_apply_prereqs.sql",
|
||||
"scripts/kb_proposal_normalize.py",
|
||||
"tests/test_apply_proposal.py",
|
||||
"tests/test_apply_worker.py",
|
||||
"tests/test_kb_proposal_normalize.py"
|
||||
],
|
||||
"status": "isolated_runtime_green_generic_and_helmer_37_of_37"
|
||||
}
|
||||
],
|
||||
"blocked_by_tooling_or_capacity": [],
|
||||
"blocked_by_collision_risk": [],
|
||||
"blocked_by_external_gate": [
|
||||
{
|
||||
"gate": "Cloud SQL Studio has no enabled existing IAM database principal and requires built-in password authentication",
|
||||
"impact": "Cloud SQL schema, row-count, proposal-state, and VPS-versus-GCP database parity queries cannot run",
|
||||
"clear_CTA": "Authenticate Studio with an already-existing authorized database principal without sharing the credential, then report GCP Studio authenticated; otherwise authorize a separate IAM/database-user enablement window."
|
||||
},
|
||||
{
|
||||
"gate": "no verified no-key read-only VM session",
|
||||
"impact": "GCP Leo runtime parity and DC-01 through DC-06 remain unproven",
|
||||
"clear_CTA": "Use an already-authenticated no-key runtime route, or separately authorize the minimum IAP/OS Login setup needed for a read-only VM session."
|
||||
}
|
||||
],
|
||||
"repair_action": "Authenticated Chrome cleared the old blanket login/DNS blocker and retained live project, VM, and private Cloud SQL inventory. Cloud SQL Studio was opened and safely stopped at the database-principal gate without submitting credentials. The next database pass starts from an already-authenticated Studio session; runtime parity remains a separate no-key session gate.",
|
||||
"shared_checkout_rule": "All GCP workers are read-only. The source thread is the sole apply-engine integration owner.",
|
||||
"gui_rule": "Authenticated Chrome or Computer Use is authorized for GCP login and read-only console navigation. AppleScript, osascript, open, focus stealing, credential creation, and representational communication are forbidden.",
|
||||
"telegram_messages_authorized": false,
|
||||
"database_mutation_authorized": false,
|
||||
"infrastructure_mutation_authorized": false,
|
||||
"no_x402_scope": true
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,11 +1,13 @@
|
|||
{
|
||||
"pack": "leo-teleo-skill-pack",
|
||||
"created_date": "2026-07-09",
|
||||
"status": "draft_ready_for_review",
|
||||
"last_updated_utc": "2026-07-10T15:47:17Z",
|
||||
"status": "repo_native_validated",
|
||||
"contains_secrets": false,
|
||||
"production_mutation_authorized": false,
|
||||
"install_target": "/Users/user/.codex/skills",
|
||||
"claim_ceiling": "Reusable onboarding and operations skill drafts only; they do not authorize production DB apply or runtime changes.",
|
||||
"repo_skill_root": ".agents/skills",
|
||||
"optional_local_install_target": "/Users/user/.codex/skills",
|
||||
"claim_ceiling": "Repo-native onboarding and operations skills. Generic and Helmer guarded-apply canaries pass 37/37 in disposable PostgreSQL; the production permission migration and Helmer apply remain undeployed. GCP control-plane inventory is live-proven, while DB row/runtime parity remains gated on an existing database principal.",
|
||||
"skills": [
|
||||
{
|
||||
"name": "teleo-leo-onboarding",
|
||||
|
|
@ -24,12 +26,12 @@
|
|||
},
|
||||
{
|
||||
"name": "teleo-gcp-parity-ops",
|
||||
"role": "GCP parity/cutover lane and exact auth blocker handling",
|
||||
"role": "GCP control-plane, private Cloud SQL inventory, Studio DB-auth gate, and parity workflow",
|
||||
"path": ".agents/skills/teleo-gcp-parity-ops/SKILL.md"
|
||||
},
|
||||
{
|
||||
"name": "teleo-kb-db-change-workflow",
|
||||
"role": "KB proposal normalization, clone rehearsal, apply packet, rollback proof",
|
||||
"role": "Strict proposal normalization, separated review/apply authority, isolated canary, and row-level proof",
|
||||
"path": ".agents/skills/teleo-kb-db-change-workflow/SKILL.md"
|
||||
},
|
||||
{
|
||||
|
|
@ -51,6 +53,10 @@
|
|||
"reference_files": [
|
||||
"docs/reports/leo-working-state-20260709/current-truth-index.md",
|
||||
"docs/reports/leo-working-state-20260709/operator-surface-map.md",
|
||||
"docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md"
|
||||
]
|
||||
"docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md",
|
||||
"docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md",
|
||||
"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.md"
|
||||
],
|
||||
"validation": "tests/test_repo_skill_pack.py"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Purpose: reusable onboarding and operating knowledge for future Codex/Fable workers working on Leo, Teleo, the VPS, GCP parity, Telegram canaries, and KB database changes.
|
||||
|
||||
This pack is a draft installable skill set. It is intentionally local-first and evidence-linked. It does not contain secrets, private key contents, bot tokens, or production mutation authorization.
|
||||
This is the repo-native skill set under `.agents/skills/`. It is evidence-linked and validated by `tests/test_repo_skill_pack.py`. It does not contain secrets, private key contents, bot tokens, or production mutation authorization.
|
||||
|
||||
## Skills In This Pack
|
||||
|
||||
|
|
@ -19,9 +19,9 @@ This pack is a draft installable skill set. It is intentionally local-first and
|
|||
|
||||
- VPS Leo Telegram memory, KB audit, and staged write canaries are live-proven.
|
||||
- A strict canonical `add_edge` apply canary is live-proven.
|
||||
- Rich approved proposal packets are clone/rehearsal-proven, not production-applied.
|
||||
- Generic and real Helmer guarded-apply canaries pass `37/37` with exact payload projections/table deltas, current-source hashes, and cleanup proof; neither the new permission migration nor Helmer is production-applied.
|
||||
- The live VPS runtime behavior should not be changed while the DB apply path is stabilized.
|
||||
- GCP parity is not solved; it remains blocked by account auth/access facts recorded in the current blocker artifact.
|
||||
- GCP project, VM, and private Cloud SQL inventory are live-proven. Cloud SQL row/schema parity is not: Studio requires an already-existing authenticated DB principal.
|
||||
|
||||
## Key References
|
||||
|
||||
|
|
@ -30,6 +30,6 @@ This pack is a draft installable skill set. It is intentionally local-first and
|
|||
- Fable onboarding prompt: `docs/reports/leo-working-state-20260709/fable-leo-teleo-onboarding.md`
|
||||
- Skill manifest: `skill-pack-manifest.json`
|
||||
|
||||
## Install Later
|
||||
## Discovery
|
||||
|
||||
The reusable skills are committed in `.agents/skills/`. Keep this report directory as the evidence snapshot because it points to the retained July 9 artifacts.
|
||||
Codex-compatible workers should discover the committed `.agents/skills/` tree from this repository. The manifest is the index; each skill points to current retained evidence rather than embedding secrets or pretending a stale snapshot is live truth.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Working Leo Current State - 2026-07-09
|
||||
|
||||
Generated UTC: `2026-07-10T02:12:56Z`
|
||||
Updated UTC: `2026-07-10T15:47:17Z`
|
||||
|
||||
## Current Working Definition
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ A working Leo, for Cory/m3taversal's current expectation, is a Telegram-facing a
|
|||
|
||||
## What Is Now Proven On VPS
|
||||
|
||||
- VPS gateway service is stable: `leoclean-gateway.service` is active/running, `MainPID=3252143`, `NRestarts=0`, `WorkingDirectory=/home/teleo`, `User=teleo`.
|
||||
- Fresh VPS gateway readback is stable: `leoclean-gateway.service` is active/running, `MainPID=2999690`, `NRestarts=0`; production counts are `claims=1837`, `sources=4145`, `claim_evidence=4670`, `claim_edges=4916`, `kb_proposals=26`.
|
||||
- Restart survival is now live-proven as a named Cory-standard proof row, not inferred from passive uptime: `systemctl restart leoclean-gateway.service` returned `0`, service came back active/running with `MainPID=2845730` and `ExecMainStartTimestamp=Fri 2026-07-10 03:24:43 UTC`, canonical DB counts stayed unchanged (`1837/4145/4670/4916/26`), no Telegram post or production DB apply ran, the temp profile was removed, and the post-restart no-post `DC-01` through `DC-06` handler suite returned six runtime replies. Strict post-restart answer-quality score for that pre-repair retained run was `5/6`, then the VPS/GCP KB bridge skill was tightened for row-level proof, overclaim guardrails, and proposal state semantics. After deploy SHA `090becae1621436467610e529d6328d70964d11f`, the same no-post direct-claim suite strict-scored `6/6`; DB counts stayed unchanged, no Telegram post or production DB apply ran, no live profile changed, and cleanup confirmed no handler process or temp profile left behind.
|
||||
- Telegram Bot/API readiness is proven for group `Leo` (`-5146042086`), and the live adapter accepts the Cory-working canary without posting.
|
||||
- Live Telegram-visible group canary passed through authenticated Chrome/Telegram Web:
|
||||
|
|
@ -94,8 +94,11 @@ A working Leo, for Cory/m3taversal's current expectation, is a Telegram-facing a
|
|||
- Old Cory-approved mapped rich proposals are still not fully directly applyable:
|
||||
- `14fa5ecc-ac7a-41c1-807d-a2e85b936617`
|
||||
- `ac036c9d-20a0-4ffe-881f-57d6b7bacf22`
|
||||
- `14fa5ecc` normalization is blocked by missing canonical claim/source IDs. The proposal contains approved textual successor candidates, but the exact successor rows A/B/C/D do not already exist as canonical rows.
|
||||
- Current narrow apply worker supports `add_edge`, `attach_evidence`, and `revise_strategy`; it does not create new `public.claims` or `public.sources` rows. Applying `14fa5ecc` as Cory likely expects requires either creating/identifying canonical rows first or explicitly widening the guarded apply contract.
|
||||
- `14fa5ecc` still cannot become one direct strict bundle because its many-successor supersession, strategy/context, and concept/governance fragments require the reviewed companion mappings/schema decisions already captured in the integrated packet set. Missing canonical IDs alone are no longer the general apply-engine limitation.
|
||||
- The repo implementation now adds an atomic `approve_claim` contract to the guarded apply worker. A fully resolved rich graph normalizes fail-closed into one strict bundle; malformed, missing-key, duplicate, or planner-dropped candidates produce no strict child. Type coercions and candidate-to-row accounting remain visible in `normalization_manifest` for review.
|
||||
- Review and apply authority are separated in the pending migration: `kb_review` can only execute exact-payload approval, `kb_apply` cannot directly update proposal approval/payload or existing evidence, `kb_gate_owner` owns the SECURITY DEFINER gates with a locked `pg_catalog, pg_temp` search path, and one immutable approval snapshot binds the reviewed type/payload/reviewer/timestamp/note to apply.
|
||||
- Final isolated-container runtime receipts pass `37/37` twice. The generic bundle's exact payload projection and table deltas are `2/2/2/1/1`; the live-derived Helmer bundle's are `5` claims, `13` sources, `17` evidence rows, `7` edges, and `1` reasoning tool after exact seeding of Leo and five referenced production claims. Stale payload execution, direct privilege probes, replay, source-hash conflict, legacy function overloads, role memberships, and stale ACL paths are refused without partial canonical rows. Read-only live endpoint counts stayed `1837/4145/4670/4916/17/26`, gateway stayed PID `2999690`/`NRestarts=0`, all retained source hashes match the current files, and all inner clones, outer containers, temp workdirs, and the remote source bundle were removed. This does not claim a byte-for-byte comparison of every live row.
|
||||
- The current production VPS deploy SHA `339565585dc417501488a38135998283988482d1` does not include this pending implementation. No production role/function/table migration ran, the worker remains separately gated, and Helmer remains approved/staged rather than canonical.
|
||||
- Live approved rich proposal contract preview is now available:
|
||||
- `14fa5ecc`: 4 claim rows, 5 source rows, 17 edge fragments, 9 evidence fragments; not direct-apply-ready
|
||||
- `ac036c9d`: 3 claim rows, 3 source rows, 6 edge fragments, 5 evidence fragments; not direct-apply-ready
|
||||
|
|
@ -216,11 +219,13 @@ A working Leo, for Cory/m3taversal's current expectation, is a Telegram-facing a
|
|||
- `teleo-kb-apply-worker.service` exists but is disabled/inactive and ships inert/report-only unless `KB_APPLY_WORKER_ENABLED=1`; no active `KB_APPLY_RENDER_CMD` hook or scheduled 24-hour SOUL renderer was proven
|
||||
- current VPS schema does not contain the designed decision-matrix tables `kb_stage.matrix_voters`, `kb_stage.proposal_votes`, or `kb_stage.proposal_decisions`; current approval evidence lives in `kb_stage.kb_proposals`
|
||||
- live proposal inspection showed pending/approved rows have `source_ref` pointers, but inspected source refs did not directly match `public.sources`; document proposals are real staging rows but still need guarded mapping into canonical sources/evidence/edges
|
||||
- GCP parity was rechecked with a repeatable current probe and remains not proven:
|
||||
- current local config still selects `billy@livingip.xyz` for project `teleo-501523`
|
||||
- the current Codex runner cannot resolve `oauth2.googleapis.com`, so all token refresh/project readbacks stop at `blocked_on_network_dns_before_auth_refresh`
|
||||
- the prior auth-capable July 9 artifact remains useful context: `billy@livingip.xyz` then failed non-interactive reauth, two other locally cached accounts refreshed but lacked `teleo-501523`, and `contact@attn.markets` failed token refresh
|
||||
- no GCP parity claim is valid until `scripts/probe_gcp_db_parity.py` reports `ready_for_gcp_readonly_parity=true`
|
||||
- GCP parity was rechecked across four visible owner threads plus support/review agents and remains only partially proven:
|
||||
- live Chrome control-plane readback confirms project `teleo-501523` and running VM `teleo-prod-1` in `europe-west6-a`
|
||||
- the old blanket DNS diagnosis is stale; shell connectivity can reach Google OAuth, while sandboxed gcloud contexts vary
|
||||
- authenticated Chrome now proves exactly one Cloud SQL instance: `teleo-pgvector-standby`, available/runnable PostgreSQL `16.14` Enterprise in `europe-west6-c`, private-only on `teleo-staging-net`, with no public IP
|
||||
- Cloud SQL Studio is reachable, but its login dialog exposed no enabled existing IAM database principal and selected built-in password authentication; no credential was inspected or submitted and no SQL ran
|
||||
- Cloud SQL row/schema parity, VM runtime parity, and GCP `DC-01` through `DC-06` replies remain unproven
|
||||
- exact CTA: authenticate Cloud SQL Studio with an already-existing authorized database principal without sharing the credential, then report `GCP Studio authenticated`; otherwise authorize a separate IAM/database-user enablement window
|
||||
|
||||
## Current Evidence
|
||||
|
||||
|
|
@ -286,12 +291,16 @@ A working Leo, for Cory/m3taversal's current expectation, is a Telegram-facing a
|
|||
- Integrated apply rehearsal proof image: `outputs/leo-db-state-25-integrated-apply-rehearsal.svg`
|
||||
- Direct-claim handler pass image: `work/teleo-infra-main/docs/reports/leo-working-state-20260709/leo-db-state-27-direct-claim-handler-pass.svg`
|
||||
- Current GCP parity probe: `work/teleo-infra-main/docs/reports/leo-working-state-20260709/gcp-db-parity-current-probe-current.md`
|
||||
- Current GCP Cloud SQL inventory: `work/teleo-infra-main/docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.md`
|
||||
- Current GCP Cloud SQL Studio parity gate: `work/teleo-infra-main/docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.md`
|
||||
- Prior GCP parity blocker: `outputs/gcp-db-parity-current-blocker-20260709.json`
|
||||
|
||||
## Next Actions
|
||||
|
||||
1. Keep the live Telegram canary as the top-level regression proof for Cory-working memory + KB audit.
|
||||
2. Do not auto-apply old freeform approvals.
|
||||
3. Do not run production apply without explicit authorization. The mapped base, cross-surface anchor, Rio strategy, governance/concept, and Helmer packets compose in one clone and are ready as a packet set, but are not production-applied.
|
||||
4. Use the direct-claim/follow-up benchmark cases as the expectation for Cory-style no-context questions before broadening live tests beyond the safe OE suite.
|
||||
5. Keep VPS Leo runtime behavior unchanged while the DB apply path is stabilized.
|
||||
3. Review and land the clone-proven `approve_claim` normal path. Do not deploy its production role grants or enable the apply worker without an explicit production permission-migration window.
|
||||
4. Do not run production apply without explicit authorization. The mapped base, cross-surface anchor, Rio strategy, governance/concept, and Helmer packets compose in one clone and are ready as a packet set, but are not production-applied.
|
||||
5. Use the direct-claim/follow-up benchmark cases as the expectation for Cory-style no-context questions before broadening live tests beyond the safe OE suite.
|
||||
6. Keep VPS Leo runtime behavior unchanged while the DB apply path is stabilized.
|
||||
7. Resume GCP DB parity from an already-authenticated Cloud SQL Studio session; do not handle the raw DB password or create IAM/users inside the read-only lane.
|
||||
|
|
|
|||
|
|
@ -196,6 +196,40 @@ Conclusion:
|
|||
- July 5 "working" included at least one applied strategy proposal, so a staging-only system reads as broken to Cory.
|
||||
- The local Telegram export confirms the exact unresolved tension: Leo's strategy rows changed canonically, but approved claim/evidence/internet-finance changes stayed in `kb_stage`.
|
||||
|
||||
## Canonical Mutation Standard - 2026-07-10
|
||||
|
||||
For a Cory-approved knowledge change, working now means all of the following:
|
||||
|
||||
1. Leo reads the canonical graph and names whether the requested object is a
|
||||
claim, source, evidence link, edge, strategy row, reasoning tool, or a target
|
||||
that still needs a reviewed storage decision.
|
||||
2. Rich intent normalizes fail-closed. Every candidate must be an object with a
|
||||
unique stable key; every candidate must map to exact planned rows; every type
|
||||
coercion must remain visible to the reviewer. A malformed or lossy proposal
|
||||
emits no strict child.
|
||||
3. A dedicated `kb_review` credential approves the exact proposal type and full
|
||||
JSON payload. Its DB role maps to one reviewer identity, and approval creates
|
||||
one immutable snapshot containing payload, reviewer, timestamp, and note.
|
||||
4. A separate `kb_apply` credential can insert only the canonical rows needed by
|
||||
supported contracts. It cannot directly approve, rewrite proposal payloads,
|
||||
overwrite existing evidence, or alter the immutable approval snapshot.
|
||||
5. Apply locks and compares the proposal plus immutable snapshot before writes,
|
||||
inserts conflict-safely, verifies every payload-controlled persisted field
|
||||
plus exact table deltas, and only then marks the proposal applied. Stale
|
||||
payloads, replays, hash collisions, or row mismatches roll back the whole
|
||||
transaction.
|
||||
6. The receipt names proposal ID, reviewer, applier, exact target rows, before
|
||||
and after counts/content, service state, and cleanup. "Approved" without this
|
||||
canonical receipt remains approved/staged, not implemented.
|
||||
|
||||
Final repo-owned runtime proof passes `37/37` for both a generic graph bundle and
|
||||
the real Helmer 7 Powers bundle. The receipts are bound to current source hashes,
|
||||
prove exact payload projections and table deltas in disposable PostgreSQL, and
|
||||
show unchanged live count/service endpoints. This is not production deployment
|
||||
proof: VPS production remains on deploy SHA
|
||||
`339565585dc417501488a38135998283988482d1`, counts
|
||||
`1837/4145/4670/4916/17/26`, and Helmer is still not canonical there.
|
||||
|
||||
## VPS DB Path Proof On 2026-07-09
|
||||
|
||||
Canary proposal:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Working Leo Execution Plan - 2026-07-09
|
||||
|
||||
Claim ceiling: VPS Telegram memory/KB audit, full live-safe open-ended Telegram suite, full mixed-evidence Cory-style outcome/direct-claim sandbox benchmark, live VPS GatewayRunner no-context direct-claim suite, Telegram-visible DC authorization packet, Telegram-visible DC pre-send preflight, Telegram-to-`kb_stage` proposal staging, and strict existing-ID apply are working. The live VPS handler suite for exact `DC-01` through `DC-06` prompts strict-scores `6/6` after the authorized-session-plan deploy and scorer refresh, with no Telegram post, no live profile write, no production DB apply, unchanged DB counts, temp profile cleanup, and unchanged gateway service state during the harness. The Telegram-visible DC suite is exact-scoped and ready to request authorization, but not sent; the pre-send preflight now passes from supplied top-level SSH readback payload with deploy head and `.last-deploy-sha` at `63278b6354d3f5f15ed68897be95d484530db59f`, active/running service state (`MainPID=2999690`, `NRestarts=0`), and DB counts present. The capture receipt assembler is retained but remains `awaiting_capture` until real visible replies plus DB/service/visual proof are supplied and scored. Public Argus HTTP diagnostics currently show `3` approved proposals, all `approved_needs_apply_payload`, with `worker_applyable_count=0`, which supports the expected no-overclaim answer but does not prove Docker row counts or systemd state. The full prepared DB packet set is integrated-clone-proven in dependency order: mapped rich base, cross-surface anchor, Rio strategy context, governance/concept schema, and Helmer 7 Powers. A read-only live production preflight baseline now passes `5/5`, an authorized apply operator runbook is generated, an authorized apply session plan maps every apply-window output path with authorization gates, a production apply authorization packet is ready-to-request, the authorization verifier refuses the current apply window because no exact authorization text/operator identity is present, and the receipt assembler refuses final receipt emission because post-apply session artifacts/counts are absent. None of those packets are production-applied or authorized. GCP parity remains pending.
|
||||
Claim ceiling: VPS Telegram memory/KB audit, full live-safe open-ended Telegram suite, full mixed-evidence Cory-style outcome/direct-claim sandbox benchmark, live VPS GatewayRunner no-context direct-claim suite, Telegram-visible DC authorization packet, Telegram-visible DC pre-send preflight, Telegram-to-`kb_stage` proposal staging, and strict existing-ID apply are working. The live VPS handler suite for exact `DC-01` through `DC-06` prompts strict-scores `6/6` after the authorized-session-plan deploy and scorer refresh, with no Telegram post, no live profile write, no production DB apply, unchanged DB counts, temp profile cleanup, and unchanged gateway service state during the harness. The Telegram-visible DC suite is exact-scoped and ready to request authorization, but not sent; the pre-send preflight now passes from supplied top-level SSH readback payload with deploy head and `.last-deploy-sha` at `63278b6354d3f5f15ed68897be95d484530db59f`, active/running service state (`MainPID=2999690`, `NRestarts=0`), and DB counts present. The capture receipt assembler is retained but remains `awaiting_capture` until real visible replies plus DB/service/visual proof are supplied and scored. Public Argus HTTP diagnostics currently show `3` approved proposals, all `approved_needs_apply_payload`, with `worker_applyable_count=0`, which supports the expected no-overclaim answer but does not prove Docker row counts or systemd state. The full prepared DB packet set is integrated-clone-proven in dependency order: mapped rich base, cross-surface anchor, Rio strategy context, governance/concept schema, and Helmer 7 Powers. A read-only live production preflight baseline now passes `5/5`, an authorized apply operator runbook is generated, an authorized apply session plan maps every apply-window output path with authorization gates, a production apply authorization packet is ready-to-request, the authorization verifier refuses the current apply window because no exact authorization text/operator identity is present, and the receipt assembler refuses final receipt emission because post-apply session artifacts/counts are absent. None of those packets are production-applied or authorized. GCP project/VM/private Cloud SQL inventory is current; GCP DB-row, runtime, Cory-answer, and cutover parity remain pending.
|
||||
|
||||
## WL-EXEC-01 - VPS Telegram-visible memory + KB audit
|
||||
- Status: `done`
|
||||
|
|
@ -62,6 +62,20 @@ Claim ceiling: VPS Telegram memory/KB audit, full live-safe open-ended Telegram
|
|||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/outputs/rich-proposal-creation-plan-20260709/rich-proposal-creation-plan-mapped-current.md`
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/outputs/rich-proposal-creation-plan-20260709/staging-db-commit-rehearsal-current.log`
|
||||
|
||||
## WL-EXEC-05A - Approved claim-bundle normal apply path
|
||||
- Status: `done_isolated_runtime_proven_not_deployed`
|
||||
- Result: Added `approve_claim` plus a fail-closed rich normalizer, separate `kb_review`/`kb_apply` credentials, mapped reviewer identity, immutable approval snapshots, exact-payload row locks, denied direct ledger/evidence mutation, poison-safe worker selection, and exact payload-projection plus table-delta readback. Final source-bound isolated-container receipts pass `37/37` for both the generic bundle (`2/2/2/1/1`) and real Helmer bundle (`5` claims, `13` sources, `17` evidence rows, `7` edges, `1` reasoning tool). Stale approval payload, replay, source-hash conflict, legacy function overloads, role memberships, and stale ACL paths fail closed without partial canonical rows. Read-only live endpoint counts remain `1837/4145/4670/4916/17/26`; gateway stays PID `2999690`, `NRestarts=0`; inner clone, outer container, temp workdir, and remote source-bundle cleanup are independently proven.
|
||||
- Required tier: `T2_runtime` for repo-owned apply lifecycle plus `T3_live_readonly` endpoint stability; current tiers: both proven.
|
||||
- Next action: land the reviewed code in the infrastructure repo. Keep production code deployment, the reviewer/apply permission migration, worker enablement, and canonical Helmer application as separate changes with separate authorization/readback windows.
|
||||
- Not done condition: The current VPS deploy does not contain this implementation, production role grants are unchanged, the apply worker remains a separate enablement decision, and no existing approved rich proposal has been applied to production.
|
||||
- Evidence:
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.md`
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json`
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/docs/reports/leo-working-state-20260709/helmer-approve-claim-clone-canary-current.json`
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/docs/reports/leo-working-state-20260709/helmer-approve-claim-normalization-current.json`
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/scripts/run_approve_claim_clone_canary.py`
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/scripts/run_approve_claim_isolated_container_canary.sh`
|
||||
|
||||
## WL-EXEC-05B - Production apply packet for mapped subset
|
||||
- Status: `done_packet_ready_not_executed`
|
||||
- Result: Generated packet with preflight, rollback rehearsal, authorized commit SQL, postflight, and exact delete rollback. Live preflight and rollback passed; disposable clone commit produced 7 claims, 15 sources, 17 evidence rows, 11 edges; delete rollback returned clone counts to 0/0/0/0; production stayed zero.
|
||||
|
|
@ -139,16 +153,18 @@ Claim ceiling: VPS Telegram memory/KB audit, full live-safe open-ended Telegram
|
|||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/outputs/rich-proposal-creation-plan-20260709/staging-db-mapped-commit-rehearsal-current.log`
|
||||
|
||||
## WL-EXEC-09 - GCP parity
|
||||
- Status: `blocked_on_network_dns_before_auth_refresh`
|
||||
- Result: Fresh current probe uses a temporary writable gcloud config copy and keeps token stdout redacted. Local config still selects `billy@livingip.xyz` for `teleo-501523`, but every token/project readback fails at DNS resolution for `oauth2.googleapis.com`; this runner cannot reach the prior auth/project-access gate.
|
||||
- Not done condition: No GCP parity claim until `scripts/probe_gcp_db_parity.py` reports `ready_for_gcp_readonly_parity=true` and then read-only VM/Cloud SQL/runtime parity is captured.
|
||||
- Status: `control_plane_inventory_done_database_auth_gated`
|
||||
- Result: Parallel GCP owners used CLI plus authenticated Chrome/Computer Use. Live readback confirms project `teleo-501523`, running VM `teleo-prod-1` in `europe-west6-a`, and exactly one Cloud SQL instance: available/runnable PostgreSQL `16.14` instance `teleo-pgvector-standby` in `europe-west6-c`, private-only on `teleo-staging-net`. Cloud SQL Studio loads, but the current session exposes no enabled IAM database principal and requires built-in password authentication. No credential was inspected/submitted and no SQL ran. Cloud SQL row/schema parity, VM runtime parity, and GCP `DC-01` through `DC-06` remain unproven.
|
||||
- Next action: authenticate Studio with an already-existing authorized DB principal without sharing the credential, then run the retained read-only transaction/count query; otherwise use a separately authorized IAM/database-user enablement window.
|
||||
- Not done condition: No GCP parity claim until current Cloud SQL/VM/runtime readback and DC suite proof exist.
|
||||
- Evidence:
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/docs/reports/leo-working-state-20260709/gcp-db-parity-current-probe-current.md`
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/outputs/gcp-db-parity-current-blocker-20260709.json`
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/docs/reports/leo-working-state-20260709/gcp-cloud-sql-t3-live-readonly-current.json`
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/docs/reports/leo-working-state-20260709/gcp-cloudsql-studio-parity-gate-current.json`
|
||||
|
||||
## WL-EXEC-10 - Cleanup and retained evidence
|
||||
- Status: `done_current_pass`
|
||||
- Result: Temporary VPS SQL files removed; macOS sidecar files removed from remote report dir; reports copied under leoclean profile. Fresh read-only cleanup check found no leftover VPS Postgres database names matching `rehearsal|clone|staging`; gateway stayed active/running with `MainPID=1908033`, `NRestarts=0`.
|
||||
- Result: All final review/apply inner clone databases, outer Postgres containers, remote staging trees, and temp workdirs were removed. Fresh live counts remain `1837/4145/4670/4916/26`; gateway is active/running with `MainPID=2999690`, `NRestarts=0`.
|
||||
- Not done condition: Reopen if any temp clone/VPS/bot is later created.
|
||||
- Evidence:
|
||||
- `/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/outputs/working-leo-current-state-20260709.md`
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ 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 write only ``public.strategies``,
|
||||
``public.strategy_nodes``, ``public.claim_evidence``, ``public.claim_edges`` and
|
||||
update the ``kb_stage.kb_proposals`` ledger. It holds no DELETE, no DDL, and no
|
||||
access to any other table (agents, budgets, personas, ...). This enforces
|
||||
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.
|
||||
|
|
@ -30,7 +32,7 @@ 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 (v1)
|
||||
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:
|
||||
|
|
@ -53,9 +55,30 @@ strict ``apply_payload`` object; a separate normalizer produces it. Shapes:
|
|||
"role": "<evidence_role>",
|
||||
"weight": <0..1|null>}, ...]}}
|
||||
|
||||
Note: attach_evidence requires an existing ``source_id`` per item. kb_apply has
|
||||
no INSERT on ``public.sources``, so source creation/dedup is intentionally out of
|
||||
scope for this tool and handled upstream (a follow-up + explicit grant).
|
||||
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
|
||||
|
|
@ -64,8 +87,9 @@ import argparse
|
|||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_SECRETS_FILE = "/home/teleo/.hermes/profiles/leoclean/secrets/kb-apply.env"
|
||||
DEFAULT_CONTAINER = "teleo-pg"
|
||||
|
|
@ -78,84 +102,170 @@ DEFAULT_ROLE = "kb_apply"
|
|||
# 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")
|
||||
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 a safe SQL literal."""
|
||||
"""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)
|
||||
return "'" + str(value).replace("'", "''") + "'"
|
||||
escaped = str(value).replace("\\", "\\\\").replace("'", "''")
|
||||
return "E'" + escaped + "'"
|
||||
|
||||
|
||||
def _jsonb(value: Any) -> str:
|
||||
return sql_literal(json.dumps(value, sort_keys=True)) + "::jsonb"
|
||||
|
||||
|
||||
def _ledger_and_verify(proposal_id: str, applied_by: str, extra_checks: str) -> str:
|
||||
"""Flip the proposal to applied and verify invariants, inside the txn.
|
||||
def _text_array(values: list[str]) -> str:
|
||||
if not values:
|
||||
return "'{}'::text[]"
|
||||
return "array[" + ", ".join(sql_literal(value) for value in values) + "]::text[]"
|
||||
|
||||
The flip runs in a DO block that asserts exactly one row moved from
|
||||
'approved' to 'applied' (``GET DIAGNOSTICS ... = row_count``). This closes
|
||||
the concurrent double-apply race: ``load_proposal`` (read) and this flip
|
||||
(write) are separate statements, so a ``SELECT ... FOR UPDATE`` row lock
|
||||
would not span them -- but only one concurrent apply can match
|
||||
``status='approved'``, so rowcount=1 is the authoritative guard. A loser
|
||||
sees 0 rows and RAISEs -> the whole transaction rolls back.
|
||||
|
||||
``applied_by_agent_id`` is stamped as a real FK resolved from
|
||||
``public.agents`` by handle (requires SELECT public.agents; the service-agent
|
||||
row is bootstrapped once by superuser -- see scripts/kb_apply_prereqs.sql).
|
||||
The resolve is a hard lookup: an unresolved handle RAISEs -> rollback, never
|
||||
a silent NULL FK (the column is ON DELETE SET NULL, so NULL is legal and
|
||||
would otherwise pass unnoticed).
|
||||
"""
|
||||
return f"""
|
||||
do $flip$
|
||||
declare
|
||||
flipped int;
|
||||
resolved_agent_id uuid;
|
||||
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
|
||||
select id into resolved_agent_id
|
||||
from public.agents
|
||||
where handle = {sql_literal(applied_by)};
|
||||
if resolved_agent_id is null then
|
||||
raise exception 'apply_proposal: applied_by handle % does not resolve to a public.agents row; refusing to stamp a NULL applied_by_agent_id', {sql_literal(applied_by)};
|
||||
end if;
|
||||
update kb_stage.kb_proposals
|
||||
set status = 'applied',
|
||||
applied_by_handle = {sql_literal(applied_by)},
|
||||
applied_by_agent_id = resolved_agent_id,
|
||||
applied_at = now(),
|
||||
updated_at = now()
|
||||
where id = {sql_literal(proposal_id)}::uuid
|
||||
and status = 'approved';
|
||||
get diagnostics flipped = row_count;
|
||||
if flipped <> 1 then
|
||||
raise exception 'apply_proposal: ledger flip affected % row(s), expected 1; proposal % was not approved (already applied or a concurrent apply won)', flipped, {sql_literal(proposal_id)};
|
||||
end if;
|
||||
{extra_checks}
|
||||
end
|
||||
$flip$;
|
||||
""".rstrip()
|
||||
$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, reviewer: Optional[str]
|
||||
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 []
|
||||
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"):
|
||||
|
|
@ -212,12 +322,19 @@ values
|
|||
raise exception 'apply_proposal: expected exactly one active strategy for agent %', {sql_literal(agent_id)};
|
||||
end if;"""
|
||||
|
||||
return _wrap_txn(canonical, _ledger_and_verify(proposal_id, reviewer, checks))
|
||||
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, reviewer: Optional[str]
|
||||
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")
|
||||
|
|
@ -227,7 +344,12 @@ def build_add_edge_sql(
|
|||
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,
|
||||
|
|
@ -239,20 +361,35 @@ select {sql_literal(f)}::uuid, {sql_literal(t)}::uuid,
|
|||
and edge_type = {sql_literal(et)}::edge_type);
|
||||
""".rstrip()
|
||||
|
||||
checks = f""" if not exists (select 1 from public.claim_edges
|
||||
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) then
|
||||
raise exception 'apply_proposal: claim_edge not present after apply';
|
||||
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;"""
|
||||
|
||||
return _wrap_txn(canonical, _ledger_and_verify(proposal_id, reviewer, checks))
|
||||
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, reviewer: Optional[str]
|
||||
apply_payload: dict[str, Any],
|
||||
proposal_id: str,
|
||||
applied_by: str | None,
|
||||
approval: dict[str, Any],
|
||||
) -> str:
|
||||
evidence: List[Dict[str, Any]] = apply_payload.get("evidence") or []
|
||||
_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'")
|
||||
|
||||
|
|
@ -274,37 +411,406 @@ def build_attach_evidence_sql(
|
|||
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)}
|
||||
where 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);"""
|
||||
on conflict (claim_id, source_id, role) do nothing;"""
|
||||
)
|
||||
checks.append(
|
||||
f""" if not exists (select 1 from public.claim_evidence
|
||||
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) then
|
||||
raise exception 'apply_proposal: evidence row % not present after apply', {i};
|
||||
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)
|
||||
return _wrap_txn(canonical, _ledger_and_verify(proposal_id, reviewer, "\n".join(checks)))
|
||||
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) -> str:
|
||||
return f"begin;\n{canonical_sql}\n{ledger_sql}\ncommit;\n"
|
||||
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: Optional[str]) -> str:
|
||||
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}")
|
||||
|
|
@ -312,13 +818,18 @@ def build_apply_sql(proposal: Dict[str, Any], applied_by: Optional[str]) -> str:
|
|||
apply_payload = payload.get("apply_payload")
|
||||
if apply_payload is None:
|
||||
raise ValueError(
|
||||
"proposal payload has no 'apply_payload' (contract v1). "
|
||||
"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)
|
||||
return BUILDERS[ptype](
|
||||
apply_payload,
|
||||
proposal["id"],
|
||||
applied_by or SERVICE_AGENT_HANDLE,
|
||||
proposal,
|
||||
)
|
||||
|
||||
|
||||
def assert_applyable(proposal: Dict[str, Any]) -> None:
|
||||
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)")
|
||||
|
|
@ -363,12 +874,16 @@ def run_psql(args: argparse.Namespace, sql: str, password: str) -> str:
|
|||
return result.stdout
|
||||
|
||||
|
||||
def load_proposal(args: argparse.Namespace, password: str) -> Dict[str, Any]:
|
||||
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)::text
|
||||
'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()
|
||||
|
|
@ -377,7 +892,7 @@ def load_proposal(args: argparse.Namespace, password: str) -> Dict[str, Any]:
|
|||
return json.loads(out)
|
||||
|
||||
|
||||
def parse_args(argv: List[str]) -> argparse.Namespace:
|
||||
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(
|
||||
|
|
@ -395,7 +910,7 @@ def parse_args(argv: List[str]) -> argparse.Namespace:
|
|||
return p.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(sys.argv[1:] if argv is None else argv)
|
||||
|
||||
if args.dry_run:
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
"""Event-driven worker that lands human-approved proposals into canonical state.
|
||||
|
||||
Stage 2-of-the-loop automation: Leo proposes -> HUMAN approves -> WORKER applies
|
||||
-> renderer runs -> Leo reads the new self.
|
||||
-> a supported render hook may run -> Leo reads canonical state.
|
||||
|
||||
This is the "natural evolution" engine: it makes an approval in Telegram surface
|
||||
in Leo's identity without Leo ever applying its own work. It does NOT think, does
|
||||
NOT approve, and does NOT create proposals -- it only acts on proposals a human
|
||||
has already moved to ``status='approved'``.
|
||||
This is the "natural evolution" engine: it makes an approval in Telegram canonical
|
||||
without Leo ever applying its own work. It does NOT think, does NOT approve, and
|
||||
does NOT create proposals -- it only acts on proposals a human has already moved
|
||||
to ``status='approved'``.
|
||||
|
||||
Governance boundary (why this is safe)
|
||||
--------------------------------------
|
||||
|
|
@ -24,10 +24,10 @@ Safety posture
|
|||
* Gated OFF by default. Without ``--enable`` (or ``KB_APPLY_WORKER_ENABLED=1``) the
|
||||
worker only REPORTS what it would apply -- it performs no writes. Flip it on only
|
||||
after the first apply has been proven by hand.
|
||||
* The render step is a configurable hook (``--render-cmd`` / ``KB_APPLY_RENDER_CMD``).
|
||||
Until the SOUL renderer (PR2) is deployed the hook is simply absent and the worker
|
||||
logs that render was skipped -- applying still works, Leo's canonical is correct,
|
||||
and the rendered SOUL.md just lags until the renderer exists.
|
||||
* The optional render step is a configurable hook (``--render-cmd`` /
|
||||
``KB_APPLY_RENDER_CMD``). Only ``revise_strategy`` currently has a proven
|
||||
agent-owned render contract. Other apply types update canonical state without
|
||||
invoking a generic agent render.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -38,7 +38,7 @@ import os
|
|||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
|
@ -47,23 +47,34 @@ import apply_proposal as ap # noqa: E402 (sibling module, reused verbatim)
|
|||
|
||||
# Types the worker is willing to auto-apply. Deliberately the same set the engine
|
||||
# supports; a proposal type outside this list is ignored (never applied).
|
||||
WORKER_TYPES = ("revise_strategy", "add_edge", "attach_evidence")
|
||||
WORKER_TYPES = ap.APPLYABLE_TYPES
|
||||
|
||||
# Only a revise_strategy changes an agent's identity spine, so only it triggers a
|
||||
# SOUL re-render. Evidence/edge applies change the graph but not the rendered self.
|
||||
# Only strategy revisions have a proven agent-owned render hook. Canonical claim
|
||||
# bundles and evidence/edge applies do not trigger a generic identity render.
|
||||
RENDER_TYPES = ("revise_strategy",)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Pure helpers (unit-tested without a DB) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
def build_candidate_query(types: tuple = WORKER_TYPES, limit: int = 20) -> str:
|
||||
def build_candidate_query(
|
||||
types: tuple = WORKER_TYPES,
|
||||
limit: int = 20,
|
||||
excluded_ids: tuple = (),
|
||||
) -> str:
|
||||
"""SELECT approved, applyable proposals that carry an apply_payload.
|
||||
|
||||
Filters on status='approved' at the DB level -- this is the structural
|
||||
guarantee that the worker cannot act on anything a human has not approved.
|
||||
IDs at the failure ceiling are excluded before LIMIT so exhausted rows cannot
|
||||
occupy every fetched slot and starve later eligible proposals.
|
||||
"""
|
||||
type_list = ", ".join(ap.sql_literal(t) for t in types)
|
||||
excluded = tuple(str(proposal_id) for proposal_id in excluded_ids)
|
||||
exclusion_clause = ""
|
||||
if excluded:
|
||||
excluded_list = ", ".join(ap.sql_literal(proposal_id) for proposal_id in excluded)
|
||||
exclusion_clause = f"\n and id::text not in ({excluded_list})"
|
||||
return f"""select jsonb_build_object(
|
||||
'id', id::text,
|
||||
'proposal_type', proposal_type,
|
||||
|
|
@ -71,14 +82,14 @@ def build_candidate_query(types: tuple = WORKER_TYPES, limit: int = 20) -> str:
|
|||
from kb_stage.kb_proposals
|
||||
where status = 'approved'
|
||||
and proposal_type in ({type_list})
|
||||
and payload ? 'apply_payload'
|
||||
and payload ? 'apply_payload'{exclusion_clause}
|
||||
order by created_at asc
|
||||
limit {int(limit)};"""
|
||||
|
||||
|
||||
def parse_candidates(psql_output: str) -> List[Dict[str, Any]]:
|
||||
def parse_candidates(psql_output: str) -> list[dict[str, Any]]:
|
||||
"""Parse newline-delimited JSON rows from psql -At output."""
|
||||
rows: List[Dict[str, Any]] = []
|
||||
rows: list[dict[str, Any]] = []
|
||||
for line in psql_output.splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
|
|
@ -86,7 +97,7 @@ def parse_candidates(psql_output: str) -> List[Dict[str, Any]]:
|
|||
return rows
|
||||
|
||||
|
||||
def build_render_command(render_cmd: Optional[str], agent_id: Optional[str]) -> Optional[List[str]]:
|
||||
def build_render_command(render_cmd: str | None, agent_id: str | None) -> list[str] | None:
|
||||
"""Expand the render-hook template for one agent, or None if no hook configured.
|
||||
|
||||
``render_cmd`` is a shell-word template, e.g. ``python3 render_soul.py --agent-id {agent_id}``.
|
||||
|
|
@ -101,7 +112,7 @@ def build_render_command(render_cmd: Optional[str], agent_id: Optional[str]) ->
|
|||
return [tok.format(agent_id=agent_id) for tok in shlex.split(render_cmd)]
|
||||
|
||||
|
||||
def load_failure_state(path: Optional[str]) -> Dict[str, int]:
|
||||
def load_failure_state(path: str | None) -> dict[str, int]:
|
||||
"""Load the persisted {proposal_id: consecutive_failure_count} map.
|
||||
|
||||
The worker runs oneshot per timer tick, so in-memory failure counts do not
|
||||
|
|
@ -118,11 +129,11 @@ def load_failure_state(path: Optional[str]) -> Dict[str, int]:
|
|||
try:
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
return {str(k): int(v) for k, v in data.items()} if isinstance(data, dict) else {}
|
||||
except Exception: # noqa: BLE001 -- a bad state file must never wedge the worker
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def save_failure_state(path: Optional[str], state: Dict[str, int]) -> None:
|
||||
def save_failure_state(path: str | None, state: dict[str, int]) -> None:
|
||||
if not path:
|
||||
return
|
||||
p = Path(path)
|
||||
|
|
@ -131,15 +142,15 @@ def save_failure_state(path: Optional[str], state: Dict[str, int]) -> None:
|
|||
|
||||
|
||||
def partition_candidates(
|
||||
candidates: List[Dict[str, Any]],
|
||||
failure_counts: Dict[str, int],
|
||||
candidates: list[dict[str, Any]],
|
||||
failure_counts: dict[str, int],
|
||||
max_attempts: int,
|
||||
max_per_tick: int,
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Split fetched candidates into what to apply now vs. skip.
|
||||
|
||||
- ``poisoned``: already at/over the failure ceiling -> never re-attempted,
|
||||
surfaced loudly for the operator (needs a human fix, not another retry).
|
||||
- ``poisoned``: defensive post-fetch check for a candidate already at/over
|
||||
the failure ceiling. Normal DB fetches exclude these IDs before LIMIT.
|
||||
- ``to_apply``: the first ``max_per_tick`` non-poisoned candidates -> capped
|
||||
so an enabled worker lands applies one-at-a-time and observably, rather
|
||||
than draining the whole approved queue in a single unobserved tick.
|
||||
|
|
@ -159,8 +170,12 @@ def _psql_args(args: argparse.Namespace) -> argparse.Namespace:
|
|||
)
|
||||
|
||||
|
||||
def fetch_candidates(args: argparse.Namespace, password: str) -> List[Dict[str, Any]]:
|
||||
sql = build_candidate_query(limit=args.limit)
|
||||
def fetch_candidates(
|
||||
args: argparse.Namespace,
|
||||
password: str,
|
||||
excluded_ids: tuple,
|
||||
) -> list[dict[str, Any]]:
|
||||
sql = build_candidate_query(limit=args.limit, excluded_ids=excluded_ids)
|
||||
out = ap.run_psql(_psql_args(args), sql, password)
|
||||
return parse_candidates(out)
|
||||
|
||||
|
|
@ -181,7 +196,7 @@ def apply_one(args: argparse.Namespace, proposal_id: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
def render_one(args: argparse.Namespace, agent_id: Optional[str]) -> str:
|
||||
def render_one(args: argparse.Namespace, agent_id: str | None) -> str:
|
||||
cmd = build_render_command(args.render_cmd, agent_id)
|
||||
if cmd is None:
|
||||
return "render skipped (no render-cmd configured; PR2 renderer not deployed)"
|
||||
|
|
@ -198,13 +213,26 @@ def render_one(args: argparse.Namespace, agent_id: Optional[str]) -> str:
|
|||
# --------------------------------------------------------------------------- #
|
||||
def run(args: argparse.Namespace) -> int:
|
||||
password = ap.load_password(args.secrets_file)
|
||||
candidates = fetch_candidates(args, password)
|
||||
failure_counts = load_failure_state(args.failure_state_file)
|
||||
exhausted_ids = tuple(
|
||||
proposal_id
|
||||
for proposal_id, count in failure_counts.items()
|
||||
if count >= args.max_attempts
|
||||
)
|
||||
candidates = fetch_candidates(args, password, exhausted_ids)
|
||||
|
||||
for proposal_id in exhausted_ids:
|
||||
print(
|
||||
f"EXCLUDE failure-ceiling ID {proposal_id}: failed "
|
||||
f"{failure_counts[proposal_id]}x >= max-attempts {args.max_attempts}; "
|
||||
"omitted from candidate SQL before LIMIT",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if not candidates:
|
||||
print("no approved+applyable proposals; nothing to do")
|
||||
return 0
|
||||
|
||||
failure_counts = load_failure_state(args.failure_state_file)
|
||||
split = partition_candidates(candidates, failure_counts, args.max_attempts, args.max_per_tick)
|
||||
|
||||
for c in split["poisoned"]:
|
||||
|
|
@ -235,7 +263,7 @@ def run(args: argparse.Namespace) -> int:
|
|||
print(f"applied {pid} ({ptype})")
|
||||
if ptype in RENDER_TYPES:
|
||||
print(" " + render_one(args, agent_id))
|
||||
except Exception as exc: # noqa: BLE001 -- one bad proposal must not stop the batch
|
||||
except Exception as exc:
|
||||
failures += 1
|
||||
# Leave the proposal at 'approved' so a fixed one reapplies next tick
|
||||
# (the rowcount=1 guard makes reapply safe), but bump its failure count
|
||||
|
|
@ -249,7 +277,7 @@ def run(args: argparse.Namespace) -> int:
|
|||
return 1 if failures else 0
|
||||
|
||||
|
||||
def parse_args(argv: List[str]) -> argparse.Namespace:
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
|
@ -295,7 +323,7 @@ def parse_args(argv: List[str]) -> argparse.Namespace:
|
|||
return p.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: Optional[List[str]] = None) -> int:
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
return run(parse_args(sys.argv[1:] if argv is None else argv))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,16 @@ pending, applyable proposal from ``pending_review`` to ``approved`` after a
|
|||
human/operator supplies a reviewer handle and verbatim review note. The apply
|
||||
step remains separate and is performed by ``scripts/apply_proposal.py``.
|
||||
|
||||
The guardrails intentionally mirror the apply worker:
|
||||
The guardrails intentionally mirror the apply worker while keeping review and
|
||||
apply credentials separate:
|
||||
|
||||
* proposal must be ``pending_review``;
|
||||
* proposal_type must be one of the worker-supported applyable types;
|
||||
* payload must contain a strict ``apply_payload`` contract;
|
||||
* the strict payload must build valid apply SQL before approval is recorded;
|
||||
* the UPDATE must affect exactly one row.
|
||||
* the reviewer connects as ``kb_review`` and can only execute the constrained
|
||||
``kb_stage.approve_strict_proposal`` function;
|
||||
* ``kb_review`` has no direct proposal UPDATE or canonical write grant.
|
||||
|
||||
``--dry-run`` validates the proposal and prints the SQL without writing.
|
||||
"""
|
||||
|
|
@ -32,6 +35,25 @@ sys.path.insert(0, str(HERE))
|
|||
|
||||
import apply_proposal as ap # noqa: E402
|
||||
|
||||
DEFAULT_REVIEW_SECRETS_FILE = "/home/teleo/.hermes/profiles/leoclean/secrets/kb-review.env"
|
||||
DEFAULT_REVIEW_ROLE = "kb_review"
|
||||
|
||||
|
||||
def load_review_password(secrets_file: str) -> str:
|
||||
path = Path(secrets_file)
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"review 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, _, value = line.partition("=")
|
||||
if key.strip() in ("KB_REVIEW_PASSWORD", "KB_REVIEW_DB_PASSWORD", "PGPASSWORD"):
|
||||
return value.strip().strip('"').strip("'")
|
||||
raise SystemExit(
|
||||
f"no KB_REVIEW_PASSWORD/KB_REVIEW_DB_PASSWORD/PGPASSWORD entry in {secrets_file}"
|
||||
)
|
||||
|
||||
|
||||
def load_review_note(args: argparse.Namespace) -> str:
|
||||
if args.review_note and args.review_note_file:
|
||||
|
|
@ -72,40 +94,41 @@ def assert_approvable(proposal: dict[str, Any], reviewed_by: str, review_note: s
|
|||
f"proposal {proposal_id} has no payload.apply_payload; run/repair normalization before approval"
|
||||
)
|
||||
|
||||
# Validate the strict contract before recording approval. This is pure SQL
|
||||
# building: it does not execute canonical writes.
|
||||
ap.build_apply_sql(proposal, ap.SERVICE_AGENT_HANDLE)
|
||||
# Validate the exact strict contract before recording approval. The builder
|
||||
# requires review metadata, so validate against a synthetic snapshot that
|
||||
# mirrors what the DB function will stamp. This builds SQL only.
|
||||
validation_snapshot = dict(proposal)
|
||||
validation_snapshot.update(
|
||||
{
|
||||
"status": "approved",
|
||||
"reviewed_by_handle": reviewed_by.strip(),
|
||||
"reviewed_by_agent_id": None,
|
||||
"reviewed_at": "2000-01-01T00:00:00+00:00",
|
||||
"review_note": review_note.strip(),
|
||||
}
|
||||
)
|
||||
ap.build_apply_sql(validation_snapshot, ap.SERVICE_AGENT_HANDLE)
|
||||
|
||||
|
||||
def build_approve_sql(proposal_id: str, reviewed_by: str, review_note: str) -> str:
|
||||
allowed_types_sql = ", ".join(ap.sql_literal(t) for t in ap.APPLYABLE_TYPES)
|
||||
def build_approve_sql(
|
||||
proposal: dict[str, Any], reviewed_by: str, review_note: str
|
||||
) -> str:
|
||||
proposal_id = str(proposal["id"])
|
||||
proposal_type = str(proposal["proposal_type"])
|
||||
payload = proposal.get("payload")
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("proposal payload must be an object")
|
||||
payload_sql = ap.sql_literal(json.dumps(payload, sort_keys=True)) + "::jsonb"
|
||||
return f"""
|
||||
begin;
|
||||
with reviewed as (
|
||||
update kb_stage.kb_proposals
|
||||
set status = 'approved',
|
||||
reviewed_by_handle = {ap.sql_literal(reviewed_by)},
|
||||
reviewed_at = now(),
|
||||
review_note = {ap.sql_literal(review_note)},
|
||||
updated_at = now()
|
||||
where id = {ap.sql_literal(proposal_id)}::uuid
|
||||
and status = 'pending_review'
|
||||
and proposal_type in ({allowed_types_sql})
|
||||
and payload ? 'apply_payload'
|
||||
returning id, proposal_type, status, reviewed_by_handle, reviewed_at, review_note
|
||||
)
|
||||
select coalesce(
|
||||
jsonb_agg(jsonb_build_object(
|
||||
'id', id::text,
|
||||
'proposal_type', proposal_type,
|
||||
'status', status,
|
||||
'reviewed_by_handle', reviewed_by_handle,
|
||||
'reviewed_at', reviewed_at::text,
|
||||
'review_note', review_note
|
||||
)),
|
||||
'[]'::jsonb
|
||||
)::text
|
||||
from reviewed;
|
||||
set local standard_conforming_strings = on;
|
||||
select jsonb_build_array(kb_stage.approve_strict_proposal(
|
||||
{ap.sql_literal(proposal_id)}::uuid,
|
||||
{ap.sql_literal(proposal_type)},
|
||||
{payload_sql},
|
||||
{ap.sql_literal(reviewed_by)},
|
||||
{ap.sql_literal(review_note)}
|
||||
))::text;
|
||||
commit;
|
||||
""".lstrip()
|
||||
|
||||
|
|
@ -129,11 +152,11 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|||
note.add_argument("--review-note", help="verbatim review note to record")
|
||||
note.add_argument("--review-note-file", help="path containing the verbatim review note")
|
||||
parser.add_argument("--dry-run", action="store_true", help="print the SQL, do not execute the approval")
|
||||
parser.add_argument("--secrets-file", default=ap.DEFAULT_SECRETS_FILE)
|
||||
parser.add_argument("--secrets-file", default=DEFAULT_REVIEW_SECRETS_FILE)
|
||||
parser.add_argument("--container", default=ap.DEFAULT_CONTAINER)
|
||||
parser.add_argument("--db", default=ap.DEFAULT_DB)
|
||||
parser.add_argument("--host", default=ap.DEFAULT_HOST)
|
||||
parser.add_argument("--role", default=ap.DEFAULT_ROLE)
|
||||
parser.add_argument("--role", default=DEFAULT_REVIEW_ROLE)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
|
|
@ -141,10 +164,10 @@ def main(argv: list[str] | None = None) -> int:
|
|||
args = parse_args(sys.argv[1:] if argv is None else argv)
|
||||
review_note = load_review_note(args)
|
||||
|
||||
password = ap.load_password(args.secrets_file)
|
||||
password = load_review_password(args.secrets_file)
|
||||
proposal = ap.load_proposal(args, password)
|
||||
assert_approvable(proposal, args.reviewed_by, review_note)
|
||||
sql = build_approve_sql(args.proposal_id, args.reviewed_by.strip(), review_note)
|
||||
sql = build_approve_sql(proposal, args.reviewed_by.strip(), review_note)
|
||||
|
||||
if args.dry_run:
|
||||
print(sql)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,8 +3,9 @@
|
|||
|
||||
Leo and reviewers can approve rich proposal intent before it is mechanically
|
||||
safe to apply. The apply worker intentionally accepts only strict contracts:
|
||||
``add_edge``, ``attach_evidence``, and ``revise_strategy`` proposals that carry
|
||||
``payload.apply_payload`` with canonical IDs.
|
||||
``add_edge``, ``attach_evidence``, ``revise_strategy``, and atomic
|
||||
``approve_claim`` bundles that carry ``payload.apply_payload`` with canonical
|
||||
IDs and reviewed row content.
|
||||
|
||||
This helper is read-only by default. It inspects a freeform proposal and reports:
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ import hashlib
|
|||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -29,10 +31,16 @@ sys.path.insert(0, str(HERE))
|
|||
|
||||
import apply_proposal as ap # noqa: E402
|
||||
import kb_proposal_review_packet as review_packet # noqa: E402
|
||||
import kb_rich_proposal_creation_plan as rich_plan # noqa: E402
|
||||
|
||||
UUID_RE = re.compile(
|
||||
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
||||
)
|
||||
RICH_CANDIDATE_COLLECTIONS = (
|
||||
("claim_candidates", "claim_key", "claim"),
|
||||
("proposed_claims", "claim_key", "claim"),
|
||||
("source_candidates", "source_key", "source"),
|
||||
)
|
||||
|
||||
|
||||
def _payload_dict(payload: dict[str, Any], key: str) -> dict[str, Any]:
|
||||
|
|
@ -94,6 +102,190 @@ def _blocked(kind: str, reason: str, missing: list[str], fragment: Any) -> dict[
|
|||
}
|
||||
|
||||
|
||||
def _candidate_inventory(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate rich candidate inputs before the planner can skip or dedupe them."""
|
||||
inventory: dict[str, Any] = {
|
||||
"claim_items": [],
|
||||
"source_items": [],
|
||||
"collection_counts": {},
|
||||
"blocked_fragments": [],
|
||||
}
|
||||
|
||||
for collection, key_field, kind in RICH_CANDIDATE_COLLECTIONS:
|
||||
if collection not in payload:
|
||||
inventory["collection_counts"][collection] = 0
|
||||
continue
|
||||
candidates = payload.get(collection)
|
||||
if not isinstance(candidates, list):
|
||||
block = _blocked(
|
||||
f"{kind}_candidate",
|
||||
f"payload.{collection} must be a list of objects",
|
||||
[key_field],
|
||||
candidates,
|
||||
)
|
||||
block["location"] = f"payload.{collection}"
|
||||
inventory["blocked_fragments"].append(block)
|
||||
inventory["collection_counts"][collection] = None
|
||||
continue
|
||||
|
||||
inventory["collection_counts"][collection] = len(candidates)
|
||||
for index, candidate in enumerate(candidates):
|
||||
location = f"payload.{collection}[{index}]"
|
||||
if not isinstance(candidate, dict):
|
||||
block = _blocked(
|
||||
f"{kind}_candidate",
|
||||
f"{location} must be an object",
|
||||
[key_field],
|
||||
candidate,
|
||||
)
|
||||
block["location"] = location
|
||||
inventory["blocked_fragments"].append(block)
|
||||
continue
|
||||
|
||||
raw_key = candidate.get(key_field)
|
||||
if not isinstance(raw_key, str) or not raw_key.strip():
|
||||
block = _blocked(
|
||||
f"{kind}_candidate",
|
||||
f"{location} is missing a nonempty string {key_field}",
|
||||
[key_field],
|
||||
candidate,
|
||||
)
|
||||
block["location"] = location
|
||||
inventory["blocked_fragments"].append(block)
|
||||
continue
|
||||
|
||||
inventory[f"{kind}_items"].append(
|
||||
{
|
||||
"collection": collection,
|
||||
"index": index,
|
||||
"key": raw_key.strip(),
|
||||
"fragment": candidate,
|
||||
}
|
||||
)
|
||||
|
||||
for kind, key_field in (("claim", "claim_key"), ("source", "source_key")):
|
||||
by_key: dict[str, list[dict[str, Any]]] = {}
|
||||
for item in inventory[f"{kind}_items"]:
|
||||
by_key.setdefault(item["key"], []).append(item)
|
||||
for candidate_key, occurrences in by_key.items():
|
||||
if len(occurrences) == 1:
|
||||
continue
|
||||
locations = [f"payload.{item['collection']}[{item['index']}]" for item in occurrences]
|
||||
block = _blocked(
|
||||
f"{kind}_candidate",
|
||||
f"duplicate {key_field} {candidate_key!r} at {', '.join(locations)}",
|
||||
[],
|
||||
[item["fragment"] for item in occurrences],
|
||||
)
|
||||
block["locations"] = locations
|
||||
inventory["blocked_fragments"].append(block)
|
||||
|
||||
return inventory
|
||||
|
||||
|
||||
def _planned_row_accounting(
|
||||
plan: dict[str, Any], inventory: dict[str, Any]
|
||||
) -> tuple[dict[str, Any], list[str]]:
|
||||
"""Reconcile every validated input candidate to exactly one planned row."""
|
||||
rows = plan.get("planned_rows") if isinstance(plan.get("planned_rows"), dict) else {}
|
||||
generated = plan.get("generated_ids") if isinstance(plan.get("generated_ids"), dict) else {}
|
||||
claim_id_map = generated.get("claims") if isinstance(generated.get("claims"), dict) else {}
|
||||
source_id_map = generated.get("sources") if isinstance(generated.get("sources"), dict) else {}
|
||||
body_source_id_map = (
|
||||
generated.get("claim_body_sources") if isinstance(generated.get("claim_body_sources"), dict) else {}
|
||||
)
|
||||
claim_rows = rows.get("claims") if isinstance(rows.get("claims"), list) else []
|
||||
source_rows = rows.get("sources") if isinstance(rows.get("sources"), list) else []
|
||||
|
||||
claim_keys = [item["key"] for item in inventory["claim_items"]]
|
||||
source_keys = [item["key"] for item in inventory["source_items"]]
|
||||
actual_claim_ids = [row.get("id") for row in claim_rows if isinstance(row, dict)]
|
||||
actual_source_ids = [row.get("id") for row in source_rows if isinstance(row, dict)]
|
||||
expected_claim_ids = [claim_id_map.get(key) for key in claim_keys]
|
||||
expected_candidate_source_ids = [source_id_map.get(key) for key in source_keys]
|
||||
expected_body_source_ids = [body_source_id_map.get(key) for key in claim_keys]
|
||||
candidate_source_id_set = {value for value in expected_candidate_source_ids if value is not None}
|
||||
body_source_id_set = {value for value in expected_body_source_ids if value is not None}
|
||||
planned_candidate_source_ids = [value for value in actual_source_ids if value in candidate_source_id_set]
|
||||
planned_body_source_ids = [value for value in actual_source_ids if value in body_source_id_set]
|
||||
|
||||
accounting = {
|
||||
"input_collections": inventory["collection_counts"],
|
||||
"claim_candidates": {
|
||||
"input_count": len(claim_keys),
|
||||
"input_keys": claim_keys,
|
||||
"planned_row_count": len(claim_rows),
|
||||
"planned_row_ids": actual_claim_ids,
|
||||
},
|
||||
"source_candidates": {
|
||||
"input_count": len(source_keys),
|
||||
"input_keys": source_keys,
|
||||
"expected_row_ids": expected_candidate_source_ids,
|
||||
"planned_row_count": len(planned_candidate_source_ids),
|
||||
"planned_row_ids": planned_candidate_source_ids,
|
||||
},
|
||||
"generated_claim_body_sources": {
|
||||
"expected_count": len(claim_keys),
|
||||
"expected_row_ids": expected_body_source_ids,
|
||||
"planned_row_count": len(planned_body_source_ids),
|
||||
"planned_row_ids": planned_body_source_ids,
|
||||
},
|
||||
"total_planned_source_rows": len(source_rows),
|
||||
}
|
||||
|
||||
errors: list[str] = []
|
||||
if set(claim_id_map) != set(claim_keys):
|
||||
errors.append("planned claim key set does not exactly match validated claim candidates")
|
||||
if set(source_id_map) != set(source_keys):
|
||||
errors.append("planned source key set does not exactly match validated source candidates")
|
||||
if set(body_source_id_map) != set(claim_keys):
|
||||
errors.append("planned claim-body source key set does not exactly match validated claim candidates")
|
||||
if any(value is None for value in expected_claim_ids):
|
||||
errors.append("one or more validated claim candidates has no generated claim id")
|
||||
if any(value is None for value in expected_candidate_source_ids):
|
||||
errors.append("one or more validated source candidates has no generated source id")
|
||||
if any(value is None for value in expected_body_source_ids):
|
||||
errors.append("one or more validated claim candidates has no generated claim-body source id")
|
||||
if Counter(actual_claim_ids) != Counter(expected_claim_ids):
|
||||
errors.append("planned claim rows do not account for every validated claim candidate exactly once")
|
||||
if Counter(actual_source_ids) != Counter(expected_candidate_source_ids + expected_body_source_ids):
|
||||
errors.append(
|
||||
"planned source rows do not account for every validated source candidate and generated claim body exactly once"
|
||||
)
|
||||
return accounting, errors
|
||||
|
||||
|
||||
def _normalization_manifest(plan: dict[str, Any], accounting: dict[str, Any]) -> dict[str, Any]:
|
||||
rows = plan["planned_rows"]
|
||||
body_source_ids = set(plan["generated_ids"]["claim_body_sources"].values())
|
||||
claim_type_mappings: list[dict[str, Any]] = []
|
||||
source_type_mappings: list[dict[str, Any]] = []
|
||||
|
||||
for row in rows["claims"]:
|
||||
mapping = row.get("type_normalization")
|
||||
if mapping:
|
||||
claim_type_mappings.append(
|
||||
{"claim_key": row.get("claim_key"), "claim_id": row.get("id"), **dict(mapping)}
|
||||
)
|
||||
for row in rows["sources"]:
|
||||
mapping = row.get("source_type_normalization")
|
||||
if mapping:
|
||||
source_type_mappings.append(
|
||||
{
|
||||
"source_key": row.get("source_key"),
|
||||
"source_id": row.get("id"),
|
||||
"generated_claim_body_source": row.get("id") in body_source_ids,
|
||||
**dict(mapping),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"candidate_accounting": accounting,
|
||||
"claim_type_mappings": claim_type_mappings,
|
||||
"source_type_mappings": source_type_mappings,
|
||||
}
|
||||
|
||||
|
||||
def _edge_candidate(fragment: dict[str, Any], default_from_claim: str | None = None) -> dict[str, Any] | None:
|
||||
edge_type = fragment.get("edge_type")
|
||||
from_claim = _first_uuid(fragment, ("from_claim", "from_claim_id", "from_claim_uuid")) or default_from_claim
|
||||
|
|
@ -137,6 +329,88 @@ def _strict_child(kind: str, apply_payload: dict[str, Any], source_fragment: Any
|
|||
}
|
||||
|
||||
|
||||
def _strict_approve_claim_child(
|
||||
proposal: dict[str, Any], plan: dict[str, Any], normalization_manifest: dict[str, Any]
|
||||
) -> dict[str, Any] | None:
|
||||
"""Convert a fully resolved rich graph into one atomic strict child."""
|
||||
rows = plan["planned_rows"]
|
||||
if not rows["claims"]:
|
||||
return None
|
||||
if plan["blocked_fragments"] or plan["unresolved_design_decisions"]:
|
||||
return None
|
||||
|
||||
agent_id = proposal.get("proposed_by_agent_id")
|
||||
apply_payload = {
|
||||
"contract_version": 2,
|
||||
"source_proposal_id": proposal.get("id"),
|
||||
"agent_id": agent_id,
|
||||
"claims": [
|
||||
{
|
||||
key: row.get(key)
|
||||
for key in ("id", "type", "text", "status", "confidence", "tags", "created_by")
|
||||
}
|
||||
for row in rows["claims"]
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
key: row.get(key)
|
||||
for key in ("id", "source_type", "url", "storage_path", "excerpt", "hash", "created_by")
|
||||
}
|
||||
for row in rows["sources"]
|
||||
],
|
||||
"evidence": [
|
||||
{
|
||||
"claim_id": row.get("claim_id"),
|
||||
"source_id": row.get("source_id"),
|
||||
"role": row.get("role"),
|
||||
"weight": row.get("weight"),
|
||||
"created_by": agent_id,
|
||||
}
|
||||
for row in rows["claim_evidence"]
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"from_claim": row.get("from_claim"),
|
||||
"to_claim": row.get("to_claim"),
|
||||
"edge_type": row.get("edge_type"),
|
||||
"weight": row.get("weight"),
|
||||
"created_by": agent_id,
|
||||
}
|
||||
for row in rows["claim_edges"]
|
||||
],
|
||||
"reasoning_tools": [
|
||||
{
|
||||
key: row.get(key)
|
||||
for key in ("id", "agent_id", "name", "description", "category")
|
||||
}
|
||||
for row in rows["reasoning_tools"]
|
||||
],
|
||||
"normalization_manifest": normalization_manifest,
|
||||
}
|
||||
return _strict_child("approve_claim", apply_payload, proposal.get("payload") or {})
|
||||
|
||||
|
||||
def _rich_plan_blockers(plan: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
blocked: list[dict[str, Any]] = []
|
||||
for fragment in plan.get("blocked_fragments") or []:
|
||||
reasons = fragment.get("reasons") if isinstance(fragment, dict) else None
|
||||
reason = "; ".join(str(item) for item in reasons) if isinstance(reasons, list) else "rich fragment is blocked"
|
||||
source_fragment = fragment.get("fragment") if isinstance(fragment, dict) else fragment
|
||||
kind = fragment.get("kind", "rich_fragment") if isinstance(fragment, dict) else "rich_fragment"
|
||||
blocked.append(_blocked(kind, reason, [], source_fragment))
|
||||
for decision in plan.get("unresolved_design_decisions") or []:
|
||||
if isinstance(decision, dict):
|
||||
blocked.append(
|
||||
_blocked(
|
||||
"design_decision",
|
||||
str(decision.get("reason") or "rich proposal has an unresolved design decision"),
|
||||
[str(decision.get("decision") or "reviewed design decision")],
|
||||
decision,
|
||||
)
|
||||
)
|
||||
return blocked
|
||||
|
||||
|
||||
def _normalize_edges(payload: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
strict: list[dict[str, Any]] = []
|
||||
blocked: list[dict[str, Any]] = []
|
||||
|
|
@ -260,11 +534,27 @@ def _dedupe_children(children: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||
return out
|
||||
|
||||
|
||||
def normalize_proposal(proposal: dict[str, Any]) -> dict[str, Any]:
|
||||
def normalize_proposal(
|
||||
proposal: dict[str, Any], mappings: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
payload = proposal.get("payload") or {}
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
|
||||
inventory = _candidate_inventory(payload)
|
||||
if inventory["blocked_fragments"]:
|
||||
return {
|
||||
"proposal_id": proposal.get("id"),
|
||||
"normalization_state": "blocked_lossy_rich_normalization",
|
||||
"strict_child_proposals": [],
|
||||
"blocked_fragments": inventory["blocked_fragments"],
|
||||
"blocked_count": len(inventory["blocked_fragments"]),
|
||||
"strict_child_count": 0,
|
||||
"next_normalization_action": (
|
||||
"Repair every malformed, missing-key, or duplicate rich candidate, then rerun normalization."
|
||||
),
|
||||
}
|
||||
|
||||
if payload.get("apply_payload") and proposal.get("proposal_type") in ap.APPLYABLE_TYPES:
|
||||
return {
|
||||
"proposal_id": proposal.get("id"),
|
||||
|
|
@ -276,6 +566,75 @@ def normalize_proposal(proposal: dict[str, Any]) -> dict[str, Any]:
|
|||
"next_normalization_action": "Proposal already carries a strict apply_payload; use dry-run/apply review.",
|
||||
}
|
||||
|
||||
has_rich_candidates = bool(inventory["claim_items"] or inventory["source_items"])
|
||||
if has_rich_candidates:
|
||||
plan = rich_plan.build_creation_plan(proposal, mappings)
|
||||
accounting, accounting_errors = _planned_row_accounting(plan, inventory)
|
||||
if not inventory["claim_items"]:
|
||||
accounting_errors.append(
|
||||
"rich source candidates cannot be emitted by approve_claim without at least one claim candidate"
|
||||
)
|
||||
if accounting_errors:
|
||||
accounting_blocks = [
|
||||
{
|
||||
"kind": "candidate_accounting",
|
||||
"reason": reason,
|
||||
"missing": [],
|
||||
"candidate_accounting": accounting,
|
||||
"fragment": {"fingerprint": _fingerprint(accounting)},
|
||||
}
|
||||
for reason in accounting_errors
|
||||
]
|
||||
return {
|
||||
"proposal_id": proposal.get("id"),
|
||||
"normalization_state": "blocked_lossy_rich_normalization",
|
||||
"strict_child_proposals": [],
|
||||
"blocked_fragments": accounting_blocks,
|
||||
"blocked_count": len(accounting_blocks),
|
||||
"strict_child_count": 0,
|
||||
"next_normalization_action": (
|
||||
"Repair rich planner row accounting so every candidate maps to exactly one planned row, then rerun."
|
||||
),
|
||||
}
|
||||
|
||||
manifest = _normalization_manifest(plan, accounting)
|
||||
approve_claim_child = _strict_approve_claim_child(proposal, plan, manifest)
|
||||
if approve_claim_child:
|
||||
return {
|
||||
"proposal_id": proposal.get("id"),
|
||||
"normalization_state": "strict_children_ready",
|
||||
"strict_child_proposals": [approve_claim_child],
|
||||
"blocked_fragments": [],
|
||||
"blocked_count": 0,
|
||||
"strict_child_count": 1,
|
||||
"next_normalization_action": (
|
||||
"Review and stage the atomic approve_claim child, then use dry-run/apply review."
|
||||
),
|
||||
}
|
||||
|
||||
blocked_fragments = _rich_plan_blockers(plan)
|
||||
if not blocked_fragments:
|
||||
blocked_fragments = [
|
||||
{
|
||||
"kind": "rich_plan",
|
||||
"reason": "rich candidates were accounted for but no atomic strict child was produced",
|
||||
"missing": [],
|
||||
"candidate_accounting": accounting,
|
||||
"fragment": {"fingerprint": _fingerprint(accounting)},
|
||||
}
|
||||
]
|
||||
return {
|
||||
"proposal_id": proposal.get("id"),
|
||||
"normalization_state": "blocked_rich_normalization",
|
||||
"strict_child_proposals": [],
|
||||
"blocked_fragments": blocked_fragments,
|
||||
"blocked_count": len(blocked_fragments),
|
||||
"strict_child_count": 0,
|
||||
"next_normalization_action": (
|
||||
"Resolve every blocked rich fragment or design decision, then rerun the atomic normalization."
|
||||
),
|
||||
}
|
||||
|
||||
strict_children: list[dict[str, Any]] = []
|
||||
blocked_fragments: list[dict[str, Any]] = []
|
||||
for strict, blocked in (
|
||||
|
|
@ -325,19 +684,26 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|||
parser.add_argument("--proposal-id", help="load one live proposal from Postgres")
|
||||
parser.add_argument("--status", default="approved", help="load live proposals by status")
|
||||
parser.add_argument("--limit", type=int, default=20)
|
||||
parser.add_argument("--mapping-json", help="optional reviewed mapping overlay for rich claim graphs")
|
||||
parser.add_argument("--secrets-file", default=ap.DEFAULT_SECRETS_FILE)
|
||||
parser.add_argument("--container", default=ap.DEFAULT_CONTAINER)
|
||||
parser.add_argument("--db", default=ap.DEFAULT_DB)
|
||||
parser.add_argument("--host", default=ap.DEFAULT_HOST)
|
||||
parser.add_argument("--role", default=ap.DEFAULT_ROLE)
|
||||
parser.add_argument("--output", type=Path, help="optional JSON output path")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(sys.argv[1:] if argv is None else argv)
|
||||
proposals = load_from_input(args.input_json) if args.input_json else load_from_db(args)
|
||||
previews = [normalize_proposal(proposal) for proposal in proposals]
|
||||
print(json.dumps({"normalization_previews": previews}, indent=2, sort_keys=True))
|
||||
mappings = json.loads(Path(args.mapping_json).read_text(encoding="utf-8")) if args.mapping_json else None
|
||||
previews = [normalize_proposal(proposal, mappings) for proposal in proposals]
|
||||
rendered = json.dumps({"normalization_previews": previews}, indent=2, sort_keys=True) + "\n"
|
||||
if args.output:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(rendered, encoding="utf-8")
|
||||
print(rendered, end="")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,11 @@ import os
|
|||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
from typing import Any
|
||||
|
||||
REPORT_DIR = Path("docs/reports/leo-working-state-20260709")
|
||||
DEFAULT_OUT = REPORT_DIR / "gcp-db-parity-current-probe-current.json"
|
||||
|
|
@ -155,14 +156,49 @@ def accounts_from_auth_list(auth_list: dict[str, Any], fallback_account: str | N
|
|||
return accounts
|
||||
|
||||
|
||||
def flatten_command_results(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
flattened: list[dict[str, Any]] = []
|
||||
for result in results:
|
||||
if "stderr_excerpt" in result:
|
||||
flattened.append(result)
|
||||
continue
|
||||
for value in result.values():
|
||||
if isinstance(value, dict):
|
||||
flattened.extend(flatten_command_results([value]))
|
||||
elif isinstance(value, list):
|
||||
flattened.extend(flatten_command_results([item for item in value if isinstance(item, dict)]))
|
||||
return flattened
|
||||
|
||||
|
||||
def has_network_dns_error(results: list[dict[str, Any]]) -> bool:
|
||||
needle_set = ("Failed to resolve", "NameResolutionError", "nodename nor servname", "oauth2.googleapis.com")
|
||||
return any(any(needle in item.get("stderr_excerpt", "") for needle in needle_set) for item in results)
|
||||
needle_set = (
|
||||
"failed to resolve",
|
||||
"nameresolutionerror",
|
||||
"nodename nor servname",
|
||||
"name or service not known",
|
||||
"temporary failure in name resolution",
|
||||
"could not resolve host",
|
||||
)
|
||||
return any(
|
||||
any(needle in item.get("stderr_excerpt", "").lower() for needle in needle_set)
|
||||
for item in flatten_command_results(results)
|
||||
)
|
||||
|
||||
|
||||
def has_reauth_error(results: list[dict[str, Any]]) -> bool:
|
||||
needle_set = ("Reauthentication failed", "cannot prompt during non-interactive execution", "gcloud auth login")
|
||||
return any(any(needle in item.get("stderr_excerpt", "") for needle in needle_set) for item in results)
|
||||
needle_set = ("reauthentication failed", "cannot prompt during non-interactive execution", "gcloud auth login")
|
||||
return any(
|
||||
any(needle in item.get("stderr_excerpt", "").lower() for needle in needle_set)
|
||||
for item in flatten_command_results(results)
|
||||
)
|
||||
|
||||
|
||||
def classify_failure(results: list[dict[str, Any]]) -> str | None:
|
||||
if has_network_dns_error(results):
|
||||
return "dns_unavailable"
|
||||
if has_reauth_error(results):
|
||||
return "reauth_required"
|
||||
return None
|
||||
|
||||
|
||||
def summarize_project(project_result: dict[str, Any]) -> dict[str, Any] | None:
|
||||
|
|
@ -227,7 +263,7 @@ def status_from_results(
|
|||
command_results.append(instance_readback)
|
||||
if sql_readback is not None:
|
||||
command_results.append(sql_readback)
|
||||
if has_network_dns_error(command_results):
|
||||
if classify_failure(command_results) == "dns_unavailable":
|
||||
return "blocked_on_network_dns_before_auth_refresh"
|
||||
if selected_account != expected_account:
|
||||
return "blocked_on_wrong_active_account"
|
||||
|
|
@ -235,7 +271,8 @@ def status_from_results(
|
|||
return "blocked_on_wrong_active_project"
|
||||
selected_token = next((item for item in token_checks if item["account"] == selected_account), None)
|
||||
if selected_token and not selected_token["token_refresh"]["ok"]:
|
||||
return "blocked_on_gcloud_reauth" if has_reauth_error([selected_token["token_refresh"]]) else "blocked_on_token_refresh"
|
||||
classification = classify_failure([selected_token])
|
||||
return "blocked_on_gcloud_reauth" if classification == "reauth_required" else "blocked_on_token_refresh"
|
||||
if not project_readback["ok"]:
|
||||
return "blocked_on_project_access"
|
||||
if instance_readback is not None and not instance_readback["ok"]:
|
||||
|
|
|
|||
2141
scripts/run_approve_claim_clone_canary.py
Normal file
2141
scripts/run_approve_claim_clone_canary.py
Normal file
File diff suppressed because it is too large
Load diff
328
scripts/run_approve_claim_isolated_container_canary.sh
Executable file
328
scripts/run_approve_claim_isolated_container_canary.sh
Executable file
|
|
@ -0,0 +1,328 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
source_container="${SOURCE_CONTAINER:-teleo-pg}"
|
||||
source_db="${SOURCE_DB:-teleo}"
|
||||
service="${LEO_SERVICE:-leoclean-gateway.service}"
|
||||
image="${POSTGRES_CANARY_IMAGE:-postgres:16}"
|
||||
normalization_json=""
|
||||
output="${repo_root}/docs/reports/leo-working-state-20260709/approve-claim-clone-canary-current.json"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--normalization-json)
|
||||
normalization_json="$2"
|
||||
shift 2
|
||||
;;
|
||||
--output)
|
||||
output="$2"
|
||||
shift 2
|
||||
;;
|
||||
--source-container)
|
||||
source_container="$2"
|
||||
shift 2
|
||||
;;
|
||||
--source-db)
|
||||
source_db="$2"
|
||||
shift 2
|
||||
;;
|
||||
--service)
|
||||
service="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
container="working-leo-approve-claim-$RANDOM-$$"
|
||||
workdir="/tmp/${container}"
|
||||
apply_password="isolated-apply-${RANDOM}-${RANDOM}"
|
||||
review_password="isolated-review-${RANDOM}-${RANDOM}"
|
||||
|
||||
cleanup() {
|
||||
docker rm -f "$container" >/dev/null 2>&1 || true
|
||||
rm -rf "$workdir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
source_counts() {
|
||||
docker exec "$source_container" psql -U postgres -d "$source_db" -Atq \
|
||||
-v ON_ERROR_STOP=1 -c "begin transaction read only;
|
||||
select 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),
|
||||
'public.reasoning_tools',(select count(*) from public.reasoning_tools),
|
||||
'kb_stage.kb_proposals',(select count(*) from kb_stage.kb_proposals)
|
||||
)::text;
|
||||
rollback;"
|
||||
}
|
||||
|
||||
service_state() {
|
||||
systemctl show "$service" --no-pager \
|
||||
-p ActiveState -p SubState -p MainPID -p NRestarts -p ExecMainStartTimestamp
|
||||
}
|
||||
|
||||
mkdir -p "$workdir" "$(dirname "$output")"
|
||||
source_counts_before="$(source_counts)"
|
||||
service_before="$(service_state)"
|
||||
|
||||
docker run -d --name "$container" \
|
||||
-e POSTGRES_PASSWORD="isolated-postgres-${RANDOM}-${RANDOM}" \
|
||||
-e POSTGRES_DB=teleo \
|
||||
"$image" >/dev/null
|
||||
|
||||
for _ in $(seq 1 40); do
|
||||
if docker exec "$container" pg_isready -U postgres -d teleo >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
docker exec "$container" pg_isready -U postgres -d teleo >/dev/null
|
||||
|
||||
docker exec "$source_container" pg_dump -U postgres -d "$source_db" \
|
||||
--schema-only --no-owner --no-privileges >"$workdir/schema.sql"
|
||||
docker exec "$source_container" pg_dump -U postgres -d "$source_db" \
|
||||
--data-only --no-owner --no-privileges \
|
||||
--table=public.agents --table=public.claims >"$workdir/reference-data.sql"
|
||||
docker cp "$workdir/schema.sql" "$container:/tmp/schema.sql"
|
||||
docker cp "$workdir/reference-data.sql" "$container:/tmp/reference-data.sql"
|
||||
|
||||
docker exec -i "$container" psql -U postgres -d postgres -v ON_ERROR_STOP=1 -q <<SQL
|
||||
create role kb_apply login noinherit nosuperuser nocreatedb nocreaterole
|
||||
noreplication nobypassrls password '${apply_password}';
|
||||
create role kb_review login noinherit nosuperuser nocreatedb nocreaterole
|
||||
noreplication nobypassrls password '${review_password}';
|
||||
create role kb_gate_owner nologin noinherit nosuperuser nocreatedb nocreaterole
|
||||
noreplication nobypassrls;
|
||||
SQL
|
||||
docker exec "$container" psql -U postgres -d teleo -v ON_ERROR_STOP=1 -q \
|
||||
-f /tmp/schema.sql
|
||||
docker exec "$container" psql -U postgres -d teleo -v ON_ERROR_STOP=1 -q \
|
||||
-f /tmp/reference-data.sql
|
||||
docker exec "$container" psql -U postgres -d teleo -v ON_ERROR_STOP=1 -q \
|
||||
-c "insert into public.agents (id, handle, kind)
|
||||
values ('99999999-9999-9999-9999-999999999999', 'm3ta', 'human')
|
||||
on conflict (handle) do nothing"
|
||||
|
||||
cat >"$workdir/kb-apply.env" <<EOF
|
||||
KB_APPLY_PASSWORD=${apply_password}
|
||||
EOF
|
||||
cat >"$workdir/kb-review.env" <<EOF
|
||||
KB_REVIEW_PASSWORD=${review_password}
|
||||
EOF
|
||||
chmod 600 "$workdir/kb-apply.env" "$workdir/kb-review.env"
|
||||
|
||||
normalization_args=()
|
||||
if [[ -n "$normalization_json" ]]; then
|
||||
normalization_args=(--normalization-json "$normalization_json")
|
||||
fi
|
||||
|
||||
set +e
|
||||
python3 "$repo_root/scripts/run_approve_claim_clone_canary.py" \
|
||||
--container "$container" \
|
||||
--source-db teleo \
|
||||
--database-prefix teleo_approve_claim_isolated \
|
||||
--service "$service" \
|
||||
--approve-script "$repo_root/scripts/approve_proposal.py" \
|
||||
--apply-script "$repo_root/scripts/apply_proposal.py" \
|
||||
--prereqs "$repo_root/scripts/kb_apply_prereqs.sql" \
|
||||
--review-secrets-file "$workdir/kb-review.env" \
|
||||
--secrets-file "$workdir/kb-apply.env" \
|
||||
"${normalization_args[@]}" \
|
||||
--output "$output" \
|
||||
>"$workdir/stdout.json" 2>"$workdir/stderr.log"
|
||||
canary_rc=$?
|
||||
set -e
|
||||
|
||||
set +e
|
||||
docker rm -f "$container" >/dev/null 2>&1
|
||||
outer_container_remove_rc=$?
|
||||
rm -rf "$workdir"
|
||||
outer_workdir_remove_rc=$?
|
||||
container_name_readback="$(
|
||||
docker container ls -a --filter "name=^/${container}$" --format '{{.Names}}' 2>&1
|
||||
)"
|
||||
container_name_readback_rc=$?
|
||||
if [[ -e "$workdir" || -L "$workdir" ]]; then
|
||||
outer_workdir_lexists=true
|
||||
else
|
||||
outer_workdir_lexists=false
|
||||
fi
|
||||
set -e
|
||||
trap - EXIT
|
||||
|
||||
source_counts_after="$(source_counts)"
|
||||
service_after="$(service_state)"
|
||||
|
||||
SOURCE_COUNTS_BEFORE="$source_counts_before" \
|
||||
SOURCE_COUNTS_AFTER="$source_counts_after" \
|
||||
SOURCE_CONTAINER="$source_container" \
|
||||
SOURCE_DB="$source_db" \
|
||||
SERVICE_BEFORE="$service_before" \
|
||||
SERVICE_AFTER="$service_after" \
|
||||
REPO_ROOT="$repo_root" \
|
||||
OUTER_CONTAINER_REMOVE_RC="$outer_container_remove_rc" \
|
||||
OUTER_WORKDIR_REMOVE_RC="$outer_workdir_remove_rc" \
|
||||
CONTAINER_NAME_READBACK="$container_name_readback" \
|
||||
CONTAINER_NAME_READBACK_RC="$container_name_readback_rc" \
|
||||
OUTER_WORKDIR_LEXISTS="$outer_workdir_lexists" \
|
||||
CANARY_RC="$canary_rc" \
|
||||
python3 - "$output" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
if not path.is_file():
|
||||
raise SystemExit("inner canary did not produce a report")
|
||||
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
def service(raw: str) -> dict[str, str]:
|
||||
return dict(line.split("=", 1) for line in raw.splitlines() if "=" in line)
|
||||
|
||||
def table_deltas(before: dict[str, int], after: dict[str, int]) -> dict[str, int]:
|
||||
if set(before) != set(after):
|
||||
raise SystemExit(
|
||||
"live source count keys changed during the canary: "
|
||||
f"before={sorted(before)} after={sorted(after)}"
|
||||
)
|
||||
return {key: after[key] - before[key] for key in sorted(before)}
|
||||
|
||||
def verify_source_binding(root: Path, records: list[dict[str, object]]) -> dict[str, object]:
|
||||
readbacks = []
|
||||
for record in records:
|
||||
relative_text = str(record.get("repo_relative_path", ""))
|
||||
relative = Path(relative_text)
|
||||
if not relative_text or relative.is_absolute() or ".." in relative.parts:
|
||||
raise SystemExit(f"unsafe repo-relative source path: {relative_text!r}")
|
||||
candidate = (root / relative).resolve()
|
||||
try:
|
||||
candidate.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise SystemExit(
|
||||
f"source path escaped repository root: {relative_text}"
|
||||
) from exc
|
||||
if not candidate.is_file():
|
||||
readbacks.append(
|
||||
{
|
||||
"repo_relative_path": relative.as_posix(),
|
||||
"expected_sha256": record.get("sha256"),
|
||||
"actual_sha256": None,
|
||||
"matches": False,
|
||||
}
|
||||
)
|
||||
continue
|
||||
content = candidate.read_bytes()
|
||||
actual_sha256 = hashlib.sha256(content).hexdigest()
|
||||
readbacks.append(
|
||||
{
|
||||
"repo_relative_path": relative.as_posix(),
|
||||
"expected_sha256": record.get("sha256"),
|
||||
"actual_sha256": actual_sha256,
|
||||
"size_bytes": len(content),
|
||||
"matches": (
|
||||
actual_sha256 == record.get("sha256")
|
||||
and len(content) == record.get("size_bytes")
|
||||
),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"files": readbacks,
|
||||
"all_match": bool(readbacks) and all(row["matches"] for row in readbacks),
|
||||
}
|
||||
|
||||
def contains_ephemeral_path(value: object) -> bool:
|
||||
if isinstance(value, str):
|
||||
return "/tmp/" in value
|
||||
if isinstance(value, dict):
|
||||
return any(contains_ephemeral_path(item) for item in value.values())
|
||||
if isinstance(value, list):
|
||||
return any(contains_ephemeral_path(item) for item in value)
|
||||
return False
|
||||
|
||||
before_counts = json.loads(os.environ["SOURCE_COUNTS_BEFORE"])
|
||||
after_counts = json.loads(os.environ["SOURCE_COUNTS_AFTER"])
|
||||
live_table_deltas = table_deltas(before_counts, after_counts)
|
||||
before_service = service(os.environ["SERVICE_BEFORE"])
|
||||
after_service = service(os.environ["SERVICE_AFTER"])
|
||||
service_stable = all(
|
||||
before_service.get(key) == after_service.get(key)
|
||||
for key in ("ActiveState", "SubState", "MainPID", "NRestarts")
|
||||
)
|
||||
container_absent = (
|
||||
int(os.environ["CONTAINER_NAME_READBACK_RC"]) == 0
|
||||
and not os.environ["CONTAINER_NAME_READBACK"].strip()
|
||||
)
|
||||
workdir_absent = os.environ["OUTER_WORKDIR_LEXISTS"] == "false"
|
||||
source_binding = verify_source_binding(
|
||||
Path(os.environ["REPO_ROOT"]).resolve(),
|
||||
data.get("source_binding", {}).get("files", []),
|
||||
)
|
||||
data["source_binding"]["independent_post_cleanup_verification"] = source_binding
|
||||
source_binding_ok = (
|
||||
data["source_binding"].get("unchanged_during_run") is True
|
||||
and source_binding["all_match"] is True
|
||||
)
|
||||
stale_ephemeral_paths_absent = not contains_ephemeral_path(data)
|
||||
inner_runtime_pass = data.get("status") == "pass" and int(os.environ["CANARY_RC"]) == 0
|
||||
outer_ok = (
|
||||
before_counts == after_counts
|
||||
and all(delta == 0 for delta in live_table_deltas.values())
|
||||
and service_stable
|
||||
and container_absent
|
||||
and workdir_absent
|
||||
and source_binding_ok
|
||||
and stale_ephemeral_paths_absent
|
||||
)
|
||||
data["required_tier"] = "T3_live_readonly"
|
||||
data["required_tiers"] = ["T2_runtime", "T3_live_readonly"]
|
||||
data["runtime_scope"] = "isolated_postgres_container_from_readonly_live_schema"
|
||||
data["outer_isolation"] = {
|
||||
"source_container": os.environ.get("SOURCE_CONTAINER", "teleo-pg"),
|
||||
"source_database": os.environ.get("SOURCE_DB", "teleo"),
|
||||
"source_counts_before": before_counts,
|
||||
"source_counts_after": after_counts,
|
||||
"canonical_table_deltas": live_table_deltas,
|
||||
"source_counts_unchanged": before_counts == after_counts,
|
||||
"service_before": before_service,
|
||||
"service_after": after_service,
|
||||
"service_stable": service_stable,
|
||||
"cleanup_readback": {
|
||||
"container_remove_returncode": int(os.environ["OUTER_CONTAINER_REMOVE_RC"]),
|
||||
"container_name_query_returncode": int(
|
||||
os.environ["CONTAINER_NAME_READBACK_RC"]
|
||||
),
|
||||
"container_name_query_stdout": os.environ["CONTAINER_NAME_READBACK"],
|
||||
"temporary_container_absent": container_absent,
|
||||
"workdir_remove_returncode": int(os.environ["OUTER_WORKDIR_REMOVE_RC"]),
|
||||
"workdir_lexists_after_cleanup": not workdir_absent,
|
||||
"temporary_workdir_absent": workdir_absent,
|
||||
},
|
||||
}
|
||||
data["checks"]["outer_live_counts_exactly_unchanged"] = before_counts == after_counts
|
||||
data["checks"]["outer_live_table_deltas_exactly_zero"] = all(
|
||||
delta == 0 for delta in live_table_deltas.values()
|
||||
)
|
||||
data["checks"]["outer_service_stable"] = service_stable
|
||||
data["checks"]["outer_container_absent_independent_readback"] = container_absent
|
||||
data["checks"]["outer_workdir_absent_independent_readback"] = workdir_absent
|
||||
data["checks"]["source_binding_independently_verified"] = source_binding_ok
|
||||
data["checks"]["stale_ephemeral_paths_absent"] = stale_ephemeral_paths_absent
|
||||
data["checks"]["outer_isolation_complete"] = outer_ok
|
||||
if outer_ok and inner_runtime_pass:
|
||||
data["current_tier"] = "T3_live_readonly"
|
||||
else:
|
||||
data["status"] = "fail"
|
||||
data["current_tier"] = "T2_runtime" if inner_runtime_pass else "T1_model"
|
||||
path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
raise SystemExit(0 if data["status"] == "pass" else 1)
|
||||
PY
|
||||
|
|
@ -4,8 +4,8 @@ Pure tests: exercise the SQL builders, payload validation, and dispatch without
|
|||
a live database. Runnable under pytest or standalone (`python3 tests/test_apply_proposal.py`).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import pytest
|
||||
|
|
@ -31,9 +31,24 @@ sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|||
import apply_proposal as ap # noqa: E402
|
||||
|
||||
|
||||
def _approval_meta(proposal_type, apply_payload):
|
||||
return {
|
||||
"proposal_type": proposal_type,
|
||||
"payload": {"apply_payload": apply_payload},
|
||||
"reviewed_by_handle": "m3ta",
|
||||
"reviewed_by_agent_id": None,
|
||||
"reviewed_at": "2026-07-10T00:00:00+00:00",
|
||||
"review_note": "Reviewed exact strict payload.",
|
||||
}
|
||||
|
||||
|
||||
# --- literals ------------------------------------------------------------- #
|
||||
def test_sql_literal_escapes_single_quotes():
|
||||
assert ap.sql_literal("O'Brien") == "'O''Brien'"
|
||||
assert ap.sql_literal("O'Brien") == "E'O''Brien'"
|
||||
|
||||
|
||||
def test_sql_literal_escapes_backslashes_independently_of_session_mode():
|
||||
assert ap.sql_literal("a\\b'c") == "E'a\\\\b''c'"
|
||||
|
||||
|
||||
def test_sql_literal_none_is_null():
|
||||
|
|
@ -64,7 +79,8 @@ def _revise_payload():
|
|||
|
||||
|
||||
def test_revise_strategy_sql_shape():
|
||||
sql = ap.build_revise_strategy_sql(_revise_payload()["apply_payload"], "pid-1", "m3ta")
|
||||
payload = _revise_payload()["apply_payload"]
|
||||
sql = ap.build_revise_strategy_sql(payload, "pid-1", "kb-apply", _approval_meta("revise_strategy", payload))
|
||||
assert sql.startswith("begin;")
|
||||
assert sql.rstrip().endswith("commit;")
|
||||
# deactivate old, insert versioned active, retire nodes, insert new nodes
|
||||
|
|
@ -74,7 +90,7 @@ def test_revise_strategy_sql_shape():
|
|||
assert "update public.strategy_nodes" in sql
|
||||
assert "insert into public.strategy_nodes" in sql
|
||||
# ledger + invariant
|
||||
assert "status = 'applied'" in sql
|
||||
assert "kb_stage.finish_approved_proposal" in sql
|
||||
assert "exactly one active strategy" in sql
|
||||
# proximate_objectives rendered as jsonb
|
||||
assert "::jsonb" in sql
|
||||
|
|
@ -84,45 +100,320 @@ def test_revise_strategy_requires_agent_id():
|
|||
payload = _revise_payload()["apply_payload"]
|
||||
del payload["agent_id"]
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_revise_strategy_sql(payload, "pid-1", None)
|
||||
ap.build_revise_strategy_sql(payload, "pid-1", None, _approval_meta("revise_strategy", payload))
|
||||
|
||||
|
||||
def test_revise_strategy_rejects_bad_node_type():
|
||||
payload = _revise_payload()["apply_payload"]
|
||||
payload["strategy_nodes"][0]["node_type"] = "manifesto"
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_revise_strategy_sql(payload, "pid-1", None)
|
||||
ap.build_revise_strategy_sql(payload, "pid-1", None, _approval_meta("revise_strategy", payload))
|
||||
|
||||
|
||||
# --- add_edge ------------------------------------------------------------- #
|
||||
def test_add_edge_sql_dedup_guard():
|
||||
payload = {"from_claim": "aaaa", "to_claim": "bbbb", "edge_type": "supersedes", "weight": 0.9}
|
||||
sql = ap.build_add_edge_sql(payload, "pid-2", None)
|
||||
sql = ap.build_add_edge_sql(payload, "pid-2", None, _approval_meta("add_edge", payload))
|
||||
assert "insert into public.claim_edges" in sql
|
||||
assert "not exists" in sql
|
||||
assert "::edge_type" in sql
|
||||
assert "status = 'applied'" in sql
|
||||
assert "kb_stage.finish_approved_proposal" in sql
|
||||
assert "pg_advisory_xact_lock" in sql
|
||||
|
||||
|
||||
def test_add_edge_semantic_row_accepts_match_and_rejects_weight_mismatch():
|
||||
payload = {"from_claim": "aaaa", "to_claim": "bbbb", "edge_type": "supports", "weight": 0.9}
|
||||
sql = ap.build_add_edge_sql(payload, "pid-2", None, _approval_meta("add_edge", payload))
|
||||
verify = sql[sql.index("do $verify$") : sql.index("kb_stage.finish_approved_proposal")]
|
||||
|
||||
assert "weight is not distinct from 0.9" in verify
|
||||
assert "weight is distinct from 0.9" in verify
|
||||
assert "claim_edge row does not match strict apply payload" in verify
|
||||
assert sql.startswith("begin;")
|
||||
assert sql.rstrip().endswith("commit;")
|
||||
assert sql.index("raise exception 'apply_proposal: claim_edge row") < sql.index(
|
||||
"kb_stage.finish_approved_proposal"
|
||||
)
|
||||
|
||||
|
||||
def test_add_edge_rejects_self_loop():
|
||||
payload = {"from_claim": "same", "to_claim": "same", "edge_type": "supports"}
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_add_edge_sql(payload, "pid-2", None)
|
||||
ap.build_add_edge_sql(payload, "pid-2", None, _approval_meta("add_edge", payload))
|
||||
|
||||
|
||||
# --- attach_evidence ------------------------------------------------------ #
|
||||
def test_attach_evidence_sql_with_source_id():
|
||||
payload = {"evidence": [{"claim_id": "cccc", "source_id": "dddd", "role": "grounds", "weight": 0.78}]}
|
||||
sql = ap.build_attach_evidence_sql(payload, "pid-3", None)
|
||||
sql = ap.build_attach_evidence_sql(payload, "pid-3", None, _approval_meta("attach_evidence", payload))
|
||||
assert "insert into public.claim_evidence" in sql
|
||||
assert "::evidence_role" in sql
|
||||
assert "not exists" in sql
|
||||
|
||||
|
||||
def test_attach_evidence_semantic_row_accepts_match_and_rejects_weight_mismatch():
|
||||
payload = {
|
||||
"evidence": [
|
||||
{"claim_id": "cccc", "source_id": "dddd", "role": "grounds", "weight": 0.78}
|
||||
]
|
||||
}
|
||||
sql = ap.build_attach_evidence_sql(
|
||||
payload, "pid-3", None, _approval_meta("attach_evidence", payload)
|
||||
)
|
||||
verify = sql[sql.index("do $verify$") : sql.index("kb_stage.finish_approved_proposal")]
|
||||
|
||||
assert "weight is not distinct from 0.78" in verify
|
||||
assert "weight is distinct from 0.78" in verify
|
||||
assert "evidence row % does not match strict apply payload" in verify
|
||||
assert sql.startswith("begin;")
|
||||
assert sql.rstrip().endswith("commit;")
|
||||
assert sql.index("raise exception 'apply_proposal: evidence row") < sql.index(
|
||||
"kb_stage.finish_approved_proposal"
|
||||
)
|
||||
|
||||
|
||||
def test_attach_evidence_requires_source_id():
|
||||
payload = {"evidence": [{"claim_id": "cccc"}]}
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_attach_evidence_sql(payload, "pid-3", None)
|
||||
ap.build_attach_evidence_sql(payload, "pid-3", None, _approval_meta("attach_evidence", payload))
|
||||
|
||||
|
||||
# --- approve_claim canonical bundle -------------------------------------- #
|
||||
def _approve_claim_bundle():
|
||||
claim_a = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
||||
claim_b = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
|
||||
source_a = "cccccccc-cccc-4ccc-8ccc-cccccccccccc"
|
||||
source_b = "dddddddd-dddd-4ddd-8ddd-dddddddddddd"
|
||||
return {
|
||||
"contract_version": 2,
|
||||
"agent_id": "11111111-1111-4111-8111-111111111111",
|
||||
"claims": [
|
||||
{
|
||||
"id": claim_a,
|
||||
"type": "structural",
|
||||
"text": "Canonical claim A",
|
||||
"status": "open",
|
||||
"confidence": 0.8,
|
||||
"tags": ["test", "working-leo"],
|
||||
"created_by": None,
|
||||
},
|
||||
{
|
||||
"id": claim_b,
|
||||
"type": "concept",
|
||||
"text": "Canonical claim B",
|
||||
"status": "open",
|
||||
"confidence": None,
|
||||
"tags": [],
|
||||
"created_by": None,
|
||||
},
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"id": source_a,
|
||||
"source_type": "paper",
|
||||
"url": "https://example.test/a",
|
||||
"storage_path": None,
|
||||
"excerpt": "Evidence A",
|
||||
"hash": "hash-a",
|
||||
"created_by": None,
|
||||
},
|
||||
{
|
||||
"id": source_b,
|
||||
"source_type": "observation",
|
||||
"url": None,
|
||||
"storage_path": None,
|
||||
"excerpt": "Evidence B",
|
||||
"hash": "hash-b",
|
||||
"created_by": None,
|
||||
},
|
||||
],
|
||||
"evidence": [
|
||||
{"claim_id": claim_a, "source_id": source_a, "role": "grounds", "weight": 0.9},
|
||||
{"claim_id": claim_b, "source_id": source_b, "role": "illustrates", "weight": None},
|
||||
],
|
||||
"edges": [
|
||||
{"from_claim": claim_a, "to_claim": claim_b, "edge_type": "supports", "weight": 0.7}
|
||||
],
|
||||
"reasoning_tools": [
|
||||
{
|
||||
"id": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
||||
"agent_id": None,
|
||||
"name": "Canonical test tool",
|
||||
"description": "A strict reasoning-tool row.",
|
||||
"category": "test",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_approve_claim_bundle_sql_is_atomic_and_row_verified():
|
||||
payload = _approve_claim_bundle()
|
||||
sql = ap.build_approve_claim_sql(
|
||||
payload,
|
||||
"99999999-9999-4999-8999-999999999999",
|
||||
None,
|
||||
_approval_meta("approve_claim", payload),
|
||||
)
|
||||
assert sql.startswith("begin;")
|
||||
assert sql.rstrip().endswith("commit;")
|
||||
assert "insert into public.claims" in sql
|
||||
assert "insert into public.sources" in sql
|
||||
assert "insert into public.claim_evidence" in sql
|
||||
assert "insert into public.claim_edges" in sql
|
||||
assert "insert into public.reasoning_tools" in sql
|
||||
assert "source hash collision" in sql
|
||||
assert "claim row does not match strict apply payload" in sql
|
||||
assert "source row does not match strict apply payload" in sql
|
||||
assert "reasoning tool row does not match strict apply payload" in sql
|
||||
assert "kb_stage.finish_approved_proposal" in sql
|
||||
|
||||
|
||||
def test_approve_claim_semantic_rows_accept_matches_and_reject_payload_mismatches():
|
||||
payload = _approve_claim_bundle()
|
||||
sql = ap.build_approve_claim_sql(
|
||||
payload,
|
||||
"99999999-9999-4999-8999-999999999999",
|
||||
None,
|
||||
_approval_meta("approve_claim", payload),
|
||||
)
|
||||
verify = sql[sql.index("do $verify$") : sql.index("kb_stage.finish_approved_proposal")]
|
||||
|
||||
assert "weight is not distinct from 0.9" in verify
|
||||
assert "weight is distinct from 0.9" in verify
|
||||
assert "weight is not distinct from 0.7" in verify
|
||||
assert "weight is distinct from 0.7" in verify
|
||||
assert "weight is not distinct from null" in verify
|
||||
assert "weight is distinct from null" in verify
|
||||
expected_creator = "E'11111111-1111-4111-8111-111111111111'::uuid"
|
||||
assert f"created_by is not distinct from {expected_creator}" in verify
|
||||
assert f"created_by is distinct from {expected_creator}" in verify
|
||||
assert "approve_claim: evidence row does not match strict apply payload" in verify
|
||||
assert "approve_claim: edge row does not match strict apply payload" in verify
|
||||
assert sql.startswith("begin;")
|
||||
assert sql.rstrip().endswith("commit;")
|
||||
assert sql.index("raise exception 'approve_claim: evidence row") < sql.index(
|
||||
"kb_stage.finish_approved_proposal"
|
||||
)
|
||||
|
||||
|
||||
def test_approve_claim_bundle_rejects_duplicate_source_hashes():
|
||||
payload = _approve_claim_bundle()
|
||||
payload["sources"][1]["hash"] = payload["sources"][0]["hash"]
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_approve_claim_sql(
|
||||
payload,
|
||||
"99999999-9999-4999-8999-999999999999",
|
||||
None,
|
||||
_approval_meta("approve_claim", payload),
|
||||
)
|
||||
|
||||
|
||||
def test_approve_claim_bundle_rejects_invalid_enum_and_self_edge():
|
||||
payload = _approve_claim_bundle()
|
||||
payload["claims"][0]["type"] = "vibes"
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_approve_claim_sql(
|
||||
payload,
|
||||
"99999999-9999-4999-8999-999999999999",
|
||||
None,
|
||||
_approval_meta("approve_claim", payload),
|
||||
)
|
||||
|
||||
payload = _approve_claim_bundle()
|
||||
payload["edges"][0]["to_claim"] = payload["edges"][0]["from_claim"]
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_approve_claim_sql(
|
||||
payload,
|
||||
"99999999-9999-4999-8999-999999999999",
|
||||
None,
|
||||
_approval_meta("approve_claim", payload),
|
||||
)
|
||||
|
||||
|
||||
def test_approve_claim_rejects_unknown_keys_and_contract_versions():
|
||||
payload = _approve_claim_bundle()
|
||||
payload["contract_version"] = 1
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_approve_claim_sql(
|
||||
payload,
|
||||
"99999999-9999-4999-8999-999999999999",
|
||||
None,
|
||||
_approval_meta("approve_claim", payload),
|
||||
)
|
||||
|
||||
payload = _approve_claim_bundle()
|
||||
payload["contract_version"] = 2
|
||||
payload["claimz"] = payload.pop("claims")
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_approve_claim_sql(
|
||||
payload,
|
||||
"99999999-9999-4999-8999-999999999999",
|
||||
None,
|
||||
_approval_meta("approve_claim", payload),
|
||||
)
|
||||
|
||||
|
||||
def test_approve_claim_verifies_superseded_by_and_pins_string_mode():
|
||||
payload = _approve_claim_bundle()
|
||||
payload["contract_version"] = 2
|
||||
payload["claims"][0]["superseded_by"] = None
|
||||
sql = ap.build_approve_claim_sql(
|
||||
payload,
|
||||
"99999999-9999-4999-8999-999999999999",
|
||||
None,
|
||||
_approval_meta("approve_claim", payload),
|
||||
)
|
||||
assert "superseded_by" in sql
|
||||
assert "set local standard_conforming_strings = on" in sql.lower()
|
||||
|
||||
|
||||
def test_apply_sql_locks_and_binds_approval_before_canonical_write():
|
||||
payload = _approve_claim_bundle()
|
||||
payload["contract_version"] = 2
|
||||
proposal = {
|
||||
"id": "99999999-9999-4999-8999-999999999999",
|
||||
"proposal_type": "approve_claim",
|
||||
"status": "approved",
|
||||
"payload": {"apply_payload": payload},
|
||||
"reviewed_by_handle": "m3ta",
|
||||
"reviewed_by_agent_id": None,
|
||||
"reviewed_at": "2026-07-10T00:00:00+00:00",
|
||||
"review_note": "Reviewed exact strict payload.",
|
||||
}
|
||||
sql = ap.build_apply_sql(proposal, "kb-apply")
|
||||
assert "kb_stage.assert_approved_proposal" in sql
|
||||
assert "kb_stage.finish_approved_proposal" in sql
|
||||
assert sql.index("kb_stage.assert_approved_proposal") < sql.index("insert into public.claims")
|
||||
assert sql.index("insert into public.claims") < sql.index("kb_stage.finish_approved_proposal")
|
||||
assert "Reviewed exact strict payload." in sql
|
||||
|
||||
|
||||
def test_build_apply_sql_requires_review_provenance():
|
||||
proposal = {
|
||||
"id": "99999999-9999-4999-8999-999999999999",
|
||||
"proposal_type": "approve_claim",
|
||||
"status": "approved",
|
||||
"payload": {"apply_payload": _approve_claim_bundle()},
|
||||
}
|
||||
with pytest.raises(ValueError):
|
||||
ap.build_apply_sql(proposal, "kb-apply")
|
||||
|
||||
|
||||
def test_approve_claim_dispatch_and_prerequisite_grants():
|
||||
proposal = {
|
||||
"id": "99999999-9999-4999-8999-999999999999",
|
||||
"proposal_type": "approve_claim",
|
||||
"payload": {"apply_payload": _approve_claim_bundle()},
|
||||
}
|
||||
proposal.update(_approval_meta("approve_claim", proposal["payload"]["apply_payload"]))
|
||||
sql = ap.build_apply_sql(proposal, None)
|
||||
assert "insert into public.claims" in sql
|
||||
assert "approve_claim" in ap.APPLYABLE_TYPES
|
||||
|
||||
prereqs = (REPO_ROOT / "scripts" / "kb_apply_prereqs.sql").read_text(encoding="utf-8")
|
||||
assert "grant select, insert on public.claims to kb_apply" in prereqs.lower()
|
||||
assert "grant select, insert on public.sources to kb_apply" in prereqs.lower()
|
||||
assert "grant select, insert on public.reasoning_tools to kb_apply" in prereqs.lower()
|
||||
|
||||
|
||||
# --- dispatch + guards ---------------------------------------------------- #
|
||||
|
|
@ -153,33 +444,39 @@ def test_assert_applyable_allows_approved():
|
|||
|
||||
|
||||
# --- ledger flip: concurrency guard + FK stamp --------------------------- #
|
||||
def test_ledger_flip_asserts_rowcount_one():
|
||||
# The flip must guard against a concurrent double-apply by asserting exactly
|
||||
# one 'approved' row moved, not by re-reading status afterwards.
|
||||
sql = ap.build_add_edge_sql({"from_claim": "a", "to_claim": "b", "edge_type": "supports"}, "pid", None)
|
||||
assert "get diagnostics" in sql
|
||||
assert "flipped <> 1" in sql
|
||||
assert "and status = 'approved'" in sql
|
||||
def test_ledger_transition_uses_payload_bound_security_functions():
|
||||
payload = {"from_claim": "a", "to_claim": "b", "edge_type": "supports"}
|
||||
sql = ap.build_add_edge_sql(payload, "pid", None, _approval_meta("add_edge", payload))
|
||||
assert "kb_stage.assert_approved_proposal" in sql
|
||||
assert "kb_stage.finish_approved_proposal" in sql
|
||||
assert sql.index("kb_stage.assert_approved_proposal") < sql.index("insert into public.claim_edges")
|
||||
assert sql.index("insert into public.claim_edges") < sql.index("kb_stage.finish_approved_proposal")
|
||||
|
||||
prereqs = (REPO_ROOT / "scripts" / "kb_apply_prereqs.sql").read_text(encoding="utf-8").lower()
|
||||
assert "status = 'approved'" in prereqs
|
||||
assert "for update" in prereqs
|
||||
assert "payload = p_payload" in prereqs
|
||||
|
||||
|
||||
def test_ledger_flip_stamps_agent_fk():
|
||||
sql = ap.build_add_edge_sql({"from_claim": "a", "to_claim": "b", "edge_type": "supports"}, "pid", "kb-apply")
|
||||
payload = {"from_claim": "a", "to_claim": "b", "edge_type": "supports"}
|
||||
sql = ap.build_add_edge_sql(payload, "pid", "kb-apply", _approval_meta("add_edge", payload))
|
||||
# Hard resolve into a variable + NOT-NULL assert, never a silent inline
|
||||
# subselect that would stamp NULL on an unresolved handle.
|
||||
assert "select id into resolved_agent_id" in sql
|
||||
assert "resolved_agent_id is null then" in sql
|
||||
assert "applied_by_agent_id = resolved_agent_id" in sql
|
||||
assert "applied_by_handle = 'kb-apply'" in sql
|
||||
assert "kb_stage.finish_approved_proposal" in sql
|
||||
assert "'kb-apply'" in sql
|
||||
|
||||
|
||||
def test_build_apply_sql_defaults_applied_by_to_service_agent():
|
||||
payload = {"from_claim": "a", "to_claim": "b", "edge_type": "supports"}
|
||||
proposal = {
|
||||
"id": "pid",
|
||||
"proposal_type": "add_edge",
|
||||
"payload": {"apply_payload": {"from_claim": "a", "to_claim": "b", "edge_type": "supports"}},
|
||||
"payload": {"apply_payload": payload},
|
||||
}
|
||||
proposal.update(_approval_meta("add_edge", payload))
|
||||
sql = ap.build_apply_sql(proposal, None)
|
||||
assert f"applied_by_handle = '{ap.SERVICE_AGENT_HANDLE}'" in sql
|
||||
assert f"E'{ap.SERVICE_AGENT_HANDLE}'" in sql
|
||||
|
||||
|
||||
def test_load_password_accepts_live_db_password_key(tmp_path):
|
||||
|
|
@ -197,7 +494,7 @@ if __name__ == "__main__":
|
|||
try:
|
||||
fn()
|
||||
print(f"PASS {name}")
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
failures += 1
|
||||
print(f"FAIL {name}")
|
||||
traceback.print_exc()
|
||||
|
|
|
|||
|
|
@ -25,7 +25,14 @@ except ImportError: # standalone fallback so the file runs without pytest insta
|
|||
return
|
||||
raise AssertionError(f"expected {exc.__name__} to be raised")
|
||||
|
||||
def _fixture(*_args, **_kwargs):
|
||||
def decorator(func):
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
pytest.raises = _raises
|
||||
pytest.fixture = _fixture
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
|
@ -36,11 +43,13 @@ import apply_worker as w # noqa: E402
|
|||
@pytest.fixture(autouse=True)
|
||||
def restore_apply_worker_dependencies():
|
||||
original_load_password = w.ap.load_password
|
||||
original_load_failure_state = w.load_failure_state
|
||||
original_fetch_candidates = w.fetch_candidates
|
||||
original_apply_one = w.apply_one
|
||||
original_render_one = w.render_one
|
||||
yield
|
||||
w.ap.load_password = original_load_password
|
||||
w.load_failure_state = original_load_failure_state
|
||||
w.fetch_candidates = original_fetch_candidates
|
||||
w.apply_one = original_apply_one
|
||||
w.render_one = original_render_one
|
||||
|
|
@ -68,6 +77,20 @@ def test_candidate_query_lists_worker_types_only():
|
|||
assert "reject_claim" not in sql
|
||||
|
||||
|
||||
def test_candidate_query_can_exclude_poisoned_ids_before_limit():
|
||||
sql = w.build_candidate_query(excluded_ids=("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",))
|
||||
assert "id::text not in" in sql.lower()
|
||||
assert "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" in sql
|
||||
assert sql.lower().index("id::text not in") < sql.lower().index("limit 20")
|
||||
|
||||
|
||||
def test_worker_includes_approved_claim_bundles_without_unproven_render_hook():
|
||||
assert "approve_claim" in w.WORKER_TYPES
|
||||
assert "approve_claim" not in w.RENDER_TYPES
|
||||
sql = w.build_candidate_query()
|
||||
assert "'approve_claim'" in sql
|
||||
|
||||
|
||||
# --- candidate parsing ---------------------------------------------------- #
|
||||
def test_parse_candidates_skips_blank_lines():
|
||||
out = '{"id": "a", "proposal_type": "add_edge", "agent_id": null}\n\n'
|
||||
|
|
@ -106,7 +129,9 @@ def _args(**over):
|
|||
def test_report_only_gate_does_not_apply(monkeypatch=None):
|
||||
calls = []
|
||||
w.ap.load_password = lambda _f: "pw"
|
||||
w.fetch_candidates = lambda a, p: [{"id": "p1", "proposal_type": "revise_strategy", "agent_id": "ag"}]
|
||||
w.fetch_candidates = lambda a, p, excluded: [
|
||||
{"id": "p1", "proposal_type": "revise_strategy", "agent_id": "ag"}
|
||||
]
|
||||
w.apply_one = lambda a, pid: calls.append(pid)
|
||||
|
||||
rc = w.run(_args(enable=False))
|
||||
|
|
@ -117,7 +142,9 @@ def test_report_only_gate_does_not_apply(monkeypatch=None):
|
|||
def test_enabled_worker_applies_and_renders():
|
||||
applied, rendered = [], []
|
||||
w.ap.load_password = lambda _f: "pw"
|
||||
w.fetch_candidates = lambda a, p: [{"id": "p1", "proposal_type": "revise_strategy", "agent_id": "ag"}]
|
||||
w.fetch_candidates = lambda a, p, excluded: [
|
||||
{"id": "p1", "proposal_type": "revise_strategy", "agent_id": "ag"}
|
||||
]
|
||||
w.apply_one = lambda a, pid: applied.append(pid)
|
||||
w.render_one = lambda a, agent_id: rendered.append(agent_id) or "ok"
|
||||
|
||||
|
|
@ -130,7 +157,9 @@ def test_enabled_worker_applies_and_renders():
|
|||
def test_enabled_worker_skips_render_for_add_edge():
|
||||
applied, rendered = [], []
|
||||
w.ap.load_password = lambda _f: "pw"
|
||||
w.fetch_candidates = lambda a, p: [{"id": "e1", "proposal_type": "add_edge", "agent_id": None}]
|
||||
w.fetch_candidates = lambda a, p, excluded: [
|
||||
{"id": "e1", "proposal_type": "add_edge", "agent_id": None}
|
||||
]
|
||||
w.apply_one = lambda a, pid: applied.append(pid)
|
||||
w.render_one = lambda a, agent_id: rendered.append(agent_id) or "ok"
|
||||
|
||||
|
|
@ -140,6 +169,21 @@ def test_enabled_worker_skips_render_for_add_edge():
|
|||
assert rendered == [] # add_edge changes the graph, not the rendered self
|
||||
|
||||
|
||||
def test_enabled_worker_skips_render_for_approve_claim():
|
||||
applied, rendered = [], []
|
||||
w.ap.load_password = lambda _f: "pw"
|
||||
w.fetch_candidates = lambda a, p, excluded: [
|
||||
{"id": "c1", "proposal_type": "approve_claim", "agent_id": "ag"}
|
||||
]
|
||||
w.apply_one = lambda a, pid: applied.append(pid)
|
||||
w.render_one = lambda a, agent_id: rendered.append(agent_id) or "ok"
|
||||
|
||||
rc = w.run(_args(enable=True))
|
||||
assert rc == 0
|
||||
assert applied == ["c1"]
|
||||
assert rendered == [] # no proven agent-owned approve_claim render contract
|
||||
|
||||
|
||||
# --- per-tick cap + poison-pill ceiling ----------------------------------- #
|
||||
def _cands(*ids):
|
||||
return [{"id": i, "proposal_type": "add_edge", "agent_id": None} for i in ids]
|
||||
|
|
@ -160,7 +204,7 @@ def test_partition_skips_poison_pill_over_ceiling():
|
|||
def test_enabled_worker_caps_applies_at_one_per_tick():
|
||||
applied = []
|
||||
w.ap.load_password = lambda _f: "pw"
|
||||
w.fetch_candidates = lambda a, p: _cands("e1", "e2", "e3")
|
||||
w.fetch_candidates = lambda a, p, excluded: _cands("e1", "e2", "e3")
|
||||
w.apply_one = lambda a, pid: applied.append(pid)
|
||||
|
||||
rc = w.run(_args(enable=True, max_per_tick=1))
|
||||
|
|
@ -185,7 +229,7 @@ def test_apply_failure_increments_persisted_count():
|
|||
|
||||
path = str(Path(tempfile.mkdtemp()) / "failures.json")
|
||||
w.ap.load_password = lambda _f: "pw"
|
||||
w.fetch_candidates = lambda a, p: _cands("bad")
|
||||
w.fetch_candidates = lambda a, p, excluded: _cands("bad")
|
||||
|
||||
def _boom(a, pid):
|
||||
raise RuntimeError("deterministic failure")
|
||||
|
|
@ -196,12 +240,31 @@ def test_apply_failure_increments_persisted_count():
|
|||
assert w.load_failure_state(path).get("bad") == 1 # count persisted for next tick
|
||||
|
||||
|
||||
def test_run_loads_failure_state_and_excludes_exhausted_ids_before_fetch():
|
||||
events = []
|
||||
poisoned_id = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
||||
w.ap.load_password = lambda _f: "pw"
|
||||
w.load_failure_state = lambda _path: events.append("load") or {poisoned_id: 3}
|
||||
|
||||
def _fetch(args, password, excluded_ids):
|
||||
events.append(("fetch", excluded_ids))
|
||||
return []
|
||||
|
||||
w.fetch_candidates = _fetch
|
||||
|
||||
rc = w.run(_args(max_attempts=3))
|
||||
assert rc == 0
|
||||
assert events == ["load", ("fetch", (poisoned_id,))]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import traceback
|
||||
|
||||
failures = 0
|
||||
for name, fn in sorted(globals().items()):
|
||||
if name.startswith("test_") and callable(fn):
|
||||
restore = restore_apply_worker_dependencies()
|
||||
next(restore)
|
||||
try:
|
||||
fn()
|
||||
print(f"PASS {name}")
|
||||
|
|
@ -209,4 +272,9 @@ if __name__ == "__main__":
|
|||
failures += 1
|
||||
print(f"FAIL {name}")
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
try:
|
||||
next(restore)
|
||||
except StopIteration:
|
||||
pass
|
||||
sys.exit(1 if failures else 0)
|
||||
|
|
|
|||
|
|
@ -82,15 +82,52 @@ def test_assert_approvable_rejects_freeform_without_apply_payload():
|
|||
|
||||
|
||||
def test_build_approve_sql_has_required_guards():
|
||||
sql = approve.build_approve_sql("00957f6c-9883-4015-95a4-6b09367efb0e", "m3ta", "O'Brien approves")
|
||||
assert "status = 'approved'" in sql
|
||||
assert "where id = '00957f6c-9883-4015-95a4-6b09367efb0e'::uuid" in sql
|
||||
assert "and status = 'pending_review'" in sql
|
||||
assert "and payload ? 'apply_payload'" in sql
|
||||
assert "'O''Brien approves'" in sql
|
||||
assert "'add_edge'" in sql
|
||||
assert "'attach_evidence'" in sql
|
||||
assert "'revise_strategy'" in sql
|
||||
proposal = _strict_pending()
|
||||
sql = approve.build_approve_sql(proposal, "m3ta", "O'Brien approves")
|
||||
assert "kb_stage.approve_strict_proposal" in sql
|
||||
assert "update kb_stage.kb_proposals" not in sql.lower()
|
||||
assert "E'00957f6c-9883-4015-95a4-6b09367efb0e'::uuid" in sql
|
||||
assert "E'add_edge'" in sql
|
||||
assert "edge_type" in sql
|
||||
assert "supports" in sql
|
||||
assert "E'O''Brien approves'" in sql
|
||||
assert "set local standard_conforming_strings = on" in sql.lower()
|
||||
|
||||
prereqs = (REPO_ROOT / "scripts" / "kb_apply_prereqs.sql").read_text(encoding="utf-8")
|
||||
for proposal_type in ("add_edge", "attach_evidence", "revise_strategy", "approve_claim"):
|
||||
assert f"'{proposal_type}'" in prereqs
|
||||
|
||||
|
||||
def test_approval_prereqs_bind_exact_payload_and_authenticated_principal():
|
||||
prereqs = (REPO_ROOT / "scripts" / "kb_apply_prereqs.sql").read_text(encoding="utf-8").lower()
|
||||
assert "p_expected_proposal_type" in prereqs
|
||||
assert "p_expected_payload" in prereqs
|
||||
assert "payload = p_expected_payload" in prereqs
|
||||
assert "kb_review_principals" in prereqs
|
||||
assert "session_user::name" in prereqs
|
||||
assert "kb_proposal_approvals" in prereqs
|
||||
assert "owner to kb_gate_owner" in prereqs
|
||||
assert "set search_path = pg_catalog, pg_temp" in prereqs
|
||||
|
||||
|
||||
def test_review_cli_uses_separate_role_and_secret_defaults():
|
||||
args = approve.parse_args(
|
||||
[
|
||||
"00957f6c-9883-4015-95a4-6b09367efb0e",
|
||||
"--reviewed-by", "m3ta",
|
||||
"--review-note", "approved",
|
||||
]
|
||||
)
|
||||
assert args.role == "kb_review"
|
||||
assert args.secrets_file.endswith("kb-review.env")
|
||||
|
||||
prereqs = (REPO_ROOT / "scripts" / "kb_apply_prereqs.sql").read_text(encoding="utf-8").lower()
|
||||
assert "revoke update on kb_stage.kb_proposals from kb_apply" in prereqs
|
||||
assert "grant execute on function kb_stage.approve_strict_proposal" in prereqs
|
||||
assert "to kb_review" in prereqs
|
||||
assert "grant execute on function kb_stage.assert_approved_proposal" in prereqs
|
||||
assert "grant execute on function kb_stage.finish_approved_proposal" in prereqs
|
||||
assert "to kb_apply" in prereqs
|
||||
|
||||
|
||||
def test_parse_reviewed_rows_requires_exactly_one_row():
|
||||
|
|
|
|||
498
tests/test_kb_apply_prereqs.py
Normal file
498
tests/test_kb_apply_prereqs.py
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
MIGRATION = REPO_ROOT / "scripts" / "kb_apply_prereqs.sql"
|
||||
POSTGRES_IMAGE = "postgres:16-alpine"
|
||||
DATABASE = "teleo_clone"
|
||||
|
||||
BOOTSTRAP_SQL = r"""
|
||||
create role kb_gate_owner nologin inherit;
|
||||
create role kb_review login inherit;
|
||||
create role kb_apply login inherit;
|
||||
create role broad_writer nologin;
|
||||
create role rogue_owner nologin;
|
||||
create role rogue_grantee nologin;
|
||||
|
||||
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);
|
||||
create table public.sources (
|
||||
id uuid primary key,
|
||||
hash text not null
|
||||
);
|
||||
create table public.reasoning_tools (id uuid primary key);
|
||||
|
||||
insert into public.agents (id, handle, kind)
|
||||
values ('11111111-1111-1111-1111-111111111111', 'm3ta', '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
|
||||
);
|
||||
|
||||
create function kb_stage.approve_strict_proposal(uuid, text, text)
|
||||
returns jsonb language sql security definer
|
||||
as $function$ select '{}'::jsonb $function$;
|
||||
create function kb_stage.approve_strict_proposal(uuid, text, jsonb, text, text)
|
||||
returns jsonb language sql security definer
|
||||
as $function$ select '{}'::jsonb $function$;
|
||||
create function kb_stage.assert_approved_proposal(
|
||||
uuid, text, jsonb, text, uuid, timestamptz, text
|
||||
) returns void language sql security definer
|
||||
as $function$ select $function$;
|
||||
create function kb_stage.finish_approved_proposal(
|
||||
uuid, text, jsonb, text, uuid, timestamptz, text, text
|
||||
) returns jsonb language sql security definer
|
||||
as $function$ select '{}'::jsonb $function$;
|
||||
|
||||
grant all privileges on all tables in schema public
|
||||
to kb_gate_owner, kb_apply, kb_review;
|
||||
grant all privileges on all tables in schema kb_stage
|
||||
to public, kb_gate_owner, kb_apply, kb_review;
|
||||
grant all privileges on all functions in schema kb_stage
|
||||
to public, kb_gate_owner, kb_apply, kb_review;
|
||||
grant update (review_note) on kb_stage.kb_proposals to kb_apply;
|
||||
grant select (review_note) on kb_stage.kb_proposals to public;
|
||||
"""
|
||||
|
||||
|
||||
def _psql(container: str, sql: str, *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
completed = subprocess.run(
|
||||
[
|
||||
"docker",
|
||||
"exec",
|
||||
"-i",
|
||||
container,
|
||||
"psql",
|
||||
"-X",
|
||||
"-v",
|
||||
"ON_ERROR_STOP=1",
|
||||
"-U",
|
||||
"postgres",
|
||||
"-d",
|
||||
DATABASE,
|
||||
"-At",
|
||||
"-F",
|
||||
"|",
|
||||
],
|
||||
input=sql,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if check and completed.returncode != 0:
|
||||
raise AssertionError(
|
||||
f"psql failed with exit {completed.returncode}\nstdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
|
||||
)
|
||||
return completed
|
||||
|
||||
|
||||
def _apply_migration(container: str, *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return _psql(container, MIGRATION.read_text(), check=check)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def migrated_postgres() -> Iterator[str]:
|
||||
if shutil.which("docker") is None:
|
||||
pytest.skip("Docker is required for the disposable PostgreSQL migration canary")
|
||||
|
||||
container = f"kb-apply-prereqs-{uuid.uuid4().hex[:12]}"
|
||||
started = subprocess.run(
|
||||
[
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"-d",
|
||||
"--name",
|
||||
container,
|
||||
"-e",
|
||||
"POSTGRES_PASSWORD=postgres",
|
||||
"-e",
|
||||
f"POSTGRES_DB={DATABASE}",
|
||||
POSTGRES_IMAGE,
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if started.returncode != 0:
|
||||
pytest.fail(f"could not start disposable PostgreSQL: {started.stderr}")
|
||||
|
||||
try:
|
||||
for _ in range(120):
|
||||
logs = subprocess.run(
|
||||
["docker", "logs", container],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
query = _psql(container, "select 1;", check=False)
|
||||
ready_transitions = (logs.stdout + logs.stderr).count("database system is ready to accept connections")
|
||||
if ready_transitions >= 2 and query.returncode == 0:
|
||||
break
|
||||
time.sleep(0.25)
|
||||
else:
|
||||
pytest.fail("disposable PostgreSQL did not become ready")
|
||||
|
||||
_psql(container, BOOTSTRAP_SQL)
|
||||
_apply_migration(container)
|
||||
yield container
|
||||
finally:
|
||||
subprocess.run(
|
||||
["docker", "rm", "-f", container],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def _lines(completed: subprocess.CompletedProcess[str]) -> set[str]:
|
||||
return {line for line in completed.stdout.splitlines() if line}
|
||||
|
||||
|
||||
def _catalog_snapshot(container: str) -> str:
|
||||
return _psql(
|
||||
container,
|
||||
r"""
|
||||
select 'role|' || rolname || '|' || rolcanlogin || '|' || rolinherit || '|' || rolsuper
|
||||
from pg_catalog.pg_roles
|
||||
where rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
|
||||
order by rolname;
|
||||
select 'owner|' || namespace.nspname || '|' || relation.relname || '|' || owner_role.rolname
|
||||
from pg_catalog.pg_class relation
|
||||
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||
join pg_catalog.pg_roles owner_role on owner_role.oid = relation.relowner
|
||||
where (namespace.nspname, relation.relname) in (
|
||||
('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'),
|
||||
('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'),
|
||||
('public', 'sources'), ('public', 'reasoning_tools'),
|
||||
('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'),
|
||||
('kb_stage', 'kb_proposal_approvals')
|
||||
)
|
||||
order by namespace.nspname, relation.relname;
|
||||
select 'function|' || procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) ||
|
||||
')|' || owner_role.rolname || '|' || procedure.prosecdef || '|' || procedure.proconfig::text
|
||||
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
|
||||
where namespace.nspname = 'kb_stage'
|
||||
and procedure.proname in (
|
||||
'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal'
|
||||
)
|
||||
order by procedure.proname, procedure.proargtypes::text;
|
||||
select 'table_acl|' || namespace.nspname || '|' || relation.relname || '|' ||
|
||||
case when acl.grantee = 0 then 'PUBLIC' else grantee_role.rolname end || '|' ||
|
||||
pg_catalog.upper(acl.privilege_type) || '|' || acl.is_grantable
|
||||
from pg_catalog.pg_class relation
|
||||
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||
cross join lateral pg_catalog.aclexplode(
|
||||
coalesce(relation.relacl, pg_catalog.acldefault('r', relation.relowner))
|
||||
) acl
|
||||
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
||||
where (namespace.nspname, relation.relname) in (
|
||||
('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'),
|
||||
('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'),
|
||||
('public', 'sources'), ('public', 'reasoning_tools'),
|
||||
('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'),
|
||||
('kb_stage', 'kb_proposal_approvals')
|
||||
)
|
||||
and acl.grantee <> relation.relowner
|
||||
and (acl.grantee = 0 or grantee_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply'))
|
||||
order by namespace.nspname, relation.relname, grantee_role.rolname, acl.privilege_type;
|
||||
select 'function_acl|' || procedure.proname || '(' ||
|
||||
pg_catalog.oidvectortypes(procedure.proargtypes) || ')|' ||
|
||||
case when acl.grantee = 0 then 'PUBLIC' else pg_catalog.pg_get_userbyid(acl.grantee) end ||
|
||||
'|' || pg_catalog.upper(acl.privilege_type) || '|' || acl.is_grantable
|
||||
from pg_catalog.pg_proc procedure
|
||||
join pg_catalog.pg_namespace namespace on namespace.oid = procedure.pronamespace
|
||||
cross join lateral pg_catalog.aclexplode(
|
||||
coalesce(procedure.proacl, pg_catalog.acldefault('f', procedure.proowner))
|
||||
) acl
|
||||
where namespace.nspname = 'kb_stage'
|
||||
and procedure.proname in (
|
||||
'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal'
|
||||
)
|
||||
and acl.grantee <> procedure.proowner
|
||||
order by procedure.proname, procedure.proargtypes::text, acl.grantee;
|
||||
""",
|
||||
).stdout
|
||||
|
||||
|
||||
def test_upgrade_normalizes_exact_role_ownership_and_acl_matrix(migrated_postgres: str) -> None:
|
||||
role_rows = _lines(
|
||||
_psql(
|
||||
migrated_postgres,
|
||||
"""
|
||||
select rolname || '|' || rolcanlogin || '|' || rolinherit || '|' || rolsuper || '|' ||
|
||||
rolcreatedb || '|' || rolcreaterole || '|' || rolreplication || '|' || rolbypassrls
|
||||
from pg_catalog.pg_roles
|
||||
where rolname in ('kb_gate_owner', 'kb_review', 'kb_apply')
|
||||
order by rolname;
|
||||
""",
|
||||
)
|
||||
)
|
||||
assert role_rows == {
|
||||
"kb_apply|true|false|false|false|false|false|false",
|
||||
"kb_gate_owner|false|false|false|false|false|false|false",
|
||||
"kb_review|true|false|false|false|false|false|false",
|
||||
}
|
||||
|
||||
assert (
|
||||
_psql(
|
||||
migrated_postgres,
|
||||
"""
|
||||
select count(*)
|
||||
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 ('kb_gate_owner', 'kb_review', 'kb_apply')
|
||||
or granted_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply');
|
||||
""",
|
||||
).stdout.strip()
|
||||
== "0"
|
||||
)
|
||||
|
||||
schema_rows = _lines(
|
||||
_psql(
|
||||
migrated_postgres,
|
||||
"""
|
||||
select protected_role.rolname || '|' || protected_schema.nspname || '|' ||
|
||||
pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'USAGE') || '|' ||
|
||||
pg_catalog.has_schema_privilege(protected_role.rolname, protected_schema.nspname, 'CREATE')
|
||||
from (values ('kb_gate_owner'), ('kb_review'), ('kb_apply')) protected_role(rolname)
|
||||
cross join (values ('public'), ('kb_stage')) protected_schema(nspname);
|
||||
""",
|
||||
)
|
||||
)
|
||||
assert schema_rows == {
|
||||
f"{role}|{schema}|true|false"
|
||||
for role in ("kb_gate_owner", "kb_review", "kb_apply")
|
||||
for schema in ("public", "kb_stage")
|
||||
}
|
||||
|
||||
function_rows = _lines(
|
||||
_psql(
|
||||
migrated_postgres,
|
||||
"""
|
||||
select procedure.proname || '(' || pg_catalog.oidvectortypes(procedure.proargtypes) || ')|' ||
|
||||
owner_role.rolname || '|' || procedure.prosecdef || '|' || procedure.proconfig::text
|
||||
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
|
||||
where namespace.nspname = 'kb_stage'
|
||||
and procedure.proname in (
|
||||
'approve_strict_proposal', 'assert_approved_proposal', 'finish_approved_proposal'
|
||||
);
|
||||
""",
|
||||
)
|
||||
)
|
||||
assert function_rows == {
|
||||
'approve_strict_proposal(uuid, text, jsonb, text, text)|kb_gate_owner|true|{"search_path=pg_catalog, pg_temp"}',
|
||||
"assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)|"
|
||||
'kb_gate_owner|true|{"search_path=pg_catalog, pg_temp"}',
|
||||
"finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)|"
|
||||
'kb_gate_owner|true|{"search_path=pg_catalog, pg_temp"}',
|
||||
}
|
||||
assert (
|
||||
_psql(
|
||||
migrated_postgres,
|
||||
"select pg_catalog.to_regprocedure('kb_stage.approve_strict_proposal(uuid,text,text)') is null;",
|
||||
).stdout.strip()
|
||||
== "t"
|
||||
)
|
||||
assert (
|
||||
_psql(
|
||||
migrated_postgres,
|
||||
"""
|
||||
select count(*)
|
||||
from pg_catalog.pg_attribute attribute
|
||||
join pg_catalog.pg_class relation on relation.oid = attribute.attrelid
|
||||
join pg_catalog.pg_namespace namespace on namespace.oid = relation.relnamespace
|
||||
cross join lateral pg_catalog.aclexplode(attribute.attacl) acl
|
||||
left join pg_catalog.pg_roles grantee_role on grantee_role.oid = acl.grantee
|
||||
where (namespace.nspname, relation.relname) in (
|
||||
('public', 'agents'), ('public', 'strategies'), ('public', 'strategy_nodes'),
|
||||
('public', 'claim_evidence'), ('public', 'claim_edges'), ('public', 'claims'),
|
||||
('public', 'sources'), ('public', 'reasoning_tools'),
|
||||
('kb_stage', 'kb_proposals'), ('kb_stage', 'kb_review_principals'),
|
||||
('kb_stage', 'kb_proposal_approvals')
|
||||
)
|
||||
and (acl.grantee = 0 or grantee_role.rolname in ('kb_gate_owner', 'kb_review', 'kb_apply'));
|
||||
""",
|
||||
).stdout.strip()
|
||||
== "0"
|
||||
)
|
||||
|
||||
for runtime_role, target_role in (
|
||||
("kb_apply", "broad_writer"),
|
||||
("kb_apply", "kb_gate_owner"),
|
||||
("kb_review", "kb_gate_owner"),
|
||||
):
|
||||
denied = _psql(
|
||||
migrated_postgres,
|
||||
f"set session authorization {runtime_role}; set role {target_role};",
|
||||
check=False,
|
||||
)
|
||||
assert denied.returncode != 0
|
||||
assert "permission denied to set role" in denied.stderr
|
||||
|
||||
snapshot_lines = set(_catalog_snapshot(migrated_postgres).splitlines())
|
||||
expected_table_acls = {
|
||||
"table_acl|public|agents|kb_gate_owner|SELECT|false",
|
||||
"table_acl|public|agents|kb_apply|SELECT|false",
|
||||
"table_acl|public|agents|kb_review|SELECT|false",
|
||||
"table_acl|public|strategies|kb_apply|SELECT|false",
|
||||
"table_acl|public|strategies|kb_apply|INSERT|false",
|
||||
"table_acl|public|strategies|kb_apply|UPDATE|false",
|
||||
"table_acl|public|strategy_nodes|kb_apply|SELECT|false",
|
||||
"table_acl|public|strategy_nodes|kb_apply|INSERT|false",
|
||||
"table_acl|public|strategy_nodes|kb_apply|UPDATE|false",
|
||||
"table_acl|public|claim_evidence|kb_apply|SELECT|false",
|
||||
"table_acl|public|claim_evidence|kb_apply|INSERT|false",
|
||||
"table_acl|public|claim_edges|kb_apply|SELECT|false",
|
||||
"table_acl|public|claim_edges|kb_apply|INSERT|false",
|
||||
"table_acl|public|claims|kb_apply|SELECT|false",
|
||||
"table_acl|public|claims|kb_apply|INSERT|false",
|
||||
"table_acl|public|sources|kb_apply|SELECT|false",
|
||||
"table_acl|public|sources|kb_apply|INSERT|false",
|
||||
"table_acl|public|reasoning_tools|kb_apply|SELECT|false",
|
||||
"table_acl|public|reasoning_tools|kb_apply|INSERT|false",
|
||||
"table_acl|kb_stage|kb_proposals|kb_gate_owner|SELECT|false",
|
||||
"table_acl|kb_stage|kb_proposals|kb_gate_owner|UPDATE|false",
|
||||
"table_acl|kb_stage|kb_proposals|kb_apply|SELECT|false",
|
||||
"table_acl|kb_stage|kb_proposals|kb_review|SELECT|false",
|
||||
}
|
||||
assert {line for line in snapshot_lines if line.startswith("table_acl|")} == expected_table_acls
|
||||
assert {line for line in snapshot_lines if line.startswith("function_acl|")} == {
|
||||
"function_acl|approve_strict_proposal(uuid, text, jsonb, text, text)|kb_review|EXECUTE|false",
|
||||
"function_acl|assert_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text)|"
|
||||
"kb_apply|EXECUTE|false",
|
||||
"function_acl|finish_approved_proposal(uuid, text, jsonb, text, uuid, timestamp with time zone, text, text)|"
|
||||
"kb_apply|EXECUTE|false",
|
||||
}
|
||||
|
||||
|
||||
def test_migration_rerun_preserves_catalog_snapshot(migrated_postgres: str) -> None:
|
||||
before = _catalog_snapshot(migrated_postgres)
|
||||
_apply_migration(migrated_postgres)
|
||||
assert _catalog_snapshot(migrated_postgres) == before
|
||||
|
||||
|
||||
def _assert_migration_fails(container: str, expected_error: str) -> None:
|
||||
completed = _apply_migration(container, check=False)
|
||||
assert completed.returncode != 0
|
||||
assert expected_error in completed.stderr
|
||||
|
||||
|
||||
def test_migration_fails_closed_on_protected_role_membership(migrated_postgres: str) -> None:
|
||||
_psql(migrated_postgres, "grant broad_writer to kb_apply;")
|
||||
try:
|
||||
_assert_migration_fails(migrated_postgres, "unexpected protected role membership")
|
||||
finally:
|
||||
_psql(migrated_postgres, "revoke broad_writer from kb_apply;")
|
||||
|
||||
|
||||
def test_migration_fails_closed_on_ownership_drift(migrated_postgres: str) -> None:
|
||||
_psql(migrated_postgres, "alter table kb_stage.kb_proposal_approvals owner to rogue_owner;")
|
||||
try:
|
||||
_assert_migration_fails(migrated_postgres, "unexpected protected table owner")
|
||||
finally:
|
||||
_psql(migrated_postgres, "alter table kb_stage.kb_proposal_approvals owner to kb_gate_owner;")
|
||||
|
||||
|
||||
def test_migration_fails_closed_on_unexpected_table_grantee(migrated_postgres: str) -> None:
|
||||
_psql(migrated_postgres, "grant select on kb_stage.kb_proposal_approvals to rogue_grantee;")
|
||||
try:
|
||||
_assert_migration_fails(migrated_postgres, "unexpected protected table grantee")
|
||||
finally:
|
||||
_psql(migrated_postgres, "revoke all on kb_stage.kb_proposal_approvals from rogue_grantee;")
|
||||
|
||||
|
||||
def test_migration_fails_closed_on_unexpected_column_grantee(migrated_postgres: str) -> None:
|
||||
_psql(
|
||||
migrated_postgres,
|
||||
"grant select (review_note) on kb_stage.kb_proposal_approvals to rogue_grantee;",
|
||||
)
|
||||
try:
|
||||
_assert_migration_fails(migrated_postgres, "unexpected protected table grantee")
|
||||
finally:
|
||||
_psql(
|
||||
migrated_postgres,
|
||||
"revoke select (review_note) on kb_stage.kb_proposal_approvals from rogue_grantee;",
|
||||
)
|
||||
|
||||
|
||||
def test_migration_fails_closed_on_unexpected_function_grantee(migrated_postgres: str) -> None:
|
||||
signature = "kb_stage.assert_approved_proposal(uuid,text,jsonb,text,uuid,timestamptz,text)"
|
||||
_psql(migrated_postgres, f"grant execute on function {signature} to rogue_grantee;")
|
||||
try:
|
||||
_assert_migration_fails(migrated_postgres, "unexpected protected function grantee")
|
||||
finally:
|
||||
_psql(migrated_postgres, f"revoke all on function {signature} from rogue_grantee;")
|
||||
|
||||
|
||||
def test_migration_fails_closed_on_unexpected_gate_overload(migrated_postgres: str) -> None:
|
||||
_psql(
|
||||
migrated_postgres,
|
||||
"""
|
||||
create function kb_stage.assert_approved_proposal(text)
|
||||
returns void language sql security definer as $function$ select $function$;
|
||||
""",
|
||||
)
|
||||
try:
|
||||
_assert_migration_fails(migrated_postgres, "unexpected protected function overload")
|
||||
finally:
|
||||
_psql(migrated_postgres, "drop function kb_stage.assert_approved_proposal(text);")
|
||||
|
|
@ -11,7 +11,7 @@ sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|||
import kb_proposal_normalize as norm # noqa: E402
|
||||
|
||||
|
||||
def test_freeform_claim_split_is_blocked_until_canonical_ids_exist():
|
||||
def test_rich_claim_split_stays_atomic_when_one_fragment_is_blocked():
|
||||
preview = norm.normalize_proposal(
|
||||
{
|
||||
"id": "p-freeform",
|
||||
|
|
@ -37,12 +37,12 @@ def test_freeform_claim_split_is_blocked_until_canonical_ids_exist():
|
|||
}
|
||||
)
|
||||
|
||||
assert preview["normalization_state"] == "blocked_missing_canonical_ids"
|
||||
assert preview["normalization_state"] == "blocked_rich_normalization"
|
||||
assert preview["strict_child_count"] == 0
|
||||
assert preview["blocked_count"] == 2
|
||||
assert preview["blocked_fragments"][0]["missing"] == ["canonical from_claim"]
|
||||
assert preview["blocked_fragments"][1]["missing"] == ["canonical claim_id", "canonical source_id"]
|
||||
assert "Create or identify canonical claim/source rows" in preview["next_normalization_action"]
|
||||
assert preview["strict_child_proposals"] == []
|
||||
assert preview["blocked_count"] == 1
|
||||
assert "unsupported edge_type" in preview["blocked_fragments"][0]["reason"]
|
||||
assert "Resolve every blocked rich fragment" in preview["next_normalization_action"]
|
||||
|
||||
|
||||
def test_exact_edge_fragment_emits_strict_add_edge_child():
|
||||
|
|
@ -129,3 +129,191 @@ def test_existing_apply_payload_is_already_strict():
|
|||
assert preview["normalization_state"] == "already_strict_apply_payload"
|
||||
assert preview["strict_child_count"] == 0
|
||||
assert preview["blocked_count"] == 0
|
||||
|
||||
|
||||
def test_rich_claim_graph_normalizes_to_one_atomic_approve_claim_bundle():
|
||||
preview = norm.normalize_proposal(
|
||||
{
|
||||
"id": "77777777-7777-4777-8777-777777777777",
|
||||
"proposal_type": "attach_evidence",
|
||||
"status": "approved",
|
||||
"proposed_by_agent_id": "11111111-1111-4111-8111-111111111111",
|
||||
"source_ref": "telegram:test:approved",
|
||||
"payload": {
|
||||
"claim_candidates": [
|
||||
{
|
||||
"claim_key": "canonical_test_claim",
|
||||
"headline": "A reviewed claim becomes canonical after approval.",
|
||||
"type": "structural",
|
||||
"confidence": 0.8,
|
||||
"tags": ["working-leo"],
|
||||
"evidence": [{"source_key": "canonical_test_source", "role": "grounds"}],
|
||||
}
|
||||
],
|
||||
"proposed_claims": [
|
||||
{
|
||||
"claim_key": "second_canonical_test_claim",
|
||||
"text": "A proposed_claims item is normalized too.",
|
||||
"type": "normative",
|
||||
}
|
||||
],
|
||||
"source_candidates": [
|
||||
{
|
||||
"source_key": "canonical_test_source",
|
||||
"source_type": "paper",
|
||||
"url": "https://example.test/canonical",
|
||||
"key_excerpt": "Reviewed source excerpt.",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert preview["normalization_state"] == "strict_children_ready"
|
||||
assert preview["blocked_count"] == 0
|
||||
assert preview["strict_child_count"] == 1
|
||||
child = preview["strict_child_proposals"][0]
|
||||
assert child["proposal_type"] == "approve_claim"
|
||||
bundle = child["payload"]["apply_payload"]
|
||||
assert bundle["agent_id"] == "11111111-1111-4111-8111-111111111111"
|
||||
assert len(bundle["claims"]) == 2
|
||||
assert len(bundle["sources"]) == 3 # reviewed source plus two proposal-body sources
|
||||
assert len(bundle["evidence"]) == 3
|
||||
assert bundle["edges"] == []
|
||||
accounting = bundle["normalization_manifest"]["candidate_accounting"]
|
||||
assert accounting["input_collections"] == {
|
||||
"claim_candidates": 1,
|
||||
"proposed_claims": 1,
|
||||
"source_candidates": 1,
|
||||
}
|
||||
assert accounting["claim_candidates"]["input_count"] == 2
|
||||
assert accounting["claim_candidates"]["planned_row_count"] == 2
|
||||
assert accounting["source_candidates"]["input_count"] == 1
|
||||
assert accounting["source_candidates"]["planned_row_count"] == 1
|
||||
assert accounting["generated_claim_body_sources"]["planned_row_count"] == 2
|
||||
assert accounting["total_planned_source_rows"] == 3
|
||||
|
||||
|
||||
def test_rich_normalization_blocks_malformed_or_duplicate_candidates():
|
||||
proposal = {
|
||||
"id": "77777777-7777-4777-8777-777777777777",
|
||||
"proposal_type": "attach_evidence",
|
||||
"status": "approved",
|
||||
"payload": {
|
||||
"claim_candidates": [
|
||||
{"claim_key": "duplicate", "text": "first"},
|
||||
{"claim_key": "duplicate", "text": "second"},
|
||||
{"text": "missing key"},
|
||||
"not-an-object",
|
||||
],
|
||||
"proposed_claims": [
|
||||
{"claim_key": "duplicate", "text": "duplicate across claim collections"},
|
||||
{"claim_key": " ", "text": "blank key"},
|
||||
42,
|
||||
],
|
||||
"source_candidates": [
|
||||
{"source_key": "source", "source_type": "paper"},
|
||||
{"source_key": "source", "source_type": "paper"},
|
||||
],
|
||||
},
|
||||
}
|
||||
preview = norm.normalize_proposal(proposal)
|
||||
assert preview["normalization_state"] == "blocked_lossy_rich_normalization"
|
||||
assert preview["strict_child_count"] == 0
|
||||
assert preview["strict_child_proposals"] == []
|
||||
reasons = " ".join(item["reason"] for item in preview["blocked_fragments"])
|
||||
assert "duplicate" in reasons
|
||||
assert "missing" in reasons
|
||||
assert "object" in reasons
|
||||
|
||||
|
||||
def test_rich_normalization_retains_type_mapping_manifest_for_review():
|
||||
preview = norm.normalize_proposal(
|
||||
{
|
||||
"id": "77777777-7777-4777-8777-777777777778",
|
||||
"proposal_type": "attach_evidence",
|
||||
"status": "approved",
|
||||
"payload": {
|
||||
"claim_candidates": [
|
||||
{"claim_key": "governance_claim", "text": "Governance process.", "type": "governance"},
|
||||
{"claim_key": "typed_claim", "text": "Typed detail.", "type": "normative/detail"},
|
||||
],
|
||||
"source_candidates": [
|
||||
{"source_key": "book_source", "source_type": "book", "key_excerpt": "Excerpt"},
|
||||
{"source_key": "web_source", "url": "https://example.test/source"},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
child = preview["strict_child_proposals"][0]
|
||||
manifest = child["payload"]["apply_payload"]["normalization_manifest"]
|
||||
assert {
|
||||
(item["claim_key"], item["from"], item["to"])
|
||||
for item in manifest["claim_type_mappings"]
|
||||
} == {
|
||||
("governance_claim", "governance", "meta"),
|
||||
("typed_claim", "normative/detail", "normative"),
|
||||
}
|
||||
assert {
|
||||
(item["source_key"], item["from"], item["to"])
|
||||
for item in manifest["source_type_mappings"]
|
||||
} == {
|
||||
("book_source", "book", "other"),
|
||||
("web_source", "url", "article"),
|
||||
("governance_claim__proposal_body", "proposal_body", "other"),
|
||||
("typed_claim__proposal_body", "proposal_body", "other"),
|
||||
}
|
||||
|
||||
|
||||
def test_rich_normalization_rejects_non_list_candidate_collection():
|
||||
preview = norm.normalize_proposal(
|
||||
{
|
||||
"id": "77777777-7777-4777-8777-777777777779",
|
||||
"proposal_type": "attach_evidence",
|
||||
"payload": {"claim_candidates": {"claim_key": "not-in-a-list"}},
|
||||
}
|
||||
)
|
||||
|
||||
assert preview["normalization_state"] == "blocked_lossy_rich_normalization"
|
||||
assert preview["strict_child_proposals"] == []
|
||||
assert preview["blocked_fragments"][0]["location"] == "payload.claim_candidates"
|
||||
assert "list of objects" in preview["blocked_fragments"][0]["reason"]
|
||||
|
||||
|
||||
def test_rich_normalization_fails_closed_when_planner_skips_candidate(monkeypatch):
|
||||
proposal = {
|
||||
"id": "77777777-7777-4777-8777-777777777780",
|
||||
"proposal_type": "attach_evidence",
|
||||
"payload": {
|
||||
"claim_candidates": [{"claim_key": "must_be_planned", "text": "Keep me."}],
|
||||
"source_candidates": [{"source_key": "must_be_planned_too", "source_type": "paper"}],
|
||||
},
|
||||
}
|
||||
lossy_plan = norm.rich_plan.build_creation_plan(proposal)
|
||||
lossy_plan["planned_rows"]["claims"] = []
|
||||
monkeypatch.setattr(norm.rich_plan, "build_creation_plan", lambda *_args, **_kwargs: lossy_plan)
|
||||
|
||||
preview = norm.normalize_proposal(proposal)
|
||||
|
||||
assert preview["normalization_state"] == "blocked_lossy_rich_normalization"
|
||||
assert preview["strict_child_proposals"] == []
|
||||
assert preview["strict_child_count"] == 0
|
||||
assert any("claim rows" in item["reason"] for item in preview["blocked_fragments"])
|
||||
|
||||
|
||||
def test_source_candidates_without_claims_are_not_silently_dropped():
|
||||
preview = norm.normalize_proposal(
|
||||
{
|
||||
"id": "77777777-7777-4777-8777-777777777781",
|
||||
"proposal_type": "attach_evidence",
|
||||
"payload": {
|
||||
"source_candidates": [
|
||||
{"source_key": "orphan_source", "source_type": "paper", "key_excerpt": "Keep me."}
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
assert preview["normalization_state"] == "blocked_lossy_rich_normalization"
|
||||
assert preview["strict_child_proposals"] == []
|
||||
assert "without at least one claim candidate" in preview["blocked_fragments"][0]["reason"]
|
||||
|
|
|
|||
|
|
@ -82,13 +82,10 @@ def test_api_kb_proposals_returns_review_packets_and_counts():
|
|||
assert data["review_state_counts"]["approved_needs_apply_payload"] == 1
|
||||
assert data["review_state_counts"]["approved_applyable"] == 1
|
||||
assert data["packets"][0]["identity_and_authorization"]["source_ref"] == "telegram:leo:m3taversal:claim-review"
|
||||
assert data["packets"][0]["normalization_preview"]["normalization_state"] == "blocked_missing_canonical_ids"
|
||||
assert data["packets"][0]["normalization_preview"]["normalization_state"] == "blocked_lossy_rich_normalization"
|
||||
assert data["packets"][1]["normalization_preview"]["normalization_state"] == "already_strict_apply_payload"
|
||||
assert data["packets"][0]["apply_preview"]["state"] == "not_applyable_yet"
|
||||
assert data["packets"][0]["apply_preview"]["blocked_fragments"][0]["missing"] == [
|
||||
"canonical from_claim",
|
||||
"canonical to_claim",
|
||||
]
|
||||
assert data["packets"][0]["apply_preview"]["blocked_fragments"][0]["missing"] == ["claim_key"]
|
||||
assert data["packets"][1]["apply_preview"]["state"] == "strict_apply_payload_ready"
|
||||
assert data["packets"][1]["apply_preview"]["rows"] == [
|
||||
{
|
||||
|
|
@ -128,13 +125,13 @@ def test_page_renders_applyability_and_next_admin_action():
|
|||
assert "claim-row normalization" in html
|
||||
assert "normalize into one or more strict apply_payload" in html
|
||||
assert "Normalization" in html
|
||||
assert "blocked_missing_canonical_ids" in html
|
||||
assert "blocked_lossy_rich_normalization" in html
|
||||
assert 'href="/kb/claims/d0000000-0000-0000-0000-000000000005"' in html
|
||||
assert "<code>d0000000-0000-0000-0000-000000000005</code>" in html
|
||||
assert "Apply preview" in html
|
||||
assert "not_applyable_yet" in html
|
||||
assert "No canonical row preview is executable yet." in html
|
||||
assert "canonical from_claim" in html
|
||||
assert "claim_key" in html
|
||||
|
||||
|
||||
def test_page_renders_strict_apply_payload_claim_links():
|
||||
|
|
|
|||
|
|
@ -38,11 +38,7 @@ def test_dns_gate_uses_static_config_and_redacts_token_stdout(tmp_path: Path):
|
|||
"secret-token-should-not-leak",
|
||||
"Failed to resolve 'oauth2.googleapis.com' ([Errno 8] nodename nor servname provided)",
|
||||
)
|
||||
return probe.CommandResult(
|
||||
1,
|
||||
"",
|
||||
"HTTPSConnectionPool(host='oauth2.googleapis.com'): Failed to resolve 'oauth2.googleapis.com'",
|
||||
)
|
||||
return probe.CommandResult(1, "", "Project readback unavailable after token failure")
|
||||
|
||||
result = probe.probe_gcp_parity(
|
||||
gcloud_path=Path("/fake/gcloud"),
|
||||
|
|
@ -58,10 +54,38 @@ def test_dns_gate_uses_static_config_and_redacts_token_stdout(tmp_path: Path):
|
|||
assert result["local_gcloud"]["static_active_config"]["account"] == "billy@livingip.xyz"
|
||||
assert result["local_gcloud"]["static_active_config"]["project"] == "teleo-501523"
|
||||
assert result["accounts_tested"][0]["token_refresh"]["stdout_redacted"] is True
|
||||
assert probe.classify_failure(result["accounts_tested"]) == "dns_unavailable"
|
||||
assert "secret-token" not in json.dumps(result)
|
||||
assert "oauth2.googleapis.com" in result["clear_CTA"]
|
||||
|
||||
|
||||
def test_reauth_url_hostname_is_not_classified_as_dns(tmp_path: Path):
|
||||
config_dir = tmp_path / "gcloud"
|
||||
write_gcloud_config(config_dir)
|
||||
|
||||
def fake_runner(command: list[str], _env: dict[str, str], _timeout: int) -> probe.CommandResult:
|
||||
if command[1:3] == ["auth", "list"]:
|
||||
return probe.CommandResult(0, json.dumps([{"account": "billy@livingip.xyz", "status": "ACTIVE"}]), "")
|
||||
if command[1:3] == ["auth", "print-access-token"]:
|
||||
return probe.CommandResult(
|
||||
1,
|
||||
"",
|
||||
"Reauthentication failed. Visit https://oauth2.googleapis.com/token or run gcloud auth login.",
|
||||
)
|
||||
return probe.CommandResult(1, "", "Project readback unavailable after token failure")
|
||||
|
||||
result = probe.probe_gcp_parity(
|
||||
gcloud_path=Path("/fake/gcloud"),
|
||||
source_config=config_dir,
|
||||
runner=fake_runner,
|
||||
use_temp_config=False,
|
||||
)
|
||||
|
||||
assert probe.classify_failure(result["accounts_tested"]) == "reauth_required"
|
||||
assert result["status"] == "blocked_on_gcloud_reauth"
|
||||
assert "gcloud auth login" in result["clear_CTA"]
|
||||
|
||||
|
||||
def test_ready_status_requires_project_vm_and_cloudsql_readbacks(tmp_path: Path):
|
||||
config_dir = tmp_path / "gcloud"
|
||||
write_gcloud_config(config_dir)
|
||||
|
|
|
|||
91
tests/test_repo_skill_pack.py
Normal file
91
tests/test_repo_skill_pack.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPORT_DIR = ROOT / "docs" / "reports" / "leo-working-state-20260709"
|
||||
MANIFEST = REPORT_DIR / "skill-pack-manifest.json"
|
||||
|
||||
|
||||
def _skill(name: str) -> str:
|
||||
return (ROOT / ".agents" / "skills" / name / "SKILL.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def test_manifest_indexes_valid_repo_native_skills() -> None:
|
||||
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
|
||||
assert manifest["status"] == "repo_native_validated"
|
||||
assert manifest["repo_skill_root"] == ".agents/skills"
|
||||
assert manifest["validation"] == "tests/test_repo_skill_pack.py"
|
||||
assert "37/37" in manifest["claim_ceiling"]
|
||||
|
||||
for entry in manifest["skills"]:
|
||||
path = ROOT / entry["path"]
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert path.is_file()
|
||||
assert text.startswith("---\n")
|
||||
assert f"name: {entry['name']}\n" in text.split("---\n", 2)[1]
|
||||
assert "description:" in text.split("---\n", 2)[1]
|
||||
|
||||
for relative in manifest["reference_files"]:
|
||||
assert (ROOT / relative).is_file(), relative
|
||||
|
||||
|
||||
def test_db_change_skill_tracks_guarded_v2_runtime_proof() -> None:
|
||||
text = _skill("teleo-kb-db-change-workflow")
|
||||
|
||||
for required in (
|
||||
"`approve_claim` v2",
|
||||
"kb_review",
|
||||
"kb_apply",
|
||||
"kb_gate_owner",
|
||||
"NOINHERIT",
|
||||
"37/37",
|
||||
"exact table deltas",
|
||||
"source hashes",
|
||||
"trusted operator-only canonical writer",
|
||||
"run_approve_claim_isolated_container_canary.sh",
|
||||
):
|
||||
assert required in text
|
||||
|
||||
assert "does not prove byte-for-byte live content" in text
|
||||
assert "production permission migration" in text
|
||||
|
||||
|
||||
def test_gcp_skill_uses_current_cloud_sql_truth_and_exact_gate() -> None:
|
||||
text = _skill("teleo-gcp-parity-ops")
|
||||
squashed = " ".join(text.split())
|
||||
|
||||
for required in (
|
||||
"teleo-pgvector-standby",
|
||||
"PostgreSQL `16.14`",
|
||||
"teleo-staging-net",
|
||||
"public IP disabled",
|
||||
"Cloud SQL Studio",
|
||||
"GCP Studio authenticated",
|
||||
"begin transaction read only",
|
||||
"DB contents",
|
||||
):
|
||||
assert required in text
|
||||
|
||||
assert "no enabled existing" in squashed
|
||||
assert "IAM DB principal" in squashed
|
||||
assert "Do not submit a raw password" in text
|
||||
assert "AppleScript" in text
|
||||
|
||||
|
||||
def test_vps_onboarding_and_cory_skills_point_to_current_proof() -> None:
|
||||
vps = _skill("teleo-vps-runtime-ops")
|
||||
onboarding = _skill("teleo-leo-onboarding")
|
||||
cory = _skill("working-leo-cory-outcomes")
|
||||
|
||||
assert "reasoning tools `17`" in vps
|
||||
assert "current source hashes" in vps
|
||||
assert "gcp-cloud-sql-t3-live-readonly-current.md" in onboarding
|
||||
assert "GCP Studio authenticated" not in onboarding
|
||||
assert "37/37" in cory
|
||||
assert "Approved is not the same as applied" in cory
|
||||
assert "Next Cory-style follow-up:" in cory
|
||||
Loading…
Reference in a new issue