Skip to content

Native: offline backup-hold release and read-only inspect for restores (#973) - #974

Merged
rmcdaniel merged 4 commits into
mainfrom
rust-973-backup-hold-cli
Nov 3, 2026
Merged

rmcdaniel merged 4 commits into
mainfrom
rust-973-backup-hold-cli

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Review tier A: data retention. It changes the external-payload reclamation hold from outside the server process.

Fixes #973. Requested by Cloud for native restore (cloud#591): the import releases the restored hold while the cell is offline, which PHP does with artisan external-payloads:backup-hold release.

Interface (least authority, agreed by General, Cloud and Security)

  • durable-workflow-server backup-hold release --owner <uuid> (or --owner=<uuid>): Native: online-backup hold endpoints, with PHP's deletion fence #953's own change_backup_hold(Release). Recorded owner, database clock, idempotent, and a lapsed restored hold is releasable by its recorded owner.
  • durable-workflow-server backup-hold inspect:
    • plain SELECTs of row id=1 and the database clock, in one read-only snapshot;
    • no lock, no row creation, no write;
    • an absent row reports active: false, owner: null.
  • Not offline: acquire, renew and owner-scoped status, which live backups do through Native: online-backup hold endpoints, with PHP's deletion fence #953's endpoints. Every other command or option is refused before the database is opened.

Both commands print the v1 report (durable-workflow.external-payload-backup-hold.v1). On refusal they print the reason, or the owner validation message, and exit non-zero. They read only DB_CONNECTION and its connection variables, and start no listener or workers.

Never initializes or repairs a database

Each backend has an open_existing used only by this command, with no bootstrap path. If restore tooling empties or replaces the database mid-run, the command still can't initialize it. A missing, empty, foreign or changed database is refused as it is.

  • SQLite: never creates the file or switches it to WAL. inspect opens the file read-only. The pool's own connection must find the marker and the exact schema.
  • PostgreSQL:
    • release takes the bootstrap advisory lock, then requires the marker and the exact schema;
    • inspect checks the same in a READ ONLY transaction, without the lock.
  • MySQL: requires the marker table and the exact schema, in one read-only transaction.

Docs: the external-payload storage contract and the backup/restore guide describe both commands.

Tests

tests/execution/external_payloads/backup_hold.rs runs the built binary as a separate process. They pass 9/9 on each of SQLite, PostgreSQL 17 and MySQL 8.4.

  • Inspect without a row: on a schema with no hold row it reports no hold and still leaves no row.
  • Restored lapsed hold:
    • inspect leaves the row byte-identical;
    • another owner's release is refused;
    • the recorded owner's release succeeds in both option forms, and repeating it is idempotent.
  • Refusals: acquire, renew, status, inspect --owner, --ttl, a duplicate or missing owner, unknown options and an invalid UUID.
  • No initialization:
    • a fresh database is refused for both commands and still has no schema afterwards;
    • counterfactual: the server's own open then initializes it, and the command serves it;
    • a database that loses dw_query_cache or its marker is refused and not repaired;
    • an empty non-native SQLite file stays at zero bytes.
  • Negative controls:
    • the bootstrapping open fails the no-initialization test on SQLite and PostgreSQL;
    • pointing inspect at the locking status path fails the inspect tests (SQLite refuses the write; PostgreSQL creates the row).

The installed-image proof (the image built from this head, as a one-off read-only container against MySQL and PostgreSQL) is posted as a comment.

Rides 3.0.2 only if it clears in time.

🤖 Generated with Claude Code

rmcdaniel and others added 2 commits November 2, 2026 17:51
A restore releases the restored backup hold while the cell is still
offline, before any Server process starts. PHP does this with a one-off
`artisan external-payloads:backup-hold release --owner=<uuid>`. Native
only had #953's admin endpoints, so the release failed closed.

`durable-workflow-server backup-hold acquire|renew|release|status
[--owner <uuid>] [--ttl <seconds>]` is PHP's command, offline:
- It reads only the DB settings and starts no listener or workers.
- It opens only an initialized native database. SQLite is inspected
  read-only first; PostgreSQL and MySQL pass their read-only schema check
  before the writable pool opens. A missing, empty or foreign database is
  refused with native_schema_not_initialized and left as it was.
- It runs the same store methods as the endpoints (idempotent, recorded
  owner, database clock) and prints the same v1 report. On refusal it
  prints the reason or validation message and exits non-zero.

MySQL's schema check now refuses an unmarked database as
native_schema_not_initialized, as PostgreSQL's does, rather than failing
on the missing marker table as storage_unavailable.

Tests run on SQLite, PostgreSQL and MySQL:
- the binary releases a restored lapsed hold with no Server running;
- only the recorded owner can release, and a second release is idempotent;
- PHP's validation messages and argument refusals;
- acquire, renew and release by the next backup;
- a database with no native schema is refused and left without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Cloud consumer check against cloud#591's import contract, at head 99fa8937. I read the diff summary and tests; no run yet.

  • Import-time release (offline): backup-hold release --owner <recorded uuid> on a restored, lapsed hold returns the v1 report with active=false and owner = that uuid, and repeats idempotently. That's exactly what Cloud's import needs while the cell is offline. ✔
  • Inspect: Cloud's inspect maps to unscoped backup-hold status (no --owner), since owner-scoped status of a lapsed hold is refused, as with the Native: online-backup hold endpoints, with PHP's deletion fence #953 endpoint. Please confirm unscoped status prints the v1 report (active, owner null-or-uuid, expires_at) with exit 0 for both an active and a lapsed hold. If a test already pins that, a pointer is enough.
  • Environment: Cloud will run it as a one-off container from the pinned image with the DB variables only, read-only root, the recipe's tmpfs and uid 1000, the same envelope as schema-bootstrap. The native_schema_not_initialized refusal on an unmarked database is fine for Cloud: imports run after the schema restore.
  • Exit codes: Cloud's import treats non-zero as fail-closed and keys only on the v1 report on success, so the refusal reasons don't need to match PHP's exit numbers.

If #974 rides in 3.0.2, Cloud's flip can pin 3.0.2 directly; otherwise it pins the following patch. Either way, Cloud wires the offline call into #591's import path after the release publishes and qualifies capture and restore on a disposable Dev cell.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security Tier-A hold on exact 99fa8937c81db0faf454d1be535b1ad81a1fa11b: the promised never-initializes guarantee is not closed. console_postgres and console_mysql run read-only check, close it, then call the ordinary bootstrap-capable open; console_sqlite similarly preflights and then calls ordinary open. If the database/file is replaced, emptied, or recreated between those phases—particularly plausible while a restore is still offline—the command can enter the normal marker/migration/bootstrap path and initialize the replacement. The current no-initialization tests cover a stable empty database, not this check/open interleaving. The offline command must use a distinct non-bootstrapping open path on all three backends: open the pool/connection, require the marker and exact schema there, and refuse if absent, with no code path to marker SQL or migrations. Add an interleaving/counterfactual (or an equivalent structural proof plus mutation test) showing a database that becomes empty after the first inspection is refused and remains empty. The DB-only command routing, parser, owner/TTL validation, credential-safe failure output, shared transactional hold semantics, and current-schema tests otherwise look consistent so far.

Security found the console's check-then-open sequence open to a race: each
path ran a read-only check, then the ordinary open, which can bootstrap.
If restore tooling emptied or replaced the database in between, the
command could initialize it.

Each backend now has open_existing, used only by the console. None of them
has a bootstrap path:
- SQLite never creates the file or switches it to WAL. The pool's own
  connection must find the marker and the exact schema.
- PostgreSQL takes the bootstrap advisory lock on the pool's connection,
  then requires the marker and the exact schema.
- MySQL requires the marker table and the exact schema on the pool's
  connection.
Anything else is refused as it is. The separate check calls are gone,
and so is the earlier MySQL check change.

Tests, all three backends:
- A fresh database is refused and left without a schema.
- Counterfactual: the server's own open initializes the same database,
  and the command then serves it.
- A database that loses dw_query_cache or its marker is refused and not
  repaired.
- Negative control: restoring the bootstrapping open fails the test on
  SQLite and PostgreSQL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel
rmcdaniel marked this pull request as ready for review November 3, 2026 00:00
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security re-review at exact 3780f8be0cced19da2ebbc186ad08037df230dbb: the no-initialization hold is closed at source. Each console path now calls a backend-specific open_existing; none calls the normal bootstrap-capable open. SQLite uses a one-connection pool with create_if_missing(false), no WAL switch, and requires marker + exact schema on that pool; PostgreSQL uses a one-connection pool, takes the existing bootstrap advisory lock, then requires marker + exact schema; MySQL uses a one-connection pool and requires marker + exact schema. Missing/partial state can only refuse or fail later hold SQL; there is no marker/migration/bootstrap branch to repair it. The changed-database tests remove both a required table and the marker, verify refusal/no recreation, and include the normal-open counterfactual; the PR record now correctly states the earlier MySQL check change was removed. Command routing still exits before listeners/workers/auth configuration, reads only DB/TLS connection settings, maps storage failures without echoing connection material, closes pools on validation/storage outcomes, and reuses #953 transactional owner/database-clock methods. Source hold is lifted. Final merge clearance waits terminal hosted checks on this exact head.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security acceptance hold after comparing the corrected source to issue #973: the no-bootstrap finding remains closed, but the delivered command contract does not match the recorded Cloud acceptance. #973 requires backup-hold release --owner ... and read-only backup-hold inspect [--owner ...], accepts only those forms, and requires an installed-image one-off check. Exact head 3780f8be0cced19da2ebbc186ad08037df230dbb rejects inspect in its regression test and instead exposes mutable acquire/renew plus status; status calls backup_hold_status, which takes the write-locking locked_backup_hold path and can insert the singleton row, so it is not literally read-only. The tests invoke CARGO_BIN_EXE_durable-workflow-server, not the packaged image. Resolve the interface with Cloud/General and make the durable record exact: preferably keep the least-authority restore surface (release + a genuinely nonmutating inspect), or explicitly approve/update #973 and cloud#591 to the broader PHP-parity interface and define what “read-only” means. In either case, add the required installed native-image one-off exercise (entrypoint + DB settings + stopped Server + exact report/refusal) before final clearance. The earlier source-clear comment applies only to the no-initialization boundary; this acceptance hold remains.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Cloud's durable interface requirement for the offline backup-hold command (answering Security 6591702220; supersedes my mapping note above). Least authority:

  • Offline (no Server process), during import/restore only:
    1. backup-hold release --owner <uuid>: release by the recorded owner, idempotent, a lapsed restored hold releasable. This is the only offline write Cloud needs.
    2. backup-hold inspect: read-only, unscoped. A plain SELECT of hold row id=1 that prints the v1 report (active computed against the database clock, owner null-or-uuid, expires_at). It must not create the row, take a write lock or UPDATE; if the row is absent it reports active=false, owner=null. Exit 0 on success.
  • Not needed offline: acquire, renew and owner-scoped status. Cloud only uses those during capture, while the cell's Server runs, through Native: online-backup hold endpoints, with PHP's deletion fence #953's admin endpoints (cloud#591). Please drop them from the offline command, or keep them out of the shipped surface.
  • Installed-image proof: Cloud runs it as a one-off container of the published image. The proof should do the same: docker run --rm --read-only with the recipe's tmpfs, uid 1000, cap-drop ALL and DB variables only, exercising release (including the lapsed restored case and idempotent repeat) and inspect (active, lapsed and absent row; confirm no row was created), against the packaged image rather than CARGO_BIN_EXE.

cloud#591 will call exactly release --owner and inspect through that one-off container.

General, Cloud and Security settled the offline interface on least
authority, matching Cloud's recorded contract for cloud#591:
- `backup-hold release --owner <uuid>` (or `--owner=<uuid>`): #953's
  release, by the recorded owner, idempotent, a lapsed restored hold
  included.
- `backup-hold inspect`: the hold as stored, from plain SELECTs of row
  id=1 in one read-only snapshot. No lock, no row creation, no write. An
  absent row reports active false and owner null.
Acquire, renew and status are gone from the console; live backups use
the endpoints. Any other argument is refused before the database opens.

inspect also opens read-only. SQLite opens the file read-only, and
PostgreSQL checks the schema in a READ ONLY transaction without the
bootstrap lock. MySQL's check was already one read-only transaction and
now is for release too.

Tests, all three backends:
- inspect on a schema without the hold row reports no hold and creates
  none;
- inspect leaves a restored row byte-identical;
- release, both forms and repeated;
- every other command or option is refused.
Negative control: pointing inspect at the locking status path fails the
test (SQLite refuses the write; PostgreSQL creates the row).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel rmcdaniel changed the title Native: offline backup-hold command for restores (#973) Native: offline backup-hold release and read-only inspect for restores (#973) Nov 3, 2026
@sonarqubecloud

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Installed-image proof at exact head 4cd133d0135b84cd36d23877332f9daf61306fe0.

Image:

  • built from git archive 4cd133d0 with this repository's Dockerfile.native;
  • release build arg DW_SERVER_VERSION=3.0.2-pr974, so no DW_RUST_EXPERIMENTAL;
  • linux/arm64, USER 1000:1000, image id sha256:a006b96b319a691e716c4994916fc293cec939ef85f68f96991e3f990d800977.

Each command is a one-off container:

  • docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64;
  • on a private network, with only DB_CONNECTION DB_HOST DB_PORT DB_DATABASE DB_USERNAME DB_PASSWORD set (disposable credentials);
  • against disposable PostgreSQL 17 and MySQL 8.4.13 containers.

The restored lapsed hold is inserted with SQL after the image's own schema-bootstrap. Row values are read back with psql/mysql.

Results, on both backends:

  • A fresh database is refused (native_schema_not_initialized, exit 1) and still has 0 tables afterwards.
  • inspect reports the lapsed hold and leaves the row unchanged.
  • Another owner's release is refused (backup_hold_not_owned).
  • The recorded owner's release succeeds in both option forms; the repeat returns the same released hold.
  • inspect then shows expires_at: null.
  • acquire is refused (backup_hold_action_invalid).
  • No one-off containers are left.
Transcript
=== pgsql: image sha256:a006b96b319a691e716c4994916fc293cec939ef85f68f96991e3f990d800977
NOTICE:  database "proof974_pgsql" does not exist, skipping
--- a fresh database is refused and left without a schema
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold inspect
native_schema_not_initialized
Error: "backup hold command refused"
[exit 1]
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold release --owner 7f1c3a1e-0000-4000-8000-0000000000a1
native_schema_not_initialized
Error: "backup hold command refused"
[exit 1]
tables after: 0
--- initialize the native schema (the image's schema-bootstrap), then restore a lapsed hold
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 schema-bootstrap
{"backend":"pgsql","schema_version":2,"status":"ready"}
[exit 0]
row: 7f1c3a1e-0000-4000-8000-0000000000a1 2000-01-01 00:00:00 2000-01-01 01:00:00
--- inspect reads it without changing it
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold inspect
{"schema":"durable-workflow.external-payload-backup-hold.v1","active":false,"owner":"7f1c3a1e-0000-4000-8000-0000000000a1","acquired_at":"2000-01-01 00:00:00","expires_at":"2000-01-01 01:00:00","database_time":"2026-11-03 00:16:02"}
[exit 0]
row: 7f1c3a1e-0000-4000-8000-0000000000a1 2000-01-01 00:00:00 2000-01-01 01:00:00
row unchanged: yes
--- another owner cannot release it; its recorded owner can, twice
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold release --owner 7f1c3a1e-0000-4000-8000-0000000000b2
backup_hold_not_owned
Error: "backup hold command refused"
[exit 1]
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold release --owner 7f1c3a1e-0000-4000-8000-0000000000a1
{"schema":"durable-workflow.external-payload-backup-hold.v1","active":false,"owner":"7f1c3a1e-0000-4000-8000-0000000000a1","acquired_at":"2000-01-01 00:00:00","expires_at":null,"database_time":"2026-11-03 00:16:02"}
[exit 0]
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold release --owner=7f1c3a1e-0000-4000-8000-0000000000a1
{"schema":"durable-workflow.external-payload-backup-hold.v1","active":false,"owner":"7f1c3a1e-0000-4000-8000-0000000000a1","acquired_at":"2000-01-01 00:00:00","expires_at":null,"database_time":"2026-11-03 00:16:02"}
[exit 0]
row: 7f1c3a1e-0000-4000-8000-0000000000a1 2000-01-01 00:00:00 NULL
--- inspect after release; acquire is not an offline command
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold inspect
{"schema":"durable-workflow.external-payload-backup-hold.v1","active":false,"owner":"7f1c3a1e-0000-4000-8000-0000000000a1","acquired_at":"2000-01-01 00:00:00","expires_at":null,"database_time":"2026-11-03 00:16:02"}
[exit 0]
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold acquire --owner 7f1c3a1e-0000-4000-8000-0000000000b2
backup_hold_action_invalid
Error: "backup hold command refused"
[exit 1]
=== mysql: image sha256:a006b96b319a691e716c4994916fc293cec939ef85f68f96991e3f990d800977
--- a fresh database is refused and left without a schema
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold inspect
native_schema_not_initialized
Error: "backup hold command refused"
[exit 1]
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold release --owner 7f1c3a1e-0000-4000-8000-0000000000a1
native_schema_not_initialized
Error: "backup hold command refused"
[exit 1]
tables after: 0
--- initialize the native schema (the image's schema-bootstrap), then restore a lapsed hold
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 schema-bootstrap
{"backend":"mysql","schema_version":2,"status":"ready"}
[exit 0]
row: 7f1c3a1e-0000-4000-8000-0000000000a1 2000-01-01 00:00:00 2000-01-01 01:00:00
--- inspect reads it without changing it
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold inspect
{"schema":"durable-workflow.external-payload-backup-hold.v1","active":false,"owner":"7f1c3a1e-0000-4000-8000-0000000000a1","acquired_at":"2000-01-01 00:00:00","expires_at":"2000-01-01 01:00:00","database_time":"2026-11-03 00:16:04"}
[exit 0]
row: 7f1c3a1e-0000-4000-8000-0000000000a1 2000-01-01 00:00:00 2000-01-01 01:00:00
row unchanged: yes
--- another owner cannot release it; its recorded owner can, twice
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold release --owner 7f1c3a1e-0000-4000-8000-0000000000b2
backup_hold_not_owned
Error: "backup hold command refused"
[exit 1]
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold release --owner 7f1c3a1e-0000-4000-8000-0000000000a1
{"schema":"durable-workflow.external-payload-backup-hold.v1","active":false,"owner":"7f1c3a1e-0000-4000-8000-0000000000a1","acquired_at":"2000-01-01 00:00:00","expires_at":null,"database_time":"2026-11-03 00:16:04"}
[exit 0]
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold release --owner=7f1c3a1e-0000-4000-8000-0000000000a1
{"schema":"durable-workflow.external-payload-backup-hold.v1","active":false,"owner":"7f1c3a1e-0000-4000-8000-0000000000a1","acquired_at":"2000-01-01 00:00:00","expires_at":null,"database_time":"2026-11-03 00:16:04"}
[exit 0]
row: 7f1c3a1e-0000-4000-8000-0000000000a1 2000-01-01 00:00:00 NULL
--- inspect after release; acquire is not an offline command
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold inspect
{"schema":"durable-workflow.external-payload-backup-hold.v1","active":false,"owner":"7f1c3a1e-0000-4000-8000-0000000000a1","acquired_at":"2000-01-01 00:00:00","expires_at":null,"database_time":"2026-11-03 00:16:04"}
[exit 0]
$ docker run --rm --read-only --tmpfs /tmp --user 1000:1000 --cap-drop ALL --security-opt no-new-privileges --init --cpus 1 --memory 256m --pids-limit 64 --network dw974-proof <DB_* only> dw-pr974:4cd133d0 backup-hold acquire --owner 7f1c3a1e-0000-4000-8000-0000000000b2
backup_hold_action_invalid
Error: "backup hold command refused"
[exit 1]
=== leftover one-off containers: 0

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security acceptance re-review: the interface and installed-image holds are closed at exact head 4cd133d0135b84cd36d23877332f9daf61306fe0.

Source review:

  • The shipped offline parser exposes exactly backup-hold inspect and backup-hold release --owner <uuid> / --owner=<uuid>. Acquire, renew, status, owner-scoped inspect, TTL and unknown forms refuse before storage opens.
  • inspect uses begin_read plus read_backup_hold: plain row/clock reads and rollback, with no locked_backup_hold, insert or update. SQLite opens the file read-only; PostgreSQL verifies and reads in read-only repeatable-read transactions without the bootstrap advisory lock; MySQL verifies and reads in read-only transactions. A missing hold row reports inactive/null without creating it.
  • release reuses Native: online-backup hold endpoints, with PHP's deletion fence #953's owner validation and locked transition, including a lapsed restored hold and idempotent repeat. The backend-specific open_existing path remains bootstrap-free and refuses missing/changed native schema.

Installed proof review:

  • The recorded image was built from git archive of this exact head with Dockerfile.native, and ran every invocation as --rm --read-only, tmpfs, uid 1000, all capabilities dropped, no-new-privileges, init, bounded CPU/memory/PIDs, private network, and DB variables only.
  • PostgreSQL 17 and MySQL 8.4 both refused a fresh DB and left zero tables; inspect preserved a lapsed restored row byte-for-byte; another owner was refused; the recorded owner released in both forms and on repeat; post-release inspect showed null expiry; acquire was refused.
  • Independent local image readback matches the transcript: ID sha256:a006b96b319a691e716c4994916fc293cec939ef85f68f96991e3f990d800977, arm64, user 1000:1000, entrypoint /usr/local/bin/durable-workflow-server; zero dw974 proof containers remain.

This clears Security's source/interface/installed-image acceptance hold at 4cd133d0…. The new head's full hosted matrix is still running, so final merge clearance waits terminal green on this exact head. Any source/image change needs requalification.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security final carry for exact head 4cd133d0135b84cd36d23877332f9daf61306fe0: clear for merge and inclusion in 3.0.2.

The source and installed-image acceptance recorded above remain exact: the offline surface is only read-only backup-hold inspect and owner-bound, idempotent backup-hold release; unsupported verbs/options refuse before database open; inspect uses a genuinely read-only/no-bootstrap path; and the constrained installed image proof covers PostgreSQL 17 and MySQL 8.4 with no residual containers.

Hosted qualification is now terminal on this same head: 57 check contexts, 56 successful and the expected prepared-local cancellation context skipped, with no failure/cancellation. This closes Security's source, installed-artifact, and hosted-check holds. The release composition still needs to bind the published 3.0.2 image to this merged commit before Cloud enables its dormant offline floor.

@rmcdaniel
rmcdaniel added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 6760837 Nov 3, 2026
58 checks passed
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security generated-merge clearance for exact merge-queue commit 6760837e677275d1b65e609332a522069c9fa47c: clear to merge.

  • GitHub's verified merge commit has exactly the accepted main 17c52f5f9e57d07b2a270c39c9ded07a4c3e50c4 and Security-cleared PR head 4cd133d0135b84cd36d23877332f9daf61306fe0 as parents.
  • Relative to accepted main, the generated merge changes only the nine Native: offline backup-hold release and read-only inspect for restores (#973) #974 paths. Eight result blobs are byte-identical to the reviewed head. rust/src/runtime/mod.rs differs as a whole blob only because main added an adjacent module line; the generated patch remains the exact reviewed one-line visibility change (pub(crate) mod backup_hold → pub mod backup_hold) and preserves main's addition.
  • All three merge-group workflows are terminal success on this exact commit: Shared Server fixtures 41559953600, PHPUnit Feature 41559953686, and Public Boundary 41559953682.
  • The final commit check inventory is 52 completed contexts: 51 successful, the expected prepared-local cancellation context skipped, and no failure/cancellation.

The earlier source/interface/installed-image acceptance remains exact. This clears #974's queue carry and inclusion in 3.0.2; publication still must bind the final release artifact to the eventual main commit after #969 merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native: offline backup-hold console commands (release, inspect) for restore

1 participant