pub struct MesofactServeBundle {
pub digest: BlakeHash,
pub runtime: String,
pub lifecycle: BundleLifecycle,
pub port: Option<u16>,
pub env: BTreeMap<String, String>,
pub origin: Option<String>,
}Expand description
Serve-time reference to a published W272 bundle (R599-F4) — the
{bundle_digest, runtime, lifecycle} triple a mesofact-static workload
carries when kamaji, not the build reconciler, serves it.
@yah:ticket(R870-B6, “Bundle origin is node-wide, so a second tenant’s bundle can never be materialized”)
@yah:status(review)
@yah:at(2026-09-09T03:23:08Z)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:parent(R870)
@yah:gotcha(“REPORTED BY THE NOISETABLE CAMP, which is R870’s second tenant made concrete. Its bundle deploy gets ALL THE WAY to admission and then fails: ‘materialize bundle f75c6940…: missing blob manifests/f75c6940… for path "manifest.toml"’. Ident and placement were correct — the CLI printed ‘noisetable admitted by us-east-001 (http://100.64.0.3:7443)’ — so this is not a discovery or placement bug. The node accepted a digest it has no way to fetch.”)
@yah:gotcha(“ROOT CAUSE, read not guessed: MesofactServeBundle is { digest, runtime, lifecycle, port, env } and carries NEITHER a bucket NOR an origin (oss/yah-base/crates/workload-spec/src/lib.rs). BundleSlot::serve_bundle constructs it from the slot and drops the slot’s bucket on the floor (oss/yubaba/crates/cloud/src/reconciler/mesofact_bundle.rs). kamaji then fetches from the NODE-WIDE KAMAJI_BUNDLE_ORIGIN (oss/kamaji/crates/kamaji-bin/src/main.rs:86). So the publish side is per-service and the fetch side is per-node, and they only agree while the fleet has exactly one tenant.”)
@yah:gotcha(“MEASURED, WITH A NEGATIVE CONTROL — all four taken 2026-09-08. (1) our manifest blob IS in the noisetable-marketing bucket: yah cloud bucket ls --bucket noisetable-marketing lists manifests/f75c6940…. (2) https://cdn.noisetable.com/manifests/f75c6940… = 200. (3) https://cdn.yah.dev/manifests/f75c6940… = 404 — the node’s origin, where it looked. (4) POSITIVE CONTROL, so 404 is not just a broken URL shape: https://cdn.yah.dev/manifests/0279f43e… (a real yah bundle) = 200. yah-dev holds 49 manifests/ objects; noisetable-marketing holds 1, and it is ours.”)
@yah:gotcha(“THE RUNTIME ASSET HALF FAILS IDENTICALLY AND IS A SECOND BLOCKER, not the same one twice — fixing only the manifest fetch leaves the deploy failing one step later. runtimes/mesofact/0.8.32/x86_64-unknown-linux-musl.toml exists in yah-dev and in NO other bucket, and the apply printed ‘no mesofact/0.8.32 runtime asset is published yet’ against the tenant’s own bucket. Whatever carries the origin must cover manifests, blobs AND the runtime-asset lookup.”)
@yah:gotcha(“THE WORKAROUND WAS CONSIDERED AND REFUSED BY THE OPERATOR, recorded so it is not re-proposed as a shortcut: point the tenant’s bundle slot at bucket = "yah-dev" so it lands in the store the node already reads. It works today and is one word. It also puts a tenant’s build output in yah’s bucket, which makes the tenant boundary fictional for bundle content in exactly the way reusing the account-scoped ACME token would have for DNS — the same call R870-F1 already made the other way when it minted a noisetable-only token instead. Noisetable’s mirror still declares bucket = "noisetable-marketing" and is correct as written; it is yah that cannot consume it.”)
@yah:next(“THE PRECEDENT IS IN THE SAME STRUCT AND SHOULD BE COPIED RATHER THAN REDESIGNED. MesofactServeBundle::port’s own doc records that it ‘used to mean fall back to kamaji’s node-wide default (KAMAJI_BUNDLE_PORT, else 8080), which was a single node-wide slot wearing the word default — correct only while a node hosted one’. R844-F2 fixed that axis by making the value travel with the workload and letting the node-wide setting be a fallback. KAMAJI_BUNDLE_ORIGIN is the same defect on the store axis, unfixed. Do the same thing: an Optionbucket (oss/yubaba/crates/cloud/src/reconciler/mesofact_bundle.rs); (c) kamaji resolves the per-workload origin ahead of KAMAJI_BUNDLE_ORIGIN for manifest, blob AND runtime-asset fetches (oss/kamaji/crates/kamaji-bin). A bucket name is not directly fetchable, so decide deliberately whether the field carries a public origin URL or a bucket that the node maps to one — the tenant’s blobs are reachable at https://cdn.noisetable.com today, so an origin URL needs no new credential on the node and keeps kamaji credential-free, which is the property worth preserving.”)
@yah:next(“DO NOT SOLVE THIS BY GIVING KAMAJI R2 CREDENTIALS PER TENANT. It fetches over plain HTTP from a public origin today and holds no bucket credential at all; adding one would put a tenant-scoped R2 key on three public boxes for content that is already world-readable. The W295 warning about the account-wide R2 write pair is the adjacent precedent.”)
@yah:verify(“End to end, from the noisetable camp: yah cloud apply --service noisetable-marketing --env cloud with bucket = "noisetable-marketing" unchanged reaches Running rather than Failed on us-east-001, and https://noisetable.com/ serves the site instead of the R870-F5 holding page.”)
@yah:verify(“Regression, single-tenant: yah-marketing’s own deploy is unchanged with no edit to its mirror — it declares bucket = "yah-dev" and the node’s KAMAJI_BUNDLE_ORIGIN already points there, so the fallback path must produce a byte-identical spec. Assert it at the wire type, not just by observing yah.dev stay up.”)
@yah:verify(“The runtime-asset half specifically: the deploy must NOT print ‘no mesofact/MesofactServeBundle gains origin: Option<String> — the public HTTPS origin serving the bucket the workload was published to — appended after env (postcard is positional; no skip_serializing_if), #[serde(default)] + #[ts(optional = nullable)]. None means the node’s own KAMAJI_BUNDLE_ORIGIN, so every existing single-tenant deploy is byte-identical and no yah-owned mirror needs an edit.”)
@yah:handoff(“A URL, NOT A BUCKET, decided rather than defaulted. A bucket name is not fetchable: resolving one would need a node-side bucket→origin table (the same node-wide defect one level down) or R2 credentials on every box, for content that is world-readable and against a node that deliberately holds none. providers.static.asset_origin already makes this call one tier over. Declared rather than derived from zone, because https://cdn.<zone> is a guess about an R2 custom-domain binding that may not exist.”)
@yah:handoff(“PUBLISHER SIDE (oss/yubaba/crates/cloud/src/reconciler/mesofact_bundle.rs): BundleSlot gains origin, origin joins ALLOWED_SLOT_KEYS, and serve_bundle stops dropping the store on the floor. Parsed strictly — a schemeless value (cdn.noisetable.com, or a bucket name) is REFUSED at parse with the reason, because its only consumer joins keys onto it as path segments, so accepting one would deploy clean and fail on the node at materialize time. A trailing slash is trimmed once, here.”)
@yah:handoff(“NODE SIDE (oss/kamaji/crates/kamaji-bin/src/server.rs): new BundleBackend::store_for(origin). None returns the node’s store itself, unwrapped. Some builds an HttpReadOnlyObjectStore (on the blocking pool — a reqwest blocking client panics if constructed inside a tokio runtime) and puts it in FRONT of the node’s, not in place of it. All three fetch sites take it: the manifest+blob materialize, the serve runtime-asset resolve, and the feed-tier fetcher (threaded through fork_revalidate_receiver → fork_feed_tier).”)
@yah:handoff(“THE READ-THROUGH IS WHAT MAKES THE RUNTIME-ASSET HALF WORK, and it is why this is a chain and not a swap. The fleet publishes the stock mesofact/<ver> serve runtime once, to its own origin; a tenant has no reason to mirror ~70MB of it. Tenant origin answers for the tenant’s bundle, node origin answers for the stock runtime, and which is which is not knowable per key. New yah_object_store::FallbackObjectStore (oss/yah-base/crates/object-store/src/fallback.rs) does exactly that and nothing else: reads chain, writes are REFUSED (a chain has no principled answer to which member a put lands in — guessing would put one tenant’s bytes in another’s store, the boundary this ticket exists to draw). Safe because every key on this path is content-addressed and blake3-verified after the fetch, so a fallback can return the wrong store’s bytes only if they are the right bytes. A primary ERROR is not laundered into a miss: an unreachable tenant origin fails loudly instead of quietly serving yah’s copy.”)
@yah:handoff(“APPLY-TIME NOTE CORRECTED (app/yah/cli/src/cloud.rs): ‘no mesofact/cargo run -p xtask -- emit-schemas + export-ts. .yah/schema/workload.toml.schema.json and packages/yah/workload-spec/index.ts carry the new field. (.yah/schema/machine.toml.schema.json also moved — it was already dirty in this shared tree when this session started, not something this ticket authored.)”)
@yah:verify(“cargo test -p yah-object-store -p yah-workload-spec (oss/yah-base): 101 pass, 0 fail — includes 7 new FallbackObjectStore tests (primary wins, clean miss falls through, head chains, a primary error is NOT a miss, locate names both members, writes refused, list_prefix unions).”)
@yah:verify(“cargo test -p yah-cloud –lib (oss/yubaba): 1134 pass, 0 fail — 4 new: a declared origin reaches the serve_bundle; NO declared origin leaves the node-wide one in charge (the single-tenant regression, asserted at the wire type, not by watching yah.dev stay up); trailing slash trimmed; a schemeless origin is refused naming the shape.”)
@yah:verify(“cargo test -p kamaji-bin –features bundle-serving –lib (oss/kamaji): 270 pass, 0 fail (was 267). THE END-TO-END ONE IS a_second_tenants_bundle_materializes_from_its_own_origin: the tenant’s bundle is published to a store served over a REAL loopback HTTP/1.1 origin (not a second injected ObjectStore — a test that handed the backend an in-memory store would pass with the URL ignored, which is the bug), the node’s store holds ONLY the fleet’s stock runtime, and the deploy comes up: tenant content from the tenant’s origin, stock runtime read through to the node’s. without_an_origin_a_tenant_bundle_fails_after_a_clean_admission is its negative control — the reported bug verbatim. an_undeclared_origin_resolves_to_the_node_store_unchanged pins Arc::ptr_eq on the None path so the single-tenant case pays nothing for the mechanism.”)
@yah:verify(“cargo check -p kamaji-bin with NO features (the bundle-serving-off build) and cargo test -p kamaji-proto (33 pass, the postcard round-trip): both clean.”)
@yah:verify(“cargo check –workspace: exit 0. Every remaining warning is pre-existing in files this ticket did not touch (board.rs, runner/sessions.rs, mesofact_static.rs, agent-tools/shared_pool.rs).”)
@yah:verify(“NOT VERIFIED LIVE, AND CANNOT BE FROM HERE: the ticket’s first verify is a yah cloud apply from the noisetable camp reaching Running. That needs the fleet to be RUNNING this kamaji — us-east-001 still runs the pre-change binary, which ignores the field. Filed as R870-T9 (roll kamaji, then add the one origin line to noisetable’s mirror), which depends_on this ticket.”)
@yah:gotcha(“ROLL ORDER DOES NOT BITE, checked rather than assumed: origin reaches yubaba as JSON and MesofactServeBundle carries no deny_unknown_fields, so a mirror declaring an origin against an un-rolled node has the field ignored and fails exactly as it does today — not a parse error. Only kamaji has to move.”)
@yah:gotcha(“THE NODE CACHE IS SHARED ACROSS ORIGINS AND THAT IS FINE FOR BUNDLES, LESS OBVIOUSLY SO FOR RUNTIME ASSETS. Materialized bundles are keyed by blake3 digest, so two tenants cannot collide. Runtime assets are keyed by <runtime>/<version>/<triple> — so two tenants publishing DIFFERENT bytes under the same mesofact/<ver> would share one node cache entry, first writer wins. Not reachable today (nobody but the fleet publishes a mesofact runtime, and publish_runtime_asset’s own docs already say publish a new version rather than repointing one), but it is the next thing this axis will need if a tenant ever ships its own build of a stock runtime name.”)
Fields§
§digest: BlakeHashBLAKE3 digest of the published bundle manifest — the content-address
kamaji materializes from the bundle store (yah_mesofact_bundle,
R599-F1). Same 64-hex shape the bundle crate’s BundleHash validates.
runtime: StringRuntime that serves the bundle: "self" (bundle ships its own
bins/<triple>/serve) or "mesofact/<version>" (resolve the stock
serve runtime asset from the node cache). Wire-mirrors
yah_mesofact_bundle::BundleRuntime; kept as a plain String here so
workload-spec stays free of the bundle crate and its non-TS/schema
newtypes.
lifecycle: BundleLifecycleHow kamaji supervises the served bundle. Default: keep-alive.
port: Option<u16>Port the served bundle listens on (R599-F12). This is the bundle-tier
analogue of a container’s expose.mesh.ports: the declared serving
port, which a proxy pairs with the workload’s mesh IP to get a dialable
address.
None → the supervisor allocates one (R844-F2), and reports the
port it bound back to yubaba on the next workload listing, where it
lands in the service record an ingress upstream is rendered from. This
is the normal case: a mirror should not have to name a port at all.
It used to mean “fall back to kamaji’s node-wide default
(KAMAJI_BUNDLE_PORT, else 8080)”, which was a single node-wide slot
wearing the word default — correct only while a node hosted one
bundle, and a silent collision for the second. R599-F12 added this field
so a workload could opt out of that; R844-F2 removed the default itself,
so opting out is no longer something anyone has to remember to do.
Declaring a port still pins it exactly, for a workload that must be reachable at a known number.
No skip_serializing_if — see serve_bundle’s note: the postcard wire
codec is positional, so an omitted byte shifts every later field.
env: BTreeMap<String, String>Environment the serve process is forked with (R556-T12) — already
resolved values, NAME → value.
This is what makes an SSR route that reads a private source deployable
at all: mesofact serve resolves a source’s credentials from its own
process environment at request time, and before this field the static /
SSR serve process was forked with env: vec![] while only the
revalidate_receiver sub-slot carried any. A declared-authed SSR site
therefore deployed clean and failed per request on the node.
Resolution happens deploy-side, exactly like
MesofactRevalidateReceiver::env: the mirror declares source URIs
(vault:<slot> / env:<VAR>), yah cloud apply resolves them against
the operator’s vault, and the node receives values. Keystore slot names
never cross the wire.
Appended after port — see port’s note: the postcard wire codec
is positional, so a new field goes last and never carries
skip_serializing_if.
origin: Option<String>Public HTTPS origin serving the bundle store this workload was published
to (R870-B6) — e.g. "https://cdn.noisetable.com", the R2 custom domain
bound to the tenant’s own bucket.
None → the node’s own KAMAJI_BUNDLE_ORIGIN, which is the shape every
yah-owned mirror uses and the reason this is optional rather than
required.
§Why the store has to travel with the workload
This is port’s defect one axis over, and it was found the same way: by
a second tenant. Publishing is per-service — providers.bundle.bucket
names the tenant’s own R2 bucket — while fetching was per-node, from
the single KAMAJI_BUNDLE_ORIGIN the systemd drop-in sets. Those two
agree only while the fleet hosts one tenant. The noisetable deploy got
all the way to admission and then failed with missing blob manifests/<digest>: its manifest was in noisetable-marketing, and the
node looked in cdn.yah.dev.
Pointing the tenant’s slot at yah’s bucket would also have worked, and was refused — a tenant’s build output in yah’s store makes the tenant boundary fictional for bundle content.
§Why a URL and not the bucket name
A bucket name is not fetchable. Resolving one would need either a
node-side bucket→origin map (another node-wide table, the same defect
again) or R2 credentials on every box — for content that is already
world-readable, and against a node that deliberately holds no bucket
credential at all (see HttpReadOnlyObjectStore). Integrity comes from
the content address, not the transport, so an unauthenticated origin is
exactly as safe here as an authenticated one.
The declared origin does not replace the node’s: kamaji reads through
to KAMAJI_BUNDLE_ORIGIN on a miss, which is what lets a tenant fetch
the stock mesofact/<ver> serve runtime the fleet publishes once
without republishing ~70MB into their own bucket.
Appended after env — see port’s note on the positional codec.
Trait Implementations§
Source§impl Clone for MesofactServeBundle
impl Clone for MesofactServeBundle
Source§impl Debug for MesofactServeBundle
impl Debug for MesofactServeBundle
Source§impl<'de> Deserialize<'de> for MesofactServeBundle
impl<'de> Deserialize<'de> for MesofactServeBundle
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for MesofactServeBundle
Source§impl PartialEq for MesofactServeBundle
impl PartialEq for MesofactServeBundle
Source§impl Serialize for MesofactServeBundle
impl Serialize for MesofactServeBundle
impl StructuralPartialEq for MesofactServeBundle
Source§impl TS for MesofactServeBundle
impl TS for MesofactServeBundle
Source§type WithoutGenerics = MesofactServeBundle
type WithoutGenerics = MesofactServeBundle
WithoutGenerics should just be Self.
If the type does have generic parameters, then all generic parameters must be replaced with
a dummy type, e.g ts_rs::Dummy or (). The only requirement for these dummy types is that
EXPORT_TO must be None. Read moreSource§type OptionInnerType = MesofactServeBundle
type OptionInnerType = MesofactServeBundle
std::option::Option<T>, then this associated type is set to T.
All other implementations of TS should set this type to Self instead.Source§fn docs() -> Option<String>
fn docs() -> Option<String>
TS is derived, docs are
automatically read from your doc comments or #[doc = ".."] attributesSource§fn decl_concrete(cfg: &Config) -> String
fn decl_concrete(cfg: &Config) -> String
TS::decl().
If this type is not generic, then this function is equivalent to TS::decl().Source§fn decl(cfg: &Config) -> String
fn decl(cfg: &Config) -> String
type User = { user_id: number, ... }.
This function will panic if the type has no declaration. Read moreSource§fn inline(cfg: &Config) -> String
fn inline(cfg: &Config) -> String
{ user_id: number }.
This function will panic if the type cannot be inlined.Source§fn inline_flattened(cfg: &Config) -> String
fn inline_flattened(cfg: &Config) -> String
Source§fn visit_generics(v: &mut impl TypeVisitor)where
Self: 'static,
fn visit_generics(v: &mut impl TypeVisitor)where
Self: 'static,
Source§fn output_path() -> Option<PathBuf>
fn output_path() -> Option<PathBuf>
T should be exported, relative to the output directory.
The returned path does not include any base directory. Read moreSource§fn visit_dependencies(v: &mut impl TypeVisitor)where
Self: 'static,
fn visit_dependencies(v: &mut impl TypeVisitor)where
Self: 'static,
Source§fn dependencies(cfg: &Config) -> Vec<Dependency>where
Self: 'static,
fn dependencies(cfg: &Config) -> Vec<Dependency>where
Self: 'static,
Source§fn export(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
fn export(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
TS::export_all. Read moreSource§fn export_all(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
fn export_all(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
TS::export. Read moreSource§fn export_to_string(cfg: &Config) -> Result<String, ExportError>where
Self: 'static,
fn export_to_string(cfg: &Config) -> Result<String, ExportError>where
Self: 'static,
Auto Trait Implementations§
impl Freeze for MesofactServeBundle
impl RefUnwindSafe for MesofactServeBundle
impl Send for MesofactServeBundle
impl Sync for MesofactServeBundle
impl Unpin for MesofactServeBundle
impl UnsafeUnpin for MesofactServeBundle
impl UnwindSafe for MesofactServeBundle
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.