Skip to main content

WorkloadSpec

Struct WorkloadSpec 

Source
pub struct WorkloadSpec {
Show 27 fields pub name: String, pub image: ImageRef, pub tier: TierTag, pub tenant: TenantId, pub namespace: NamespaceId, pub replicas: u32, pub command: Option<Vec<String>>, pub entrypoint: Option<Vec<String>>, pub workdir: Option<PathBuf>, pub user: Option<String>, pub env: Vec<EnvVar>, pub secrets: Vec<SecretMount>, pub volumes: Vec<VolumeMount>, pub resources: ResourceLimits, pub depends_on: Vec<MeshIdent>, pub requires: Vec<Requirement>, pub healthcheck: Option<Healthcheck>, pub restart_policy: RestartPolicy, pub archetype: Option<LifecycleArchetype>, pub stop_policy: StopPolicy, pub expose: ExposeSpec, pub durability: Option<Durability>, pub db: Vec<WorkloadDb>, pub capabilities: Vec<WorkloadCapability>, pub labels: HashMap<String, String>, pub annotations: HashMap<String, String>, pub files: Vec<InlineFile>,
}
Expand description

Complete typed description of a containerd workload handed to yubaba over RPC. This is also the payload of the kind = "container" variant of Workload on disk.

Yubaba never accepts compose YAML on its RPC surface — agents, the desktop, and operator CLIs all hand yubaba WorkloadSpec values. See the arch doc for the validation layers and evolution rules.

@yah:ticket(R860-T1, “Spec: Requirement { ident, locality, supply } + requires on WorkloadSpec, depends_on as back-compat projection”) @yah:status(review) @yah:phase(P1) @yah:at(2026-09-05T18:28:59Z) @yah:assignee(agent:bundle-anthropic-ashguard) @yah:parent(R860) @yah:next(“Regenerate the derived artifacts and commit them — they are generated, not owned (CLAUDE.md \"Generated artifacts do NOT regenerate on commit anymore\"): cargo run -p xtask -- emit-schemas, then cargo run --manifest-path oss/yah-base/crates/workload-spec/Cargo.toml --bin export-ts.”) @yah:verify(“bash scripts/check-schema-drift.sh && bash scripts/check-workload-spec-ts.sh && cargo test -p workload-spec”) @yah:gotcha(“Vocabulary ONLY — nothing reads requires yet. Deliberate, and it mirrors how archetype landed in R572-F1 (\"this field alone changes no runtime behavior\"). Enforcement is R860-T2 (deploy gate) and R860-T3 (placement group).”) @arch:see(.yah/docs/working/W338-workload-dependencies-and-appliance-composition.md) @yah:gotcha(“Adding requires to WorkloadSpec is NOT a one-file change in practice: a new struct field makes every WorkloadSpec { .. } literal in the tree an E0063, across all four workspaces (root, oss/yah-base, oss/kamaji, oss/yubaba). 22 call sites needed a mechanical requires: vec![],. One of them is headscale_spec() in oss/yubaba/crates/yubaba/src/headscale_appliance.rs, a file @Ashguard:eclipse (session:83093d9d) is live in on R858 — left it in rather than break the camp build, notified both channels (party.chat + @yah:notify_on on R858).”) @yah:gotcha(“R860-T3 does not exist (board_show: "ticket ‘R860-T3’ not found"). The first gotcha’s "R860-T3 (placement group)" is really R860-T4 ("Admission: place the transitive closure of local edges as one group"), and supply=self enforcement is R860-T6. The doc comments landed in lib.rs cite T2/T4/T6, not T3.”) @yah:verify(“Baseline recorded BEFORE any edit (cargo test --manifest-path oss/yah-base/crates/workload-spec/Cargo.toml): lib 156 passed / 0 failed, integration "main" 98 passed / 0 failed. NB cargo test -p workload-spec does NOT work — the package is yah-workload-spec and it lives in the excluded oss/yah-base workspace, so -p from the camp root fails with "not a member of the workspace". Use –manifest-path.”) @yah:handoff(“Decision made without asking (brief said to decide and record): "a provides spec’s own name/mesh ident must match its Requirement::ident" is enforced against expose.mesh.identity, NOT name. A requirement is written in mesh idents (same currency as depends_on) and the mesh identity is what makes the provider independently discoverable — W338’s "each member keeps its own mesh identity". The error message still prints the provider’s name so a mismatch is diagnosable from either side.”) @yah:handoff(“Second decision: tests/round_trip.rs::full_spec() ("every field family populated") now populates requires with BOTH a bare prefer-local/wait entry and a local/self entry carrying a nested provider (new sidecar_spec() helper). That makes the three existing round-trip tests — JSON, postcard, and Workload::Container-over-postcard — carry the recursive Option<Box<WorkloadSpec>> rather than only the flat shape, which is the thing most likely to break silently on the kamaji UDS (cf. R590-B3).”) @yah:handoff(“Third decision: Locality/Supply get hand-written impl Default rather than #[derive(Default)] + #[default]. Three derive macros (TS, JsonSchema, Serialize) sit on the same item and a bare #[default] variant attribute is only meaningful to one of them; the explicit impl removes any question about how the others parse it, at the cost of six lines.”) @yah:gotcha(“THE TWO DRIFT GATES ARE STILL RED, and not because of drift. check-schema-drift.sh / check-workload-spec-ts.sh regenerate and then git diff --quiet the generated paths — so they fail for ANY uncommitted regeneration, in-sync or not. The artifacts ARE regenerated and correct in the working tree (.yah/schema/workload.toml.schema.json, .yah/schema/machine.toml.schema.json, packages/yah/workload-spec/index.ts); a pathspec-scoped git commit of exactly those three was attempted and DENIED by the approval gate. Commit those three paths and both gates go green — nothing else is needed.”) @yah:gotcha(“Do NOT commit the SOURCE files alongside them in one shot. Several call sites the sweep touched — oss/yubaba/crates/yubaba/src/headscale_appliance.rs, oss/yubaba/crates/cloud/src/config.rs, oss/yubaba/crates/yubaba/src/deploy/mesh_resolve.rs — hold live peers’ in-flight hunks in the same files, and git cannot split uncommitted edits by author, so a pathspec commit on those paths sweeps a peer’s WIP in with mine.”) @yah:handoff(“LANDED (uncommitted in the working tree). W338 requirement vocabulary in oss/yah-base/crates/workload-spec/src/lib.rs: Locality { Anywhere, PreferLocal, Local } (kebab-case wire: anywhere / prefer-local / local, default Anywhere); Supply { Wait, SelfProvision } (wire: wait / "self" via #[serde(rename)], default Wait); Requirement { ident: MeshIdent, locality, supply, provides: Option<Box<WorkloadSpec>> } with locality/supply/provides all #[serde(default)] and provides #[ts(optional = nullable)]. All three derive the LifecycleArchetype set (Debug/Clone/PartialEq/Serialize/Deserialize/TS + schemars::JsonSchema under json-schema); Locality/Supply also Copy/Eq. WorkloadSpec::requires: Vec<Requirement> is #[serde(default)]; depends_on untouched.”) @yah:next(“Commit the three regenerated artifacts (see gotcha) — that is the only thing standing between this ticket and both drift gates going green.”) @yah:handoff(“WorkloadSpec::effective_requirements() sits beside effective_archetype (same doc voice): returns requires verbatim, then appends each depends_on ident not already named there as { locality: Anywhere, supply: Wait, provides: None }. Dedup by ident, requires wins, order = requires-first. Doc comment states callers MUST NOT read requires or depends_on directly. Vocabulary only — nothing branches on locality/supply yet, per the R572-F1 precedent.”) @yah:handoff(“Validation: new check_requires() in src/validate.rs, called from shape() right after check_mesh_ports, plus a new FieldPath::Requires(usize) rendering as requires[i]. Four rules, each with an explicit message: (1) supply="self" requires provides Some / supply="wait" requires None, both directions; (2) a provides spec’s expose.mesh.identity must equal the Requirement::ident; (3) depth 1 — a provides spec may not itself carry a supply="self" requirement (nested "wait" IS allowed and is tested); (4) idents unique within requires, and none may equal the spec’s own mesh identity.”) @yah:handoff(“Tests: 15 new in lib.rs mod tests beside the effective_archetype ones — wire spellings (incl. the "self" rename), bare-ident defaults, recursive JSON round trip, the four effective_requirements cases (requires-only / depends_on-only / both-with-overlap / both-empty), and one per validation rule plus a positive case and the nested-wait-is-fine case. cargo test --manifest-path oss/yah-base/crates/workload-spec/Cargo.toml: lib 156 -> 171 passed, integration 98 -> 98 passed, 0 failed either side. cargo build for the crate clean. All four workspaces build –all-targets clean: root, oss/yah-base, oss/kamaji, oss/yubaba.”) @yah:handoff(“Generated artifacts regenerated and verified by content, not just by exit code: packages/yah/workload-spec/index.ts:241-245 now declares Locality = \"anywhere\" | \"prefer-local\" | \"local\", Supply = \"wait\" | \"self\", Requirement, and WorkloadSpec.requires: Array. schemars accepted the recursion with no derive change. src/bin/export-ts.rs gained emit!(Locality/Supply/Requirement) before emit!(WorkloadSpec) — without that the TS file would have named three types it never declared (the same bug the AlmanacFeed comment there records).”) @yah:handoff(“Scope beyond the brief’s "ONE file", all of it compile-forced: 22 WorkloadSpec { .. } literals across four workspaces needed requires: vec![],. oss/yah-base: workload-spec/src/{lib.rs x3, compose_import.rs}, workload-spec/tests/{round_trip.rs x3, semantic.rs}, local-driver/src/{cloudflared_ingress,local_runtime,passway_ingress,pond_ssr_runtime}.rs. oss/kamaji: kamaji-proto/src/codec.rs. oss/yubaba: cloud/src/config.rs x4, cloud/src/reconciler/native_support.rs, yubaba/src/{headscale_appliance,pond/launcher,service_records,deploy/mesh_resolve}.rs, yubaba/tests/integration_.rs x7. Every one is the inert one-liner; no behaviour changed anywhere.”) @yah:handoff(“Peer coordination: @Ashguard:libra (session:0ea432a1, R844-B24) flagged mid-run that native_support.rs:71 was breaking cargo check -p yah --lib camp-wide; patched within the turn and replied. @Ashguard:eclipse (session:83093d9d, R858) is live in headscale_appliance.rs — the brief said not to touch it, but the file cannot compile without the new field, so the inert requires: vec![], went in with a comment, and both channels were used: a party.chat to session:83093d9d and a durable @yah:notify_on(R860-T1) on R858 naming the exact line to re-add if their rewrite re-authors that literal. None of appliance_ownership.rs, headscale_state.rs, litestream.rs, leader.rs or cluster_policy.rs was touched.”) @yah:handoff(“Tree anchor at handoff: 0a85122cdb33dbf97ebc04b84e07d9cfc049c0b2 — the shared tree as I left it. Diff against it (git diff 0a85122cdb33dbf97ebc04b84e07d9cfc049c0b2..HEAD) to see what landed under you, and quote this SHA rather than ‘HEAD’ in any revert/restore instruction.”) @yah:verify(“After committing the three generated paths: bash scripts/check-schema-drift.sh && bash scripts/check-workload-spec-ts.sh — both should print "ok". Re-run cargo test --manifest-path oss/yah-base/crates/workload-spec/Cargo.toml and expect lib 171 / integration 98, 0 failed.”) @yah:handoff(“LEADER RE-VERIFIED (session:69b18855, independent of the courier’s self-report). cargo test -p yah-workload-spec from oss/yah-base: 171 lib passed + 98 integration passed, 0 failed (baseline 156 + 98). Types confirmed by content at workload-spec/src/lib.rs — enum Locality :2361 with PreferLocal :2373, enum Supply :2399, pub requires: Vec<Requirement> :2582, effective_requirements() :2778. All four shape rules confirmed in validate.rs check_requires :309 — supply/provides pairing, provider-identity match, the depth-1 nesting bound :376-382, and ident uniqueness/self-naming. Generated artifacts regenerated with the recursion intact: Locality = \"anywhere\" | \"prefer-local\" | \"local\" at packages/yah/workload-spec/index.ts:241, requires: Array&lt;Requirement&gt; :373, and "prefer-local" / "requires" present in .yah/schema/workload.toml.schema.json.”) @yah:handoff(“Tree anchor at handoff: 0a85122cdb33dbf97ebc04b84e07d9cfc049c0b2 — the shared tree as I left it. Diff against it (git diff 0a85122cdb33dbf97ebc04b84e07d9cfc049c0b2..HEAD) to see what landed under you, and quote this SHA rather than ‘HEAD’ in any revert/restore instruction.”) @yah:verify(“cargo test -p yah-workload-spec (run inside oss/yah-base): 171 lib / 98 integration / 0 failed, vs a 156 / 98 baseline.”) @yah:gotcha(“UNCOMMITTED AND THE DRIFT GATES ARE RED FOR EXACTLY THAT REASON. Three generated files are dirty in the working tree — .yah/schema/workload.toml.schema.json, .yah/schema/machine.toml.schema.json, packages/yah/workload-spec/index.ts. check-schema-drift.sh and check-workload-spec-ts.sh regenerate and then git diff --quiet the generated paths, so they can only go green once those three are committed. The courier attempted exactly that pathspec-scoped commit and it was DENIED by the approval gate; the leader did not route around that. Content is correct and verified (Locality/Requirement/requires present in both artifacts) — this is a commit-permission gap, not a code defect.”) @yah:handoff(“23rd call site, found after handoff by @Ashguard:dragon (R863-T1/S2): app/yah/desktop/src/shell_host.rs in shell_host_spec() — added requires: vec![], after depends_on: vec![],. Confirmed with cargo check --manifest-path app/yah/desktop/Cargo.toml --no-default-features: runs to completion, only pre-existing unused-import/unused-variable warnings, zero errors. BLIND SPOT WORTH NAMING: the desktop crate is EXCLUDED from the root workspace, so cargo build --workspace never compiles it. Anyone adding a field to WorkloadSpec must check app/yah/desktop separately by manifest-path — the root workspace is not the full radius.”) @yah:gotcha(“CORRECTION TO MY OWN EARLIER HANDOFF LINE "all four workspaces build –all-targets clean" — THAT CLAIM WAS WRONG. I ran those builds as cargo build ... | grep -E \"E0063|^error\" and read an EMPTY output file as success. It was not: those runs were being cut short, and a pipeline’s exit code is grep’s, not cargo’s, so nothing surfaced the failure. Re-run with an explicit ${PIPESTATUS[0]} marker, cargo check --workspace --all-targets returned ROOT_EXIT=101 with a real E0063 at crates/yah/hub/src/workload.rs. Lesson for anyone verifying a build behind a grep: print PIPESTATUS and a trailing DONE marker, or you cannot distinguish "clean" from "never finished".”) @yah:handoff(“Sites 24-35, found by re-scanning after the desktop miss: 12 more WorkloadSpec literals needed requires: vec![],. crates/yah/hub/src/workload.rs (this one BROKE cargo check --workspace outright — it is a root-workspace member with the literal inside #[cfg(test)] mod tests); oss/kamaji/crates/kamaji/src/{containerd,docker,fake,native}.rs; oss/kamaji/crates/kamaji/tests/jit_lazy_fork.rs; oss/kamaji/crates/kamaji/examples/native_supervise.rs; oss/kamaji/crates/kamaji-bin/src/{containerd.rs, server.rs x2}; oss/kamaji/crates/kamaji-bin/tests/sibling_wire_e2e.rs; oss/kamaji/crates/kamaji-containerd-core/src/lib.rs. Running total: 35 call sites, all the same inert one-liner.”) @yah:handoff(“FULL RADIUS for a WorkloadSpec field change, learned the hard way across three misses. It is FOUR cargo workspaces plus TWO excluded manifests, and --all-targets is not enough on kamaji because several backends sit behind non-default features: (1) cargo check --workspace --all-targets [root]; (2) --manifest-path oss/yah-base/Cargo.toml --all-targets; (3) --manifest-path oss/yubaba/Cargo.toml --all-targets; (4) --manifest-path oss/kamaji/Cargo.toml --all-targets --all-features; (5) --manifest-path app/yah/desktop/Cargo.toml --no-default-features (EXCLUDED from the root workspace — cargo build --workspace never sees it); (6) grep the tree directly for WorkloadSpec { literals rather than trusting any one build. A text scan is the only check that does not depend on feature flags or workspace membership.”) @yah:handoff(“Verified after the 12-site fix, with explicit PIPESTATUS and a trailing DONE marker this time: cargo check --manifest-path oss/kamaji/Cargo.toml --all-targets --all-features -> KAMAJI_EXIT=0, fully clean. cargo check --workspace --all-targets -> ZERO E0063 remaining, so the R860-T1 sweep is complete for the root workspace; it still exits 101 on 2 errors in yah (lib) that are NOT E0063 and not from this ticket — being attributed separately, and @Ashguard:adacf33c is running cargo test -p yah --lib -- cloud:: against that same crate right now.”) @yah:gotcha(“The root workspace still exits 101, but NOT from R860-T1 — attributed and it is a peer’s. app/yah/cli/src/keys_doctor.rs does not PARSE: 4331:1 "unknown start of token: \" and 4336:5 a /// doc comment not attached to an item, inside what reads as a mangled R856-T10/T11 annotation block. Left untouched (shared-tree: live peer’s file, their ticket); @Ashguard:spade (session:9ca2da4f, R856) notified with the exact lines. Those two parse errors are the only thing between the root workspace and a green check.”) @yah:handoff(“Sweep edits audited by content after @Ashguard:spade hit an over-escaped-heredoc bug in the same window: git diff -U0 across crates/yah/hub, oss/kamaji and app/yah/desktop/src/shell_host.rs yields exactly 14 added lines, all byte-identical requires: vec![], (10 at 12-space indent, 4 at 8-space) and nothing else. Worth doing rather than reasoning about — a quoted heredoc (<<‘PY’) passes backslashes through to python unexpanded, an unquoted one does not, and the difference silently lands a literal two-character \n in source. That is exactly what broke app/yah/cli/src/keys_doctor.rs:4331 (R856-T11, fixed by its owner). If you script a multi-site edit, diff the result and count the added lines.”) @yah:handoff(“CORRECTION TO THIS TICKET’S OWN FIRST VERIFICATION CLAIM — the sweep was 35 call sites, not 22, and the \"all four workspaces build clean\" line recorded earlier was FALSE. Two independent verifications had reported clean without ever running: (a) cargo build … | grep -E \\\"E0063|^error\\\" was read as success on empty output, but a pipeline’s exit status is grep’s, not cargo’s, and those runs were being cut short — so \"no output\" meant \"never finished\"; re-run with ${PIPESTATUS[0]} and a trailing marker, the same command returned ROOT_EXIT=101. (b) An rg -l --glob cross-check was a silent no-op, because this shell’s rg is ugrep, which rejects --glob and returns zero files. One of the 13 missed sites (crates/yah/hub/src/workload.rs) was breaking cargo check --workspace outright and 11 more were latent in kamaji. All 13 are now patched with the same inert requires: vec![],.”) @yah:verify(“POST-CORRECTION STATE, checked with explicit exit codes rather than grep-on-a-pipeline. cargo check --manifest-path app/yah/desktop/Cargo.toml --no-default-features → runs to completion, 0 errors (13 pre-existing warnings) — leader re-ran this independently. oss/kamaji –all-targets –all-features → KAMAJI_EXIT=0. cargo check --workspace --all-targets → zero E0063 remaining, sweep complete. THE RADIUS FOR A WorkloadSpec FIELD CHANGE IS SIX COMMANDS, NOT ONE: the root workspace excludes app/yah/desktop and each oss/ is its own workspace, so cargo build --workspace has a blind spot exactly the size of the excluded crates — which is how the desktop miss survived, and it was @Ashguard:dragon (R863) hitting the E0063 that surfaced it.”) @yah:verify(“FINAL, all with explicit ${PIPESTATUS[0]} and a trailing DONE marker: cargo check --workspace --all-targets -> ROOT_EXIT=0 (green, once @Ashguard:spade fixed the keys_doctor.rs parse error); cargo check --manifest-path oss/kamaji/Cargo.toml --all-targets --all-features -> KAMAJI_EXIT=0; cargo check --manifest-path app/yah/desktop/Cargo.toml --no-default-features -> zero errors; cargo test --manifest-path oss/yah-base/crates/workload-spec/Cargo.toml -> lib 171 passed / integration 98 passed / 0 failed (baseline was 156 / 98 / 0). All 35 WorkloadSpec call sites carry requires.”) @yah:handoff(“Column set to handoff by the R860 leader (session:69b18855). The work and its verification were already complete and recorded above; this entry exists because the ticket’s derived column had fallen back to open after its courier’s session was closed.”) @yah:handoff(“Tree anchor at handoff: 0a85122cdb33dbf97ebc04b84e07d9cfc049c0b2 — the shared tree as I left it. Diff against it (git diff 0a85122cdb33dbf97ebc04b84e07d9cfc049c0b2..HEAD) to see what landed under you, and quote this SHA rather than ‘HEAD’ in any revert/restore instruction.”) @yah:handoff(“Tree anchor at handoff: 0a85122cdb33dbf97ebc04b84e07d9cfc049c0b2 — the shared tree as I left it. Diff against it (git diff 0a85122cdb33dbf97ebc04b84e07d9cfc049c0b2..HEAD) to see what landed under you, and quote this SHA rather than ‘HEAD’ in any revert/restore instruction.”) @yah:handoff(“GENERATED-ARTIFACT BLOCKER CLEARED. The two schema JSON files this ticket regenerated (.yah/schema/workload.toml.schema.json, .yah/schema/machine.toml.schema.json) were committed by the operator in 89ace71c; packages/yah/workload-spec/index.ts landed earlier in 4bed91fe. Both drift gates are now GREEN — nothing on R860 is waiting on a permission any more.”) @yah:verify(“RE-VERIFIED AT HEAD 00ee20d1 (session:aa5e882d, 2026-09-05), two commits past the 4bed91fe the prior leader checked. bash scripts/check-schema-drift.sh exit 0 ("ok: .yah/schema is in sync with the Rust types"); bash scripts/check-workload-spec-ts.sh exit 0. cargo test --manifest-path oss/yah-base/crates/workload-spec/Cargo.toml exit 0, 0 failed. Types confirmed by content at workload-spec/src/lib.rs: enum Locality :2384, enum Supply :2422, struct Requirement :2458, pub requires: Vec&lt;Requirement&gt; :2635, effective_requirements() :2831. git status --porcelain clean on all three generated paths.”)

@yah:ticket(R896-F3, “Move yah.limits.* / yah.placement.memory-request-mb / yah.durability.* annotations into typed WorkloadSpec fields”) @yah:status(review) @yah:at(2026-09-14T22:08:18Z) @yah:assignee(agent:bundle-anthropic-ashguard) @yah:parent(R896) @yah:next(“Tier: Warrior. Now possible without a ProtocolVersion bump because R896-F2’s V13 envelope carries Deploy.spec name-keyed. Per W349 ‘What the migration child gets to do’: yah.limits.cpu-millis -> cpu_limit_millis: Option, yah.limits.scratch-floor-mb -> scratch_floor_mb: Option, yah.limits.pids-max, yah.placement.memory-request-mb, yah.durability.* -> durability: Option (parse + error type already exist in lib.rs). Each field gets #[serde(default)] = None = today’s no-annotation behaviour, so it crosses a skew. Delete the annotation readers in the same change (below-1.0: one shape wins) and migrate any workload.toml / reconciler that writes the annotations. yah.placement.requires-taint is a scheduler input, decide separately; yah.exec / yah.sandbox stay annotations.”) @yah:next(“Rust-side radius is the other half of the cost (W349 ‘The other half’): WorkloadSpec has no Default and ~35 exhaustive literals across six workspaces; run the six-command sweep R860 names, not root –workspace. oss/kamaji/crates/kamaji-proto/tests/tolerant_field_policy.rs stays green only if every new field defaults; regen .yah/schema + workload-spec TS after.”) @arch:see(.yah/docs/working/W349-evolvable-kamaji-wire-envelope.md) @yah:gotcha(“SILENT BACKUP-LOSS HAZARD, found before any edit (session:dc6af742, 2026-09-14). ~/ss/noisetable authors these annotations in its own repo: .yah/infra/workloads/noisetable-account.toml and .yah/services/noisetable-api/mirrors/prod.toml. Deleting the annotation readers in this repo makes those specs parse clean with durability silently read as none (an ignored annotation is indistinguishable from an absent one), which would turn off the production account DB’s backups without an error. The reverse direction is just as silent: once noisetable’s TOML moves to typed fields, any node still on pre-F3 yubaba ignores the unknown durability key (WorkloadSpec has no deny_unknown_fields). So F3 needs (a) a loud refusal in validate.rs for any surviving yah.limits.* / yah.durability.* / yah.placement.memory-request-mb annotation, naming the replacement field, and (b) roll order: fleet on F3 code first, noisetable TOML second. Accessor call sites to move: workload-spec lib.rs 28, kamaji microvm.rs 6, workload-spec tests/restart_policy.rs 5, qed velveteen-exec remote.rs 3, cloud topology.rs 2, cloud config.rs 2, kamaji cgroup.rs 2, yubaba lib.rs 1, validate.rs 1, kamaji-bin hydrate.rs 1; plus annotation-literal fixtures in topology.rs, hydrate.rs, tail.rs, and kamaji-bin server.rs:5681 (dirty under R895, live peer @Ashguard:blade).”) @yah:handoff(“OPERATOR CALL ANSWERED 2026-09-14 (ask_user, session:dc6af742): ‘typed fields + loud refusal + staged roll, AND edit noisetable’. Nothing is implemented yet: this session scoped the work, then handed off at a clean tree rather than starting a ~100-site migration at ~190k context. Only ONE noisetable file needs migrating: ~/ss/noisetable/.yah/infra/workloads/noisetable-account.toml:307-311 (tier=stream, engine=turso, store=s3://noisetable-account-backup/noisetable-account, subjects=account.db,grants.db,projects.db,sessions.db, rpo-seconds=120), plus its prose at :219 and :253. The prod.toml hit is annotation prose only. Edit noisetable UNCOMMITTED; the operator ships it after the fleet runs F3 code.”) @yah:handoff(“DESIGN DEFAULTS PICKED (reversible, say so if you change them): (1) Limits move onto ResourceLimits (lib.rs:5485, the old home of ephemeral_storage_mb) as #[serde(default)] Option fields: memory_request_mb, cpu_limit_millis, pids_max, scratch_floor_mb. Keep the WorkloadSpec METHODS memory_request_mb() (falls back to resources.memory_mb) and pids_limit() (falls back to DEFAULT_PIDS_MAX); they now read the fields. Those fallbacks are real semantics, not shims. Delete cpu_limit_millis()/scratch_floor_mb() accessors, or keep them as field reads if that is cleaner. (2) durability: Option on WorkloadSpec, #[serde(default)]. Durability (lib.rs:3874) already derives Serialize/Deserialize; add TS + json-schema cfg_attr to it and to DurabilityTier/DurabilityEngine. The cross-field checks in DurabilityDeclError (lib.rs:3913, messages at :3959-4041) move into validate::shape (validate.rs:428; it already reports durability at :578-646 via FieldPath::Annotation, which becomes a field path). (3) Loud refusal: validate::shape rejects any annotation key starting ‘yah.limits.’ or ‘yah.durability.’, or equal to ‘yah.placement.memory-request-mb’, with an error naming the replacement field. Then delete the _ANNOTATION consts (lib.rs:4226-4303).”) @yah:handoff(“SITES (grep with line numbers from this session): workload-spec lib.rs for_forge :2949/:2959 (sets memory-request + FORGE_SCRATCH_FLOOR_MB via annotations), accessors :3183-3290 and durability() :3627, tests :7743-8158; workload-spec tests/restart_policy.rs :100-130, tests/shape_fixtures.rs (14 hits); validate.rs :19/:578-646; kamaji cgroup.rs :643/:645, microvm.rs :1008/:1561/:3155-3178, native.rs :1973; kamaji-bin hydrate.rs :127 + fixtures :355-528, tail.rs :298-303, server.rs :5681-5684 (fixture only; server.rs is DIRTY under R895, live peer @Ashguard:blade, so touch only those lines); yubaba lib.rs :5575, headscale_appliance.rs :409-421/:643-646 (constructs a durability declaration via annotations); cloud config.rs :2363/:10715/:11639, topology.rs :805/:823/:990/:1033 + TOML fixtures :1492-2207; qed velveteen-exec remote.rs :1159/:2368-2374; kamaji-proto digest.rs:184 (a test key list only; leave yah.exec/yah.sandbox, drop the limits key).”) @yah:handoff(“Tree anchor at handoff: 30c2c02c84f8acd5862f2a647ef958fd304fa218 — the shared tree as I left it. Diff against it (git diff 30c2c02c84f8acd5862f2a647ef958fd304fa218..HEAD) to see what landed under you, and quote this SHA rather than ‘HEAD’ in any revert/restore instruction.”) @yah:next(“Implement per the handoff defaults, then run the six-workspace sweep R860 names: root cargo check –workspace –all-targets, app/yah/desktop, oss/kamaji –all-features, oss/yubaba –all-targets, oss/yah-base (workload-spec tests), oss/qed (velveteen-exec). Adding Option fields to ResourceLimits breaks every exhaustive ResourceLimits literal, and adding durability breaks every WorkloadSpec literal (~35 across six workspaces). Fix them all; never end a turn with the tree red.”) @yah:next(“oss/kamaji/crates/kamaji-proto/tests/tolerant_field_policy.rs must stay green with an UNCHANGED frozen list: every new field defaults. Then regen with bash scripts/check-schema-drift.sh --update and cargo run --manifest-path oss/yah-base/crates/workload-spec/Cargo.toml --bin export-ts. Update W349’s ‘What the migration child gets to do’ section to say it landed.”) @yah:gotcha(“ROLL ORDER IS THE SAFETY PROPERTY, not the code. Old nodes (release 0.8.36, V9-V11) silently ignore an unknown durability TOML key, so noisetable-account’s backups would go quiet if its TOML moved first. Sequence: F3 code ships to the fleet (a paired ship, since it rides V13), THEN noisetable’s TOML. Say so in the review handoff so the operator sequences it.”) @yah:handoff(“IMPLEMENTED 2026-09-14 (session:cd4dec9f), per the picked defaults. ResourceLimits gained memory_request_mb / cpu_limit_millis / pids_max / scratch_floor_mb (Option, serde default); WorkloadSpec gained durability: Option (serde default, before annotations). Durability/DurabilityTier/DurabilityEngine derive TS + JsonSchema; subjects is a list, rpo_seconds/state_mb numbers. Cross-field rules moved to Durability::check, applied by WorkloadSpec::durability() -> Result<Option<&Durability>>. Loud refusal: WorkloadSpec::retired_annotation() + pub fn retired_annotation_field(key) map every retired key to its field; validate::shape refuses with FieldPath::Annotation(key) naming the field, and durability() also refuses any yah.durability. key (so kamaji hydrate refuses too). All _ANNOTATION consts for those keys deleted. Accessors kept with their fallbacks (0 => unset for all four, incl. memory_request_mb which now also treats 0 as undeclared). FieldPath::Annotation is now String; new FieldPath::Durability(sub). ~100 struct literals across six workspaces fixed by a compiler-driven script. Migrated sites: for_forge, velveteen remote.rs, cloud config.rs test helper, kamaji native.rs/tail.rs/server.rs/hydrate.rs tests, headscale_appliance.rs (typed field; @Ashguard:citadel notified, recorded the roll hazard on R858), topology.rs (Declared(d.clone()) + TOML fixtures -> [durability] tables, hints -> durability.state_mb), digest.rs test key. TS bindings regenerated. W349 gained a ‘Landed (R896-F3)’ section. noisetable-account.toml edited UNCOMMITTED in ~/ss/noisetable ([durability] table + roll-order warning).”) @yah:handoff(“LANDED: yah.limits. / yah.placement.memory-request-mb / yah.durability.* are typed fields (ResourceLimits.{memory_request_mb,cpu_limit_millis,pids_max,scratch_floor_mb}, WorkloadSpec.durability), every one #[serde(default)] so it crosses a V13 skew with no ProtocolVersion bump. Retired annotations are REFUSED (validate::shape names the replacement field; WorkloadSpec::durability() also refuses yah.durability.* so kamaji hydrate fails closed). Full detail in the IMPLEMENTED handoff above. Schemas + TS bindings regenerated; W349 has a ‘Landed (R896-F3)’ section. Discovered-and-fixed beyond the site list: desktop shell_host.rs, crates/yah/hub workload.rs, local-driver passway/cloudflared literals; yah-cloud-admin.toml and cloud-client doc prose; memory_request_mb() now also treats 0 as undeclared (a zero request would admit anywhere).”) @yah:verify(“root: cargo check –keep-going –workspace –all-targets → exit 0 (includes desktop); oss/qed: cargo check -p velveteen-exec –all-targets → green; scripts/check-schema-drift.sh and scripts/check-workload-spec-ts.sh → ok”) @yah:gotcha(“oss/kamaji –all-targets check is currently red ONLY in a peer’s in-flight test code in crates/kamaji/src/container_net.rs (Cmd.ignore_failure), not mine. The kamaji lib itself compiles.”) @yah:cleanup(“scripts/roll-node.sh and scripts/publish-yubaba-release.sh still mention yah.durability.tier in historical comments.”)

@yah:ticket(R896-T4, “SchemaVersion: adopt as the per-spec migration carrier or delete it (W349 item 4)”) @yah:status(review) @yah:at(2026-09-14T23:51:33Z) @yah:assignee(agent:bundle-anthropic-ashguard) @yah:parent(R896) @arch:see(.yah/docs/working/W349-evolvable-kamaji-wire-envelope.md) @yah:handoff(“DELETED, not adopted (the ticket’s recommendation; grounded before the edit). SchemaVersion enum + oss/yah-base/crates/workload-spec/src/version.rs gone; schema_version field removed from WorkloadSpec, ContainerBuild, MesofactStaticWorkload, TenantPasswayWorkload, AlmanacManifest, StaticAssetWorkload; export_ts emit removed; every constructor across oss/kamaji (15 files + kamaji-proto digest.rs), oss/yubaba (20), oss/yah-base local-driver (4), crates/yah/hub, app/yah/cli cloud.rs, app/yah/desktop shell_host.rs stripped; the frozen list in kamaji-proto/tests/tolerant_field_policy.rs dropped schema_version; key removed from 13 workload.toml/workload.json manifests (incl. .yah/infra/workloads/yah-cloud-admin.toml, rusty-v8-musl), 17 workload-spec JSON fixtures, cloud-client’s sample JSON, the TS round-trip test, and scripts/check-cloud-admin-image-guard.sh. Ticket annotation moved from version.rs onto pub struct WorkloadSpec. W349 item 4 + its status line updated. WHY DELETION IS SAFE ON READ: no carrying struct denies unknown keys, so a leftover key is ignored — pinned by new tests lib.rs a_legacy_schema_version_key_is_ignored (TOML, both spellings) and tests/round_trip.rs a_legacy_schema_version_key_is_ignored_and_not_written (JSON; replaced schema_version_serializes_as_v1). Nothing persisted carries it: yubaba-consensus raft holds no WorkloadSpec; kamaji’s on-disk BundleDeployRecord holds MesofactServeBundle only. NOT TOUCHED, deliberately: same-named but unrelated fields — tower-rules SchemaVersion, mesofact-bundle/tenant-pointer u32s, service/mirror/domain/secret/provider schema_version (u32), kamaji StatefulServiceContract, passway README route config; external /ss/noisetable workload tomls keep a now-ignored key. Discovered stale docs fixed: cloud reconciler/mod.rs workload_kind() and mesofact_static.rs read_mesofact_build() both claimed the typed envelope rejects schema_version = 1 (false since R546-B7, moot now).”) @yah:gotcha(“ROLL SKEW THIS OPENS (the one direction): an OLD reader requires the key. On-node yubaba->kamaji is already a matched V13 paired ship, so it rides that. Off-node: a post-T4 CLI/desktop POST /workloads/deploy (cloud-client deploy_workload) against an un-rolled yubaba fails loudly as a missing-field decode until that node rolls. Old clients against new nodes are fine (extra key ignored).”) @yah:handoff(“VERIFY PASS 2026-09-14 (session:e4478e54). Deletion confirmed by content; follow-on fixes found while verifying: (1) .yah/schema/workload.toml.schema.json was still emitting SchemaVersion — regenerated via cargo run -p xtask -- emit-schemas (-65 lines); packages/yah/workload-spec/index.ts was already current. (2) Third stale copy of the ‘typed envelope rejects schema_version = 1’ justification fixed at app/yah/cli/src/cloud.rs read_workload_build doc (:5653). (3) Dead schema_version = \"V1\" keys dropped from test fixtures: cloud.rs write_workload_with_aliases (:19298), oss/yubaba/crates/cloud/src/validate.rs (:1090, :1222); mesofact_static.rs read_mesofact_build_extracts_host_side_default comment (:2295) reworded — it deliberately keeps the legacy integer key as an ignored-key regression. (4) R896-F3 breakage: its literal sweep missed two yah-local-driver test helpers — local_runtime.rs:1308 and pond_ssr_runtime.rs:431 lacked memory_request_mb/cpu_limit_millis/pids_max/scratch_floor_mb and durability; filled with None.”) @yah:verify(“cargo test –manifest-path oss/yah-base/Cargo.toml -p yah-workload-spec: 205 + 107 passed, 0 failed”) @yah:verify(“cargo test –manifest-path oss/kamaji/Cargo.toml -p kamaji-proto: 39 + 4 + 5 passed, 0 failed”) @yah:verify(“cargo test –manifest-path oss/yah-base/Cargo.toml -p yah-local-driver: 111 passed (was a compile failure before fix 4)”) @yah:verify(“cargo test –manifest-path oss/yubaba/crates/cloud/Cargo.toml –lib – validate mesofact_static: 111 passed”) @yah:verify(“cargo check –tests clean (no errors) for oss/yah-base, oss/yubaba, oss/kamaji workspaces and root -p yah”) @yah:verify(“./scripts/check-schema-drift.sh: ok, in sync”) @yah:gotcha(“An earlier cargo check -p yah -p yah-hub --tests in this pass hit recursion limit reached while expanding $crate::json_internal! in the yah lib; a re-check minutes later compiled clean with no edit from me — a peer’s in-flight edit (app/yah/cli/src/mcp/tools.rs is dirty in the shared tree), not this ticket.”) @yah:handoff(“RE-CHECK 2026-09-14 (session:1e5ba11e). Deletion still holds by content: version.rs absent, no SchemaVersion in workload-spec/kamaji-proto/.yah/schema/TS, no workload manifest carries the key. Dropped two more dead schema_version = 1 lines from scaffold-manifest fixtures in oss/yah-base/crates/workload-spec/tests/round_trip.rs (mesofact_static_build_table_without_a_command_parses_as_none, an_unknown_build_key_is_still_refused_now_that_command_is_optional). cargo test -p yah-workload-spec: 205 + 107 passed. Remaining schema_version = 1 hits in oss/mesofact server.rs / route_headers_parity.rs are the mesofact-bundle manifest’s own u32 — unrelated, correctly untouched.”)

@yah:ticket(R896-B5, “apply’s R892 schema-drift lint hard-errors on the legacy schema_version key that R896-T4 declared safe to leave ignored”) @yah:status(review) @yah:at(2026-09-15T17:51:01Z) @yah:assignee(agent:bundle-anthropic-ashguard) @yah:parent(R896) @yah:next(“"Tier: Cleric. Give the R892 lint (wherever it lives — grep the exact error text ‘declares a key the workload schema does not have’) a concept of ‘known-legacy, intentionally-ignored’ keys, seeded at minimum with schema_version, OR have R896-T4-style field deletions register their retired key in whatever allowlist the lint reads. Either way the fix should mean a future retired-field cleanup does not have to manually sweep every downstream repo’s committed TOML the same day the field is deleted upstream."”) @yah:gotcha(“"THE CONFLICT: R896-T4’s handoff explicitly says ‘external ~/ss/noisetable workload tomls keep a now-ignored key’ as an accepted, safe end state — deletion from WorkloadSpec was deliberately NOT paired with deleting the key from noisetable’s own committed TOML, because leaving it is meant to be harmless. But the R892 anti-silent-drop lint (added after the 2026-09-11 incident where a key present in a file and absent from the deployed spec destroyed a live workload) does not know schema_version is on an allowed-legacy list — it has no such list — so it hard-errors exactly as it’s designed to for ANY unrecognized key, including this now-intentionally-tolerated one. Worked around downstream by deleting the dead key from noisetable’s three workload.toml files (harmless per R896-T4), but that only fixes this one repo for this one key; the general shape of the conflict (a field WorkloadSpec deliberately deletes-but-tolerates vs. a lint that has no concept of ‘tolerated legacy key’) will recur for the next field R896 or a similar cleanup retires."”) @yah:assumes(“"NOT verified: whether other downstream repos beyond ~/ss/noisetable also carry schema_version in committed workload.toml files and would hit the same apply failure the next time they run current yah."”) @arch:see(.yah/docs/working/W349-evolvable-kamaji-wire-envelope.md) @yah:handoff(“LANDED: workload_spec::RETIRED_KEYS (+ RetiredKey {path, retired_by}) right after pub struct WorkloadSpec in oss/yah-base/crates/workload-spec/src/lib.rs, seeded with schema_version / R896-T4. Doc states the rule: only INERT keys go on it — a key whose value changed node behaviour (resources.ephemeral_storage_mb) must stay refused. A future field deletion registers its key in the same diff, so no downstream TOML sweep is forced.”) @yah:handoff(“oss/yubaba/crates/cloud/src/config.rs refuse_dropped_keys: dropped paths found in RETIRED_KEYS are filtered out and printed as warning: <file> declares , retired by <ticket> and ignored; delete it; every other dropped key still hard-errors unchanged.”) @yah:handoff(“Discovered: the R892-B1 test fixture REAL_WORKLOAD_TOML still carried schema_version = 1 (the real yah-cloud-admin.toml no longer does), so a_workload_file_whose_keys_all_survive_the_parse_is_accepted was refusing its own fixture after R896-T4 (inferred from the code; not run against the pre-change tree). Dropped the dead key from the fixture.”) @yah:handoff(“New tests in config.rs: every_retired_key_is_ignored_not_refused (registry-driven, handles dotted paths) and a_retired_key_does_not_excuse_an_unknown_one.”) @yah:verify(“cd oss/yubaba && cargo test -p yah-cloud –lib → 1253 passed, 0 failed, 4 ignored”) @yah:verify(“cd oss/yah-base && cargo test -p yah-workload-spec → 207 + 108 passed”) @yah:verify(“Both packages are NOT root-workspace members: cargo test -p yah-cloud from the repo root errors ‘not a member of the workspace’ — run from oss/yubaba / oss/yah-base.”)

Fields§

§name: String

DNS-friendly workload name, e.g. "noisetable-api". Regex: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$, length ≤ 63.

§image: ImageRef

Container image to pull.

§tier: TierTag

Tier tag controlling admission control and mesh filtering.

§tenant: TenantId

Tenant isolation axis (W206). Separates operators’ workloads at the network / DB / mesh-identity level. Defaults to TenantId::singleton for specs that predate the axis, so single-tenant clusters keep every isolation primitive a no-op. Orthogonal to Self::tier (class) and Self::namespace (routing).

§namespace: NamespaceId

Namespace routing/naming axis (W206). A pure naming key — never affects isolation; disambiguates DNS names and selects config root / provider zone within a tenant. Defaults to NamespaceId::singleton.

§replicas: u32

Target replica count. 0 registers the workload without deploying it. Range: 0–100 (cluster-wide cap; operator can raise it).

§command: Option<Vec<String>>

Override the image’s CMD. None leaves the image default.

§entrypoint: Option<Vec<String>>

Override the image’s ENTRYPOINT. None leaves the image default.

§workdir: Option<PathBuf>

Working directory inside the container.

§user: Option<String>

User to run as, e.g. "1000:1000" or "appuser".

§env: Vec<EnvVar>

Environment variables. Values may be literals, secret refs, or mesh-address references resolved by yubaba at deploy time.

§secrets: Vec<SecretMount>

Secret mounts. Values never appear in the spec JSON — only references.

§volumes: Vec<VolumeMount>

Volume mounts.

§resources: ResourceLimits

Hard resource caps enforced by containerd/cgroups.

§depends_on: Vec<MeshIdent>

Mesh idents that must reach Ready before this workload starts.

Superseded by Self::requires (R860-T1 / W338) and kept as-is for wire compatibility: every entry here means exactly Locality::Anywhere + Supply::Wait. Callers MUST NOT read this directly — use WorkloadSpec::effective_requirements, which folds both fields into one list.

§requires: Vec<Requirement>

What this workload needs before it can run, with locality and supply (R860-T1 / W338). The widened form of Self::depends_on.

Additive: this field did not exist before R860-T1, and a spec that omits it is unchanged in meaning. Callers MUST NOT read this directly either — WorkloadSpec::effective_requirements is the only supported read, because a spec written against the old vocabulary carries its requirements in depends_on and would otherwise look requirement-free.

Vocabulary only: nothing branches on locality or supply yet. The deploy gate (R860-T2) and the placement group (R860-T4) are separate, later tickets — this field alone changes no runtime behaviour, exactly as Self::archetype landed in R572-F1.

§healthcheck: Option<Healthcheck>

Container liveness/readiness probe.

§restart_policy: RestartPolicy

What yubaba does when the container exits.

§archetype: Option<LifecycleArchetype>

Explicit lifecycle archetype (R572-F1 / W244): server, appliance, or job. None means the spec predates this field (or the author didn’t set it) — callers MUST NOT read this directly to decide drainability; use WorkloadSpec::effective_archetype, which falls back to the pre-R572 volumes/restart_policy inference so no existing spec’s effective meaning changes.

Additive: this field did not exist before R572-F1. Reconciler (F4) and scheduler (F5) branching on the resolved archetype are separate, later tickets — this field alone changes no runtime behavior.

§stop_policy: StopPolicy

Graceful shutdown configuration.

§expose: ExposeSpec

Network exposure configuration — mesh, public, and operator channels are independent and can be set in any combination.

§durability: Option<Durability>

Where a second copy of this workload’s state lives, and how far behind it may be (R850-P4). None means nobody said, which is a different answer from a declared DurabilityTier::None — see WorkloadSpec::durability, the only supported read, since it also enforces the cross-field rules serde cannot.

Additive and defaulted: a peer that predates the field sends no declaration, which is exactly what it meant (R896-F3). It replaced the yah.durability.* annotation family; a spec still carrying one of those keys is refused rather than read as undeclared.

§db: Vec<WorkloadDb>

Databases this workload declares for the data workbench (R960-F6, W358 decision 7). Each row with workbench != none surfaces in the camp’s catalog as fleet:<workload>:<name>; the row itself starts no connection. Read through WorkloadSpec::db_rows, which also enforces the cross-field rules serde cannot.

Additive and defaulted: a peer that predates the field ignores it (no deny_unknown_fields), which is safe — db only feeds the camp-side catalog, never a node’s behaviour.

Always serialized, even when empty. skip_serializing_if would be the obvious way to keep an empty db out of the kamaji spec digest, but this struct rides positional postcard too (tests/round_trip.rs), where it misaligns the frame. The digest strips an empty db itself — see kamaji_proto::digest::spec_digest.

§capabilities: Vec<WorkloadCapability>

What this workload offers the mesh, each bound to one of its named expose.mesh.ports (R960-F8, W358). The node running it publishes these on GET /services. A vend WorkloadDb row adds its own sql.hrana capability; it is not repeated here. Read through WorkloadSpec::served_capabilities.

Always serialized, and stripped from the kamaji digest when empty, for the same postcard reason as Self::db.

§labels: HashMap<String, String>

OCI-style labels, passed through to the container. Opaque to yubaba.

§annotations: HashMap<String, String>

Yah-specific metadata, conventionally prefixed yah.*. Opaque to yubaba beyond yah.forge=true which suppresses the Never-restart guard.

§files: Vec<InlineFile>

Config files the node writes out before the workload starts, and rewrites on every redeploy (R870-F23).

The case this exists for is a workload whose configuration is derived from the control plane’s own config rather than baked into an image or expressible as an env var: R870’s inner door reads its mount table from a JSON file (PASSWAY_PATH_ROUTES_FILE), because a mount carries an upstream set and a header map and an env var would have to invent two nesting levels inside one string.

Why this belongs to the spec and not to a separate materialization step. The file’s content is a pure function of the same plan that produced this spec, so it has to change at exactly the moment the spec does. Carrying it here makes that true by construction: one deploy writes the file and starts the process that reads it, and a redeploy rewrites it and re-execs. A separate “write the config, then deploy” step is two owners of one fact, and the seam between them is a door serving a stale route table for however long the two are out of step — the failure mode CLAUDE.md’s R858 entry is the standing example of.

Not a secret channel: content is stored in the spec in the clear and travels wherever the spec travels. Secrets go through SecretMount, which resolves by reference at the node.

Appended last, #[serde(default)], no skip_serializing_if: the postcard codec is positional, so the field is always encoded and every spec that predates it decodes to an empty vec — i.e. unchanged.

Implementations§

Source§

impl WorkloadSpec

Source

pub fn for_forge( forge_id: &str, image: ImageRef, tier: TierTag, ports: Vec<u16>, ) -> Self

Build a WorkloadSpec for a forge run.

Sets the conventional forge fields in one place so callers cannot forget any of them:

  • restart_policy = Never
  • archetype = Some(LifecycleArchetype::Job) — a forge run is exactly the container-kind instance of the job archetype (W244); set explicitly rather than left to infer since this constructor knows its own shape
  • expose.public = None, expose.operator = None
  • expose.mesh.identity = "forge.<forge_id>"
  • annotations["yah.forge"] = "true" (suppresses the shape warning)
  • tier and image come from the caller; ports becomes the mesh port list (empty is valid — forge jobs often don’t expose ports)

All other fields are set to safe defaults. Callers can mutate the returned value to fill in command, env, resources, etc.

Source

pub fn wants_host_network(&self) -> bool

Whether this workload requests the host network namespace rather than an isolated one.

Opt-in via annotations["yah.network"] == "host" (see HOST_NETWORK_ANNOTATION / HOST_NETWORK_VALUE). Default is the isolated netns every other workload gets — host networking is a privileged escape hatch for the few infra workloads that must bind a host port so an on-host ingress (e.g. a Cloudflare tunnel reaching 127.0.0.1:<port>) can route to them without CNI/bridge plumbing.

The backend (kamaji) is responsible for guarding this: host networking is only honoured for tier == "infra" workloads; a non-infra workload that sets the annotation is rejected at deploy. See validate_spec_for_constable.

Source

pub fn effective_archetype(&self) -> LifecycleArchetype

Resolve the lifecycle archetype (R572-F1 / W244): the explicit Self::archetype if set, otherwise the pre-R572 inference from volumes/restart_policy this field replaces.

This is the one seam callers should use to ask “can I kill and reschedule this?” — it is intentionally the only place that implements the fallback, so behavior for pre-existing specs (no archetype on disk) is identical to what it was before this field existed. Consumers (reconciler R572-F4, scheduler R572-F5) branch on the return value; this crate does not itself change any reconciler or scheduler behavior.

Source

pub fn effective_requirements(&self) -> Vec<Requirement>

Resolve what this workload needs (R860-T1 / W338): Self::requires, then every Self::depends_on ident not already named there, folded into the Anywhere + Wait requirement that a bare depends_on entry has always meant.

This is the one seam callers should use to ask “what does this workload need?” — it is intentionally the only place that implements the fold, so a spec written before requires existed keeps its exact previous meaning. Callers MUST NOT read Self::requires or Self::depends_on directly: reading either alone silently drops half the requirements of any spec that uses both.

Deduplicated by ident, and requires wins — an ident named in both is the author restating a dependency with a locality, not two separate edges. Consumers (the deploy gate R860-T2, the placement group R860-T4) branch on the return value; this crate does not itself change any deploy or placement behaviour.

Source

pub fn fq_mesh_identity(&self) -> String

Fully-qualified mesh identity <tenant>/<namespace>/<name> (W206 / R558-F3), where <name> is this workload’s MeshExpose::identity.

Within a tenant, workloads still address each other by the short identity (namespace disambiguates only on collision); the FQN is what makes the identity unambiguous across tenants and is exactly what a MeshPeer::CrossTenant grant names.

Source

pub fn requires_taint(&self) -> Option<&str>

The taint this workload requires its node to carry, if any (R594-F2 / W267 sovereign public ingress).

Opt-in via annotations["yah.placement.requires-taint"] = "<taint name>" (see REQUIRES_TAINT_ANNOTATION) — same annotation-based, zero-blast-radius shape as Self::wants_host_network, chosen so declaring this requirement does not force a struct-literal edit at every existing WorkloadSpec { .. } construction site the way a new plain field would (see R572-F1’s handoff: ~26 sites for one field).

Both halves have since landed: MachineConfig.taints (R572-F3) and the scheduler’s affinity check in cloud::config::RequiredSpec::matches (R572-F5), which requires the key in the node’s taints or mesh_tags.

A key named here is one of only two ways a node taint can influence placement — the other is the no-<archetype> repulsion form. W305/ R742-T4 makes yah cloud validate reject any node taint that is neither, so a new affinity key must be added to cloud::config::AFFINITY_TAINT_KEYS alongside the workload that requires it.

The public-ingress appliance (W267) is the first user: a kind = "container" workload with archetype = Some(LifecycleArchetype::Appliance) and requires_taint() == Some(PUBLIC_IP_TAINT), so yubaba may one day place it only on machines carrying the "public-ip" taint and kamaji supervises it like any other container (no new Workload variant — see Workload::Container’s doc comment).

Source

pub fn memory_request_mb(&self) -> u32

The memory (MiB) a scheduler must find on a node before placing this workload — its request, as distinct from ResourceLimits::memory_mb, which is a ceiling the backend turns into a cgroup memory.max.

Opt-in via ResourceLimits::memory_request_mb; absent falls back to resources.memory_mb, so every spec that does not set it is admitted exactly as it was before the request existed.

§Why the two numbers must not be the same one

A limit answers “kill it past here”; a request answers “don’t start it somewhere smaller than here”. Generous is the safe direction for the first and the unschedulable direction for the second, so one field serving both makes a deliberately-roomy ceiling into an admission floor.

That is not hypothetical: WorkloadSpec::for_forge sets a 32 GiB ceiling explicitly reasoned as “above physical RAM on smaller build-workers ⇒ effectively unlimited there” (R590-B10), and CloudConfig::admit_workload fed that same 32768 in as the R572-F5 capacity floor. Every build-worker under 32 GiB — the three 8 GiB Pi-5s and the 16 GiB us-west-003 — became structurally unadmittable for any offloaded qed step, leaving one 47 GiB node as the fleet’s only legal target for remote CI. This is R590-B10’s own recorded follow-up (“thread a per-step memory request … instead of a blanket forge default”), reduced to the seam that closes the bug.

Source

pub fn cpu_limit_millis(&self) -> Option<u32>

The hard CPU ceiling (millicores) a backend may enforce, if the workload declares one — the exact mirror of Self::memory_request_mb, which adds the missing request beside a field that is a ceiling. Here the field (ResourceLimits::cpu_millis) is the request and this adds the missing ceiling.

Opt-in via ResourceLimits::cpu_limit_millis. Absent or 0 means no ceiling: the workload gets its declared share of a contended node and may burst to the whole box on an idle one. That is the default because cpu_millis is documented as a request, and every backend but one has always rendered it as a relative weight (ResourceLimits::cpu_shares).

§Why this exists (R885-B5 / W344 Finding 5)

R885-B1 wired the cgroup v2 driver onto the live native deploy path, and that driver rendered cpu_millis into cpu.max — a hard quota. Measured on us-east-001 on 2026-09-11: four native workloads at cpu.max = 25600 100000, i.e. capped at 0.256 of a core even on an idle node, where before the wiring they could burst to the whole machine. A request rendered as a ceiling is a semantic bug, not a missing feature.

Source

pub fn pids_limit(&self) -> u32

The hard process-count ceiling (cgroup v2 pids.max) a backend enforces on this workload’s leaf — R885-T2 (W344 Finding 3: “no bound on a fork bomb, accidental or otherwise”).

Shaped like Self::cpu_limit_millis (an opt-in override), but its default runs the OPPOSITE direction on purpose. An absent cpu_limit_millis correctly means “no ceiling”, because cpu_millis already has a well-defined request-only meaning without it. There is no such fallback for pids: “unbounded” is precisely the bug R885-T2 closed, not a feature to preserve, so absent or 0 both fall back to DEFAULT_PIDS_MAX instead of to “unset”.

DEFAULT_PIDS_MAX’s doc comment records where the number comes from and why it does not need to be tight to be useful.

Opt-in override via ResourceLimits::pids_max for a workload that legitimately needs a different bound.

Source

pub fn scratch_floor_mb(&self) -> Option<u32>

A floor on the microVM scratch disk in MiB — the smallest workspace this workload is willing to be given, or None for “the backend’s own floor is fine”.

Opt-in via ResourceLimits::scratch_floor_mb. Absent or 0 both mean no declared floor; the microVM backend still applies its own (microvm::WORKSPACE_MIN_BYTES), which is what actually sizes every workload in this tree today.

§Why this replaced a field (R885-T6 / W344)

It is the successor to ResourceLimits::ephemeral_storage_mb, which was deleted rather than renamed because the field lied. Its doc comment called it a “cap on the writable layer + tmpfs footprint” and no backend ever enforced it as one — not the OCI resources block, not docker’s argv, and deliberately not the cgroup v2 driver. Its single live consumer, microvm::workspace::disk_size_bytes, read it as a floor, i.e. the exact opposite. for_forge then set it to 512 MiB, a number that as a cap would have failed every build at its first checkout and as a floor was simply ignored.

A field that means one thing at its definition and the reverse at its only use is not a field to keep compatible with, so per the pre-1.0 doctrine in CLAUDE.md the design changed instead of being taped: the floor is now spelled floor.

Source

pub fn wants_native_exec(&self) -> bool

Whether this workload must be run by kamaji’s native (fork+exec) backend on the node’s own userland, rather than by a container backend (R577-T1 / W254).

Opt-in via annotations["yah.exec"] == "native" (see NATIVE_EXEC_ANNOTATION / NATIVE_EXEC_VALUE) — the same annotation-shaped, zero-blast-radius marker as Self::wants_host_network and Self::requires_taint, chosen over a new plain field for the reason R572-F1 recorded: a field forces a struct-literal edit at every existing construction site and an exhaustive-match update in kamaji-proto’s codec, and this marker needs neither.

§Why an annotation and not a runtime enum on the wire

The remote-execution wire already carries exactly one workload shape — Workload::Container(WorkloadSpec) — and every layer between the dispatcher and the node (yubaba admission, mesh assignment, log ingest, produced-file retrieval, teardown) is written against it. A Darwin build differs from a Linux build in one respect: there is no container that can host it, because you cannot containerize the Darwin kernel. Marking that one difference keeps the rest of the path shared instead of growing a parallel exec_native RPC that would have to re-implement all of it.

image stays populated for a native workload and is identity metadata only — nothing is pulled; the native backend resolves argv from entrypoint + command (container semantics) and execs it on the host.

Source

pub fn wants_resume_after_restart(&self) -> bool

Whether this workload asks its node’s kamaji to replay it after kamaji restarts. See RESUME_AFTER_RESTART_ANNOTATION for why it is opt-in.

Source

pub fn wants_float(&self) -> bool

Whether this workload is a cluster floater. See FLOAT_ANNOTATION.

Source

pub fn wants_microvm(&self) -> bool

Whether this workload must be run by kamaji’s microVM backend — booted in its own KVM guest with its own kernel, rather than sharing the host kernel with every other workload on the node (R605-F8 / W325 §5).

Opt-in via annotations["yah.exec"] == "microvm" (see NATIVE_EXEC_ANNOTATION / MICROVM_EXEC_VALUE).

§Why the same key as native exec, not a new one

W325’s Shape A calls this “a sibling branch on a new annotation value”, and the value — not the key — is the whole point. yah.exec names the execution substrate, and a workload has exactly one:

yah.execsubstratekernelisolation
(absent)container backendhost’snamespaces + cgroup
nativefork+exec on the hosthost’snone
microvmKVM guestits ownhardware

A second key (yah.isolation = microvm, say) would make yah.exec = native + yah.isolation = microvm expressible, and therefore something a dispatcher could emit and a backend would have to refuse — exactly the refusal validate_native_exec_spec already has to carry for the yah.sandbox pair, and for the same avoidable reason. A map key holds one value, so on this key the three substrates are mutually exclusive by construction: there is no spec on which both this and Self::wants_native_exec return true, and exec_substrate_markers_are_mutually_exclusive_by_construction pins that.

§What the marker does and does not promise

Like every marker on this struct it is inert metadata — it declares intent and nothing more. Whether a node can honour it is a node capability question (/dev/kvm, a guest kernel, a rootfs; see W325 §4), and a node whose kamaji has no microVM backend configured refuses the deploy rather than falling back to a container. That refusal is deliberate and mirrors R577-T1’s: a caller asking for microVM isolation is asking for the one property a container cannot provide, so silently downgrading it would return success while delivering the thing the caller specifically declined.

image is identity metadata only, as it is for native exec — nothing is pulled. The guest’s root filesystem comes from the node’s configured rootfs image, and argv is resolved from entrypoint + command with container semantics, so one spec shape drives all three substrates.

Source

pub fn exec_substrate(&self) -> ExecSubstrate

The substrate this spec selects, as an ordered value (R894-F1).

The same reading Self::wants_native_exec and Self::wants_microvm perform, collapsed into one total function so that “which substrate is this” has a single answer rather than two booleans a caller re-combines. Every existing if wants_native_exec() … else if wants_microvm() … ladder in the tree is that re-combination, and R605-F8’s own doc notes that a duplicated ladder is exactly what drifts when a fourth substrate arrives.

An unrecognised yah.exec value reads as ExecSubstrate::Container, preserving what both predicates already do. That is the fail-closed direction for the trust check in Self::trust’s consumers: a typo’d microvm on an untrusted spec reads as a container and is refused, rather than reading as the microVM the author meant to ask for.

Source

pub fn trust(&self) -> Result<TrustLevel, TrustDeclError>

How far this workload’s code is trusted, from TRUST_ANNOTATION (R894-F1).

Absent means TrustLevel::Trusted — see that type’s docs for why that default is safe only because of where the untrusted marker is stamped. An unrecognised value is an Err, never a fallback to either side.

Source

pub fn stamp_untrusted(&mut self)

Stamp this spec as carrying code the operator did not write, and raise its substrate request to the minimum that trust level requires.

This is the choke-point verb. Any constructor that turns third-party bytes (a tenant image, a vended camp, a user-supplied argv) into a WorkloadSpec calls it in the same function that takes those bytes, so the marker cannot be lost by a caller who forgets. R823 is the first such path.

It raises the substrate rather than only marking trust because the two halves belong to the same decision and splitting them across two call sites is how one of them goes missing. Raising is one-directional: a caller that already asked for something at least as strong keeps its own request, so stamping a spec that explicitly wants a microVM is a no-op and stamping is idempotent.

A spec that had asked for native becomes microvm. That is not a silent downgrade — it is a widening of isolation, the safe direction — and it happens at the moment the untrusted origin is established, not at admission, where the same mismatch is a refusal.

Source

pub fn wants_nested_sandbox(&self) -> bool

Whether this workload builds its own unprivileged container sandbox inside the one the backend gives it, and therefore needs the two capabilities plus the no_new_privs relaxation that setting up a user namespace requires (R636-B2).

Opt-in via annotations["yah.sandbox"] == "nested" (see NESTED_SANDBOX_ANNOTATION / NESTED_SANDBOX_VALUE) — the same annotation-shaped, zero-blast-radius marker as Self::wants_host_network and Self::wants_native_exec.

§What it actually grants, and why exactly that

Rootless BuildKit (the only user today: remote build-image steps dispatch moby/buildkit:*-rootless) boots through rootlesskit, which must map a range of sub-uids into a fresh user namespace. It does that by exec’ing the setuid-root helpers newuidmap / newgidmap, so it needs CAP_SETUID + CAP_SETGID in the bounding set and noNewPrivileges = false (with no_new_privs on, the kernel silently strips the setuid bit and the helper fails with “Could not set caps”).

Each of those three was measured on us-west-002 to be individually necessary — dropping any one of them puts rootlesskit back to failing before the first layer:

grantrootlesskit result
baseline (CAP_NET_BIND_SERVICE only, nnp on)fork/exec /usr/bin/newuidmap: operation not permitted
+CAP_SETUID only, nnp offfork/exec /usr/bin/newgidmap: operation not permitted
+CAP_SETUID +CAP_SETGID, nnp onnewuidmap: Could not set caps
+CAP_SETUID +CAP_SETGID, nnp offstarts; build runs to completion

It is deliberately not CAP_SYS_ADMIN: a non-rootless buildkitd would need that instead, which is a far wider grant. Emptying /etc/subuid to force rootlesskit’s single-mapping path does not avoid the helpers either — it just fails earlier with “No subuid ranges found”.

The backend guards this. Like host networking, it is honoured only for tier == "infra" workloads; a non-infra workload that sets the annotation is rejected at deploy. Every other workload keeps the CAP_NET_BIND_SERVICE-only, no_new_privs baseline.

§Mutually exclusive with Self::wants_native_exec

This grant is defined in terms of an OCI process spec — a capability set and a noNewPrivileges bit. A native (fork+exec) workload has no OCI spec, so there is nothing to apply it to; kamaji refuses a spec carrying both markers rather than accepting a request for widened privileges and silently dropping it (R577-T1 owns that refusal). The two are independent annotations — neither implies the other, which is what nested_sandbox_marker_is_independent_of_the_other_markers pins — but they are not a legal pair.

If a future runtime does have a sandbox worth widening (a MacVM under W254, say), give it its own annotation rather than relaxing that refusal. The grant this marker names is CAP_SETUID + CAP_SETGID + no_new_privs off and nothing else; letting it mean a different privilege set per backend would make “what does yah.sandbox=nested grant?” unanswerable without knowing which backend received it, which is precisely what a security-relevant marker must not be.

Source

pub fn writable_paths(&self) -> Result<Vec<PathBuf>, WritablePathsDeclError>

The host paths this workload declares it writes to (R885-B11 / W344), from WRITABLE_PATHS_ANNOTATION. Empty when nothing is declared.

This is the input to kamaji’s native filesystem confinement: a native workload is a plain fork+exec on the host, so the only description of what it may write is the one its spec carries. kamaji::sandbox unions these with the spec’s Bind volumes and denies writes everywhere else — and confines only a workload that declares one or the other, so a spec that says nothing about its writes is not silently guessed at.

§Why an annotation rather than a field

It qualifies the substrate markers (yah.exec, yah.sandbox) that already ride the annotation map, and it is a policy hint for the sandbox rather than a fact about the workload’s shape. (The original reason — a positional postcard wire on which every new field was a protocol bump — stopped holding at kamaji-proto V13, R896-F2.)

[annotations]
"yah.writable-paths" = "/var/lib/yah/qed,/tmp"
§What is refused, and why refused rather than normalized

A relative entry, a ./.. component, an empty entry (a stray comma), or a duplicate. Each of these would otherwise turn into a grant — a landlock rule is PathBeneath, so a mis-resolved path does not fail closed, it opens a subtree nobody asked for. Normalizing silently is how you grant write access to the wrong directory and never hear about it.

Note that a declared path is a ceiling, not a mount: nothing here creates a directory. A path that does not exist on the node is skipped (with a warning) when the ruleset is built.

Source

pub fn db_rows(&self) -> Result<&[WorkloadDb], DbDeclError>

The durability tier this workload declares for its own state, if it declares one at all (R850-P4).

Ok(None) and Ok(Some(tier: DurabilityTier::None)) are different answers and must stay different: the first is “nobody said”, the second is “somebody looked and decided not to”. A named volume with no declaration is the shape that loses every byte when its node dies, and collapsing the two would let the analyzer report that case in the same words as a deliberately-ephemeral cache.

[durability]
tier        = "stream"          # none|snapshot|dedup|stream
engine      = "turso"           # required by every tier but "none"
store       = "s3://yah-backups/noisetable-account"
subjects    = ["accounts.db", "passkeys.db", "sessions.db"]
rpo_seconds = 120               # stream only
§Read this, not the field

Serde refuses an unknown tier or engine and a non-numeric RPO, but it cannot see the rules that span fields — a shipping tier with no store, an RPO on a snapshot tier, a subject that escapes its volume. Those are Durability::check, and this accessor is the one read that applies it, so a malformed declaration refuses at every consumer rather than only at the ones that remembered to validate.

§The retired annotations (R896-F3)

This declaration was the yah.durability.* annotation family until kamaji-proto V13 made a field addition cross a skew. Any surviving key of that family is DurabilityDeclError::RetiredAnnotation, never ignored: an ignored annotation is indistinguishable from an absent one, and “absent” here means “this database has no backup”.

§Why engine and subjects are not optional (R850-F1)

The tier vocabulary is turso-backup-shaped, and P4 shipped it on a generic WorkloadSpec — so a Postgres appliance could declare tier = "stream" and mean something no code in this tree can do. engine makes that claim explicit and refusable at parse time rather than at 3am.

subjects exists because a restore has a file as its unit and a workload has a volume. The driving case (R850) is one process with three turso databases inside one named volume; “restore the volume” is not a thing turso-backup can do, and guessing which files in a directory are databases is guessing about the only copy of somebody’s data. Paths are volume-relative — the same string the analyzer prints and the hydrate helper joins onto the host volume root — and are validated against traversal, because they name a host path something will write to.

§What is and is not wired

This accessor plus validate::shape’s check on it is the whole of the runtime effect today: declaring a tier does not yet cause a backup to happen. turso-backup implements all three tiers (DurabilityTier::Snapshot = its tier 1a, DurabilityTier::Dedup = 1b, DurabilityTier::Stream = 2 with restore-by-frame-replay) and, since R850-F1, the fencing epoch a hydrate must hold (turso_backup::claim). Nothing in yubaba’s reconciler calls into any of it yet.

Until that lands, the declaration’s value is exactly that cloud::topology can tell an operator, before the topology is committed, which of their stateful workloads has no second copy of its bytes anywhere. The [[db]] rows, after the rules serde cannot enforce: names are non-blank and unique, and a snapshot row needs a [durability] whose subjects include its subject. A vend row needs a mesh port named sql and a verify-key secret mount (R960-F8), and every capabilities row must name a declared mesh port.

Source

pub fn served_capabilities(&self) -> Vec<ServedCapability>

Everything a node running this spec publishes on GET /services: the declared Self::capabilities, then one sql.hrana row per vend WorkloadDb on the sql port.

Source

pub fn durability(&self) -> Result<Option<&Durability>, DurabilityDeclError>

Source

pub fn retired_annotation(&self) -> Option<(&str, &'static str)>

The first annotation (in key order) this spec still carries from a family R896-F3 moved into typed fields, paired with the field that replaced it. None for every spec written against the fields.

validate::shape refuses on it. It exists because nothing reads those keys any more, and a key nothing reads fails silently: a yah.limits.pids-max would quietly fall back to the default bound, and a yah.durability.tier would quietly mean “no backup”.

Trait Implementations§

Source§

impl Clone for WorkloadSpec

Source§

fn clone(&self) -> Self

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 WorkloadSpec

Source§

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

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

impl<'de> Deserialize<'de> for WorkloadSpec

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 PartialEq for WorkloadSpec

Source§

fn eq(&self, other: &Self) -> 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 WorkloadSpec

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 WorkloadSpec

Source§

impl TS for WorkloadSpec

Source§

type WithoutGenerics = WorkloadSpec

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 = WorkloadSpec

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<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.