Expand description
TowerRule schema — predicate + trigger + federation types for yah-tower.
This crate is the schema source of truth for tower rules. It has zero
dependencies on the tower runtime; the tower engine, the desktop authoring
UI, tower.simulate, and tests all depend on this crate directly.
Architecture: .yah/docs/architecture/A052-yah-tower.md
@yah:ticket(R380-T4, “Add tower-rules TaskPlacement (on-wire flat form, no MeshIdent)”) @yah:assignee(agent:claude) @yah:at(2026-06-01T21:06:13Z) @yah:status(review) @yah:parent(R380) @yah:next(“The tower-rules enum at lib.rs:257 today is Local | Remote | Integration with no MeshIdent (cross-crate dep direction forbids it). Replace with TaskPlacement { location: LocationKind (Local|RemoteAny{tier}), runtime: Native|Container } — note no Remote(MeshIdent) variant here either.”) @yah:next(“Provide a converter from tower-rules TaskPlacement → task crate TaskPlacement that the consumer applies at the boundary (tower-rules can’t depend on task crate types).”) @yah:next(“Update ts-rs export — new TS file TaskPlacement.ts replaces ForgeWhere.ts. Coordinate with frontend integration tests.”) @yah:handoff(“Added TaskPlacement / TaskLocation / TaskRuntime / TierTag in crates/yah/tower-rules/src/lib.rs alongside the existing ForgeWhere (kept for T5 to migrate Trigger::AgentDispatch). Wire format chosen to match task crate: TaskLocation uses tag="kind" with snake_case discriminator so boundary converters can pass JSON through for shared variants. Tower-rules TaskLocation has only Local | RemoteAny{tier} — no Remote(MeshIdent) variant since rules can’t name specific yubaba nodes from YAML. Added 5 round-trip + wire-format tests; all 24 tower-rules tests pass. ForgeWhere is now marked deprecated in its rustdoc + the generated ForgeWhere.ts.”) @yah:next(“R380-T5 picks up: replace Trigger::AgentDispatch.forge_where: ForgeWhere with placement: TaskPlacement, update parse.rs YAML loader, update tower/src/triggers.rs ForgeSpec, update tower/src/rules/validate.rs round-trip fixtures, update tests in tower/tests/{dispatch,simulate,triggers}.rs and tower-rules/tests/round_trip.rs, update tower-rules library YAML files (yubaba-restart-on-error.yaml etc.)”) @yah:next(“Boundary converter: tower → task ForgeSpec construction needs to widen tower_rules::TaskPlacement → task::TaskPlacement. Mapping is mechanical: Local→Local, RemoteAny{tier:tower_rules::TierTag(s)}→RemoteAny{tier:workload_spec::TierTag(s)}, runtime variants pass through. Lives in whichever crate already depends on both (probably the tower/cli boundary in app/yah/cli/src/tower.rs or a tower-task adapter). The tower crate itself currently does NOT depend on task — pulling task in there is the natural seam.”) @yah:verify(“cargo test -p tower-rules”) @yah:verify(“cargo run -q -p tower-rules –bin tower-rules-export-ts && ls packages/yah/ui/src/gen/{TaskPlacement,TaskLocation,TaskRuntime,TierTag}.ts”)
@yah:ticket(R406-T7, “Structured drain protocol: versioned wire format, ack semantics, SIGTERM fallback”) @yah:assignee(agent:claude) @yah:at(2026-06-02T03:26:44Z) @yah:status(review) @yah:phase(P2) @yah:parent(R406) @arch:see(.yah/docs/working/W154-yubaba-dual-runtime.md) @yah:depends_on(R406-T6) @yah:handoff(“T7 ships the structured drain protocol end-to-end at the protocol+enforcer layer. (1) kamaji-proto: DrainOutcome { Flushed | Checkpointed | ForceKilled | UnknownWorkload | Unsupported } and a peer DrainCompleted push message (non-exhaustive, V1-compatible). DrainAck rustdoc now documents the two-mode semantics (sync T7 default / async T8 push). DrainBudget::total_ms() helper. (2) app/yah/kamaji/src/drain.rs: enforce_drain state machine — SIGTERM via pidfd_send_signal (race-free), AsyncFd::readable for exit observation bounded by flush_ms+checkpoint_ms, SIGKILL escalation + 5s safety reap. classify_clean_exit pure helper splits Flushed vs Checkpointed by elapsed-vs-flush_ms boundary (≤ inclusive on flush). (3) Server registry gained insert_drainable / take_drainable + DrainableHandle { pid, pidfd }. Drain RPC short-circuits on unknown workload, otherwise runs enforce_drain synchronously and translates DrainOutcome → DrainAck { accepted, reason } via drain_outcome_to_ack (decision-table: clean phase = accepted=true with elapsed_ms; ForceKilled / Unsupported / Unknown / DrainError = accepted=false). (4) Coverage: 19 kamaji-proto codec tests (8 new for drain outcomes + DrainCompleted round-trip + DrainBudget saturation), 47 kamaji unit tests (drain::tests pure boundary cases + server::tests decision-table-as-spec on every DrainOutcome variant + drain_unknown_workload UDS round-trip), 1 integration test extended in tests/uds_skeleton.rs (Drain over UDS for unknown id). Linux-only drain::linux::tests exercise the real-child path (SIGTERM-responsive vs SIGTERM-ignoring) but only run on Linux CI — not exercised on darwin local.”) @yah:next(“T8 picks up: extract Yubaba-proper supervision out of in-process, dispatch through Kamaji over UDS. Kamaji’s Deploy handler currently still returns Internal — T8 wires it to native::spawn and populates registry.insert_drainable(id, { pid, pidfd }) so the Drain handler has a real handle to consume.”) @yah:next(“T8 also adds the back-channel (Kamaji → Yubaba push) that lets Kamaji reply DrainAck { accepted=true, reason=‘started’ } immediately and push DrainCompleted { request_id, id, outcome } when the drain finishes — that switches the wire shape from synchronous-mode to async-mode without touching V1 protocol-version (DrainCompleted rides #[non_exhaustive]).”) @yah:next(“T11 picks up the workload-side leg: pick stdio-sentinel vs HTTP-endpoint, define the workload’s ack envelope, then drain.rs grows a phase boundary signal so workloads know they crossed flush_ms into checkpoint_ms without consulting the clock. The pidfd-level SIGTERM floor stays as the fallback.”) @yah:verify(“cargo test -p kamaji-proto -p kamaji”)
Structs§
- Agent
Class Ref - Reference to an agent class definition, e.g.
"gnome/fixer". - Field
Filter - Jsonpath-flavored filter applied to an event’s
fieldsobject. - ForgeId
- Opaque identifier for a forge run.
- Mesh
Ident - DNS-segment identity for a service on the cluster mesh, e.g.
"noisetable-api.pdx". - Prompt
Template - Mustache-style template for the agent prompt, rendered with the captured event payload at dispatch time.
- Rate
Predicate - “At least N matches in T milliseconds” sliding-window rate filter.
- RuleId
- Unique identifier for a rule instance, e.g.
"yubaba-quorum-loss". - Task
Placement - Orthogonal placement of a task: where it runs (
location) × how it’s sandboxed (runtime). - TierTag
- Capacity-tier selector for a yubaba node, e.g.
"infra"or"tenant". - Tower
Rule - A complete tower rule: a structured predicate over scryer events paired with a trigger that fires when the predicate matches.
Enums§
- Compound
Op - Boolean operator for compound predicates.
- Federation
Policy - Controls which machines’ scryer instances this rule subscribes to.
- FieldOp
- Operation applied to a field value.
- Health
Signal - Scryer substrate health condition that tower can predicate over.
- Level
Filter - Minimum log level for a predicate to match. Ordered: Debug < Info < Warn < Error.
- Notification
Channel - Channel through which a
Notificationtrigger delivers attention. - Predicate
- Structured filter over scryer events.
- Schema
Version - Wire-format schema version for tower rules.
- Scope
Filter - Filter over the scope of a scryer event.
- Severity
- Severity of a
Notificationtrigger. - String
Filter - Pattern match over a string field.
- Task
Location - Where the task runs. Wire-form view, expressible from a tower rule.
- Task
Runtime - How a task is sandboxed: subprocess vs. image-backed container.
- Trigger
- What tower does when a rule’s predicate matches.
- Yubaba
Action Kind - Bounded, reversible cluster operation dispatched by a
YubabaActiontrigger.
Functions§
- export_
ts - Export all tower-rules TypeScript bindings to
out_dir.