Expand description
WorkloadSpec — typed wire format for yubaba workloads.
This crate is the schema source of truth. It has zero dependencies on yubaba; yubaba depends on it, not the other way around. Agents and desktop code that construct specs can link this crate without pulling in yubaba’s containerd client.
Three validation layers live in validate: shape (sync, no I/O),
semantic (reads yubaba state), and environment (deploy-time). The schema
types live at the top level.
@yah:ticket(R222-T3, “Workload schema doesn’t match per-kind on-disk shapes (mesofact-static)”) @yah:assignee(agent:claude) @yah:at(2026-05-18T16:47:03Z) @yah:status(review) @yah:parent(R222) @yah:handoff(“Picked option (a): tagged-enum Workload envelope with per-kind variants. Added Workload { MesofactStatic(MesofactStaticWorkload), Container(WorkloadSpec) } + BuildConfig in workload-spec. WorkloadSpec stays the containerd RPC wire type (now also the kind="container" variant payload). xtask emit-schemas now renders workload.toml.schema.json as a oneOf over kind; schema drift test green. TS export updated. Arch doc ‘workloads — colocated, not registered’ rewritten to describe the envelope + both example kinds; B4 outlook updated to point at Workload.”) @yah:verify(“cargo check -p cloud && cargo test -p cloud && cargo check -p yah && cargo check -p agent-tools && cargo check -p yah –tests && cargo check -p agent-tools –tests”) @yah:verify(“cargo test -p xtask # schema drift test must stay green”) @yah:verify(“cargo run -p workload-spec –bin export-ts # idempotent regen”)
@yah:ticket(R256-F7, “Model mesofact container as two roles: transient build/publish job vs long-lived SSR/SPA runtime”)
@yah:assignee(agent:claude)
@yah:at(2026-05-25T20:08:29Z)
@yah:status(review)
@yah:parent(R256)
@yah:next(“role A — build/publish job: transient task that runs the build and PUTs to the object store, then exits/GC’d; needed whenever there is a build step (SSR or not)”)
@yah:next(“role B — SSR/SPA runtime: long-lived container, only present when the app has realtime/dynamic pages (this is what the ‘only if SSR/SPA’ gate applies to)”)
@yah:next(“decide the fidelity knob: does the build run in-container (matches CI, max fidelity, costs image+cold-start) or on host with yubaba orchestrating only the serving edge?”)
@yah:assumes(“in cloud these are separate: CI/build job produces artifacts, R2+CDN serve them, and a distinct worker serves any SSR — so one merged ‘mesofact container’ is the trap”)
@yah:handoff(“BuildMode enum added to workload-spec with HostSide (default) and InContainer { image } variants. MesofactStaticWorkload gains build_mode: BuildMode (skip_serializing_if default) and ssr_runtime: Option
@yah:ticket(R256-F9, “Almanac as a dependency manifest: orchestrator verifies I/O targets live before run; output invalidates mesofact sources cleanly”)
@yah:assignee(agent:claude)
@yah:at(2026-05-25T21:28:15Z)
@yah:status(review)
@yah:parent(R256)
@yah:next(“almanac is a manifest declaring inputs + outputs + cadence + command — NOT a bash cron; the declared I/O is the contract”)
@yah:next(“before a run the orchestrator verifies declared inputs exist AND output targets (e.g. the mesofact app + its source/object store) are reachable; if not → the run fails or waits/times out rather than producing orphaned output”)
@yah:next(“almanac output invalidates downstream mesofact sources cleanly + deliberately (a declared dependency edge, not a blunt rebuild-everything) — this is the reason it’s a named manifest, not a shell cron”)
@yah:next(“decide the not-ready policy knob: fail-fast vs wait-with-timeout vs requeue”)
@yah:next(“generalizes the OpenRouter refresher (spawn_almanac_refresher), which is the degenerate no-dependency case (output = JSON cache, no app target)”)
@yah:assumes(“precondition enforcement lives in the shared scheduler layer (embedded by camp for dev/sim, yubaba for cloud/ha) — it needs the workload registry + xlb-net discovery to answer ‘is the target up?’, which a bash cron lacks”)
@arch:see(.yah/docs/architecture/A024-vocabulary.md)
@yah:depends_on(R256-F6)
@yah:handoff(“AlmanacTarget (Http/Tcp probe), NotReadyPolicy (WaitWithTimeout default=5s/FailFast/Requeue), Cadence (Once/Every/Cron), and AlmanacManifest types added to workload-spec. Workload enum gains Almanac(AlmanacManifest) variant (kind=‘almanac’). NotReadyPolicy::WaitWithTimeout(5s) is the default — matches sim-tier spinup budget. AlmanacManifest.invalidates: Vec
@yah:relay(R335, “Almanac mirror-binding — scope a feed to the mirror it affects”) @yah:at(2026-05-27T02:19:09Z) @yah:status(open) @arch:see(.yah/docs/working/W058-almanac-mirror-binding.md) @yah:depends_on(R256-F9)
@yah:ticket(R335-S1, “Decide cross-env pollution mechanism: extend R256-F9 manifest vs add per-mirror capability”) @yah:assignee(agent:claude) @yah:at(2026-05-27T02:19:33Z) @yah:kind(spike) @yah:status(review) @yah:phase(P1) @yah:parent(R335) @yah:gotcha(“Build ON R256-F9’s AlmanacManifest (workload-spec/src/lib.rs) — do NOT invent a parallel manifest. R256-F9 is in review.”) @yah:depends_on(R256-F9) @yah:handoff(“Decided. Recorded in almanac-mirror-binding.md §11. KEY FINDING: two almanac paths exist; the live R330 feed uses almanac::FeedConfig (on_change=MesofactRebuild{service,route} — a service id, NOT a MeshIdent), so it never touches AlmanacManifest.invalidates. Verdict on the S1 title: NEITHER extend the manifest nor (yet) add capability is the accident fix — dev->cloud is ALREADY blocked by construction (feed path = process locality + per-mirror reconciler + MinIO/R2 backend split; manifest path = no camp-embedded MeshState, mesh resolution is yubaba-raft-only). Residual holes: /revalidate receiver is UNAUTHENTICATED, and same-tier (two clouds on one R2) has no per-mirror key prefix.”) @yah:next(“FILED: R335-F3 (P1, no yubaba dep) mirror-aware /revalidate receiver — reject feeds not bound to this mirror; satisfies R335-T2; lands with R330-F4.”) @yah:next(“FILED: R335-F4 (P2) per-mirror artifact key prefix in derive_minio_key/publish_to_r2 — closes same-tier collision.”) @yah:next(“FILED: R335-F5 (P3, BLOCKED on yubaba control plane) per-mirror capability gate on /revalidate via yubaba/xlb-net node identity.”)
@yah:ticket(R278-F4, “RolloutPolicy schema in workload-spec (TOML types)”) @yah:assignee(agent:claude) @yah:at(2026-06-01T02:31:25Z) @yah:status(review) @yah:parent(R278) @yah:next(“Add src/rollout.rs with RolloutPolicy, RolloutStrategy, RolloutGate, RolloutStep, RolloutOnFailure”) @yah:next(“Export pub mod rollout from lib.rs”) @yah:next(“Add TS export via ts-rs in export-ts.rs”) @arch:see(.yah/docs/working/W140-yah-yubaba-ci-cd.md) @yah:handoff(“RolloutPolicy, RolloutStrategy, RolloutGate, RolloutStep, RolloutOnFailure added to workload-spec/src/rollout.rs. Exported from lib.rs. toml dev-dep added for round-trip test. Tests: rollout::tests::round_trip_toml + on_failure_default both green.”)
@yah:ticket(R429-T1, “Workload::StaticAsset variant + schema in workload-spec (catalog + aliases)”)
@yah:assignee(agent:claude)
@yah:at(2026-06-03T23:24:20Z)
@yah:status(review)
@yah:phase(P1)
@yah:parent(R429)
@yah:next(“Add Workload::StaticAsset(StaticAssetWorkload) variant alongside the existing MesofactStatic + Container envelopes. Mirror the tagged-enum shape R222-T3 established.”)
@yah:next(“StaticAssetWorkload fields: kind=‘static-asset’ tag, assets: Vec
@yah:ticket(R429-F2, “static-asset reconciler: BLAKE3 verify + S3 PUT against mirror’s object_store + drift”)
@yah:assignee(agent:claude)
@yah:at(2026-06-03T23:24:38Z)
@yah:status(review)
@yah:phase(P2)
@yah:parent(R429)
@yah:next(“New reconciler that handles kind=‘static-asset’ in the same service-sync loop that already runs mesofact-static + container. Same wave-gate semantics, same drift shape.”)
@yah:next(“For each [[asset]] row: hash source file (BLAKE3) and compare to manifest entry. Mismatch → surface as drift, halt push for that asset until rebuild.”)
@yah:next(“Resolve mirror’s object_store provider → R2 bucket + credentials. HEAD cas/filename; if absent or different content-length → PUT. Idempotent on re-run.”)
@yah:next(“Drift detection: list bucket contents under the component’s prefix, compare against catalog filenames. Files in bucket ∖ catalog → report as drift (do NOT delete; that’s the prune verb’s job).”)
@yah:next(“ServicesView’s existing matrix consumes the new drift shape automatically. Confirm SyncGlyph/DriftList render correctly for a static-asset row without UI changes.”)
@yah:next(“MockR2 in tests: HashMap<key, bytes> implementing the S3 surface the reconciler hits. Cover: push first-time, push idempotent, drift catches catalog-vs-bucket mismatch, BLAKE3 mismatch halts push.”)
@yah:next(“Real-R2 integration test gated behind YAH_TEST_R2_BUCKET env var — one round-trip against a scratch bucket; skipped otherwise.”)
@yah:verify(“cargo check –workspace –locked”)
@yah:verify(“cargo test -p
@yah:ticket(R429-T3, “yah service prune verb: candidate enumeration + operator-confirm delete”)
@yah:assignee(agent:claude)
@yah:at(2026-06-03T23:24:52Z)
@yah:status(review)
@yah:phase(P3)
@yah:parent(R429)
@yah:next(“yah service prune yah cloud service prune <name> --env <env> [--dry-run] [--yes] [--format=table|json] at app/yah/cli/src/cloud.rs (ServiceCommands::Prune + handle_service_prune). MCP tools cloud.service_prune_status (read, auto-pass, –dry-run –format=json) and cloud.service_prune (write, –yes –format=json) dispatch through build_command(). New S3 helper sign_s3_get_with_query in local-driver covers ListObjectsV2 (the existing s3_sign helpers don’t handle canonical query strings); ListObjectsV2 response is parsed with a tiny hand-rolled split_tags helper to avoid a quick-xml workspace dep. Tests: 12 prune-module unit tests (live-set union, kind filtering, list response parse for single/empty/truncated/no-token, candidate filtering including catalog manifest sidecar exclusion) + 1 s3_sign helper test + 2 MCP build_command tests. cargo check –workspace clean. cargo test -p cloud –lib: 279 pass (1 pre-existing failure cloud_init::tests::embedded_template_matches_workspace_canonical unrelated, per R419-F4 docstring). cargo test -p yah –lib: 299 pass.”)
@yah:next(“R429-F4 carries the Tauri + DeployPanel UI work — depends_on R429-T3, status=open.”)
@yah:verify(“cargo check –workspace –locked”)
@yah:verify(“cargo test -p cloud –lib reconciler::static_asset_prune # 12 pass”)
@yah:verify(“cargo test -p yah –lib mcp::tools::tests::cloud_service_prune # 2 pass”)
@yah:verify(“yah cloud service prune –help # renders usage with –env/–dry-run/–yes/–format”)
@arch:see(.yah/docs/working/W164-derived-static-assets.md)
@yah:ticket(R438-T2, “AssetEntry XOR: source vs derive + shape_static_asset rules”)
@yah:assignee(agent:claude)
@yah:at(2026-06-04T21:06:51Z)
@yah:status(review)
@yah:phase(P1)
@yah:parent(R438)
@yah:next(“AssetEntry.source: PathBuf → Option
@yah:ticket(R438-T3, “ImageRef digest-pin enforcement at deserialize”)
@yah:assignee(agent:claude)
@yah:at(2026-06-04T21:06:55Z)
@yah:status(review)
@yah:phase(P1)
@yah:parent(R438)
@arch:see(.yah/docs/working/W164-derived-static-assets.md)
@arch:see(.yah/docs/working/W165-mesofact-build-mode-lowering.md)
@yah:handoff(“ImageRef now accepts either a string form (digest-pinned, W164/W165 path) or the legacy struct form (backwards-compat for WorkloadSpec configs). String form requires @sha256:
@yah:ticket(R438-T7, “Golden tests: recipe→ForgeSpec lowering + BuildMode→ForgeSpec lowering parity”)
@yah:assignee(agent:claude)
@yah:at(2026-06-04T21:07:30Z)
@yah:status(review)
@yah:phase(P3)
@yah:parent(R438)
@yah:next(“Golden test: sample transform recipe + asset.derive.transform.params lowers to expected ForgeSpec (argv, image digest, TaskPlacement)”)
@yah:next(“Golden test: MesofactStaticWorkload with build_mode=in_container lowers to expected ForgeSpec”)
@yah:next(“Round-trip parity: same Subprocess + Local + Container quadrant for both consumers; regression-guards argv-substitution and image-pin drop-through”)
@yah:verify(“cargo test -p workload-spec lowering_golden_*”)
@yah:verify(“Golden files versioned; updates require explicit –update flag”)
@arch:see(.yah/docs/working/W164-derived-static-assets.md)
@arch:see(.yah/docs/working/W165-mesofact-build-mode-lowering.md)
@yah:depends_on(R438-T5)
@yah:depends_on(R438-T6)
@yah:handoff(“T7 landed. (1) Extracted pure lowering helpers exposed at pub(crate):\n - mesofact_static::lower_build_to_forge_spec(workload_dir, &BuildConfig, &BuildMode) -> ForgeSpec (run_build now wraps this)\n - static_asset::lower_recipe_step_to_forge_spec(&TransformRecipe, &RecipeStep, substituted_argv) -> ForgeSpec (materialize_transform now calls this for each step)\n(2) New cfg(test) module crates/yah/cloud/src/reconciler/lowering_golden.rs registered from reconciler/mod.rs. Five golden tests:\n - golden_recipe_step_lowers_to_pinned_local_container_subprocess (recipe → ForgeSpec shape: argv, image digest, timeout, label, initiator)\n - golden_recipe_step_with_zero_timeout_lowers_to_none (regression-guards the timeout=0 → None mapping)\n - golden_build_in_container_lowers_to_pinned_local_container_subprocess (BuildMode::InContainer → sh -c shell wrap + pinned image + cwd label)\n - golden_build_host_side_lowers_to_native_quadrant_without_image (BuildMode::HostSide → image=None + TaskRuntime::Native)\n - parity_recipe_and_build_in_container_share_quadrant (THE architectural invariant: both consumers land in the same Subprocess + Local + Container quadrant with sha256-pinned images and Gnome initiators — lets one ForgeExecutor dispatch handle both)\n(3) Test artifacts are hand-coded assertions, not insta/snapshot files — workspace has no insta infra and explicit-Pin tests give clearer diff on drift than auto-update snapshots. The W164/W165 lowering shape is now regression-guarded against silent drift in either consumer. cargo test -p cloud –lib reconciler::lowering_golden: 5 pass. Workspace check clean.”)
@yah:next(“Sign off → archive R438-T7”)
@yah:next(“T8 (worked examples) now has tested lowering primitives to reference”)
@yah:verify(“cargo test -p cloud –lib reconciler::lowering_golden — 5 pass”)
@yah:verify(“cargo test -p cloud –lib reconciler:: — 124 pass; 4 pre-existing R441-B4 adopt_only failures (port 4321 dev-box collision) unrelated”)
@yah:verify(“cargo check –workspace –locked — clean (warnings only)”)
@yah:verify(“Parity test asserts both lowerings produce TaskPlacement{Local, Container} + ForgeCommand::Subprocess + sha256-pinned image — the shared executor dispatch invariant”)
@yah:gotcha(“Test location pivot: original ticket said cargo test -p workload-spec lowering_golden_* but the lowering primitives don’t live in workload-spec — ForgeSpec/TaskPlacement are in task, and the actual lowering helpers are in cloud (both consumers live there). Tests landed in cloud as reconciler::lowering_golden. If a future consumer outside cloud needs the BuildMode lowering, lift lower_build_to_forge_spec up to task::transforms alongside the existing recipe lowering primitives.”)
@yah:gotcha(“No snapshot/insta infra in workspace — ‘Golden files versioned; updates require explicit –update flag’ verify line interpreted as hand-coded explicit assertions instead. Drift surfaces as a single-file test diff on the lowering helper, which is more readable than a .snap diff for the small ForgeSpec shape these tests cover.”)
@yah:ticket(R594-F2, “Ingress workload kind in workload-spec: pinned-per-node appliance on public-ip-tainted machines”) @yah:status(review) @yah:assignee(agent:claude) @yah:at(2026-07-03T06:03:30Z) @yah:phase(P2) @yah:parent(R594) @yah:next(“Add the ingress workload kind to the Workload enum (lib.rs:296) as an appliance in the R572 archetype sense: pinned-per-node, non-drainable, placed by yubaba only on machines carrying a public-ip taint, supervised by kamaji. Depends on R572-F1 (lifecycle archetype discriminator) so the archetype field exists to mark it. Breaking change is fine (pre-release house style); update kamaji-bin server.rs InvalidSpec rejection list deliberately — kamaji MUST accept this kind (it supervises the proxy), unlike MesofactStatic/Almanac/StaticAsset.”) @yah:verify(“cargo test -p yah-workload-spec; cargo check -p yubaba -p kamaji-bin; kamaji admission accepts kind=ingress in a unit fixture”) @yah:gotcha(“RUNS SOLO: workload-spec is the shared-type DAG sink (yah-base) — every lane (yubaba, kamaji, qed, host app) rebuilds on its change. Pause all other wave-2/3 implementer lanes while this is active, and check R572-T2 (cpu_millis, Handoff) + R572-F1 owner state before claiming — same file.”) @yah:depends_on(R572-F1) @yah:tier(Cleric) @yah:handoff(“Modeled the W267 public-ingress appliance as a container-shaped workload (Workload::Container(WorkloadSpec)), not a new Workload variant: mark archetype = Some(LifecycleArchetype::Appliance) (R572-F1, pinned/non-drainable) and declare the public-ip placement requirement via a new annotation-based marker on WorkloadSpec (same zero-blast-radius pattern as existing wants_host_network/HOST_NETWORK_ANNOTATION, chosen specifically to avoid the ~26-call-site churn a new plain field forced for R572-F1’s archetype field, and to avoid an exhaustive-match update in peer-owned kamaji-proto/codec.rs that a new Workload variant would force). Added: WorkloadSpec::requires_taint() -> Option<&str>, const REQUIRES_TAINT_ANNOTATION = "yah.placement.requires-taint", const PUBLIC_IP_TAINT = "public-ip", plus doc comments on Workload::Container recording the modeling decision and its rationale, all in oss/yah-base/crates/workload-spec/src/lib.rs (single file changed). This only declares the requirement as inert metadata — matching taint field on machine TOML is R572-F3 (not yet present) and scheduler enforcement is R572-F5; both out of scope here, noted in the doc comments. Verified kamaji needs NO change: deploy_workload’s match in kamaji-bin/src/server.rs already dispatches any Workload::Container(_) to the containerd backend regardless of tier/annotations (only MesofactStatic/Almanac/StaticAsset hit the InvalidSpec rejection arm), confirmed by reading the code and by the existing deploy_container_without_feature_says_so / deploy_mesofast_static_is_rejected_as_invalid_spec unit tests both still passing unmodified. 2 new unit tests added (ingress_marked_spec_is_appliance_and_carries_public_ip_placement_requirement, ingress_marked_spec_round_trips_through_json_as_a_container_workload). cargo test -p yah-workload-spec –lib: 38/38 pass. cargo test -p yah-workload-spec –test round_trip: 7 pass, exactly the same pre-existing 2 postcard failures (round_trip_full_spec_through_postcard, workload_container_round_trips_through_postcard — R590-B3, unrelated) as before this change, confirmed not increased. cargo check -p yah-workload-spec / -p yubaba / -p kamaji-bin all clean, plus full cargo check –workspace in both oss/kamaji and oss/yubaba clean (only pre-existing unrelated warnings). No peer-owned file touched or needed.”)
@yah:ticket(R590-B10, “forge workload 256MB cgroup memory limit SIGKILLs real builds — rusty-v8 checkout OOMs (bumped to 32GB stopgap)”)
@yah:at(2026-07-12T00:14:52Z)
@yah:status(review)
@yah:assignee(agent:claude)
@yah:parent(R590)
@yah:severity(blocks-on-box-green)
@yah:next(“Proper fix: thread a per-step memory request from the pipeline (QedStep) through ForgeSpec -> WorkloadSpec so a build declares its footprint, instead of a blanket forge default. Also consider: build_oci_spec should treat memory_mb==0 as ‘omit the cgroup limit’ (unlimited) so dedicated build-workers aren’t capped by an arbitrary constant; pair with a node-sized default. Revisit the 32GB stopgap once per-step resources land.”)
@yah:verify(“yah qed run rusty-v8-musl on us-west-002 completes the checkout + gn/ninja compile without an OOM SIGKILL; a small forge task still runs (32GB is a ceiling, not a reservation).”)
@yah:gotcha(“PROVEN live (2026-07-11): with B7 networking fixed, the rusty-v8 build cloned the full V8 tree then git checkout third_party/icu DIED OF SIGNAL 9 (OOM). WorkloadSpec::for_forge set resources.memory_mb=256, which build_oci_spec turns into a hard cgroup memory.limit. /tmp is a RAM-backed tmpfs so the multi-GB source checkout counts against that 256MB too. Bumped for_forge to 32768 (32GB) as a CLI-side stopgap; verified the build proceeds past icu.”)
@yah:handoff(“FIXED + PROVEN LIVE (2026-07-11). WorkloadSpec::for_forge memory_mb 256 -> 32768 (oss/yah-base/crates/workload-spec/src/lib.rs). CLI-only change (spec is client-built), no kamaji redeploy. RESULT: with B7 networking, the rusty-v8 build previously OOM’d (SIGKILL/signal 9) at the icu git-checkout under the 256MB cgroup cap; now it clones the full V8 tree AND proceeds past icu into cargo/gn compilation (Compiling icu_locale_data/icu_calendar_data…) with task RUNNING. Stopgap 32GB ceiling; proper per-step memory request from the pipeline is the follow-up in the ticket body.”)
@yah:ticket(R546-B7, “workload_spec::Workload envelope is externally tagged (missing serde tag=kind) — no flat on-disk workload.toml can parse through it, broke yah cloud apply for EVERY static-asset component”)
@yah:phase(P1)
@yah:status(review)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:at(2026-08-03T00:44:43Z)
@yah:parent(R546)
@yah:next(“DO NOT simply add #[serde(tag = \"kind\")] without checking postcard: Workload is also a postcard wire type on the kamaji RPC path (kamaji-proto/src/codec.rs matches on it; round_trip tests exist). postcard is non-self-describing and cannot decode internally-tagged enums, so naive tagging risks breaking the kamaji wire. Decide deliberately: (a) tag it and prove the postcard round-trips still pass, or (b) split the types — an on-disk WorkloadManifest with tag=kind, leaving Workload as the untagged wire type.”)
@yah:next(“INTERIM FIX ALREADY LANDED (unblocks publishing): static_asset.rs::load_workload no longer routes through the envelope — it deserializes a small KindProbe { kind }, validates kind == "static-asset", then parses StaticAssetWorkload directly. Same approach seed_derivation_for_target already used successfully. This restored yah cloud apply and got the x86_64 rusty-v8 artifact published to the CDN (HTTP 200). The ENVELOPE itself is still broken for every other caller/kind.”)
@yah:next(“Fix the test/example disagreement: lib.rs ~L2544 should assert the FLAT kind = \"...\" shape that real files use, and examples/parse_whisper_toml.rs should run in CI so this cannot regress silently again.”)
@yah:gotcha(“SEVERITY: this silently broke yah cloud apply for EVERY static-asset component, not just rusty-v8. Verified against the long-published whisper catalog via the repo’s own examples/parse_whisper_toml.rs, which panics with the identical error — so the breakage is general and pre-existing, not caused by the R546 hash edits.”)
@yah:gotcha(“ROOT CAUSE: pub enum Workload (oss/yah-base/crates/workload-spec/src/lib.rs ~L386) derives Deserialize with ONLY #[serde(rename_all = \"kebab-case\")] — there is NO #[serde(tag = \"kind\")], despite its own doc comment stating ‘the kind field on the wire is the serde discriminator’. Without the tag it is EXTERNALLY tagged, so serde wants a map with exactly ONE key (the variant name). Every real workload.toml is FLAT (kind = \"static-asset\" + schema_version + [[asset]] + [aliases]), i.e. a multi-key map -> TomlError: wanted exactly 1 element, more than 1 element, reported confusingly at line 1 col 1.”)
@yah:gotcha(“WHY THE UNIT TEST DIDN’T CATCH IT: the passing test at lib.rs ~L2544 feeds the EXTERNALLY-tagged shape [[static-asset.asset]], which no on-disk file actually uses. So the test asserts the broken encoding and the example (parse_whisper_toml.rs) asserting the REAL encoding was never run in CI. The test and the example disagree; the example is right.”)
@yah:handoff(“DONE. Workload now carries TWO wire shapes behind hand-written Serialize/Deserialize that branch on is_human_readable() – option (a) and (b) from the ticket’s next-steps merged into one type instead of splitting it. TOML/JSON get the INTERNAL kind tag (the flat shape every on-disk file uses); postcard keeps the EXTERNAL variant-index encoding R590-B3 established for the kamaji UDS. Same idiom ImageRef already used for its string-vs-struct form, so there is now one precedent, not two mechanisms. Mirror enums (WorkloadTagged/WorkloadExternal + borrowing twins) carry the two encodings; their variant ORDER is load-bearing for postcard and is commented as such. schemars/ts-rs get tag=kind + rename_all via #[schemars(…)]/#[ts(…)] so the generated JSON schema and TS bindings describe the on-disk shape instead of the wire shape.”)
@yah:handoff(“SECOND BLOCKER, fixed in the same pass: after the tagging fix only 2 of 8 on-disk workload.toml files still parsed. SchemaVersion is a unit-variant enum wanting the string "V1", but 6 files (every mesofact-static + container + cloudflare-worker component) are authored schema_version = 1, and R438-T6 had worked around it by hand-extracting raw toml::Value subtrees in read_mesofact_build. Gave SchemaVersion a liberal-read/canonical-write Deserialize (accepts 1, "V1", "v1"; always serializes "V1"), on the same is_human_readable branch so postcard is untouched. B7’s stated goal is not met without it – a fixed envelope that still rejects 6 of 8 files is not fixed.”)
@yah:handoff(“FILES. (1) oss/yah-base/crates/workload-spec/src/lib.rs – Workload dual-shape impls + mirror enums; the lib.rs ~L2544 test that asserted the broken [[static-asset.asset]] encoding rewritten to the flat form, plus a new test pinning BOTH halves (flat kind in JSON, postcard round-trip). (2) …/src/version.rs – SchemaVersion custom Deserialize + 3 tests. (3) …/src/bin/export-ts.rs – path was 3 parents up from CARGO_MANIFEST_DIR, but 75d8df7e moved the crate under oss/yah-base and added a level, so since that commit the bin silently wrote to oss/yah-base/packages/ and the committed TS stopped tracking the Rust types (last real update Jun 28). Now 4. (4) scripts/check-workload-spec-ts.sh – cargo run -p yah-workload-spec fails from the camp root (crate is in the excluded oss/yah-base workspace); switched to –manifest-path. It was dead since the same commit. (5) packages/yah/workload-spec/index.ts + .yah/schema/workload.toml.schema.json regenerated (mirror.toml.schema.json also moved – that is @Ashguard:dragon’s W267 ingress field swept in by the shared regen, not mine).”)
@yah:handoff(“FIXTURE SWEEP (flagged by @Ashguard:dove mid-turn – my change, my sweep): 8 yah-cloud tests were red on hand-written externally-tagged TOML. Fixed reconciler/derive_cache_prune.rs (2 fixtures), reconciler/static_asset_prune.rs (4), validate.rs (1), tests/whisper_derive_e2e.rs (1), app/yah/cli/src/cloud.rs (alias-collision fixture + the_deploy_body_parses_as_a_workload_envelope_not_a_bare_spec, which asserted external tagging and now asserts a flat kind). NOT touched: yubaba/src/lib.rs bundle_deploy_tests – @Ashguard:dove already rewrote that one in their own relay’s module and asked me not to double-fix.”)
@yah:handoff(“COMMENTS CORRECTED, not left lying: static_asset.rs::load_workload and asset_status.rs both carried R546-B7 comments asserting the envelope is externally tagged and unusable. Both now say the envelope works and the direct StaticAssetWorkload parse is a deliberate shortcut (load_workload keeps it to produce a precise wrong-kind error naming the kind found; asset_status keeps it because component.kind is already checked upstream).”)
@yah:verify(“THE TICKET’S OWN REPRO NOW PASSES: cargo run --manifest-path oss/yah-base/crates/workload-spec/Cargo.toml --example parse_whisper_toml -> ‘parsed as StaticAsset / assets: 2 / aliases: 3 / shape validation: ok / populated-shape verifier: ok’. It panicked before against the long-published whisper catalog.”)
@yah:verify(“NEW CI GATE (the ticket’s third next-step): xtask/tests/workload_envelope.rs walks the whole camp for workload.toml, parses every file whose kind is one of the four modelled variants through workload_spec::Workload, and hard-asserts no error contains ‘wanted exactly 1 element’ – the exact external-tagging signature. It lives in xtask, not workload-spec, because that crate is in the standalone-exported oss/yah-base workspace and cannot reach app/ or .yah/. Runs under the check pipeline’s existing cargo-test step. Carries a SHRINK-ONLY KNOWN_GAPS list: a file that starts parsing FAILS the test until its entry is deleted, and a stale entry (file moved/deleted) also fails, so the list cannot rot or grow silently.”)
@yah:verify(“GREEN: yah-workload-spec –all-features (55 lib + round_trip/postcard + shape_fixtures, 10 targets, 0 failed); kamaji-proto 25/25 incl. deploy_container_round_trip and deploy_string_pinned_image_ref_survives_postcard_wire (the exact UDS path R590-B3 fixed – proves the binary branch is byte-unchanged); yubaba –lib 339/339; yah-cloud 610 lib + whisper_derive_e2e 1/1; yah –lib cloud:: 108/108; xtask schema_drift 2/2 and workload_envelope 1/1.”)
@yah:verify(“NOT MINE, seen while verifying: (a) reconciler::pond::tests::{ensure_sim_port_free_ok_when_unbound, port_has_listener_…} flake in a full-suite run and pass in isolation – they bind real ports and race on a busy dev box. (b) 10 arch::ticket::tests failures in cargo test -p yah --lib from a peer’s in-flight @yah: annotation-parser work; app/yah/cli/src/arch/ticket.rs has zero references to workload_spec, so nothing in this change can reach them. (c) MirrorConfig’s new ingress field (@Ashguard:dragon, W267/R594) broke the yah-cloud and yah-cli test builds mid-session; it cleared on its own as they swept call sites – I did not touch their files.”)
@yah:gotcha(“VARIANT ORDER IS LOAD-BEARING. WorkloadExternal / WorkloadExternalRef in lib.rs must list variants in the SAME order as Workload – postcard encodes an external tag as the variant INDEX, so reordering or inserting a variant anywhere but the end silently decodes kamaji UDS frames into the wrong variant. There is no type error for this. Commented at the definitions; the round_trip postcard tests catch a mismatch only if the payload types differ enough to fail decode.”)
@yah:gotcha(“TWO GAPS DELIBERATELY NOT CLOSED, filed as R658 (umbrella) -> R658-B1 (MesofactStaticWorkload.routes is a required top-level field but all 4 real files AND the CLI scaffold write it inside [build], so TOML scopes it to build.routes) and R658-B2 (kind = "container" selects two incompatible schemas: Workload::Container(WorkloadSpec) vs ContainerReconciler’s local docker build/run shape). Neither is the B7 tagging bug – they were invisible until the envelope started being exercised. B2 needs an operator naming decision. Both are pinned in workload_envelope.rs’s KNOWN_GAPS so they cannot be forgotten or silently widened.”)
@yah:gotcha(“HALF-STALE as of 2026-08-18: the gotcha above says both R658 gaps are pinned in workload_envelope.rs KNOWN_GAPS. Only R658-B2 (missing field image) still is. R658-B1 is CLOSED - all four missing field routes entries were deleted, every manifest and the yah cloud site init scaffold now write routes ABOVE [build], and BuildConfig carries serde(deny_unknown_fields) so the misplacement is a parse error rather than a dropped key. See R658-B1.”)
@yah:ticket(R626-S3, “Where does desired-state live? Durable per-workload replica count that survives reconcile loops and camp restarts (0↔1 vs scale-to-N)”)
@yah:status(review)
@yah:assignee(agent:bundle-anthropic-glimmerstone)
@yah:at(2026-07-23T17:47:24Z)
@yah:kind(spike)
@yah:phase(P3)
@yah:parent(R626)
@yah:handoff(“DECIDED + LANDED. Desired state lives in the CAMP DAEMON, in a durable camp-local document at replicas, where 0 = stopped — deliberately the SAME axis as workload_spec::WorkloadSpec.replicas so scale-to-N later lifts a ceiling instead of adding a second concept beside a boolean. MAX_SUPPORTED_REPLICAS = 1 today and set_replicas REJECTS anything higher rather than persisting an intent no supervisor can honour (a clamp would silently record something the operator did not ask for). No record = replicas 1: a declared workload runs unless someone said otherwise. updated_at + reason ride along so a stale intent is legible and the UI can say when/why. Writes are tmp-then-rename; reads FAIL OPEN (missing/unreadable/corrupt/newer-schema all mean ‘everything runs’, corrupt file preserved as .corrupt-<epoch_ms>) — fail-closed would mass-stop a camp on one bad byte, and a resurrection is the recoverable failure.”)
@yah:handoff(“LANDED: (1) app/yah/cli/src/desired_state.rs — DesiredStateDoc / WorkloadDesire / DesiredStateStore (load, desired_replicas, is_stopped, stopped_keys, set_replicas, stop, start, forget), 10 unit tests incl. survives-a-camp-restart, per-workload isolation, replicas>1 rejected AND not written, corrupt-file quarantine + fail-open, newer-schema fail-open, and an explicit ‘a stop is not a failure’ guard on the serialized document. (2) camp.rs: deploy_pond_mirrors and reconcile_pond_deploys now consult the store and skip stopped idents — this is THE enforcement point, since camp’s re-assert is the only place ‘stay stopped’ can be honoured. Extracted pond_idents_needing_deploy(declared, registered, stopped) as a pure helper with 5 tests, because the stopped-subtraction is the load-bearing half: a stopped workload is absent from yubaba’s registry ON PURPOSE and is indistinguishable from a failed deploy without the intent record. (3) .yah/.gitignore + app/yah/cli/templates/yah-gitignore-default gain /state. (4) .yah/docs/working/W287-desired-state-for-supervised-workloads.md carries the full rationale, the rejected options, the F4 build-on list, and the scale-to-N scoping.”)
@yah:handoff(“DELIBERATE NON-GOAL: writing intent does NOT actuate. The durable record must land even when the stop call fails, or a failed stop comes back on the next reconcile. Actuation is R626-F4’s job.”)
@yah:next(“R626-F4 is unblocked and now has a concrete spec — see W287 §5. It needs three things this ticket deliberately did not build: (a) a per-ident teardown on yubaba (PondRegistry has only shutdown_all, which drains everything; /pond/deploy and /pond/state are the only pond routes), (b) camp RPC methods workload.stop / workload.start writing through DesiredStateStore, (c) desired-vs-actual reporting.”)
@yah:next(“DO NOT add a Stopped variant to PondPhase (R626-F2’s noted gap). PondPhase is yubaba’s observation of REALITY; intent never crosses that wire by this decision. Camp is the one process holding both halves — render the pair instead: desired=stopped + actual=absent reads ‘deliberately stopped’; desired=running + actual=absent reads ‘down’.”)
@yah:next(“Scale-to-N stays scoped, not committed (W287 §6). WorkloadSpec.replicas makes N look one constant away; it is not. kamaji native.rs:592 rejects replicas>1, and the docker backend names containers by mesh identity (one identity, one container). N needs a placement layer above the single-workload supervisor: per-replica naming (identity==container name is what makes teardown resolve), per-replica host ports (pond publishes fixed ones — two replicas collide), per-replica mesh identity (a load-balanced set is an xlb-net concern), and a placement decision that is yubaba’s job on a fleet. Lifting MAX_SUPPORTED_REPLICAS is the entry point once that layer exists.”)
@yah:next(“Wire DesiredStateStore::forget into the undeclare path so the document doesn’t accumulate intent for mirrors that no longer exist.”)
@yah:verify(“cargo test -p yah –lib desired_state — 15 pass (10 desired_state::tests + 5 camp::r626_s3_desired_state_gate_tests), 0 fail”)
@yah:verify(“cargo check -p yah — clean (note: this camp’s tree is shared and was transiently broken by peers’ in-flight edits in oss/qed, yah-party, and yah-almanac during this run; none touched by this ticket)”)
@yah:verify(“BEHAVIOUR BAR (the one that matters): DesiredStateStore::for_camp(root).stop(ident) followed by a FRESH store over the same root still reports is_stopped — that is exactly a camp restart — and pond_idents_needing_deploy then omits that ident from an EMPTY registry, which is exactly a restarted yubaba. Asserted in camp::r626_s3_desired_state_gate_tests::a_stop_survives_a_camp_restart_end_to_end.”)
@yah:gotcha(“The store is camp-local and GITIGNORED on purpose. If a future ticket wants a stop to be shared/durable in the repo, that is a different decision (declaration vs intent) — re-open W287 §2 rather than moving the file into tracked territory.”)
@yah:gotcha(“Reads fail OPEN. Never ‘harden’ this into fail-closed: an unreadable document would then stop an entire camp, and the failure would be silent (nothing starts) rather than visible (the workload comes back).”)
@yah:relay(R658, “workload.toml envelope: two type-vs-reality mismatches R546-B7 uncovered but did not fix”) @yah:at(2026-08-03T00:43:00Z) @yah:status(open) @yah:assignee(agent:bundle-anthropic-ashguard) @yah:parent(R546)
@yah:ticket(R658-B1, “MesofactStaticWorkload.routes is a required top-level field, but every real file and the CLI scaffold write it inside [build]”)
@yah:status(review)
@yah:at(2026-08-19T02:08:02Z)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:parent(R658)
@yah:next(“REPRO: cargo test -p xtask --test workload_envelope with the file’s KNOWN_GAPS entry deleted -> missing field routes. Affects app/yah/web/marketing/workload.toml, external/scrabcake/site/workload.toml, .yah/infra/state/sources/scrabcake/site/site/workload.toml, oss/yubaba/crates/cloud/testdata/mesofact-in-container/workload.toml.") @yah:next("ROOT CAUSE: TOML scopes every key after a table header into that table. All four files write `routes = \"./mesofact.routes.ts\"` AFTER `[build]`, so it deserializes as `build.routes` -- but MesofactStaticWorkload declares `routes` as a required TOP-LEVEL field. BuildConfig ignores the unknown key, so it vanished silently.") @yah:next("THE SCAFFOLD AGREES WITH THE FILES, NOT THE TYPE: SITE_WORKLOAD_TOML in app/yah/cli/src/cloud.rs (~line 4977) emits `routes` inside [build] too, so every newly scaffolded site inherits the mismatch. Fix the type or fix the scaffold -- but they must agree, and whichever moves needs the other four files migrated with it.") @yah:next("WHY IT WENT UNNOTICED: nothing reads `routes` off the envelope. mesofact-static's reconciler never loads MesofactStaticWorkload whole (read_mesofact_build does raw toml::Value subtree extraction, R438-T6), and mesofact-build reads mesofact.routes.ts directly. The field is declared but dead.") @yah:next("AFTER FIXING: delete the four `missing field `routes entries from KNOWN_GAPS in xtask/tests/workload_envelope.rs – that test FAILS on a stale entry, so it will tell you.”)
@yah:next(“SPREAD, found 2026-08-14 by R715-T2: two MORE files hit this and are NOT in KNOWN_GAPS, so cargo test -p xtask --test workload_envelope is RED on a clean tree for everyone. The two are app/yah/web/chat/workload.toml and oss/mesofact/examples/hello/workload.toml, both the same routes-after-[build] shape. Deliberately NOT pinned into KNOWN_GAPS - silently widening the pin is what this ticket exists to stop. Migrate them alongside the other four when the type-vs-scaffold decision lands.”)
@yah:handoff(“DECIDED: the DATA moved to the type, not the type to the data. routes stays a TOP-LEVEL field of MesofactStaticWorkload; all eight on-disk manifests and the CLI scaffold now write it ABOVE [build]. Three reasons the reverse was wrong: (1) MesofactStaticWorkload is a postcard wire type over the kamaji UDS, so moving a field between structs is a wire break needing a lockstep kamaji+yubaba deploy; (2) the field’s own doc says it is what the RECONCILER reads to enumerate routes, i.e. deploy-time not build-time, so [build] is the wrong home semantically; (3) the reconciler’s own fixtures (mesofact_static.rs), three camp.rs fixtures and the struct literal at cloud.rs:5389 already agreed with the type - only the hand-authored TOML disagreed.”)
@yah:verify(“cargo test -p xtask –test workload_envelope - GREEN (was RED on a clean tree for everyone). All four missing field routes KNOWN_GAPS entries DELETED, not widened; only R658-B2’s missing field image remains.”)
@yah:handoff(“ROOT-CAUSE GUARD, the part that makes this not recur: workload_spec::BuildConfig now carries #[serde(deny_unknown_fields)] (oss/yah-base/crates/workload-spec/src/lib.rs). Migrating the files alone would have left the trap armed - serde silently dropping a stray [build] key is WHY a declared-but-dead field survived months unnoticed. Now the misplacement is a parse error naming routes, which is the one thing the author needs to move. Inert for the postcard kamaji wire (non-self-describing, positional); only constrains TOML/JSON.”)
@yah:verify(“cargo test –manifest-path oss/yah-base/crates/workload-spec/Cargo.toml –all-features - 121 lib (incl. 3 new R658-B1 tests) + 8 integration targets, 0 failed. New: mesofact_static_routes_parse_at_the_top_level, mesofact_static_routes_inside_build_is_rejected_by_name, unknown_build_keys_are_refused_rather_than_ignored.”)
@yah:verify(“cargo test –manifest-path oss/yubaba/Cargo.toml -p yah-cloud –lib - 878 passed, 0 failed.”)
@yah:verify(“cargo test -p yah –lib cloud:: - 120 passed, 0 failed, incl. the new site_init_tests::scaffold_workload_toml_parses_through_the_envelope_with_top_level_routes.”)
@yah:verify(“cargo test -p xtask - all 12 targets green, incl. schema_drift and workload_envelope.”)
@yah:verify(“./scripts/check-workload-spec-ts.sh - in sync (ts-rs ignores deny_unknown_fields, so no TS churn).”)
@yah:handoff(“DISCOVERED WORK, wider than the ticket title - deny_unknown_fields immediately caught TWO live files the envelope test structurally CANNOT see. app/yah/web/analytics/workload.toml and app/yah/web/dashboard/workload.toml are kind = mesofact-spa, which is not in the test’s MODELLED_KINDS, but mesofact-spa rides the SAME MesofactStaticReconciler (app/yah/cli/src/cloud.rs:5195) and therefore the same read_mesofact_build -> BuildConfig parse. Both had routes under [build]; without migrating them my own guard would have broken deploys for analytics.yah.dev and app.yah.dev. Both migrated. Swept every workload.toml in the camp for stray [build] keys: the only two remaining are crates/yah/cloud-admin (R658-B2’s file) and app/yah/workers/yah-cr, and NEITHER goes through workload_spec::BuildConfig - both use reconciler-local raw toml::Value extraction (cloudflare_worker.rs:200), so both are unaffected.”)
@yah:handoff(“ALSO FIXED IN THIS PASS (docs are canon; these were what a human copies): .yah/docs/guides/host-a-site-and-worker-on-yah.md:102 and .yah/docs/architecture/A031-yah-cloud-config-shape.md:438 both showed routes UNDER [build] - they would have re-seeded the bug into every hand-authored manifest. Also oss/yubaba/crates/cloud/src/config.rs:5890 (web_workload_round_trips fixture) and the bundle_assembly_tests fixture at app/yah/cli/src/cloud.rs.”)
@yah:handoff(“UNRELATED LANDED BREAKAGE unblocked to verify at all: oss/yubaba/crates/cloud/src/reconciler/lowering_golden.rs:48 failed to compile with E0063 missing field admission - TransformRecipe gained admission: Optionadmission: None (correct: the golden is an unsigned local recipe and pins the LOWERING shape, which the signature does not participate in). Both files were committed-clean, not a peer’s in-flight edit - checked git status before touching.”)
@yah:gotcha(“UNCOMMITTED REGEN - .yah/schema/workload.toml.schema.json is REGENERATED in the working tree (cargo run -p xtask – emit-schemas) and must be committed WITH this change. deny_unknown_fields makes schemars emit additionalProperties: false on BuildConfig. scripts/check-schema-drift.sh compares generated output against the git INDEX, so it stays RED until the regen is committed - that is the script working as designed, not drift. Only workload.toml.schema.json moved; no peer’s schema was swept in (git diff –stat – .yah/schema/ = 1 file).”)
@yah:assumes(“deny_unknown_fields on BuildConfig trades forward-compat for loudness: a manifest carrying a [build] key an older binary does not know is now a hard parse error, not an ignored key. Deliberate and argued in the type’s doc comment. Blast radius outside this monorepo is any site scaffolded by an older yah cloud site init - the template shipped routes under [build] for its whole life, so such a site now fails to parse until routes is moved above the header. The only tenant in the tree (scrabcake) was migrated; an external one would need the same one-line move.”)
@yah:gotcha(“NOW FULLY STALE as of 2026-08-19: the HALF-STALE note above says R658-B2’s missing field image is the one KNOWN_GAPS entry left. R783-F1/F2 closed that too, so KNOWN_GAPS in xtask/tests/workload_envelope.rs is EMPTY - every modelled on-disk workload.toml parses through the envelope. An entry reappearing means a real file stopped parsing.”)
@yah:ticket(R743-T4, “workload-spec: 7 test binaries to 1”)
@yah:at(2026-08-11T01:18:24Z)
@yah:status(review)
@yah:phase(P2)
@yah:parent(R743)
@yah:next(“tests/main.rs mod’ing all 7 siblings + autotests = false and [test] name = "main" in oss/yah-base/crates/workload-spec/Cargo.toml.”)
@yah:next(“tests/compose/ and tests/fixtures/ are data/module dirs, not targets — they are unaffected. Confirm the [[bin]] named export-ts in Cargo.toml is untouched by autotests = false (it is a bin, not a test, but read it before editing).”)
@yah:verify(“cargo test -p yah-workload-spec – –list count unchanged; three green runs. One commit — oss subtree.”)
@yah:tier(Cleric)
@yah:handoff(“LANDED: tests/main.rs mods in the 7 former top-level integration-test files (compose_import, mesh_resolver, restart_policy, round_trip, secrets_invariant, semantic, shape_fixtures) as submodules; Cargo.toml gained autotests = false on [package] plus a single [[test]] name = \"main\" path = \"tests/main.rs\". tests/compose/ and tests/fixtures/ untouched (data dirs); the export-ts [[bin]] untouched (autotests only scans tests/, not bins). Quick audit found nothing to fix: no std::env::set_var/remove_var, no set_current_dir, no TcpListener/bind/fixed ports in any of the 7 files, and only one inner mod secrets (in secrets_invariant.rs) which nests fine under its own file-module with no sibling collision — so no renames were needed.”)
@yah:verify(“RUSTC_WRAPPER="" cargo test -p yah-workload-spec – –list (run inside oss/yah-base): BEFORE 8 targets (lib 146 + export-ts bin 0 + 7 integration files summing to 65: compose_import 5, mesh_resolver 8, restart_policy 5, round_trip 16, secrets_invariant 7, semantic 18, shape_fixtures 6) = 211 total. AFTER 4 targets (lib 146 + export-ts bin 0 + single main integration binary 65, all 65 test names now module-qualified e.g. round_trip::round_trip_full_spec + doctests 0) = 211 total, unchanged.”)
@yah:verify(“RUSTC_WRAPPER="" cargo test -p yah-workload-spec (inside oss/yah-base): ok. 146 passed lib + ok. 65 passed main + 0 doctests, 0 failed — run three times, all green, no pre-existing failures to record.”)
@yah:ticket(R783-F1, “ContainerManifest: split the on-disk container manifest from the wire WorkloadSpec, keeping postcard byte-identical”)
@yah:status(review)
@yah:at(2026-08-19T07:11:49Z)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:parent(R783)
@arch:see(.yah/docs/working/W324-workload-kind-is-not-a-runtime.md)
@yah:next(“THE SEAM: introduce ContainerManifest = Reference(WorkloadSpec) | Recipe(ContainerBuild) and change Workload::Container’s payload to it. WorkloadExternal::Container KEEPS WorkloadSpec so the postcard kamaji wire is byte-identical - verify with the existing round_trip.rs postcard tests, which must pass UNCHANGED.”)
@yah:next(“WHY a recipe cannot just be a WorkloadSpec (this is the whole design): ImageRef.digest is String, not Optionyah-local/yah-cloud-admin:dev, a bare tag, because the digest does not exist until docker build has run. Preserve that invariant; do not weaken ImageRef to make this easier.”)
@yah:next(“Encode the invariant in the signature: ContainerBuild::into_spec(self, digest: &str) -> WorkloadSpec. The lowering is only available AFTER a build produced a digest. Serializing a Recipe to postcard must be an Err, not a panic and not a silent empty digest.”)
@yah:next(“Tier: Wizard - cross-workspace type split with a wire invariant to preserve; the postcard encoding is positional and a mistake decodes silently into the wrong variant.”)
@yah:verify(“cargo test –manifest-path oss/yah-base/crates/workload-spec/Cargo.toml –all-features - round_trip.rs postcard tests must pass UNCHANGED (they are the wire-compat gate).”)
@yah:verify(“cargo test -p xtask –test workload_envelope with the missing field image KNOWN_GAPS entry for crates/yah/cloud-admin/workload.toml DELETED - that file is the acceptance case.”)
@yah:verify(“cargo test –manifest-path oss/kamaji/Cargo.toml -p kamaji-proto codec - deploy_container_round_trip is the exact UDS path.”)
@yah:gotcha(“VARIANT ORDER IS LOAD-BEARING on WorkloadExternal/WorkloadExternalRef - postcard encodes the external tag as the variant INDEX, so reordering or inserting anywhere but the end silently decodes kamaji UDS frames into the WRONG variant, with no type error. Commented at the definitions in lib.rs.”)
@yah:gotcha(“BLAST RADIUS ~25 real construction/match sites across FOUR workspaces: oss/kamaji (incl. peer-owned kamaji-proto/src/codec.rs exhaustive matches), oss/yubaba, oss/qed, app/yah/cli, oss/yah-base. R594-F2 deliberately avoided exactly this churn by using an annotation instead of a field (lib.rs:225) - that was right for a marker, and is NOT right here, but read that note before assuming the churn is accidental.”)
@yah:gotcha(“Consider a Workload::container(spec) constructor + ContainerManifest::as_spec() accessor to keep the ~25 sites one-line mechanical rather than restructured.”)
@yah:handoff(“LANDED. ContainerManifest = Reference(WorkloadSpec) | Recipe(ContainerBuild) is now Workload::Container’s payload (oss/yah-base/crates/workload-spec/src/lib.rs). New public types: ContainerManifest, ContainerBuild, ContainerBuildStep, ContainerRunConfig, ContainerMount, plus Workload::container(spec) / Workload::container_spec() / Workload::container_manifest() so the ~25 call sites stayed one-line.”)
@yah:verify(“cargo test –manifest-path oss/yah-base/crates/workload-spec/Cargo.toml –all-features - 127 lib + 8 integration targets, 0 failed. round_trip.rs: 16 pass.”)
@yah:verify(“cargo test -p xtask - all 12 targets green incl. workload_envelope 1/1 with KNOWN_GAPS now EMPTY (R658-B2’s missing field image entry deleted, not widened) and schema_drift 3/3.”)
@yah:verify(“cargo test –manifest-path oss/kamaji/Cargo.toml –workspace - green incl. kamaji-proto codec 26/26 (deploy_container_round_trip, the exact UDS path) and kamaji-bin 213/213.”)
@yah:verify(“cargo test –manifest-path oss/yubaba/Cargo.toml -p yah-cloud –lib 881 pass / -p yubaba –lib 492 pass; cargo test -p yah –lib cloud:: 120 pass.”)
@yah:handoff(“THE WIRE CLAIM IS NOW A TEST, not an assertion. round_trip.rs::container_postcard_frame_is_the_variant_index_then_the_bare_spec asserts the frame is exactly [1] ++ postcard(WorkloadSpec) - a round-trip alone would still pass if both halves moved together. WorkloadExternal::Container keeps WorkloadSpec; Workload’s binary Serialize maps Reference through unchanged and returns Err for Recipe (round_trip.rs::container_recipe_is_refused_by_postcard_rather_than_encoded).”)
@yah:handoff(“DISCRIMINATOR: presence of a [build] table means Recipe; presence of top-level image means Reference; NEITHER is its own error naming both forms rather than a misleading missing field image. Hand-written Deserialize, not serde(untagged), specifically so a malformed reference still reports missing field tier instead of ‘data did not match any variant’.”)
@yah:gotcha(“UNCOMMITTED REGEN - both generated artifacts are regenerated in the working tree and must be committed WITH this change: .yah/schema/workload.toml.schema.json (cargo run -p xtask – emit-schemas) and packages/yah/workload-spec/index.ts (cargo run –manifest-path oss/yah-base/crates/workload-spec/Cargo.toml –bin export-ts). Both scripts/check-schema-drift.sh and scripts/check-workload-spec-ts.sh exit 1 right now because they diff generated output against the git INDEX - that is the scripts working as designed, not drift. cargo test -p xtask schema_drift (which diffs against the working tree) is GREEN.”)
@yah:gotcha(“SIGNATURE DEVIATION from the ticket text, deliberate: into_spec is ContainerBuild::into_spec(self, digest: &str, tier: TierTag) -> Result<WorkloadSpec, String>, not the infallible two-arg form the ticket sketched. Fallible because digest is a caller-supplied string and a malformed one must error rather than mint a spec that lies about being content-addressed - it routes through compose_import::parse_pinned_image_ref, the one home of the R438-T3 digest rule. tier is a parameter because admission control is cluster policy, not a manifest fact. Recorded in W324 under a new ‘As shipped (R783-F1)’ section.”)
@yah:assumes(“ContainerBuild::into_spec has NO production caller yet - it is the documented lowering with unit-test coverage only. Its unset-image default is yah-local/<manifest name>:dev, which is NOT the same string ContainerReconciler’s default_image_tag builds (yah-local/<service>-<component>:dev) because the manifest only knows its own name. If a future caller lowers a recipe whose [build].image was left unset and expects to find the image the reconciler built, those two defaults have to be reconciled first.”)
@yah:cleanup(“LocalProcessReconciler still parses its own private ProcessComponent for the [process] table (oss/yubaba/crates/cloud/src/reconciler/local_process.rs:696). The envelope does not model [process] at all, so that tier is still a second parser over the same file - the exact shape R783-F2 just removed for the container tier. W324 section 1 names it as the third runtime behind kind = container; folding it in is the natural next step and is deliberately NOT in R783.”)
@yah:ticket(R838-B1, “xtask workload_envelope fails on both machines: the template deliberately omits [build] command while workload_spec Workload requires it as a non-Option String”)
@yah:status(review)
@yah:at(2026-08-31T00:17:46Z)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:parent(R838)
@yah:handoff(“LANDED. workload_spec::BuildConfig.command is now Option
@yah:ticket(R658-B3, “mesofact new scaffolds a workload.toml the deploy path cannot execute: BuildConfig.command is required but the template deliberately omits it”)
@yah:status(review)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:at(2026-09-02T19:08:17Z)
@yah:parent(R658)
@yah:severity(high)
@yah:next(“DECISION REQUIRED, do not guess. oss/mesofact/crates/mesofact/src/cli/new/template/workload.toml (new in ef8bd656) declares kind = mesofact-static with no [build] command, and its own header comment says that is deliberate: ‘Left unset, mesofact-dev runs the build pipeline in-process — no third binary, no package manager, no Node.’ But BuildConfig.command is a required String (oss/yah-base/crates/workload-spec/src/lib.rs:1428), so the file does not parse through workload_spec::Workload.”)
@yah:next(“The deploy path has no in-process branch. MesofactStaticReconciler uses build.command unconditionally — oss/yubaba/crates/cloud/src/reconciler/mesofact_static.rs:1143 builds vec![sh, -c, build.command.clone()], and 1173/1179/1185 log and execute it. So making command Optioncommand: string | null at line 452. The unrelated required-"command" at schema line 126 is the Almanac/render struct (lib.rs:1661), which is correctly still a bare String. So scripts/check-schema-drift.sh and scripts/check-workload-spec-ts.sh no longer have anything to fail on for this change.”)
@yah:verify(“cargo test -p xtask –test main –locked – workload_envelope:: — 1 passed, 0 failed, 55 filtered out. every_on_disk_workload_toml_parses_through_the_envelope is ok; it was 0 passed / 1 failed with "missing field command" when this ticket was filed. This is the exact argv named in the ticket’s @yah:verify, and it is the criterion this ticket closes on.”)
@yah:gotcha(“DISPROVED A CLAIM ON R836-B2 IN PASS and recorded it there. Its COVERAGE NOTE said the failing assertion lives in xtask’s lib target which "neither" check.toml xtask step reaches, and its second @yah:next asked to widen the guard to include –lib. Measured: --tests DOES reach the lib target — running check.toml’s own xtask-tests argv produced the 43/1 result above and cargo’s footer read "error: test failed, to rerun pass -p xtask –lib". So that next step is a no-op and the bar already covers the assertion.”)
@yah:cleanup(“NOT FIXED, out of this ticket’s blast radius, flagged for whoever owns the mesofact_static reconciler: oss/yubaba/crates/cloud/src/reconciler/mesofact_static.rs:240-245 has 13 unused imports (EnvVar, ExposeSpec, ImageRef, MeshExpose, Millis, NamespaceId, ResourceLimits, RestartPolicy, SchemaVersion, StopPolicy, TenantId, TierTag, WorkloadSpec, NATIVE_IDENTITY_DIGEST). Warnings only, so nothing is blocked. They are pre-existing on committed main (file is clean in the working tree; last moved in committed 9677282b "mes", 2026-09-01), NOT introduced by the R838-B1 change and not a live peer’s WIP. Worth a look because that many newly-unused imports usually means a block of code was removed, and it is worth confirming that removal was intended rather than collateral.”)
@yah:gotcha(“CORRECTION to this ticket’s own inherited handoff line "cargo test -p xtask –tests is 54/54 green including workload_envelope". That was true when R838-B1 wrote it and is NOT true on main as of 2026-09-02. That argv now gives 43 passed / 1 failed, failing in the LIB target on cluster_epochs::tests::the_declaration_records_current_per_input_digests_for_every_axis ("state_epoch: recorded digest for rust-file oss/yubaba/crates/yubaba/src/raft/store.rs is stale"). That failure is R836-B2, is unrelated to workload.toml, and is deliberately NOT a regenerate-the-artifact case — it is a mixed-operation compatibility call (bump the state_epoch vs re-record the digest) owned by whoever owns the raft store change, so it was correctly left alone here. It matters for reading this ticket because --tests carries no –no-fail-fast: that single lib failure aborts the step before the integration binary runs, so workload_envelope is reported as neither passed nor failed rather than green. That is why this ticket was verified with the narrower --test main -- workload_envelope::, which isolates the gate this ticket actually owns. check.toml step xtask-tests stays red camp-wide until R836-B2 is answered.”)
Modules§
- admission
- Signed-recipe admission for dispatched workloads — W235 §(c), R555-F4.
- compose_
import - One-way, lossy compose YAML →
WorkloadSpecimport shim. - control_
plane_ install - Shared install-script builder for a control-plane (yubaba + kamaji) roll.
- forge_
produced - Convention for a remote forge step’s durable produced artifacts.
- forge_
state - The one host directory tree a QED forge step’s bind mounts may live under.
- rollout
- Rollout policy schema — the typed form of
.yubaba/rollout.toml. - secret_
mount - Where yubaba writes a
File-target secret it has resolved, and how the host path is derived from the container path. - secrets
- Pluggable secret resolver for
crate::SecretRefvalues, plus the access rule that decides which workloads a cluster secret may be served to. - sovereign
- The sovereign-group join rule — W305 / R742-F1, extended by R605-F12.
- validate
- Shape validators for
WorkloadSpec.
Structs§
- Almanac
Feed - One almanac feed handed to the on-node fetcher (R330-F31).
- Almanac
Manifest kind = "almanac"manifest — a declared data-pipeline job.- Asset
Derive - Provenance chain for a derived asset: required
fetchstep, optionaltransformstep. Materialized bytes replaceAssetEntry.sourcefor the rest of the static-asset reconcile loop. - Asset
Entry - A single file entry in the static-asset catalog.
- Backoff
Policy - Exponential backoff parameters for
RestartPolicy::OnFailure. - Blake
Hash - BLAKE3 content hash expressed as exactly 64 ASCII hex digits.
- Build
Config - Build step that produces the static artifact published by a
mesofact-staticworkload. - Container
Build kind = "container"in the recipe form: a Dockerfile next to the component’sworkload.toml, built and run on the operator’s box.- Container
Build Step - The
[build]table of a container recipe. - Container
Mount - One
[[run.mounts]]entry of a container recipe. - Container
RunConfig - The
[run]table of a container recipe. - Derive
Lock - W212/R518: the committed derivation lock — the in-tree action-cache
receipt.
input_hashis the input-addressed derivation key computed over the complete declared input set (fetched-input pin ⊕ recipe-file bytes ⊕ invocation params ⊕ schema version);output_blake3is what those inputs produced (== the entry’sblake3). The reconciler skips the entire build (no fetch, no transform, no PUT) when the lock matches the inputs recomputed from the current pins and the bucket already holds the output — the Nix-substituter / Bazel-remote-cache behaviour. Written by the R510 bind path from the reconciler’sdiscovered_input_hash:<filename>output; thegit diffon this block is the receipt that the derivation rolled. - EnvVar
- A single environment variable injected into the container.
- Expose
Spec - Network exposure configuration. The three channels are independent; any combination is valid.
- Fetch
Source - Shared fetch primitive — usable by
asset.derivetoday, and by Almanac’sReleaseSourceafter a follow-up migration (R438-F10). Defined once in workload-spec so both consumers reject the same set of non-permissive licenses. - Healthcheck
- Container health probe configuration.
- Image
Ref - Container image reference identifying a specific image to pull.
- Machine
Id - Opaque identifier for a yubaba-managed machine within the cluster.
- Mesh
Expose - Mesh-internal port exposure and peer access control.
- Mesh
Ident - DNS-segment identity for a workload on the cluster mesh, e.g.
"noisetable-api.pdx". Regex constraint:^[a-z0-9]([a-z0-9-]*[a-z0-9])?$, length ≤ 63. Enforced in shape validation (R090-F2). - Mesofact
Revalidate Receiver - Revalidate receiver config (R330-F12) — tells kamaji to fork a second
mesofact serve --revalidateprocess alongside the static bundle server. - Mesofact
Serve Bundle - Serve-time reference to a published W272 bundle (R599-F4) — the
{bundle_digest, runtime, lifecycle}triple amesofact-staticworkload carries when kamaji, not the build reconciler, serves it. - Mesofact
Static Workload kind = "mesofact-static"payload — static-site build colocated with the frontend it deploys.- Millis
- Duration expressed as an integer millisecond count.
- Namespace
Id - Namespace routing/naming axis (W206). A pure naming key that never
affects isolation: it selects the config root, disambiguates service DNS
names within a tenant, prefixes object-store bucket names within a tenant’s
bucket scope, and selects the provider zone (e.g.
noisetable.comvsyah.dev). Two namespaces in the same tenant share networks, mesh-identity space, and PG cluster — they simply cannot collide on workload names or external domains. Specs written before this axis existed deserialize toNamespaceId::singleton. - Operator
Expose - Operator-facing exposure via a Tailscale ACL tag.
- Public
Expose - Public internet exposure via a Cloudflare tunnel route.
- Resource
Limits - Hard resource caps enforced by containerd/cgroups at runtime.
- Secret
Mount - A secret value mounted into the container as an env var or file.
- Static
Asset Workload kind = "static-asset"payload — content-addressed bucket catalog.- Stop
Policy - Graceful shutdown configuration for yubaba’s stop sequence.
- Tenant
Id - Tenant isolation axis (W206). Separates one operator’s workloads from
another’s at the network / DB / mesh-identity level. Orthogonal to
NamespaceId(routing/naming) andTierTag(workload class within a(tenant, namespace)pair). - TierTag
- Tier classification that governs admission control and mesh
allow_fromfiltering. Known values:"public","tenant","private","infra". Custom tiers are allowed per cluster; shape validation warns on unknowns rather than rejecting them (R090-F2). - Transform
Spec - Optional transform applied after a
FetchSourcedownload, lowering to aForgeCommand::Subprocessvia the recipe loader (R438-T4). The transform’s output is content-addressed by the entry’sblake3(the recipe runs only when the cache misses). - Volume
Mount - A volume mount inside the container.
- Workload
Spec - Complete typed description of a containerd workload handed to yubaba over
RPC. This is also the payload of the
kind = "container"variant ofWorkloadon disk.
Enums§
- Almanac
Target - An observable endpoint the almanac scheduler probes to check readiness.
- Build
Mode - Where the build command runs for a
mesofact-staticworkload. - Bundle
Lifecycle - Lifecycle mode for a served bundle (W272 §3).
- Cadence
- When the almanac scheduler triggers a run.
- Container
Manifest - On-disk payload of
kind = "container"— two forms, one wire type (W324 §5). - EnvValue
- Value source for an environment variable.
- Health
Probe - Mechanism used to check container health.
- License
- Closed-set, parse-time-enforced license tag. Mirrors the workspace
permissive-license rule (MIT / Apache-2.0 / BSD-2/3-Clause / ISC). Adding a
variant is an explicit schema change — non-permissive strings
(
"GPL-3.0","AGPL", etc.) fail at serde-deserialize before any shape validator runs. - Lifecycle
Archetype - Explicit lifecycle archetype for a
kind = "container"workload (W244). - Mesh
Lookup - Which aspect of a mesh peer’s address to inject.
- Mesh
Peer - A peer permitted to initiate mesh connections to a workload (W206 / R558-F3).
- NotReady
Policy - What the almanac scheduler does when a precondition check fails.
- Public
Tls - TLS mode for a public endpoint.
- Restart
Policy - What yubaba does when the container exits.
- Schema
Version - Wire-format schema version envelope.
- Secret
Ref - Where yubaba resolves the secret value from.
- Secret
Target - How the secret is surfaced inside the container.
- Volume
Source - Backing source for a volume mount.
- Workload
- On-disk
workload.tomlmanifest. Each variant matches oneServiceComponent.kindvalue; thekindfield on the wire is the serde discriminator.
Constants§
- DEFAULT_
NAMESPACE - Default single-namespace identity for specs that predate the namespace
axis (W206). See
NamespaceId::singleton. - DEFAULT_
TENANT - Default single-tenant identity written to specs that predate the tenant
axis (W206). Its concrete string is arbitrary — what matters is that a
single-tenant cluster only ever sees this one value, so every per-tenant
isolation primitive collapses to a no-op. See
TenantId::singleton. - FORGE_
MEMORY_ LIMIT_ MB - The cgroup memory ceiling
WorkloadSpec::for_forgesets (MiB). - FORGE_
MEMORY_ REQUEST_ MB - The memory request
WorkloadSpec::for_forgedeclares (MiB). - HOST_
NETWORK_ ANNOTATION - Annotation key requesting a workload share the host network namespace.
See
WorkloadSpec::wants_host_network. - HOST_
NETWORK_ VALUE - Annotation value (for
HOST_NETWORK_ANNOTATION) selecting host networking. Any other value leaves the workload in an isolated netns. - MEMORY_
REQUEST_ ANNOTATION - Annotation key carrying a workload’s memory request in MiB — what a
scheduler must find free on a node — separate from the
memory_mbceiling the backend enforces as a cgroup limit. SeeWorkloadSpec::memory_request_mb. - MICROVM_
EXEC_ VALUE - Annotation value (for
NATIVE_EXEC_ANNOTATION) selecting a microVM: the workload boots in its own KVM guest rather than sharing the host kernel. SeeWorkloadSpec::wants_microvm. - NATIVE_
EXEC_ ANNOTATION - Annotation key selecting which execution substrate kamaji runs a
workload on. Absent (or unrecognised) means a container backend; see
NATIVE_EXEC_VALUEandMICROVM_EXEC_VALUEfor the two opt-outs. - NATIVE_
EXEC_ VALUE - Annotation value (for
NATIVE_EXEC_ANNOTATION) selecting native host execution. Any other value leaves the workload on a container backend. - NESTED_
SANDBOX_ ANNOTATION - Annotation key requesting the capabilities a workload needs to stand up an
unprivileged container sandbox of its own.
See
WorkloadSpec::wants_nested_sandbox. - NESTED_
SANDBOX_ VALUE - Annotation value (for
NESTED_SANDBOX_ANNOTATION) requesting the nested-sandbox grant (CAP_SETUID+CAP_SETGID,no_new_privsoff). Any other value leaves the workload on the baseline sandbox. - PUBLIC_
IP_ TAINT - Taint name (for
REQUIRES_TAINT_ANNOTATION) identifying machines with a publicly-routable IP — the W267 sovereign-ingress placement requirement.MachineConfig.taints(R572-F3) is the matching node-side field andRequiredSpec::matches(R572-F5) is the consumer, so this is a live key on both sides: a node may carry it, and the cloudflared/passway ingress specs require it. - REQUIRES_
TAINT_ ANNOTATION - Annotation key declaring that a workload must land only on a node
carrying a specific taint. See
WorkloadSpec::requires_taint.
Traits§
- Workload
Runtime - Shared interface for deploying and managing
WorkloadSpeccontainers.
Functions§
- intra_
tenant_ address - The name by which a workload is addressed within its own tenant (W206 /
R558-F3), given every
(namespace, identity)pair present in that tenant.