diff --git a/dag/gunbc/fabric/fabric_storage_serve.dag b/dag/gunbc/fabric/fabric_storage_serve.dag index 6b8bdb56f74..f6e960f25ce 100644 --- a/dag/gunbc/fabric/fabric_storage_serve.dag +++ b/dag/gunbc/fabric/fabric_storage_serve.dag @@ -4,7 +4,7 @@ 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 { @@ -12,6 +12,7 @@ import gunbc.fabric_storage_file_store { } 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 @@ -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 @@ -84,8 +81,9 @@ fn malformed(detail: String) -> ServeWireResponse { // put // advance absent | at // 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, 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" => @@ -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 } => @@ -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"], "")) } } @@ -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, 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) } } } @@ -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) } } } diff --git a/dag/gunbc/fabric/fabric_storage_wire.dag b/dag/gunbc/fabric/fabric_storage_wire.dag index 17430857bb8..2a2c8d2ae27 100644 --- a/dag/gunbc/fabric/fabric_storage_wire.dag +++ b/dag/gunbc/fabric/fabric_storage_wire.dag @@ -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, @@ -65,6 +65,10 @@ fn fault_words(fault: FabricStorageFault) -> List { 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] @@ -188,6 +192,8 @@ fn decode_fault(ws: List, 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") } } diff --git a/dag/gunbc/fabric/fabric_writer_roster.dag b/dag/gunbc/fabric/fabric_writer_roster.dag new file mode 100644 index 00000000000..a4d3743ac90 --- /dev/null +++ b/dag/gunbc/fabric/fabric_writer_roster.dag @@ -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 { 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 { + 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) -> List { + 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 { + 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, 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 } } } + } +} diff --git a/dag/gunbc/rung_drop/fabric_storage_append_principal_unrefused.dag b/dag/gunbc/rung_drop/fabric_storage_append_principal_unrefused.dag index d0ee3919435..c6be24a02e4 100644 --- a/dag/gunbc/rung_drop/fabric_storage_append_principal_unrefused.dag +++ b/dag/gunbc/rung_drop/fabric_storage_append_principal_unrefused.dag @@ -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, @@ -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", diff --git a/dag/std/fabric_storage.dag b/dag/std/fabric_storage.dag index 902b045b354..53185d4f0e2 100644 --- a/dag/std/fabric_storage.dag +++ b/dag/std/fabric_storage.dag @@ -166,6 +166,9 @@ fn fabric_head_admits(expected: FabricHeadExpectation, observed: FabricHeadReadi // skew between handler and store, not a store fault. // FabricHeadNameRefused the head name does not address one head (it would navigate); the // caller's defect, refused before the store is touched. +// FabricWriterRefused the writer's principal is not one the store admits to write; refused +// before the store is touched. Which case -- no principal presented, or +// a named principal outside the roster -- is an arm, never prose. // // A lost race is NOT a fault and has no arm here: it is FabricHeadMoved, an outcome carrying both // sides. The git realization answered both "lost race" and "unreachable" with exit 128. @@ -176,6 +179,11 @@ type FabricStorageFault | FabricObjectCorrupt { object: FabricObjectRef, detail: NonEmptyStr } | FabricReplyUndecodable { detail: NonEmptyStr } | FabricHeadNameRefused { name: NonEmptyStr } + | FabricWriterRefused { refusal: FabricWriterRefusal } + +type FabricWriterRefusal + = WriterLoginAbsent + | WriterLoginUnrostered { login: NonEmptyStr } type FabricPut = FabricObjectStored { object: FabricObjectRef } @@ -250,6 +258,10 @@ fn fabric_storage_fault_wire(fault: FabricStorageFault) -> String { FabricObjectCorrupt { object: o, detail: d } => join(["object corrupt: ", fabric_object_ref_wire(object: o) as String, " ", d as String], "") FabricReplyUndecodable { detail: d } => join(["reply undecodable: ", d as String], "") FabricHeadNameRefused { name: n } => join(["head name refused: ", n as String], "") + FabricWriterRefused { refusal: r } => match r { + WriterLoginAbsent => "writer refused: no login was presented" + WriterLoginUnrostered { login: l } => join(["writer refused: login ", l as String, " is not a rostered fabric writer"], "") + } } } diff --git a/dag/test/claim/fabric/fabric_storage_file_store_wet_witness_test.dag b/dag/test/claim/fabric/fabric_storage_file_store_wet_witness_test.dag index da82f575c83..90a897f2009 100644 --- a/dag/test/claim/fabric/fabric_storage_file_store_wet_witness_test.dag +++ b/dag/test/claim/fabric/fabric_storage_file_store_wet_witness_test.dag @@ -5,7 +5,7 @@ import extdeps.access.posix { FileMode, PermissionBits, file_mode_octal } import extdeps.tools.coreutils_stat { coreutils_stat_path_mode, PathModeObserved, PathModeUnobservable } import gunbc.durable_cas_file_store { cas_file_slot_path } import extdeps.filesystem.filesystem_io { Filesystem } -import std.types { Bool, NonEmptyStr, String } +import std.types { Bool, NonEmptyStr, String, List, FilePath } import std.durable_compare_and_set { cas_generation_count } import std.fabric_storage { FabricObject, fabric_object_ref_of, fabric_object_ref_eq, fabric_object_ref_wire, @@ -14,12 +14,13 @@ import std.fabric_storage { FabricHeadObserved, FabricHeadReadRefused, FabricHeadAdvanced, FabricHeadMoved, FabricHeadAdvanceRefused, FabricClosureRead, FabricClosureBudgetExhausted, FabricClosureRefused, - FabricObjectMissing, FabricObjectCorrupt, FabricHeadNameRefused, + FabricObjectMissing, FabricObjectCorrupt, FabricHeadNameRefused, FabricWriterRefused, + FabricPut, FabricHeadAdvance, fabric_object_preimage, } import gunbc.fabric_storage_serve { fabric_storage_serve_handle_over } import gunbc.fabric_storage_wire { fabric_storage_unwire_identity, FabricIdentityPresented, FabricIdentityUnproxied, FabricIdentityReadRefused } import extdeps.tailscale.identity { TailnetUser, TailnetIdentityAbsent } -import gunbc.fabric_storage_wire { fabric_storage_unwire_advance } +import gunbc.fabric_storage_wire { fabric_storage_unwire_advance, fabric_storage_unwire_put } import gunbc.fabric_storage_client { served_reply_from, ServedAnswered, ServedFault } import gunbc.fabric_storage_file_store { FabricStorageFileRoot, fabric_storage_file_root, fabric_storage_file_root_with, fabric_storage_file_root_ensure, FabricStorageRootReady, FabricStorageRootRefused, @@ -38,6 +39,8 @@ fn fresh_store() -> FabricStorageFileRoot { } } +data fixture_writer: NonEmptyStr = "writer@example.ts.net" as NonEmptyStr + fn event(body: NonEmptyStr, parent: FabricObject?) -> FabricObject { match parent { Absent => FabricObject { links: [], body: body } @@ -159,7 +162,7 @@ test fn a_lost_race_through_the_served_handler_decodes_as_the_same_lost_race_by_ let b = event(body: "b", parent: Present { value: a }) let _p = [fabric_storage_file_put(root: s, object: a), fabric_storage_file_put(root: s, object: b)] let _1 = fabric_storage_file_advance(root: s, name: "p", expected: ExpectHeadAbsent, target: fabric_object_ref_of(object: a)) - let reply = fabric_storage_serve_handle_over(root: s, method: "POST", path: "/fabric-storage/advance", body: join(["p absent ", fabric_object_ref_wire(object: fabric_object_ref_of(object: b)) as String], ""), tailscale_identity: "") + let reply = fabric_storage_serve_handle_over(root: s, roster: [fixture_writer], method: "POST", path: "/fabric-storage/advance", body: join(["p absent ", fabric_object_ref_wire(object: fabric_object_ref_of(object: b)) as String], ""), tailscale_identity: fixture_writer as String) match served_reply_from(exit_code: 0, stdout: join([reply.body, "\n", to_string(reply.status)], ""), stderr: "") { ServedFault { fault: _ } => false ServedAnswered { body: text } => @@ -175,7 +178,7 @@ test fn a_lost_race_through_the_served_handler_decodes_as_the_same_lost_race_by_ // before and after). An empty header decodes as absent, never as a login. test fn the_identity_route_echoes_the_presented_login_and_touches_no_head_by_real_execution() -> Bool { let s = fresh_store() - let reply = fabric_storage_serve_handle_over(root: s, method: "POST", path: "/fabric-storage/identity", body: "", tailscale_identity: "writer@example.ts.net") + let reply = fabric_storage_serve_handle_over(root: s, roster: [], method: "POST", path: "/fabric-storage/identity", body: "", tailscale_identity: "writer@example.ts.net") let echoed = match served_reply_from(exit_code: 0, stdout: join([reply.body, "\n", to_string(reply.status)], ""), stderr: "") { ServedFault { fault: _ } => false ServedAnswered { body: text } => @@ -185,7 +188,7 @@ test fn the_identity_route_echoes_the_presented_login_and_touches_no_head_by_rea FabricIdentityReadRefused { fault: _ } => false } } - let absent = match fabric_storage_unwire_identity(text: fabric_storage_serve_handle_over(root: s, method: "POST", path: "/fabric-storage/identity", body: "", tailscale_identity: "").body) { + let absent = match fabric_storage_unwire_identity(text: fabric_storage_serve_handle_over(root: s, roster: [], method: "POST", path: "/fabric-storage/identity", body: "", tailscale_identity: "").body) { FabricIdentityPresented { identity: i } => match i { TailnetIdentityAbsent => true TailnetUser { login: _ } => false } _ => false } @@ -233,3 +236,92 @@ test fn a_declared_entry_mode_is_the_published_mode_of_objects_and_heads() -> Bo let undeclared_mode = observed_mode_octal(path: fabric_storage_object_path(root: base, object: fabric_object_ref_of(object: a))) ready && put && object_mode == "0444" && head_mode == "0444" && undeclared_mode != "0444" } + +// ── THE WRITER WALL AT THE SERVED DOOR, BY REAL EXECUTION ──────────────────────────────────── +// A put and an advance through the real handler, over real files, from three presented identities +// against one supplied roster: the rostered login writes (the positive control, same requests); a +// login outside the roster and an absent login are each refused with FabricWriterRefused, decoded +// on the caller's side as that arm, and neither leaves an object file or moves the head. Deleting +// the admission from fabric_storage_serve's put or advance arm reds the refused cases here. +fn served_put(s: FabricStorageFileRoot, roster: List, login: String, o: FabricObject) -> FabricPut { + let reply = fabric_storage_serve_handle_over(root: s, roster: roster, method: "POST", path: "/fabric-storage/put", body: fabric_object_preimage(object: o) as String, tailscale_identity: login) + match served_reply_from(exit_code: 0, stdout: join([reply.body, "\n", to_string(reply.status)], ""), stderr: "") { + ServedFault { fault: f } => FabricPutRefused { fault: f } + ServedAnswered { body: text } => fabric_storage_unwire_put(text: text) + } +} + +fn served_advance_absent(s: FabricStorageFileRoot, roster: List, login: String, o: FabricObject) -> FabricHeadAdvance { + let reply = fabric_storage_serve_handle_over(root: s, roster: roster, method: "POST", path: "/fabric-storage/advance", body: join(["p absent ", fabric_object_ref_wire(object: fabric_object_ref_of(object: o)) as String], ""), tailscale_identity: login) + match served_reply_from(exit_code: 0, stdout: join([reply.body, "\n", to_string(reply.status)], ""), stderr: "") { + ServedFault { fault: f } => FabricHeadAdvanceRefused { fault: f } + ServedAnswered { body: text } => fabric_storage_unwire_advance(text: text) + } +} + +fn put_writer_refused(p: FabricPut) -> Bool { + match p { FabricPutRefused { fault: f } => match f { FabricWriterRefused { refusal: _ } => true _ => false } FabricObjectStored { object: _ } => false } +} + +fn advance_writer_refused(a: FabricHeadAdvance) -> Bool { + match a { FabricHeadAdvanceRefused { fault: f } => match f { FabricWriterRefused { refusal: _ } => true _ => false } _ => false } +} + +fn head_absent(s: FabricStorageFileRoot) -> Bool { + match fabric_storage_file_head(root: s, name: "p") { FabricHeadObserved { reading: r } => match r { FabricHeadAbsent => true FabricHeadAt { object: _, generation: _ } => false } FabricHeadReadRefused { fault: _ } => false } +} + +fn object_absent(s: FabricStorageFileRoot, o: FabricObject) -> Bool { + match fabric_storage_file_get(root: s, object: fabric_object_ref_of(object: o)) { FabricObjectGetRefused { fault: f } => match f { FabricObjectMissing { object: _ } => true _ => false } FabricObjectFetched { stored: _ } => false } +} + +test fn a_put_or_advance_from_a_login_outside_the_roster_is_refused_at_the_served_door_by_real_execution() -> Bool { + let s = fresh_store() + let a = event(body: "a", parent: none) + let roster = [fixture_writer] + let outsider_put = served_put(s: s, roster: roster, login: "stranger@example.ts.net", o: a) + let outsider_left_nothing = object_absent(s: s, o: a) + let _stored = fabric_storage_file_put(root: s, object: a) + let outsider_advance = served_advance_absent(s: s, roster: roster, login: "stranger@example.ts.net", o: a) + put_writer_refused(p: outsider_put) && outsider_left_nothing && advance_writer_refused(a: outsider_advance) && head_absent(s: s) +} + +test fn a_put_or_advance_with_no_presented_login_is_refused_at_the_served_door_by_real_execution() -> Bool { + let s = fresh_store() + let a = event(body: "a", parent: none) + let roster = [fixture_writer] + let absent_put = served_put(s: s, roster: roster, login: "", o: a) + let absent_left_nothing = object_absent(s: s, o: a) + let _stored = fabric_storage_file_put(root: s, object: a) + let absent_advance = served_advance_absent(s: s, roster: roster, login: "", o: a) + put_writer_refused(p: absent_put) && absent_left_nothing && advance_writer_refused(a: absent_advance) && head_absent(s: s) +} + +test fn a_rostered_login_puts_and_advances_through_the_served_door_by_real_execution() -> Bool { + let s = fresh_store() + let a = event(body: "a", parent: none) + let roster = [fixture_writer] + let put = match served_put(s: s, roster: roster, login: fixture_writer as String, o: a) { FabricObjectStored { object: _ } => true FabricPutRefused { fault: _ } => false } + let advanced = match served_advance_absent(s: s, roster: roster, login: fixture_writer as String, o: a) { FabricHeadAdvanced { object: _, generation: _ } => true _ => false } + put && advanced && head_absent(s: s) == false +} + +// ── THE WRITER WALL ON THE PLACED HOST'S LOCAL PATH, BY REAL EXECUTION ─────────────────────── +// A local writer on the placed host runs the file store in process, so its admission is the +// kernel's: the store areas' ownership and mode, derived from the placement's writer dependents +// (gunbc.fabric_storage_placement fabric_storage_directory, whose writer roster claim is +// test.claim.fabric.fabric_writer_roster_witness). This claim is the file store's half: when the +// areas grant this process's principal no write, the REAL file store's put and advance refuse with +// a typed fault, create nothing and leave the head absent -- never a panic, never a partial write. +// The control is the same operations on the same store before write is withdrawn. Run as root the +// kernel ignores the mode and this claim is red: it is a wet claim for an unprivileged principal. +test fn a_put_or_advance_by_a_principal_the_store_areas_do_not_admit_is_refused_by_the_real_file_store() -> Bool { + let s = fresh_store() + let a = event(body: "a", parent: none) + let b = event(body: "b", parent: none) + let control = match fabric_storage_file_put(root: s, object: a) { FabricObjectStored { object: _ } => true FabricPutRefused { fault: _ } => false } + let withdrawn = shell.Chmod.RecursiveReadOnly(path: (s.objects as String) as FilePath).success && shell.Chmod.RecursiveReadOnly(path: (s.heads as String) as FilePath).success + let put_refused = match fabric_storage_file_put(root: s, object: b) { FabricPutRefused { fault: _ } => true FabricObjectStored { object: _ } => false } + let advance_refused = match fabric_storage_file_advance(root: s, name: "p", expected: ExpectHeadAbsent, target: fabric_object_ref_of(object: a)) { FabricHeadAdvanceRefused { fault: _ } => true _ => false } + control && withdrawn && put_refused && object_absent(s: s, o: b) && advance_refused && head_absent(s: s) +} diff --git a/dag/test/claim/fabric/fabric_storage_wire_witness_test.dag b/dag/test/claim/fabric/fabric_storage_wire_witness_test.dag index f33f002acf2..4fb0290ae43 100644 --- a/dag/test/claim/fabric/fabric_storage_wire_witness_test.dag +++ b/dag/test/claim/fabric/fabric_storage_wire_witness_test.dag @@ -8,9 +8,10 @@ import std.fabric_storage { FabricStoreUnreachable, FabricStoreRefused, FabricObjectMissing, FabricReplyUndecodable, FabricHeadObserved, FabricHeadReadRefused, FabricHeadMoved, FabricHeadAdvanced, FabricHeadAdvanceRefused, FabricClosureRead, FabricClosureRefused, + FabricWriterRefused, WriterLoginAbsent, WriterLoginUnrostered, FabricPutRefused, } import gunbc.fabric_storage_wire { - fabric_storage_wire_head, fabric_storage_wire_advance, fabric_storage_wire_closure, + fabric_storage_wire_head, fabric_storage_wire_advance, fabric_storage_wire_closure, fabric_storage_wire_put, fabric_storage_unwire_put, fabric_storage_unwire_head, fabric_storage_unwire_advance, fabric_storage_unwire_closure, fabric_storage_wire_identity, fabric_storage_unwire_identity, FabricIdentityPresented, FabricIdentityUnproxied, FabricIdentityReadRefused, } @@ -144,3 +145,22 @@ test fn a_presented_login_crosses_the_wire_as_that_login_and_absent_as_absent() let trailing_newline = match fabric_storage_unwire_identity(text: "fabric_storage/1 identity absent\n") { FabricIdentityReadRefused { fault: _ } => true _ => false } present && absent && skew && other && absent_extra && present_extra && absent_continued && present_continued && trailing_newline } + +// A WRITER REFUSAL CROSSES THE WIRE AS ITS ARM: no login presented stays absent, and an unrostered +// login stays that login -- the caller tells the two cases apart by the arm, never by parsing prose. +// A trailing word on either is undecodable. +test fn a_writer_refusal_crosses_the_wire_as_its_arm() -> Bool { + let absent = match fabric_storage_unwire_put(text: fabric_storage_wire_put(put: FabricPutRefused { fault: FabricWriterRefused { refusal: WriterLoginAbsent } })) { + FabricPutRefused { fault: f } => match f { FabricWriterRefused { refusal: r } => match r { WriterLoginAbsent => true _ => false } _ => false } + _ => false + } + let unrostered = match fabric_storage_unwire_put(text: fabric_storage_wire_put(put: FabricPutRefused { fault: FabricWriterRefused { refusal: WriterLoginUnrostered { login: "stranger@example.ts.net" as NonEmptyStr } } })) { + FabricPutRefused { fault: f } => match f { FabricWriterRefused { refusal: r } => match r { WriterLoginUnrostered { login: l } => (l as String) == "stranger@example.ts.net" _ => false } _ => false } + _ => false + } + let trailing = match fabric_storage_unwire_put(text: "fabric_storage/1 put refused writer-login a b") { + FabricPutRefused { fault: f } => match f { FabricReplyUndecodable { detail: _ } => true _ => false } + _ => false + } + absent && unrostered && trailing +} diff --git a/dag/test/claim/fabric/fabric_writer_roster_witness_test.dag b/dag/test/claim/fabric/fabric_writer_roster_witness_test.dag new file mode 100644 index 00000000000..c875a7d593a --- /dev/null +++ b/dag/test/claim/fabric/fabric_writer_roster_witness_test.dag @@ -0,0 +1,101 @@ +module test.claim.fabric.fabric_writer_roster_witness + +import std.types { Bool, NonEmptyStr, String, List, Int } +import extdeps.tailscale.identity { TailnetUser, TailnetIdentityAbsent } +import extdeps.access.posix { PosixUser, LoginShellUnobserved, FilePermissions } +import std.fabric_storage { FabricStoreUnreachable, WriterLoginAbsent, WriterLoginUnrostered } +import product.placement_supply { HostIdentity } +import gunbc.fabric_storage_client { FabricStorageServed, FabricStorageLocalFiles } +import gunbc.fabric_storage_file_store { fabric_storage_file_root } +import gunbc.fabric_storage_wire { FabricIdentityPresented, FabricIdentityUnproxied, FabricIdentityReadRefused } +import gunbc.fabric_writer_identity_observe { FabricWriterIdentityReceipt } +import gunbc.fabric_writer_roster { + FabricWriterIdentityObservation, fabric_served_writer_roster_of, fabric_served_writer_roster, + fabric_served_writer_admission_over, FabricServedWriterAdmitted, FabricServedWriterRefused, +} +import gunbc.fabric_storage_placement { fabric_storage_directory, fabric_storage_store_root } +import gunbc.managed_directory { ManagedDirectory, managed_directory_permissions, dependent_class_of, OwnerClass, GroupClass, OtherClass } +import gunbc.fleet_posix_accounts { fleet_posix_operator_user, fleet_posix_ci_runner_user } + +// ── THE SERVED ROSTER IS DERIVED FROM READINGS OF THE DOOR ─────────────────────────────────── +// Supplied receipts, one per ground arm: a presented login enters the roster, and an absent login, +// an unproxied (placed-host) reading and an unread door contribute nothing. A login observed on two +// hosts is one principal (the user grain), rostered once. +fn receipt(host: String, read: gunbc.fabric_storage_wire.FabricIdentityRead) -> FabricWriterIdentityObservation { + FabricWriterIdentityObservation { + run: "fixture" as NonEmptyStr, + receipt: FabricWriterIdentityReceipt { + host: host as HostIdentity, + binding: FabricStorageServed { endpoint: "https://srv1.example.ts.net:10000/fabric-storage" as NonEmptyStr }, + read: read, + observed_at: "2026-09-27T00:00:00Z" as NonEmptyStr, + }, + } +} + +fn supplied_observations() -> List { + [ + receipt(host: "srv2", read: FabricIdentityPresented { identity: TailnetUser { login: "writer@example.ts.net" as NonEmptyStr } }), + receipt(host: "srv3", read: FabricIdentityPresented { identity: TailnetUser { login: "writer@example.ts.net" as NonEmptyStr } }), + receipt(host: "srv4", read: FabricIdentityPresented { identity: TailnetIdentityAbsent }), + receipt(host: "srv1", read: FabricIdentityUnproxied), + receipt(host: "srv5", read: FabricIdentityReadRefused { fault: FabricStoreUnreachable { detail: "fixture" as NonEmptyStr } }), + ] +} + +test fn the_served_roster_holds_exactly_the_observed_logins_once() -> Bool { + let roster = fabric_served_writer_roster_of(observations: supplied_observations()) + length(roster) == 1 && match first(roster) { Present { value: l } => (l as String) == "writer@example.ts.net" Absent => false } +} + +test fn the_admission_admits_a_rostered_login_and_refuses_an_outsider_and_an_absent_login() -> Bool { + let roster = ["writer@example.ts.net" as NonEmptyStr] + let admitted = match fabric_served_writer_admission_over(roster: roster, identity: TailnetUser { login: "writer@example.ts.net" as NonEmptyStr }) { FabricServedWriterAdmitted { login: _ } => true FabricServedWriterRefused { refusal: _ } => false } + let outsider = match fabric_served_writer_admission_over(roster: roster, identity: TailnetUser { login: "Writer@example.ts.net" as NonEmptyStr }) { FabricServedWriterRefused { refusal: r } => match r { WriterLoginUnrostered { login: l } => (l as String) == "Writer@example.ts.net" WriterLoginAbsent => false } FabricServedWriterAdmitted { login: _ } => false } + let absent = match fabric_served_writer_admission_over(roster: roster, identity: TailnetIdentityAbsent) { FabricServedWriterRefused { refusal: r } => match r { WriterLoginAbsent => true WriterLoginUnrostered { login: _ } => false } FabricServedWriterAdmitted { login: _ } => false } + admitted && outsider && absent +} + +// THE PRODUCTION ROSTER ADMITS NO LOGIN IT WAS NOT OBSERVED PRESENTING. With every recorded +// reading unread or unproxied (gunbc.fabric_writer_roster fabric_writer_identity_observations), the +// served door refuses every login -- including one that looks like an operator's -- rather than +// guessing which hosts are user-owned. +test fn the_production_served_door_refuses_a_login_no_reading_observed() -> Bool { + match fabric_served_writer_admission_over(roster: fabric_served_writer_roster(), identity: TailnetUser { login: "unobserved@example.ts.net" as NonEmptyStr }) { + FabricServedWriterRefused { refusal: _ } => true + FabricServedWriterAdmitted { login: _ } => false + } +} + +// ── THE LOCAL ROSTER IS THE STORE AREAS' WRITER DEPENDENTS ─────────────────────────────────── +// On the placed host the kernel is the wall: a principal writes an area only through the class the +// derived permissions give it. The two local writers the placement declares -- the operator +// (owner) and the CI runner (the area's group) -- each land in a class holding write and traverse; +// a principal outside both lands in OTHER, which holds neither, on objects and on heads alike. +fn class_writes(d: ManagedDirectory, who: PosixUser) -> Bool { + let p = managed_directory_permissions(d: d) + match dependent_class_of(owner: d.owner, group_gid: p.ownership.gid, who: who) { + OwnerClass => p.mode.owner.write && p.mode.owner.execute + GroupClass => p.mode.group.write && p.mode.group.execute + OtherClass => p.mode.other.write || p.mode.other.execute + } +} + +fn outsider() -> PosixUser { + PosixUser { name: "fabric-outsider", uid: 4242, gid: 4242, shell: LoginShellUnobserved } +} + +fn other_class_writes(d: ManagedDirectory) -> Bool { + let p = managed_directory_permissions(d: d) + p.mode.other.write || p.mode.other.execute +} + +test fn the_placed_store_areas_admit_writes_from_the_operator_and_the_ci_runner_and_no_one_else() -> Bool { + let areas = fabric_storage_file_root(root: fabric_storage_store_root()) + let objects = fabric_storage_directory(member: "fabric-storage-objects", path: areas.objects, writes: true) + let heads = fabric_storage_directory(member: "fabric-storage-heads", path: areas.heads, writes: true) + class_writes(d: objects, who: fleet_posix_operator_user) && class_writes(d: heads, who: fleet_posix_operator_user) + && class_writes(d: objects, who: fleet_posix_ci_runner_user) && class_writes(d: heads, who: fleet_posix_ci_runner_user) + && other_class_writes(d: objects) == false && other_class_writes(d: heads) == false + && match dependent_class_of(owner: objects.owner, group_gid: managed_directory_permissions(d: objects).ownership.gid, who: outsider()) { OtherClass => true _ => false } +} diff --git a/docs/design-rung-drops.md b/docs/design-rung-drops.md index b802182a7f7..c8a6648d38f 100644 --- a/docs/design-rung-drops.md +++ b/docs/design-rung-drops.md @@ -396,7 +396,7 @@ two serving processes on one host can both open the single-writer approval store ### an append to the fabric event log by a principal that is not a fleet writer — declared 2026-09-19 -an append to the fabric event log by a principal that is not a fleet writer: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: gunbc.fabric_storage_serve fabric_storage_serve_handle, reached only through tailscale serve on srv1 (tailnet membership is the only boundary)). 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, gunbc.fabric_event_log event_log_append, gunbc.fabric_event_log event_log_append_with, gunbc.fabric_event_log fabric_seat_acquire, gunbc.spark.pair_serving_authority_log (every writer to a pair-serving-authority partition and to host-placement: establishment, transition, entry state, cancellation, host-effect claim and release, placement prepare / claim / finalize / abort), gunbc.durable_cas_fabric_storage fabric_storage_compare_and_set (the D0 operator-consent claim slot). Restored when: BOTH of: (served writers) a modeled roster of fabric writer USER principals, grounded in the observed reading gunbc.fabric_writer_identity_observe takes of the Tailscale-User-Login each fleet writer presents through tailscale serve on srv1 -- SUFFICIENT FOR fabric_storage_serve_handle to refuse a put or advance from any login outside the roster, including an absent login, with a discriminating RED over the real handler; AND (local writers) principal admission on the placed host's own write path -- at the FabricStorageLocalFiles binding or by routing the placed host's writes through the served door -- SUFFICIENT FOR a put or advance by an unrostered local principal to refuse, with a discriminating RED over the real file store. A roster at the served door alone retires nothing: the placed host's writes never reach it. +an append to the fabric event log by a principal that is not a fleet writer: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: gunbc.fabric_storage_serve fabric_storage_serve_handle, reached only through tailscale serve on srv1 (tailnet membership is the only boundary)). 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, gunbc.spark.pair_serving_authority_log (every writer to a pair-serving-authority partition and to host-placement: establishment, transition, entry state, cancellation, host-effect claim and release, placement prepare / claim / finalize / abort), gunbc.durable_cas_fabric_storage fabric_storage_compare_and_set (the D0 operator-consent claim slot). Restored when: BOTH of: (served writers) a modeled roster of fabric writer USER principals, grounded in the observed reading gunbc.fabric_writer_identity_observe takes of the Tailscale-User-Login each fleet writer presents through tailscale serve on srv1 -- SUFFICIENT FOR fabric_storage_serve_handle to refuse a put or advance from any login outside the roster, including an absent login, with a discriminating RED over the real handler; AND (local writers) principal admission on the placed host's own write path -- at the FabricStorageLocalFiles binding or by routing the placed host's writes through the served door -- SUFFICIENT FOR a put or advance by an unrostered local principal to refuse, with a discriminating RED over the real file store. A roster at the served door alone retires nothing: the placed host's writes never reach it. ### the required namespace-wave-admission phase: merge-path adjudication of closure, subject-membership and occurrence-binding deltas between merge base and head — declared 2026-09-19 diff --git a/src/v2/workflow/floor_route_gap.dag b/src/v2/workflow/floor_route_gap.dag index a4fdfc06235..4bc9dcf763e 100644 --- a/src/v2/workflow/floor_route_gap.dag +++ b/src/v2/workflow/floor_route_gap.dag @@ -1388,6 +1388,14 @@ fn floor_route_gap_expectation_chunk_17() -> List { tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.fabric.fabric_storage_file_store_wet_witness.the_identity_route_echoes_the_presented_login_and_touches_no_head_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.fabric.fabric_storage_file_store_wet_witness.a_put_or_advance_from_a_login_outside_the_roster_is_refused_at_the_served_door_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.fabric.fabric_storage_file_store_wet_witness.a_put_or_advance_with_no_presented_login_is_refused_at_the_served_door_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.fabric.fabric_storage_file_store_wet_witness.a_rostered_login_puts_and_advances_through_the_served_door_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.fabric.fabric_storage_file_store_wet_witness.a_put_or_advance_by_a_principal_the_store_areas_do_not_admit_is_refused_by_the_real_file_store", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.fabric.fabric_partition_read_wet_witness.an_empty_partition_reports_an_absent_head_and_no_events_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.fabric.fabric_partition_read_wet_witness.a_two_event_partition_reports_both_events_oldest_first_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, @@ -1417,6 +1425,10 @@ fn floor_route_gap_expectation_chunk_17() -> List { } } } + } + } + } + } } // The four test.claim.shell_spawn_refused_real_execution_witness identities. Their subject IS the diff --git a/src/v2/workflow/local_repo_wet_terminal.dag b/src/v2/workflow/local_repo_wet_terminal.dag index 9b829861165..81d353f687f 100644 --- a/src/v2/workflow/local_repo_wet_terminal.dag +++ b/src/v2/workflow/local_repo_wet_terminal.dag @@ -1291,6 +1291,30 @@ fn local_repo_wet_schedule() -> List { function: "a_declared_entry_mode_is_the_published_mode_of_objects_and_heads", expectation: ExpectedToHold {} }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.fabric.fabric_storage_file_store_wet_witness", function: "a_put_or_advance_from_a_login_outside_the_roster_is_refused_at_the_served_door_by_real_execution" }, + entry: "dag/test/claim/fabric/fabric_storage_file_store_wet_witness_test.dag", + function: "a_put_or_advance_from_a_login_outside_the_roster_is_refused_at_the_served_door_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.fabric.fabric_storage_file_store_wet_witness", function: "a_put_or_advance_with_no_presented_login_is_refused_at_the_served_door_by_real_execution" }, + entry: "dag/test/claim/fabric/fabric_storage_file_store_wet_witness_test.dag", + function: "a_put_or_advance_with_no_presented_login_is_refused_at_the_served_door_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.fabric.fabric_storage_file_store_wet_witness", function: "a_rostered_login_puts_and_advances_through_the_served_door_by_real_execution" }, + entry: "dag/test/claim/fabric/fabric_storage_file_store_wet_witness_test.dag", + function: "a_rostered_login_puts_and_advances_through_the_served_door_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.fabric.fabric_storage_file_store_wet_witness", function: "a_put_or_advance_by_a_principal_the_store_areas_do_not_admit_is_refused_by_the_real_file_store" }, + entry: "dag/test/claim/fabric/fabric_storage_file_store_wet_witness_test.dag", + function: "a_put_or_advance_by_a_principal_the_store_areas_do_not_admit_is_refused_by_the_real_file_store", + expectation: ExpectedToHold {} + }, WetScheduledClaim { identity: WitnessIdentity { module_path: "test.claim.fabric.fabric_storage_file_store_wet_witness", function: "the_identity_route_echoes_the_presented_login_and_touches_no_head_by_real_execution" }, entry: "dag/test/claim/fabric/fabric_storage_file_store_wet_witness_test.dag",