Repository navigation
Persistent drive leases, reconciliation, NAS persistence, protocol rig, compat capture, MFA pending, capacity budget - #46
Merged
Conversation
…ery (roadmap 7/8) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Adds DriveLeaseRecord (new table via create_all), the frozen DriveLease domain dataclass, and CatalogRepository acquire/heartbeat/release/ lease_is_live/live_leases/release_leases_for_job. One live lease per drive and the monotonic fencing token are enforced under SQLite BEGIN IMMEDIATE (verified against installed SQLAlchemy 2.1.4/pysqlite). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…rtbeat DriveScheduler keeps its public shape but delegates to a LeaseStore protocol: InMemoryLeaseStore (default; existing behaviour for unit tests) and CatalogLeaseStore (shared across processes via the catalog, polled at 0.25 s). DriveHandle gains lease_id/fencing_token; verify() raises the new typed StaleLeaseError, heartbeat() extends the ttl. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…tep 3) verify() before each READ_WRITE mount (both stripe paths), before the shard write batch, and before unmount/unload+commit; heartbeat once per batch after the writes. StaleLeaseError aborts the job (re-raised past the batch's generic handler); the existing finally still unmounts and unloads. archive.py/restore.py/tree_restore.py, the six construction sites and AppContext wiring are NOT done yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…ock-stripe writes - AppContext gains lease_store (CatalogLeaseStore over context.catalog). - The six DriveScheduler construction sites (archive/restore routes, CLI restore, restore-tree, archive-sharded) pass store= and job_id=; the CLI sites now build the scheduler after their catalog job exists. The tree restore route's worker thread uses a CatalogLeaseStore over its own session (the context session is not thread-safe). - sharded_archive block-stripe path: verify before the shard write batch and before unmount/unload, heartbeat after writes. - store stays optional on DriveScheduler: ~40 test constructions omit it. - Integration test: a lease held through a second catalog session makes POST /archive/sharded wait until it is released (mutation-checked). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…eport-only) Bootstrap hook and GET /jobs/recovery route not yet wired. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…ET /jobs/recovery Recovery runs right after the catalog opens, before anything can claim a drive. The report is read-only on the native surface: bearer-gated by the existing middleware (the auth sweep enumerates it) and 404 under OPENBLADE_SCALAR_API_ONLY. Wiki API reference regenerated for the new route. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…tation deviations in the spec Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…ed one A lease poll from a worker thread could commit or roll back another thread's pending rows on the shared Session. Lease ops now open a short-lived Session over the engine; on a file-backed catalog that is a separate connection, so BEGIN IMMEDIATE is a real cross-process lock. Adds expired_leases() and set_lease_physical_drive() for recovery. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…process may own The catalog is shared by the API and the CLI, so 'state == running' is not evidence a job is dead. Recovery now fails only jobs whose lease stopped heartbeating, releases live leases of already-finished jobs as bookkeeping, and leaves everything else alone. Inventory read goes through InventoryService (SAFETY_003). Mutation-checked: the test fails if recovery regresses to failing every running job. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…t; persist physical drive - On StaleLeaseError the error path no longer unmounts/unloads: the drive may already hold the new lease-holder's mounted tape. Physical state is logged as unknown for reconciliation. Mutation-checked. - DriveScheduler keeps held leases alive from a daemon thread (ttl/3), so a single long LTO write cannot expire its own lease. Mutation-checked. - record_physical_drives() persists where a pre-loaded cartridge actually is, so the recovery report reconciles the right drive. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…cleanup rules Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Sharded-archive lifecycle states. Stored as plain strings in
file_instances.state (no CHECK/enum type), so no migration is needed;
every catalog 'archived/restorable' filter is an explicit allowlist of
{archived, verified}, so the new states are excluded by construction.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Striping is a performance choice; only redundant copies make a policy protected. Erasure coding is modelled but rejected by require_plannable(). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
postcheck --read-only-appliance-checks wraps every probe in ReadOnlyProbe so no non-GET request reaches the appliance, then GETs /inventory/ and /jobs/recovery. rollback --to <digest> refuses tags, redeploys application code only via OPENBLADE_IMAGE, and prints the tape-state notice before and after. Both guards mutation-checked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…ore service Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…ion, unmount shutdown Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…ver the Hydrator Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
… chain finalize_tape_generation() is the single place that orders metadata writes: manifest -> catalog-shard -> tape.json -> commit-<generation>.json. Manifest v2 adds generation_id, sibling_tapes, placement and protection; v1 still parses, unknown schemas raise UnsupportedManifestSchemaError. TapeJson gains current_generation/previous_generation (its existing 'generation' is LTO media). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…staged deploy build -> scan (pip-audit + trivy HIGH+, SBOMs) -> sign (keyless cosign + SLSA provenance, id-token only here) -> verify -> release + staging (suites, read-only postcheck, rollback) -> production behind environment approval. All actions pinned to SHAs; digest-pinned compose examples for both envs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…rd cases replayable The end-to-end capture leaked a session token in an application/xml login body and an expires= timestamp in Set-Cookie. Redaction also collapsed right/wrong passwords; non-login passwords now become <redacted-invalid>. Directory recipes skip the tool's own outputs. XML rule mutation-checked (removing it fails 1 test). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
… to harness self-captured is treated like inferred (regression guard, never fidelity); redacted request passwords replay with the emulator default / a wrong value. --diff over the 15 existing inferred cases showed 0 drift. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…pture loop Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…ive-plan Route the upload plan through plan_with_pool_reservation and map ReservationError to 409. Mutation-checked: bypassing the helper fails both new tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…logout
require_auth now rejects an MFA-pending session with the existing 401
(cookie kept); POST /users/login/mfa, GET /users/login/mfa/{type}/key and
logout use require_auth_mfa_pending_ok. A valid TOTP code clears the flag;
an invalid one keeps it. Inferred behaviour, mutation-checked.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Add CatalogRepository.delete_nas_reservations_for_owner and call it from run_ingest_job once the job reaches archived/failed/cancelled (the job carries the plan, whose plan_id is the reservation owner). A non-terminal exit keeps the booking until its 1h expiry. Mutation-checked: removing the release fails test_finished_job_releases_its_plan_reservation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Corpus cases may declare "setup": {"mfa": {"user", "type"}}; the harness
enables TOTP for that user via GET /aml/users/login/mfa/{type}/key +
PUT /aml/users/mfa before replaying the request. Adds the inferred
aml-auth-login-mfa-pending case (200 + Warning "MFA Required") and
corrects logout.json fidelity_notes, which still called MFA,
session-expiry, max-sessions and default-password states unmodelled.
Mutation-checked: with the setup call disabled the case fails
(Warning lacks "MFA Required").
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…elements
- token/serial/password-class keys now mask every scalar leaf beneath them
(a list/dict value was previously walked and leaked, e.g.
{"x-auth-tokens": ["eyJ..."]}).
- _scrub_xml matches elements that carry attributes and scrubs attribute
values whose name hits a key rule (sessionId="...").
- Basic credentials were already covered by the Bearer|Basic rule; now tested.
Mutation-checked: reverting the list masking, the attribute pass, the
element-attrs pattern, or dropping Basic each fails the new tests.
Self-capture re-run over the 12 aml recipes: no sanitizer-attributable
changes (sanitize() is identity on all 12 committed files), so the corpus
is not regenerated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…ts (B1) With `pool` set, /nas/archive-plan and share-push now source available_tapes and tape_capacities from library inventory (InventoryService) + catalog via nas.capacity.server_budget_request. Body-supplied available_tapes / tape_capacities / reserved_bytes / pool_reserved_bytes -> 422; unknown pool -> 404. The planner's pool budget check keys on `pool`, not on a non-empty tape list, so an empty server tape set blocks instead of skipping. Replaces routes_upload._available_data_tapes (which used library.inventory() directly and silently dropped all catalog barcodes via a dead dict branch). Mutation-checked: disabling the field refusal fails the 4 x 422 tests; disabling the pool lookup fails the 404 test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
… too "MFA verification required" was invented text on an undocumented surface. Strict mode now answers 412 only for the documented default-password condition, else 200; MFA is signalled solely by Warning: 199 - "MFA Required". Tests: strict+default-password keeps the documented 412 summary; strict without it is 200 with only the MFA warning (fails if MFA re-enters the 412 condition — mutation-checked). Compat note and contract-notes row updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
Verified on the pinned digest: docker run --rm fedora@sha256:f1a3fab4... dnf list --available nfs-ganesha nfs-ganesha-vfs nfs-ganesha.x86_64 5.9-4.fc41 fedora nfs-ganesha-vfs.x86_64 5.9-4.fc41 fedora Globs are quoted so the RUN shell cannot expand them. docker build --target nfs-ganesha deploy/nas-protocols succeeds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…der-only signal, Ganesha pin
plan_with_pool_reservation -> plan_with_pool_budget (budget only). New book_plan_capacity books total*copies under owner=plan_id with a server-derived capacity ceiling; /nas/ingest/start and share-push call it before start_ingest_job and map ReservationError to 409. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…om pool budget (S1) count_scratch_cartridges counted catalog rows, but blank tapes are not rows. count_scratch_tapes counts inventory slots not in a volume group and not catalogued as written; server_data_tapes also skips VG members. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…r (S3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…3 test) run() converts failures to FAILED, but if it raises anyway (e.g. _mark_dataset_failed on a session in PendingRollbackError) the job can still be ARCHIVING, so the terminal-status check skipped the release and the booking sat until TTL. An escaping exception now releases too. Tests: executor raises + release raises -> RuntimeError surfaces and the "reservation release failed" warning is logged; executor raises with a non-terminal status -> booking released. Mutation-checked: removing the try/except fails the first; dropping the raised-path fails both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…scratch Capacity reservations decision: client capacity fields 422 / unknown pool 404 when pool is set, booking at ingest start (409, no job) not preview, release on terminal state or executor exception, scratch counted from the library inventory. Open risks: booking uses plan.pool not the start request's pool; share-push 409 leaves a registered plan with no job. tech-debt adjusted to match. Wiki API reference regenerated: unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…test B1 makes /nas/archive-plan derive the tape set server-side when `pool` is set and reject client-supplied tape fields with 422; the end-to-end archive/recall test still posted `available_tapes`, so it failed 422. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…t ingest start, inventory scratch, safe release
…quest With `pool` set, /nas/archive-plan now rejects client-supplied available_tapes/tape_capacities (422) so the budget cannot be inflated; the demo was still sending the tape list and failed on the first run of the gate. The unused `get_context` import that fed it is dropped too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
github-actions Bot
pushed a commit
that referenced
this pull request
Oct 11, 2026
…s, appliance preflight, reboot setting (#48) * ci(auto-merge): wait for every required check and dispatch master lanes after merge Trigger on workflow_run of both CI and operability. Before merging, read the required contexts from branch protection (fallback ["ci-gate", "operability-gate"] on 403/404, which GITHUB_TOKEN normally gets) and merge only when every one is "success" on the PR head SHA (check runs, newest per name, plus combined commit statuses). Pending -> notice and exit; the other workflow's completion is the retry (PR #47 merged-while-in-progress failure). Failed -> notice and exit. The merge pins sha=head to avoid racing a push. After a successful merge, dispatch ci.yml and operability.yml on master: a GITHUB_TOKEN merge emits no push run, so full-suite never ran on the merge commit (PR #46). Job grants checks:read, statuses:read, actions:write per-job; workflow-level stays contents:read. decide() is a pure function with three worked examples in comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * docs(readme): document trusted auto-merge two-workflow wait and post-merge dispatch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(safety): consume format safety token atomically before the job delete_safety_token now runs DELETE under BEGIN IMMEDIATE and returns whether this caller consumed the token; FormatService.confirm consumes first and refuses on False, so concurrent confirms with one token run the job once and a failed job does not re-arm the token. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(auth): fail closed at startup in production on open auth gates Production refuses to start (typed ConfigurationError, names vars, never values) when OPENBLADE_API_TOKEN/_FILE is unset, CORS origins contain '*', or the AML admin/service password or service token is the default. Elsewhere one multi-line banner lists every open gate. validate_config reports the same set, so scripts/validate_config.py stays the gate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * test(hardware): opt-in appliance soft-reboot persistence test Reboots via POST /aml/system/reboot only when OPENBLADE_REBOOT_TESTS=enabled on a real-hardware run; otherwise skips. Refuses to reboot unless the library is quiescent (no live leases, no drive loaded or mounted). A dropped connection after the POST counts as accepted; login_ok treats HTTPException as down; polls every 2s within bounded 300s/600s waits. Does not call recover_after_restart (no report-only mode); the lane's recovery-report step is the recovery assertion. A no-hardware unit test pins that RebootClient.request keeps the read-only guard (refuses POST /aml/media/move and POST /aml/system/reboot); mutation- checked: widening request makes it fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * ci(hardware-destructive): opt-in reboot persistence step Runs tests/hardware/test_reboot_persistence.py only when repo var HARDWARE_REBOOT_TESTS == 'true' and every earlier step succeeded (success(), never always()), exporting OPENBLADE_REBOOT_TESTS=enabled. Placed after the recovery report and the lane's after-snapshot (so the lane diff is not polluted by the reboot) and before the snapshot upload; same environment approval. timeout-minutes: 20; TLS verification pinned on (OPENBLADE_APPLIANCE_INSECURE=false); -p no:cacheprovider --tb=short. An unset var yields a visible ::notice:: instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * feat(hardware): add read-only appliance preflight tool python -m tools.hardware.preflight --lane readonly|destructive [--json]: env, tools, device/inquiry/serial/by-id/serial-map, mtx status drive count, SACRIFICIAL_BARCODES inventory + not-loaded, appliance login/logout, armed-env warning. Route tools/hardware/* to the unit category. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * test(hardware): unit-test every preflight check with fakes Mutation-checked: disabling the duplicate-serial and allowlist-loaded guards each fails its test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(auth): make the test-runner route dev-only and loopback-targeted POST /api/test-runner/run returns 404 unless OPENBLADE_ENABLE_TEST_RUNNER=true and OPENBLADE_ENV != production, and rejects any i3_aml_url whose host is not localhost/127.0.0.1/::1/host.docker.internal (SSRF via spawned pytest env). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * docs(runbooks): document the opt-in reboot persistence setting §2 gains HARDWARE_REBOOT_TESTS; §5 "Reboot persistence" describes the two-level opt-in (repo var + OPENBLADE_REBOOT_TESTS), the success()-only ordering after the lane's after-snapshot, the quiescence precondition, the bounded waits, the pinned TLS verification, and why the test does not call recover_after_restart. The hard power-cycle stays a manual fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * ci(hardware): run the preflight tool instead of inline env checks All three rig lanes call python -m tools.hardware.preflight --json, print the report on failure and upload preflight.json. The job-level OPENBLADE_REAL_HARDWARE_ENABLED is blanked for that step so the preflight never runs armed. HARDWARE_RIG_AVAILABLE gating is untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * ci(auto-merge): address review — keep retries, Actions-only checks, fail on dispatch error - Drop the job-level conclusion=='success' filter: decide() already enforces success per required check, and skipping non-success completions could strand a PR that was waiting on that workflow. - Count only check runs whose app is github-actions, so a look-alike gate name from another app cannot mask a real failure. - core.setFailed when a post-merge dispatch fails, so a missing full-suite run on master is visible instead of a warning on a green job. - Per-head-SHA concurrency group (no cancel) so near-simultaneous CI and operability completions do not race two merges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * docs(readme): state the auto-merge fallback list and master-tip dispatch accurately Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * build: add make hw-preflight target Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * docs(runbook): add §0 Day one checklist and reference the preflight Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * feat(deploy): bind compose to loopback; add opt-in production LAN override docker-compose.yml publishes 8000/8010/5173/5175 on 127.0.0.1 only. deploy/lan/docker-compose.lan.yml (make lan-up/lan-down) publishes api/web/ web-flask on ${OPENBLADE_BIND_IP:-0.0.0.0}:8090/8091/8092 via ports !override, OPENBLADE_ENV=production, and requires every secret via ${VAR:?}. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * ci: only PR runs cancel in-progress runs; dispatched master runs never cancel each other Auto-merge now dispatches CI and operability on master after every merge. With cancel-in-progress unconditional, two quick merges cancelled the first merge's full-suite, leaving a commit on master with no post-merge result. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * feat(auth): warn on insecure Flask cookies in production; document LAN exposure validate_config warns (insecure_session_cookies) when production sets OPENBLADE_WEB_SECURE_COOKIES off. Tests pin the existing Flask refusal without FLASK_SECRET_KEY and Secure-by-default cookies in production. docs/deployment.md gains 'Exposing on a LAN'. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(nas): refuse ingest start when pool_id differs from the plan's booked pool start_ingest booked capacity against plan.pool but created the dataset in request.pool_id, so a mismatch wrote to an unbudgeted pool and a pool-less preview could start into any pool with nothing booked. Now 409 on mismatch and the job is created in plan.pool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * feat(safety): enforce scratch-media allowlist in FormatService on real backend Barcode-list parsing moves to openblade/domain/media_allowlist.py (the CI tool now imports it). On a REAL backend, dry_run and confirm refuse any barcode not in OPENBLADE_SCRATCH_BARCODES; an empty allowlist refuses everything. Simulator behaviour is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(nas): book share-push capacity before registering the plan A refused booking (409) used to leave the plan orphaned in the in-memory registry. Book first, register after, and drop the plan via the new unregister_archive_plan if job creation fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * feat(safety): real backend requires an API token; accept simulator alias load_config raises RealHardwareDisabledError when OPENBLADE_BACKEND=real and OPENBLADE_REAL_HARDWARE_ENABLED=true but no OPENBLADE_API_TOKEN(_FILE) is set, and validate_config reports it as BLOCKING. OPENBLADE_BACKEND= simulator (the Dockerfile value) now maps to mock explicitly, and any other unknown value logs a warning instead of silently mocking. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * docs(safety): document new gates; flag vacuous operator_acknowledgment Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(nas): roll back the capacity booking when an ingest start fails Both start paths (start_ingest and share-push) now release the plan's booking and unregister the plan if anything after book_plan_capacity raises, then re-raise the original error. release_plan_capacity logs and swallows a failed release so it can never mask the start failure (booking expires by TTL). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * docs(decisions): mark the capacity start-path risks resolved Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * ci(hardware): mint a masked per-run API token in every real lane Real mode now refuses to start without OPENBLADE_API_TOKEN. Each rig lane mints one per run (masked) before preflight/pytest; mhvtl threads it through the explicit sudo env lists and env.sh emits it (keeping the caller's). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * refactor(hardware): share the read-only ApplianceClient between the lane and preflight Move ApplianceClient, ApplianceResponse, ReadOnlyViolation and the request guard into openblade/hardware/appliance_client.py. The read-only conftest re-exports them (fixtures and RebootClient unchanged); preflight's duplicate _UrllibAppliance becomes a thin probe over the shared client, keeping its scheme check and curated unreachable error. Every preflight request now also passes the read-only guard. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(config): redact credentials from OPENBLADE_DB_URL in validation findings The startup banner logs WARNING findings verbatim; a postgres URL with a password leaked into logs. Add redact_url() (scheme+host+path, creds ***). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(nas): ingest refuses to auto-format on a real backend _ensure_formatted minted its own SafetyToken and called ltfs.format, so on OPENBLADE_BACKEND=real ingest could mkltfs any unformatted tape, bypassing the scratch allowlist and the persisted single-use token. On a real backend (same detection FormatService uses, now real_backend_from_config) it now raises FormatRequiresConfirmationError naming the barcode and the two-phase /ltfs/format flow; run() marks the job FAILED and nothing is written. Simulator keeps the direct auto-format. Mutation-checked: dropping the real-backend check fails the refusal test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(format): normalise barcodes once at the FormatService boundary is_scratch_barcode compared strip().upper() but dry_run/confirm used the raw barcode, so " abc123" passed the allowlist and then failed as unknown. dry_run and confirm now normalise (strip().upper()) before the allowlist, inventory lookup, token and catalog. Mutation-checked: an identity normalize_barcode fails the new test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(hardware): preflight FAILs (exit 1) when run armed check_armed only WARNed when OPENBLADE_REAL_HARDWARE_ENABLED was set, although preflight is read-only and must never run armed. It now FAILs, so main() returns EXIT_FAIL. The three lane preflight steps already blank the flag (hardware-nightly-readonly.yml:88, hardware-library-smoke.yml:89, hardware-destructive.yml:102), so lanes are unaffected. Mutation-checked: reverting to WARN fails both armed tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * docs(agents): ingest never formats on a real backend Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(auth): reject blank, short (<12) and default AML passwords password_problem() strips whitespace, enforces 12 chars and rejects known defaults; shared by the production BLOCKING gate, the dev WARNING, and the aml_state getters (production raise), so a ' ' value no longer passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * ci(auto-merge): require owner approval on head SHA for non-owner trusted authors Copilot PR content is steered by issue text; it now merges only when Amantux's latest substantive review is APPROVED on pr.head.sha (pure reviewGate(), listReviews). Amantux still merges directly. README updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * test(format): claim a safety token from two independent repositories once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * test: armed-real-backend fixtures supply the now-required API token Real mode refuses to load without OPENBLADE_API_TOKEN(_FILE); the five tests that arm real hardware through monkeypatched env now set a token, and the clean-env fixture clears it so default assertions stay meaningful. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * fix(capacity): isolate rollback cleanups so the original start error propagates Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD * ci(operability): positive-control production config carries an API token Production now refuses to promote with the native API open (native_api_open), which is exactly what the gate is for; the positive control must therefore set OPENBLADE_API_TOKEN like any real production deployment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three fleet rounds on top of the persistent-drive-leases work. Everything below is verified locally (sharded gate 2909 passed / 0 failed / 14 skipped; live i3 suite 175 passed; protocol rig 33/33 x3; contract 24 passed; ruff 0 / format clean / mypy strict 0; actionlint + zizmor clean; cross-repo emulator contract validation passes).
Round 1–2 (safety + persistence)
physical_state_unknownjournal with a real consumer: drive and barcode refusal in the scheduler and every load helper,POST /jobs/recovery/reconcile/{drive}, fail-closed when the journal write itself fails (lease is held, typedJournalWriteError).JobQueuepersisted over catalog jobs + leases, heartbeated, reconciliation-gated.scsi+fake-ltfscontract pairing over a statefulmtxfake; SMB cross-session lock conflict and NFS mode-bit scenarios on the containerised rig.OPENBLADE_EMULATOR_FAULT_PROFILE); mutation baseline for 6/6 safety modules.Round 3 (this push)
/nas/archive-plan; budget and capacity are server-derived from inventory + catalog (client-suppliedtape_capacities/available_tapeswithpool→ 422, unknown pool → 404); booking happens atPOST /nas/ingest/start/ share-push (409, no job), released on terminal state or executor exception without masking the original error. Scratch health component counts scratch from library inventory;GET/PUT /nas/scratch-thresholds.tools/compat_capture.pycapture/diff/sanitize (refusessource: capturedwithout firmware + attestation); 12 self-captured emulator regression cases, 0 drift against the inferred set.Warning: 199 - "MFA Required"header only; 401 on everything but the MFA challenge/key/logout until a valid TOTP). Labelled inferred.scalar_httpno longer raises).Reviewer
Adversarial review ran on every round. Round 3's blocker (caller-controlled pool budget) and all five should-fixes are closed in this push; every new guard is mutation-checked.
Still open (recorded in docs/tech-debt.md / roadmap)
book_plan_capacitybooks againstplan.pool, not the start request'spool_id; share-push leaves a registered plan with no job on 409.🤖 Generated with Claude Code
https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD