herdr phase 1a: missions runtime_kind + target_node schema

First slice of the second-runtime path. Missions now carry
runtime_kind ('zeroclaw' | 'local_herdr') + target_node_id (FK to
nodes) so the mission_orchestrator + phase executors can dispatch
differently depending on where the operator wants execution.

Migration:
  - 0055_missions_runtime_kind.sql — adds runtime_kind (NOT NULL
    DEFAULT 'zeroclaw' + CHECK), target_node_id (nullable FK ON
    DELETE SET NULL). All existing missions backfill to 'zeroclaw'
    so behavior is unchanged.
  - topology_runs also grows herdr_workspace_id / herdr_tab_id /
    herdr_pane_id text columns so a resumed run can reattach to the
    same Herdr pane instead of spawning a duplicate.

Code:
  - cm-db::repo::missions — Mission + NewMission carry the two new
    fields; all SELECTs updated; INSERT COALESCE-defaults
    runtime_kind to 'zeroclaw' when unspecified.
  - routes::missions::create — validates runtime_kind and requires
    target_node_id when kind='local_herdr' (400 otherwise).
  - lib/api/missions.ts — RuntimeKind type; Mission carries both;
    CreateMissionRequest optional fields.

Behavior is opt-in: no path exists yet to actually create a
local_herdr mission — that lands in Phase 1c (wizard picker). This
commit just makes the schema + validation in place so Phase 1b's
fleet_herdr dispatch module can key on it.

Tests: mission_orchestrator integration test still green.
This commit is contained in:
Omar Sobh
2026-07-20 09:45:15 -07:00
parent 1f0117e35a
commit d6dbd044c8
4 changed files with 73 additions and 4 deletions
+17
View File
@@ -35,6 +35,9 @@ pub struct CreateMissionRequest {
pub config: Value,
#[serde(default)]
pub phases: Vec<PhaseSpec>,
/// Defaults to "zeroclaw". "local_herdr" requires target_node_id.
pub runtime_kind: Option<String>,
pub target_node_id: Option<Uuid>,
}
fn default_schedule() -> Value {
@@ -120,6 +123,18 @@ pub async fn create(
if body.title.trim().is_empty() {
return Err(ApiError::BadRequest);
}
// Validate runtime_kind + require target_node when local_herdr.
let runtime_kind = body.runtime_kind.as_deref().unwrap_or("zeroclaw");
match runtime_kind {
"zeroclaw" => {}
"local_herdr" => {
if body.target_node_id.is_none() {
return Err(ApiError::BadRequest);
}
}
_ => return Err(ApiError::BadRequest),
}
let new = NewMission {
workspace_id: user.workspace_id.as_uuid(),
title: body.title.trim(),
@@ -130,6 +145,8 @@ pub async fn create(
schedule: body.schedule,
description: body.description.as_deref(),
config: body.config,
runtime_kind: Some(runtime_kind),
target_node_id: body.target_node_id,
phases: body
.phases
.into_iter()