Skip to content

Add lease-safe control authority model - #7

Open
danielbrownjr wants to merge 5 commits into
mainfrom
feat/control-authority-model
Open

danielbrownjr wants to merge 5 commits into
mainfrom
feat/control-authority-model

Conversation

@danielbrownjr

@danielbrownjr danielbrownjr commented Sep 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Implements product-local control authority for Jump Jet with independent operating mode, active authority, recoverable control inhibit, latched safety fault, and thermal-handling state.

  • adds NONE / REMOTE / AUTOMATIC / REACQUIRING authority
  • binds Manual demand to a remote lease
  • keeps Automatic demand product-local after an eligible atomic transition
  • adds generation/revision-validated atomic mutations and explicit takeover
  • enforces acquire → authoritative refresh → new explicit request after control loss
  • adds a sparse control HMI with effective-authority labels and human-readable primary constraints
  • preserves raw protocol codes in advanced diagnostics

Safety behavior

  • Manual lease expiry removes heater demand without creating or clearing a hardware fault.
  • Cooldown/fan-afterrun remains product-owned and may continue after control loss.
  • Reconnection and takeover never restore previous Manual output.
  • Automatic is not browser-heartbeat-bound, but stale/missing/ineligible PrusaLink and every product safety veto fail cold.
  • Product safety remains the final veto.
  • The 60 s lease TTL is a usability timeout, not a safety boundary.
  • The bounded 8-entry request-ID cache provides idempotent retry support, not a safety boundary.
  • Idempotent retries preserve original mutation result semantics but always return and evaluate current authoritative control state; historical demand is never replayed.

Cold-safe foundation

No production GPIO, PWM, ADC, thermistor conversion, fan driver, heater actuator, or hardware-dependent threshold was added or frozen. Production remains uncommissioned and Automatic target mapping remains unavailable, so this branch cannot energize hardware.

Refresh and compatibility

  • rebased without conflicts onto main 9664e3664418a30b274ca3ef4afafc46801e7dc4
  • compatibility-reviewed against Phase 1 PR Phase 1: establish Rev A hardware contract and PCB #5 head 0a2febc007795a988fc3eedec3f3080f09437ef2
  • preserves the branch-retention policy added on main
  • does not change the approved control-authority or fail-cold semantics
  • does not freeze unresolved Phase 1 hardware TBDs

Validation

Validated head: eb7b5c37e5434a0101c46c4ede054beec211f444

GitHub Actions: firmware-build run 34479056348 — passed, including the native ESP-IDF v5.3 / ESP32-S3 build.

Local checks passed:

  • 9 control-authority host scenarios
  • 7 interlock host scenarios
  • 9 browser/client tests
  • UBSan host builds
  • JavaScript syntax
  • identity, API/product, control-safety, actuation allowlist, GPIO/ADC/thermistor compile-out, and OTA contracts
  • cppcheck with CI-equivalent settings
  • git diff --check

Review gates

Green CI does not authorize merge. This PR remains draft pending HQ final product approval and review against the Phase 1 hardware and product-safety contracts.

@danielbrownjr
danielbrownjr force-pushed the feat/control-authority-model branch from 9e653a4 to eb7b5c3 Compare September 10, 2026 12:50
@danielbrownjr
danielbrownjr marked this pull request as ready for review September 10, 2026 12:56
acquire, refresh, heartbeat, and mutate were registered without the
same authorize() gate every other mutating dc_portal/jj_portal route
uses, so anyone reachable on the LAN could take control authority and
issue mutations with no credential. Add the check to all four handlers
and extend check_control_safety_contract.sh so a missing gate on any
of them fails CI instead of shipping silently again.
@danielbrownjr

Copy link
Copy Markdown
Collaborator Author

Found during review: acquire_post, refresh_post, heartbeat_post, and mutate_post were registered without the authorize() gate that every other mutating dc_portal/jj_portal route uses (OTA, WiFi provisioning, factory reset). dc_portal does not apply that gate automatically to product-registered routes — each handler has to call it, and these four didn't. Not exploitable as a heater hazard yet (Phase 0 has no actuator), but it's the exact code that will gate real heat once Phase 3 lands, so it shouldn't ship unauthenticated.

Fixed in 36ce92d: all four handlers now require X-Dragon-Auth before doing anything else, and check_control_safety_contract.sh now statically asserts each handler calls authorize(req) so this can't regress silently again.

Separately, not fixed here: this PR's jj_interlock.c changes also implement the actual AUTOMATIC-mode heat-enable gating (automatic_target_available/automatic_target_c), which is inert today (nothing in production code sets those fields — confirmed via grep, only host tests do) but is scoped as its own Phase 4 policy decision per ROADMAP.md and PRODUCT_SAFETY_CONTRACT.md. Worth deciding whether that belongs in this PR or should split out before merge.

The bed-target-to-chamber-target policy is its own Phase 4 gate per
ROADMAP.md and PRODUCT_SAFETY_CONTRACT.md: it must be derived and
reviewed on its own, not inferred, and AUTOMATIC production heating
stays unavailable until it exists. That heat-delivery path had
started riding in under this control-authority PR, gated only on
automatic_target_available/automatic_target_c (fields nothing in
production sets yet, but a real live wire once something does).

Restores the interlock's AUTOMATIC branch to an unconditional
JJ_BLOCK_AUTO_POLICY_UNAVAILABLE, matching main's original invariant,
while keeping the new authority check (JJ_BLOCK_AUTO_AUTHORITY_UNAVAILABLE)
ahead of it, since who holds automatic authority is legitimately this
PR's scope. jj_authority.c's own AUTOMATIC-mutation eligibility check
is left as-is; it only gates whether the mode switch itself is
accepted, never whether heat is delivered, so it doesn't carry the
same risk.

The removed heat-delivery logic and its test coverage are preserved
verbatim on feat/automatic-bed-target-policy for its own PR once the
Phase 4 policy is actually derived.
@danielbrownjr

Copy link
Copy Markdown
Collaborator Author

Split out the AUTOMATIC-mode heat-delivery scope question flagged in review. This PR (6bcd3fc) now restores the interlock's AUTOMATIC branch to an unconditional JJ_BLOCK_AUTO_POLICY_UNAVAILABLE, matching main's original invariant, while keeping JJ_BLOCK_AUTO_AUTHORITY_UNAVAILABLE ahead of it since who holds automatic authority is genuinely this PR's scope.

The removed heat-delivery logic (gated on automatic_target_available/automatic_target_c) and its test coverage are preserved verbatim on feat/automatic-bed-target-policy for its own PR once the actual bed-target-to-chamber-target policy is derived and ready for Phase 4 review, per ROADMAP.md and PRODUCT_SAFETY_CONTRACT.md.

jj_authority.c's own AUTOMATIC-mutation eligibility check is untouched — it only gates whether the mode switch itself is accepted, never whether heat is delivered, so it carries none of the same risk and didn't need splitting.

All host tests and static contract checks pass after the split (interlock, authority, and all five check_*.sh scripts).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant