Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 25 additions & 18 deletions dag/gunbc/fabric/fabric_storage_serve.dag
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import std.types { String, NonEmptyStr, Int, List, CommitSha }
import extdeps.http.server { ServeWireResponse }
import std.fabric_storage {
FabricObject, fabric_object_decode, fabric_object_ref_of_wire, FabricHeadExpectation, ExpectHeadAbsent, ExpectHeadAt,
FabricReplyUndecodable, FabricHeadNameRefused,
FabricReplyUndecodable, FabricHeadNameRefused, FabricWriterRefused,
FabricPutRefused, FabricHeadReadRefused, FabricHeadAdvanceRefused, FabricClosureRefused,
}
import gunbc.fabric_storage_file_store {
FabricStorageFileRoot, fabric_storage_file_head, fabric_storage_file_put, fabric_storage_file_advance, fabric_storage_file_closure,
}
import gunbc.fabric_storage_wire { fabric_storage_wire_head, fabric_storage_wire_put, fabric_storage_wire_advance, fabric_storage_wire_closure, fabric_storage_wire_identity }
import extdeps.tailscale.identity { tailnet_identity_of }
import gunbc.fabric_writer_roster { fabric_served_writer_admission_over, fabric_served_writer_roster, FabricServedWriterAdmitted, FabricServedWriterRefused }
import gunbc.fabric_storage_placement { fabric_storage_placement, FabricStoragePlaced, FabricStorageUnplaced, fabric_storage_route_prefix, fabric_storage_placed_file_root, FabricStoragePlacedRootReady, FabricStoragePlacedRootRefused }

// THE FABRIC DB ENDPOINT: the store side of the served handler bound to std.fabric_storage. One
Expand All @@ -30,19 +31,15 @@ import gunbc.fabric_storage_placement { fabric_storage_placement, FabricStorageP
//
// ACCESS: the backend binds loopback only and is reached through `tailscale serve`, the same
// deployment the dashboard uses, so only tailnet members reach it and the Tailscale-User-Login
// header is trustworthy exactly to the extent extdeps.tailscale.identity states. DECLARED GAP: no
// principal is refused here. Every tailnet member that reaches the mount may append; the caller's
// login is not yet joined to a writer roster, because no roster of fabric writers is modeled.
// This is a declared rung drop, rostered as gunbc.rung_drop.fabric_storage_append_principal_unrefused, whose
// restoration trigger is an observed fabric writer principal roster this handler refuses outside of.
// THE OBSERVATION THAT TRIGGER NAMES IS TAKEN THROUGH THIS DOOR: the `identity` operation echoes
// the login the proxy presented for the request, so each fleet writer reads the principal it
// presents from the one place the roster will be consulted (gunbc.fabric_writer_identity_observe).
// It writes nothing and reads no head. WHAT A ROSTER HERE CAN AND CANNOT WALL: the login is the
// tailnet USER behind the request, so the roster admits a user identity, not an exact node; and
// the placed host's own writes never arrive here (FabricStorageLocalFiles runs the file store in
// process), so this door's roster is the wall over SERVED writers only -- the drop row carries the
// local population as its second half.
// header is trustworthy exactly to the extent extdeps.tailscale.identity states. THE WRITER WALL:
// a put or advance is admitted only for a login in gunbc.fabric_writer_roster's served roster --
// derived from the readings gunbc.fabric_writer_identity_observe took through this door's own
// `identity` operation -- and an absent login is refused, never admitted as anonymous. The refusal
// is a typed store outcome (FabricWriterRefused) and touches no file. Reads (head, closure) and the
// identity echo stay open to the tailnet: they move nothing. WHAT THIS WALL CANNOT SEE: the placed
// host's own writes never arrive here (FabricStorageLocalFiles runs the file store in process; their
// wall is the store directories' ownership, gunbc.fabric_storage_placement fabric_storage_directory),
// and the roster is a USER grain, not a node grain.
//
// EVERY TYPED OUTCOME IS HTTP 200. The status says the protocol was answered; the arm is in the
// body. A non-200 means the request itself was not this protocol (unknown operation, malformed
Expand Down Expand Up @@ -84,8 +81,9 @@ fn malformed(detail: String) -> ServeWireResponse {
// put <canonical preimage>
// advance <name> absent <target> | <name> at <expected-ref> <target>
// identity (no body)
fn fabric_storage_respond(root: FabricStorageFileRoot, operation: String, body: String, tailscale_identity: String) -> ServeWireResponse {
fn fabric_storage_respond(root: FabricStorageFileRoot, roster: List<NonEmptyStr>, operation: String, body: String, tailscale_identity: String) -> ServeWireResponse {
let ws = words(text: body)
let admission = fabric_served_writer_admission_over(roster: roster, identity: tailnet_identity_of(header_value: tailscale_identity))
match operation {
"identity" => ok(body: fabric_storage_wire_identity(identity: tailnet_identity_of(header_value: tailscale_identity)))
"head" =>
Expand All @@ -105,11 +103,18 @@ fn fabric_storage_respond(root: FabricStorageFileRoot, operation: String, body:
}
}
"put" =>
match admission {
FabricServedWriterRefused { refusal: r } => ok(body: fabric_storage_wire_put(put: FabricPutRefused { fault: FabricWriterRefused { refusal: r } }))
FabricServedWriterAdmitted { login: _ } =>
match fabric_object_decode(preimage: body) {
Absent => malformed(detail: "put body is not one fabric object")
Present { value: o } => ok(body: fabric_storage_wire_put(put: fabric_storage_file_put(root: root, object: o)))
}
}
"advance" =>
match admission {
FabricServedWriterRefused { refusal: r } => ok(body: fabric_storage_wire_advance(advance: FabricHeadAdvanceRefused { fault: FabricWriterRefused { refusal: r } }))
FabricServedWriterAdmitted { login: _ } =>
match head_name(w: word(ws: ws, i: 0)) {
Absent => malformed(detail: "advance names no head")
Present { value: n } =>
Expand All @@ -118,6 +123,7 @@ fn fabric_storage_respond(root: FabricStorageFileRoot, operation: String, body:
Present { value: req } => ok(body: fabric_storage_wire_advance(advance: fabric_storage_file_advance(root: root, name: n, expected: req.expected, target: req.target)))
}
}
}
_ => text_response(status: 404, body: join(["fabric_storage/1 no operation ", operation, "\n"], ""))
}
}
Expand Down Expand Up @@ -153,13 +159,14 @@ fn fabric_storage_operation_of(path: String) -> String? {
if starts_with(s: path, prefix: prefix) { Present { value: substring(s: path, start: length(prefix), end: length(path)) } } else { none }
}

fn fabric_storage_serve_handle_over(root: FabricStorageFileRoot, method: String, path: String, body: String, tailscale_identity: String) -> ServeWireResponse {
// The roster is a parameter so a claim can supply one; the served entry below binds the derived one.
fn fabric_storage_serve_handle_over(root: FabricStorageFileRoot, roster: List<NonEmptyStr>, method: String, path: String, body: String, tailscale_identity: String) -> ServeWireResponse {
if method != "POST" {
text_response(status: 405, body: "fabric_storage/1 only POST is admitted\n")
} else {
match fabric_storage_operation_of(path: path) {
Absent => text_response(status: 404, body: join(["fabric_storage/1 no route ", path, "\n"], ""))
Present { value: op } => fabric_storage_respond(root: root, operation: op, body: body, tailscale_identity: tailscale_identity)
Present { value: op } => fabric_storage_respond(root: root, roster: roster, operation: op, body: body, tailscale_identity: tailscale_identity)
}
}
}
Expand All @@ -173,7 +180,7 @@ fn fabric_storage_serve_handle(method: String, path: String, body: String, tails
FabricStoragePlaced { host: _, store_root: r, endpoint: _ } =>
match fabric_storage_placed_file_root(store_root: r) {
FabricStoragePlacedRootRefused { detail } => text_response(status: 503, body: join(["fabric_storage/1 the store's entry mode is refused: ", detail as String, "\n"], ""))
FabricStoragePlacedRootReady { root } => fabric_storage_serve_handle_over(root: root, method: method, path: path, body: body, tailscale_identity: tailscale_identity)
FabricStoragePlacedRootReady { root } => fabric_storage_serve_handle_over(root: root, roster: fabric_served_writer_roster(), method: method, path: path, body: body, tailscale_identity: tailscale_identity)
}
}
}
8 changes: 7 additions & 1 deletion dag/gunbc/fabric/fabric_storage_wire.dag
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import std.fabric_storage {
FabricObjectRef, FabricObject, FabricStoredObject, fabric_object_ref_wire, fabric_object_ref_of_wire, fabric_object_preimage, fabric_object_ref_of, fabric_object_ref_eq,
fabric_objects_decode_stream,
FabricHeadReading, FabricHeadAbsent, FabricHeadAt, FabricHeadExpectation, ExpectHeadAbsent, ExpectHeadAt,
FabricStorageFault, FabricStoreUnreachable, FabricStoreRefused, FabricObjectMissing, FabricObjectCorrupt, FabricReplyUndecodable, FabricHeadNameRefused,
FabricStorageFault, FabricStoreUnreachable, FabricStoreRefused, FabricObjectMissing, FabricObjectCorrupt, FabricReplyUndecodable, FabricHeadNameRefused, FabricWriterRefused, WriterLoginAbsent, WriterLoginUnrostered,
FabricPut, FabricObjectStored, FabricPutRefused,
FabricHeadRead, FabricHeadObserved, FabricHeadReadRefused,
FabricHeadAdvance, FabricHeadAdvanced, FabricHeadMoved, FabricHeadAdvanceRefused,
Expand Down Expand Up @@ -65,6 +65,10 @@ fn fault_words(fault: FabricStorageFault) -> List<String> {
FabricObjectCorrupt { object: o, detail: d } => ["corrupt", ref_word(object: o), d as String]
FabricReplyUndecodable { detail: d } => ["undecodable", d as String]
FabricHeadNameRefused { name: n } => ["name", n as String]
FabricWriterRefused { refusal: r } => match r {
WriterLoginAbsent => ["writer-absent"]
WriterLoginUnrostered { login: l } => ["writer-login", l as String]
}
FabricStoreRefused { cause: c } =>
match c {
CasGenerationPublicationRefused { detail: d } => ["store-publish", d as String]
Expand Down Expand Up @@ -188,6 +192,8 @@ fn decode_fault(ws: List<String>, i: Int, line: String) -> FabricStorageFault {
"missing" => match decode_ref(word: word_at(ws: ws, i: i + 1)) { Present { value: o } => FabricObjectMissing { object: o } Absent => undecodable(line: line) }
"corrupt" => match decode_ref(word: word_at(ws: ws, i: i + 1)) { Present { value: o } => FabricObjectCorrupt { object: o, detail: nonempty_or(text: rest_from(ws: ws, i: i + 2), fallback: "unspecified") } Absent => undecodable(line: line) }
"name" => FabricHeadNameRefused { name: nonempty_or(text: detail, fallback: "unnamed") }
"writer-absent" => if detail == "" { FabricWriterRefused { refusal: WriterLoginAbsent } } else { undecodable(line: line) }
"writer-login" => if length(ws) == i + 2 && word_at(ws: ws, i: i + 1) != "" { FabricWriterRefused { refusal: WriterLoginUnrostered { login: word_at(ws: ws, i: i + 1) as NonEmptyStr } } } else { undecodable(line: line) }
"unreachable" => FabricStoreUnreachable { detail: nonempty_or(text: detail, fallback: "unspecified") }
"undecodable" => FabricReplyUndecodable { detail: nonempty_or(text: detail, fallback: "unspecified") }
"store-publish" => FabricStoreRefused { cause: CasGenerationPublicationRefused { detail: nonempty_or(text: detail, fallback: "unspecified") } }
Expand Down
96 changes: 96 additions & 0 deletions dag/gunbc/fabric/fabric_writer_roster.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
module gunbc.fabric_writer_roster

import std.types { String, NonEmptyStr, List, Bool }
import extdeps.tailscale.identity { TailnetIdentity, TailnetUser, TailnetIdentityAbsent }
import product.placement_supply { HostIdentity }
import gunbc.fabric_storage_client { FabricStorageBinding, fabric_storage_binding_for }
import gunbc.fabric_storage_placement { fabric_storage_placement }
import std.fabric_storage { FabricStoreUnreachable, FabricWriterRefusal, WriterLoginAbsent, WriterLoginUnrostered }
import gunbc.fabric_storage_wire { FabricIdentityPresented, FabricIdentityUnproxied, FabricIdentityReadRefused }
import gunbc.fabric_writer_identity_observe {
FabricWriterIdentityReceipt, fabric_writer_identity_ground, WriterUserPrincipalObserved, WriterPrincipalUnavailable, NotAReadingOfTheDoor,
}

// THE SERVED DOOR'S WRITER ROSTER: the tailnet USER principals admitted to put and advance through
// gunbc.fabric_storage_serve. IT IS AN AUTHORED ROSTER OF RECORDED READINGS, and says so: each row is
// the receipt gunbc.fabric_writer_identity_observe printed on one fleet host, transcribed by hand
// with the run that printed it, and the roster is the fold of those receipts' grounds -- a login
// enters exactly when a recorded reading presented it, and a host that presented none contributes
// nothing. WHAT IS NOT MECHANIZED, stated at its rung (DESIGN §4b(1)): nothing checks that a row
// matches the run it names, so that correspondence is review diligence -- rung 1, mitigatable --
// and adding a writer is a dispatch of the observe mode plus a hand-appended row. Its next-rung
// trigger is a typed receipt store the observe mode writes and this fold reads, so a row can no
// longer be authored without the run.
//
// THE GRAIN IS A USER, NOT A NODE (the drop row's chosen trust boundary): every device a rostered
// user owns may write. An absent login is never admitted -- it is the shape a tagged device, a
// stripped header or a direct connection produces (extdeps.tailscale.identity), and none is a
// principal.
//
// WHAT THIS ROSTER DOES NOT WALL: the placed host's in-process writers, which never reach the
// served door (their wall is the store directories' ownership, gunbc.fabric_storage_placement
// fabric_storage_directory); and anything that reaches the backend socket without the proxy, for
// which the header is self-asserted (extdeps.tailscale.identity's deployment property).
type FabricWriterIdentityObservation {
run: NonEmptyStr
receipt: FabricWriterIdentityReceipt
}

// THE READINGS, 2026-09-27 (fleet-converge mode fabric_writer_identity_observe, one run per host).
// NONE IS A LOGIN: srv1 is the placed host and read unproxied, and srv3 and srv4 could not resolve
// the door's tailnet name (curl exit 6), so the roster is EMPTY and the door refuses every served
// put and advance -- costing no writer that works today, since none reached the door. The binding
// each row carries is DERIVED from the current placement (fabric_storage_binding_for), not a copy
// of the URL the run printed, so a moved placement moves it.
fn recorded_binding(host: String) -> FabricStorageBinding {
fabric_storage_binding_for(placement: fabric_storage_placement(), executor: host as HostIdentity)
}

fn door_unresolved() -> NonEmptyStr {
"could not resolve the placed host's tailnet name (curl exit 6)" as NonEmptyStr
}

fn fabric_writer_identity_observations() -> List<FabricWriterIdentityObservation> {
[
FabricWriterIdentityObservation { run: "36347411725" as NonEmptyStr, receipt: FabricWriterIdentityReceipt {
host: "srv1" as HostIdentity, binding: recorded_binding(host: "srv1"),
read: FabricIdentityUnproxied, observed_at: "2026-09-27T20:29Z" as NonEmptyStr } },
FabricWriterIdentityObservation { run: "36347414620" as NonEmptyStr, receipt: FabricWriterIdentityReceipt {
host: "srv3" as HostIdentity, binding: recorded_binding(host: "srv3"),
read: FabricIdentityReadRefused { fault: FabricStoreUnreachable { detail: door_unresolved() } }, observed_at: "2026-09-27T20:29Z" as NonEmptyStr } },
FabricWriterIdentityObservation { run: "36347416176" as NonEmptyStr, receipt: FabricWriterIdentityReceipt {
host: "srv4" as HostIdentity, binding: recorded_binding(host: "srv4"),
read: FabricIdentityReadRefused { fault: FabricStoreUnreachable { detail: door_unresolved() } }, observed_at: "2026-09-27T20:30Z" as NonEmptyStr } },
]
}

fn observed_login(o: FabricWriterIdentityObservation) -> List<NonEmptyStr> {
match fabric_writer_identity_ground(read: o.receipt.read) {
WriterUserPrincipalObserved { login: l } => [l]
WriterPrincipalUnavailable { reason: _ } => []
NotAReadingOfTheDoor { reason: _ } => []
}
}

fn fabric_served_writer_roster_of(observations: List<FabricWriterIdentityObservation>) -> List<NonEmptyStr> {
fold(flat_map(observations, o => observed_login(o: o)), init: [], f: (acc, l) =>
if any(acc, a => (a as String) == (l as String)) { acc } else { concat(acc, [l]) })
}

fn fabric_served_writer_roster() -> List<NonEmptyStr> {
fabric_served_writer_roster_of(observations: fabric_writer_identity_observations())
}

type FabricServedWriterAdmission
= FabricServedWriterAdmitted { login: NonEmptyStr }
| FabricServedWriterRefused { refusal: FabricWriterRefusal }

// The comparison is exact, as extdeps.tailscale.identity requires of any identity match.
fn fabric_served_writer_admission_over(roster: List<NonEmptyStr>, identity: TailnetIdentity) -> FabricServedWriterAdmission {
match identity {
TailnetIdentityAbsent => FabricServedWriterRefused { refusal: WriterLoginAbsent }
TailnetUser { login: l } =>
if any(roster, r => (r as String) == (l as String)) { FabricServedWriterAdmitted { login: l } }
else { FabricServedWriterRefused { refusal: WriterLoginUnrostered { login: l } } }
}
}
15 changes: 15 additions & 0 deletions dag/gunbc/rung_drop/fabric_storage_append_principal_unrefused.dag
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ import gunbc.guarantee_rung { Mitigatable, MechanicallyPreventable }
// d0_store_operation_wall, a missing construction rather than a drop: it never existed under git
// either) -- so retiring this row alone does not open D0, and Cut 0 with the escalation ->
// authorization producer cannot make D0 executable before both walls stand, by construction.
//
// THE SERVED HALF STANDS; THE ROW STILL DOES (2026-09-27). gunbc.fabric_storage_serve now refuses a
// put or advance from any login outside gunbc.fabric_writer_roster's roster (an authored roster of recorded readings), including an absent
// login, as FabricWriterRefused (discriminating reds over the real handler in
// test.claim.fabric.fabric_storage_file_store_wet_witness). The roster is the fold of the recorded
// readings, which today ground no login (srv1 unproxied; srv3, srv4 could not resolve the door), so
// it is empty and the door refuses every served write. The row stands because the local half does
// not: the placed host's in-process writers are walled only by the store directories' ownership
// (operator owner, CI-runner group, no other-write -- test.claim.fabric.fabric_writer_roster_witness),
// and the backend listens on TCP loopback, so ANY srv1 local account can reach the served door and
// present a forged Tailscale-User-Login (extdeps.tailscale.identity: header trust is a deployment
// property). Operator-ruled restoration (proud-deer-538, 2026-09-27): the backend moves to a
// group-restricted unix socket behind tailscale serve and FabricStorageLocalFiles is retired, so the
// placed host writes through the same door; that change deletes this row.
data fabric_storage_append_principal_unrefused: RungDrop = RungDrop {
identity: "fabric_storage_append_principal_unrefused" as NonEmptyStr,

Expand All @@ -66,6 +80,7 @@ data fabric_storage_append_principal_unrefused: RungDrop = RungDrop {
population: [
"SERVED WRITERS (FabricStorageServed, every executor but the placed host): gunbc.fabric_storage_serve fabric_storage_serve_handle",
"LOCAL WRITERS (FabricStorageLocalFiles, the placed host srv1): gunbc.fabric_storage_file_store fabric_storage_file_put / fabric_storage_file_advance, reached in process by gunbc.fabric_storage_client fabric_storage_binding_for",
"LOOPBACK (any account on the placed host): the served backend's TCP loopback listener, where a forged Tailscale-User-Login passes the served roster",
"gunbc.fabric_event_log event_log_append",
"gunbc.fabric_event_log event_log_append_with",
"gunbc.fabric_event_log fabric_seat_acquire",
Expand Down
Loading