Skip to main content

MesofactServeBundle

Struct MesofactServeBundle 

Source
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 Option origin (or bucket) on MesofactServeBundle, threaded from BundleSlot::serve_bundle, with KAMAJI_BUNDLE_ORIGIN demoted to the fallback so every existing single-tenant deploy is byte-identical.”) @yah:next(“SCOPE IS THREE EDIT SITES, all named: (a) MesofactServeBundle gains the field (oss/yah-base/crates/workload-spec/src/lib.rs) — wire type, so schema + TS export + drift test move with it; (b) BundleSlot::serve_bundle stops discarding the slot’s bucket (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/ runtime asset is published yet’ when the tenant’s own origin serves one, and must still resolve the stock runtime for a tenant that publishes none.”) @yah:handoff(“FIXED, AND THE FIX IS THE PRECEDENT THE TICKET NAMED. 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/ runtime asset is published yet … or the node will have nothing to fork’ was true for a single-tenant fleet and is now false — for a tenant bucket it is the NORMAL state. It names the bucket it checked and says the node reads through to its own origin for the stock one.”) @yah:handoff(“REGENERATED: 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: BlakeHash

BLAKE3 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: String

Runtime 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: BundleLifecycle

How 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

Source§

fn clone(&self) -> MesofactServeBundle

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for MesofactServeBundle

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for MesofactServeBundle

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Eq for MesofactServeBundle

Source§

impl PartialEq for MesofactServeBundle

Source§

fn eq(&self, other: &MesofactServeBundle) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for MesofactServeBundle

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for MesofactServeBundle

Source§

impl TS for MesofactServeBundle

Source§

type WithoutGenerics = MesofactServeBundle

If this type does not have generic parameters, then 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 more
Source§

type OptionInnerType = MesofactServeBundle

If the implementing type is 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 ident(cfg: &Config) -> String

Identifier of this type, excluding generic parameters.
Source§

fn docs() -> Option<String>

JSDoc comment to describe this type in TypeScript - when TS is derived, docs are automatically read from your doc comments or #[doc = ".."] attributes
Source§

fn name(cfg: &Config) -> String

Name of this type in TypeScript, including generic parameters
Source§

fn decl_concrete(cfg: &Config) -> String

Declaration of this type using the supplied generic arguments. The resulting TypeScript definition will not be generic. For that, see TS::decl(). If this type is not generic, then this function is equivalent to TS::decl().
Source§

fn decl(cfg: &Config) -> String

Declaration of this type, e.g. type User = { user_id: number, ... }. This function will panic if the type has no declaration. Read more
Source§

fn inline(cfg: &Config) -> String

Formats this types definition in TypeScript, e.g { user_id: number }. This function will panic if the type cannot be inlined.
Source§

fn inline_flattened(cfg: &Config) -> String

Flatten a type declaration. This function will panic if the type cannot be flattened.
Source§

fn visit_generics(v: &mut impl TypeVisitor)
where Self: 'static,

Iterates over all type parameters of this type.
Source§

fn output_path() -> Option<PathBuf>

Returns the output path to where T should be exported, relative to the output directory. The returned path does not include any base directory. Read more
Source§

fn visit_dependencies(v: &mut impl TypeVisitor)
where Self: 'static,

Iterates over all dependency of this type.
Source§

fn dependencies(cfg: &Config) -> Vec<Dependency>
where Self: 'static,

Resolves all dependencies of this type recursively.
Source§

fn export(cfg: &Config) -> Result<(), ExportError>
where Self: 'static,

Manually export this type to the filesystem. To export this type together with all of its dependencies, use TS::export_all. Read more
Source§

fn export_all(cfg: &Config) -> Result<(), ExportError>
where Self: 'static,

Manually export this type to the filesystem, together with all of its dependencies. To export only this type, without its dependencies, use TS::export. Read more
Source§

fn export_to_string(cfg: &Config) -> Result<String, ExportError>
where Self: 'static,

Manually generate bindings for this type, returning a String. This function does not format the output, even if the format feature is enabled. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Compare self to key and return true if they are equal.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.