Merge pull request #104 from living-ip/codex/kb-rebuild-and-broad-hardening-20260713
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
Rebuild Leo KB quickly and harden broad database reasoning
This commit is contained in:
commit
279962fe8a
14 changed files with 2614 additions and 38 deletions
|
|
@ -61,6 +61,24 @@ retains a private custom dump, dump SHA-256, object TOC, catalog/data manifest,
|
|||
and before/after service state. It never restarts Leo or writes to the source
|
||||
database.
|
||||
|
||||
Prove that this exact snapshot can rebuild a blank Postgres target before using
|
||||
it for GCP:
|
||||
|
||||
```bash
|
||||
.venv/bin/python ops/run_local_canonical_postgres_rebuild.py \
|
||||
--dump <private-output-dir>/teleo-canonical.dump \
|
||||
--source-manifest <private-output-dir>/source-manifest.jsonl \
|
||||
--output /tmp/teleo-canonical-rebuild-receipt.json
|
||||
```
|
||||
|
||||
The local runner starts a uniquely named `postgres:16-alpine` container with
|
||||
network mode `none` and tmpfs-only database storage. It waits for an actual
|
||||
`psql` connection to the named database, restores with `pg_restore
|
||||
--no-owner --no-privileges --exit-on-error`, compares the full parity manifest,
|
||||
then removes the container and proves it is absent. A passing local receipt is
|
||||
the exact-recovery preflight; it is not semantic recompilation from raw source
|
||||
documents.
|
||||
|
||||
Run `ops/postgres_parity_manifest.sql` against the isolated restored target,
|
||||
then compare source and target:
|
||||
|
||||
|
|
@ -85,7 +103,7 @@ drill into `teleo`, `teleo_kb`, or `teleo_canonical`. Database isolation does
|
|||
not isolate cluster-global roles or extensions, so verify those separately and
|
||||
do not run the Docker-only gate bootstrap against the shared Cloud SQL instance.
|
||||
|
||||
After the parity verifier passes, run the no-send Cory composition replay from
|
||||
After the parity verifier passes, run the no-send operator composition replay from
|
||||
staging compute against that generated database. Only then delete the generated
|
||||
database and uploaded import object and retain cleanup readback.
|
||||
|
||||
|
|
|
|||
124
docs/kb-rebuild-and-recompile.md
Normal file
124
docs/kb-rebuild-and-recompile.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# Rebuilding Leo's Knowledge Database
|
||||
|
||||
## Outcome
|
||||
|
||||
Leo should improve by compiling durable source material and reviewed changes
|
||||
into Postgres. Repeatedly changing prompts or retraining the chat behavior is
|
||||
not the knowledge system.
|
||||
|
||||
There are two different rebuilds:
|
||||
|
||||
1. **Exact recovery** restores the current canonical database from a verified
|
||||
snapshot. This is working now.
|
||||
2. **Semantic recompilation** starts from the retained source corpus and the
|
||||
reviewed change ledger, then reproduces the canonical rows. This is partly
|
||||
recoverable but is not yet complete.
|
||||
|
||||
## Exact Recovery: Working
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
.venv/bin/python ops/run_local_canonical_postgres_rebuild.py \
|
||||
--dump /private/path/teleo-canonical.dump \
|
||||
--source-manifest /private/path/source-manifest.jsonl \
|
||||
--output /tmp/teleo-canonical-rebuild-receipt.json
|
||||
```
|
||||
|
||||
The 2026-07-13 live canary restored a fresh, network-isolated Postgres in 3.81
|
||||
seconds and then removed it. The restored target matched the source across all
|
||||
39 manifest tables and all 52,164 rows, with no schema, data, constraint, role,
|
||||
or performance mismatch. Key rows included:
|
||||
|
||||
- 1,837 claims;
|
||||
- 4,145 sources;
|
||||
- 4,670 claim-evidence links;
|
||||
- 4,916 claim edges;
|
||||
- 17 reasoning tools;
|
||||
- 26 proposals.
|
||||
|
||||
This is the fastest disaster-recovery path. It does not require Leo to
|
||||
re-extract or relearn the corpus.
|
||||
|
||||
## Source Recompilation: Current Evidence
|
||||
|
||||
Read-only VPS inspection found two retained June import runs. Both point to the
|
||||
Forgejo-era workspace at `/opt/teleo-eval/workspaces/main` and retained
|
||||
inventory JSONL files under `/opt/teleo-eval/kb-import/`.
|
||||
|
||||
The database still retains:
|
||||
|
||||
- `kb_stage.import_runs` for the two inventories;
|
||||
- staged claims, sources, claim-source links, and claim edges;
|
||||
- `kb_stage.canonical_mappings` from legacy keys to canonical UUIDs;
|
||||
- mappings for 1,807 of 1,837 canonical claims;
|
||||
- mappings for all 4,145 canonical sources.
|
||||
|
||||
For the 1,807 mapped claims, current canonical type, text, status, confidence,
|
||||
tags, and creator match the retained staged rows exactly. Creation timestamps
|
||||
also follow a recoverable rule: use the legacy timestamp when present and the
|
||||
mapping timestamp for the eight rows that had no legacy timestamp.
|
||||
|
||||
The simple retained-row joins currently reproduce:
|
||||
|
||||
- 4,254 of 4,670 canonical evidence links;
|
||||
- 4,878 of 4,916 canonical edge rows can be accounted for by a staged relation;
|
||||
historical duplicate multiplicity still needs an explicit replay rule.
|
||||
|
||||
The remaining gaps are concrete rather than mysterious:
|
||||
|
||||
- 30 claims were created after or outside the mapped import;
|
||||
- 284 mapped source rows do not have a matching retained `staged_sources` row
|
||||
and need the original source-synthesis rule or an explicit genesis record;
|
||||
- 416 evidence links need source-synthesis or later-change provenance;
|
||||
- 38 edge rows need later-change receipts or explicit replay records;
|
||||
- old applied proposal rows do not describe every historical canonical write.
|
||||
|
||||
This proves that most of the initial database came from the retained file-KB
|
||||
import path. It does not yet prove a clean blank-database recompile.
|
||||
|
||||
## Target Compiler
|
||||
|
||||
The durable rebuild model is:
|
||||
|
||||
```text
|
||||
immutable source corpus + file hashes
|
||||
-> deterministic inventory and classification
|
||||
-> staged claims, sources, evidence links, and edges
|
||||
-> stable canonical ID mapping
|
||||
-> review decisions
|
||||
-> append-only accepted apply payloads and receipts
|
||||
-> canonical Postgres
|
||||
-> render/sync/restart
|
||||
-> answer benchmark
|
||||
```
|
||||
|
||||
Use the current verified snapshot as **genesis epoch 1**. Preserve its dump,
|
||||
manifest, source commit, inventory files, mappings, and aggregate rebuild
|
||||
receipt. Every accepted change after that epoch must carry a replayable strict
|
||||
apply payload and row-level postflight receipt. This prevents the historical
|
||||
gap from growing while the old import rules are reconstructed.
|
||||
|
||||
The existing local ingestion canary already proves one hash-bound document
|
||||
fixture can become a normalized `pending_review` proposal without touching
|
||||
canonical rows. The existing full-data clone canary proves a reviewed packet
|
||||
can create source, claim, evidence, and edge rows and affect later reasoning.
|
||||
The next capability is to join those into a corpus runner and replay ledger.
|
||||
|
||||
## Definition Of Working
|
||||
|
||||
Semantic recompilation is complete only when all of these pass:
|
||||
|
||||
1. A command creates a blank database from the retained source corpus plus the
|
||||
reviewed ledger.
|
||||
2. Schema, constraints, roles, table counts, row hashes, and key query results
|
||||
match the canonical manifest.
|
||||
3. Every canonical row traces to a genesis import record or a reviewed apply
|
||||
receipt.
|
||||
4. A new document can be hash-captured, extracted into grounded candidates,
|
||||
deduplicated, staged, reviewed, applied in a disposable clone, and read back.
|
||||
5. After render/sync and restart, Leo answers the related broad question using
|
||||
the new rows and cites the source chain.
|
||||
|
||||
Until then, exact snapshot recovery is production recovery; source
|
||||
recompilation is an active build capability, not a finished claim.
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# Local Canonical Postgres Rebuild Canary
|
||||
|
||||
Generated UTC: `2026-07-13T08:34:54.016399+00:00`
|
||||
|
||||
Status: **pass**
|
||||
|
||||
## Result
|
||||
|
||||
- Total command time: `3.809726s`
|
||||
- Named database ready: `0.781077s`
|
||||
- `pg_restore`: `1.026398s`
|
||||
- Manifest capture and parity comparison: `1.229484s`
|
||||
- Cleanup: `0.088110s`
|
||||
- Dump bytes: `13,462,116`
|
||||
- Dump SHA-256: `8a217e32bf6c83cb2b629e5ca5f91fc57f759e5010d9c40babe0d13c9a450822`
|
||||
- Source manifest SHA-256: `d077f859b5f8ece908c61958a90914272ddc6fb9930df3b97d2ad2ac85b8e39a`
|
||||
|
||||
## Parity
|
||||
|
||||
- Manifest tables: `39/39`
|
||||
- Total rows: `52,164/52,164`
|
||||
- Table mismatches: `0`
|
||||
- Column, constraint, function, index, policy, schema, sequence, trigger,
|
||||
type, and view hash mismatches: `0`
|
||||
- Application role mismatches: `0`
|
||||
- Performance threshold failures: `0`
|
||||
|
||||
Key restored rows:
|
||||
|
||||
| Object | Rows |
|
||||
|---|---:|
|
||||
| `public.claims` | 1,837 |
|
||||
| `public.sources` | 4,145 |
|
||||
| `public.claim_evidence` | 4,670 |
|
||||
| `public.claim_edges` | 4,916 |
|
||||
| `public.reasoning_tools` | 17 |
|
||||
| `kb_stage.kb_proposals` | 26 |
|
||||
|
||||
## Isolation And Cleanup
|
||||
|
||||
- Docker image: `postgres:16-alpine`
|
||||
- Network mode: `none`
|
||||
- Postgres data directory: `tmpfs`, no persistent Docker volume
|
||||
- Named-database readiness used an actual `psql -d teleo` connection
|
||||
- Production database touched: `false`
|
||||
- Container absence after cleanup: `true`
|
||||
- Independent label-filtered orphan check: no containers found
|
||||
|
||||
## Command
|
||||
|
||||
```bash
|
||||
.venv/bin/python ops/run_local_canonical_postgres_rebuild.py \
|
||||
--dump /private/path/teleo-canonical.dump \
|
||||
--source-manifest /private/path/source-manifest.jsonl \
|
||||
--output /tmp/teleo-canonical-rebuild-receipt.json
|
||||
```
|
||||
|
||||
This proves fast exact recovery from the canonical snapshot. It does not claim
|
||||
that the whole database can yet be semantically recompiled from raw documents
|
||||
and reviewed change records alone.
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# Working Leo Blind Review Before Broad Hardening
|
||||
|
||||
Date: `2026-07-13`
|
||||
|
||||
Status: **not reliable for unattended operator use**
|
||||
|
||||
## Method
|
||||
|
||||
A 12-question temporary-profile handler run used broad prompts without known
|
||||
proposal IDs. It posted nothing to Telegram and made no database or service
|
||||
change. Two independent strict reviews evaluated the same transcript against
|
||||
the live v1 schema and retained proof.
|
||||
|
||||
Reviewer results differed in severity but agreed on the verdict:
|
||||
|
||||
- strict binary review: `1/12 pass`;
|
||||
- pass/partial/fail review: `2 pass`, `4 partial`, `6 fail`.
|
||||
|
||||
## Converged Failures
|
||||
|
||||
1. Replies presented target-architecture fields as current schema, including
|
||||
source author/channel/date, claim bodies, evidence excerpts, edge rationale,
|
||||
and forecast-resolution fields.
|
||||
2. Replies invented edge types such as `superseded_by`, `relates_to`, and
|
||||
`resolves`. Current v1 uses `supersedes`; `superseded_by` is a claim column.
|
||||
3. Replies treated unchanged canonical counts as a complete explanation of
|
||||
behavior, ignoring deployed skills, runtime configuration, rendered files,
|
||||
and persisted Hermes session state.
|
||||
4. A handler-only no-post run was described as proof of the live Telegram path.
|
||||
5. A temporary conversation label was promoted into source provenance rather
|
||||
than resolving a real URL, storage path, file hash, or retained artifact.
|
||||
6. Staging was treated as though it required the same authorization as
|
||||
canonical apply. Requested source capture and proposal staging are allowed;
|
||||
guarded canonical apply is the approval boundary.
|
||||
7. Forecast handling proposed rewriting historical confidence and inventing
|
||||
resolution storage that current v1 does not have.
|
||||
8. Claims/evidence were not consistently treated as shared knowledge while
|
||||
beliefs and positions remain agent-specific.
|
||||
9. Replies were too long for operational use: `9,033` words total, `753` words
|
||||
per answer on average, with several over `1,000` words.
|
||||
|
||||
## Repair Incorporated
|
||||
|
||||
- Explicit current-schema and edge contracts in both VPS and GCP bridge skills.
|
||||
- Runtime/session/canonical proof-tier separation.
|
||||
- Source-link versus provenance-quality distinction.
|
||||
- Shared claims/evidence versus agent-specific positions.
|
||||
- Forecast-history preservation and current schema gap.
|
||||
- Autonomous source capture and staging boundary.
|
||||
- Maximum response length in the out-of-sample scorer.
|
||||
- Expansion from 9 to 15 broad prompts covering these failure modes.
|
||||
|
||||
## Claim Ceiling
|
||||
|
||||
This is a pre-fix handler baseline. It is not Telegram-visible proof. The repair
|
||||
is not accepted until the deployed 15-question run passes, independent strict
|
||||
review converges, restart survival passes, and a separate Telegram-visible
|
||||
no-mutation canary succeeds.
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,285 @@
|
|||
{
|
||||
"db_counts_changed": false,
|
||||
"generated_at_utc": "2026-07-13T08:15:42.852150+00:00",
|
||||
"memory_token": "demo-ledger-61c5dcf6",
|
||||
"mode": "working_leo_m3taversal_out_of_sample_live_vps_handler_score",
|
||||
"posted_to_telegram": false,
|
||||
"production_db_apply_ran": false,
|
||||
"score": {
|
||||
"expected_prompt_count": 9,
|
||||
"expected_prompt_ids": [
|
||||
"OOS-01",
|
||||
"OOS-02",
|
||||
"OOS-03",
|
||||
"OOS-04",
|
||||
"OOS-05",
|
||||
"OOS-06",
|
||||
"OOS-07",
|
||||
"OOS-08",
|
||||
"OOS-09"
|
||||
],
|
||||
"failures": [
|
||||
{
|
||||
"concepts": {
|
||||
"canonical_readback": true,
|
||||
"live_check": false,
|
||||
"material_action": true,
|
||||
"state_boundary": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {},
|
||||
"dimension": "demo_pressure_without_ids",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": true,
|
||||
"legacy_signals": {
|
||||
"canonical_db": true,
|
||||
"fresh_readback": true,
|
||||
"next_action": true,
|
||||
"no_overclaim": true,
|
||||
"state_semantics": true
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": false,
|
||||
"prompt_id": "OOS-01",
|
||||
"response_issue_detected": false
|
||||
},
|
||||
{
|
||||
"concepts": {
|
||||
"receipt": true,
|
||||
"source_evidence_chain": false,
|
||||
"staged_review_apply": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {},
|
||||
"dimension": "source_pointer_is_not_evidence",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": false,
|
||||
"legacy_signals": {
|
||||
"canonical_db": true,
|
||||
"document_artifact_linking": true,
|
||||
"no_overclaim": false,
|
||||
"row_level_proof": true,
|
||||
"staging_or_review": false
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": false,
|
||||
"prompt_id": "OOS-05",
|
||||
"response_issue_detected": false
|
||||
}
|
||||
],
|
||||
"missing_prompt_ids": [],
|
||||
"pass": false,
|
||||
"passes": 7,
|
||||
"prompt_count": 9,
|
||||
"scores": [
|
||||
{
|
||||
"concepts": {
|
||||
"canonical_readback": true,
|
||||
"live_check": false,
|
||||
"material_action": true,
|
||||
"state_boundary": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {},
|
||||
"dimension": "demo_pressure_without_ids",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": true,
|
||||
"legacy_signals": {
|
||||
"canonical_db": true,
|
||||
"fresh_readback": true,
|
||||
"next_action": true,
|
||||
"no_overclaim": true,
|
||||
"state_semantics": true
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": false,
|
||||
"prompt_id": "OOS-01",
|
||||
"response_issue_detected": false
|
||||
},
|
||||
{
|
||||
"concepts": {
|
||||
"contradiction": true,
|
||||
"deduplication": true,
|
||||
"provenance": true,
|
||||
"receipt": true,
|
||||
"staged_review_apply": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {},
|
||||
"dimension": "multi_surface_knowledge_ingestion",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": false,
|
||||
"legacy_signals": {
|
||||
"canonical_db": true,
|
||||
"caveat_retention": false,
|
||||
"document_artifact_linking": true,
|
||||
"next_action": true,
|
||||
"no_overclaim": true,
|
||||
"staging_or_review": false
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": true,
|
||||
"prompt_id": "OOS-02",
|
||||
"response_issue_detected": false
|
||||
},
|
||||
{
|
||||
"concepts": {
|
||||
"identity_chain": true,
|
||||
"receipt": true,
|
||||
"state_boundary": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {},
|
||||
"dimension": "identity_restart_truth",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": true,
|
||||
"legacy_signals": {
|
||||
"canonical_db": true,
|
||||
"identity_rendering": true,
|
||||
"no_overclaim": true,
|
||||
"row_level_proof": true
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": true,
|
||||
"prompt_id": "OOS-03",
|
||||
"response_issue_detected": false
|
||||
},
|
||||
{
|
||||
"concepts": {
|
||||
"canonical_readback": true,
|
||||
"receipt": true,
|
||||
"state_boundary": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {},
|
||||
"dimension": "partner_demo_claim_ceiling",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": false,
|
||||
"legacy_signals": {
|
||||
"artifact": true,
|
||||
"canonical_db": true,
|
||||
"no_overclaim": false,
|
||||
"row_level_proof": true,
|
||||
"state_semantics": true
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": true,
|
||||
"prompt_id": "OOS-04",
|
||||
"response_issue_detected": false
|
||||
},
|
||||
{
|
||||
"concepts": {
|
||||
"receipt": true,
|
||||
"source_evidence_chain": false,
|
||||
"staged_review_apply": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {},
|
||||
"dimension": "source_pointer_is_not_evidence",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": false,
|
||||
"legacy_signals": {
|
||||
"canonical_db": true,
|
||||
"document_artifact_linking": true,
|
||||
"no_overclaim": false,
|
||||
"row_level_proof": true,
|
||||
"staging_or_review": false
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": false,
|
||||
"prompt_id": "OOS-05",
|
||||
"response_issue_detected": false
|
||||
},
|
||||
{
|
||||
"concepts": {
|
||||
"heterogeneous_types": true,
|
||||
"receipt": true,
|
||||
"staged_review_apply": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {},
|
||||
"dimension": "database_composition_judgment",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": false,
|
||||
"legacy_signals": {
|
||||
"canonical_db": true,
|
||||
"caveat_retention": false,
|
||||
"next_action": true,
|
||||
"no_overclaim": true,
|
||||
"staging_or_review": true
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": true,
|
||||
"prompt_id": "OOS-06",
|
||||
"response_issue_detected": false
|
||||
},
|
||||
{
|
||||
"concepts": {
|
||||
"blocker_definition": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {
|
||||
"memory_token": true
|
||||
},
|
||||
"dimension": "same_session_memory_set",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": false,
|
||||
"legacy_signals": {
|
||||
"next_action": true,
|
||||
"no_overclaim": false
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": true,
|
||||
"prompt_id": "OOS-07",
|
||||
"response_issue_detected": false
|
||||
},
|
||||
{
|
||||
"concepts": {
|
||||
"blocker_definition": true,
|
||||
"receipt": true
|
||||
},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {
|
||||
"closure_proof": true,
|
||||
"memory_token": true
|
||||
},
|
||||
"dimension": "same_session_memory_recall",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": false,
|
||||
"legacy_signals": {
|
||||
"next_action": true,
|
||||
"no_overclaim": false,
|
||||
"row_level_proof": true
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": true,
|
||||
"prompt_id": "OOS-08",
|
||||
"response_issue_detected": false
|
||||
},
|
||||
{
|
||||
"concepts": {},
|
||||
"current_schema_overclaims": [],
|
||||
"custom_signals": {
|
||||
"current_update_identity_boundary": true,
|
||||
"exact_participant_handle": true,
|
||||
"no_cross_participant_identity_bleed": true,
|
||||
"no_unverified_alias": true
|
||||
},
|
||||
"dimension": "telegram_participant_identity",
|
||||
"invalid_count_invariant_detected": false,
|
||||
"legacy_pass": false,
|
||||
"legacy_signals": {
|
||||
"no_overclaim": false
|
||||
},
|
||||
"overclaim_detected": false,
|
||||
"pass": true,
|
||||
"prompt_id": "OOS-09",
|
||||
"response_issue_detected": false
|
||||
}
|
||||
],
|
||||
"unexpected_prompt_ids": []
|
||||
},
|
||||
"service_unchanged": true,
|
||||
"source_results_json": "/Users/user/Documents/Codex/2026-07-09/019f34eb-d297-72d0-b7e2-b222d5515ab9-load/work/teleo-infra-main/docs/reports/leo-working-state-20260709/telegram-handler-m3taversal-oos-suite-current.json",
|
||||
"temp_profile_removed": true
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# Working Leo m3taversal Out-of-Sample Score
|
||||
|
||||
Generated UTC: `2026-07-13T08:15:42.852150+00:00`
|
||||
Pass: `False`
|
||||
Prompts: `7/9`
|
||||
DB counts changed: `False`
|
||||
Service unchanged: `True`
|
||||
Temporary profile removed: `True`
|
||||
Posted to Telegram: `False`
|
||||
|
||||
## Prompt Scores
|
||||
|
||||
- `OOS-01` / `demo_pressure_without_ids`: `pass=False`
|
||||
- `OOS-02` / `multi_surface_knowledge_ingestion`: `pass=True`
|
||||
- `OOS-03` / `identity_restart_truth`: `pass=True`
|
||||
- `OOS-04` / `partner_demo_claim_ceiling`: `pass=True`
|
||||
- `OOS-05` / `source_pointer_is_not_evidence`: `pass=False`
|
||||
- `OOS-06` / `database_composition_judgment`: `pass=True`
|
||||
- `OOS-07` / `same_session_memory_set`: `pass=True`
|
||||
- `OOS-08` / `same_session_memory_recall`: `pass=True`
|
||||
- `OOS-09` / `telegram_participant_identity`: `pass=True`
|
||||
|
||||
## Claim Ceiling
|
||||
|
||||
A pass proves broad out-of-sample and same-session-memory behavior through the live VPS GatewayRunner using a temporary profile, with no Telegram post and no DB count change. It does not prove human-visible Telegram delivery, production DB apply, or GCP parity.
|
||||
|
|
@ -52,6 +52,32 @@ good default is:
|
|||
3. final answer with what is grounded, what is weak, and what evidence would
|
||||
improve it.
|
||||
|
||||
Lead with the answer. Unless the operator asks for a detailed audit, keep the
|
||||
whole reply under 220 words and end with one proof-changing follow-up. Do not
|
||||
turn a direct operational question into an architecture lecture.
|
||||
|
||||
Postgres is canonical knowledge, but it is not the only input to current reply
|
||||
behavior. Deployed skills, runtime configuration, rendered `SOUL.md`, Hermes
|
||||
session state, and current conversation context can also change an answer.
|
||||
Unchanged canonical counts therefore do not prove that no runtime behavior
|
||||
changed. Hermes `state.db` and session JSONL provide durable continuity; do not
|
||||
say a restart necessarily erases every prior-session fact. Separate these proof
|
||||
tiers explicitly:
|
||||
|
||||
- handler or temporary-profile success with no Telegram post is handler proof,
|
||||
not Telegram-visible delivery proof;
|
||||
- a Telegram-visible reply proves delivery and reply behavior, not canonical
|
||||
DB mutation;
|
||||
- canonical mutation requires row-level `public.*` readback and the matching
|
||||
applied proposal receipt.
|
||||
|
||||
Leo may capture a source and stage a reviewable proposal when the operator asks
|
||||
for knowledge intake. Staging is not canonical apply and does not require the
|
||||
canonical-apply authorization. Approval is required before the guarded apply
|
||||
step. Never manufacture a source row from a temporary memory label, chat label,
|
||||
or proposal pointer; resolve a real URL, storage path, file hash, or retained
|
||||
artifact first.
|
||||
|
||||
For no-context direct claims such as "Is X in Leo now?", "did the DB change?",
|
||||
"did the decision matrix approve this?", or "is it still just proposals?", do
|
||||
not stop at `search` or default `list-proposals`. Run the status-specific
|
||||
|
|
@ -134,6 +160,18 @@ Current `public.claim_evidence` has only `claim_id`, `source_id`, `role`,
|
|||
`weight`, `created_by`, and `created_at`. Its accepted roles are `grounds`,
|
||||
`illustrates`, and `contradicts`. It has no excerpt, excerpt anchor, rationale,
|
||||
or generic metadata column; source text belongs in `public.sources.excerpt`.
|
||||
A `public.claim_evidence` link from a claim to a `public.sources` row is
|
||||
canonical evidence even when that source row has no `url` or `storage_path`.
|
||||
Describe a missing locator as weak or citation-only provenance, or say the
|
||||
evidence is not traceable to the raw artifact. Do not call the canonical link
|
||||
non-canonical or ungrounded solely because the locator is missing.
|
||||
|
||||
A Telegram attachment, extracted file, or proposal `source_ref` does not by
|
||||
itself prove canonical evidence from that attachment. That proof requires a
|
||||
`public.sources` row representing the attachment and a `public.claim_evidence`
|
||||
link from the claim to that source row. Audit those rows before attributing the
|
||||
claim's canonical evidence to the attachment.
|
||||
|
||||
Current `public.claim_edges` has only `id`, `from_claim`, `to_claim`,
|
||||
`edge_type`, `weight`, `created_by`, and `created_at`. Both endpoints are claim
|
||||
IDs, so do not claim that a `reasoning_tools` row is directly connected through
|
||||
|
|
@ -142,16 +180,22 @@ IDs, so do not claim that a `reasoning_tools` row is directly connected through
|
|||
For heterogeneous research packets, map only to structures proven in the
|
||||
current schema:
|
||||
|
||||
- claims, sources, and evidence links are shared knowledge objects; an agent's
|
||||
confidence, stance, or position belongs in agent-specific belief/position
|
||||
structures rather than duplicate agent-authored copies of the same claim;
|
||||
|
||||
- factual observations and disputed interpretations may become separate
|
||||
`public.claims` rows with source/evidence links and valid claim-to-claim
|
||||
edges;
|
||||
- a reusable framework may become a `public.reasoning_tools` row, but the
|
||||
current schema has no generic reasoning-tool-to-claim edge and no shipped
|
||||
`concept_maps` or `claim_concept_map_links` table;
|
||||
- `public.governance_gates` can store an evaluative gate with `name`,
|
||||
`criteria`, `evidence_bar`, and `pass_condition`; it is not a generic
|
||||
behavioral-rule or policy table, so state a schema gap when a governance rule
|
||||
does not fit that contract;
|
||||
- a behavioral or operating rule belongs in the existing
|
||||
`public.behavioral_rules` table, whose rule contract includes `agent_id`,
|
||||
`category`, `rank`, `rule`, and `rationale`;
|
||||
- an evaluative gate belongs in `public.governance_gates`, with `name`,
|
||||
`criteria`, `evidence_bar`, and `pass_condition`; do not flatten a behavioral
|
||||
rule into this gate table;
|
||||
- a belief correction may create a new claim, a valid `supersedes` edge, and
|
||||
set the old claim's `superseded_by` column. `superseded_by` is a column, not
|
||||
an edge type.
|
||||
|
|
@ -175,11 +219,13 @@ universal success condition.
|
|||
|
||||
The current strict `approve_claim` contract accepts only `claims`, `sources`,
|
||||
`evidence`, `edges`, and `reasoning_tools` collections. It does not insert
|
||||
`governance_gates`, update an existing claim's `status` or `superseded_by`, or
|
||||
write arbitrary soul/context rows. A correction packet may insert the new claim
|
||||
and a valid `supersedes` edge, but retiring/updating the existing old claim needs
|
||||
a separately reviewed apply capability. Do not describe that unsupported update
|
||||
as part of one atomic `approve_claim` transaction.
|
||||
`behavioral_rules` or `governance_gates`, update an existing claim's `status`
|
||||
or `superseded_by`, or write arbitrary soul/context rows. Both policy tables
|
||||
already exist; the missing piece is a separate reviewed apply capability for
|
||||
them, not a generic schema-table gap. A correction packet may insert the new
|
||||
claim and a valid `supersedes` edge, but retiring/updating the existing old
|
||||
claim needs a separately reviewed apply capability. Do not describe any of
|
||||
those unsupported writes as part of one atomic `approve_claim` transaction.
|
||||
|
||||
Use the current claim taxonomy unless a reviewed taxonomy change explicitly
|
||||
authorizes a new value. The live values are `structural`, `normative`,
|
||||
|
|
@ -192,6 +238,13 @@ evidence, say that `public.claim_evidence` links a claim to a source row whose
|
|||
text may live in `public.sources.excerpt`; never say the evidence row stores or
|
||||
carries the excerpt.
|
||||
|
||||
Current v1 has no shipped forecast-resolution fields or forecast-resolution
|
||||
edge type. Preserve the original probability and its timestamp. Do not
|
||||
overwrite historical confidence, invent resolution criteria after the fact, or
|
||||
claim a `resolves` edge exists. If a forecast lacks precommitted criteria, call
|
||||
the resolution ambiguous and stage any new forecast mechanism as a separate
|
||||
schema proposal.
|
||||
|
||||
- "Did we actually update the KB?": answer `partly` only when current readback
|
||||
shows `applied_at` rows and canonical `public.*` rows. Otherwise say
|
||||
`mostly still proposals`; list applied, approved-but-not-applied, pending,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,32 @@ For KB questions, prefer the bridge over raw database access. A good default is:
|
|||
3. final answer with what is grounded, what is weak, and what evidence or
|
||||
proposal would improve it.
|
||||
|
||||
Lead with the answer. Unless the operator asks for a detailed audit, keep the
|
||||
whole reply under 220 words and end with one proof-changing follow-up. Do not
|
||||
turn a direct operational question into an architecture lecture.
|
||||
|
||||
Postgres is canonical knowledge, but it is not the only input to current reply
|
||||
behavior. Deployed skills, runtime configuration, rendered `SOUL.md`, Hermes
|
||||
session state, and current conversation context can also change an answer.
|
||||
Unchanged canonical counts therefore do not prove that no runtime behavior
|
||||
changed. Hermes `state.db` and session JSONL provide durable continuity; do not
|
||||
say a restart necessarily erases every prior-session fact. Separate these proof
|
||||
tiers explicitly:
|
||||
|
||||
- handler or temporary-profile success with no Telegram post is handler proof,
|
||||
not Telegram-visible delivery proof;
|
||||
- a Telegram-visible reply proves delivery and reply behavior, not canonical
|
||||
DB mutation;
|
||||
- canonical mutation requires row-level `public.*` readback and the matching
|
||||
applied proposal receipt.
|
||||
|
||||
Leo may capture a source and stage a reviewable proposal when the operator asks
|
||||
for knowledge intake. Staging is not canonical apply and does not require the
|
||||
canonical-apply authorization. Approval is required before the guarded apply
|
||||
step. Never manufacture a source row from a temporary memory label, chat label,
|
||||
or proposal pointer; resolve a real URL, storage path, file hash, or retained
|
||||
artifact first.
|
||||
|
||||
For no-context direct claims such as "Is X in Leo now?", "did the DB change?",
|
||||
"did the decision matrix approve this?", or "is it still just proposals?", do
|
||||
not stop at `search` or default `list-proposals`. Run the status-specific
|
||||
|
|
@ -127,6 +153,18 @@ Current `public.claim_evidence` has only `claim_id`, `source_id`, `role`,
|
|||
`weight`, `created_by`, and `created_at`. Its accepted roles are `grounds`,
|
||||
`illustrates`, and `contradicts`. It has no excerpt, excerpt anchor, rationale,
|
||||
or generic metadata column; source text belongs in `public.sources.excerpt`.
|
||||
A `public.claim_evidence` link from a claim to a `public.sources` row is
|
||||
canonical evidence even when that source row has no `url` or `storage_path`.
|
||||
Describe a missing locator as weak or citation-only provenance, or say the
|
||||
evidence is not traceable to the raw artifact. Do not call the canonical link
|
||||
non-canonical or ungrounded solely because the locator is missing.
|
||||
|
||||
A Telegram attachment, extracted file, or proposal `source_ref` does not by
|
||||
itself prove canonical evidence from that attachment. That proof requires a
|
||||
`public.sources` row representing the attachment and a `public.claim_evidence`
|
||||
link from the claim to that source row. Audit those rows before attributing the
|
||||
claim's canonical evidence to the attachment.
|
||||
|
||||
Current `public.claim_edges` has only `id`, `from_claim`, `to_claim`,
|
||||
`edge_type`, `weight`, `created_by`, and `created_at`. Both endpoints are claim
|
||||
IDs, so do not claim that a `reasoning_tools` row is directly connected through
|
||||
|
|
@ -135,16 +173,22 @@ IDs, so do not claim that a `reasoning_tools` row is directly connected through
|
|||
For heterogeneous research packets, map only to structures proven in the
|
||||
current schema:
|
||||
|
||||
- claims, sources, and evidence links are shared knowledge objects; an agent's
|
||||
confidence, stance, or position belongs in agent-specific belief/position
|
||||
structures rather than duplicate agent-authored copies of the same claim;
|
||||
|
||||
- factual observations and disputed interpretations may become separate
|
||||
`public.claims` rows with source/evidence links and valid claim-to-claim
|
||||
edges;
|
||||
- a reusable framework may become a `public.reasoning_tools` row, but the
|
||||
current schema has no generic reasoning-tool-to-claim edge and no shipped
|
||||
`concept_maps` or `claim_concept_map_links` table;
|
||||
- `public.governance_gates` can store an evaluative gate with `name`,
|
||||
`criteria`, `evidence_bar`, and `pass_condition`; it is not a generic
|
||||
behavioral-rule or policy table, so state a schema gap when a governance rule
|
||||
does not fit that contract;
|
||||
- a behavioral or operating rule belongs in the existing
|
||||
`public.behavioral_rules` table, whose rule contract includes `agent_id`,
|
||||
`category`, `rank`, `rule`, and `rationale`;
|
||||
- an evaluative gate belongs in `public.governance_gates`, with `name`,
|
||||
`criteria`, `evidence_bar`, and `pass_condition`; do not flatten a behavioral
|
||||
rule into this gate table;
|
||||
- a belief correction may create a new claim, a valid `supersedes` edge, and
|
||||
set the old claim's `superseded_by` column. `superseded_by` is a column, not
|
||||
an edge type.
|
||||
|
|
@ -168,11 +212,13 @@ universal success condition.
|
|||
|
||||
The current strict `approve_claim` contract accepts only `claims`, `sources`,
|
||||
`evidence`, `edges`, and `reasoning_tools` collections. It does not insert
|
||||
`governance_gates`, update an existing claim's `status` or `superseded_by`, or
|
||||
write arbitrary soul/context rows. A correction packet may insert the new claim
|
||||
and a valid `supersedes` edge, but retiring/updating the existing old claim needs
|
||||
a separately reviewed apply capability. Do not describe that unsupported update
|
||||
as part of one atomic `approve_claim` transaction.
|
||||
`behavioral_rules` or `governance_gates`, update an existing claim's `status`
|
||||
or `superseded_by`, or write arbitrary soul/context rows. Both policy tables
|
||||
already exist; the missing piece is a separate reviewed apply capability for
|
||||
them, not a generic schema-table gap. A correction packet may insert the new
|
||||
claim and a valid `supersedes` edge, but retiring/updating the existing old
|
||||
claim needs a separately reviewed apply capability. Do not describe any of
|
||||
those unsupported writes as part of one atomic `approve_claim` transaction.
|
||||
|
||||
Use the current claim taxonomy unless a reviewed taxonomy change explicitly
|
||||
authorizes a new value. The live values are `structural`, `normative`,
|
||||
|
|
@ -185,6 +231,13 @@ evidence, say that `public.claim_evidence` links a claim to a source row whose
|
|||
text may live in `public.sources.excerpt`; never say the evidence row stores or
|
||||
carries the excerpt.
|
||||
|
||||
Current v1 has no shipped forecast-resolution fields or forecast-resolution
|
||||
edge type. Preserve the original probability and its timestamp. Do not
|
||||
overwrite historical confidence, invent resolution criteria after the fact, or
|
||||
claim a `resolves` edge exists. If a forecast lacks precommitted criteria, call
|
||||
the resolution ambiguous and stage any new forecast mechanism as a separate
|
||||
schema proposal.
|
||||
|
||||
- "Did we actually update the KB?": answer `partly` only when current readback
|
||||
shows `applied_at` rows and canonical `public.*` rows. Otherwise say
|
||||
`mostly still proposals`; list applied, approved-but-not-applied, pending,
|
||||
|
|
|
|||
819
ops/run_local_canonical_postgres_rebuild.py
Executable file
819
ops/run_local_canonical_postgres_rebuild.py
Executable file
|
|
@ -0,0 +1,819 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Restore and verify a canonical PostgreSQL dump in an isolated Docker canary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from .verify_postgres_parity_manifest import compare_manifests, load_manifest
|
||||
except ImportError: # pragma: no cover - direct script execution
|
||||
from verify_postgres_parity_manifest import compare_manifests, load_manifest
|
||||
|
||||
|
||||
DEFAULT_IMAGE = "postgres:16-alpine"
|
||||
DEFAULT_MANIFEST_SQL = Path(__file__).with_name("postgres_parity_manifest.sql")
|
||||
DATA_DIR = "/var/lib/postgresql/data"
|
||||
DUMP_DESTINATION = "/tmp/teleo-canonical.dump"
|
||||
MANIFEST_DESTINATION = "/tmp/postgres-parity-manifest.sql"
|
||||
CANARY_LABEL = "com.livingip.teleo.canonical-rebuild-canary"
|
||||
DATABASE_NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]{0,62}\Z")
|
||||
CONTAINER_PREFIX_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,39}\Z")
|
||||
ALLOWED_APPLICATION_ROLES = frozenset({"kb_apply", "kb_review"})
|
||||
ROLE_BOOLEAN_FIELDS = (
|
||||
"can_login",
|
||||
"superuser",
|
||||
"create_db",
|
||||
"create_role",
|
||||
"inherit",
|
||||
"replication",
|
||||
"bypass_rls",
|
||||
)
|
||||
KEY_TABLES = (
|
||||
"public.claims",
|
||||
"public.sources",
|
||||
"public.claim_evidence",
|
||||
"public.claim_edges",
|
||||
"public.reasoning_tools",
|
||||
"kb_stage.kb_proposals",
|
||||
)
|
||||
|
||||
|
||||
class TerminationRequested(RuntimeError):
|
||||
"""Raised after SIGINT/SIGTERM so the normal cleanup path still runs."""
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def validate_custom_dump(path: Path) -> None:
|
||||
if not path.is_file():
|
||||
raise ValueError(f"custom dump does not exist or is not a regular file: {path}")
|
||||
if path.stat().st_size <= 5:
|
||||
raise ValueError("custom dump is empty or truncated")
|
||||
with path.open("rb") as handle:
|
||||
signature = handle.read(5)
|
||||
if signature != b"PGDMP":
|
||||
raise ValueError("dump is not PostgreSQL custom format (missing PGDMP signature)")
|
||||
|
||||
|
||||
def new_container_name(prefix: str) -> str:
|
||||
timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
|
||||
return f"{prefix}-{timestamp}-{uuid.uuid4().hex[:12]}"
|
||||
|
||||
|
||||
def _run(
|
||||
command: list[str],
|
||||
*,
|
||||
timeout: float,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
def _bounded_error(completed: subprocess.CompletedProcess[str]) -> str:
|
||||
message = (completed.stderr or completed.stdout or "no command output").strip()
|
||||
return message[-2000:]
|
||||
|
||||
|
||||
def _require_success(completed: subprocess.CompletedProcess[str], action: str) -> str:
|
||||
if completed.returncode != 0:
|
||||
raise RuntimeError(f"{action} failed (exit {completed.returncode}): {_bounded_error(completed)}")
|
||||
return completed.stdout
|
||||
|
||||
|
||||
def _absence_result(completed: subprocess.CompletedProcess[str]) -> tuple[bool | None, str | None]:
|
||||
if completed.returncode == 0:
|
||||
return False, None
|
||||
message = (completed.stderr or completed.stdout or "").strip()
|
||||
lowered = message.lower()
|
||||
if "no such object" in lowered or "no such container" in lowered:
|
||||
return True, None
|
||||
return None, message[-2000:] or f"docker inspect exited {completed.returncode}"
|
||||
|
||||
|
||||
def inspect_container_absence(
|
||||
docker_bin: str,
|
||||
container: str,
|
||||
*,
|
||||
timeout: float,
|
||||
) -> tuple[bool | None, str | None]:
|
||||
completed = _run([docker_bin, "container", "inspect", container], timeout=timeout)
|
||||
return _absence_result(completed)
|
||||
|
||||
|
||||
def inspect_container_isolation(
|
||||
docker_bin: str,
|
||||
container: str,
|
||||
*,
|
||||
expected_image: str,
|
||||
expected_label: str,
|
||||
timeout: float,
|
||||
) -> dict[str, Any]:
|
||||
completed = _run([docker_bin, "container", "inspect", container], timeout=timeout)
|
||||
raw = _require_success(completed, "container isolation inspection")
|
||||
try:
|
||||
rows = json.loads(raw)
|
||||
observed = rows[0]
|
||||
host_config = observed["HostConfig"]
|
||||
config = observed["Config"]
|
||||
except (IndexError, KeyError, TypeError, json.JSONDecodeError) as exc:
|
||||
raise RuntimeError("container isolation inspection returned invalid JSON") from exc
|
||||
|
||||
network_mode = host_config.get("NetworkMode")
|
||||
tmpfs = host_config.get("Tmpfs") or {}
|
||||
labels = config.get("Labels") or {}
|
||||
image = config.get("Image")
|
||||
running = bool((observed.get("State") or {}).get("Running"))
|
||||
problems = []
|
||||
if network_mode != "none":
|
||||
problems.append(f"network_mode={network_mode!r}")
|
||||
if DATA_DIR not in tmpfs:
|
||||
problems.append("postgres_data_tmpfs_missing")
|
||||
if labels.get(CANARY_LABEL) != expected_label:
|
||||
problems.append("canary_label_mismatch")
|
||||
if image != expected_image:
|
||||
problems.append(f"image={image!r}")
|
||||
if not running:
|
||||
problems.append("container_not_running")
|
||||
if problems:
|
||||
raise RuntimeError("container isolation check failed: " + ", ".join(problems))
|
||||
return {
|
||||
"network_mode": network_mode,
|
||||
"postgres_data_tmpfs": tmpfs[DATA_DIR],
|
||||
"image": image,
|
||||
"label": labels[CANARY_LABEL],
|
||||
"running": running,
|
||||
}
|
||||
|
||||
|
||||
def _psql_command(container: str, database: str, *arguments: str, docker_bin: str) -> list[str]:
|
||||
return [
|
||||
docker_bin,
|
||||
"exec",
|
||||
container,
|
||||
"psql",
|
||||
"-X",
|
||||
"-U",
|
||||
"postgres",
|
||||
"-d",
|
||||
database,
|
||||
"-Atq",
|
||||
"-v",
|
||||
"ON_ERROR_STOP=1",
|
||||
*arguments,
|
||||
]
|
||||
|
||||
|
||||
def wait_for_named_database(
|
||||
docker_bin: str,
|
||||
container: str,
|
||||
database: str,
|
||||
*,
|
||||
startup_timeout: float,
|
||||
command_timeout: float,
|
||||
poll_interval: float,
|
||||
) -> int:
|
||||
deadline = time.monotonic() + startup_timeout
|
||||
attempts = 0
|
||||
last_error = "no psql attempt completed"
|
||||
while True:
|
||||
attempts += 1
|
||||
remaining = max(0.1, deadline - time.monotonic())
|
||||
completed = _run(
|
||||
_psql_command(
|
||||
container,
|
||||
database,
|
||||
"-c",
|
||||
"select current_database();",
|
||||
docker_bin=docker_bin,
|
||||
),
|
||||
timeout=min(command_timeout, remaining),
|
||||
)
|
||||
if completed.returncode == 0 and completed.stdout.strip() == database:
|
||||
return attempts
|
||||
last_error = _bounded_error(completed)
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError(
|
||||
f"named database did not accept a psql connection within {startup_timeout:g}s: {last_error}"
|
||||
)
|
||||
time.sleep(min(poll_interval, max(0.0, deadline - time.monotonic())))
|
||||
|
||||
|
||||
def _role_keyword(value: bool, enabled: str, disabled: str) -> str:
|
||||
return enabled if value else disabled
|
||||
|
||||
|
||||
def application_role_sql(source_manifest: dict[str, Any]) -> str:
|
||||
items = source_manifest["singleton"]["application_roles"].get("items", [])
|
||||
if not isinstance(items, list):
|
||||
raise ValueError("source manifest application_roles.items must be a list")
|
||||
statements = []
|
||||
seen: set[str] = set()
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError("source manifest application role must be an object")
|
||||
name = item.get("name")
|
||||
if name not in ALLOWED_APPLICATION_ROLES:
|
||||
raise ValueError(f"source manifest contains non-allowlisted application role: {name!r}")
|
||||
if name in seen:
|
||||
raise ValueError(f"source manifest contains duplicate application role: {name}")
|
||||
seen.add(name)
|
||||
for field in ROLE_BOOLEAN_FIELDS:
|
||||
if type(item.get(field)) is not bool:
|
||||
raise ValueError(f"source manifest application role {name} has invalid {field}")
|
||||
attributes = [
|
||||
_role_keyword(item["can_login"], "LOGIN", "NOLOGIN"),
|
||||
_role_keyword(item["superuser"], "SUPERUSER", "NOSUPERUSER"),
|
||||
_role_keyword(item["create_db"], "CREATEDB", "NOCREATEDB"),
|
||||
_role_keyword(item["create_role"], "CREATEROLE", "NOCREATEROLE"),
|
||||
_role_keyword(item["inherit"], "INHERIT", "NOINHERIT"),
|
||||
_role_keyword(item["replication"], "REPLICATION", "NOREPLICATION"),
|
||||
_role_keyword(item["bypass_rls"], "BYPASSRLS", "NOBYPASSRLS"),
|
||||
]
|
||||
statements.append(f'CREATE ROLE "{name}" {" ".join(attributes)};')
|
||||
return "\n".join(statements)
|
||||
|
||||
|
||||
def execute_application_role_bootstrap(
|
||||
docker_bin: str,
|
||||
container: str,
|
||||
database: str,
|
||||
source_manifest: dict[str, Any],
|
||||
*,
|
||||
timeout: float,
|
||||
) -> list[str]:
|
||||
sql = application_role_sql(source_manifest)
|
||||
if not sql:
|
||||
return []
|
||||
completed = _run(
|
||||
_psql_command(container, database, "-c", sql, docker_bin=docker_bin),
|
||||
timeout=timeout,
|
||||
)
|
||||
_require_success(completed, "application role bootstrap")
|
||||
return sorted(item["name"] for item in source_manifest["singleton"]["application_roles"].get("items", []))
|
||||
|
||||
|
||||
def query_scalar(
|
||||
docker_bin: str,
|
||||
container: str,
|
||||
database: str,
|
||||
sql: str,
|
||||
*,
|
||||
timeout: float,
|
||||
) -> str:
|
||||
completed = _run(
|
||||
_psql_command(container, database, "-c", sql, docker_bin=docker_bin),
|
||||
timeout=timeout,
|
||||
)
|
||||
return _require_success(completed, "PostgreSQL readback").strip()
|
||||
|
||||
|
||||
def collect_key_counts(
|
||||
docker_bin: str,
|
||||
container: str,
|
||||
database: str,
|
||||
*,
|
||||
timeout: float,
|
||||
) -> tuple[dict[str, int], int]:
|
||||
counts: dict[str, int] = {}
|
||||
for table in KEY_TABLES:
|
||||
exists = query_scalar(
|
||||
docker_bin,
|
||||
container,
|
||||
database,
|
||||
f"select to_regclass('{table}') is not null;",
|
||||
timeout=timeout,
|
||||
)
|
||||
if exists != "t":
|
||||
raise RuntimeError(f"required canonical table is missing after restore: {table}")
|
||||
schema, name = table.split(".", 1)
|
||||
raw_count = query_scalar(
|
||||
docker_bin,
|
||||
container,
|
||||
database,
|
||||
f'select count(*) from "{schema}"."{name}";',
|
||||
timeout=timeout,
|
||||
)
|
||||
try:
|
||||
counts[table] = int(raw_count)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(f"invalid row count for {table}: {raw_count!r}") from exc
|
||||
raw_table_count = query_scalar(
|
||||
docker_bin,
|
||||
container,
|
||||
database,
|
||||
"select count(*) from pg_tables where schemaname in ('public', 'kb_stage');",
|
||||
timeout=timeout,
|
||||
)
|
||||
try:
|
||||
table_count = int(raw_table_count)
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(f"invalid canonical schema table count: {raw_table_count!r}") from exc
|
||||
return counts, table_count
|
||||
|
||||
|
||||
def run_parity_check(
|
||||
docker_bin: str,
|
||||
container: str,
|
||||
database: str,
|
||||
*,
|
||||
source_manifest_path: Path,
|
||||
source_manifest: dict[str, Any],
|
||||
manifest_sql: Path,
|
||||
command_timeout: float,
|
||||
max_target_query_ms: float,
|
||||
max_target_source_ratio: float,
|
||||
) -> dict[str, Any]:
|
||||
copy_result = _run(
|
||||
[docker_bin, "cp", str(manifest_sql), f"{container}:{MANIFEST_DESTINATION}"],
|
||||
timeout=command_timeout,
|
||||
)
|
||||
_require_success(copy_result, "parity manifest copy")
|
||||
completed = _run(
|
||||
_psql_command(
|
||||
container,
|
||||
database,
|
||||
"-f",
|
||||
MANIFEST_DESTINATION,
|
||||
docker_bin=docker_bin,
|
||||
),
|
||||
timeout=command_timeout,
|
||||
)
|
||||
target_payload = _require_success(completed, "target parity manifest capture")
|
||||
with tempfile.TemporaryDirectory(prefix="teleo-canonical-parity-") as temp_dir:
|
||||
target_path = Path(temp_dir) / "target-manifest.jsonl"
|
||||
target_path.write_text(target_payload, encoding="utf-8")
|
||||
target_manifest = load_manifest(target_path)
|
||||
problems, details = compare_manifests(
|
||||
source_manifest,
|
||||
target_manifest,
|
||||
max_target_query_ms=max_target_query_ms,
|
||||
max_target_source_ratio=max_target_source_ratio,
|
||||
)
|
||||
target_receipt = {
|
||||
"bytes": target_path.stat().st_size,
|
||||
"sha256": sha256_file(target_path),
|
||||
"line_count": sum(1 for line in target_payload.splitlines() if line.strip()),
|
||||
}
|
||||
return {
|
||||
"requested": True,
|
||||
"status": "pass" if not problems else "fail",
|
||||
"verifier": "ops.verify_postgres_parity_manifest.compare_manifests",
|
||||
"source_manifest": {
|
||||
"path": str(source_manifest_path),
|
||||
"bytes": source_manifest_path.stat().st_size,
|
||||
"sha256": sha256_file(source_manifest_path),
|
||||
},
|
||||
"target_manifest": target_receipt,
|
||||
"problems": problems,
|
||||
"details": details,
|
||||
}
|
||||
|
||||
|
||||
def cleanup_container(
|
||||
docker_bin: str,
|
||||
container: str,
|
||||
*,
|
||||
command_timeout: float,
|
||||
poll_interval: float,
|
||||
) -> dict[str, Any]:
|
||||
cleanup: dict[str, Any] = {
|
||||
"attempted": True,
|
||||
"container": container,
|
||||
"force_remove_attempted": True,
|
||||
"container_absent": None,
|
||||
"status": "unproven",
|
||||
}
|
||||
try:
|
||||
removed = _run([docker_bin, "rm", "--force", container], timeout=command_timeout)
|
||||
cleanup["force_remove_exit_code"] = removed.returncode
|
||||
if removed.returncode != 0:
|
||||
cleanup["force_remove_message"] = _bounded_error(removed)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
cleanup["force_remove_error"] = str(exc)
|
||||
except (KeyboardInterrupt, TerminationRequested) as exc:
|
||||
cleanup["interrupted"] = str(exc)
|
||||
cleanup["force_remove_error"] = str(exc)
|
||||
|
||||
last_error = None
|
||||
for attempt in range(1, 6):
|
||||
cleanup["absence_inspection_attempts"] = attempt
|
||||
try:
|
||||
absent, error = inspect_container_absence(
|
||||
docker_bin,
|
||||
container,
|
||||
timeout=command_timeout,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
absent, error = None, str(exc)
|
||||
except (KeyboardInterrupt, TerminationRequested) as exc:
|
||||
cleanup["interrupted"] = str(exc)
|
||||
absent, error = None, str(exc)
|
||||
if absent is True:
|
||||
cleanup["container_absent"] = True
|
||||
cleanup["status"] = "pass"
|
||||
cleanup.pop("absence_inspection_error", None)
|
||||
return cleanup
|
||||
cleanup["container_absent"] = absent
|
||||
last_error = error
|
||||
if error and "interrupted" not in cleanup:
|
||||
cleanup["absence_inspection_error"] = error
|
||||
break
|
||||
if attempt < 5:
|
||||
try:
|
||||
time.sleep(poll_interval)
|
||||
except (KeyboardInterrupt, TerminationRequested) as exc:
|
||||
cleanup["interrupted"] = str(exc)
|
||||
if last_error:
|
||||
cleanup["absence_inspection_error"] = last_error
|
||||
cleanup["status"] = "fail" if cleanup["container_absent"] is False else "unproven"
|
||||
return cleanup
|
||||
|
||||
|
||||
def _initial_receipt(args: argparse.Namespace, container: str) -> dict[str, Any]:
|
||||
return {
|
||||
"artifact": "local_canonical_postgres_rebuild_canary",
|
||||
"generated_at_utc": datetime.now(UTC).isoformat(),
|
||||
"status": "running",
|
||||
"database": args.database,
|
||||
"dump": {
|
||||
"path": str(args.dump.resolve()),
|
||||
"bytes": None,
|
||||
"sha256": None,
|
||||
"custom_format_signature_valid": False,
|
||||
},
|
||||
"container": {
|
||||
"name": container,
|
||||
"image": args.image,
|
||||
"network_mode_required": "none",
|
||||
"postgres_data_tmpfs_required": f"rw,nosuid,nodev,noexec,size={args.tmpfs_mb}m",
|
||||
"preexisting_absence_proven": False,
|
||||
"started": False,
|
||||
"isolation": None,
|
||||
},
|
||||
"application_roles_bootstrapped": [],
|
||||
"key_counts": {},
|
||||
"canonical_schema_table_count": None,
|
||||
"parity": {
|
||||
"requested": args.source_manifest is not None,
|
||||
"status": "pending" if args.source_manifest is not None else "not_requested",
|
||||
"reason": None if args.source_manifest is not None else "--source-manifest was not supplied",
|
||||
},
|
||||
"timings_seconds": {},
|
||||
"cleanup": {
|
||||
"attempted": False,
|
||||
"container": container,
|
||||
"container_absent": None,
|
||||
"status": "pending",
|
||||
},
|
||||
"safety": {
|
||||
"production_database_touched": False,
|
||||
"network_access_available_to_container": False,
|
||||
"persistent_postgres_storage_used": False,
|
||||
},
|
||||
"error": None,
|
||||
}
|
||||
|
||||
|
||||
def run_canary(args: argparse.Namespace) -> dict[str, Any]:
|
||||
started_at = time.monotonic()
|
||||
container = new_container_name(args.container_prefix)
|
||||
receipt = _initial_receipt(args, container)
|
||||
phase = "validation"
|
||||
source_manifest: dict[str, Any] | None = None
|
||||
primary_error: dict[str, str] | None = None
|
||||
|
||||
try:
|
||||
hash_started = time.monotonic()
|
||||
validate_custom_dump(args.dump)
|
||||
receipt["dump"].update(
|
||||
{
|
||||
"bytes": args.dump.stat().st_size,
|
||||
"sha256": sha256_file(args.dump),
|
||||
"custom_format_signature_valid": True,
|
||||
}
|
||||
)
|
||||
receipt["timings_seconds"]["dump_validation_and_hash"] = round(time.monotonic() - hash_started, 6)
|
||||
|
||||
if args.source_manifest is not None:
|
||||
phase = "source_manifest_validation"
|
||||
source_manifest = load_manifest(args.source_manifest)
|
||||
|
||||
phase = "container_name_preflight"
|
||||
absent, absence_error = inspect_container_absence(
|
||||
args.docker_bin,
|
||||
container,
|
||||
timeout=args.command_timeout,
|
||||
)
|
||||
if absent is not True:
|
||||
detail = absence_error or "generated container name unexpectedly already exists"
|
||||
raise RuntimeError(f"could not prove generated container name is unused: {detail}")
|
||||
receipt["container"]["preexisting_absence_proven"] = True
|
||||
|
||||
phase = "container_start"
|
||||
startup_started = time.monotonic()
|
||||
tmpfs_options = f"{DATA_DIR}:rw,nosuid,nodev,noexec,size={args.tmpfs_mb}m"
|
||||
run_result = _run(
|
||||
[
|
||||
args.docker_bin,
|
||||
"run",
|
||||
"--detach",
|
||||
"--rm",
|
||||
"--network",
|
||||
"none",
|
||||
"--name",
|
||||
container,
|
||||
"--label",
|
||||
f"{CANARY_LABEL}={container}",
|
||||
"--tmpfs",
|
||||
tmpfs_options,
|
||||
"--env",
|
||||
f"POSTGRES_DB={args.database}",
|
||||
"--env",
|
||||
"POSTGRES_HOST_AUTH_METHOD=trust",
|
||||
args.image,
|
||||
],
|
||||
timeout=args.command_timeout,
|
||||
)
|
||||
container_id = _require_success(run_result, "networkless PostgreSQL container start").strip()
|
||||
if not container_id:
|
||||
raise RuntimeError("networkless PostgreSQL container start returned no container id")
|
||||
receipt["container"]["started"] = True
|
||||
receipt["container"]["id_prefix"] = container_id[:12]
|
||||
receipt["container"]["isolation"] = inspect_container_isolation(
|
||||
args.docker_bin,
|
||||
container,
|
||||
expected_image=args.image,
|
||||
expected_label=container,
|
||||
timeout=args.command_timeout,
|
||||
)
|
||||
|
||||
phase = "named_database_readiness"
|
||||
readiness_attempts = wait_for_named_database(
|
||||
args.docker_bin,
|
||||
container,
|
||||
args.database,
|
||||
startup_timeout=args.startup_timeout,
|
||||
command_timeout=args.command_timeout,
|
||||
poll_interval=args.poll_interval,
|
||||
)
|
||||
receipt["container"]["named_database_psql_ready"] = True
|
||||
receipt["container"]["named_database_psql_attempts"] = readiness_attempts
|
||||
receipt["timings_seconds"]["container_start_and_named_database_ready"] = round(
|
||||
time.monotonic() - startup_started,
|
||||
6,
|
||||
)
|
||||
|
||||
if source_manifest is not None:
|
||||
phase = "application_role_bootstrap"
|
||||
receipt["application_roles_bootstrapped"] = execute_application_role_bootstrap(
|
||||
args.docker_bin,
|
||||
container,
|
||||
args.database,
|
||||
source_manifest,
|
||||
timeout=args.command_timeout,
|
||||
)
|
||||
|
||||
phase = "dump_copy"
|
||||
copy_result = _run(
|
||||
[args.docker_bin, "cp", str(args.dump.resolve()), f"{container}:{DUMP_DESTINATION}"],
|
||||
timeout=args.command_timeout,
|
||||
)
|
||||
_require_success(copy_result, "custom dump copy")
|
||||
|
||||
phase = "restore"
|
||||
restore_started = time.monotonic()
|
||||
restore_result = _run(
|
||||
[
|
||||
args.docker_bin,
|
||||
"exec",
|
||||
container,
|
||||
"pg_restore",
|
||||
"-U",
|
||||
"postgres",
|
||||
"-d",
|
||||
args.database,
|
||||
"--no-owner",
|
||||
"--no-privileges",
|
||||
"--exit-on-error",
|
||||
DUMP_DESTINATION,
|
||||
],
|
||||
timeout=args.restore_timeout,
|
||||
)
|
||||
_require_success(restore_result, "canonical PostgreSQL restore")
|
||||
receipt["timings_seconds"]["pg_restore"] = round(time.monotonic() - restore_started, 6)
|
||||
|
||||
phase = "key_count_readback"
|
||||
count_started = time.monotonic()
|
||||
key_counts, table_count = collect_key_counts(
|
||||
args.docker_bin,
|
||||
container,
|
||||
args.database,
|
||||
timeout=args.command_timeout,
|
||||
)
|
||||
receipt["key_counts"] = key_counts
|
||||
receipt["canonical_schema_table_count"] = table_count
|
||||
receipt["timings_seconds"]["key_count_readback"] = round(time.monotonic() - count_started, 6)
|
||||
|
||||
if source_manifest is not None:
|
||||
phase = "parity_manifest_and_compare"
|
||||
parity_started = time.monotonic()
|
||||
receipt["parity"] = run_parity_check(
|
||||
args.docker_bin,
|
||||
container,
|
||||
args.database,
|
||||
source_manifest_path=args.source_manifest,
|
||||
source_manifest=source_manifest,
|
||||
manifest_sql=args.manifest_sql,
|
||||
command_timeout=args.manifest_timeout,
|
||||
max_target_query_ms=args.max_target_query_ms,
|
||||
max_target_source_ratio=args.max_target_source_ratio,
|
||||
)
|
||||
receipt["timings_seconds"]["manifest_capture_and_parity_compare"] = round(
|
||||
time.monotonic() - parity_started,
|
||||
6,
|
||||
)
|
||||
if receipt["parity"]["status"] != "pass":
|
||||
raise RuntimeError("canonical parity verification failed")
|
||||
receipt["status"] = "pass"
|
||||
except (
|
||||
OSError,
|
||||
ValueError,
|
||||
RuntimeError,
|
||||
subprocess.TimeoutExpired,
|
||||
json.JSONDecodeError,
|
||||
KeyboardInterrupt,
|
||||
TerminationRequested,
|
||||
) as exc:
|
||||
primary_error = {
|
||||
"phase": phase,
|
||||
"type": type(exc).__name__,
|
||||
"message": str(exc)[-2000:],
|
||||
}
|
||||
receipt["status"] = "fail"
|
||||
receipt["error"] = primary_error
|
||||
finally:
|
||||
cleanup_started = time.monotonic()
|
||||
cleanup = cleanup_container(
|
||||
args.docker_bin,
|
||||
container,
|
||||
command_timeout=args.command_timeout,
|
||||
poll_interval=args.poll_interval,
|
||||
)
|
||||
receipt["cleanup"] = cleanup
|
||||
receipt["timings_seconds"]["cleanup"] = round(time.monotonic() - cleanup_started, 6)
|
||||
receipt["timings_seconds"]["total"] = round(time.monotonic() - started_at, 6)
|
||||
if cleanup.get("container_absent") is not True:
|
||||
receipt["status"] = "fail"
|
||||
cleanup_error = {
|
||||
"phase": "cleanup",
|
||||
"type": "CleanupProofError",
|
||||
"message": "container absence was not independently proven",
|
||||
}
|
||||
if primary_error is None:
|
||||
receipt["error"] = cleanup_error
|
||||
else:
|
||||
receipt["cleanup_error"] = cleanup_error
|
||||
if cleanup.get("interrupted"):
|
||||
receipt["status"] = "fail"
|
||||
interruption_error = {
|
||||
"phase": "cleanup",
|
||||
"type": "TerminationRequested",
|
||||
"message": cleanup["interrupted"],
|
||||
}
|
||||
if primary_error is None:
|
||||
receipt["error"] = interruption_error
|
||||
else:
|
||||
receipt["cleanup_interruption"] = interruption_error
|
||||
return receipt
|
||||
|
||||
|
||||
def write_receipt(path: Path, payload: dict[str, Any]) -> None:
|
||||
path = path.expanduser()
|
||||
if not path.is_absolute():
|
||||
path = Path.cwd() / path
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path = path.parent.resolve() / path.name
|
||||
encoded = (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode()
|
||||
temporary = path.parent / f".{path.name}.{uuid.uuid4().hex}.tmp"
|
||||
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb") as handle:
|
||||
handle.write(encoded)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary, path)
|
||||
os.chmod(path, 0o600)
|
||||
finally:
|
||||
try:
|
||||
temporary.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--dump", required=True, type=Path, help="PostgreSQL custom-format pg_dump")
|
||||
parser.add_argument("--database", default="teleo")
|
||||
parser.add_argument("--image", default=DEFAULT_IMAGE)
|
||||
parser.add_argument("--container-prefix", default="teleo-canonical-rebuild")
|
||||
parser.add_argument("--tmpfs-mb", default=1024, type=int)
|
||||
parser.add_argument("--source-manifest", type=Path)
|
||||
parser.add_argument("--manifest-sql", default=DEFAULT_MANIFEST_SQL, type=Path)
|
||||
parser.add_argument("--max-target-query-ms", default=2000.0, type=float)
|
||||
parser.add_argument("--max-target-source-ratio", default=20.0, type=float)
|
||||
parser.add_argument("--startup-timeout", default=60.0, type=float)
|
||||
parser.add_argument("--restore-timeout", default=300.0, type=float)
|
||||
parser.add_argument("--manifest-timeout", default=180.0, type=float)
|
||||
parser.add_argument("--command-timeout", default=30.0, type=float)
|
||||
parser.add_argument("--poll-interval", default=0.25, type=float)
|
||||
parser.add_argument("--docker-bin", default="docker")
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if not DATABASE_NAME_RE.fullmatch(args.database):
|
||||
parser.error("--database must be a safe PostgreSQL identifier up to 63 characters")
|
||||
if not CONTAINER_PREFIX_RE.fullmatch(args.container_prefix):
|
||||
parser.error("--container-prefix must be 1-40 Docker-safe characters")
|
||||
if not args.image or any(character.isspace() for character in args.image):
|
||||
parser.error("--image must be a non-empty Docker image reference without whitespace")
|
||||
if not args.docker_bin or any(character.isspace() for character in args.docker_bin):
|
||||
parser.error("--docker-bin must be one executable name or path without whitespace")
|
||||
if not 64 <= args.tmpfs_mb <= 65536:
|
||||
parser.error("--tmpfs-mb must be between 64 and 65536")
|
||||
for value, flag in (
|
||||
(args.startup_timeout, "--startup-timeout"),
|
||||
(args.restore_timeout, "--restore-timeout"),
|
||||
(args.manifest_timeout, "--manifest-timeout"),
|
||||
(args.command_timeout, "--command-timeout"),
|
||||
(args.poll_interval, "--poll-interval"),
|
||||
(args.max_target_query_ms, "--max-target-query-ms"),
|
||||
(args.max_target_source_ratio, "--max-target-source-ratio"),
|
||||
):
|
||||
if value <= 0:
|
||||
parser.error(f"{flag} must be positive")
|
||||
if not args.dump.is_file():
|
||||
parser.error("--dump must be an existing regular file")
|
||||
if args.source_manifest is not None:
|
||||
if not args.source_manifest.is_file():
|
||||
parser.error("--source-manifest must be an existing regular file")
|
||||
if not args.manifest_sql.is_file():
|
||||
parser.error("--manifest-sql must be an existing regular file when parity is requested")
|
||||
protected_inputs = {args.dump.resolve(), args.manifest_sql.resolve()}
|
||||
if args.source_manifest is not None:
|
||||
protected_inputs.add(args.source_manifest.resolve())
|
||||
if args.output.resolve() in protected_inputs:
|
||||
parser.error("--output must not overwrite a dump or manifest input")
|
||||
return args
|
||||
|
||||
|
||||
def _termination_handler(signum: int, _frame: Any) -> None:
|
||||
raise TerminationRequested(f"received {signal.Signals(signum).name}")
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
previous_handlers = {}
|
||||
for watched_signal in (signal.SIGINT, signal.SIGTERM):
|
||||
previous_handlers[watched_signal] = signal.getsignal(watched_signal)
|
||||
signal.signal(watched_signal, _termination_handler)
|
||||
try:
|
||||
payload = run_canary(args)
|
||||
finally:
|
||||
for watched_signal, previous in previous_handlers.items():
|
||||
signal.signal(watched_signal, previous)
|
||||
|
||||
try:
|
||||
write_receipt(args.output, payload)
|
||||
except OSError as exc:
|
||||
payload["status"] = "fail"
|
||||
payload["receipt_write_error"] = str(exc)
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
return 0 if payload["status"] == "pass" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -74,7 +74,7 @@ SCENARIOS: list[dict[str, Any]] = [
|
|||
},
|
||||
{
|
||||
"id": "OOS-05",
|
||||
"dimension": "source_pointer_is_not_evidence",
|
||||
"dimension": "source_pointer_needs_canonical_link_audit",
|
||||
"message": (
|
||||
"A proposal points to a Telegram attachment and its extracted text exists on disk. Does that mean the "
|
||||
"claim already has canonical evidence? Walk the link chain I should audit and tell me what a valid "
|
||||
|
|
@ -87,7 +87,13 @@ SCENARIOS: list[dict[str, Any]] = [
|
|||
"row_level_proof",
|
||||
"no_overclaim",
|
||||
],
|
||||
"required_concepts": ["source_evidence_chain", "staged_review_apply", "receipt"],
|
||||
"required_concepts": [
|
||||
"source_evidence_chain",
|
||||
"canonical_evidence_boundary",
|
||||
"evidence_provenance_quality",
|
||||
"staged_review_apply",
|
||||
"receipt",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "OOS-06",
|
||||
|
|
@ -99,7 +105,13 @@ SCENARIOS: list[dict[str, Any]] = [
|
|||
"review and apply sequence too, but do not write anything now."
|
||||
),
|
||||
"required_signals": ["canonical_db", "staging_or_review", "caveat_retention", "next_action", "no_overclaim"],
|
||||
"required_concepts": ["heterogeneous_types", "staged_review_apply", "receipt"],
|
||||
"required_concepts": [
|
||||
"heterogeneous_types",
|
||||
"behavioral_rule_storage",
|
||||
"reviewed_policy_apply",
|
||||
"staged_review_apply",
|
||||
"receipt",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "OOS-07",
|
||||
|
|
@ -134,6 +146,72 @@ SCENARIOS: list[dict[str, Any]] = [
|
|||
"required_signals": ["no_overclaim"],
|
||||
"required_concepts": [],
|
||||
},
|
||||
{
|
||||
"id": "OOS-10",
|
||||
"dimension": "runtime_vs_canonical_causality",
|
||||
"message": (
|
||||
"The five database totals are unchanged after a restart. Does that prove Leo's answer behavior is "
|
||||
"unchanged and that every fact from the prior session was erased? Explain which persisted and deployed "
|
||||
"surfaces can affect the answer, and separate the proof tiers. Read-only; keep it under 180 words."
|
||||
),
|
||||
"required_signals": ["canonical_db", "no_overclaim"],
|
||||
"required_concepts": ["runtime_inputs", "durable_session_continuity", "proof_tiers"],
|
||||
},
|
||||
{
|
||||
"id": "OOS-11",
|
||||
"dimension": "shared_claims_agent_positions",
|
||||
"message": (
|
||||
"Two agents read the same evidence and reach different conclusions. Should Leo duplicate the factual "
|
||||
"claim once per agent, or keep shared knowledge and represent agent-specific positions elsewhere? Give "
|
||||
"the current-schema answer and how disagreement remains queryable. Do not write anything."
|
||||
),
|
||||
"required_signals": ["canonical_db", "caveat_retention", "no_overclaim"],
|
||||
"required_concepts": ["shared_knowledge_commons", "agent_specific_positions", "contradiction"],
|
||||
},
|
||||
{
|
||||
"id": "OOS-12",
|
||||
"dimension": "forecast_resolution_without_rewriting_history",
|
||||
"message": (
|
||||
"An old claim recorded a 60% forecast but never defined resolution criteria. The event is now over. "
|
||||
"What may Leo record in the current database without rewriting history, and what needs a schema "
|
||||
"proposal? Do not apply anything."
|
||||
),
|
||||
"required_signals": ["canonical_db", "staging_or_review", "caveat_retention", "no_overclaim"],
|
||||
"required_concepts": ["forecast_history", "forecast_schema_gap", "staged_review_apply"],
|
||||
},
|
||||
{
|
||||
"id": "OOS-13",
|
||||
"dimension": "handler_is_not_telegram_delivery",
|
||||
"message": (
|
||||
"A temporary-profile GatewayRunner answered every test prompt, but the run explicitly posted nothing "
|
||||
"to Telegram. Can I tell a partner the Telegram path is proven live? Answer first, then state exactly "
|
||||
"what this run proves and the smallest test that closes the gap."
|
||||
),
|
||||
"required_signals": ["artifact", "next_action", "no_overclaim"],
|
||||
"required_concepts": ["handler_not_telegram", "proof_tiers", "receipt"],
|
||||
},
|
||||
{
|
||||
"id": "OOS-14",
|
||||
"dimension": "autonomous_source_intake_boundary",
|
||||
"message": (
|
||||
"I hand Leo a document and say: absorb this as far as safely possible without making me approve every "
|
||||
"mechanical step. What can Leo capture and stage immediately, what real source identity must be retained, "
|
||||
"and where does explicit approval begin? Explain only; do not ingest this prompt."
|
||||
),
|
||||
"required_signals": ["authorization", "staging_or_review", "artifact", "no_overclaim"],
|
||||
"required_concepts": ["staging_without_apply_authorization", "real_source_identity", "staged_review_apply"],
|
||||
},
|
||||
{
|
||||
"id": "OOS-15",
|
||||
"dimension": "schema_valid_supersession",
|
||||
"message": (
|
||||
"A canonical claim is wrong. I want the replacement, an explanation, and the old claim visibly retired. "
|
||||
"In current v1, which exact claim and edge fields exist, which requested writes fit approve_claim, and "
|
||||
"which require a separate reviewed apply capability? Do not mutate anything."
|
||||
),
|
||||
"required_signals": ["canonical_db", "staging_or_review", "row_level_proof", "no_overclaim"],
|
||||
"required_concepts": ["valid_supersession", "current_edge_schema", "apply_capability_boundary"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -146,7 +224,9 @@ CONCEPT_PATTERNS: dict[str, tuple[re.Pattern[str], ...]] = {
|
|||
re.I,
|
||||
),
|
||||
),
|
||||
"live_check": (re.compile(r"checked live|I ran `?teleo-kb|fresh readback|current canonical row counts", re.I),),
|
||||
"live_check": (
|
||||
re.compile(r"checked live|I ran `?teleo-kb|fresh readback|live readback|current canonical row counts", re.I),
|
||||
),
|
||||
"material_action": (
|
||||
re.compile(
|
||||
r"next .*action|rebuild .*apply_payload|operator .*authoriz|review .*apply|apply sequence|postflight",
|
||||
|
|
@ -175,7 +255,22 @@ CONCEPT_PATTERNS: dict[str, tuple[re.Pattern[str], ...]] = {
|
|||
re.compile(r"file|attachment|source_ref", re.I),
|
||||
re.compile(r"public\.sources", re.I),
|
||||
re.compile(r"claim_evidence", re.I),
|
||||
re.compile(r"not canonical|staging-layer pointer|proves nothing about the KB", re.I),
|
||||
re.compile(r"audit|link|join|row chain|before-and-after|before/after", re.I),
|
||||
),
|
||||
"canonical_evidence_boundary": (
|
||||
re.compile(r"canonical evidence", re.I),
|
||||
re.compile(r"claim_evidence", re.I),
|
||||
re.compile(r"public\.sources|source row", re.I),
|
||||
re.compile(
|
||||
r"attachment.{0,160}(?:does not|doesn't|is not|isn't|cannot|can't|alone|until|unless)|"
|
||||
r"(?:does not|doesn't|is not|isn't|cannot|can't).{0,100}canonical evidence from (?:that|the) attachment",
|
||||
re.I | re.S,
|
||||
),
|
||||
),
|
||||
"evidence_provenance_quality": (
|
||||
re.compile(r"(?:missing|no|without).{0,50}(?:url|storage(?:_path)?|locator)", re.I | re.S),
|
||||
re.compile(r"weak|citation-only|citation only|not traceable|raw artifact", re.I),
|
||||
re.compile(r"canonical evidence|canonical link", re.I),
|
||||
),
|
||||
"heterogeneous_types": (
|
||||
re.compile(r"claim", re.I),
|
||||
|
|
@ -185,6 +280,97 @@ CONCEPT_PATTERNS: dict[str, tuple[re.Pattern[str], ...]] = {
|
|||
re.compile(r"correction|supersed", re.I),
|
||||
re.compile(r"disput|contradict", re.I),
|
||||
),
|
||||
"behavioral_rule_storage": (
|
||||
re.compile(r"public\.behavioral_rules", re.I),
|
||||
re.compile(r"\bagent_id\b", re.I),
|
||||
re.compile(r"\bcategory\b", re.I),
|
||||
re.compile(r"\brank\b", re.I),
|
||||
re.compile(r"\brule\b", re.I),
|
||||
re.compile(r"\brationale\b", re.I),
|
||||
),
|
||||
"reviewed_policy_apply": (
|
||||
re.compile(r"approve_claim", re.I),
|
||||
re.compile(r"behavioral_rules", re.I),
|
||||
re.compile(r"governance_gates", re.I),
|
||||
re.compile(r"does not (?:accept|insert|support)|supports neither|neither.{0,80}nor", re.I | re.S),
|
||||
re.compile(r"separate.{0,50}reviewed apply|reviewed.{0,50}apply capability", re.I | re.S),
|
||||
),
|
||||
"runtime_inputs": (
|
||||
re.compile(r"Postgres|canonical (?:DB|database|counts?)", re.I),
|
||||
re.compile(r"skills?|runtime config|configuration|SOUL\.md", re.I),
|
||||
re.compile(r"session|conversation context", re.I),
|
||||
re.compile(r"unchanged.{0,80}(?:does not|doesn't|do not).{0,80}(?:behavior|answer)", re.I | re.S),
|
||||
),
|
||||
"durable_session_continuity": (
|
||||
re.compile(r"state\.db|session JSONL", re.I),
|
||||
re.compile(r"persist|durable|continuity", re.I),
|
||||
re.compile(r"restart.{0,80}(?:does not|doesn't|need not|not necessarily).{0,80}(?:erase|forget)", re.I | re.S),
|
||||
),
|
||||
"proof_tiers": (
|
||||
re.compile(r"handler|temporary[- ]profile|GatewayRunner", re.I),
|
||||
re.compile(r"Telegram", re.I),
|
||||
re.compile(r"canonical|public\.\*|DB mutation|database mutation", re.I),
|
||||
),
|
||||
"shared_knowledge_commons": (
|
||||
re.compile(r"shared (?:claim|knowledge|commons)|claims.{0,40}shared", re.I | re.S),
|
||||
re.compile(r"source|evidence", re.I),
|
||||
re.compile(r"do not duplicate|don'?t duplicate|one shared claim|single shared claim", re.I),
|
||||
),
|
||||
"agent_specific_positions": (
|
||||
re.compile(r"agent[- ]specific", re.I),
|
||||
re.compile(r"belief|position|stance|confidence", re.I),
|
||||
re.compile(r"separate|elsewhere|per-agent", re.I),
|
||||
),
|
||||
"forecast_history": (
|
||||
re.compile(r"original (?:probability|confidence)|60%|history", re.I),
|
||||
re.compile(r"preserve|retain|do not overwrite|don'?t overwrite", re.I),
|
||||
re.compile(r"ambiguous|missing.{0,30}criteria|no.{0,30}criteria", re.I | re.S),
|
||||
),
|
||||
"forecast_schema_gap": (
|
||||
re.compile(r"current (?:v1|schema)|public\.claims", re.I),
|
||||
re.compile(
|
||||
r"no.{0,50}(?:forecast[- ]resolution|resolution field|resolved_at)|does not have.{0,50}resolution",
|
||||
re.I | re.S,
|
||||
),
|
||||
re.compile(r"resolves.{0,30}(?:not|isn'?t|does not)|no.{0,30}resolves", re.I | re.S),
|
||||
),
|
||||
"handler_not_telegram": (
|
||||
re.compile(r"no|not", re.I),
|
||||
re.compile(r"handler|GatewayRunner|temporary[- ]profile", re.I),
|
||||
re.compile(r"did not post|posted nothing|not Telegram-visible|does not prove Telegram", re.I),
|
||||
),
|
||||
"staging_without_apply_authorization": (
|
||||
re.compile(r"capture|hash|archive", re.I),
|
||||
re.compile(r"stage|pending_review|proposal", re.I),
|
||||
re.compile(
|
||||
r"does not require.{0,50}(?:apply )?approval|without.{0,50}(?:apply )?approval|approval begins.{0,80}apply",
|
||||
re.I | re.S,
|
||||
),
|
||||
),
|
||||
"real_source_identity": (
|
||||
re.compile(r"URL|storage path|file hash|content hash|retained artifact", re.I),
|
||||
re.compile(r"temporary label|chat label|proposal pointer|source_ref", re.I),
|
||||
re.compile(r"not.{0,40}(?:a )?(?:source|provenance)|must not.{0,40}source|do not manufacture", re.I | re.S),
|
||||
),
|
||||
"valid_supersession": (
|
||||
re.compile(r"new|replacement", re.I),
|
||||
re.compile(r"supersedes", re.I),
|
||||
re.compile(r"superseded_by", re.I),
|
||||
re.compile(r"old claim", re.I),
|
||||
),
|
||||
"current_edge_schema": (
|
||||
re.compile(r"claim_edges", re.I),
|
||||
re.compile(r"from_claim", re.I),
|
||||
re.compile(r"to_claim", re.I),
|
||||
re.compile(r"edge_type", re.I),
|
||||
re.compile(r"no.{0,30}rationale|does not.{0,30}rationale", re.I | re.S),
|
||||
),
|
||||
"apply_capability_boundary": (
|
||||
re.compile(r"approve_claim", re.I),
|
||||
re.compile(r"insert.{0,50}(?:new|replacement) claim|new claim.{0,50}insert", re.I | re.S),
|
||||
re.compile(r"supersedes edge", re.I),
|
||||
re.compile(r"separate.{0,50}reviewed apply|does not update.{0,80}(?:status|superseded_by)", re.I | re.S),
|
||||
),
|
||||
"blocker_definition": (
|
||||
re.compile(r"blocker", re.I),
|
||||
re.compile(r"approved|applied_at", re.I),
|
||||
|
|
@ -206,7 +392,8 @@ COUNT_INVARIANT_REJECTION_RE = re.compile(
|
|||
|
||||
SCHEMA_GAP_QUALIFIER_RE = re.compile(
|
||||
r"\b(?:proposed|future|not current|not shipped|does not exist|doesn't exist|absent|"
|
||||
r"has no|have no|no column|not an edge|would require|schema gap|must be added)\b",
|
||||
r"has no|have no|no column|not an edge|would require|schema gap|must be added|"
|
||||
r"does not support|doesn't support|supports neither|not supported)\b",
|
||||
re.I,
|
||||
)
|
||||
CURRENT_SCHEMA_ASSERTION_PATTERNS: dict[str, re.Pattern[str]] = {
|
||||
|
|
@ -247,6 +434,53 @@ CURRENT_SCHEMA_ASSERTION_PATTERNS: dict[str, re.Pattern[str]] = {
|
|||
}
|
||||
UNVERIFIED_M3TAVERSAL_ALIAS_RE = re.compile(r"\b(?:Cory|m3ta)\b", re.I)
|
||||
|
||||
SOURCE_EVIDENCE_CANONICAL_OBJECT_RE = re.compile(r"claim_evidence|public\.sources|source rows?", re.I)
|
||||
SOURCE_EVIDENCE_LOCATOR_GAP_RE = re.compile(
|
||||
r"(?:missing|without|no).{0,50}(?:url|storage(?:_path)?|locator)", re.I | re.S
|
||||
)
|
||||
SOURCE_EVIDENCE_DENIAL_RE = re.compile(
|
||||
r"(?:not|isn't|is not|doesn't count as|does not count as).{0,40}(?:canonical|grounded) evidence",
|
||||
re.I | re.S,
|
||||
)
|
||||
SOURCE_EVIDENCE_CITATION_STUB_DENIAL_RE = re.compile(
|
||||
r"citation stubs?.{0,50}(?:not|isn't|is not).{0,30}(?:canonical|grounded) evidence",
|
||||
re.I | re.S,
|
||||
)
|
||||
|
||||
BEHAVIORAL_RULES_FALSE_ABSENCE_RE = re.compile(
|
||||
r"(?:public\.)?behavioral_rules(?: table)?.{0,50}(?:does not exist|doesn't exist|is absent|is missing|not shipped)|"
|
||||
r"(?:no|missing) (?:public\.)?behavioral_rules table",
|
||||
re.I | re.S,
|
||||
)
|
||||
|
||||
DB_ONLY_CAUSALITY_RE = re.compile(
|
||||
r"(?:unchanged|same) (?:canonical )?(?:DB|database|counts?).{0,80}"
|
||||
r"(?:proves?|means|therefore).{0,80}(?:behavior|answer).{0,30}(?:unchanged|cannot change)",
|
||||
re.I | re.S,
|
||||
)
|
||||
RESTART_ERASES_ALL_RE = re.compile(
|
||||
r"restart.{0,100}(?:erases?|forgets?|loses?).{0,40}(?:all|every|prior[- ]session)", re.I | re.S
|
||||
)
|
||||
HANDLER_TELEGRAM_OVERCLAIM_RE = re.compile(
|
||||
r"(?:Telegram (?:path|delivery).{0,40}(?:is|was) (?:live|proven)|"
|
||||
r"live Telegram (?:path|delivery)).{0,160}(?:posted nothing|did not post|no Telegram post)",
|
||||
re.I | re.S,
|
||||
)
|
||||
FORECAST_HISTORY_REWRITE_RE = re.compile(
|
||||
r"(?<!not )(?<!don't )(?<!dont )(?:overwrite|update|replace).{0,40}"
|
||||
r"(?:original )?(?:probability|confidence).{0,80}"
|
||||
r"(?:resolved|actual|outcome)",
|
||||
re.I | re.S,
|
||||
)
|
||||
TEMP_LABEL_AS_SOURCE_RE = re.compile(
|
||||
r"(?:use|write|create|store|promote).{0,50}(?:temporary|memory|chat) (?:label|token).{0,100}"
|
||||
r"(?:source_ref|public\.sources|canonical source)",
|
||||
re.I | re.S,
|
||||
)
|
||||
|
||||
DEFAULT_MAX_RESPONSE_WORDS = 300
|
||||
MAX_RESPONSE_WORDS = {"OOS-09": 100, "OOS-10": 180}
|
||||
|
||||
|
||||
def prompt_catalog(memory_token: str) -> list[dict[str, Any]]:
|
||||
return [
|
||||
|
|
@ -279,6 +513,51 @@ def current_schema_overclaims(reply: str) -> list[str]:
|
|||
return sorted(set(findings))
|
||||
|
||||
|
||||
def source_evidence_semantic_issues(reply: str) -> list[str]:
|
||||
"""Reject answers that confuse canonical linkage with locator quality."""
|
||||
|
||||
findings: set[str] = set()
|
||||
for segment in re.split(r"(?<=[.!?])\s+|\n+", reply):
|
||||
if (
|
||||
SOURCE_EVIDENCE_CANONICAL_OBJECT_RE.search(segment)
|
||||
and SOURCE_EVIDENCE_LOCATOR_GAP_RE.search(segment)
|
||||
and SOURCE_EVIDENCE_DENIAL_RE.search(segment)
|
||||
):
|
||||
findings.add("locator_gap_called_noncanonical")
|
||||
if SOURCE_EVIDENCE_CITATION_STUB_DENIAL_RE.search(reply):
|
||||
findings.add("citation_stub_called_ungrounded")
|
||||
return sorted(findings)
|
||||
|
||||
|
||||
def behavioral_rule_schema_issues(reply: str) -> list[str]:
|
||||
"""Reject claims that the current behavioral-rules table is absent."""
|
||||
|
||||
return ["behavioral_rules_false_absence"] if BEHAVIORAL_RULES_FALSE_ABSENCE_RE.search(reply) else []
|
||||
|
||||
|
||||
def broad_semantic_issues(reply: str) -> list[str]:
|
||||
"""Reject high-impact false claims that can hide inside otherwise complete prose."""
|
||||
|
||||
findings: set[str] = set()
|
||||
if DB_ONLY_CAUSALITY_RE.search(reply) and not re.search(
|
||||
r"does not prove|doesn't prove|do not prove|does not mean|doesn't mean|do not mean|cannot prove", reply, re.I
|
||||
):
|
||||
findings.add("unchanged_db_called_complete_behavior_proof")
|
||||
if RESTART_ERASES_ALL_RE.search(reply) and not re.search(
|
||||
r"restart.{0,100}(?:does not|doesn't|need not|not necessarily).{0,60}(?:erase|forget|lose)",
|
||||
reply,
|
||||
re.I | re.S,
|
||||
):
|
||||
findings.add("restart_called_total_memory_erasure")
|
||||
if HANDLER_TELEGRAM_OVERCLAIM_RE.search(reply):
|
||||
findings.add("handler_proof_called_telegram_live")
|
||||
if FORECAST_HISTORY_REWRITE_RE.search(reply):
|
||||
findings.add("forecast_history_rewrite")
|
||||
if TEMP_LABEL_AS_SOURCE_RE.search(reply):
|
||||
findings.add("temporary_label_promoted_to_source")
|
||||
return sorted(findings)
|
||||
|
||||
|
||||
def score_reply(prompt: dict[str, Any], reply: str, *, memory_token: str) -> dict[str, Any]:
|
||||
legacy_score = base.score_reply(prompt, reply)
|
||||
concepts = {concept: matched_concept(reply, concept) for concept in prompt["required_concepts"]}
|
||||
|
|
@ -307,6 +586,12 @@ def score_reply(prompt: dict[str, Any], reply: str, *, memory_token: str) -> dic
|
|||
)
|
||||
invalid_count_invariant = asserts_invalid_count_invariant(reply)
|
||||
schema_overclaims = current_schema_overclaims(reply)
|
||||
source_evidence_issues = source_evidence_semantic_issues(reply) if prompt["id"] == "OOS-05" else []
|
||||
behavioral_rule_issues = behavioral_rule_schema_issues(reply) if prompt["id"] == "OOS-06" else []
|
||||
semantic_issues = broad_semantic_issues(reply)
|
||||
word_count = len(re.findall(r"\b\w+(?:[-']\w+)*\b", reply))
|
||||
max_response_words = MAX_RESPONSE_WORDS.get(prompt["id"], DEFAULT_MAX_RESPONSE_WORDS)
|
||||
response_too_long = word_count > max_response_words
|
||||
return {
|
||||
"prompt_id": prompt["id"],
|
||||
"dimension": prompt["dimension"],
|
||||
|
|
@ -318,12 +603,22 @@ def score_reply(prompt: dict[str, Any], reply: str, *, memory_token: str) -> dic
|
|||
"response_issue_detected": legacy_score["response_issue_detected"],
|
||||
"invalid_count_invariant_detected": invalid_count_invariant,
|
||||
"current_schema_overclaims": schema_overclaims,
|
||||
"source_evidence_semantic_issues": source_evidence_issues,
|
||||
"behavioral_rule_schema_issues": behavioral_rule_issues,
|
||||
"broad_semantic_issues": semantic_issues,
|
||||
"word_count": word_count,
|
||||
"max_response_words": max_response_words,
|
||||
"response_too_long": response_too_long,
|
||||
"pass": bool(
|
||||
all(concepts.values())
|
||||
and all(custom_signals.values())
|
||||
and not legacy_score["overclaim_detected"]
|
||||
and not invalid_count_invariant
|
||||
and not schema_overclaims
|
||||
and not source_evidence_issues
|
||||
and not behavioral_rule_issues
|
||||
and not semantic_issues
|
||||
and not response_too_long
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ def test_vps_kb_skill_keeps_vps_scope_explicit() -> None:
|
|||
assert "new or updated row IDs" in text
|
||||
assert "I cannot claim canonical DB changed" in text
|
||||
assert "explicit operator/admin authorization" in text
|
||||
assert "Do not call an approved proposal \"implemented\"" in text
|
||||
assert 'Do not call an approved proposal "implemented"' in text
|
||||
assert "repository contains a live-proven strict existing-ID `add_edge` path" in squashed
|
||||
assert "Never end a normal Telegram answer by offering to run direct `INSERT`, `UPDATE`" in text
|
||||
assert "Next admin-panel action" in text
|
||||
|
|
@ -152,6 +152,48 @@ def test_leoclean_kb_skills_anchor_external_doctrine_in_target_project_language(
|
|||
assert "Consent is action-specific" in text
|
||||
|
||||
|
||||
def test_leoclean_kb_skills_distinguish_canonical_evidence_from_provenance_quality() -> None:
|
||||
for surface in ("vps", "gcp"):
|
||||
text = (SKILL_ROOT / surface / "teleo-kb-bridge" / "SKILL.md").read_text()
|
||||
squashed = " ".join(text.split())
|
||||
|
||||
assert "is canonical evidence even when that source row has no `url` or `storage_path`" in squashed
|
||||
assert "weak or citation-only provenance" in squashed
|
||||
assert "not traceable to the raw artifact" in squashed
|
||||
assert "does not by itself prove canonical evidence from that attachment" in squashed
|
||||
assert "`public.sources` row representing the attachment" in squashed
|
||||
assert "`public.claim_evidence` link from the claim to that source row" in squashed
|
||||
|
||||
|
||||
def test_leoclean_kb_skills_use_existing_behavioral_rule_storage_and_separate_apply() -> None:
|
||||
for surface in ("vps", "gcp"):
|
||||
text = (SKILL_ROOT / surface / "teleo-kb-bridge" / "SKILL.md").read_text()
|
||||
squashed = " ".join(text.split())
|
||||
|
||||
assert "existing `public.behavioral_rules` table" in squashed
|
||||
for field in ("`agent_id`", "`category`", "`rank`", "`rule`", "`rationale`"):
|
||||
assert field in text
|
||||
assert "does not insert `behavioral_rules` or `governance_gates`" in squashed
|
||||
assert "separate reviewed apply capability" in squashed
|
||||
assert "not a generic schema-table gap" in squashed
|
||||
|
||||
|
||||
def test_leoclean_kb_skills_separate_runtime_session_and_delivery_proof_tiers() -> None:
|
||||
for surface in ("vps", "gcp"):
|
||||
text = (SKILL_ROOT / surface / "teleo-kb-bridge" / "SKILL.md").read_text()
|
||||
squashed = " ".join(text.split())
|
||||
|
||||
assert "keep the whole reply under 220 words" in squashed
|
||||
assert "Postgres is canonical knowledge, but it is not the only input" in squashed
|
||||
assert "Hermes `state.db` and session JSONL provide durable continuity" in squashed
|
||||
assert "not Telegram-visible delivery proof" in squashed
|
||||
assert "Staging is not canonical apply" in squashed
|
||||
assert "Never manufacture a source row from a temporary memory label" in squashed
|
||||
assert "claims, sources, and evidence links are shared knowledge objects" in squashed
|
||||
assert "no shipped forecast-resolution fields" in squashed
|
||||
assert "Do not overwrite historical confidence" in squashed
|
||||
|
||||
|
||||
def test_vps_live_telegram_skill_uses_systemd_for_gateway_liveness() -> None:
|
||||
text = (SKILL_ROOT / "vps" / "live-leo-telegram" / "SKILL.md").read_text()
|
||||
|
||||
|
|
|
|||
382
tests/test_run_local_canonical_postgres_rebuild.py
Normal file
382
tests/test_run_local_canonical_postgres_rebuild.py
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
import argparse
|
||||
import json
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from ops import run_local_canonical_postgres_rebuild as rebuild
|
||||
|
||||
COUNTS = {
|
||||
"public.claims": 1837,
|
||||
"public.sources": 4145,
|
||||
"public.claim_evidence": 4670,
|
||||
"public.claim_edges": 4916,
|
||||
"public.reasoning_tools": 1,
|
||||
"kb_stage.kb_proposals": 26,
|
||||
}
|
||||
|
||||
|
||||
def completed(command: list[str], returncode: int = 0, stdout: str = "", stderr: str = ""):
|
||||
return subprocess.CompletedProcess(command, returncode, stdout, stderr)
|
||||
|
||||
|
||||
class FakeDocker:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
restore_fails: bool = False,
|
||||
preserve_on_remove: bool = False,
|
||||
manifest_text: str = "",
|
||||
) -> None:
|
||||
self.restore_fails = restore_fails
|
||||
self.preserve_on_remove = preserve_on_remove
|
||||
self.manifest_text = manifest_text
|
||||
self.commands: list[list[str]] = []
|
||||
self.running = False
|
||||
self.container_name = ""
|
||||
self.image = ""
|
||||
self.label = ""
|
||||
self.tmpfs = ""
|
||||
|
||||
def __call__(self, command: list[str], *, timeout: float):
|
||||
assert timeout > 0
|
||||
self.commands.append(command)
|
||||
|
||||
if command[1:3] == ["container", "inspect"]:
|
||||
if not self.running:
|
||||
return completed(command, 1, stderr=f"Error: No such object: {command[-1]}")
|
||||
payload = [
|
||||
{
|
||||
"Config": {
|
||||
"Image": self.image,
|
||||
"Labels": {rebuild.CANARY_LABEL: self.label},
|
||||
},
|
||||
"HostConfig": {
|
||||
"NetworkMode": "none",
|
||||
"Tmpfs": {rebuild.DATA_DIR: self.tmpfs.split(":", 1)[1]},
|
||||
},
|
||||
"State": {"Running": True},
|
||||
}
|
||||
]
|
||||
return completed(command, stdout=json.dumps(payload))
|
||||
|
||||
if command[1] == "run":
|
||||
self.running = True
|
||||
self.container_name = command[command.index("--name") + 1]
|
||||
self.label = command[command.index("--label") + 1].split("=", 1)[1]
|
||||
self.tmpfs = command[command.index("--tmpfs") + 1]
|
||||
self.image = command[-1]
|
||||
return completed(command, stdout="0123456789abcdef\n")
|
||||
|
||||
if command[1] == "cp":
|
||||
return completed(command)
|
||||
|
||||
if command[1] == "exec" and command[3] == "pg_restore":
|
||||
if self.restore_fails:
|
||||
return completed(command, 1, stderr="pg_restore: error: fixture restore failure")
|
||||
return completed(command)
|
||||
|
||||
if command[1] == "exec" and command[3] == "psql":
|
||||
if "-f" in command:
|
||||
return completed(command, stdout=self.manifest_text)
|
||||
sql = command[command.index("-c") + 1]
|
||||
if sql == "select current_database();":
|
||||
return completed(command, stdout="teleo\n")
|
||||
if sql.startswith("CREATE ROLE"):
|
||||
return completed(command)
|
||||
if sql.startswith("select to_regclass("):
|
||||
return completed(command, stdout="t\n")
|
||||
if "from pg_tables" in sql:
|
||||
return completed(command, stdout="47\n")
|
||||
for table, count in COUNTS.items():
|
||||
schema, name = table.split(".", 1)
|
||||
if f'from "{schema}"."{name}"' in sql:
|
||||
return completed(command, stdout=f"{count}\n")
|
||||
raise AssertionError(f"unexpected psql query: {sql}")
|
||||
|
||||
if command[1:3] == ["rm", "--force"]:
|
||||
if not self.preserve_on_remove:
|
||||
self.running = False
|
||||
return completed(command, stdout=f"{command[-1]}\n")
|
||||
return completed(command, 1, stderr="fixture refused removal")
|
||||
|
||||
raise AssertionError(f"unexpected command: {command}")
|
||||
|
||||
|
||||
def write_dump(path: Path, payload: bytes = b"fixture") -> Path:
|
||||
path.write_bytes(b"PGDMP" + payload)
|
||||
return path
|
||||
|
||||
|
||||
def manifest_rows() -> list[dict]:
|
||||
role = {
|
||||
"name": "kb_apply",
|
||||
"can_login": True,
|
||||
"superuser": False,
|
||||
"create_db": False,
|
||||
"create_role": False,
|
||||
"inherit": True,
|
||||
"replication": False,
|
||||
"bypass_rls": False,
|
||||
}
|
||||
rows = [
|
||||
{
|
||||
"kind": "identity",
|
||||
"database": "teleo",
|
||||
"server_version_num": 160010,
|
||||
"transaction_read_only": "on",
|
||||
"server_address": None,
|
||||
"server_port": None,
|
||||
"ssl": False,
|
||||
"ssl_version": None,
|
||||
"database_collation": "C",
|
||||
"database_ctype": "C",
|
||||
},
|
||||
{"kind": "schemas", "items": ["kb_stage", "public"]},
|
||||
{"kind": "extensions", "items": []},
|
||||
{"kind": "application_roles", "items": [role]},
|
||||
]
|
||||
rows.extend(
|
||||
{"kind": kind, "items": []}
|
||||
for kind in (
|
||||
"columns",
|
||||
"constraints",
|
||||
"indexes",
|
||||
"sequences",
|
||||
"views",
|
||||
"functions",
|
||||
"triggers",
|
||||
"types",
|
||||
"policies",
|
||||
)
|
||||
)
|
||||
rows.extend(
|
||||
[
|
||||
{
|
||||
"kind": "table",
|
||||
"schema": "public",
|
||||
"table": "claims",
|
||||
"row_count": 1837,
|
||||
"rowset_md5": "same-row-hash",
|
||||
},
|
||||
{
|
||||
"kind": "performance",
|
||||
"query": "count_claims",
|
||||
"elapsed_ms": 1.0,
|
||||
"observed_rows": 1837,
|
||||
},
|
||||
]
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def write_manifest(path: Path) -> Path:
|
||||
path.write_text("".join(json.dumps(row) + "\n" for row in manifest_rows()), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def args_for(tmp_path: Path, **overrides) -> argparse.Namespace:
|
||||
values = {
|
||||
"dump": write_dump(tmp_path / "canonical.dump"),
|
||||
"database": "teleo",
|
||||
"image": rebuild.DEFAULT_IMAGE,
|
||||
"container_prefix": "teleo-canonical-rebuild-test",
|
||||
"tmpfs_mb": 256,
|
||||
"source_manifest": None,
|
||||
"manifest_sql": Path("ops/postgres_parity_manifest.sql").resolve(),
|
||||
"max_target_query_ms": 2000.0,
|
||||
"max_target_source_ratio": 20.0,
|
||||
"startup_timeout": 2.0,
|
||||
"restore_timeout": 5.0,
|
||||
"manifest_timeout": 5.0,
|
||||
"command_timeout": 2.0,
|
||||
"poll_interval": 0.001,
|
||||
"docker_bin": "docker",
|
||||
"output": tmp_path / "receipt.json",
|
||||
}
|
||||
values.update(overrides)
|
||||
return argparse.Namespace(**values)
|
||||
|
||||
|
||||
def test_success_uses_networkless_tmpfs_named_database_and_required_restore_flags(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake = FakeDocker()
|
||||
monkeypatch.setattr(rebuild, "_run", fake)
|
||||
|
||||
result = rebuild.run_canary(args_for(tmp_path))
|
||||
|
||||
assert result["status"] == "pass"
|
||||
assert result["container"]["name"].startswith("teleo-canonical-rebuild-test-")
|
||||
assert result["container"]["named_database_psql_ready"] is True
|
||||
assert result["container"]["isolation"]["network_mode"] == "none"
|
||||
assert result["key_counts"] == COUNTS
|
||||
assert result["canonical_schema_table_count"] == 47
|
||||
assert result["cleanup"]["status"] == "pass"
|
||||
assert result["cleanup"]["container_absent"] is True
|
||||
|
||||
run_command = next(command for command in fake.commands if command[1] == "run")
|
||||
assert run_command[run_command.index("--network") + 1] == "none"
|
||||
assert run_command[run_command.index("--tmpfs") + 1] == (f"{rebuild.DATA_DIR}:rw,nosuid,nodev,noexec,size=256m")
|
||||
assert "--rm" in run_command
|
||||
|
||||
readiness_index = next(
|
||||
index for index, command in enumerate(fake.commands) if "select current_database();" in command
|
||||
)
|
||||
restore_index = next(
|
||||
index for index, command in enumerate(fake.commands) if len(command) > 3 and command[3] == "pg_restore"
|
||||
)
|
||||
assert readiness_index < restore_index
|
||||
readiness_command = fake.commands[readiness_index]
|
||||
assert readiness_command[readiness_command.index("-d") + 1] == "teleo"
|
||||
assert not any("pg_isready" in part for command in fake.commands for part in command)
|
||||
|
||||
restore_command = fake.commands[restore_index]
|
||||
assert "--no-owner" in restore_command
|
||||
assert "--no-privileges" in restore_command
|
||||
assert "--exit-on-error" in restore_command
|
||||
|
||||
|
||||
def test_optional_manifest_bootstraps_allowlisted_roles_and_uses_existing_verifier(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
source_manifest = write_manifest(tmp_path / "source-manifest.jsonl")
|
||||
fake = FakeDocker(manifest_text=source_manifest.read_text())
|
||||
monkeypatch.setattr(rebuild, "_run", fake)
|
||||
|
||||
result = rebuild.run_canary(args_for(tmp_path, source_manifest=source_manifest))
|
||||
|
||||
assert result["status"] == "pass"
|
||||
assert result["application_roles_bootstrapped"] == ["kb_apply"]
|
||||
assert result["parity"]["status"] == "pass"
|
||||
assert result["parity"]["verifier"] == "ops.verify_postgres_parity_manifest.compare_manifests"
|
||||
assert result["parity"]["problems"] == []
|
||||
assert result["parity"]["details"]["source_total_rows"] == 1837
|
||||
assert result["parity"]["target_manifest"]["line_count"] == len(manifest_rows())
|
||||
|
||||
role_command = next(
|
||||
command
|
||||
for command in fake.commands
|
||||
if command[1] == "exec" and command[3] == "psql" and any("CREATE ROLE" in part for part in command)
|
||||
)
|
||||
role_sql = role_command[role_command.index("-c") + 1]
|
||||
assert 'CREATE ROLE "kb_apply" LOGIN NOSUPERUSER' in role_sql
|
||||
assert "PASSWORD" not in role_sql
|
||||
assert any(command[1] == "cp" and rebuild.MANIFEST_DESTINATION in command[-1] for command in fake.commands)
|
||||
|
||||
|
||||
def test_restore_failure_still_forces_removal_and_proves_absence(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake = FakeDocker(restore_fails=True)
|
||||
monkeypatch.setattr(rebuild, "_run", fake)
|
||||
|
||||
result = rebuild.run_canary(args_for(tmp_path))
|
||||
|
||||
assert result["status"] == "fail"
|
||||
assert result["error"]["phase"] == "restore"
|
||||
assert "fixture restore failure" in result["error"]["message"]
|
||||
assert result["cleanup"]["force_remove_exit_code"] == 0
|
||||
assert result["cleanup"]["container_absent"] is True
|
||||
assert result["cleanup"]["status"] == "pass"
|
||||
assert any(command[1:3] == ["rm", "--force"] for command in fake.commands)
|
||||
assert fake.running is False
|
||||
|
||||
|
||||
def test_unproven_cleanup_overrides_success_and_fails_closed(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake = FakeDocker(preserve_on_remove=True)
|
||||
monkeypatch.setattr(rebuild, "_run", fake)
|
||||
monkeypatch.setattr(rebuild.time, "sleep", lambda _seconds: None)
|
||||
|
||||
result = rebuild.run_canary(args_for(tmp_path))
|
||||
|
||||
assert result["status"] == "fail"
|
||||
assert result["error"]["phase"] == "cleanup"
|
||||
assert result["error"]["type"] == "CleanupProofError"
|
||||
assert result["cleanup"]["container_absent"] is False
|
||||
assert result["cleanup"]["status"] == "fail"
|
||||
assert result["cleanup"]["absence_inspection_attempts"] == 5
|
||||
|
||||
|
||||
def test_cleanup_interruption_is_receipted_and_does_not_bypass_absence_proof(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
fake = FakeDocker()
|
||||
interrupted = False
|
||||
|
||||
def interrupt_cleanup(command: list[str], *, timeout: float):
|
||||
nonlocal interrupted
|
||||
if command[1:3] == ["rm", "--force"] and not interrupted:
|
||||
interrupted = True
|
||||
fake.running = False
|
||||
raise rebuild.TerminationRequested("received SIGTERM")
|
||||
return fake(command, timeout=timeout)
|
||||
|
||||
monkeypatch.setattr(rebuild, "_run", interrupt_cleanup)
|
||||
|
||||
result = rebuild.run_canary(args_for(tmp_path))
|
||||
|
||||
assert result["status"] == "fail"
|
||||
assert result["error"] == {
|
||||
"phase": "cleanup",
|
||||
"type": "TerminationRequested",
|
||||
"message": "received SIGTERM",
|
||||
}
|
||||
assert result["cleanup"]["container_absent"] is True
|
||||
assert result["cleanup"]["status"] == "pass"
|
||||
|
||||
|
||||
def test_invalid_dump_fails_before_start_but_still_proves_name_absent(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
invalid = tmp_path / "plain.sql"
|
||||
invalid.write_text("select 1;")
|
||||
fake = FakeDocker()
|
||||
monkeypatch.setattr(rebuild, "_run", fake)
|
||||
|
||||
result = rebuild.run_canary(args_for(tmp_path, dump=invalid))
|
||||
|
||||
assert result["status"] == "fail"
|
||||
assert result["error"]["phase"] == "validation"
|
||||
assert "PGDMP" in result["error"]["message"]
|
||||
assert not any(command[1] == "run" for command in fake.commands)
|
||||
assert result["cleanup"]["container_absent"] is True
|
||||
|
||||
|
||||
def test_application_role_sql_rejects_roles_outside_exact_allowlist() -> None:
|
||||
source = {"singleton": {"application_roles": {"items": [{"name": "postgres"}]}}}
|
||||
|
||||
with pytest.raises(ValueError, match="non-allowlisted"):
|
||||
rebuild.application_role_sql(source)
|
||||
|
||||
|
||||
def test_container_names_are_unique_and_bounded() -> None:
|
||||
first = rebuild.new_container_name("teleo-rebuild")
|
||||
second = rebuild.new_container_name("teleo-rebuild")
|
||||
|
||||
assert first != second
|
||||
assert first.startswith("teleo-rebuild-")
|
||||
assert len(first) <= 63
|
||||
|
||||
|
||||
def test_receipt_write_is_atomic_private_and_replaces_symlink_not_target(tmp_path: Path) -> None:
|
||||
target = tmp_path / "unrelated.json"
|
||||
target.write_text("keep")
|
||||
receipt = tmp_path / "receipt.json"
|
||||
receipt.symlink_to(target)
|
||||
|
||||
rebuild.write_receipt(receipt, {"status": "pass"})
|
||||
|
||||
assert json.loads(receipt.read_text()) == {"status": "pass"}
|
||||
assert target.read_text() == "keep"
|
||||
assert stat.S_IMODE(receipt.stat().st_mode) == 0o600
|
||||
|
|
@ -35,14 +35,20 @@ def good_reply(prompt_id: str, token: str) -> str:
|
|||
)
|
||||
if prompt_id == "OOS-05":
|
||||
return common + (
|
||||
"A Telegram attachment and proposal source_ref are staging pointers, not canonical evidence. Audit the "
|
||||
"document_evaluation, source_ref, public.sources row, claim_evidence join, and guarded apply receipt."
|
||||
"A Telegram attachment or proposal source_ref alone does not prove canonical evidence from that "
|
||||
"attachment. Canonical evidence exists when public.claim_evidence links the claim to a public.sources "
|
||||
"row. A missing URL or storage_path leaves weak, citation-only provenance that is not traceable to the "
|
||||
"raw artifact; it does not undo the canonical link. Audit the document_evaluation, source_ref, source "
|
||||
"row, claim_evidence join, and guarded apply receipt."
|
||||
)
|
||||
if prompt_id == "OOS-06":
|
||||
return common + (
|
||||
"Keep observations as claims with sources and evidence, link edges, place a framework in a reasoning tool "
|
||||
"or concept map, retain the disputed interpretation as a caveat, stage governance separately, and record "
|
||||
"the correction as a superseding relationship before review and apply."
|
||||
"Keep factual observations as empirical claims with sources and evidence, link contradictory claims, "
|
||||
"place the framework in a reasoning tool, and retain the disputed interpretation as a caveat. Store the "
|
||||
"behavioral rule in public.behavioral_rules using agent_id, category, rank, rule, and rationale; reserve "
|
||||
"public.governance_gates for the evaluative gate. Current approve_claim supports neither "
|
||||
"behavioral_rules nor governance_gates, so each needs a separate reviewed apply capability. Record the "
|
||||
"correction as a superseding relationship before review and apply."
|
||||
)
|
||||
if prompt_id == "OOS-07":
|
||||
return common + (
|
||||
|
|
@ -61,21 +67,67 @@ def good_reply(prompt_id: str, token: str) -> str:
|
|||
"Never transfer or mix one participant's identity into another user's reply.\n"
|
||||
"I cannot claim another identity source without explicit proof."
|
||||
)
|
||||
if prompt_id == "OOS-10":
|
||||
return (
|
||||
"Postgres is the canonical database, but an unchanged count receipt does not prove answer behavior is "
|
||||
"unchanged. "
|
||||
"Deployed skills, runtime configuration, rendered SOUL.md, session state, and conversation context also "
|
||||
"affect replies. Hermes state.db and session JSONL provide durable continuity, so a restart does not "
|
||||
"necessarily erase every prior-session fact. Proof tiers are separate: a temporary-profile handler run, "
|
||||
"Telegram-visible delivery, and canonical public.* database mutation with a row receipt."
|
||||
)
|
||||
if prompt_id == "OOS-11":
|
||||
return common + (
|
||||
"Use one shared claim with shared sources and evidence; do not duplicate the factual claim per agent. "
|
||||
"Store agent-specific beliefs, positions, stances, or confidence separately. Preserve each rationale and "
|
||||
"retain disagreement as a caveat or contradiction edge so it remains queryable."
|
||||
)
|
||||
if prompt_id == "OOS-12":
|
||||
return common + (
|
||||
"Preserve the original 60% probability and history; do not overwrite it. The missing criteria make the "
|
||||
"outcome ambiguous, so retain that caveat. Current v1 public.claims has no forecast-resolution field and "
|
||||
"there is no resolves edge. Stage a separate schema proposal, then review and apply it before using a new "
|
||||
"resolution mechanism."
|
||||
)
|
||||
if prompt_id == "OOS-13":
|
||||
return (
|
||||
"No. The temporary-profile GatewayRunner handler run posted nothing to Telegram, so it is not "
|
||||
"Telegram-visible delivery proof. It proves handler execution and reply behavior only. Canonical database "
|
||||
"mutation is a third tier requiring public.* row and applied proposal receipts. The next action is one "
|
||||
"authorized Telegram-visible prompt with message and reply readback while confirming no DB count change."
|
||||
)
|
||||
if prompt_id == "OOS-14":
|
||||
return common + (
|
||||
"Leo may capture the retained artifact, URL or storage path, and file/content hash; extract candidates, "
|
||||
"deduplicate them, record contradictions, and stage a pending_review proposal without canonical-apply "
|
||||
"approval. A temporary chat label or source_ref is not real source provenance. Explicit operator "
|
||||
"authorization begins at the guarded canonical apply path, after review."
|
||||
)
|
||||
if prompt_id == "OOS-15":
|
||||
return common + (
|
||||
"Insert the replacement claim and a supersedes edge from new claim to old claim. Current public.claim_edges "
|
||||
"has from_claim, to_claim, edge_type, weight, created_by, and created_at; it has no rationale field. The "
|
||||
"old claim has a superseded_by column. approve_claim can insert the new claim and supersedes edge, but it "
|
||||
"does not update the old claim status or superseded_by; those require a separate reviewed apply capability."
|
||||
)
|
||||
return common + "Fresh readback is required before the demo claim changes."
|
||||
|
||||
|
||||
def test_oos_catalog_is_broad_and_uses_randomized_memory_token() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompts = benchmark.prompt_catalog(token)
|
||||
assert [prompt["id"] for prompt in prompts] == [f"OOS-{index:02d}" for index in range(1, 10)]
|
||||
assert token in prompts[-3]["message"]
|
||||
assert token not in prompts[-2]["message"]
|
||||
assert [prompt["id"] for prompt in prompts] == [f"OOS-{index:02d}" for index in range(1, 16)]
|
||||
by_id = {prompt["id"]: prompt for prompt in prompts}
|
||||
assert token in by_id["OOS-07"]["message"]
|
||||
assert token not in by_id["OOS-08"]["message"]
|
||||
joined = "\n".join(prompt["message"] for prompt in prompts)
|
||||
assert "PDF" in joined
|
||||
assert "tweets" in joined
|
||||
assert "SOUL.md" in joined
|
||||
assert "Do not ask me for row IDs" in joined
|
||||
assert "@m3taversal" in joined
|
||||
assert "temporary-profile GatewayRunner" in joined
|
||||
assert "old claim" in joined
|
||||
|
||||
|
||||
def test_oos_score_passes_complete_behavior_and_memory_pair() -> None:
|
||||
|
|
@ -86,7 +138,11 @@ def test_oos_score_passes_complete_behavior_and_memory_pair() -> None:
|
|||
]
|
||||
score = benchmark.score_results(results, memory_token=token)
|
||||
assert score["pass"] is True
|
||||
assert score["passes"] == 9
|
||||
assert score["passes"] == 15
|
||||
|
||||
|
||||
def test_oos_live_check_accepts_live_readback_wording() -> None:
|
||||
assert benchmark.matched_concept("Here is the live readback from Postgres.", "live_check") is True
|
||||
|
||||
|
||||
def test_oos_score_fails_when_memory_token_is_not_recalled() -> None:
|
||||
|
|
@ -95,7 +151,8 @@ def test_oos_score_fails_when_memory_token_is_not_recalled() -> None:
|
|||
{"prompt_id": prompt["id"], "reply": good_reply(prompt["id"], token)}
|
||||
for prompt in benchmark.prompt_catalog(token)
|
||||
]
|
||||
results[-2]["reply"] = results[-2]["reply"].replace(token, "some-other-label")
|
||||
recall = next(result for result in results if result["prompt_id"] == "OOS-08")
|
||||
recall["reply"] = recall["reply"].replace(token, "some-other-label")
|
||||
score = benchmark.score_results(results, memory_token=token)
|
||||
assert score["pass"] is False
|
||||
assert score["failures"][0]["prompt_id"] == "OOS-08"
|
||||
|
|
@ -104,7 +161,7 @@ def test_oos_score_fails_when_memory_token_is_not_recalled() -> None:
|
|||
|
||||
def test_oos_identity_case_requires_exact_visible_handle_and_no_alias() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[-1]
|
||||
prompt = benchmark.prompt_catalog(token)[8]
|
||||
good = benchmark.score_reply(prompt, good_reply(prompt["id"], token), memory_token=token)
|
||||
assert good["pass"] is True
|
||||
|
||||
|
|
@ -119,7 +176,7 @@ def test_oos_identity_case_requires_exact_visible_handle_and_no_alias() -> None:
|
|||
|
||||
def test_oos_identity_case_accepts_inflected_carry_language() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[-1]
|
||||
prompt = benchmark.prompt_catalog(token)[8]
|
||||
reply = (
|
||||
"Call the current visible participant m3taversal, exactly.\n"
|
||||
"Use only the current Telegram update and visible sender.\n"
|
||||
|
|
@ -175,6 +232,122 @@ def test_oos_schema_guard_rejects_unreviewed_claim_taxonomy_and_apply_surface()
|
|||
assert benchmark.current_schema_overclaims(unsupported_apply) == ["unsupported_approve_claim_surface"]
|
||||
|
||||
|
||||
def test_oos_schema_guard_allows_correct_approve_claim_capability_boundary() -> None:
|
||||
reply = (
|
||||
"Current approve_claim supports neither behavioral_rules nor governance_gates; both need a separate "
|
||||
"reviewed apply capability."
|
||||
)
|
||||
assert benchmark.current_schema_overclaims(reply) == []
|
||||
|
||||
|
||||
def test_oos_source_evidence_case_accepts_canonical_link_and_weak_provenance_distinction() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[4]
|
||||
score = benchmark.score_reply(prompt, good_reply(prompt["id"], token), memory_token=token)
|
||||
assert score["pass"] is True
|
||||
assert score["source_evidence_semantic_issues"] == []
|
||||
|
||||
|
||||
def test_oos_source_evidence_case_rejects_locator_gap_called_noncanonical() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[4]
|
||||
reply = good_reply(prompt["id"], token) + (
|
||||
" A public.claim_evidence link to a source row with no URL or storage locator is not canonical evidence."
|
||||
)
|
||||
score = benchmark.score_reply(prompt, reply, memory_token=token)
|
||||
assert score["pass"] is False
|
||||
assert score["source_evidence_semantic_issues"] == ["locator_gap_called_noncanonical"]
|
||||
|
||||
|
||||
def test_oos_source_evidence_conflation_guard_is_order_independent() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[4]
|
||||
reply = good_reply(prompt["id"], token) + (
|
||||
" A source row with no locator is not canonical evidence even when public.claim_evidence links it."
|
||||
)
|
||||
score = benchmark.score_reply(prompt, reply, memory_token=token)
|
||||
assert score["pass"] is False
|
||||
assert score["source_evidence_semantic_issues"] == ["locator_gap_called_noncanonical"]
|
||||
|
||||
|
||||
def test_oos_source_evidence_case_rejects_citation_stub_conflation() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[4]
|
||||
reply = good_reply(prompt["id"], token) + " These source rows are citation stubs, not grounded evidence."
|
||||
score = benchmark.score_reply(prompt, reply, memory_token=token)
|
||||
assert score["pass"] is False
|
||||
assert score["source_evidence_semantic_issues"] == ["citation_stub_called_ungrounded"]
|
||||
|
||||
|
||||
def test_oos_database_composition_requires_existing_behavioral_rule_table() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[5]
|
||||
good = benchmark.score_reply(prompt, good_reply(prompt["id"], token), memory_token=token)
|
||||
assert good["pass"] is True
|
||||
|
||||
bad_reply = good_reply(prompt["id"], token) + " The public.behavioral_rules table is absent."
|
||||
bad = benchmark.score_reply(prompt, bad_reply, memory_token=token)
|
||||
assert bad["pass"] is False
|
||||
assert bad["behavioral_rule_schema_issues"] == ["behavioral_rules_false_absence"]
|
||||
|
||||
|
||||
def test_oos_runtime_case_rejects_db_only_causality_and_total_memory_erasure() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[9]
|
||||
good = benchmark.score_reply(prompt, good_reply(prompt["id"], token), memory_token=token)
|
||||
assert good["pass"] is True
|
||||
|
||||
bad_reply = (
|
||||
"Postgres is canonical. The unchanged database proves answer behavior is unchanged. "
|
||||
"A restart erases every prior-session fact."
|
||||
)
|
||||
bad = benchmark.score_reply(prompt, bad_reply, memory_token=token)
|
||||
assert bad["pass"] is False
|
||||
assert bad["broad_semantic_issues"] == [
|
||||
"restart_called_total_memory_erasure",
|
||||
"unchanged_db_called_complete_behavior_proof",
|
||||
]
|
||||
|
||||
|
||||
def test_oos_handler_case_rejects_telegram_live_overclaim() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[12]
|
||||
reply = good_reply(prompt["id"], token) + (
|
||||
" The Telegram path is live and proven even though the run posted nothing to Telegram."
|
||||
)
|
||||
score = benchmark.score_reply(prompt, reply, memory_token=token)
|
||||
assert score["pass"] is False
|
||||
assert score["broad_semantic_issues"] == ["handler_proof_called_telegram_live"]
|
||||
|
||||
|
||||
def test_oos_forecast_case_rejects_historical_probability_overwrite() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[11]
|
||||
reply = good_reply(prompt["id"], token) + " Overwrite the original probability with the actual resolved outcome."
|
||||
score = benchmark.score_reply(prompt, reply, memory_token=token)
|
||||
assert score["pass"] is False
|
||||
assert score["broad_semantic_issues"] == ["forecast_history_rewrite"]
|
||||
|
||||
|
||||
def test_oos_source_intake_case_rejects_temporary_label_as_source() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[13]
|
||||
reply = good_reply(prompt["id"], token) + " Create the temporary chat label as a canonical source_ref."
|
||||
score = benchmark.score_reply(prompt, reply, memory_token=token)
|
||||
assert score["pass"] is False
|
||||
assert score["broad_semantic_issues"] == ["temporary_label_promoted_to_source"]
|
||||
|
||||
|
||||
def test_oos_score_rejects_architecture_lecture_length() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[0]
|
||||
reply = good_reply(prompt["id"], token) + " detail" * 301
|
||||
score = benchmark.score_reply(prompt, reply, memory_token=token)
|
||||
assert score["pass"] is False
|
||||
assert score["response_too_long"] is True
|
||||
assert score["max_response_words"] == benchmark.DEFAULT_MAX_RESPONSE_WORDS
|
||||
|
||||
|
||||
def test_oos_score_rejects_blanket_all_five_counts_must_move_claim() -> None:
|
||||
token = "demo-ledger-deadbeef"
|
||||
prompt = benchmark.prompt_catalog(token)[3]
|
||||
|
|
|
|||
Loading…
Reference in a new issue