Florete

Artifact Store

How A0 writes, checks and reads compiled artifacts, on the operator's machine and on a node

rete compile produces each node's artifacts, rete issue-bundle packs one node's, flor enroll lands them on that node, and flor-vertex reads its own. This page designs the one library all four reach them through: what an artifact is while it is moved, where a node's artifacts rest, what is checked before one is written, and what a consumer gets back.

If you write or read artifacts from a command or a workload, Using the store is the whole surface. If you are implementing the store, the sections after the Shape take a write through its checks and then describe the files beneath. If you are changing the design, start at the Shape and the Decision Log. How a bundle carries artifacts to a node is Enrollment's, and what a vertex does with the configuration it reads is A0's Design's.

Using the store

Everything goes through a handle, and no path comes out of one. A caller opens the store for one instance and asks it for documents; it never joins a path of its own, on either side of the rete. A handle is opened with a trust domain, the trust anchors and, where the store belongs to one node, that node — the values the scope's identity store holds or, in the run that first establishes it, the values its check has just accepted. A scope's handle names its node, its identity store beside it in the scope root; the operator's names none, that instance holding every node's artifacts, and its identity store is the one in rete's own tree.

What the handle names is what a write is held to. A handle bound to a node admits that node's documents and refuses any other, so a bundle carrying another node's artifact never lands. A handle bound to none sorts each document by the node its own address carries, and what it checks is that the document parses, belongs to this rete, and joins a coherent set.

An artifact is addressed to a workload, and that address is its key. The envelope names one node-scoped workload — spiffe://flor.demo/workload/alpha/hop — and one plane, and the pair is how every caller asks for it. A node's set is its mgmt documents at one version, and the version numbers the set's content — its payloads and its membership — so a set signed again over the same content keeps its version. In A0 a set holds one document, its vertex's.

Two calls write, and each leaves exactly what it was given. flor enroll replaces a set, its scope's. rete compile rebuilds the store, every set at once, so a node the source no longer names stops being shipped and an artifact no longer compiled stops being kept. An admitted write reports the version the store held before and the version it admitted; equal versions mean the content did not change.

Two calls read. A consumer asks for one document by its key and gets it back with its envelope parsed; the payload is handed over as the type the caller names, which nothing else parses, or raw. The other read is a node's whole set: rete issue-bundle packs it, and rete compile reads each node's as the ladder its next versions are built on (Validate & Compile). Deciding versions and signing are compile's; the store compares nothing but versions.

A write is refused whole, before anything is written. The store refuses a document whose envelope it cannot read, one addressed to another rete or another node, one whose signature does not check out, and a set older than the mark. Any of those refuses the batch it arrived in, leaving what the store holds untouched. A consumer's read refuses little: a key naming another node than the handle's is refused, as a write would refuse it; a document of the handle's own node that is not there is reported absent, and the caller decides what that means; and a document stamped for a newer release than the one reading it is the same upgrade error a write would raise.

Shape

STRUCTURE

  subject     the artifact store: one library through which every A0 user
              writes and reads compiled artifacts, with no process behind it

  users       rete compile          reads each node's set as its version
                                    ladder, then rebuilds the store
              rete issue-bundle     reads one node's set
              flor enroll           replaces its scope's set
              flor-vertex           reads its own document

  layers      documents  an artifact's bytes, fixed when it is signed, with
                         its envelope parsed beside them; the payload is
                         handed out raw, or as the type the caller names
              families   one payload model per family, shared by that
                         family's producer and consumer, outside the store
              store      sets of documents over a backend; parses envelopes,
                         never a payload
              backend    where documents rest; files, the only backend

  key         (workload, plane), both claims of the envelope; the workload
              is a node-scoped workload's SPIFFE ID

  set         one node's mgmt documents at one version, at least one member

  version     numbers a set's content: its members' payloads and its
              membership; a new signature over the same content keeps it

  instances   operator   the compiled tree, every node's set, committed to git
              node       the scope, its own node's set

  handle      every user, producer or consumer, opens one; no path leaves it
              opened with a trust domain, the anchors and, on a scope, a node
              — what the identity store holds, or what its check has just
              accepted in the run that establishes it
              a handle naming a node admits that node's documents alone
              one naming none admits every node's, each grouped into its own
              set by the node its own address carries

  layout      <plane>/<node>/<name>.json, the workload's path below its
              kind, on both instances
              operator   <compiled tree>/mgmt/<node>/<name>.json
              node       <scope root>/artifacts/mgmt/<node>/<name>.json

FLOW

  write       two writes; afterwards the store holds exactly what it was
              given
                replace a set      one node's set — enroll
                rebuild the store  every set — compile, removing nodes the
                                   source no longer names

              check, writing nothing
                1  gate each envelope's schema_version
                2  placement   the workload parses as a node-scoped
                               workload ID; its trust domain is the
                               handle's; its node is the handle's where the
                               handle names one, and otherwise the set it
                               joins; the plane is the write unit's; no two
                               members share a key; one version per set
                3  signature   the signer's certificate against the anchors
                               as of now, its kind against the plane, then
                               the signature
                4  freshness   against the mark
                                 ├─ older   → refused
                                 └─ otherwise admitted
              the caller records its run
              write
                5  every member, whole, by rename
                6  remove every member the batch does not name

              a refusal at 1–4 refuses the whole batch, and nothing is written
              an admitted write reports the version held before and the
              version admitted; equal versions mean unchanged content

  read        one document by key
                1  the key: the workload the caller names, and the plane (mgmt)
                2  another node than the handle's → refused
                3  absent → reported
                4  gate the envelope's schema_version
                5  hand back the document; its payload is the caller's to
                   parse and gate, or to take raw
              also offered: one node's set

RULES

  bound         the store promises no more than a directory of files written
                by one process at a time: each file whole or absent, a set
                converging when its writer re-runs, nothing read while it is
                written, no history beyond what is on disk, reads by key

  admission     a write is the one place a document is verified; no read
                verifies

  placement     a document's place comes from its envelope alone

  pruning       a write removes what it was not given: a member missing
                from a node's set, and, in a rebuild, a node missing from
                every set

  opacity       the store parses envelopes and never a payload; it compares
                versions, never bytes

  freshness     the mark is the highest version admitted, kept per node
                on files it is the highest stamp present, with no record of
                its own
                nothing overrides a refusal; the way past one is a newer set

  integrity     the store keeps no journal; members at different versions
                mark an interrupted write, and re-running the same batch
                completes it

  files         directories 0700 and files 0600, each created with its final
                mode by the account that writes; a member is written to a
                temporary file in its own directory, flushed, then renamed

  locking       the store takes no lock; a node-side caller holds the scope
                lock across a check and its write, the vertex for its life

  one module    nothing outside the store joins a path under a store's root

Admitting a write

Admission is one step and one place. Every writer — a compile on the operator's machine, an enrollment on a node — passes the same checks, and no reader repeats them. A document that is in a store was admitted to it, and what reads it afterwards trusts it, as a vertex trusts the material the store hands it.

The checks run over the whole batch before any of it is written. First the envelope's schema_version, which is fail-closed: a document stamped newer than the binary reading it is an upgrade error rather than a parse failure. Then placement: the address parses as a node-scoped workload, its trust domain is the handle's, its node is the handle's where the handle names one and otherwise the set the document joins, the plane is the one being written, no two members share a key, and one version runs through each set. Then the signature: the certificate the envelope carries is validated against the handle's anchors as of now, its kind is checked against the plane, and only then is the signature itself checked.

The check and the write are separate calls, because a caller has work between them. flor enroll checks the bundle's identity material and its artifacts before either store writes anything, then records that its run has begun, then writes both. So a bundle that would be refused is refused with the scope root untouched and the bundle still on disk.

On a first enrollment the signature check shows only that the documents will work. The anchors the handle carries arrive in the same bundle, so the check catches a wrong signer or a corrupted envelope and proves nothing against whoever wrote the bundle. On every later enrollment the anchors are the ones the scope pinned, and the same check is a judgement about authority.

The check's clock is safe because the ledger keeps signatures current. A signer's certificate is checked against the present time, and compile's ledger re-signs whatever a rotated signer signed (Validate & Compile), so nothing a write carries is refused for its age once the signer is rotated.

Freshness

The store never goes backwards. It keeps a mark per node, the highest version it has admitted, and refuses a set older than it. A bundle that has been overtaken — cut before a revocation and enrolled after it — is refused naming both versions, so a node cannot be rolled back by delivering stale material to it. Nothing overrides that refusal: the way past one is a newer set. A rollback is therefore a revert of the sources, compiled forward at a higher version; reverting the compiled tree along with them puts back versions the nodes have already passed, and every node refuses them.

Anything not older is written whole, an equal version included. Two sets at one version carry the same content, so what an equal-version write can change is the signature, and writing it is how a re-signed set reaches the compiled tree and, at its next re-key, a node. Re-enrolling to re-key a node therefore rewrites its set and reports the same version before and after, which the caller reads as unchanged. A write that was interrupted leaves members at different versions, and re-running it completes it.

A version issued twice is not the store's to catch. The compiled tree is the ledger, and rete issue-bundle refusing an uncommitted tree is what keeps a number from being handed out twice. A set arriving at the version already held is, as far as the store can tell, the content it holds, and it is written like any other.

A node removed from the source takes its ladder with it. A rebuild removes the node's set, and with it the only record of the versions it reached. Declared again later, it starts low, below the mark its host still holds, and its next enrollment is refused until that scope root is removed by hand.

Storing documents on files

A store is a directory, and the design promises nothing a directory does not give. Each file is written whole or not at all; a set converges when its writer re-runs; nothing reads a scope while it is being written; there is no history beyond what is on disk; and a read is a key. Anything more — readers during a write, a switch across several files at once, sets kept for rollback, a mark of its own — is a different backend's to offer, not a stronger file layout's.

Documents lie under their plane, then their node. The operator's instance keeps <compiled tree>/mgmt/<node>/<name>.json, and a scope keeps <scope root>/artifacts/mgmt/<node>/<name>.json: the workload's own path below its kind, under the plane that authorised it. One directory is one node's set, which is what a write replaces and what issue-bundle reads.

Every directory is created 0700 and every file 0600, each with its final mode, by the account that writes. A member is first written to a temporary file in its own destination directory and flushed there, so the rename that puts it in place never crosses a filesystem, and a power cut leaves either the old file or the new one whole.

A member is written by one rename, and the pass that follows removes what the batch did not name. The store keeps no journal: an interrupted write leaves members at different versions, which is what says the run did not finish, and the writer's own record — flor enroll's receipt on a node, git status on the operator's machine — says whose run it was. Re-running the same batch completes it.

The store takes no lock. On a node the scope's lock is what excludes everyone else, and it is held across a check and its write, and for as long as a vertex serves; it guards the identity store and this one together, which neither could do alone. On the operator's machine there is one person and one repository.

Evolution sketches

Recorded so that nothing above forecloses them. None is designed, and each names the property it needs from this design.

  • A backend that is not a directory. A store serving readers while it is written, keeping earlier sets, or holding a mark of its own, is a second backend rather than a richer file layout — a document database, most likely, where "write this set, serve that document" is the engine's problem rather than this design's. Owed: the calls stay what they are, and the mark stays the store's rather than any file's (ART-A0-01, ART-A0-02).
  • A supervisor that admits artifacts. Where a supervisor receives a node's artifacts from a channel of its own, it writes them through the same call an enrollment uses, and its own checks are already the store's: the verification and the mark are what an intake would otherwise carry (ART-A0-09, ART-A0-15). Owed: nothing on a node may need artifacts staged anywhere before they are admitted; and a set arriving over a channel not trusted to deliver it whole carries a statement of its own membership inside what is signed — a root document naming its members, or a list of digests as a bundle's manifest has — since the store takes a set as given and cannot tell a withheld member from a removed one.
  • A relay that serves artifacts it cannot parse. A cache between the operator and a node stores documents byte for byte and serves them by key and by version. Owed: documents keep their producer's bytes, and a set's version is the stamp each member carries, readable from any one of them; and whatever a relay serves as a set carries its membership inside what is signed, as the supervisor sketch owes (ART-A0-03, ART-A0-15).
  • Artifacts that arrive one at a time. A stream of decisions for one workload has no set to be coherent with: each document carries its own version and replaces its predecessor. Owed: the key already carries the plane, so a second plane needs a write of its own and no new vocabulary (ART-A0-06).
  • Access narrowed per process. Every process that can open a handle can read every document in that store, as it can read every key in the identity store. A custodian standing behind the same calls could hand each workload only what is addressed to it. Owed: consumers read by key and never list, so there is a single read to narrow (ART-A0-12).
  • An issuance ledger apart from the source. The source is authored state, branched and reverted at will; the ladder records what was issued and must never move back. One working tree holding both gives the ladder the source's life: a revert rewinds it, a removed node loses it, and a clone must carry the whole compiled tree to keep it. An append-only record of what each node was last issued — its version, a digest of its content, its signer — kept where a revert of the source cannot reach, lets compile compare its projection with what was issued rather than with the tree: a revert compiles forward, a node declared again continues its ladder, and the compiled tree becomes a regenerable output. The same holds for any plane whose ladder compile keeps, and a record of issued credentials is the same kind of thing. Owed: the ladder is read through the store, never from git or a path, so its source can change behind the same read; and the store compares a version only as not older (ART-A0-04, ART-A0-13, ART-A0-15).

Decision Log

Entries are grouped by what a change would touch.

The store

ART-A0-01. Promise no more than a directory of files written by one process at a time. The store's guarantees about storage are what such a directory gives by itself: each file whole or absent, a set converging when its writer re-runs, nothing read while it is written, no history beyond what is on disk, reads by key. The bound covers storage only; what a write checks (ART-A0-08, ART-A0-09, ART-A0-15) is a rule about content, and not bounded by it. Because a bound that names the medium drops a mechanism the moment it is proposed — concurrent readers, an atomic switch across files, a journal, retained sets, a mark kept apart, queries beyond a key — with no argument needed. Accepting that a user needing any of those needs another backend, not a stronger file backend. Rejected a git working tree as the reference, which fits the operator's instance only and holds one writer by convention; a tar extracted into place, which says nothing of reads; the identity store, the same bound borrowed from a component a reader may not know; no bound beyond admission, which bounds nothing.

ART-A0-02. Build the store as a library: documents over one concrete file backend. The store is linked into each user, with no process, socket or daemon. It is two layers: documents, which carry every rule and know no storage, and a file backend, which knows no rules. The backend is concrete; no backend interface is drawn. Because A0 runs one long-lived process per node and has no supervisor to run another, and a library serves the operator's short-lived commands as well. Because the document layer is what every user speaks whatever stores it, so rules and storage are separated from the start, while an interface drawn over one implementation would be shaped by that implementation alone. Accepting that a second backend brings its interface with it, drawn from two implementations; callers do not change, speaking documents and sets. Revisit when a backend other than files is needed. Rejected a process of its own, which A0 cannot run; one module fusing rules with files, which a second backend would have to take apart; a backend interface with one implementation.

ART-A0-03. Speak documents: fixed bytes with a parsed envelope, and a payload handed out raw or typed by the caller. A document is an artifact's bytes, fixed when its producer signs and serialises it and never re-serialised after, with its envelope parsed beside them. It hands its payload out raw, or as the type its caller names, gating that family's version. Payload models are modules of their own, one per family, shared by that family's producer and consumer; the store depends on none of them. Because a signature covers a canonical form and a compiled tree is reviewed as a diff, so the bytes a producer wrote are the bytes every later party holds. Because the envelope checks need one shared home, while a payload is parsed only by the party that consumes it — the one component that may parse a family is that family's consumer. Because a raw payload is payload-blind: it lets a party compare payloads, as the version ledger does, without parsing one. Rejected opaque blobs, which push every envelope check back to each caller; a store that parses payloads, which makes it enumerate every family; a parsed value re-serialised on write, which breaks the signature and the diff.

Users and keys

ART-A0-04. Route every A0 user through one store, with two instances. rete compile reads each node's set as its version ladder and rebuilds the store; rete issue-bundle reads one node's set; flor enroll replaces its scope's set; flor-vertex reads its own document. The compiled tree is one instance, holding every node's set and committed to git; a scope is the other, holding its own node's set. Both use the same file backend and layout. Because a tree and a scope differ only in how many nodes they hold, and every place that knows where an artifact lives or what admits one is then one module — issue-bundle included, which otherwise reads back compile's layout by its own path code. Because the version ledger is the producer's, and the store only holds and admits: a store that versioned and signed would verify signatures it had just made, which leaves the operator's instance no party independent of the producer. Accepting that the scope's layout is fixed by the compiled tree's, which is a format people review; and that two modules reason about versions — compile decides them, and the store refuses an older one. Rejected the store owning the ledger — comparing, versioning, signing — which makes its own admission circular, hands the signer's key to a library that runs on every node, and makes the store the envelope's producer; a store query answering the ledger's question, which only compile asks and a set read already answers; the store on the node alone, which leaves the operator's tree to path arithmetic; the document layer everywhere with sets on the node only, the same defect one layer up; a separate backend per instance from the start, a divergence nothing in A0 needs.

ART-A0-05. Give every user a handle on the whole store, opened with the scope's identity. Producers and consumers alike open a handle; no path leaves the store. A consumer sees the whole store and reads one document at a time. A handle is opened with a trust domain, the anchors, and a node where the store belongs to one; its caller takes all of them from the identity store, on either side of the rete — the values it holds or, in the run that first establishes it, the values its check has just accepted. The operator's handle differs in naming no node. What the handle names is what admission enforces: a handle bound to a node admits that node's documents alone, and a handle bound to none admits every node's, grouping each by the node its own address carries. Because artifacts then reach a process as identity material does (ID-C0-28): asked for by key through a module that owns where it lives. Because every caller already opens the identity store for itself, so handing its values over keeps the two stores independent; the artifact store depends on identity's certificate checks, never on its store. Accepting that any process holding a handle can read every document on its node, as it can every key today — artifacts carry no secret; that a caller handing over a wrong node defeats the node check, as any check is bounded by its input; and that a store belonging to no node cannot check a destination, only its form and its rete. Rejected a resolved file handed to each consumer, which gives it a path and leaves nothing for a narrower access to stand behind; a handle narrowed per consumer, a narrowing nothing in one account can enforce; content handed in by a supervisor A0 does not have; the store reading the identity store itself, which couples two stores kept independent; a copy of the scope record of its own.

ART-A0-06. Key a document by (workload, plane), the workload one SPIFFE ID in the envelope. The envelope names its destination in one claim, workload, holding a node-scoped workload's SPIFFE ID — spiffe://flor.demo/workload/alpha/hop — and the plane stays a claim of its own. The key reads workload first. Because an artifact is one workload's configuration, its plane choosing which of the workload's configurations it is (ID-C0-09), so the plane narrows the workload and the key reads that way. Because one typed claim validates by parsing: the kind, the trust domain and the name's syntax come from the ID's own rules, and the document states which rete it belongs to. Because a claim named for the one kind it may hold makes any other ID a contradiction at the claim itself, the ID's kind segment being the check. Accepting that a relay extracts the node from the ID; that the claim's name repeats the ID's kind; and that the key reads workload first while the layout reads plane first (ART-A0-13). Rejected node and name as separate claims, two fields for one fact with no trust domain; target, a principal's role in a flow; destination, which reads as traffic in a proxy; addressee, a relation the claim's position already states; (plane, workload), which reads the key against its meaning.

Writes and admission

ART-A0-07. Write whole sets, and remove what a write was not given. A set is one node's mgmt documents at one version, with at least one member. There are two writes: replace a set, one node's, and rebuild the store, every set at once. Afterwards the store holds exactly what it was given: a member missing from a node's set is removed, and so, in a rebuild, is a node missing from every set. An admitted write reports the version held before and the version admitted. Besides a document, a read offers one node's set. Because a node's set is the statement of what runs on it, so a write that left members behind would keep a renamed or removed vertex's artifact for the vertex to find. Because the compiled tree must hold exactly what the source names, and the rule stated once in the store serves both sizes. Because an empty set carries no version, and would erase the mark. Accepting a whole-store write only the operator's instance uses; and that removing every artifact from a node is not a write. Rejected additive writes, which leave stale members for the vertex to find; readers filtering stale members by version, which re-implements one delete in every reader; per-node writes with the caller computing removals; document-level writes with sets as the caller's convention, which hands the set's rule to every caller.

ART-A0-08. Check placement at every write, and check the whole batch before writing any of it. A write gates each envelope's schema_version, then checks that each workload parses as a node-scoped workload ID whose trust domain is the handle's, that its node is the handle's where the handle names one and otherwise the set the document joins, that the plane is the write's, that no two members share a key, and that one version runs through each set. A write is two steps: a check that writes nothing, then the write; the caller records its run between them. Because the store is where an envelope becomes a place, so each fact that decides placement is checked where it is used, and a bundle cut for another node never lands on a scope. Because a store belonging to no node has no destination to enforce: a document's own address is what sorts it, and what is left to check is that it parses, that it belongs to this rete, and that the set it joins is coherent. Because enroll refuses before its receipt claims a run, and writes the identity store and the artifact store together, the identity store's import having the same two steps (ID-C0-31). Accepting that an admitted batch is valid only under the lock it was checked under. Rejected a node check and a path-component check alone, which let a malformed name reach a path; comparing the trust domain read out of an unverified signer certificate, a check that looks like authority and carries none; one call validating internally, which makes enroll write its receipt before it knows; a dry-run flag, which runs the checks twice.

ART-A0-09. Verify signatures at every write, and nowhere else. A write validates each signer's certificate against the handle's anchors as of now, checks its kind against the plane, then checks the signature. Every writer passes this check, and no read repeats it. Because a write is where every document enters a store, so admission is one call for every writer, and a producer's signing is checked before its output is committed. Because a read that verified would re-authorise at every start, and an expired signer would then stop a node that restarted (ADR-0015). Accepting that on a fresh enrollment the anchors arrive in the same bundle, so there the check shows only that the documents will work — a wrong signer or a corrupted envelope — and proves nothing against whoever altered the bundle; on a re-enrollment it checks authority against the pinned anchors. Accepting that the check depends on compile's ledger re-signing whatever a rotated signer signed; without that, a signer's expiry would refuse every later rebuild, re-key and late enrollment. Rejected verifying nowhere on a node; verification as a write option, off for compile, which skips it for the writer where signing starts; the validity window skipped on the operator's instance, one rule per instance; the window measured against the document's own time, which lets the holder of an expired key choose its validity; verifying at read.

ART-A0-10. Superseded by ART-A0-15.

ART-A0-15. Refuse a set older than the mark, write every other set whole, and number content rather than envelopes. The mark is the highest version the store admitted, per node. A write refuses a set older than it and writes any other whole, an equal version included. A version numbers a set's content — its members' payloads and its membership — so a new signature over the same content keeps its version. Nothing overrides a refusal. On files the mark is the highest version stamped on a member present, with no record of its own. Because a stale bundle enrolled after a newer one would otherwise roll its node back, a revocation included, and the way past a refusal is always a newer set — a rollback being a revert of the sources compiled forward, never a revert of the compiled tree. Because with versions numbering content, two sets at one version differ only in their signature, so writing the newer delivery can only bring in a fresher one — which is how a re-signed set reaches the compiled tree and, at its next re-key, a node — and a re-run after an interrupted write needs no branch of its own. Because equal versions then mean unchanged content, so a caller learns "unchanged" from the versions a write reports, and the store compares no bytes. Because a version issued twice is a fault of the ledger, which the commit gate on issue-bundle guards (ENRL-C0-35), not of the store. Accepting that one version can exist under several signatures, a node keeping the one it admitted until it is re-enrolled; that a version reissued with different content overwrites what a node holds; that a node whose compiled tree is lost for good is re-enrolled only once the tree is restored, or its scope root is removed by hand; and that a node removed from the source and declared again restarts its ladder below its host's mark, with the same remedy. Revisit when the version ladder is designed apart from the compiled tree. Rejected accepting a rollback; a warning, which lands after the harm; an override, which has no case a newer set does not cover; keeping the held copy at an equal version, which strands a re-signed set outside the tree; one equal-version rule per write, two rules where one serves; a version bumped by a re-signature, which makes every rotation a fleet-wide change; comparing bytes at an equal version, a fork check for the ledger's fault; freshness left to each caller, which splits admission in two.

ART-A0-11. Write each file by rename, with no journal and no lock of the store's own. A member is written to a temporary file in its own destination directory, flushed, and renamed into place; after the writes, a pass removes what the batch did not name. Every directory is created 0700 and every file 0600, each with its final mode, by the account that writes. Members at different versions mark an interrupted write, which the caller's record of its run says, and re-running the batch completes it. The store takes no lock: a node-side caller holds its scope's lock across a check and its write, and the vertex holds it for its life. Because nothing reads a scope mid-write and nothing starts on an interrupted one, so an interruption needs only convergence — the identity store's rule (ID-C0-32) applied to documents. Because the scope lock already excludes every reader and writer on a node, and guards both stores together, which a lock inside either could not. Because a rename is atomic only within one filesystem, and a file renamed before it is flushed can survive a power cut empty, so a temporary file beside its destination, flushed first, is what makes each file whole or absent. Because the modes are the scope root's own rule, which the identity store also states for its files (ID-C0-28), and a mode set at creation leaves no moment when a file is more open than it should be. Accepting that the operator's instance has one writer by convention, one operator machine and git; and that it takes the same modes, which git keeps no record of. Rejected the writer's umask, which makes a store's modes depend on whoever ran the write; a mode changed after creation, which leaves a window; a temporary file outside the destination directory, whose rename can become a copy; a staged directory swapped in, whose recovery logic serves a live reader A0 does not have; a versioned directory behind a pointer, the same; the store taking the scope lock, which its owners hold across more than the store; a lock of its own, the scope lock's extent twice.

Reads

ART-A0-12. Read one document by the key its caller names, and gate its envelope again. A consumer names the workload and the plane. On a handle bound to a node, a key naming another node is refused; a missing document of the handle's own node is reported absent, and the caller decides what that means. The read gates the envelope's schema_version again and verifies nothing. No consumer lists keys. Because the name reaches a consumer from outside the store, so its read is one key and nothing else, and access narrowed later has only single reads to narrow. Because a handle bound to a node deals in that node's documents alone, for reads as for writes, and a key naming another node is a caller's mistake rather than an absence — a vertex forms its key from the handle's own node. Because a binary older than the store's documents would otherwise meet an envelope minor it cannot read as a parse error indistinguishable from corruption; the gate is about parsing, not trust. Rejected finding the set's sole member, a listing for consumers; reporting another node's key as absent, which hides a caller's mistake as an ordinary outcome; gating at replace only, which leaves the older-binary case to a strict parse error.

Layout and the bundle

ART-A0-13. Lay documents out plane first: <plane>/<node>/<name>.json. On both instances: <compiled tree>/mgmt/<node>/<name>.json, and <scope root>/artifacts/mgmt/<node>/<name>.json. The path is the workload's below its kind, under its plane. Because every write works on one plane, so a node's set is one directory, and a plane is a separable tree. Because the kind leads and the node follows, as the identity store keys principals (ID-C0-30), and keeping the node segment on a scope is what gives both instances one layout. Accepting that reviewing one node across planes takes a glob. Rejected node first, which groups a node's planes that no operation touches together; dropping the node segment on a scope, two layouts for one backend.

ART-A0-14. Keep the bundle a container of Enrollment's, outside the store. Enroll hands the store the documents it extracted; the store receives documents and knows no bundle entry. Enrollment renders an entry's name from its envelope with one function, which names entries in issue-bundle and checks them in enroll. Because the bundle is a wire format read across releases that skew, and the store's layout a repository format; sharing one would turn every layout change into a manifest version. Because the documents already carry their key, so an API taking labels beside them states one fact twice. Rejected the bundle as a backend of the store, which lets a library change alter a versioned container; the set's directory tarred as it lies, whose entry names become destinations; each tool spelling entry names itself, three owners for one spelling; labels passed to the store, a second copy of the envelope's claims.

On this page