Skip to content

feat(vault): record a registry handle by hand - #487

Merged
LKSNDRTMLKV merged 13 commits into
mainfrom
feat/registry-record-handle
Nov 4, 2026
Merged

LKSNDRTMLKV merged 13 commits into
mainfrom
feat/registry-record-handle

Conversation

@LKSNDRTMLKV

@LKSNDRTMLKV LKSNDRTMLKV commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Summary

A registration the drain cannot settle by itself stays submitted and waits for a person, and until now it said nothing about why and nothing could settle it. This makes the row say why, and adds the one write that settles it: POST /vault/api/v1/dpp/{dppId}/registry/handle, with odal registry handle <passport-id> <correlation-id>.

Stacked on #484 (base feat/registry-recovery-and-typed-outcomes). It uses confirm_submission and SubmissionConfirmation from odal-node/dpp-core#490, released in dpp-core 0.23.0. Brought up to date on 2026-11-04: it took #484's branch, which now carries main and the published 0.23.0, so it builds against crates.io and needs no local core. Its migration is renumbered from 0043 to 0045, because main took 0043 and 0044 in the meantime; an unreleased migration has been applied nowhere, so the rename is safe. The runbook paragraph it rewrites had moved under Monitoring (#515) and gained #484's lone-candidate case, so the change was made there, and the outbox test double #561 added learned the two new methods.

Related issue

Closes #485. (GitHub closes it when this reaches main, not while it is stacked.)

Changes

The row says why. message on a submitted row now holds the reason it is waiting: the candidates of an ambiguous lookup, that the handle was not found and whether all of the task list was read, that the adapter cannot look a submission up, that the row's payload cannot say what was submitted, or a submission state this node has no name for. GET /vault/api/v1/dpp/{dppId}/registry shows it. A new outbox method, mark_submitted_noting, backs the row off and counts the attempt exactly as mark_submitted does and keeps the message; mark_submitted still clears it, so a row that moves on stops carrying a reason that no longer applies.

A person can record the handle. POST /vault/api/v1/dpp/{dppId}/registry/handle, admin scope, body { "correlationId": "…" }.

  • It does not take the operator's word for it. The submission the handle names is read back from the registry through core's confirm_submission, and the request is refused with 422 unless that submission reports the unique product identifier this passport was submitted under. The detail says what the registry did show: other passports' identifiers, none, a submission still being checked, no such handle among the tasks read, or an adapter that cannot read a submission.
  • One conditional write. record_handle is a single UPDATE … WHERE status = 'submitted' AND registry_id IS NULL/'', so the check and the write cannot be pulled apart by another writer. A handle the registry gave is never overwritten (409), a row already ruled on is not touched (409), and a row the registry has not accepted yet has no handle to record (409).
  • The attempts start again. The new poll is on a handle somebody checked, so the count that earned the stall must not keep the row reported as stalled while it is being polled. The message is cleared and the row is due at once.
  • The ruling is the drain's. The route records the handle and nothing else; the drain polls by it on its next pass and applies the registry's ruling. This differs from what Registry rows that need a person give no reason and cannot be settled #485 proposed ("a found submission takes its ruling in the same step") on purpose: what a registration, a refusal or an unnamed state does to a row is decided in one place, and applying it here as well would be a second copy of that rule. The response still carries what the registry reported when the handle was checked, so the answer is known now.
  • Idempotent without a key. Recording the handle a row already holds is a 200 with recorded: false that changes nothing, asks the registry nothing and writes the trail nothing, so a retry after a lost response does not fail on its own success. A different handle is a 409. So it is not in the idempotency policy table, and the route documents why.
  • Audited. registry_handle_recorded, with who recorded which handle and what the registry reported. Migration 0045 widens the audit action CHECK; no table, grant or hash-chain change.
  • odal registry handle <passport-id> <correlation-id>. odal registry [id] keeps its old form (args_conflicts_with_subcommands), and a test pins that, because the runbook and the row messages name it.

The registry adapter implements confirm_submission: it finds the one task the handle names, and for a finished one compares the identifiers its result reports with the one the passport was submitted under. It reads one result, not every result in the list.

Docs. The API description (path, request, response and SubmissionReport schemas, bundles regenerated), the runbook paragraph that said there was no node-side way to do this, and CHANGELOG.md. The "no way to record a handle by hand" item in #484's second-opinion list is now this PR.

Decisions I would like a second opinion on

  • Admin scope. The write overrides the drain's own judgement about which submission is a passport's, so it is an admin's act. A narrower scope would be a new scope for one route.
  • No "give this one up" write. A row whose submission was never accepted could be marked abandoned from here. That is a different and more dangerous write (it ends a legal registration obligation's attempt), so it is not in this PR.
  • 501 for a node with no registry queues, and 502 for an unreadable registry. 501 is the precedent the ruleset route sets for a feature that is absent; 502 because the registry is an upstream, and the request is safe to retry. The 501 has no integration test, because every harness wires the queues; a unit test pins the status of every refusal.

Checklist

  • Tests added or updated for new behaviour: the adapter against a mock registry (match, mismatch, none reported, a near-miss identifier, still checking, unlisted with and without the whole list read, one result read, an unreachable dashboard), the outbox against Postgres (record_handle only on a submitted row with no handle, the whole way from "needs a person" to registered, the reason kept and cleared), the route over HTTP (recorded and audited, replay, every unconfirmed answer leaves the row and the trail untouched, 404/409/422 told apart, a held handle never overwritten, a 502, admin only, malformed identifiers), and the CLI
  • The new tests were shown to bite: the conditional write, the confirmation check and the identifier comparison were each broken in turn and a test went red
  • just lint passes locally
  • just fmt applied
  • just check exits 0 against the published dpp-core 0.23.0 (1682 tests), and the Docker suites it touches pass: the DAL registry and state-change suites (31), the handle route (9), and the outbox, OpenAPI contract and operator-path suites (54), which upgrade the v0.14.0 and v0.15.0 fixtures through migration 0045. just openapi-check passes and the bundle regenerates unchanged. The handle route test published under a VAT number, which chore!: repin dpp-core 0.23.0 #561 now refuses, and publishes under an LEI (98c84bc9)
  • No println!/eprintln!/dbg! in service-crate src/
  • No secrets, credentials, or .env files in the diff
  • Docs updated if a public API, endpoint, or CLI command changed
  • If the DB schema changed: migration 0045 widens the audit action CHECK only; no table or grant changes

@LKSNDRTMLKV
LKSNDRTMLKV force-pushed the feat/registry-recovery-and-typed-outcomes branch from 470295e to 8165020 Compare October 20, 2026 22:32
@LKSNDRTMLKV
LKSNDRTMLKV force-pushed the feat/registry-record-handle branch from be6483e to 1420f86 Compare October 20, 2026 22:32
@LKSNDRTMLKV
LKSNDRTMLKV marked this pull request as ready for review November 4, 2026 01:21
@LKSNDRTMLKV LKSNDRTMLKV added enhancement New feature or request epic/registry EU registry loop: registration, transfers, handles, the local stand-in review-ready Opt this PR into a CodeRabbit review labels Nov 4, 2026
@LKSNDRTMLKV

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 52 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 986b811 and 71260c7.

Files ignored due to path filters: 2
  • api/openapi.bundled.json is excluded by !api/openapi.bundled.json
  • api/openapi.bundled.yaml is excluded by !api/openapi.bundled.yaml
Files selected for processing: 32
  • CHANGELOG.md
  • api/components/schemas/registry/RecordRegistryHandleRequest.yaml
  • api/components/schemas/registry/RecordRegistryHandleResponse.yaml
  • api/components/schemas/registry/RegistrationView.yaml
  • api/components/schemas/registry/SubmissionReport.yaml
  • api/openapi.yaml
  • api/paths/vault/vault_api_v1_dpp_{dppId}_registry_handle.yaml
  • cli/src/cli_args.rs
  • cli/src/commands/registry.rs
  • cli/src/core/registry.rs
  • cli/src/dispatch.rs
  • cli/src/main.rs
  • cli/src/stateless/render.rs
  • cli/tests/error_rendering.rs
  • cli/tests/lifecycle_routes.rs
  • crates/dpp-dal/src/pg/repo_registry_sync.rs
  • crates/dpp-node/src/infra/registry/mapping.rs
  • crates/dpp-node/src/infra/registry/tests.rs
  • crates/dpp-node/src/infra/registry_drain.rs
  • crates/dpp-node/src/infra/transfer_drain.rs
  • crates/dpp-node/tests/openapi_contract.rs
  • crates/dpp-node/tests/registry_outbox.rs
  • crates/dpp-types/src/registry_sync.rs
  • crates/dpp-vault/src/domain/service/mod.rs
  • crates/dpp-vault/src/domain/service/registry_handle.rs
  • crates/dpp-vault/src/handlers/mod.rs
  • crates/dpp-vault/src/handlers/registry_handle.rs
  • crates/dpp-vault/src/router.rs
  • crates/dpp-vault/tests/helpers/mod.rs
  • crates/dpp-vault/tests/registry_handle_route.rs
  • docs/ops/PRODUCTION-RUNBOOK.md
  • ops/pg/0045_registry_handle_audit_action.sql
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@LKSNDRTMLKV
LKSNDRTMLKV changed the base branch from feat/registry-recovery-and-typed-outcomes to main November 4, 2026 15:23
@LKSNDRTMLKV
LKSNDRTMLKV merged commit d9ec508 into main Nov 4, 2026
51 of 52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request epic/registry EU registry loop: registration, transfers, handles, the local stand-in review-ready Opt this PR into a CodeRabbit review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Registry rows that need a person give no reason and cannot be settled

1 participant