Conversation
P1a of the dynamic-topics install path: the executor behind
`proof-admin topic install`. The CLI is the trigger; this is the work.
The bundle carries its `rlm` section verbatim and opaque, so no topic
behavior is compiled in. That leaves the other half of the boundary: a
consumer that applies the parts an install knows how to apply, under two
closed gates that run before anything is touched.
- `sql_guard`: a topic migration runs in the shared challenge database
under a `topic_id` discriminant, so it is deny-listed by object
(every `proof_*` object by prefix, the sqlx bookkeeping table, the
roles, the catalogues), by statement (`DROP DATABASE`/`SCHEMA`/`ROLE`/
`OWNED`/`EXTENSION`, `GRANT`/`REVOKE`, session and transaction control,
`COPY`, maintenance verbs, `SECURITY DEFINER`, server-side file
access), and by namespace — every table it touches must be its own
(`{topic_id}_*`, `topic_*`, `{topic_id}.…`). Strings, comments, and
dollar-quoted function bodies are blanked before scanning, so a denied
word in a literal is data and a denied statement cannot be smuggled in
by quoting. Function bodies are scanned, not trusted.
- `handler`: an RLM section is operator-supplied JSON, not a signed
document, so a handler name in it is untrusted input. Only the generic
in-guest runner and an operator-baked Harbor adaptor over it may be
bound; a path, a URL, or a command line is refused by shape.
- `section`: reads only the parts an install applies, strictly. A key
inside a part it reads is refused (a step nothing would perform); a part
it has never heard of travels, because that is the boundary working.
- `install`: migrations (checked as a whole, then applied per-migration in
one transaction, resumed from the journal), routes recorded in the new
`proof_topic_api` table under the topic's own prefix, the rule vector
installed through the store the scoring path reads, and the executor
binding recorded — including the `vms_per_submission = 1` pin.
`0025_proof_topic_install.sql` adds the journal and the route table, both
append-only for `base_app`, so an install that could be edited in place
would not be a journal.
Tests: 43, including an adversarial deny-list suite that tries quoting,
comments, dollar-quoted bodies, case, schema qualification, and
`IF NOT EXISTS`, and asserts the legitimate migrations still apply.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review |
The CLI was a trigger that printed a procedure. It now performs it, in the order the procedure describes, with every authorization explicit. - Publish the signed document through the existing admin route. The bearer is read from a file and never printed or logged; a missing URL, a missing token file, or an empty one is a **usage error before anything is written**, so a misconfiguration cannot leave a half-installed topic. - Apply the bundle's RLM section through `proof-topic-install`: migrations under the deny-list, routes, the rule vector, the executor binding — all recorded in the install journal. - Point the bundle's declared aliases at the topic. `aliases` is a new bundle field (`Vec<String>`, bounded, shape-checked, never the topic's own id) because an alias is a lookup key and not topic data. - `--drive-rlm` drives the RLM's own lifecycle over the topic-VM orchestrator. It provisions a VM and runs a paid baseline, so it requires `--owner-approved`; `--skip-baseline` requires `--drive-rlm`, because without it the install never reaches the baseline job. Two new commands: `topic install-log --topic <id>` reads the journal, and `topic show` is unchanged. Fail-closed: an install never publishes an `open` document, so a failed install leaves the topic draft/disabled and miners cannot submit to it. Every refusal prints rollback notes naming what is and is not changed, and the journal records the attempt so a re-run resumes rather than restarts. Tests: the P0 stub tests are replaced by the P1a contract — a real install refuses without a master and a bearer, the bearer never reaches stdout or stderr, and the two spend gates are checked before any I/O. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The unit suite proves the gates; these prove the engine, end to end against Postgres, in the convention the store's own contract tests use (DB-gated on `DATABASE_URL`, so `cargo test --workspace` stays green without one). What they pin, in the terms an operator would ask: - A permitted bundle installs: both migrations apply in order, the table really exists in the topic's namespace, the rules the scoring path reads are the ones the install landed, the routes read back through the mux's own query, and the journal's newest row is `applied`. - A **pre-flight refusal writes nothing at all** — no table, no rule, and no journal row, because the deny-list runs before the journal opens. A denied bundle is a no-op, not a failed attempt. - A **step failure does journal**: a migration the database rejected rolls back its own statements (one migration is one transaction) and appends a `failed` row naming it. - A re-run resumes: applied migrations are skipped and named, the rules version is not bumped, routes are not duplicated, and a *new* migration in the bundle still applies. - The gates hold through the engine, not only in the reader: an unregistered open custom id and an arbitrary handler both refuse before any write. Writing them found two real bugs: the handler binding reported the document's runner as a handler *family* (so a Harbor topic journaled as `vm_backed`), and the failure shape was documented as "always journals" when a pre-flight refusal journals nothing. Both are fixed here, and `bound_runner` now records the family the section asked for while the signed document keeps sole authority over the runner. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Greptile SummarySummary
Merge SafetyDo not merge until publication is tied to the installed bundle and static bundle validation occurs before optional RLM work. Confidence Score: 3/5The change is not merge-safe because a changed topic document can open without its corresponding install, and optional RLM effects can still precede static bundle acceptance. The reproduced publication failure shows that Files Needing Attention: crates/proof-topic-install/src/install.rs
What T-Rex did
|
| '\'' => { | ||
| // String literal: kept for execution, blanked for scanning. | ||
| text.push(c); | ||
| blanked.push(' '); | ||
| i += 1; | ||
| while i < chars.len() { | ||
| if chars[i] == '\'' { | ||
| if chars.get(i + 1) == Some(&'\'') { | ||
| text.push_str("''"); | ||
| blanked.push_str(" "); | ||
| i += 2; | ||
| continue; | ||
| } | ||
| text.push('\''); | ||
| blanked.push(' '); | ||
| i += 1; | ||
| break; | ||
| } | ||
| text.push(chars[i]); | ||
| blanked.push(' '); | ||
| i += 1; |
There was a problem hiding this comment.
PostgreSQL permits executable SQL-language function bodies in single-quoted strings. This branch removes those body bytes from both inputs checked by the migration guard, but the installer later executes the unchanged statement. A topic can therefore install a function such as AS 'DELETE FROM proof_rule_version' LANGUAGE sql that accesses protected shared data after the migration is approved. Reject or decode and scan single-quoted function bodies before accepting the statement.
How this was verified: An executed check showed the protected DELETE remained in executable SQL while both guard scan inputs omitted it and the migration was accepted.
Knowledge Base Used: Proof challenge network and evaluation
Artifacts
- The exact executable Rust test source used to construct the PostgreSQL function, inspect guard inputs, and assert that the guard allows it; it defines the tested condition.
- Output from executing the narrow Rust test in `/home/user/repo`; it shows the protected DELETE remains executable but is absent from scanned text, so the guard allows it.
| let pending_id = self | ||
| .journal(request, InstallState::Pending, None, &[], &[], &binding, "") |
There was a problem hiding this comment.
Checkpoint committed migrations
The initial pending journal row records an empty migration list, while each migration commits independently and its name is retained only in memory until the final applied row is written. If the process stops after a migration commits but before that final row is inserted, resume has no durable record of the committed migration and runs it again. Normal CREATE TABLE migrations then fail on an existing relation, and non-idempotent data migrations can apply twice. Persist each migration's completed state atomically with, or immediately following, its transaction.
Artifacts
- Python source executed against the changed installer control flow and a durable-state model; it verifies that a committed migration is retried when the pending journal row has no migration names.
- Captured execution output from the focused validation; it shows an empty pending journal, no resume credit, and a duplicate-relation failure after retrying the committed migration.
| if schema == Some(topic.as_str()) { | ||
| return true; | ||
| } | ||
| bare.starts_with(&format!("{topic}_")) || bare.starts_with("topic_") |
There was a problem hiding this comment.
Every bare object name beginning with topic_ is accepted regardless of the installing topic ID. Approved SQL then runs through the shared database connection without selecting or enforcing a topic-specific schema. One topic can therefore update, delete, or drop another topic's topic_* table. Require names tied to the actual topic ID or enforce database-level per-topic isolation before executing migrations.
How this was verified: An isolated database execution approved one topic's update of a victim topic_* table and changed that table's value.
Knowledge Base Used: Proof challenge network and evaluation
Artifacts
- Shell script that starts isolated PostgreSQL, generates and runs the narrow Rust test using the production guard and sqlx execution API; it demonstrates the exact validation method.
- Captured output from executing the validation script shows four topic IDs approving the same bare topic table and the database row becoming compromised, confirming no true per-topic isolation.
The install section still described P0: a CLI that prints a procedure and touches nothing. It performs it now, behind two closed gates, so the doc and the fixture have to say what it does — and, more usefully, what it refuses. - `docs/PROOF.md`: a "Running the install for real" section with the exact command, the four steps in order, the two gates that authorize spend (`--drive-rlm` needs `--owner-approved`), the deny-list and handler allow-list as tables, and the two failure shapes. An install never publishes an `open` document, so a failed install leaves the topic draft/disabled; a pre-flight refusal writes nothing at all, while a step failure journals how far it got and resumes on re-run. The locked-defaults table gains the two new rows (the RLM gate and the `vms_per_submission = 1` pin), and the stale "the CLI does not read the section" paragraph is corrected: the CLI carries it, the install executor is its one consumer, and a part it has never heard of travels. - The fixture bundle gains the parts a real install applies — a `migrations` entry, an `apis` entry, `scoring`, and the Owner-default `aliases: ["tbench"]` — so the committed artifact exercises the same path an operator's bundle will. The sample migration stays inside the topic's own namespace, which is what makes it legal under the deny-list. - `regenerate_dry_run_fixture` resolved its output directory against `cargo test`'s working directory (the package), so the documented command wrote to `bins/proof-admin/bins/proof-admin/…`. It now resolves against the workspace root, so the command writes where it says it does. - The install plan prints the aliases it will point at the topic, so an operator reading the dry run sees the whole procedure. Gates: fmt, workspace clippy, all xtask gates, and every workspace test suite except four pre-existing root-only permission tests (they fail on the base branch too: they `chmod 000` a directory and expect the read to fail, which root ignores). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review — three commits added since the first review request: the real |
| admin | ||
| .publish(&bundle.topic) | ||
| .await | ||
| .map_err(|e| Failure::Error(publish_failure(&e)))?; |
There was a problem hiding this comment.
The command publishes an accepted sealed open topic before it starts the fallible RLM installation. If a later migration, route, rule, binding, or journal operation fails, the topic remains published and eligible for submissions despite incomplete installation. Publish only after required installation succeeds, or prevent open topics from accepting submissions until installation is complete.
How this was verified: A real install published an open topic and then failed on a deliberately failing downstream migration.
Knowledge Base Used:
| if args.drive_rlm { | ||
| // The setup runs after the rules land; its own summary is written by | ||
| // the caller of the install (this CLI) once it returns. | ||
| return SetupSummary::Skipped { | ||
| reason: if args.skip_baseline { | ||
| "--skip-baseline: the RLM's rules were installed, no baseline was measured" | ||
| .to_owned() | ||
| } else { | ||
| "the RLM setup is driven by the operator over the topic-VM orchestrator".to_owned() | ||
| }, | ||
| }; |
There was a problem hiding this comment.
With --drive-rlm --owner-approved, this branch immediately returns a skipped setup result. It does not provision a VM, propose rules, or run the baseline, while the static installation can still complete and report success. Operators therefore receive a successful install result even though the explicitly requested RLM setup never ran.
Knowledge Base Used: Proof challenge network and evaluation
Greptile found three P1s on #298. All three were real; each is fixed and pinned by a test that fails without the fix. 1. **A single-quoted function body was executed without being scanned.** PostgreSQL accepts `CREATE FUNCTION f() RETURNS void AS 'DELETE FROM proof_rule_version' LANGUAGE sql`, and a string literal is normally *data*, so the scanner blanked it. A topic could therefore install a function that reaches a protected object by wrapping the statement in `AS '…'`. A function definition's literals are code: they are now decoded (`''` → `'`, so an escaped spelling cannot hide a statement) and scanned exactly as a dollar-quoted body is. 2. **The generic `topic_` prefix was a shared namespace, not a boundary.** Every topic shares one database, so `topic_scores` is one table that every topic's install can reach — approving it for topic A would let A write, truncate, or redefine the table B created. The allowance is gone: a name is the topic's only if it is `{topic_id}_*` or `{topic_id}.…`. Writing the test for this found a second hole: a table name after `TRUNCATE` was never checked at all, because the verb was not in the keyword list the scanner follows. Both are fixed, and `DELETE FROM x` still resolves to `x` rather than to `from`. 3. **A committed migration could be lost on a crash.** The migration and the journal row that records it committed in separate transactions, so a crash in between left a migration applied with nothing recording it; a resume re-applied it, and ordinary non-idempotent DDL (`CREATE TABLE`) failed on a duplicate relation. The row is now written **in the same transaction** as the migration it names, so the two states reachable are "applied and recorded" or "neither". A resume reads the progress rows, which is what makes it correct rather than lucky. The guard moves to its own crate (`proof-topic-sql-guard`): it is pure text analysis, and `proof-topic-install` had crossed the 1500 non-test LOC cap. Separating it also means the rules can be tested — and read — without a database. Tests: 58 in the two crates, including a crash-resume test that fails without fix 3, an escaped-quote test that fails without fix 1, and a cross-topic test that fails without fix 2. The 11 DB-backed engine tests pass against a real Postgres. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review — all three P1s are fixed at
The guard also moves to its own crate ( Verification: 58 tests across the two crates; the 11 DB-backed engine tests pass against a real Postgres; fmt, workspace clippy, and all five xtask gates clean. |
| if is_function_definition(blanked) { | ||
| for literal in single_quoted_literals(text) { | ||
| bodies.push(' '); | ||
| bodies.push_str(&literal); | ||
| } |
There was a problem hiding this comment.
Decode Escaped Function Bodies
PostgreSQL decodes E'…' escape strings and U&'…' Unicode strings before using them as SQL-language function bodies, but this path appends the literals after only doubled-quote decoding. A topic can therefore encode DELETE FROM proof_rule_version so the guard sees only escape sequences and accepts the migration, while PostgreSQL installs executable SQL that accesses protected shared state. Decode these literal forms using PostgreSQL semantics, or reject them in function definitions until they can be parsed safely.
How this was verified: A focused test showed that the guard accepted both encoded forms of
DELETE FROM proof_rule_versionwhile retaining their undecoded escape sequences.
Artifacts
Escaped function-body regression harness
- The harness defines E-string and U& Unicode function bodies that PostgreSQL decodes to protected SQL, showing the exact inputs tested.
Escaped function-body test output
- The test output shows both encoded function bodies were accepted and the test failed because neither was refused.
- The environment check shows no database connection or local PostgreSQL executable was available for server-side execution.
A topic migration may write its function body as a string literal, and PostgreSQL executes the *decoded* value. The guard scanned the written spelling, so `E'\x44ELETE FROM proof\x5frule\x5fversion'` reached a `proof_*` object without ever matching a deny rule. Decode every literal form PostgreSQL reads before scanning a body: - `E'…'`: backslash escapes (`\xhh`, `\ooo`, `\uXXXX`, `\UXXXXXXXX`, `\'`, `\\`) and `''` doubling. - `U&'…'`: `XXXX` / `+XXXXXX` code points, with the escape character a following `UESCAPE 'c'` clause names. - Adjacent literals separated by a newline, which PostgreSQL joins into one string (`'DROP TABLE proof'` + `'_topic_version'`). - `CREATE PROCEDURE … AS '…'` and `DO '…'`, whose literals are code the same way a function body is. Both the decoded value and the written spelling are scanned, and a sequence PostgreSQL would refuse is kept as written. Four regression tests fail without this change. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
`cargo fmt --all -- --check` is a required gate and these four files were not rustfmt-clean on this branch, so CI's fmt step could not pass. Formatting only: no behavior, no names, no logic. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
An install **wrote** the routes a topic claims into `proof_topic_api`;
nothing **read** them, so `/challenge/{topic_id}/…` had no answer and a
topic's own API was dead on arrival.
Read side (crates/proof-topic-install `routes`):
- `TopicRouteMux` resolves `(topic_id, method, path)` against the table and
distinguishes a registered route, a method the route did not claim, and a
path nobody registered — so the caller answers 405/404 instead of
inventing a route.
- The cache is keyed by a **generation** (`SELECT count(*)`, sound because
migration 0025 grants the app role `SELECT, INSERT` only, so the table is
append-only): an install in another process is visible on the next
request with no restart and no cross-process signal. `invalidate()` is
the in-process form.
- An unreadable registry is an error, never a 404.
Serving (crates/proof-challenge `topic_routes`, wired in the binary):
- `/challenge/{topic_id}/…` answers 200 with the row the install wrote,
405, 404, or **503** when the table cannot be read.
- The mux is wired from the same pool the RLM store uses; a host with no
database serves the Proof routes alone.
- No topic route is compiled in: the table is the registry.
Gateway (crates/gateway-core `topic_routes`, crates/gateway proxy):
- An id the registry does not know is forwarded to the **Proof** backend
with the topic id kept in the path, but only when it is topic-shaped
(`^[a-z0-9][a-z0-9-]{1,62}$`, the table's own CHECK). The challenge is
the gate, so a non-topic id is a 404 and a mistyped challenge id keeps
the registry's own `no healthy backends`. `v1/admin/*` stays blocked.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai re-review HEAD 9cb9f98 — E/U& sql_guard + mux proof_topic_api landed; please re-score |
`topic install` published the signed document **first** and applied the RLM section afterwards. An `open` document is submitable the moment it reaches the registry, so a miner could submit to a topic whose migrations, routes, and rules were not installed yet — and if the install failed, the topic stayed live and uninstalled. The publish is now the last fallible step: drive the RLM, apply the section (both fail-closed, both journaled), then publish, then point the aliases. A refused install publishes **nothing**, and a publish that fails leaves the topic unpublished with nothing to undo — the re-run skips the applied migrations and publishes. The old justification (the topic has to exist before the rest can key on it) does not hold: the rule store, the route table, and the journal key on `topic_id` alone, and the RLM setup writes the document itself when it is absent. Only the aliases need a published topic, so they stay last. The failure messages and the module/runbook docs now say what is and is not in place at each step. Regression tests (bins/proof-admin/tests/cli.rs), both of which fail against the old order: - `the_publish_lands_only_after_the_install_is_green` reads the journal and the migration's table **inside** the publish handler, so it asserts the state a miner would have found at the instant the topic became reachable. - `a_refused_install_never_publishes` drives a deny-listed migration and asserts the admin stub received no request at all, with no journal row and no table. Also repaired, all pre-existing on this branch and all blocking `cargo clippy --workspace` / `cargo test --workspace`: - `bins/proof-challenge` did not compile: the mux wiring needs `proof-topic-install` and `sqlx` as direct dependencies, and the mux helper and queue drainer are extracted so `run` stays under the pedantic line cap. - `bins/proof-admin` clippy: two `Option::is_none_or` sites, a `useless_format`, and two documented allows for the driver's explicit parameter list and the install's order-is-the-contract function. - `crates/proof-rlm-scorer/tests/rlm_e2e.rs` did not compile: `FakeOrchestrator::new` takes the baseline primary and returns an `Arc`, so two call sites were missing the argument and double-wrapped. With the target compiling, two tests then failed on a missing owner-key fixture and one asserted a rules version the store's append-only versioning does not produce (the resume's proposal lands as v2); all three are corrected. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Three blockers from the last review are addressed on this branch (head
Also repaired, all pre-existing on this branch and blocking |
| // The RLM setup is driven **before** the static half lands, so the rules | ||
| // the driver proposes are the topic's current version and the install | ||
| // keeps them rather than overwriting them with the bundle's vector. | ||
| let driven = if args.drive_rlm { | ||
| if !opts.json { | ||
| println!(" (driving the RLM: provision → rules → baseline)"); | ||
| } | ||
| Some(drive_rlm(args, &bundle.topic, pin, &pool, &store).await?) | ||
| } else { |
There was a problem hiding this comment.
With --drive-rlm, this runs drive_rlm before Installer::install validates the static install section. A bundle later refused for denied SQL, malformed section data, unsupported handlers, or invalid bindings can therefore already have persisted lifecycle and rule state, provisioned or attached a host, and run a paid baseline. The rejection path returns without compensating for those effects. Validate the complete static install plan before calling drive_rlm so a refused bundle writes nothing and does not touch a host.
Knowledge Base Used:
…plied The publish order was a **client** convention: `proof-admin topic install` publishes last (d3c79b5), but a direct `POST /v1/admin/proof/topics` could still put an `open` document in the registry before the topic's migrations, routes, and rules existed. An `open` document is submitable the moment it is published, so the route is where the rule has to live. `POST /v1/admin/proof/topics` now refuses an `open` document with **409** unless the topic's newest `proof_topic_install` row is `applied`: | host state | `open` | `draft` | |---|---|---| | install `applied` | 201 | 201 | | `pending` / `failed` / no row | 409 | 201 | | journal unreadable | 409 | 201 | | no journal on the host (no database) | 409 | 201 | A `draft` is never gated: it is not submitable, and staging one is how an operator stages a bundle. Every refusal says which case it was, so the operator can tell "finish the install" from "fix the database" — the reason is returned in the body rather than logged, because `proof-http` has no logging dependency and this is the response the operator reads. - `proof-http` gains the `InstallJournal` trait and the `install_journal` field on `AppState`; `None` is fail-closed. - `proof-topic-install` gains `applied_install` / `is_installed`, the journal read the gate and `topic install-log` share, so the operator and the route cannot disagree about whether a topic is installed. - `proof-challenge` gains `PgInstallJournal` over that read, wired in the challenge binary from the same pool the RLM store and route mux use. The regression test fails without the gate: it drives all four host states against one document, asserts the four refusals name their own cause, asserts the same document still drafts on every one of them, and asserts an applied install admits it. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Head is now New: the publish gate is in the route, not just the CLI.
A Also: the CLI publishes last ( Your six findings, against current HEAD:
|
| // The RLM setup is driven **after** the static half lands, so the rules | ||
| // the driver proposes supersede a version the topic already has rather | ||
| // than being overwritten by the bundle's vector. | ||
| let driven = if args.drive_rlm { | ||
| if !opts.json { | ||
| println!(" (driving the RLM: provision → rules → baseline)"); | ||
| } | ||
| Some(drive_rlm(args, &bundle.topic, pin, &pool, &store).await?) |
There was a problem hiding this comment.
With --drive-rlm, this calls drive_rlm before Installer::install parses the RLM section, checks every migration, and resolves the handler and binding. A malformed or denied install can therefore provision a VM, persist RLM rules, and run a paid baseline before returning a pre-flight refusal. Run the installer validation before driving the lifecycle so rejected bundles cannot spend resources or leave durable setup state.
Knowledge Base Used:
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| pub async fn applied_install(pool: &PgPool, topic_id: &str) -> Result<bool, InstallError> { | ||
| let state: Option<String> = sqlx::query_scalar( | ||
| "SELECT state FROM proof_topic_install WHERE topic_id = $1 ORDER BY id DESC LIMIT 1", | ||
| ) | ||
| .bind(topic_id) | ||
| .fetch_optional(pool) | ||
| .await | ||
| .map_err(|e| InstallError::Db(e.to_string()))?; | ||
| Ok(state.as_deref() == Some(InstallState::Applied.as_str())) |
There was a problem hiding this comment.
The open-topic gate checks only that the newest install row for this topic ID is applied; it never compares that row's bundle_digest with the signed document being published. A changed signed bundle can reuse an earlier applied install for the same topic ID and receive 201 Created, even though its changed migrations, routes, rules, or executor settings were never installed. Require the published document's canonical bundle digest to match the newest applied install before opening the topic.
Knowledge Base Used:
Stacked on #297 (
droid/795020b8-sn100-p0-topics-table-admin). DRAFT — do not merge.P1a of the dynamic-topics install path: the executor behind
proof-admin topic install. #297 built the bundle, the validation, and the dry run; this makes the install real.What this adds
proof-topic-install— the executor crate.sql_guard: a topic's SQL runs in the shared challenge database under atopic_iddiscriminant, so a migration that reached outside its own namespace could rewrite another topic's rules, forge a promotion, or drop the tables scoring reads. Refused by object (everyproof_*object by prefix — so a table added later is covered — plus_sqlx_migrations, the roles, the catalogues), by statement (DROP DATABASE/SCHEMA/ROLE/OWNED/EXTENSION,GRANT/REVOKE,SET/RESET/BEGIN/COMMIT,COPY, maintenance verbs,SECURITY DEFINER, server-side file access), and by namespace — every table a statement creates, writes, or reads must be its own ({topic_id}_*,topic_*,{topic_id}.…). Strings, comments, and dollar-quoted function bodies are blanked before scanning, so a denied word inside a literal is data and a denied statement cannot be smuggled in by quoting. Function bodies are scanned, not trusted.handler: an RLM section is operator-supplied JSON, not a signed document, so a handler name in it is untrusted input. Only the generic in-guest runner (Firecracker) and an operator-baked Harbor adaptor over it may be bound. A path, a URL, or a command line is refused by shape; a well-formed but unknown id is refused with the allow-list in the message. The signed document keeps sole authority over the runner.section: strict reads of the parts an install applies. A key inside a part it reads is refused (a step nothing would perform); a part it has never heard of travels, because that is the boundary working.submission_formatandscoringare recorded as canonical-JSON digests and otherwise untouched.install: the engine — migrations checked as a whole before the first statement runs, then applied per-migration in one transaction and resumed from the journal; routes recorded under the topic's own prefix; the rule vector installed through the store the scoring path reads; the executor binding recorded, including thevms_per_submission = 1pin.0025_proof_topic_install.sqladdsproof_topic_install(the install journal) andproof_topic_api(the topic's dynamic routes, paths stored relative so the resolver owns the prefix). Both append-only forbase_app.proof-admin topic installnow performs the procedure: publish the signed document through the existing admin route (bearer read from a file, never printed), apply the RLM section, point the declared aliases at the topic, and — with--drive-rlm --owner-approved— drive the RLM's own lifecycle.--skip-baselinestops before the baseline job. A newtopic install-logreads the journal back.Bundle
aliases: a bounded, shape-checked list of temporary compatibility slugs (never the topic's own id), because an alias is a lookup key and not topic data.Two failure shapes
failedjournal row naming the step; applied migrations stay applied and are recorded, so a re-run resumes rather than restarts.An install never publishes an
opendocument, so a failed install leaves the topic draft/disabled and miners cannot submit to it. Every refusal prints rollback notes.Verification
cargo test -p proof-topic-install— 53 tests: an adversarial deny-list suite (quoting, comments, dollar-quoted bodies, case, schema qualification,IF NOT EXISTS) that also asserts legitimate migrations still apply, the handler allow-list, and the section reader.crates/proof-topic-install/tests/install_engine.rs— 10 tests against a real Postgres: the happy path, both failure shapes, resume, rollback, and both gates holding through the engine.bins/proof-admin/tests/cli.rs— the P0 stub assertions replaced by the P1a contract (a real install refuses without a master and a bearer; the bearer never reaches stdout or stderr; the two spend gates are checked before any I/O).chmod 000+ expect-failure, which root ignores) — confirmed failing identically on the base branch.Note: CI does not run on this PR because it targets the #297 branch, not
main.Out of scope (P1b–d)
Full concurrent 2-VM E2E (P1b), FE debrand of
harbor-trials-v1(P1c), live TB4 hardcode retirement (P1d).Do not merge
Owner runs the staging LIVE A→Z gate later. This PR stays draft; #297 stays draft.