fuse

package
v1.2.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package fuse is the STATE half of the ingestion fuse: reading and writing the box, the JSON file the two emergency powers live in. cmd/nova-fuse is the thin command on top of it. The box's path always comes from the caller — there is no default location and no environment variable, per this repo's no-guessed-paths rule.

WHAT IS ACTUALLY DECIDED HERE, and why each one is not arbitrary:

  1. THE READ HAS ONE YES AND TWO NOES. A readable box says what it says. "cannot read the box" is CANNOT TELL, and "no box at the path" is CANNOT TELL too: a fuse box that is not where the caller said can prove nothing is blown, and a reader that answered it with an empty box turned a mistyped --box, a box moved or deleted, or a second --box pointing somewhere empty into VERIFIED CLEAR -- a fail-open in a safety control. Both noes are errors, and every caller must treat an error as BLOWN; ErrNoBox tells them apart, so a refusal can name the right remedy (CreateBox for the first, a hand repair for the second). Lstat refuses a link or non-regular path before open; SameFile then proves the opened descriptor is the file that was inspected. The read is limited to 1 MiB and refuses a file that reaches the limit: a box is only a few lines, and an unbounded read lets a directory writer exhaust memory. errors.Is with fs.ErrNotExist distinguishes absent from unreadable; it does not say which part of the path is missing, and it does not need to, since both answers refuse. A box comes into being by CreateBox, which never replaces one, or by a write verb on a box that was read.

  2. MALFORMED IS UNREADABLE. A JSON array, a bare string, a truncated file, a lockdown whose value is not an object -- every one fails the decode and comes back as CANNOT TELL, which every caller must treat as BLOWN. A bare JSON null fails no decode -- a struct ignores it with no error -- so the read requires a JSON OBJECT at the top level before it decodes anything, and names the kind it found instead: a null, an array, a string, a number or a boolean is refused, because a wrong-shaped value is CANNOT TELL, never the VERIFIED CLEAR a normalised empty map would make of it. Its remedy is a hand restoration, not init: init is the remedy for a path with no box and never replaces one. An UNKNOWN top-level member is refused too, which is the policy for a gate: a key the reader does not know is a key it cannot account for, and answering CLEAR over a box someone meant to block with it is the fail-open this package exists to prevent. `"quarantine": null` stays deliberately supported -- a box a person hand-edits into that shape is a readable empty box. Reaching the fail-closed answer by a crash deep inside a caller is not a design; this is.

  3. THE WRITE IS TEMP-FILE + RENAME, PUBLISHED UNDER THE BOX'S LOCK. The file whose corruption means PERMANENT LOCKDOWN must never be left torn: a truncating write can leave half a file if the process dies, and a half file is an unreadable box that only a person can clear, by hand, live. Rename within one directory is atomic, so a reader sees the old box or the new one and never a fragment. A box mutation (MutateBox) holds the lock file beside the box -- <box>.lock, pkg/filelock -- across its read, its change and its publish, so two copies of the tool blowing fuses at once land BOTH writes, and neither can produce a corrupt box. The one deliberate exception is a lockdown whose lock cannot be taken: it blows unserialized rather than not at all -- a fuse you cannot blow is not a fuse -- and cmd/nova-fuse says so on a NOTE line (BoxLockUntaken tells that failure from a box that cannot be written at all).

  4. SURFACE NAMES ARE MATCHED NORMALIZED, AND THAT CUTS BOTH WAYS. Raw string comparison lets `quarantine Discord` then `check discord` answer CLEAR -- a fail-OPEN in a safety control, reached by a capital letter. Normalizing collapses spellings into one surface, and the honest statement of what that does is not "it can only ever block more": it is that EQUIVALENT SPELLINGS ARE ONE SURFACE, IN BOTH DIRECTIONS. `check` refuses on any spelling, so the class blocks more. LiftQuarantine removes EVERY spelling in the class, so a lift lifts more too -- deliberately, because a lift that left one spelling behind would verify its own failure, and because an operator who lifts a surface means the surface. It is not silent either way: each removal is returned under its stored spelling so the caller can announce it on its own line. The consequence to know: a box holding two keys that fold together holds ONE surface, not two.

  5. THE ESCAPE LIVES AT PRINT TIME, because the tool cannot trust what it did not write. The box is world-readable on purpose (a fuse nobody else can see is a fuse that stops nothing) and hand-editable on purpose (that is the only lockdown-replacement mechanism there is), so on a shared machine the reason, the `at` stamp and the stored surface names are authored by whoever can write the file. Echoed raw into a one-line output grammar, a newline forges a SECOND event line beneath a real one -- a `FUSE OK lockdown=clear` under a `FUSE FAIL`, which a caller scanning the grammar reads as permission -- and an ESC sequence does the same to an operator's terminal. cmd/nova-fuse prints every such string through pkg/oneline's Escape, which escapes every control character as the text is printed and so holds for a box this tool never wrote; Fold only tidies what this tool writes itself, and is never a refusal, because a fuse you cannot blow is not a fuse. Constraining writes alone would defend exactly the case that needs no defending. The escaped set is category Cc plus U+2028 and U+2029, which break a line for readers that follow Unicode rather than counting newlines. Folding also widens the surface classes of note 4, in both directions -- see there.

Index

Constants

View Source
const LockSuffix = ".lock"

LockSuffix names the sibling lock file every box mutation holds: <box>.lock. The box itself cannot be the lock: a write publishes by rename (note 3), so a lock taken on the bytes a writer read is a lock on an inode the next writer replaces, and two writers then hold "the lock" at once.

View Source
const UnreadableSuffix = ".unreadable"

UnreadableSuffix names where the bytes of an unreadable box are kept when a lockdown has to clobber it. Back up before mutating what cannot be reconstructed: a corrupt fuse box is evidence -- of a torn write, a bad hand-edit, or something worse -- and blowing an emergency power must not be the thing that destroys it.

Variables

View Source
var ErrNoBox = errors.New("no box")

ErrNoBox is ReadBox's answer when nothing is at the path: CANNOT TELL, like an unreadable box, and told apart from it only so a refusal can name its remedy (CreateBox). See note 1.

Functions

func BoxLockPath added in v1.2.3

func BoxLockPath(path string) string

BoxLockPath is the lock file beside the box at path.

func BoxLockUntaken added in v1.2.3

func BoxLockUntaken(err error) bool

BoxLockUntaken reports whether a box mutation failed because the box's LOCK could not be taken -- another writer holds it, the wait timed out, or only askers stood in the way (pkg/filelock's ErrHeld, ErrTimeout and ErrBusy; MutateBox returns that failure verbatim). It tells "the box is being mutated elsewhere right now" from "this box cannot be written here at all", and the difference is a policy, not a detail: a lockdown answers the first by blowing unserialized and saying so (a fuse you cannot blow is not a fuse), while every other failure stays a failed blow. See note 3.

func CreateBox added in v1.2.3

func CreateBox(path string) error

CreateBox makes an empty box at path, only where nothing is: it NEVER replaces a box, because replacing one is the lockdown reset this package does not have. The write uses atomicfile.NoReplace for atomic, exclusive creation. A box that appears between the check and publication is kept, and the error is fs.ErrExist. See atomicfile.NoReplace for its publication mechanism and filesystem requirements.

func Fold added in v0.8.0

func Fold(s string) string

Fold tidies text this tool is about to WRITE: every control character becomes a blank, then runs of whitespace collapse to a single ASCII blank and the ends are trimmed. The collapse is Unicode-aware, so a non-breaking whitespace character or a line separator inside the text becomes an ordinary blank too. It is not the defense (oneline.Escape at print time is), because a box written by another hand still arrives holding anything at all (note 5). And it is never a REFUSAL: a fuse you cannot blow is not a fuse, so a reason is accepted whatever it contains and only its spelling in the file is tidied.

func MutateBox added in v1.2.3

func MutateBox(path string, write bool, change func(b Box, readErr error) (Box, bool, error)) error

MutateBox is the one way a box is changed: one cross-process read-modify-write. It takes the box's lock (pkg/filelock, whose design is tla/FileLock.tla), reads the box under it, hands the read to change and publishes what change returns, so no writer publishes a snapshot older than the state it overwrote (tla/FuseBox.tla: LostUpdate is the action this removes, and LockdownMonotone is the invariant that keeps -- a lockdown blown while a soft writer holds its read survives that writer's publish, because the soft writer reads it first).

change answers three things: the box to publish, whether to publish it, and a reason not to go on. It runs exactly once, under the lock, and it is where a caller's own policy on a read that failed lives -- ReadBox's error is handed to it unchanged, so ErrNoBox and an unreadable box stay told apart (note 1). Publishing nothing is not an error: a caller whose state already stands writes nothing and keeps the record it found.

A dry run (write false) neither takes the lock nor creates the lock file: it reads, lets change decide, and makes every check the write would make (planBox) without writing.

init is not a mutation and does not come through here: CreateBox publishes exclusively (atomicfile.NoReplace), so it refuses a box another writer made instead of replacing a state it did not read.

func PlanCreateBox added in v1.2.3

func PlanCreateBox(path string) error

PlanCreateBox is CreateBox with nothing written: every check the creation makes (the parent as MkdirAll would make it, no symlink parent, a directory this process can create in, nothing at the path), and the same error.

func PreserveUnreadable

func PreserveUnreadable(path string) (string, error)

PreserveUnreadable copies an unreadable box aside before it is replaced. It returns the destination it TRIED, so the caller can name it either way -- reporting where the bytes went and reporting that they could not be saved are both better than silence.

func Surface

func Surface(s string) string

Surface normalizes a surface name for storage and for matching. See notes 4 and 5: control characters fold to spaces before the lower-casing, which widens the class of spellings that count as one surface exactly as the lower-casing does. That is not a one-way "blocks more" guarantee -- it makes `check` refuse on more spellings AND makes `lift quarantine` remove more of them, because they are one surface in both directions.

func WriteBox

func WriteBox(path string, b Box) error

WriteBox replaces the fuse box atomically. See note 3. A symlink at the cleaned path is refused and is not followed.

Types

type Box

type Box struct {
	Lockdown   *Fuse           `json:"lockdown"`
	Quarantine map[string]Fuse `json:"quarantine"`
}

Box is the whole fuse box. Lockdown is a pointer so that absent, null and present are three distinguishable states rather than one zero value.

func ReadBox

func ReadBox(path string) (Box, error)

ReadBox returns the fuse box, or the reason it could not be read. See note 1.

A nil error means the box was read, and an empty Box then means VERIFIED CLEAR. A non-nil error -- ErrNoBox included -- means CANNOT TELL, and the only correct treatment of that is BLOWN.

func (Box) LiftQuarantine

func (b Box) LiftQuarantine(surface string) map[string]Fuse

LiftQuarantine removes EVERY quarantine entry matching surface (normalized, see note 4) and returns the removed entries under their stored keys, so the caller can announce exactly what was lifted and why it had been blown. An empty surface matches NOTHING, mirroring Quarantined: a missing argument must never empty the box.

Every match is removed, not just the first: the box is hand-editable, so two spellings of one surface can coexist, and a lift that removed only one would leave the surface still answering as quarantined -- a lift that verifies its own failure.

So the normalization in note 4 WIDENS this: the coarser the equivalence, the more spellings one lift removes. `lift "dis cord"` on a box holding "dis cord", "dis\tcord" and "DIS\x01CORD" removes all three, where a build without the fold removed one. That is the same design case already had (lifting "discord" removes "Discord"), it is what "they are one surface" means, and nothing is hidden by it: every removed entry is returned here under its stored spelling for the caller to announce.

THE SOFT HALF ONLY. The fuse design separates the powers: quarantine is your own decision in both directions, so this function exists; lockdown is hard -- a blown fuse is not reset, it is REPLACED, and only in a live conversation with the person you work with -- so no LiftLockdown exists here, and none may be added.

func (Box) Quarantined

func (b Box) Quarantined(surface string) (string, Fuse, bool)

Quarantined answers whether this surface is blocked, returning the key AS STORED so a refusal can quote the file rather than the caller's spelling of it.

An empty surface matches NOTHING, and the caller must say so out loud: a check with no surface has verified only that there is no lockdown, and a printed claim must never outrun what was measured.

func (Box) Surfaces

func (b Box) Surfaces() []string

Surfaces lists the quarantined surfaces in a fixed order. Map iteration is randomized, so an unsorted listing would print a different order every run -- and a status output that reorders itself is one a reader stops diffing.

type Fuse

type Fuse struct {
	At     string `json:"at"`
	Reason string `json:"reason"`
}

Fuse is one blown fuse: when, and why. Both are recorded so a fuse found at 2am can be audited without asking anyone, and both are read back defensively because a person HAND-EDITS this file -- that is the only lockdown-replacement mechanism there is.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL