CaltechExperimentalGravity / CaltechExperimentalGravity/system_ident

Stage E: per-injection human-approval gate; reject --yes on hardware; fix _confirm(twin=True)

Open
#17 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Python
Stars
0
Forks
1
PR merge metrics
No merged PRs in 30d

Description

**Stage E (spec §1, §5).** Enforce in code the hardware-safety rules now in
[`CLAUDE.md`](https://github.com/CaltechExperimentalGravity/system_ident/blob/feat/cds-hardware-backend/CLAUDE.md): *only humans authorize the hardware*, and *each individual injection needs separate
approval — one approval never carries over*.

The sibling project enforces **neither in code** — no `input()`, no `--confirm`, no dry-run on its hardware
path; the rules are documentation-only there. This repo should do better, because it already has most of
the machinery.

### The gate goes inside `CDSBackend.inject()`

It is the only code path from any caller to the actuator, and the only place holding the actual samples.

Rejected alternatives, and why:

- **A `Watchdog` pre-injection hook** or a `loop.py` change is a *convention*, not an invariant: any script,
notebook or test that constructs the backend and calls `inject()` directly bypasses it entirely. It also
makes `SysIDLoop` backend-aware, contradicting [`src/system_ident/backends/base.py:3`](https://github.com/CaltechExperimentalGravity/system_ident/blob/feat/cds-hardware-backend/src/system_ident/backends/base.py#L3) ("the loop and
the safety handoff use only these methods, so simulation and hardware are truly interchangeable"), and
the watchdog has no access to the drive array so it cannot show the operator what is about to happen.
- **A decorator or mixin** has the same bypass risk unless applied at class level, at which point it *is*
the inject-internal gate with extra indirection.

### Approval is a single-use token

Staging and actuation are now separate (`inject()` stages, `read()` executes), so approval granted at
`inject()` is for an actuation that happens later:

- `inject()` prompts — printing channel, hardware rate, duration, **RMS, peak and crest**, and the
`start_gps` window — then mints `_approval[channel]`.
- `read()` **refuses to start** any staged injection whose token is missing or consumed; consumes it on
start.
- Re-reads of an already-running injection do **not** re-prompt: nothing new is actuated. A new `inject()`
mints a new token. That is Rule 2 exactly.
- The prompt is injectable (`authorizer=`) for testability, and **defaults to deny** on `EOFError` /
non-TTY. Never proceed in batch.

### Fix the existing CLI gate

- [`src/system_ident/cli.py:89`](https://github.com/CaltechExperimentalGravity/system_ident/blob/feat/cds-hardware-backend/src/system_ident/cli.py#L89) calls `_confirm(twin=True)` **unconditionally**, so the `"HARDWARE"`
label at [`src/system_ident/cli.py:129`](https://github.com/CaltechExperimentalGravity/system_ident/blob/feat/cds-hardware-backend/src/system_ident/cli.py#L129) is dead code and a hardware run would announce itself as a
twin run → `_confirm(twin=args.twin or args.rtsfreerun)`.
- `--yes` ([`src/system_ident/cli.py:45`](https://github.com/CaltechExperimentalGravity/system_ident/blob/feat/cds-hardware-backend/src/system_ident/cli.py#L45)) is a campaign-wide skip, i.e. by definition a carry-over
approval, which Rule 2 forbids → **reject it at argparse level (exit 2)** when neither `--twin` nor
`--rtsfreerun` is given. The CLI prompt then becomes a pre-flight; the *gate* is the token.

### Acceptance — tests that assert it cannot be skipped

All on the Stage A fakes:

1. Denying authorizer → raises, **and `FakeArbitraryLoop` was never constructed** (assert the construction
counter, not `.start`).
2. Authorizer called exactly twice for two `inject()` calls; exactly once for one `inject()` + three
`read()`s.
3. `read()` with a staged-but-unapproved injection → raises, `.start` never called.
4. Default authorizer with `input` raising `EOFError` → denies.
5. `main(["run", cfg, "--yes"])` without `--twin` → exit 2.

---
**Campaign:** CDS hardware backend · branch [`feat/cds-hardware-backend`](https://github.com/CaltechExperimentalGravity/system_ident/tree/feat/cds-hardware-backend)
· [spec](https://github.com/CaltechExperimentalGravity/system_ident/blob/feat/cds-hardware-backend/docs/superpowers/specs/2026-08-03-cds-hardware-backend-design.md) · [plan](https://github.com/CaltechExperimentalGravity/system_ident/blob/feat/cds-hardware-backend/docs/superpowers/plans/2026-08-03-cds-hardware-backend.md) · [handoff](https://github.com/CaltechExperimentalGravity/system_ident/blob/feat/cds-hardware-backend/notes/cds-hardware-bringup-2026-08.md)
*Code is deferred until the plan and issues have been reviewed.*

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the CDSBackend inject/read path and the Stage A fakes described in the acceptance criteria, then inspect src/system_ident/cli.py and src/system_ident/backends/base.py. Confirm that each injection requires a single-use approval, denied approvals prevent construction or start, EOFError denies by default, and hardware rejects --yes with exit 2.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, cli
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.