Add safety gating and harden evidentiary audit for v1 - #2
Merged
Conversation
Adds a safety package that gates operator output on conditions it can affirmatively establish, and strengthens audit logs from integrity-only to evidentiary. safety: - Command timeout measured on a monotonic clock, so a wall-clock step cannot make stale input appear fresh. - Dead-man switch with a re-actuation deadline: a control held indefinitely is treated as defeated rather than engaged. - Control-loop watchdog, latching trips requiring an explicit re-arm, and a latching emergency stop that Arm alone cannot clear. - Guards run as controller processors, so transitions are recorded and the gate trips on its own timer without application involvement. audit: - Ed25519 signing over tree heads via crypto.Signer, separating verification from authorship. Verification distinguishes signatures checked against the log's self-declared key from those checked against a caller-supplied trusted key; only the latter carries weight. - RFC 6962 Merkle tree with inclusion and consistency proofs, enabling selective disclosure and append-only verification. - Periodic signed checkpoints and an Anchor interface publishing heads outside the recording process, making destruction and truncation detectable rather than merely suspected. - Session provenance: build revision, host, configuration, operator. - Format version 3. Version 1 and 2 logs remain readable and are now covered by tests. Verify is fuzzed, since it parses untrusted input. teleop: - Monotonic readings on every event header, plus wall-clock step detection published as ClockEvent. - Controller.RecordCommand closes the causal chain at the actuator boundary. It never blocks; a full queue is an explicit fault. Also carries the pending architecture-review remediation across the xbox providers, terminal monitor, gesture, action, and testkit packages.
CI on Linux hung for ten minutes on the CGO_ENABLED=0 step. Reproducing it in a container surfaced one pre-existing defect and two of my own. xbox: poll readiness is not a whole event waitReadable returns once poll reports readable, which guarantees one byte, not a 24-byte evdev event. Read then called binary.Read, which is io.ReadFull, and blocked in a syscall that neither the context nor Close can interrupt until the remainder arrived. For a partial event at the tail of a kernel buffer that is never, so the input path stops delivering with no error and no timeout. Reads now go through a buffer that decodes only complete records, leaving poll as the single blocking point. A spurious readiness is treated as spurious rather than as a fault, and a read deadline bounds the read where the descriptor supports one. TestLinuxReadCancellation also leaked its descriptor on the failure path, and because these tests run in parallel, a recycled descriptor number appeared as a spurious readiness in a sibling test. teleop: state and freshness metadata were not committed together handleObservation set c.state before publishing and c.meta after, so a snapshot taken in between returned new state with stale metadata. A consumer enforcing a command timeout reads that as "no input has ever arrived". It fails safe, but it trips a gate for no reason, and a gate that trips spuriously is one operators learn to work around. Both are now committed under one lock before publishing. teleop: accepted commands could be discarded at close RecordCommand reports success once a command is queued. Close could then discard the queue without publishing, dropping a command from the record after telling the caller it was accepted. terminate now drains pending commands before the terminal neutral state, preserving issue order. safety: an unobserved hold is not a defeated switch A control the snapshot shows as held whose press the guard has not yet seen was reported as ReasonDeadManStale, which latches. That state is ordinary while a press propagates through the pipeline, so arming during one demanded a re-arm. It is now ReasonDeadManUnconfirmed, which inhibits without latching and clears when the press is observed. Verified on Linux in a container across all four CI steps: cgo enabled, cgo disabled, and -race, each repeated five times.
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
Prepares
teleopfor its v1 compatibility commitment by adding fail-closed safety gating, strengthening the evidentiary audit format, closing trusted-verification gaps, and documenting the full exported API.Safety
safety.Guardwith command timeout, dead-man re-actuation, loop watchdog, latching trips, and emergency stop.Audit and cryptography
ReadTrusted, which requires an independently obtained public key, a complete footer, format v3, and signed coverage through every returned event.PublicKeyinVerifyOptionsmakes signature verification implicit and rejects key substitution.KeyID; the short ID is never treated as a trust anchor.VerifyCheckpoint.Key-management boundary
The package handles safe key use and trust pinning, but lifecycle management remains a deployment responsibility: hardware/remote signer provisioning, device-to-key binding, public-key distribution, rotation, revocation, archival verification, destruction, and external anchor monitoring. Production signers must support Ed25519.
GenerateKeyremains intended for tests and development.Compatibility and documentation
ReadTrustedexample, documentation for every exported identifier, and a pkg.go.dev badge.Validation
go test ./...CGO_ENABLED=0 go test ./...go test -race ./..., with the final audit changes re-run undergo test -race ./auditgo vet ./...golangci-lint run ./...— 0 issuesFuzzVerifyruns over hundreds of thousands of executionsgovulncheck ./...— no reachable vulnerabilitiesgo mod tidy, formatting, and diff checks cleanThis is a focused internal review and test hardening pass, not a formal independent cryptographic assessment.
Automated release
The release automation now mirrors
open-ships/n2k:VERSIONestablishes the deliberate1.0.0baseline.maincommit creates and pushesv1.0.0, then publishes a generated GitHub Release.maincommits increment the latest patch version unlessVERSIONis raised to a newer baseline.