Repository navigation
Fail-closed security posture, hardware gates in code, auto-merge fixes, appliance preflight, reboot setting - #48
Merged
Merged
Conversation
…es 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
…merge dispatch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
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
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
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
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
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
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
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
§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
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
…ail 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
…tch accurately Co-Authored-By: Claude Opus 5.5 <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
…rride
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
…r lanes after merge
…r 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
…N 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
…oked 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
…l 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
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
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
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…wlist, real backend requires API token
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
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…st runner, loopback compose + LAN override
…anes use it, runbook §0
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
…, rollback on start failure
…ane 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
…indings 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
_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
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
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
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
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
…ted 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
…once Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
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
…propagates Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD
…alisation, preflight fails when armed
…approval for Copilot PRs, token test, rollback isolation
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
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
Round 4 — five owner decisions implemented. Verified locally: sharded gate 3007 passed / 0 failed / 14 skipped, hardware (non-real) + i3 parity 123 passed, safety/contract 413 passed, ruff 0 / format clean / mypy strict 0 (180 files), actionlint + zizmor clean, operability gate scripts rc=0. Adversarial review ran twice; blocker and all should-fixes closed, every new guard mutation-checked.
1. Auto-merge (both gaps)
auto-merge-trusted.ymlnow triggers on CI and operability, verifies every required check on the head SHA with a puredecide(), merges with ashapin, then dispatches CI + operability on master sofull-suiteruns for every merge (dispatch is the one eventGITHUB_TOKENcan trigger — no new secret). Copilot-authored PRs additionally require an APPROVED review from the owner on the head SHA.cancel-in-progressonly for PR runs. Proven with a Node harness (18 scenarios, mutation-checked).2. Reboot persistence as a setting
tests/hardware/test_reboot_persistence.py+ a destructive-lane step gated on repo varHARDWARE_REBOOT_TESTS=true(exportsOPENBLADE_REBOOT_TESTS=enabled). Quiescence precondition, bounded waits, read-only guard bypassed for exactlyPOST /aml/system/reboot, runs after the recovery report and after-snapshot, TLS verification pinned on.3. Appliance day-one
tools/hardware/preflight.py(read-only: env, tools, devices,sg_inq,mtx status, allowlist, appliance login/logout; fails if run armed), replaces the inline env checks in the three hardware lanes,make hw-preflight, runbook §0 checklist. Proven on the local mhvtl rig. Sharedopenblade/hardware/appliance_client.py.4. Safest/most secure
OPENBLADE_API_TOKEN, with default/short (<12) admin or service passwords, or*in CORS. Dev prints one loud banner. Secrets never echoed (URLs redacted).POST /api/test-runner/runis dev-only + loopback-targeted. Compose binds 127.0.0.1 by default; LAN exposure only viadeploy/lan/docker-compose.lan.yml(${VAR:?}secrets, production).FormatServiceon a real backend (empty = refuse all); real backend requires an API token; ingest never auto-formats on a real backend; real lanes mint a masked per-run token.pool_idmust match the plan (409), share-push books before registering, rollback on any start failure without masking the original error.Not done / flags
operator_acknowledgmentconstant) was blocked by the permission classifier as a security-test removal; it stays, documented as not a control.FormatServiceis a follow-up.BEGIN IMMEDIATEdoes not break the test — recorded honestly.🤖 Generated with Claude Code
https://claude.ai/code/session_016uyKrkqBY5KxLuSjotaVGD