Conversation
Dynamic-topics slice 1 (Arch pins): a per-topic install registry and the operator CLI that writes it. No behavior change — nothing on a scoring path reads the new table, no route and no allocator changes, and the compiled-in topic bindings stay (P4). - db: migration 0024 adds `proof_topic` to the shared challenge DB, keyed by `topic_id` (the discriminant), with `display_name`, `version`, `environment`, `runner_id`, `aliases`, `enabled`, `config` JSONB, `pin_rlm`, `pin_experiment`, `pack_digest`, `n_concurrent`, `sealed_custom_value`, `schema_version`, the bundle verbatim + its digest, and timestamps. Shape CHECKs guard the slug, the `sha256:<64 hex>` pins, a finite baseline, and the alias array. Mutable (enable/disable, re-install, seal) so `base_app` gets SELECT/INSERT/UPDATE and never DELETE: a topic is disabled, never dropped. - db::topics: upsert / list / get over runtime sqlx (no compile-time DB), plus DB-gated integration tests (install, ordering, re-install keeps created_at and does not silently disable, CHECK refusals, app-role grants). - crates/proof-topic-bundle: the topic install bundle schema v1 — parse, validate, canonical digest, install plan. Unknown keys are refused, every pin is `sha256:<64 hex>` or absent (never invented), and an in-guest `runner_id` without a `pack_digest` is refused. - bins/proof-admin: `topic validate` (no writes), `topic install [--dry-run]` (dry-run needs no database), `topic list`, `topic show`. Installing writes `enabled = false`; `topic enable` / `disable` / `seal` exit 3 with a clear "not implemented in this slice". Process-level tests cover validate, dry-run, the env mismatch, the missing-database path, and the stubs. - docs: PROOF.md § Topic install bundles, COMPLETENESS row, ARCHITECTURE bin. Arch defaults: first topic slug `tb4` with alias `tbench`; shared DB with a `topic_id` discriminant. Alias resolution, routes (P1), the allocator (P2), the full install (P3), and removing the hardcoded bindings (P4) are later slices. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review |
Greptile SummaryThis PR introduces a topic-administration bundle and CLI over the existing signed-topic publication path, adds latest-topic and alias views to the RLM stores, and adds collision-safe alias persistence.
Confidence Score: 4/5The PR is not yet safe to merge because duplicate named RLM fields can bypass the bundle’s required shape and explicit-null validation. The raw RLM hand-off now preserves the enclosing object as intended, and the previous migration walkthrough issue is fixed. However, validation still collapses duplicate keys into a map before checking them, allowing invalid content to remain in the exact bytes accepted for RLM installation. Files Needing Attention: crates/proof-topic-bundle/src/lib.rs
|
| Filename | Overview |
|---|---|
| crates/proof-topic-bundle/src/lib.rs | Preserves the complete RLM object verbatim, but duplicate named fields can evade shape and null validation. |
| bins/proof-admin/src/main.rs | Adds validation, dry-run publication planning, registry inspection, alias administration, and fail-closed lifecycle stubs. |
| crates/db/migrations/0024_proof_topic_alias.sql | Adds temporary aliases and serialized cross-table collision guards without duplicating topic records. |
| crates/proof-rlm-store/src/pg.rs | Adds latest-topic registry views and fail-closed alias operations over the existing topic-version table. |
| bins/proof-admin/tests/fixtures/README-dry-run.md | Correctly directs staging operators to boot-time embedded migrations instead of the unavailable sqlx CLI. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
B[Install bundle] --> V[Bundle and signed-document validation]
V --> R[RLM raw install hand-off]
V --> D[Dry-run publication plan]
D --> A[Existing admin topic endpoint]
A --> T[(proof_topic_version)]
L[proof-admin list/show] --> T
X[proof_topic_alias] --> T
Reviews (15): Last reviewed commit: "fix(proof): carry the whole RLM section ..." | Re-trigger Greptile
| if opts.json { | ||
| let body = serde_json::json!({ | ||
| "ok": true, | ||
| "installed": true, | ||
| "topic_id": plan.topic_id, | ||
| "environment": plan.environment.as_str(), | ||
| "bundle_digest": plan.bundle_digest, | ||
| "enabled": false, | ||
| }); | ||
| println!( | ||
| "{}", | ||
| serde_json::to_string_pretty(&body).unwrap_or_else(|_| "{}".into()) | ||
| ); | ||
| return Ok(()); | ||
| } | ||
| print_plan(&plan); | ||
| println!(); | ||
| println!( | ||
| "Installed {} (DISABLED). Enabling is a later slice: nothing scores this topic yet.", |
There was a problem hiding this comment.
Reinstalling a topic deliberately preserves its existing enabled state, but both success-output paths always say the topic is disabled. An enabled topic therefore remains enabled while operators and automation are told the opposite, creating avoidable configuration mistakes. Return the persisted state in the install result before rendering it.
Artifacts
- Runs a disposable migrated PostgreSQL instance, installs, enables, reinstalls, and queries the proof topic, demonstrating the output/state mismatch.
- Captured execution shows the reinstall JSON says enabled false while the direct database query returns enabled true, confirming the defect.
- Records environment availability and runs the repository test covering preservation of enabled state during a topic reinstall.
- Captured test run passes the targeted reinstall-preservation test; it also records unset database URLs and absence of local PostgreSQL executables.
- Checks Docker availability and executes the real-install-without-database CLI test, confirming the initial environment's database constraint.
| fn is_digest(s: &str) -> bool { | ||
| s.strip_prefix(DIGEST_PREFIX) | ||
| .is_some_and(proof_canon::is_hex64) | ||
| } |
There was a problem hiding this comment.
Canonical Digests Pass Validation
The digest validator accepts uppercase and whitespace-padded SHA-256 pins, then leaves those values unchanged in the install plan. The database accepts only an exact lowercase, unpadded digest, so a bundle can successfully validate or dry-run but fail during a real install. Require exactly sha256: followed by 64 lowercase hexadecimal characters before accepting the bundle.
Artifacts
- The authored script creates the two bundle variants and invokes the real proof-admin dry-run install command, showing the validator accepts the noncanonical values.
- The control command completed with exit code 0 and emitted canonical lowercase pin digests, establishing the accepted persistence-compatible shape.
- The noncanonical command completed with exit code 0 and emitted uppercase and whitespace-padded pin digests unchanged, proving validation accepts persistence-incompatible values.
- The focused bundle and CLI regression suites completed successfully with 26 passing tests, showing the reproduced path executed in the normal testable stack.
- The DB topic test target compiled and returned success, but the source test gate causes all five tests to return early without DATABASE_URL, so live PostgreSQL rejection could not be executed.
| /// A `u32` that fits the row's `INTEGER` columns. The bundle's own bounds keep | ||
| /// every value far below `i32::MAX`, so this only guards the cast. | ||
| fn clamp_i32(v: u32) -> i32 { | ||
| i32::try_from(v).unwrap_or(i32::MAX) |
There was a problem hiding this comment.
Bundles with version or n_concurrent above i32::MAX pass validation, but installation silently writes i32::MAX to the typed columns. The stored row then disagrees with the validated, digest-covered bundle, so later consumers can act on values the operator did not install. Reject out-of-range values instead of clamping them.
Artifacts
- Creates boundary and overflow bundle files, runs the actual no-DB proof-admin dry-run path, and records command metadata plus output; it demonstrates the validator accepts the overflow input.
- Actual proof-admin CLI output for a bundle with version and n_concurrent equal to i32::MAX; it exits 0 and shows both boundary values unchanged.
- Actual proof-admin CLI output for a bundle with version and n_concurrent equal to i32::MAX plus one; it also exits 0 and exposes the unbounded values that the persistence mapper will clamp.
- Records the command, repository working directory, and zero exit status for the authored focused reproduction script; it confirms both comparison captures were produced by execution.
- Records the process-level proof-admin CLI test command and its 11 passing tests; it confirms existing CLI coverage remains green while missing the oversized-integer case.
| CONSTRAINT proof_topic_aliases_bound CHECK (cardinality(aliases) <= 8), | ||
| CONSTRAINT proof_topic_aliases_shape CHECK ( | ||
| cardinality(aliases) = 0 | ||
| OR array_to_string(aliases, ',') ~ '^[a-z0-9][a-z0-9-]{1,62}(,[a-z0-9][a-z0-9-]{1,62})*$' | ||
| ), | ||
| CONSTRAINT proof_topic_aliases_not_self CHECK (NOT (topic_id = ANY (aliases))), |
There was a problem hiding this comment.
The alias constraints allow a TEXT[] containing a NULL element because the joined-string check omits NULL values and CHECK expressions accept unknown results. The typed topic reader expects every alias to be a string, so one accepted row makes topic list and show fail while decoding aliases. Add a constraint that rejects NULL array elements.
Artifacts
- Captured the database environment check, showing DATABASE_URL was unset and only Docker was available, ending with the takeaway that no configured local database endpoint existed.
- Authored and executed script that creates the relevant constraints and inserts a text array with a NULL alias element, ending with the takeaway that it exercises the candidate condition on PostgreSQL.
- Captured the executed Docker PostgreSQL 16 reproduction, where INSERT succeeded and the returned row contains `{valid-alias,NULL}`, ending with the takeaway that the migration permits the invalid typed value.
- Captured the migration constraints and the Vec<String> decoding/list/show code paths, ending with the takeaway that an accepted NULL array element reaches an incompatible typed decode.
- Captured the executed db topic test command with four unit tests passing and DATABASE_URL-gated integration tests skipped, ending with the takeaway that existing tests do not cover NULL alias rejection.
| let canonical = self.canonical()?; | ||
| if canonical.len() > MAX_CONFIG_BYTES { | ||
| return Err(BundleError::ConfigTooLarge(canonical.len())); | ||
| } |
There was a problem hiding this comment.
Config Limit Includes Metadata
The advertised config-size limit is applied to the complete canonical bundle instead of the config object. A configuration below 16 KiB can be rejected solely because otherwise valid metadata increases the bundle size, and the error incorrectly attributes that rejection to the config. Measure the canonical config object alone.
Artifacts
- The uploaded Bash script generates and executes a Rust harness against the repository crate for minimal and metadata-heavy bundles, demonstrating the measurement scope.
- Running the probe with minimal metadata measured a 16,010-byte canonical config and a 16,142-byte bundle, which validation accepted.
- Running the same probe with valid extra metadata kept the config at 16,010 bytes but grew the bundle to 17,164 bytes, which validation rejected as `ConfigTooLarge`.
Five findings from the Greptile review of #297, each with a regression test. - digest pins (P1): accept exactly `sha256:` + 64 lowercase hex. The row's CHECK is `^sha256:[0-9a-f]{64}$`, but `proof_canon::is_hex64` trims and accepts uppercase, so `sha256:AB…` or `sha256: ab… ` validated and dry-ran and then failed a real install on the host that matters. A local `is_lower_hex64` checks the one spelling both places agree on. - numeric columns (P1): refuse `version` / `n_concurrent` above `i32::MAX` instead of clamping to it. A clamped row disagreed with the validated, digest-covered bundle an operator reviewed; out of range is now `IntColumnOverflow`, and the CLI's clamp is gone. - NULL aliases (P1): `array_to_string` drops NULL elements, so the joined-string shape check accepted `{tbench,NULL}` — and the typed reader decodes every element as a `String`, so that one row made `topic list` and `topic show` fail for the whole table. `array_position(aliases, NULL)` is the probe that holds, in its own constraint so each one fails for one reason. - reinstall status (P2): report the **persisted** state after install. A re-install deliberately leaves `enabled` alone, so telling an operator (or an automation reading `--json`) that a live topic is disabled was the mistake that produces a surprise on a live host. - config bound (P2): measure the `config` object alone, not the whole canonical bundle, and report that number in the error. Tests: bundle 18 (was 15), db topics 6 (was 5), CLI 14 (was 11) — including a DB-gated reinstall test that enables a row and asserts the re-install reports `enabled: true`. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review All five findings from the previous review are addressed in
Regression tests added for each: bundle 15→18, db topics 5→6, CLI 11→14 (incl. a DB-gated reinstall test asserting |
| // would be exactly the mistake that leads to a surprise on a live host. | ||
| let persisted = db::get_topic(&pool, &plan.topic_id) | ||
| .await | ||
| .map_err(|e| Failure::Error(format!("read back {}: {e}", plan.topic_id)))? | ||
| .ok_or_else(|| { | ||
| Failure::Error(format!( | ||
| "install {} reported success but the row is missing", | ||
| plan.topic_id | ||
| )) | ||
| })?; |
There was a problem hiding this comment.
Committed Install Reports Failure
The install is committed before the separate get_topic query runs. If that read fails, the command returns an error even though the topic was successfully installed, so automation may retry and overwrite a newer concurrent install. Return the persisted fields from the upsert itself, for example with RETURNING, so the write and reported result have one outcome.
Greptile's re-review of #297: the install committed the upsert and then ran a separate `get_topic` readback. If that read failed the command returned an error for an install that had already landed, so an automation could retry and overwrite a newer concurrent install. `upsert_topic` now returns the row via `RETURNING` (reusing the same column list the reads use, so the write and the read cannot drift apart), and the CLI reports the persisted state from that value: one statement, one outcome. The returned `enabled` is still the persisted one a re-install deliberately leaves alone. Tests: `db` topics 6 → 7 with an explicit "the returned row is the persisted row" case (including a re-install that reports `enabled: true`). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review The readback finding is fixed in |
Arch §11 hardcoding inventory: cortex scoring and its DB are already multi-topic. The hardcoding is branding/docs/FE plus the `tbench-harbor-v1` alias, not a missing topics table. So P0 must wrap the path that already exists, not invent a parallel one. Removed (the parallel registry this slice had added): - migration `0024_proof_topics.sql` and its `proof_topic` table - `db::topics` (upsert/list/get) and its integration tests Those duplicated `proof_topic_version` (migration `0020`), which is already the durable topic table, written by the scoring path. A second table would have been a second source of truth for the same facts. Added, leaning on what exists: - `RlmStore::latest_topics` — a read-only registry **view** over the same `proof_topic_version` rows (`DISTINCT ON (topic_id) … ORDER BY version DESC`), implemented by both stores and pinned by the shared contract test. No new table, no new column, no migration. - `crates/proof-topic-bundle` reworked: the bundle now carries the signed `TopicDocument` verbatim plus a `host` block that must **agree** with it. Runner id, pack digest, and custom id come from the document's own `constraints.params` (read through `proof-experiment::ExperimentBinding`), so every binding has exactly one copy. A host expectation that contradicts the document is a reject, never an override. `pack_dir` is a directory, not a digest. - `bins/proof-admin` reworked: `topic validate` runs the same acceptance `POST /v1/admin/proof/topics` runs (`TopicDocument::validate` + `verify_signature` against `config/proof-pin.toml`), and `install --dry-run` prints that publish call plus the host env. `topic list` / `topic show` read the registry view. A real install is not implemented (exit 3) because publishing needs the operator bearer, which stays on the host; `enable` / `disable` / `seal` are stubs pointing at re-sign + re-publish. KEPT: runner `rlm_fc_in_guest_harbor`, the proof-experiment anti-hardcode locks, and the signed-topic bindings. NOT P0: FE debrand and the `harbor-trials-v1` rename. Tests: bundle 15, CLI 12 (incl. a DB-gated case that persists through the scoring path's own store and reads it back through the CLI), store contract extended to the view. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Remapped per the Arch §11 hardcoding-inventory delta in Removed the parallel registry: migration Now leans on what exists:
Kept: runner Still do not merge — Owner holds merge until staging A→Z. |
| " --data-binary @{} \\", | ||
| signed_document_hint(bundle_path) | ||
| ); |
There was a problem hiding this comment.
The dry run prints --data-binary @<extract .topic from …>, but this placeholder is invalid shell syntax rather than a document file. Substituting the bundle file also fails because the publish endpoint accepts a TopicDocument, not the bundle envelope. Operators therefore cannot use the advertised command to publish a bundle they just validated. Print an executable extraction step and reference its output file. This is non-blocking, but it makes the installation procedure unusable.
Artifacts
- The executable Bash script runs and captures the CLI dry-run and HTTP route-contract checks, providing the exact authored validation source.
- A valid signed fixture ran through `proof-admin topic install --dry-run`; its generated curl contains the placeholder and Bash rejected it with exit code 2, confirming the advertised command cannot be pasted and run.
- An authenticated full bundle received HTTP 422 while its extracted TopicDocument received HTTP 201 Created, confirming the endpoint contract and the required body shape.
- The authored validation script completed successfully with exit code 0 after capturing both runtime evidence files.
Greptile: the dry run printed `--data-binary @<extract .topic from …>`, which is not shell syntax, and substituting the bundle file would fail too — the publish route takes a `TopicDocument`, not the bundle envelope. So the advertised procedure could not actually be run. The plan now prints two real steps: 1. `jq '.topic' '<bundle>' > /tmp/proof-topic-document.json && proof-admin topic validate --bundle '<bundle>' --pin config/proof-pin.toml` 2. the `curl` against the existing route, reading that extracted file Paths are single-quoted for `sh` with embedded quotes escaped, so a path with a space cannot turn the printed command into a different one. The test asserts the extraction step passes `sh -n` and that no `<extract` placeholder remains. `PUBLISH_PATH` names the challenge-nested path the printed call uses. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Fixed in Paths are single-quoted for |
| "jq '.topic' {bundle} > /tmp/proof-topic-document.json && \ | ||
| proof-admin topic validate --bundle {bundle} --pin config/proof-pin.toml" | ||
| ); | ||
| let publish = format!( | ||
| "curl -sS -X POST \\\n \ | ||
| -H \"Authorization: Bearer $PROOF_ADMIN_TOKEN\" \\\n \ | ||
| -H 'content-type: application/json' \\\n \ | ||
| --data-binary @/tmp/proof-topic-document.json \\\n \ |
There was a problem hiding this comment.
The printed procedure extracts and validates a document, then later reads the predictable shared path /tmp/proof-topic-document.json for publication. Another local process can replace that file between the two steps, causing the authenticated request to publish a different topic than the operator validated. Use a private unique file with cleanup and bind validation to the exact bytes sent, or make extraction, validation, and publication a single operation.
Artifacts
- The authored Bash script executes the extraction, validation, and curl procedure against a local request-capture server and demonstrates the race.
- The control run validates and extracts SAFE without replacement, then the receiver gets SAFE.
- The replacement run validates SAFE, replaces the fixed temporary file with ATTACKER, and captures ATTACKER at the receiver.
- The CLI test confirms the current output uses the same fixed temporary path for extraction and curl.
Greptile: the printed procedure wrote the extracted document to a fixed
`/tmp/proof-topic-document.json` and published it in a later step. Any local
process could replace that file between validation and publication, so the
document the route received would not be the one the operator validated —
and the route is authenticated, which makes it a real substitution path.
Extraction and publication are now **one block**:
PROOF_TOPIC_DIR=$(mktemp -d) \
&& jq '.topic' '<bundle>' > "$PROOF_TOPIC_DIR/document.json" \
&& chmod 600 "$PROOF_TOPIC_DIR/document.json" \
&& curl … --data-binary @"$PROOF_TOPIC_DIR/document.json" <host>… \
&& rm -rf "$PROOF_TOPIC_DIR"
`mktemp -d` creates the directory 0700, the document is 0600, and the path
variable and the file it names cannot drift apart because they are the same
shell block. The test now asserts the private directory is used, that the
fixed shared path is gone, that extraction and publication are one block, and
that the block passes `sh -n`.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Fixed in The path variable and the file it names cannot drift apart because they are the same shell block. The test asserts the private directory is used, the fixed |
Owner defaults locked: first topic slug `tb4` with temporary alias
`tbench`; shared challenge DB with a `topic_id` discriminant (not a
per-topic schema); metal `topic install` is Owner-only, staging first.
Alias (migration `0024_proof_topic_alias.sql`):
- A row is `alias -> topic_id` and nothing else: no name, no pins, no
status, no document. Every one of those stays in `proof_topic_version`,
so an alias cannot drift from the topic it names, and retiring it is
deleting the row. This is not a second topic table.
- Shape CHECKs (slug, not-self) plus a `topic_id` index. `base_app` gets
DELETE here because retiring a temporary alias is the intended end
state — unlike the journal tables.
- `RlmStore::{put_alias, resolve_alias, aliases_for, delete_alias}`, in
both stores, pinned by the shared contract test. Resolution is
fail-closed on both sides: an alias whose topic has no published version
resolves to **nothing** (`None`), never to an empty document, and
`put_alias` refuses a topic that is not published yet.
- `proof-admin topic alias set|list|rm`, and `topic show` resolves an
alias to its canonical slug and says which topic it hit.
Owner-only metal gate:
- `--env metal` is refused (exit 2) without `--owner-metal-ack`, which
asserts that an Owner authorized the install and that staging passed for
that bundle. The refusal names the staging command to run first.
- `--env staging` is never gated. The plan output states which gate
applied (`owner_gate acknowledged` / `n/a (staging)`).
Docs record the locked defaults, including that `tbench` is two different
bindings: the temporary alias (retirable) and the runner registry's custom
id (the scoring binding, permanent).
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Locked Owner defaults in Alias map (migration
Owner-only metal gate: Still do not merge — Owner holds merge until staging A→Z. |
| async fn put_alias(&self, alias: &str, topic_id: &str) -> Result<(), StoreError> { | ||
| // Fail closed before the write: an alias must name a topic that | ||
| // actually has a published version, or resolution would hand back | ||
| // nothing and look like an unknown topic. | ||
| if self.latest_topic(topic_id).await?.is_none() { | ||
| return Err(StoreError::Malformed(format!( | ||
| "alias {alias:?} names topic {topic_id:?}, which has no published version" | ||
| ))); | ||
| } | ||
| sqlx::query( | ||
| "INSERT INTO proof_topic_alias (alias, topic_id) VALUES ($1, $2) \ | ||
| ON CONFLICT (alias) DO UPDATE SET topic_id = EXCLUDED.topic_id, \ | ||
| updated_at = now()", | ||
| ) | ||
| .bind(alias) | ||
| .bind(topic_id) | ||
| .execute(&self.pool) | ||
| .await?; | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
Canonical Topics Can Be Shadowed
When tb4 and another-topic are both published, this accepts put_alias("tb4", "another-topic") because it validates only the target topic. topic show tb4 resolves aliases before canonical IDs, so it returns another-topic’s signed document instead of the real tb4 document. Reject aliases that collide with an existing published canonical topic ID in both store implementations.
Artifacts
- Authored Rust test source publishes tb4 and another-topic, writes the colliding alias, and reproduces the same alias-first selection used by topic show; it makes the conflicting behavior executable.
- Executed baseline test with only canonical tb4 published; it shows no alias resolution and selection of tb4, establishing the expected canonical behavior.
- Executed collision test after publishing both topics and writing tb4 to another-topic; it shows the write succeeds and alias-first selection returns another-topic, confirming the defect.
Greptile: an alias could take another published topic's canonical slug, so `topic show <slug>` resolved through the alias and returned a *different* topic's signed document. An alias must never change what a published id means. Guarded in both directions, at three layers: - **Write** (`put_alias`): refuse an alias that is itself a published topic id, and refuse a topic that is not published (already the rule). - **Read** (`resolve_alias`): the Postgres lookup gains `AND NOT EXISTS (… WHERE s.topic_id = a.alias)`, so a row written before this guard still cannot shadow a canonical slug. The memory store returns `None` when the alias names a published topic. - **Schema** (`0024_proof_topic_alias.sql`): a `BEFORE INSERT OR UPDATE` trigger on `proof_topic_alias` *and* on `proof_topic_version`, because a writer going straight to SQL bypasses the store. `topic_id` alone is not unique in `proof_topic_version` (keyed `(topic_id, version)`), so this cannot be a UNIQUE constraint. Both trigger directions were probed against Postgres: claiming a published slug as an alias is refused, and publishing a topic whose id an existing alias claims is refused, with neither probe writing a row. Tests: the shared contract now covers the collision (refused at write, `None` at read) for both stores. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Fixed in
Both trigger directions were probed against Postgres: claiming a published slug as an alias is refused ( Still do not merge — Owner holds merge until staging A→Z. |
| IF EXISTS (SELECT 1 FROM proof_topic_version WHERE topic_id = NEW.alias) THEN | ||
| RAISE EXCEPTION 'alias % is already a published topic id', NEW.alias | ||
| USING ERRCODE = 'check_violation'; | ||
| END IF; | ||
| ELSE | ||
| IF EXISTS (SELECT 1 FROM proof_topic_alias WHERE alias = NEW.topic_id) THEN |
There was a problem hiding this comment.
Concurrent Slug Claims Bypass Guard
Concurrent publication of a topic and creation of an alias using the same slug can both pass their cross-table checks before either transaction commits. Both rows are then stored, leaving a canonical slug claimed as an alias. Resolution fails closed, but the unusable alias remains in alias listings until an operator removes it. Serialize claims for each slug across both trigger paths, such as with the same transaction-scoped advisory lock before checking and writing.
Artifacts
- Authored executable script that starts isolated PostgreSQL 16, applies the repository migrations, runs sequential and concurrent inserts, and queries the resulting invariant; it provides the exact executed source.
- Captured control-run output showing the alias insert is rejected after the topic is already committed, leaving one topic row and zero alias rows; the ordinary non-concurrent guard works.
- Captured concurrent-run output showing both transactions committed and the final query returned `invalid_shadow_state=true`; the cross-table trigger race is confirmed.
Greptile: the alias shadow guard was only atomic per statement. Under READ COMMITTED two concurrent claims for the same slug each see no row from the other, so **both commit** and the slug is shadowed after all. Reproduced against Postgres before fixing: an alias insert and a topic publish for the same slug, each holding an open transaction, both committed (1 alias row + 1 topic row for one slug). Fixed by serializing claims on the slug with a transaction-scoped advisory lock (`pg_advisory_xact_lock(hashtextextended(slug, 0))`), taken: - in the migration's trigger, before the `EXISTS` check, in both directions; - in `put_alias`, which is now **one transaction** — the lock is released at the end of its transaction, so issuing the lock and the insert as separate statements would drop it before the insert and reopen the race. Re-running the same race now yields one winner and one refusal (`topic race-slug is already claimed as an alias`); sequential paths are unchanged. The lock is keyed on the slug, so unrelated topics never contend, and it is released automatically at commit/rollback. Test: a `multi_thread` regression that races two claims for one slug and asserts at most one commits and that a published canonical slug never resolves through an alias. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Fixed in Reproduced first (before fixing): an alias insert and a topic publish for the same slug, each in an open transaction, both committed → Fix: claims on a slug are serialized with
Re-ran the same race: one winner, one refusal ( New Still do not merge — Owner holds merge until staging A→Z. |
| let mut second = doc.clone(); | ||
| second.id = "race-slug-v0".into(); | ||
| store.put_topic_version(&second).await.unwrap(); | ||
|
|
||
| // Race an alias claim against a *different* alias claim for the same slug. | ||
| // One must win; the loser must be refused, never silently applied. | ||
| let slug = "race-slug-v0"; | ||
| let a = store.clone(); | ||
| let b = store.clone(); | ||
| let (ra, rb) = tokio::join!( | ||
| async move { a.put_alias(slug, &doc.id).await }, | ||
| async move { b.put_alias(slug, "race-slug-v0").await }, | ||
| ); |
There was a problem hiding this comment.
The test publishes race-slug-v0 before starting its concurrent work, then races two alias writes. Both writes can be refused by the existing canonical-topic check before either reaches the alias insert, so this does not exercise an alias claim racing canonical publication for an initially unclaimed slug. Removing the publication trigger's shared lock could therefore restore the cross-table race while this regression test still passes. Race put_alias against put_topic_version for the same previously unclaimed slug and assert that exactly one claim commits.
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!
Artifacts
- The executable script prints the relevant test and trigger lines, records database availability, and runs the exact focused test; it provides the reproducible validation command.
- Captured output from executing the evidence script shows the pre-publication setup, the publication trigger lock branch, `DATABASE_URL=unset`, and the focused test passing through its skip path; it confirms runtime database validation was blocked.
- The command-generated diff captures the exact authored evidence script that was executed; it confirms the uploaded command content.
Greptile: the previous regression did not cover the cross-table race it was
named for — it pre-published the contested slug and only raced alias writes,
so it passed with or without the lock. Verified that directly: with the
advisory lock stripped from both layers, the old test still passed.
The replacement is a **deterministic** detector rather than a timing race. It
holds an alias insert open in one transaction, then tries to publish a topic
with that slug on another connection:
- with the shared transaction-scoped lock, the publish **blocks** until the
holder ends, which is the pass condition;
- without it, the publish sees no committed alias and succeeds immediately.
Confirmed it fails without the lock (`the publish did not block on the slug
claim … Ok(PgQueryResult { rows_affected: 1 })`) and passes with it, so a
future change that drops the lock cannot slip through.
`tokio` gains the `time` feature for the bounded wait.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Fixed in The replacement is a deterministic detector instead of a timing race. It holds an alias insert open in one transaction, then tries to publish a topic with that slug on another connection:
Confirmed both ways: it fails without the lock ( Still do not merge — Owner holds merge until staging A→Z. |
| // Releasing A lets B proceed; the invariant still holds because A's claim | ||
| // is then visible and B is refused. | ||
| holder.rollback().await.expect("rollback holder"); | ||
| drop(publisher); |
There was a problem hiding this comment.
The regression confirms that publication initially waits for the alias transaction, but it then rolls the alias back and drops the blocked publication without observing its result. If the advisory lock remains but the publisher's collision check is removed, the alias can commit and the publication can subsequently succeed, leaving both tables claiming the same slug while this test still passes. Commit the alias transaction, await the publication with a timeout, and assert that it is rejected.
Artifacts
- Authored shell script starts an ephemeral PostgreSQL database, runs the exact contract test before and after removing only the publisher-side collision check while retaining the advisory lock, and restores sources on exit. The script captures the proof runs.
- Executed the specified database-backed Rust contract test against migrated PostgreSQL with the original trigger. The test passed, establishing the baseline.
- Executed the same database-backed Rust contract test after removing the topic publisher's post-lock alias collision check but retaining the advisory lock. The test still passed, showing the regression escapes this test.
- Attempted to execute a corrected release-and-await test against the mutant, but compilation failed because the authored correction passed a transaction rather than the publish future to `timeout`. This is not conclusive behavioral proof.
- Captured wrapper output from the final mutation-script attempt. The wrapper exited 101 because the follow-up corrected detector did not compile.
Greptile: the regression verified the block but not the rejection. Removing the publisher-side collision check while keeping the lock left the test green, so a realistic path — both an alias and a topic claiming one slug — was not covered. The test now commits the held alias claim and asserts the waiting publish is **refused** by the collision check (`already claimed as an alias`), then reads back that exactly one claim exists and the refused publish wrote no topic row. Verified both ways: with the publisher-side check removed (lock retained) the test fails with `the publish was admitted after the alias claim committed, so both claimed the slug`; restored, it passes. So it now covers both layers — the lock that serializes the claim and the check that rejects the loser. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Fixed in The test now does both, because each covers a different layer:
Verified both ways: with the publisher-side check removed (lock retained) it fails with Still do not merge — Owner holds merge until staging A→Z. |
Owner reinforcement: topics are RLM-based and autonomous. The admin CLI only
asks the RLM to install and set the topic up — it is the hook that hands
control to the RLM Topic Install Bundle. Nothing topic-specific may be
compiled into challenge, gateway, orchestrator, or CLI code.
New `rlm` section on the bundle, owned by the RLM and opaque to Rust:
rules · migrations · apis · submission_format · scoring
The crate checks the section's *shape* (an object or array, bounded at
256 KiB) and carries it byte-for-byte into the install plan as `rlm_install`.
It does not know what a rule, a migration, an API, a submission format, or a
scoring function means — recognising them would mean this crate knows the
topic. Unknown content is not an error; a shape Rust has never seen is data.
The install plan now leads with the hand-off and names the RLM's own
lifecycle steps (`provision -> propose_rules -> baseline`, the existing
`TopicSetup` driver), then the publish call and host env. The CLI does not run
those steps and does not read into the section.
Two guard tests enforce the boundary, and both were verified to fail on the
thing they forbid:
- `no_topic_literal_appears_in_this_crates_logic` (bundle crate) and
`the_cli_does_not_bake_in_topic_behavior` (CLI) scan the non-test source
with comments stripped, so prose may explain the rule while a literal in a
`let`/`match`/`if` is caught. Injecting `topic_id == "tb4"` fails the CLI
guard; restoring passes.
- The CLI guard also forbids naming a metric, a task, a benchmark, or the
RLM section's own keys — the first draft enumerated
`rules/migrations/apis/submission_format/scoring` in its output and the
guard caught it.
Seed ids stay strings: `tb4` and `tbench` appear only in fixtures, operator
examples, and docs. The bundle crate's non-test source has no topic literal
at all, and its doc comment that *named* the seed ids was reworded rather
than exempted.
Docs: the RLM-owned boundary and the hand-off are recorded in PROOF.md,
COMPLETENESS.md, and ARCHITECTURE.md.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Owner reinforcement in New opaque The install plan leads with the hand-off: it names the RLM's own lifecycle steps ( Two guard tests enforce this, both verified to fail on what they forbid:
Seed ids stay strings: Still do not merge — Owner holds merge until staging A→Z. |
| pub struct RlmSection { | ||
| /// Anti-cheat rules the RLM ticks before any paid inference. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub rules: Option<serde_json::Value>, | ||
| /// SQL migrations the topic's install needs. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub migrations: Option<serde_json::Value>, | ||
| /// APIs the topic exposes. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub apis: Option<serde_json::Value>, | ||
| /// The topic's submission format. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub submission_format: Option<serde_json::Value>, | ||
| /// The topic's scoring definition. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub scoring: Option<serde_json::Value>, |
There was a problem hiding this comment.
The RLM hand-off is parsed into JSON values and then serialized again for rlm_install. An input with reordered properties and duplicate object keys therefore reaches the plan with reordered properties and only the final duplicate value retained. This breaks the promised byte-for-byte hand-off, so an RLM cannot receive the exact installation content that the operator submitted. Preserve the validated source bytes separately and use those bytes for the hand-off.
Artifacts
- The authored executable constructs a duplicate-key RLM JSON payload, invokes the real parser and planner, and asserts that source bytes are not preserved, proving the issue.
- The executed source-side capture records the supplied RLM bytes and shows parsing retained only the final duplicate key value, proving source information was lost.
- The executed plan-side capture records the serialized `rlm_install` with reconstructed key order and reports `BYTE_FOR_BYTE=false`, proving the RLM receives different bytes.
- The authored narrow test builds a valid bundle, runs both populated and explicit-null RLM cases, and asserts their install-plan outcomes; it reproduces the null-to-omission behavior.
- Executed the populated-RLM baseline test from /home/user/repo; parsing and shape validation succeeded and the generated plan retained rlm_install.
- Executed the valid bundle with rlm.rules explicitly set to null from /home/user/repo; parsing and shape validation succeeded while the generated plan omitted rlm_install.
| pub struct RlmSection { | ||
| /// Anti-cheat rules the RLM ticks before any paid inference. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub rules: Option<serde_json::Value>, | ||
| /// SQL migrations the topic's install needs. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub migrations: Option<serde_json::Value>, | ||
| /// APIs the topic exposes. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub apis: Option<serde_json::Value>, | ||
| /// The topic's submission format. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub submission_format: Option<serde_json::Value>, | ||
| /// The topic's scoring definition. Opaque. | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub scoring: Option<serde_json::Value>, |
There was a problem hiding this comment.
If a bundle explicitly sets an RLM field to null, deserialization turns it into an absent optional value. The shape validation accepts it and the generated plan omits rlm_install entirely. A malformed explicit value can therefore silently remove the RLM installation request rather than being rejected, leaving the topic without the setup the bundle attempted to provide. Track field presence independently or reject explicit null values during parsing.
Artifacts
- The authored executable constructs a duplicate-key RLM JSON payload, invokes the real parser and planner, and asserts that source bytes are not preserved, proving the issue.
- The executed source-side capture records the supplied RLM bytes and shows parsing retained only the final duplicate key value, proving source information was lost.
- The executed plan-side capture records the serialized `rlm_install` with reconstructed key order and reports `BYTE_FOR_BYTE=false`, proving the RLM receives different bytes.
- The authored narrow test builds a valid bundle, runs both populated and explicit-null RLM cases, and asserts their install-plan outcomes; it reproduces the null-to-omission behavior.
- Executed the populated-RLM baseline test from /home/user/repo; parsing and shape validation succeeded and the generated plan retained rlm_install.
- Executed the valid bundle with rlm.rules explicitly set to null from /home/user/repo; parsing and shape validation succeeded while the generated plan omitted rlm_install.
Greptile, two defects in the RLM hand-off:
1. **The bytes were reconstructed.** Each part was a `serde_json::Value`, so
parsing and re-serializing reordered keys, **collapsed duplicate keys**,
and normalised whitespace. The operator would sign off one bundle and the
RLM would receive another — the opposite of a verbatim hand-off.
2. **An explicit `null` was silently dropped.** `Option<Value>` folds
`"rules": null` into "absent", so a part the operator wrote vanished from
the install.
Each part is now `Box<RawValue>` — the exact JSON text, carried untouched.
`RawValue` has no `PartialEq`, so equality is defined on the raw bytes, which
is the honest comparison for a hand-off. A hand-written `Deserialize` (the
derive cannot express this) keeps explicit `null` as the text `null`, keeps
unknown keys an error, and preserves each part's bytes; `Serialize` writes
them straight through.
The shape check now refuses an explicit `null` (`RlmExplicitNull`) instead of
dropping it: a part the operator wrote is never silently discarded. The
object-or-array check reads the raw first byte, so it still holds without
parsing.
Verified through the real CLI: a bundle whose `scoring` is
`{"b": 1, "a": 2, "a": 3, "sp": "x y"}` reaches the plan's JSON output with
key order, duplicate keys, and inner whitespace all intact, and
`"rules": null` is refused with a message naming the field.
Tests: bundle 18 -> 20, with regressions for both defects (the byte-exactness
test splices its awkward part as *text*, so it does not itself round-trip
through a `Value`).
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Both RLM hand-off defects fixed in 1. Bytes were reconstructed. Each part was a 2. Explicit Verified through the real CLI, not just unit tests:
Bundle tests 18 → 20. The byte-exactness regression splices its awkward part as text, so the test does not itself round-trip through a Still do not merge — Owner holds merge until staging A→Z. |
|
@greptileai please re-review HEAD |
|
@greptileai Owner staging dry-run skeleton PASS on HEAD |
…claims Two deliverables for the Owner A→Z walkthrough. **Committed dry-run fixture** under `bins/proof-admin/tests/fixtures/`: - `tb4.install-bundle.json` — a Topic Install Bundle for slug `tb4` with alias `tbench`, target `staging`, carrying a signed `TopicDocument` and an illustrative RLM section. - `tb4.pin.toml` — the pin that document is checked against. - `README-dry-run.md` — the exact `validate` / `install --dry-run` commands, the migrate note, and what the fixture is *not*. `--pin` is required and the README says so up front: the fixture is signed with the test mini-secret, so the default `config/proof-pin.toml` (the real trust root) correctly refuses it. Both documented commands are exercised on every test run by `the_committed_dry_run_fixture_still_validates_and_plans`, so a schema change that breaks the fixture fails CI instead of reaching the Owner. `regenerate_dry_run_fixture` (env-gated) regenerates both files, so they cannot be hand-edited out of sync with their signature. **Corrected schema claims.** The docs said "no new table, no behavior change" while the same row cited migration `0024` — contradictory, and wrong at HEAD: `0024_proof_topic_alias.sql` **does** ship. Both `docs/PROOF.md` and `docs/COMPLETENESS.md` now state it exactly: `0024` only; adds `proof_topic_alias` plus the `BEFORE INSERT`/`UPDATE` trigger pair that fails closed when an alias would shadow a published slug — a publish-path integrity guard, not scoring math; no `ALTER`/`DROP`; `0020` tables keep their columns, keys, and grants. No P0 scope expansion: no new route, no scoring change, a real install is still exit 3. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
The first fixture-generation run used a relative dump path and wrote a second copy under `bins/proof-admin/bins/proof-admin/tests/fixtures/`. The committed fixture lives at `bins/proof-admin/tests/fixtures/`; this removes the stray tree. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Two corrections to `README-dry-run.md`, both found by running the commands rather than reading them: - The package is **`proof-admin-bin`**, not `proof-admin` — this repo names binary packages with a `-bin` suffix (`trustroot-bin` → `trustroot`, `validator-bin` → `validator`), so `cargo run -p proof-admin` fails with "package(s) proof-admin not found". The README now gives `cargo run -p proof-admin-bin -- …` and notes the package-agnostic `cargo run --bin proof-admin -- …`. - `--pin` is required and is now called out before the commands, not after: the fixture is signed with the test mini-secret, so the default `config/proof-pin.toml` (the real trust root) correctly refuses it. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Two Owner/Arch deliverables in 1. Corrected the schema claims. The body and docs said "no new table, no behavior change" while the same row cited
2. Committed dry-run fixture at Two things I found by running the commands rather than reading them, both now documented:
Also removed a stray nested directory ( Still do not merge — Owner holds merge until staging A→Z. |
| flow: | ||
|
|
||
| ```bash | ||
| cargo sqlx migrate run # or: restart the service with BASE_DATABASE_URL set |
There was a problem hiding this comment.
The staging walkthrough says there is no manual SQLx migration step, but then presents cargo sqlx migrate run as one. A clean checkout does not provide the separately installed sqlx-cli subcommand, so this command exits before applying the migration. Remove the standalone command and direct operators to the established service-restart path, or document the required CLI installation and database connection configuration. This is non-blocking, but it can interrupt the Owner staging procedure.
Artifacts
- The authored script models a clean checkout with an empty Cargo home, checks workspace manifests for sqlx-cli, and runs the exact documented command; it provides the reproducible validation procedure.
- The executed focused validation shows no sqlx-cli manifest entry and that `cargo sqlx migrate run` exits 101 because Cargo has no sqlx subcommand; the documented command cannot run in a clean checkout.
Greptile, two findings, both reproduced before fixing.
**The enclosing `rlm` object was still rebuilt.** Each named part was raw, but
the object around them was deserialized into fields and re-serialized in
`RLM_KEYS` order, so the object's own key order did not survive. Reproduced
through the real CLI: submitting `{"scoring": …, "rules": …, "apis": …}` gave
the plan `["rules", "apis", "scoring"]`.
`RlmSection` now holds **one** `Box<RawValue>` — the whole object, verbatim.
`validate_shape` parses a *copy* to check the shape (an object; each named
part an object or array; no explicit `null`; bounded), and the hand-off is
always the original bytes. Checking is not transforming. A key this crate has
never heard of is no longer refused either: it is the RLM's business, which is
what "opaque" should have meant from the start. Re-verified: the same input
now reaches the plan as `["scoring", "rules", "apis"]`.
**`cargo sqlx migrate run` is not a command in this repo.** `sqlx-cli` is not
a workspace dependency and is absent from a clean checkout (`error: no such
command: sqlx`). The README no longer presents it; migrations are embedded in
the `db` crate and applied on boot where `BASE_DATABASE_URL` is set, so the
staging path is the service restart.
Tests: bundle 20, reworked for the single raw object. The byte-preservation
test now asserts at the **object** level (keys out of canonical order, a
duplicate key, inner whitespace) and splices its input as text, so it cannot
itself pass through the lossy path it guards.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@greptileai review Both findings fixed in 1. The enclosing
2. Tests: bundle 20, reworked for the single raw object. The byte-preservation test now asserts at the object level (keys out of canonical order, a duplicate key, inner whitespace) and splices its input as text, so the test cannot itself travel the lossy path it guards. Still do not merge — Owner holds merge until staging A→Z, and this awaits human GO. |
| let parsed: serde_json::Map<String, serde_json::Value> = | ||
| serde_json::from_str(text).map_err(|e| BundleError::Parse(e.to_string()))?; | ||
| for (key, value) in &parsed { |
There was a problem hiding this comment.
Duplicate RLM Keys Bypass Validation
validate_shape converts the preserved RLM object into a serde_json::Map, which collapses duplicate keys before checking them. A bundle such as "rules": null, "rules": [] can therefore pass the no-null and object-or-array checks even though the exact content handed to the RLM still contains the invalid occurrence. Validate every occurrence or reject duplicate named keys so malformed RLM content cannot bypass bundle acceptance.
Summary
Dynamic-topics P0 — remapped onto the existing admin publish path per the
Arch §11 hardcoding-inventory delta: cortex scoring and its DB are already
multi-topic, so P0 wraps what exists instead of inventing a parallel registry.
This PR must NOT be merged by the agent. Owner holds merge until the
staging A→Z run.
What this is now
A topic already has one home: the operator-signed
TopicDocumentpublished through
POST /v1/admin/proof/topicsand persisted inproof_topic_version(migration0020). Its bindings are signed topic datatoo —
constraints.paramscarries the in-guest runner and its pinned packdigest. This PR adds the missing procedure, not a second registry.
xtask proof-topic(sr25519,base-proof-topic-v1)TopicDocument::validate+verify_signature— the same pair the admin route runsPOST /v1/admin/proof/topics(operator bearer)proof_topic_versionviaRlmStore::put_topic_versionPROOF_VM_RUNNER_CUSTOM_IDS+ pack underPROOF_VM_AGENT_EXPERIMENT_PACK_DIRChanges
RlmStore::latest_topics— a read-only registry view over the sameproof_topic_versionrows (DISTINCT ON (topic_id) … ORDER BY version DESC), in both the Postgres and memory stores, pinned by the sharedcontract test. No new table, no new column, no migration.
crates/proof-topic-bundle— the bundle carries the signed documentverbatim plus a
hostblock that must agree with it. Runner / pack /custom id are read from the document's own
constraints.paramsviaproof-experiment::ExperimentBinding, so every binding has exactly onecopy. A host expectation that contradicts the document is a reject, never
an override.
bins/proof-admin—topic validateruns the same acceptance thepublish route runs;
install --dry-runprints that publish call plus thehost env;
topic list/topic showread the registry view.docs/PROOF.md§ Topic install bundles rewritten,docs/COMPLETENESS.mdrow,docs/ARCHITECTURE.mdbin entry.Removed (the parallel table this slice had added)
Migration
0024_proof_topics.sql/ theproof_topictable,db::topics, andits integration tests — they duplicated
proof_topic_version, which isalready the durable topic table. A second table would have been a second
source of truth for the same facts.
KEPT, as instructed
rlm_fc_in_guest_harbor.proof-experimentanti-hardcode locks (untouched).NOT P0 (per the delta)
Front-end debrand and the
harbor-trials-v1rename are P1; neither istouched here.
Topics are RLM-based — the CLI only hands control to the RLM
The admin CLI's job is to ask the topic's RLM to install and set itself
up. The bundle carries an
rlmsection that owns everythingtopic-specific, and Rust never interprets it:
rulesmigrationsapissubmission_formatscoringThe bundle crate checks the section's shape only (object or array,
bounded at 256 KiB) and carries it byte-for-byte into the plan as
rlm_install. It does not know what a rule, migration, API, submissionformat, or scoring function means — recognising them would mean the crate
knows the topic. Unknown content is not an error.
The install plan leads with the hand-off and names the RLM's own lifecycle
steps (
provision -> propose_rules -> baseline, the existingTopicSetupdriver), then the publish call and host env. The CLI does not run those steps
and does not read into the section.
Two guard tests enforce the boundary, both verified to fail on what they
forbid:
no_topic_literal_appears_in_this_crates_logic(bundle) andthe_cli_does_not_bake_in_topic_behavior(CLI) scan the non-test sourcewith comments stripped — prose may explain the rule, a literal in a
let/match/ifis caught. Injectingtopic_id == "tb4"fails the CLIguard.
section's own keys. The first draft enumerated
rules/migrations/apis/submission_format/scoringin its output and theguard caught it — so the CLI now says the section is handed over without
listing what is in it.
Seed ids stay strings:
tb4andtbenchappear only in fixtures, operatorexamples, and docs. The bundle crate's non-test source has no topic literal
at all.
Owner defaults (locked)
tb4idtbenchproof_topic_alias(migration0024), rowtbench → tb4topic_iddiscriminantproof_topic_version— no per-topic schema--owner-metal-ackgatetbenchmetric.custom_idAlias map (
0024_proof_topic_alias.sql): a row isalias → topic_idand nothing else — no name, no pins, no status, no document. Everything else
stays in
proof_topic_version, so an alias cannot drift from the topic itnames and retiring it is just deleting the row. This is deliberately not a
second topic table. Resolution is fail-closed in both stores: an alias whose
topic has no published version resolves to nothing, never to an empty
document, and
put_aliasrefuses an unpublished topic outright.base_appgets DELETE here (unlike the journal tables) because retiring a temporary
alias is the intended end state.
tbenchis two different bindings and they are not the same mapping: thetemporary alias (retire with
topic alias rm tbenchonce links move)and the runner registry's custom id (
PROOF_VM_RUNNER_CUSTOM_IDS=tbench,the scoring binding, permanent).
Owner-only metal gate:
--env metalis refused (exit 2) without--owner-metal-ack, which asserts an Owner authorized the install and thatstaging passed for that bundle. The refusal names the staging command to run
first.
--env stagingis never gated. The gate is an operator assertion, nota verified precondition — it exists so a live target cannot be reached by a
default or a copy-pasted staging command.
P0 scope — deliberately not done
and nothing on a scoring path changed.
latest_topicsis a read-only view.operator bearer, which stays on the host.
install --dry-runprints thecall for an operator to run.
topic enable/disable/sealare stubs (exit 3) pointing at the realanswer: a topic's lifecycle is the signed document's
status, so re-signand re-publish.
HOLD — do not merge
Owner holds the merge until the staging A→Z run. Left open unmerged even
though CI is green.
Greptile
Test plan
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspacecargo deny checkcargo run -p xtask -- loc-cap(bundle 360,proof-admin468,proof-rlm-store797 — under 1500)cargo run -p xtask -- consensus-lintcargo run -p xtask -- spec-checkcargo run -p xtask -- design-checkcargo run -p xtask -- external-docs-checkbash deploy/scripts/assert-compose-matrix.shlatest_topics+ alias contract in both stores, plustwo DB-gated CLI tests (a document persisted through the scoring path's own
PgRlmStoreread back viatopic list/show, and thetbench → tb4aliasresolving end to end)
validate(accepts the Arch-default bundle,refuses a wrong-key signature and a contradicting host block),
install --dry-run(prints the runnable publish block + 4 host env lines), themetal gate refusing without
--owner-metal-ackand resolving with it, thealias set|list|rmround trip, stubs exit 3bins/proof-admin/tests/fixtures/tb4.install-bundle.json+tb4.pin.toml+README-dry-run.md. Both documented commands are run onevery test run by
the_committed_dry_run_fixture_still_validates_and_plans.Two failures in my sandbox are pre-existing and unrelated (both reproduce
on a clean tree):
crates/db/tests/gateway_store.rs(3 tests, fail against a local Postgres 18that CI does not have) and
proof-challenge-bin::seed_pf_allocator_refuses_boot_when_a_topic_dir_cannot_be_read(relies on
chmod 000blocking reads, which does not hold as root; verifiedpassing as a non-root user, which is how CI runs).
Risk
Low. Two things ship, both bounded and both stated here rather than
implied:
Schema:
crates/db/migrations/0024_proof_topic_alias.sqlonly. Itadds
proof_topic_alias(alias → topic_id+ atopic_idindex) and aBEFORE INSERT/UPDATEtrigger pair (proof_topic_alias_no_shadow,proof_topic_version_no_shadow) that makes an alias collision with apublished slug fail closed in both directions. That trigger sits on the
publish path —
INSERTintoproof_topic_version— and is apublish-path integrity guard, not scoring math: it cannot change a score,
a payout, or a sealed vector. The migration does not
ALTERorDROPanything; the
0020tables keep their columns, keys, and grants.Code: no route change, no scoring change, no allocator change, no
BASE_*/ deployed-path / crypto-domain-tag change. The CLI reads existingproof_topic_versionrows and prints an operator procedure; a real install isstill exit 3.
Earlier revisions of this body claimed "no schema change" and described a
0024topics table that was removed in the remap. Both were wrong for HEADand are corrected here and in
docs/PROOF.md/docs/COMPLETENESS.md.Naming
I did not rename
BASE_*environment variables, deployed host paths,GHCR
baseintelligence/basepackage names, orbase-*-v1cryptographicdomain tags.