feat(missions): choose the independent validator per mission (#53)

`CLAWMATES_VALIDATOR_MODEL` is deployment-wide, so proving Slice 2 put a second
provider on the critical path of EVERY phase verdict. `cross_provider_judge`
deliberately does not fall back when the independent judge fails — a verdict
quietly produced by a same-family model would claim a property it does not have —
so a z.ai outage makes phases unmeetable rather than merely unverified. That is a
per-mission trade, not a per-deployment one.

`missions.validator_model` (0068), settable at create, with three distinct states
because an empty string and NULL mean opposite things in a nullable text column:

  NULL          use the deployment default
  ''            explicitly NO independent validator — judge with the house model.
                The default must not quietly reinstate independence a mission was
                told to skip.
  'glm:glm-4.7' this spec, subject to the same three refusals as before:
                same-family rejected, unregistered provider rejected, and a failed
                independent judge does not fall back.

Whitespace counts as empty: a column hand-set to " " meant to say nothing.

478 tests pass, clippy clean. Behaviour is unchanged for existing missions — they
have NULL and so keep following the deployment default.
This commit is contained in:
Omar Sobh
2026-08-05 23:07:38 -07:00
parent b17e18aa67
commit c840688adb
4 changed files with 108 additions and 9 deletions
+5
View File
@@ -41,6 +41,10 @@ pub struct CreateMissionRequest {
/// Which per-CLI rootfs a `microvm` mission boots (`missions.backend`), e.g.
/// "claude". NULL boots the node's default image.
pub backend: Option<String>,
/// Model that independently validates this mission's phase verdicts, e.g.
/// `glm:glm-4.7`. Omit to use the deployment default; send `""` to opt out of
/// independent validation and judge with the house model.
pub validator_model: Option<String>,
}
fn default_schedule() -> Value {
@@ -285,6 +289,7 @@ pub async fn create(
runtime_kind: Some(runtime_kind),
target_node_id: body.target_node_id,
backend: body.backend.as_deref(),
validator_model: body.validator_model.as_deref(),
phases: phases_for_create(
crate::workflow_registry::get(body.template_kind.trim()),
body.phases,