feat(workflow): add exact Fleet schema, Reasoning Router, and disclosure redaction

Introduces the Rust-owned core of Saved Fleets and the Reasoning Router as
pure library modules, with no runtime call path yet. Everything here is
parse/resolve/verify logic that later slices wire into the TUI.

- `fleet_exact`: the exact Fleet schema. Routes are frozen — provider and
  model are literals, and the selector tokens that are legal elsewhere in
  CodeWhale (`auto`, `inherit`, `faster`, ...) are rejected by value, while
  `deny_unknown_fields` rejects `model_strength`/`loadout`/`model_class` as
  keys. Roles compare under one canonical key, so the `oracle`/`advisor` →
  `consultant` rename resolves in both directions.
- `reasoning_router`: the saved Router service profile, addressable either
  as a named reference several Fleets can share or as a legacy inline
  router captured from an older file.
- `fleet_reasoning`: requested → effective reasoning resolution, the Router
  call plan, and strict decision parsing. Parsing rejects trailing content,
  duplicate keys, and anything that would mutate a frozen route, so a
  malformed Router reply fails closed rather than silently re-routing.
- `fleet_preflight`: endpoint identity and credential readiness, evaluated
  before any Router spend.
- `fleet_snapshot`: content-addressed Fleet snapshots. Hashes are
  recomputed and validated on read, and no absolute path is ever captured.
- `redaction`: the single chokepoint that strips secrets and absolute paths
  out of anything bound for a receipt.

`named_fleet` gains `FleetSearchRoot`/`FleetDocument`/`load_by_name` and is
committed together with the leaf modules rather than ahead of them: it
imports `fleet_exact` and `fleet_snapshot`, and those import back, so the
two halves do not compile apart.

Two corrections to the harvested source:

- `redaction` dropped a dead `arm` reset that `redact_token` overwrites
  before it can be read. Behavior is unchanged; the assignment only tripped
  `unused_assignments` under `-D warnings`.
- `legacy_advisory_role_names_canonicalize_to_consultant` asserted that a
  lookup by the legacy spelling returns `None`, which contradicts both the
  module's stated contract and `member_by_role`, which canonicalizes the
  lookup key. The assertion now checks that both spellings land on the same
  member. This is a test fix, not a behavior change.

Tests: 222 workflow lib tests, 16 exact_fleet_workflow integration tests.
This commit is contained in:
Hmbown
2026-07-27 02:51:53 -07:00
parent aaeb2fbeb9
commit 9c3177e549
11 changed files with 8194 additions and 2 deletions
Generated
+1
View File
@@ -1169,6 +1169,7 @@ dependencies = [
"sha2 0.11.0",
"tempfile",
"thiserror 2.0.18",
"toml 1.1.2+spec-1.1.0",
]
[[package]]
+1
View File
@@ -12,6 +12,7 @@ serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
thiserror.workspace = true
toml.workspace = true
[dev-dependencies]
serde_json.workspace = true
File diff suppressed because it is too large Load Diff
+394
View File
@@ -0,0 +1,394 @@
//! Worker route **preflight**: everything about a route that must be true and
//! frozen *before* a Workflow starts, and certainly before any Router is
//! asked anything.
//!
//! An exact Fleet's promise is that the saved provider/model is the one that
//! runs. That promise is only worth something if it is *checked* at the point
//! the run is admitted, not discovered at the first API call:
//!
//! - **Provider identity** — the exact configured provider key and its kind.
//! - **Canonical wire model** — the model string that will actually be placed
//! on the request. Receipt and child spawn must use *this* value, not the
//! file's spelling of it, or the receipt describes a request nobody made.
//! - **Credential / readiness** — decided **locally**, from configuration. No
//! live probe: a preflight that hits the network would spend money and leak
//! the fact of the run before the operator's gates have even been evaluated.
//! Keyless local providers (`vllm`, `ollama`, `sglang`, …) are
//! [`CredentialReadiness::KeylessLocal`] and are perfectly valid.
//! - **Endpoint identity** — a non-secret label for *where* the request goes,
//! so two members pointed at different deployments of the same model id are
//! distinguishable on a receipt. Never a full URL with credentials in it.
//! - **Reasoning capability** — what the route can truthfully express, derived
//! once here so a later launch cannot invent one.
//!
//! Everything in this module is a plain value with no clock, no filesystem, and
//! no network. The host supplies the facts; this crate defines their shape and
//! the invariants over them.
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::fleet_exact::FrozenRoute;
use crate::fleet_reasoning::ReasoningCapability;
/// Whether a route can be called at all, decided from local configuration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum CredentialReadiness {
/// A credential for this provider is configured on this machine.
Configured,
/// The provider is a local, keyless endpoint. Valid, and not a downgrade.
KeylessLocal,
/// No credential is configured. The route cannot run.
Missing { detail: String },
}
impl CredentialReadiness {
#[must_use]
pub const fn is_ready(&self) -> bool {
matches!(self, Self::Configured | Self::KeylessLocal)
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Configured => "configured",
Self::KeylessLocal => "keyless_local",
Self::Missing { .. } => "missing",
}
}
}
/// A non-secret identity for the endpoint a route talks to.
///
/// Deliberately **not** a base URL: a configured base URL can carry a token in
/// its path or query, and receipts are durable. Host plus a coarse path label is
/// enough to tell two deployments apart, which is the only thing this is for.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EndpointIdentity {
/// Host (and port, when non-default), lowercased. Never credentials.
pub host: String,
/// Whether the endpoint resolves to loopback / a private address.
pub local: bool,
}
impl EndpointIdentity {
/// Build an endpoint identity from a base URL, keeping only the host.
///
/// Parsing is deliberately minimal and dependency-free: strip the scheme,
/// drop anything before an `@` (which is exactly where a credential would
/// live), then keep the authority up to the first `/`.
#[must_use]
pub fn from_base_url(base_url: &str) -> Self {
let without_scheme = base_url
.trim()
.split_once("://")
.map_or(base_url.trim(), |(_, rest)| rest);
let authority = without_scheme
.split(['/', '?', '#'])
.next()
.unwrap_or_default();
// `user:password@host` — everything before the `@` is a credential.
let host = authority
.rsplit_once('@')
.map_or(authority, |(_, host)| host)
.to_ascii_lowercase();
let bare = host.split(':').next().unwrap_or(&host);
let local = bare == "localhost"
|| bare == "127.0.0.1"
|| bare == "::1"
|| bare.starts_with("192.168.")
|| bare.starts_with("10.")
|| bare.ends_with(".local");
Self { host, local }
}
/// The compact receipt form.
#[must_use]
pub fn label(&self) -> String {
if self.local {
format!("{} (local)", self.host)
} else {
self.host.clone()
}
}
}
/// One worker's route, fully preflighted and frozen.
///
/// Constructed once, at Workflow start. A launch reads it; nothing rewrites
/// it. The `wire_model` here is the single source of truth for both the
/// receipt and the child spawn.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PreflightedRoute {
/// The member this route belongs to.
pub member_id: String,
/// Exact configured provider key, as the operator named it.
pub provider_id: String,
/// Provider kind (`zai`, `openai`, `deepseek`, `vllm`, …).
pub provider_kind: String,
/// The model id exactly as saved in the Fleet file.
pub declared_model: String,
/// The canonical model string that will be placed on the wire. Receipt and
/// child spawn both use this.
pub wire_model: String,
/// Where the request goes.
pub endpoint: EndpointIdentity,
/// Locally decided readiness. Never a live probe.
pub credential: CredentialReadiness,
/// What the route can truthfully express about reasoning.
pub capability: ReasoningCapability,
}
impl PreflightedRoute {
/// The frozen provider/model pair, in canonical wire form.
///
/// This is what a receipt records and what a child spawns with — the two
/// cannot disagree because there is only one value.
#[must_use]
pub fn frozen(&self) -> FrozenRoute {
FrozenRoute {
provider: self.provider_id.clone(),
model: self.wire_model.clone(),
}
}
/// Whether the declared model string differed from the canonical wire form.
/// Recorded rather than hidden: `glm-5` resolving to `glm-5-20260101` is a
/// fact the operator should be able to see on a receipt.
#[must_use]
pub fn model_canonicalized(&self) -> bool {
self.declared_model != self.wire_model
}
/// Fail if this route is not runnable. Called at Workflow start.
pub fn require_ready(&self) -> Result<(), PreflightError> {
match &self.credential {
CredentialReadiness::Configured | CredentialReadiness::KeylessLocal => Ok(()),
CredentialReadiness::Missing { detail } => Err(PreflightError::CredentialMissing {
member: self.member_id.clone(),
provider: self.provider_id.clone(),
detail: detail.clone(),
}),
}
}
}
/// A frozen preflight for every worker in a Workflow, plus the Router.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct RoutePreflight {
workers: Vec<PreflightedRoute>,
router: Option<PreflightedRoute>,
}
impl RoutePreflight {
#[must_use]
pub fn new(workers: Vec<PreflightedRoute>, router: Option<PreflightedRoute>) -> Self {
Self { workers, router }
}
#[must_use]
pub fn workers(&self) -> &[PreflightedRoute] {
&self.workers
}
#[must_use]
pub fn router(&self) -> Option<&PreflightedRoute> {
self.router.as_ref()
}
/// The frozen route for one member id.
#[must_use]
pub fn worker(&self, member_id: &str) -> Option<&PreflightedRoute> {
let key = member_id.trim().to_ascii_lowercase();
self.workers.iter().find(|route| route.member_id == key)
}
/// Fail unless every worker route is runnable.
///
/// Called before a Workflow is allowed to start, so a member with no
/// credential is a startup error rather than a first-task surprise.
pub fn require_all_ready(&self) -> Result<(), PreflightError> {
for route in &self.workers {
route.require_ready()?;
}
Ok(())
}
/// Whether any worker route talks to a different provider than the Router.
/// This is what a receipt discloses as cross-provider inference.
#[must_use]
pub fn crosses_providers(&self, member_id: &str) -> bool {
match (self.worker(member_id), self.router()) {
(Some(worker), Some(router)) => worker.provider_id != router.provider_id,
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PreflightError {
#[error(
"fleet member `{member}` is pinned to provider `{provider}`, which does not resolve to a \
configured provider: {detail}"
)]
ProviderUnresolved {
member: String,
provider: String,
detail: String,
},
#[error(
"fleet member `{member}` is pinned to model `{model}` on provider `{provider}`, which is \
not a valid route: {detail}"
)]
ModelUnresolved {
member: String,
provider: String,
model: String,
detail: String,
},
#[error(
"fleet member `{member}` cannot run: provider `{provider}` has no credential configured \
on this machine ({detail}). This is decided locally — no provider was contacted. Keyless \
local providers do not need one."
)]
CredentialMissing {
member: String,
provider: String,
detail: String,
},
#[error(
"cannot determine what reasoning control provider `{provider}` actually expresses for \
model `{model}`: {detail}. An exact fleet fails closed here rather than claiming a \
capability it did not verify."
)]
CapabilityUnknown {
provider: String,
model: String,
detail: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
fn route(member: &str, provider: &str, wire: &str) -> PreflightedRoute {
PreflightedRoute {
member_id: member.to_string(),
provider_id: provider.to_string(),
provider_kind: provider.to_string(),
declared_model: wire.to_string(),
wire_model: wire.to_string(),
endpoint: EndpointIdentity::from_base_url("https://api.z.ai/api/paas/v4"),
credential: CredentialReadiness::Configured,
capability: ReasoningCapability::tiered(),
}
}
#[test]
fn an_endpoint_identity_keeps_the_host_and_drops_credentials() {
let identity =
EndpointIdentity::from_base_url("https://user:sk-secret@api.z.ai/api/paas/v4");
assert_eq!(identity.host, "api.z.ai");
assert!(!identity.local);
assert!(!identity.label().contains("sk-secret"));
assert!(!identity.label().contains('/'));
}
#[test]
fn loopback_and_private_endpoints_are_marked_local() {
for url in [
"http://127.0.0.1:8000/v1",
"http://localhost:11434",
"http://192.168.1.20:8000/v1",
"http://box.local/v1",
] {
let identity = EndpointIdentity::from_base_url(url);
assert!(identity.local, "{url} must be local");
assert!(identity.label().ends_with("(local)"));
}
assert!(!EndpointIdentity::from_base_url("https://api.openai.com/v1").local);
}
/// The receipt and the child spawn must not be able to disagree, so there
/// is exactly one canonical wire model and both read it.
#[test]
fn the_frozen_route_uses_the_canonical_wire_model() {
let mut preflighted = route("implementer", "zai", "glm-5");
preflighted.wire_model = "glm-5-20260101".to_string();
assert_eq!(preflighted.frozen().model, "glm-5-20260101");
assert_eq!(preflighted.frozen().provider, "zai");
assert!(preflighted.model_canonicalized());
assert_eq!(preflighted.declared_model, "glm-5");
}
/// Keyless local providers are first-class: readiness is about whether the
/// route can run, not about whether a key exists.
#[test]
fn keyless_local_providers_are_ready() {
let mut local = route("worker", "vllm", "qwen3");
local.credential = CredentialReadiness::KeylessLocal;
local.endpoint = EndpointIdentity::from_base_url("http://127.0.0.1:8000/v1");
assert!(local.credential.is_ready());
local.require_ready().expect("keyless local is valid");
assert_eq!(local.credential.as_str(), "keyless_local");
}
#[test]
fn a_missing_credential_fails_the_workflow_locally() {
let mut route = route("implementer", "zai", "glm-5");
route.credential = CredentialReadiness::Missing {
detail: "no ZAI_API_KEY".to_string(),
};
let preflight = RoutePreflight::new(vec![route], None);
let err = preflight
.require_all_ready()
.expect_err("a member with no credential must not start");
assert!(matches!(err, PreflightError::CredentialMissing { .. }));
let message = err.to_string();
assert!(
message.contains("decided locally"),
"the error must say no provider was contacted: {message}"
);
}
#[test]
fn cross_provider_inference_is_detectable_from_the_preflight() {
let preflight = RoutePreflight::new(
vec![route("implementer", "zai", "glm-5")],
Some(route("router", "openai", "gpt-5.6-luna")),
);
assert!(preflight.crosses_providers("implementer"));
let same = RoutePreflight::new(
vec![route("implementer", "zai", "glm-5")],
Some(route("router", "zai", "glm-5-turbo")),
);
assert!(!same.crosses_providers("implementer"));
// With no router, nothing crosses.
let none = RoutePreflight::new(vec![route("implementer", "zai", "glm-5")], None);
assert!(!none.crosses_providers("implementer"));
}
#[test]
fn a_preflight_serializes_without_secrets_or_paths() {
let preflight = RoutePreflight::new(
vec![route("implementer", "zai", "glm-5")],
Some(route("router", "openai", "gpt-5.6-luna")),
);
let json = serde_json::to_string(&preflight).expect("serialize");
let lowered = json.to_ascii_lowercase();
for forbidden in [
"api_key", "secret", "bearer", "base_url", "/users/", "https://",
] {
assert!(!lowered.contains(forbidden), "{forbidden} in {json}");
}
let back: RoutePreflight = serde_json::from_str(&json).expect("round-trip");
assert_eq!(back, preflight);
}
}
File diff suppressed because it is too large Load Diff
+874
View File
@@ -0,0 +1,874 @@
//! Immutable Fleet snapshot taken at Workflow start.
//!
//! A saved Fleet is editable; a *running* Workflow is not. At start we capture
//! a secret-free, durable value containing the qualified Fleet identity, the
//! schema kind/revision/hash, the exact members, the exact routes, the
//! reasoning policies, and the permission ceilings. Editing the saved file
//! afterwards changes only future runs — the snapshot in flight is unaffected,
//! because it owns copies and exposes no mutators.
//!
//! **No-secrets invariant**: every field here is a non-sensitive id, model
//! string, tier label, or boolean. There is deliberately no field that could
//! hold a credential, token, or base URL.
use serde::{Deserialize, Serialize};
use crate::fleet_exact::{
ExactFleet, ExactFleetError, FrozenRoute, PermissionCeiling, RequestedReasoning,
canonical_member_key, canonical_role_key,
};
use crate::named_fleet::{FleetDocument, FleetSchema};
use crate::reasoning_router::CapturedReasoningRouter;
/// A Fleet identity qualified by where the definition came from.
///
/// Deliberately **path-free**. An absolute filesystem path in a durable receipt
/// leaks the operator's home directory, username, and machine layout into
/// journals and events that travel further than the machine that wrote them.
/// `origin/name` plus the schema/content hashes identify a definition precisely
/// enough to compare two runs, without any of that. Local diagnostic errors
/// (fleet not found, ambiguous fleet) still name paths — those are read on the
/// machine that produced them and never persisted onto a receipt.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QualifiedFleetId {
/// Fleet name as declared in the file.
pub name: String,
/// Non-secret origin label, e.g. `workspace` or `codewhale_home`.
pub origin: String,
}
impl QualifiedFleetId {
/// `origin/name` — the stable display form.
#[must_use]
pub fn qualified(&self) -> String {
format!("{}/{}", self.origin, self.name)
}
}
/// One member as frozen into the snapshot.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FleetSnapshotMember {
pub id: String,
pub role: String,
/// The exact route, frozen before any reasoning resolution.
pub route: FrozenRoute,
/// The reasoning policy the member requested (not the effective tier —
/// that is resolved per run and recorded on the receipt).
pub requested_reasoning: RequestedReasoning,
pub permissions: PermissionCeiling,
}
/// The Reasoning Router service a snapshot is attached to.
///
/// This is [`CapturedReasoningRouter`] under its historic name — the Router is
/// no longer a Fleet member, so the alias exists only to keep older call sites
/// and serialized shapes readable.
pub type FleetSnapshotRouter = CapturedReasoningRouter;
/// A legacy fleet's role → profile binding, recorded for provenance.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FleetSnapshotLegacyRole {
pub role: String,
pub profile: String,
}
/// The immutable value captured at Workflow start.
///
/// Fields are private and there are no setters: once captured, the only way to
/// change a snapshot is to take a new one.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FleetSnapshot {
fleet: QualifiedFleetId,
schema_kind: String,
schema_revision: u32,
/// SHA-256 of the fleet definition bytes.
schema_hash: String,
/// SHA-256 over the captured members/routes/policies themselves, so two
/// snapshots can be compared without re-reading the source file.
content_hash: String,
members: Vec<FleetSnapshotMember>,
/// The attached Reasoning Router service, if this Fleet references one.
/// Resolved by the host (which owns the search roots) and handed in, so a
/// snapshot stays a pure value with no loader inside it.
router: Option<FleetSnapshotRouter>,
legacy_roles: Vec<FleetSnapshotLegacyRole>,
/// Caller-supplied timestamp; this crate has no clock.
captured_at: String,
}
impl FleetSnapshot {
/// Capture a snapshot from a parsed fleet document and an already-resolved
/// Reasoning Router service.
///
/// Exact rosters are **revalidated here**, not trusted. `ExactFleet` is
/// public and `Deserialize`, so a document can reach this point without
/// having passed the TOML parser's invariant checks; capture is the last
/// place to catch a duplicate role, an id/role collision, or a worker
/// claiming the Router's identity before those become a running Workflow.
///
/// `router` is the captured service, whether it came from a saved reusable
/// profile or was normalized out of the legacy inline form. Resolution
/// happens in the host because it needs the fleet search roots; capture
/// only records the result.
pub fn capture(
fleet: QualifiedFleetId,
document: &FleetDocument,
captured_at: impl Into<String>,
router: Option<CapturedReasoningRouter>,
) -> Result<Self, ExactFleetError> {
let (members, legacy_roles) = match document.schema() {
FleetSchema::Exact(exact) => {
exact.validate()?;
(exact_members(exact), Vec::new())
}
FleetSchema::Legacy(legacy) => (
Vec::new(),
legacy
.roles
.iter()
.map(|(role, profile)| FleetSnapshotLegacyRole {
role: role.clone(),
profile: profile.clone(),
})
.collect(),
),
};
let mut snapshot = Self {
fleet,
schema_kind: document.schema_kind().to_string(),
schema_revision: document.schema_revision(),
schema_hash: document.source_hash().to_string(),
content_hash: String::new(),
members,
router,
legacy_roles,
captured_at: captured_at.into(),
};
snapshot.content_hash = snapshot.compute_content_hash();
Ok(snapshot)
}
/// Recompute the canonical content hash and reject a snapshot whose
/// recorded hash does not describe its own contents.
///
/// `FleetSnapshot` is `Deserialize` and its `content_hash` is an ordinary
/// field, so a snapshot can reach a launch without ever having passed
/// [`Self::capture`] — through a replay file, a cache, or an IPC hop. That
/// hash is then stamped onto the durable receipt as the evidence that a run
/// matched a saved definition, so an unverified one is not weak evidence but
/// *false* evidence: it asserts a definition the members may not describe.
///
/// Call this before anything durable or costly happens. It is cheap (one
/// canonical serialization plus a SHA-256) and it is the only thing standing
/// between a tampered or migrated snapshot and a receipt that vouches for
/// it.
pub fn verify_content_hash(&self) -> Result<(), ExactFleetError> {
let recomputed = self.compute_content_hash();
if recomputed == self.content_hash {
return Ok(());
}
Err(ExactFleetError::ContentHashMismatch {
fleet: self.fleet.qualified(),
recorded: self.content_hash.clone(),
recomputed,
})
}
/// [`Self::verify_content_hash`], as a guard that yields the snapshot.
///
/// Exists so a load path cannot verify and then accidentally go on to use a
/// *different* value: the only thing this returns is the snapshot it just
/// checked.
pub fn into_verified(self) -> Result<Self, ExactFleetError> {
self.verify_content_hash()?;
Ok(self)
}
fn compute_content_hash(&self) -> String {
// Hash only the captured shape, not the timestamp: two Workflows
// started from the same saved Fleet must agree.
#[derive(Serialize)]
struct Shape<'a> {
fleet: &'a QualifiedFleetId,
schema_kind: &'a str,
schema_revision: u32,
schema_hash: &'a str,
members: &'a [FleetSnapshotMember],
router: &'a Option<FleetSnapshotRouter>,
legacy_roles: &'a [FleetSnapshotLegacyRole],
}
let shape = Shape {
fleet: &self.fleet,
schema_kind: &self.schema_kind,
schema_revision: self.schema_revision,
schema_hash: &self.schema_hash,
members: &self.members,
router: &self.router,
legacy_roles: &self.legacy_roles,
};
let encoded = serde_json::to_vec(&shape).expect("snapshot shape is serializable");
crate::named_fleet::sha256_label(&encoded)
}
#[must_use]
pub fn fleet(&self) -> &QualifiedFleetId {
&self.fleet
}
#[must_use]
pub fn schema_kind(&self) -> &str {
&self.schema_kind
}
#[must_use]
pub const fn schema_revision(&self) -> u32 {
self.schema_revision
}
#[must_use]
pub fn schema_hash(&self) -> &str {
&self.schema_hash
}
#[must_use]
pub fn content_hash(&self) -> &str {
&self.content_hash
}
#[must_use]
pub fn members(&self) -> &[FleetSnapshotMember] {
&self.members
}
#[must_use]
pub fn router(&self) -> Option<&FleetSnapshotRouter> {
self.router.as_ref()
}
#[must_use]
pub fn legacy_roles(&self) -> &[FleetSnapshotLegacyRole] {
&self.legacy_roles
}
#[must_use]
pub fn captured_at(&self) -> &str {
&self.captured_at
}
/// Look up a member by its **member id** — what addresses a roster entry.
#[must_use]
pub fn member(&self, id: &str) -> Option<&FleetSnapshotMember> {
let key = canonical_member_key(id);
self.members
.iter()
.find(|member| canonical_member_key(&member.id) == key)
}
/// Look up a member by its **semantic role** — what gates, handoffs, and
/// records use. Kept separate from id lookup so a task can carry a
/// meaningful role while the runtime resolves a distinct profile id.
///
/// Both sides resolve through [`canonical_role_key`], so a snapshot frozen
/// from a Fleet saved under a renamed role is still addressable by a gate or
/// handoff that spells the role the old way.
#[must_use]
pub fn member_by_role(&self, role: &str) -> Option<&FleetSnapshotMember> {
let key = canonical_role_key(role);
self.members
.iter()
.find(|member| canonical_role_key(&member.role) == key)
}
/// Look up by id first, then by role. Roster invariants forbid an id/role
/// collision, so this can never be order-dependent.
#[must_use]
pub fn member_by_id_or_role(&self, id_or_role: &str) -> Option<&FleetSnapshotMember> {
self.member(id_or_role)
.or_else(|| self.member_by_role(id_or_role))
}
/// Whether any frozen member requested `auto` reasoning — i.e. whether this
/// Workflow needs a working Reasoning Router at all.
#[must_use]
pub fn has_auto_member(&self) -> bool {
self.members
.iter()
.any(|member| member.requested_reasoning.is_auto())
}
/// Ids of the members that requested `auto`, for a startup error that names
/// who actually needs the Router.
#[must_use]
pub fn auto_member_ids(&self) -> Vec<String> {
self.members
.iter()
.filter(|member| member.requested_reasoning.is_auto())
.map(|member| member.id.clone())
.collect()
}
}
fn exact_members(exact: &ExactFleet) -> Vec<FleetSnapshotMember> {
exact
.members
.iter()
.map(|member| FleetSnapshotMember {
id: canonical_member_key(&member.id),
// The snapshot is what every receipt is built from, so it records
// the *canonical* role even when the saved file used a renamed one.
// Old files keep working (lookup resolves either spelling); new
// receipts never print a name the current schema does not use.
role: canonical_role_key(&member.role),
route: member.frozen_route(),
requested_reasoning: member.reasoning,
permissions: member.permissions,
})
.collect()
}
/// Verify a snapshot that arrived from anywhere other than [`FleetSnapshot::capture`].
///
/// The free function exists for load/deserialize seams that hold a snapshot by
/// reference and only need the yes/no answer — a durable-write guard, a replay
/// loader, a cache read. It is the same check as
/// [`FleetSnapshot::verify_content_hash`]; having a named entry point is what
/// lets those call sites read as "verify before use" rather than as an
/// incidental method call.
pub fn verify_snapshot_content_hash(snapshot: &FleetSnapshot) -> Result<(), ExactFleetError> {
snapshot.verify_content_hash()
}
/// Normalize an exact Fleet's **legacy inline** Router into the captured
/// service, if it used the prototype form.
///
/// A Fleet that references a saved profile resolves through
/// [`crate::ReasoningRouterProfile::load_by_name`] instead, in the host that
/// owns the search roots. Both paths land on the same value, which is the whole
/// point of keeping only one runtime representation.
#[must_use]
pub fn captured_legacy_inline_router(exact: &ExactFleet) -> Option<CapturedReasoningRouter> {
exact
.legacy_inline_router()
.map(CapturedReasoningRouter::from_legacy_inline)
}
#[cfg(test)]
mod content_hash_tests {
use super::*;
const EXACT_FLEET: &str = r#"
name = "glm-pair"
schema = "exact"
[[members]]
id = "implementer"
role = "builder"
provider = "zai"
model = "glm-5"
reasoning = "high"
permissions = "read_write"
[[members]]
id = "advisor-one"
role = "oracle"
provider = "zai"
model = "glm-5"
reasoning = "low"
permissions = "analyst"
"#;
fn captured() -> FleetSnapshot {
let document = FleetDocument::parse(EXACT_FLEET).expect("parse fleet document");
FleetSnapshot::capture(
QualifiedFleetId {
name: "glm-pair".to_string(),
origin: "workspace".to_string(),
},
&document,
"2026-07-26T00:00:00Z",
None,
)
.expect("capture")
}
#[test]
fn a_freshly_captured_snapshot_verifies() {
let snapshot = captured();
assert!(snapshot.verify_content_hash().is_ok());
assert!(verify_snapshot_content_hash(&snapshot).is_ok());
assert!(snapshot.into_verified().is_ok());
}
/// The round trip a replay file, a cache read, or an IPC hop performs. An
/// untouched snapshot must survive it — otherwise the guard below would be
/// unusable at exactly the seams it exists for.
#[test]
fn an_untouched_round_trip_still_verifies() {
let snapshot = captured();
let encoded = serde_json::to_string(&snapshot).expect("serialize");
let decoded: FleetSnapshot = serde_json::from_str(&encoded).expect("deserialize");
assert_eq!(decoded, snapshot);
assert!(decoded.verify_content_hash().is_ok());
}
/// The tamper case. A snapshot whose members were edited after capture
/// keeps its old hash, and that hash is what a receipt would vouch for.
/// Verification must reject it *before* any launch or durable write.
#[test]
fn an_edited_member_is_rejected_while_the_hash_still_claims_the_original() {
let snapshot = captured();
let original_hash = snapshot.content_hash().to_string();
let mut value = serde_json::to_value(&snapshot).expect("serialize");
// Widen a member's route — the single most consequential edit, and the
// one a stale hash would silently certify.
value["members"][0]["route"]["model"] = serde_json::json!("glm-5-max");
let tampered: FleetSnapshot = serde_json::from_value(value).expect("deserialize");
assert_eq!(
tampered.content_hash(),
original_hash,
"the tamper does not touch the recorded hash — that is the point"
);
let error = tampered
.verify_content_hash()
.expect_err("a tampered snapshot must not verify");
assert!(matches!(
error,
ExactFleetError::ContentHashMismatch { ref recorded, .. } if *recorded == original_hash
));
assert!(tampered.into_verified().is_err());
}
/// Widening a permission ceiling is the tamper that matters most, since the
/// receipt's fingerprint is computed from the ceiling this snapshot carries.
#[test]
fn a_widened_permission_ceiling_is_rejected() {
let snapshot = captured();
let mut value = serde_json::to_value(&snapshot).expect("serialize");
value["members"][1]["permissions"]["write"] = serde_json::json!(true);
let tampered: FleetSnapshot = serde_json::from_value(value).expect("deserialize");
assert!(tampered.verify_content_hash().is_err());
}
/// A forged hash fails the same way an edited body does: the check is a
/// recomputation, not a presence test, so neither side can be trusted alone.
#[test]
fn a_forged_hash_is_rejected() {
let snapshot = captured();
let mut value = serde_json::to_value(&snapshot).expect("serialize");
value["content_hash"] = serde_json::json!("0".repeat(64));
let forged: FleetSnapshot = serde_json::from_value(value).expect("deserialize");
assert!(forged.verify_content_hash().is_err());
}
/// The migration case: a snapshot written by an older build that recorded a
/// renamed role verbatim. Capture now canonicalizes, so the *stored* role is
/// `consultant` and the hash covers that — an old snapshot carrying
/// `oracle` cannot pass verification and must be re-captured rather than
/// quietly relabelled at read time.
#[test]
fn a_pre_rename_snapshot_is_rejected_rather_than_silently_relabelled() {
let snapshot = captured();
assert_eq!(
snapshot.members()[1].role,
"consultant",
"capture records the canonical role"
);
let mut value = serde_json::to_value(&snapshot).expect("serialize");
value["members"][1]["role"] = serde_json::json!("oracle");
let migrated: FleetSnapshot = serde_json::from_value(value).expect("deserialize");
assert!(migrated.verify_content_hash().is_err());
// The alias still *resolves* — compatibility is a lookup property, not a
// licence to accept an unverified hash.
assert!(migrated.member_by_role("consultant").is_some());
}
/// Lookup canonicalization survives capture: a snapshot frozen from a Fleet
/// saved under the old name answers to either spelling.
#[test]
fn snapshot_role_lookup_accepts_both_spellings() {
let snapshot = captured();
for spelling in ["consultant", "oracle", "advisor", "ORACLE"] {
assert_eq!(
snapshot
.member_by_role(spelling)
.unwrap_or_else(|| panic!("`{spelling}` must resolve"))
.id,
"advisor-one"
);
}
assert!(snapshot.member_by_id_or_role("oracle").is_some());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fleet_exact::ShellCeiling;
use crate::reasoning_router::{
LEGACY_INLINE_ROUTER_ORIGIN, REASONING_ROUTER_SERVICE_KIND, ReasoningRouterProfile,
RouterCallReasoning,
};
/// A Fleet that references a saved, reusable Router profile — the shape new
/// Fleets use.
const EXACT: &str = r#"
name = "glm-pair"
schema = "exact"
reasoning_router = "luna-low"
[[members]]
id = "implementer"
role = "builder"
provider = "zai"
model = "glm-5"
reasoning = "auto"
permissions = "read_write"
[[members]]
id = "auditor"
provider = "zai"
model = "glm-5"
reasoning = "high"
permissions = "read_only"
"#;
/// The prototype form, retained for compatibility.
const LEGACY_INLINE: &str = r#"
name = "glm-pair"
schema = "exact"
[[members]]
id = "implementer"
role = "builder"
provider = "zai"
model = "glm-5"
reasoning = "auto"
permissions = "read_write"
[[members]]
id = "router"
kind = "router"
provider = "zai"
model = "glm-5-turbo"
"#;
const LEGACY_ROLE_MAP: &str = r#"
name = "stopship"
description = "legacy roster"
[roles]
scout = "scout"
implementer = "builder"
"#;
const LUNA: &str = r#"
name = "luna-low"
schema = "reasoning_router"
provider = "openai"
model = "gpt-5.6-luna"
call_reasoning = "low"
"#;
fn id() -> QualifiedFleetId {
QualifiedFleetId {
name: "glm-pair".to_string(),
origin: "workspace".to_string(),
}
}
fn luna() -> CapturedReasoningRouter {
let profile = ReasoningRouterProfile::parse(LUNA).expect("router profile");
CapturedReasoningRouter::from_profile(&profile, "workspace")
}
fn capture(text: &str, router: Option<CapturedReasoningRouter>) -> FleetSnapshot {
let document = FleetDocument::parse(text).expect("parse");
FleetSnapshot::capture(id(), &document, "2026-07-26T00:00:00Z", router).expect("capture")
}
#[test]
fn snapshot_captures_identity_schema_routes_and_ceilings() {
let snapshot = capture(EXACT, Some(luna()));
assert_eq!(snapshot.fleet().qualified(), "workspace/glm-pair");
assert_eq!(snapshot.schema_kind(), "exact");
assert_eq!(snapshot.schema_revision(), 1);
assert!(snapshot.schema_hash().starts_with("sha256:"));
assert!(snapshot.content_hash().starts_with("sha256:"));
// Roles and ids are separate lookups, and both find the same member.
let by_role = snapshot.member_by_role("builder").expect("role lookup");
let by_id = snapshot.member("implementer").expect("id lookup");
assert_eq!(by_role.id, by_id.id);
assert_eq!(by_id.route.provider, "zai");
assert_eq!(by_id.route.model, "glm-5");
assert_eq!(by_id.requested_reasoning, RequestedReasoning::Auto);
assert!(by_id.permissions.write);
// An id lookup must not answer to a role, or a task naming one would
// silently resolve the other.
assert!(snapshot.member("builder").is_none());
assert!(snapshot.member_by_role("implementer").is_none());
assert!(snapshot.has_auto_member());
assert_eq!(snapshot.auto_member_ids(), vec!["implementer".to_string()]);
}
/// The Router is a referenced service, not a Fleet member: it holds no
/// authority, is never dispatchable, and is not in the roster.
#[test]
fn the_attached_router_is_a_service_and_not_a_roster_member() {
let snapshot = capture(EXACT, Some(luna()));
let router = snapshot.router().expect("router service");
assert_eq!(router.service_kind, REASONING_ROUTER_SERVICE_KIND);
assert_eq!(router.qualified(), "workspace/luna-low");
assert!(!router.legacy_inline);
assert!(!router.is_dispatchable());
assert!(!router.dispatchable);
assert!(router.tool_surface().is_empty());
assert_eq!(router.route.provider, "openai");
assert_eq!(router.route.model, "gpt-5.6-luna");
assert_eq!(router.requested_call_reasoning, RouterCallReasoning::Low);
assert_eq!(router.permissions.shell, ShellCeiling::None);
assert!(!router.permissions.tools);
assert_eq!(router.permissions.delegation_depth, 0);
// Not reachable through worker lookup by either id or role.
assert!(snapshot.member("luna-low").is_none());
assert!(snapshot.member_by_role("luna-low").is_none());
assert!(snapshot.member_by_id_or_role("router").is_none());
}
/// One saved profile, two different Fleets. The service is referenced, not
/// owned, so both snapshots capture the identical value.
#[test]
fn one_router_profile_serves_two_fleets() {
let first = capture(EXACT, Some(luna()));
let second_text = EXACT.replace("name = \"glm-pair\"", "name = \"other-pair\"");
let document = FleetDocument::parse(&second_text).expect("parse");
let second = FleetSnapshot::capture(
QualifiedFleetId {
name: "other-pair".to_string(),
origin: "workspace".to_string(),
},
&document,
"2026-07-26T00:00:00Z",
Some(luna()),
)
.expect("capture");
assert_eq!(first.router(), second.router());
assert_ne!(first.fleet(), second.fleet());
assert_ne!(
first.content_hash(),
second.content_hash(),
"different fleets are still different snapshots"
);
}
/// The prototype inline form normalizes into the same captured service, so
/// nothing downstream has to know which way the operator wrote it.
#[test]
fn a_legacy_inline_router_normalizes_into_the_same_captured_service() {
let document = FleetDocument::parse(LEGACY_INLINE).expect("parse");
let exact = document.exact().expect("exact");
let captured = captured_legacy_inline_router(exact).expect("inline router");
assert!(captured.legacy_inline);
assert_eq!(captured.origin, LEGACY_INLINE_ROUTER_ORIGIN);
assert_eq!(captured.service_kind, REASONING_ROUTER_SERVICE_KIND);
assert_eq!(captured.route.model, "glm-5-turbo");
assert_eq!(captured.requested_call_reasoning, RouterCallReasoning::Off);
assert!(!captured.is_dispatchable());
assert!(!captured.permissions.tools);
let snapshot = FleetSnapshot::capture(
id(),
&document,
"2026-07-26T00:00:00Z",
Some(captured.clone()),
)
.expect("capture");
assert_eq!(snapshot.router(), Some(&captured));
// The inline member is not in the roster.
assert!(snapshot.member("router").is_none());
assert_eq!(snapshot.members().len(), 1);
}
#[test]
fn editing_the_saved_fleet_does_not_touch_a_running_snapshot() {
let snapshot = capture(EXACT, Some(luna()));
// The operator edits the saved file mid-run: different model, different
// reasoning, wider permissions.
let edited = EXACT
.replace(
"model = \"glm-5\"\nreasoning = \"auto\"",
"model = \"glm-4\"\nreasoning = \"off\"",
)
.replace("permissions = \"read_write\"", "permissions = \"full\"");
let next = capture(&edited, Some(luna()));
// The in-flight snapshot is untouched.
let member = snapshot.member("implementer").expect("member");
assert_eq!(member.route.model, "glm-5");
assert_eq!(member.requested_reasoning, RequestedReasoning::Auto);
assert!(!member.permissions.network_tool);
// The next run sees the edit, and the hashes prove they differ.
assert_eq!(next.member("implementer").unwrap().route.model, "glm-4");
assert_ne!(snapshot.schema_hash(), next.schema_hash());
assert_ne!(snapshot.content_hash(), next.content_hash());
}
#[test]
fn identical_definitions_produce_an_identical_content_hash() {
let document = FleetDocument::parse(EXACT).expect("parse");
let a = FleetSnapshot::capture(id(), &document, "2026-07-26T00:00:00Z", Some(luna()))
.expect("capture");
// Different capture time, same fleet: the content hash must not move.
let b = FleetSnapshot::capture(id(), &document, "2026-07-27T09:30:00Z", Some(luna()))
.expect("capture");
assert_eq!(a.content_hash(), b.content_hash());
assert_ne!(a.captured_at(), b.captured_at());
}
/// Swapping the attached Router is a real change to what will run, so it
/// must move the content hash.
#[test]
fn changing_the_attached_router_changes_the_content_hash() {
let with_luna = capture(EXACT, Some(luna()));
let without = capture(EXACT, None);
assert_ne!(with_luna.content_hash(), without.content_hash());
}
#[test]
fn legacy_role_map_fleets_snapshot_as_legacy() {
let document = FleetDocument::parse(LEGACY_ROLE_MAP).expect("parse legacy");
let snapshot = FleetSnapshot::capture(
QualifiedFleetId {
name: "stopship".to_string(),
origin: "workspace".to_string(),
},
&document,
"2026-07-26T00:00:00Z",
None,
)
.expect("capture");
assert_eq!(snapshot.schema_kind(), "legacy");
assert_eq!(snapshot.schema_revision(), 0);
assert!(snapshot.members().is_empty());
assert!(snapshot.router().is_none());
assert!(!snapshot.has_auto_member());
assert_eq!(snapshot.legacy_roles().len(), 2);
assert!(
snapshot
.legacy_roles()
.iter()
.any(|role| role.role == "implementer" && role.profile == "builder")
);
}
#[test]
fn snapshot_serialization_carries_no_secret_shaped_fields() {
let snapshot = capture(EXACT, Some(luna()));
let json = serde_json::to_string(&snapshot).expect("serialize");
let lowered = json.to_ascii_lowercase();
for forbidden in [
"api_key",
"apikey",
"secret",
"token",
"bearer",
"password",
"base_url",
"credential",
"authorization",
] {
assert!(
!lowered.contains(forbidden),
"snapshot must not carry `{forbidden}`: {json}"
);
}
// Round-trips as a durable value.
let back: FleetSnapshot = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, snapshot);
}
/// A durable snapshot identifies its definition by qualified origin/name
/// and by hash — never by a filesystem path, which would leak the
/// operator's home directory and username into anything that stores it.
#[test]
fn a_snapshot_carries_no_filesystem_path() {
let tmp = tempfile::tempdir().expect("tmp");
std::fs::create_dir_all(tmp.path().join("fleets")).expect("dirs");
let path = tmp.path().join("fleets/glm-pair.toml");
std::fs::write(&path, EXACT).expect("write");
let document = FleetDocument::load(&path, Some("glm-pair")).expect("load from disk");
// The document still knows where it came from, for local diagnostics.
assert!(document.source_path().is_some());
let snapshot =
FleetSnapshot::capture(id(), &document, "2026-07-26T00:00:00Z", Some(luna()))
.expect("capture");
let json = serde_json::to_string(&snapshot).expect("serialize");
assert!(!json.contains(&tmp.path().display().to_string()), "{json}");
for fragment in ["/Users/", "/home/", "/private/", ".toml", "\\Users\\"] {
assert!(
!json.contains(fragment),
"snapshot must not carry `{fragment}`: {json}"
);
}
assert_eq!(snapshot.fleet().qualified(), "workspace/glm-pair");
assert!(snapshot.content_hash().starts_with("sha256:"));
}
/// Capture is the last gate before a roster becomes a running Workflow, so
/// a value that never saw the TOML parser must still be rejected here.
#[test]
fn capture_revalidates_a_roster_that_bypassed_the_parser() {
use crate::fleet_exact::ExactMember;
let member = |id: &str, role: &str| ExactMember {
id: id.to_string(),
role: role.to_string(),
provider: "zai".to_string(),
model: "glm-5".to_string(),
reasoning: RequestedReasoning::Off,
permissions: PermissionCeiling::default(),
};
let smuggled = ExactFleet {
name: "f".to_string(),
description: None,
schema_revision: 1,
reasoning_router: None,
// Two members, one role: role lookup would resolve by list order.
members: vec![member("a", "builder"), member("b", "builder")],
router: None,
};
let document = FleetDocument::from_exact_for_tests(smuggled);
let err = FleetSnapshot::capture(id(), &document, "2026-07-26T00:00:00Z", None)
.expect_err("capture must revalidate");
assert!(
matches!(err, ExactFleetError::DuplicateRole { .. }),
"{err:?}"
);
}
}
+40 -1
View File
@@ -5,10 +5,16 @@
//! top only after their cancellation and evidence semantics are proven.
mod elevation;
pub mod fleet_exact;
pub mod fleet_preflight;
pub mod fleet_reasoning;
pub mod fleet_snapshot;
mod gates;
mod js_authoring;
mod model_policy;
mod named_fleet;
pub mod reasoning_router;
mod redaction;
mod replay;
mod role_resolve;
@@ -22,6 +28,29 @@ pub use elevation::{
DEFAULT_HIGH_BUDGET_THRESHOLD, ElevationOptions, PlanRiskHint, WorkflowPlanElevation,
assess_plan_risk_string, assess_workflow_elevation, is_shell_tool, is_write_tool,
};
pub use fleet_exact::{
EXACT_FLEET_SCHEMA_KIND, EXACT_FLEET_SCHEMA_REVISION, ExactFleet, ExactFleetError, ExactMember,
FrozenRoute, LEGACY_FLEET_SCHEMA_KIND, PermissionCeiling, ROLE_ALIASES, ROUTER_PUBLIC_ID,
ROUTER_PUBLIC_ROLE, ReasoningTier, RequestedReasoning, RouterMember, ShellCeiling,
canonical_member_key, canonical_role_key,
};
pub use fleet_preflight::{
CredentialReadiness, EndpointIdentity, PreflightError, PreflightedRoute, RoutePreflight,
};
pub use fleet_reasoning::{
EffectiveReasoning, EffectiveReasoningSource, FAITHFUL_WIRE_TIERS, FleetTaskReceipt,
ProviderEffectiveReasoning, ProviderReasoningControl, ROUTER_CALL_REASONING,
ROUTER_MAX_OUTPUT_TOKENS, ROUTER_REASONING_FIELD, ROUTER_SUMMARY_MAX_CHARS, ROUTING_SCOPE,
ReasoningCapability, ReasoningResolveError, ResolvedReasoning, RouterAvailability,
RouterCallDisclosure, RouterCallInput, RouterCallPlan, RouterDecision, RouterDecisionError,
RouterIdentity, RoutingDisclosure, RoutingPayload, TaskShape, bounded_routing_payload,
parse_router_decision, resolve_exact_member_reasoning, resolve_legacy_reasoning,
router_call_plan, router_system_prompt, router_user_message, transport_disclosure,
};
pub use fleet_snapshot::{
FleetSnapshot, FleetSnapshotLegacyRole, FleetSnapshotMember, FleetSnapshotRouter,
QualifiedFleetId, captured_legacy_inline_router, verify_snapshot_content_hash,
};
pub use gates::{
GateError, GateKind, GateOn, GateOnFail, GateOutcome, GateSpec, GateState, GateStatusLine,
HandoffArtifact, LaneGateBoard, stopship_gate_pipeline,
@@ -32,9 +61,19 @@ pub use js_authoring::{
};
pub use model_policy::*;
pub use named_fleet::{
NamedFleet, NamedFleetError, STOPSHIP_REQUIRED_ROLES, load_named_fleet, load_named_fleet_file,
FleetDocument, FleetSchema, FleetSearchRoot, NamedFleet, NamedFleetError,
STOPSHIP_REQUIRED_ROLES, exact_schema_revision, load_named_fleet, load_named_fleet_file,
parse_named_fleet,
};
pub use reasoning_router::{
CapturedReasoningRouter, FleetRouterRef, LEGACY_INLINE_ROUTER_ORIGIN, QualifiedRouterId,
REASONING_ROUTER_DIR, REASONING_ROUTER_SCHEMA_KIND, REASONING_ROUTER_SERVICE_KIND,
ReasoningRouterError, ReasoningRouterProfile, RouterCallReasoning,
};
pub use redaction::{
REDACTION_ABSOLUTE_PATH, REDACTION_RELATIVE_PATH, REDACTION_SECRET, Redaction,
redact_for_disclosure,
};
pub use replay::*;
pub use role_resolve::{
FleetRoleMap, FleetRoleResolveError, ResolvedWorkflowAgent, normalize_token,
+402 -1
View File
@@ -3,14 +3,64 @@
//! Format: TOML at `fleets/<name>.toml` (workspace) or
//! `$CODEWHALE_HOME/fleets/<name>.toml`.
//!
//! Fleet resolves roles → profile ids only. Runtime owns tmux/worktrees.
//! Two forms share this one store — there is no parallel fleet directory:
//!
//! - **Legacy**: `[roles]` maps role → AgentProfile id. Fleet resolves roles →
//! profile ids only; Runtime owns tmux/worktrees. Legacy files declare no
//! `schema` key, which is what makes the form explicitly detectable rather
//! than guessed from a missing table.
//! - **Exact**: `schema = "exact"` with fully resolved `[[members]]`. See
//! [`crate::fleet_exact`].
//!
//! [`FleetDocument`] is the form-agnostic entry point: it reports which form a
//! file is in and carries the content hash a Workflow snapshot records.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use crate::fleet_exact::{
EXACT_FLEET_SCHEMA_KIND, EXACT_FLEET_SCHEMA_REVISION, ExactFleet, ExactFleetError,
LEGACY_FLEET_SCHEMA_KIND, declared_schema_kind,
};
use crate::fleet_snapshot::QualifiedFleetId;
/// One labelled place fleet files are looked up.
///
/// The label is what makes a Fleet identity *qualified*: `workspace/glm-pair`
/// and `codewhale_home/glm-pair` are different Fleets, and the loader refuses
/// to guess between them for exact definitions.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FleetSearchRoot {
/// Non-secret origin label, e.g. `workspace` or `codewhale_home`.
pub origin: String,
/// Directory that contains a `fleets/` subdirectory.
pub root: PathBuf,
}
impl FleetSearchRoot {
pub fn new(origin: impl Into<String>, root: impl Into<PathBuf>) -> Self {
Self {
origin: origin.into(),
root: root.into(),
}
}
}
/// Split `origin/name` into its parts. A bare name yields `(None, name)`.
fn split_qualified_fleet_name(name: &str) -> (Option<&str>, &str) {
let trimmed = name.trim();
match trimmed.split_once('/') {
Some((origin, bare)) if !origin.trim().is_empty() && !bare.trim().is_empty() => {
(Some(origin.trim()), bare.trim())
}
_ => (None, trimmed),
}
}
/// Parsed named fleet file.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NamedFleet {
@@ -33,6 +83,288 @@ pub enum NamedFleetError {
MissingRole { fleet: String, role: String },
#[error("fleet name mismatch: file declares `{declared}`, expected `{expected}`")]
NameMismatch { declared: String, expected: String },
#[error(
"fleet `{name}` is defined in more than one place ({}); an exact fleet must not be \
resolved by shadowing. Name one explicitly as `origin/{name}`.",
origins.join(", ")
)]
AmbiguousFleet { name: String, origins: Vec<String> },
#[error("exact fleet `{fleet}`: {source}")]
Exact {
fleet: String,
#[source]
source: ExactFleetError,
},
}
/// Which form a `fleets/<name>.toml` file is in.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum FleetSchema {
/// Pre-exact role → AgentProfile id map.
Legacy(NamedFleet),
/// Fully resolved exact members.
Exact(ExactFleet),
}
/// A parsed fleet file plus the provenance a Workflow snapshot needs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FleetDocument {
schema: FleetSchema,
source: Option<PathBuf>,
source_hash: String,
}
impl FleetDocument {
/// Parse either form. The exact form is selected by an explicit
/// `schema = "exact"`; everything else is the legacy form.
pub fn parse(text: &str) -> Result<Self, NamedFleetError> {
let schema = match declared_schema_kind(text).as_deref() {
Some(EXACT_FLEET_SCHEMA_KIND) => {
let exact = ExactFleet::parse(text).map_err(|source| NamedFleetError::Exact {
fleet: "<memory>".to_string(),
source,
})?;
FleetSchema::Exact(exact)
}
Some(other) => {
return Err(NamedFleetError::Parse {
path: "<memory>".into(),
message: format!("unknown fleet schema `{other}`; expected `exact`"),
});
}
None => FleetSchema::Legacy(parse_named_fleet(text)?),
};
Ok(Self {
schema,
source: None,
source_hash: content_hash(text),
})
}
pub fn load(path: &Path, expect_name: Option<&str>) -> Result<Self, NamedFleetError> {
let text = std::fs::read_to_string(path).map_err(|e| NamedFleetError::Io {
path: path.display().to_string(),
message: e.to_string(),
})?;
let mut document = Self::parse(&text).map_err(|e| match e {
NamedFleetError::Parse { message, .. } => NamedFleetError::Parse {
path: path.display().to_string(),
message,
},
NamedFleetError::Exact { source, .. } => NamedFleetError::Exact {
fleet: path.display().to_string(),
source,
},
other => other,
})?;
if let Some(expected) = expect_name
&& document.name() != expected
{
return Err(NamedFleetError::NameMismatch {
declared: document.name().to_string(),
expected: expected.to_string(),
});
}
document.source = Some(path.to_path_buf());
Ok(document)
}
/// Load a fleet document by name from labelled search roots.
///
/// A bare `name` that exists under more than one origin is **ambiguous**
/// once any candidate is an exact fleet: a personal `~/.codewhale` Fleet
/// silently shadowing (or being shadowed by) a project Fleet would change
/// which exact provider/model actually runs, so the caller is asked for a
/// qualified `origin/name` instead. Purely legacy collisions keep the
/// historic first-hit-wins behavior, because a role→profile map resolves
/// through the same profile store either way.
///
/// Ambiguity is decided from a `schema`-key probe, not from a full parse of
/// every candidate: a malformed file in a *shadowed* origin must not break a
/// legacy load that has always worked. A file whose TOML does not even
/// parse therefore counts as legacy for this decision, and the first hit
/// still wins — the same outcome the pre-exact loader gave.
///
/// Accepts `origin/name` to name one origin explicitly.
pub fn load_by_name(
name: &str,
search_roots: &[FleetSearchRoot],
) -> Result<(Self, QualifiedFleetId), NamedFleetError> {
let (requested_origin, bare_name) = split_qualified_fleet_name(name);
let file_name = format!("{bare_name}.toml");
let mut candidates: Vec<(&FleetSearchRoot, PathBuf)> = Vec::new();
for root in search_roots {
if let Some(origin) = requested_origin
&& !root.origin.eq_ignore_ascii_case(origin)
{
continue;
}
let path = root.root.join("fleets").join(&file_name);
if path.is_file() {
candidates.push((root, path));
}
}
let Some((first_root, first_path)) = candidates.first() else {
return Err(NamedFleetError::NotFound(name.to_string()));
};
if candidates.len() > 1 {
// Decide ambiguity from the `schema` key alone — a cheap probe that
// does not parse the rest of the file. Fully parsing every sibling
// would mean a malformed *shadowed* file could fail a load that
// legacy first-hit-wins has always satisfied, which is a regression
// in a path the exact schema was never meant to touch. The
// ambiguity that actually matters is "one of these is exact", and
// the probe answers exactly that.
let mut any_exact = false;
for (_, path) in &candidates {
let text = std::fs::read_to_string(path).map_err(|e| NamedFleetError::Io {
path: path.display().to_string(),
message: e.to_string(),
})?;
if declared_schema_kind(&text).is_some() {
any_exact = true;
break;
}
}
if any_exact {
return Err(NamedFleetError::AmbiguousFleet {
name: bare_name.to_string(),
origins: candidates
.iter()
.map(|(root, path)| {
format!("{}/{bare_name} ({})", root.origin, path.display())
})
.collect(),
});
}
// Purely legacy collision: first hit wins, and only the first hit
// is parsed.
}
let document = Self::load(first_path, Some(bare_name))?;
Ok((
document,
QualifiedFleetId {
name: bare_name.to_string(),
origin: first_root.origin.clone(),
},
))
}
#[must_use]
pub fn name(&self) -> &str {
match &self.schema {
FleetSchema::Legacy(fleet) => &fleet.name,
FleetSchema::Exact(fleet) => &fleet.name,
}
}
#[must_use]
pub fn description(&self) -> Option<&str> {
match &self.schema {
FleetSchema::Legacy(fleet) => fleet.description.as_deref(),
FleetSchema::Exact(fleet) => fleet.description.as_deref(),
}
}
#[must_use]
pub fn schema(&self) -> &FleetSchema {
&self.schema
}
/// Explicit legacy detection — never inferred from a missing table.
#[must_use]
pub const fn is_legacy(&self) -> bool {
matches!(self.schema, FleetSchema::Legacy(_))
}
#[must_use]
pub fn legacy(&self) -> Option<&NamedFleet> {
match &self.schema {
FleetSchema::Legacy(fleet) => Some(fleet),
FleetSchema::Exact(_) => None,
}
}
#[must_use]
pub fn exact(&self) -> Option<&ExactFleet> {
match &self.schema {
FleetSchema::Exact(fleet) => Some(fleet),
FleetSchema::Legacy(_) => None,
}
}
#[must_use]
pub fn schema_kind(&self) -> &'static str {
match self.schema {
FleetSchema::Legacy(_) => LEGACY_FLEET_SCHEMA_KIND,
FleetSchema::Exact(_) => EXACT_FLEET_SCHEMA_KIND,
}
}
#[must_use]
pub fn schema_revision(&self) -> u32 {
match &self.schema {
// Legacy files carry no revision; report 0 so a snapshot can tell
// "pre-versioned" from exact revision 1.
FleetSchema::Legacy(_) => 0,
FleetSchema::Exact(fleet) => fleet.schema_revision,
}
}
/// SHA-256 of the exact file bytes this document was parsed from.
#[must_use]
pub fn source_hash(&self) -> &str {
&self.source_hash
}
#[must_use]
pub fn source_path(&self) -> Option<&Path> {
self.source.as_deref()
}
/// Build a document around an already-constructed exact roster.
///
/// Test-only, and deliberately so: it is how a roster that never passed
/// through the TOML parser reaches [`crate::FleetSnapshot::capture`], which
/// is exactly the bypass capture-time revalidation exists to close.
#[cfg(test)]
#[must_use]
pub(crate) fn from_exact_for_tests(exact: ExactFleet) -> Self {
Self {
schema: FleetSchema::Exact(exact),
source: None,
source_hash: content_hash("<constructed>"),
}
}
}
/// The schema revision an exact document is expected to declare.
#[must_use]
pub const fn exact_schema_revision() -> u32 {
EXACT_FLEET_SCHEMA_REVISION
}
pub(crate) fn content_hash(text: &str) -> String {
sha256_label(text.as_bytes())
}
/// `sha256:<hex>` over arbitrary bytes. Mirrors the hex helper in `replay.rs`
/// rather than relying on a digest `LowerHex` impl.
pub(crate) fn sha256_label(bytes: &[u8]) -> String {
use std::fmt::Write as _;
let digest = Sha256::digest(bytes);
let mut out = String::with_capacity(7 + digest.len() * 2);
out.push_str("sha256:");
for byte in digest.iter() {
let _ = write!(&mut out, "{byte:02x}");
}
out
}
/// Required roles for the stopship dogfood fleet (#4178).
@@ -275,6 +607,75 @@ scout = "scout#stable"
);
}
#[test]
fn legacy_fleet_files_still_deserialize_and_resolve_through_the_document_api() {
let document = FleetDocument::parse(STOPSHIP_TOML).expect("legacy parse");
// Legacy is explicitly detectable, not inferred.
assert!(document.is_legacy());
assert_eq!(document.schema_kind(), "legacy");
assert_eq!(document.schema_revision(), 0);
assert!(document.exact().is_none());
let legacy = document.legacy().expect("legacy body");
legacy.validate_stopship_roles().expect("all roles");
assert_eq!(legacy.resolve("implementer").unwrap(), "builder");
assert_eq!(document.name(), "stopship");
assert!(document.source_hash().starts_with("sha256:"));
}
#[test]
fn exact_fleet_files_are_selected_by_an_explicit_schema_key() {
let document = FleetDocument::parse(
r#"
name = "glm-pair"
schema = "exact"
[[members]]
id = "implementer"
provider = "zai"
model = "glm-5"
reasoning = "auto"
[[members]]
id = "router"
kind = "router"
provider = "zai"
model = "glm-5-turbo"
"#,
)
.expect("exact parse");
assert!(!document.is_legacy());
assert_eq!(document.schema_kind(), "exact");
assert_eq!(document.schema_revision(), exact_schema_revision());
assert!(document.legacy().is_none());
let exact = document.exact().expect("exact body");
assert!(exact.has_auto_member());
// The prototype inline form still parses, and is reported as the legacy
// inline router rather than as a second runtime concept.
assert!(exact.legacy_inline_router().is_some());
assert!(exact.router_ref().is_some());
}
#[test]
fn an_unknown_schema_key_fails_instead_of_falling_back_to_legacy() {
let err = FleetDocument::parse("name = \"f\"\nschema = \"experimental\"\n")
.expect_err("unknown schema must not silently parse as legacy");
assert!(matches!(err, NamedFleetError::Parse { .. }), "{err:?}");
}
#[test]
fn document_hash_follows_the_file_bytes() {
let a = FleetDocument::parse(STOPSHIP_TOML).expect("parse");
let b = FleetDocument::parse(STOPSHIP_TOML).expect("parse");
let c = FleetDocument::parse(&STOPSHIP_TOML.replace("builder", "implementer_profile"))
.expect("parse");
assert_eq!(a.source_hash(), b.source_hash());
assert_ne!(a.source_hash(), c.source_hash());
}
#[test]
fn loads_workspace_fleet_file() {
// Relative to crate CARGO_MANIFEST_DIR → repo root fleets/
+656
View File
@@ -0,0 +1,656 @@
//! The **Adaptive Reasoning Router** — a saved, reusable *service*, not a Fleet
//! member.
//!
//! A Fleet answers *who*: the exact provider/model assignments an operator
//! saved for each worker. A Reasoning Router answers a much smaller question,
//! for an already frozen worker route: *how hard should this already-chosen
//! model think on this task?* Those are different kinds of thing, so they are
//! different kinds of value here:
//!
//! - A Router is **never dispatchable**. It has no role, no tools, no shell, no
//! write authority, and no delegation budget. It cannot be named by a task.
//! - A Router is **referenced, not embedded**. It is saved once at
//! `routers/<name>.toml` and referenced by name from any number of Fleets, so
//! two Fleets can share one Router configuration without duplicating it.
//! - A Router **never changes a route**. Provider, model, member, role, tools,
//! and permissions are all frozen before it is called and are not among the
//! things it is allowed to answer.
//!
//! ## Cheap by construction
//!
//! A Router call is a per-task tax on someone's tokens, so its own reasoning is
//! capped at [`RouterCallReasoning`] — `off` or `low`, nothing else. `medium`,
//! `high`, and `max` are **rejected at parse time rather than silently clamped**:
//! an operator who wrote `high` asked for something this service will not do,
//! and quietly running at `off` while the file says `high` is exactly the kind
//! of invisible substitution receipts exist to prevent.
//!
//! The reverse lie is equally forbidden. A profile that asks for `low` is
//! *called* at `low` wherever the route can express it; nothing here forces
//! `off` and then reports `low`. Normalization against the route's real
//! capability is recorded on the receipt (see
//! [`crate::fleet_reasoning::RouterCallDisclosure`]).
//!
//! ## Legacy inline routers
//!
//! The prototype form — a `[[members]]` entry with `kind = "router"` inside the
//! Fleet file — still parses, is labelled `legacy_inline`, and is **normalized
//! into the same [`CapturedReasoningRouter`]** the named store produces. There
//! is one runtime representation of the service, whichever way it was written.
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::fleet_exact::{FrozenRoute, PermissionCeiling, ReasoningTier, RouterMember};
use crate::named_fleet::FleetSearchRoot;
/// Directory (under each search root) that holds saved Router profiles.
pub const REASONING_ROUTER_DIR: &str = "routers";
/// Wire value of the `schema` key that selects a Router profile document.
pub const REASONING_ROUTER_SCHEMA_KIND: &str = "reasoning_router";
/// Current revision of the Router profile schema.
pub const REASONING_ROUTER_SCHEMA_REVISION: u32 = 1;
/// Stable service label a receipt prints so the reader can tell at a glance
/// that this is the reasoning service and not a Fleet member.
pub const REASONING_ROUTER_SERVICE_KIND: &str = "reasoning_router";
/// Origin recorded for a Router that was written inline in a Fleet file.
pub const LEGACY_INLINE_ROUTER_ORIGIN: &str = "legacy_inline";
/// The reasoning a **Router call itself** may run at.
///
/// Deliberately not [`ReasoningTier`]: this type exists precisely so that
/// `medium`/`high`/`max` are unrepresentable. A Router emits one ~15-byte JSON
/// object; anything above `low` spends a user's tokens on thinking about a
/// question that does not need thought.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RouterCallReasoning {
#[default]
Off,
Low,
}
impl RouterCallReasoning {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Off => "off",
Self::Low => "low",
}
}
/// The concrete tier this maps onto for capability normalization.
#[must_use]
pub const fn tier(self) -> ReasoningTier {
match self {
Self::Off => ReasoningTier::Off,
Self::Low => ReasoningTier::Low,
}
}
/// Parse a configured value.
///
/// `medium`/`high`/`max` are a distinct, named error rather than a clamp —
/// see the module docs.
pub fn parse(value: &str, router: &str) -> Result<Self, ReasoningRouterError> {
let trimmed = value.trim();
match trimmed.to_ascii_lowercase().as_str() {
"off" | "none" | "disabled" => Ok(Self::Off),
"low" | "minimal" => Ok(Self::Low),
"medium" | "mid" | "high" | "max" | "maximum" | "xhigh" => {
Err(ReasoningRouterError::CallReasoningTooExpensive {
router: router.to_string(),
value: trimmed.to_string(),
})
}
_ => Err(ReasoningRouterError::InvalidCallReasoning {
router: router.to_string(),
value: trimmed.to_string(),
}),
}
}
}
/// A Router identity qualified by the origin its definition came from.
///
/// Path-free for the same reason [`crate::QualifiedFleetId`] is: an absolute
/// path in a durable receipt leaks the operator's home directory.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QualifiedRouterId {
pub name: String,
pub origin: String,
}
impl QualifiedRouterId {
#[must_use]
pub fn qualified(&self) -> String {
format!("{}/{}", self.origin, self.name)
}
}
/// A saved Router profile: one exact provider/model plus a cheap call ceiling.
///
/// This is *the* reusable unit. Any number of Fleets may reference the same
/// profile by name; none of them owns it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReasoningRouterProfile {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub schema_revision: u32,
/// Exact configured provider id.
pub provider: String,
/// Exact model id.
pub model: String,
/// What the Router's own call runs at. `off` or `low` only.
pub call_reasoning: RouterCallReasoning,
}
impl ReasoningRouterProfile {
/// Parse a Router profile document.
pub fn parse(text: &str) -> Result<Self, ReasoningRouterError> {
let doc: RouterProfileToml =
toml::from_str(text).map_err(|error| ReasoningRouterError::Parse(error.to_string()))?;
if !doc
.schema
.trim()
.eq_ignore_ascii_case(REASONING_ROUTER_SCHEMA_KIND)
{
return Err(ReasoningRouterError::UnknownSchema {
schema: doc.schema.trim().to_string(),
});
}
if doc.schema_revision != REASONING_ROUTER_SCHEMA_REVISION {
return Err(ReasoningRouterError::UnsupportedRevision {
revision: doc.schema_revision,
supported: REASONING_ROUTER_SCHEMA_REVISION,
});
}
let name = crate::role_resolve::normalize_token(&doc.name).ok_or_else(|| {
ReasoningRouterError::InvalidToken {
field: "name".to_string(),
value: doc.name.trim().to_string(),
}
})?;
let provider = exact_token(&doc.provider, &name, "provider")?;
let model = exact_token(&doc.model, &name, "model")?;
let call_reasoning = match doc.call_reasoning.as_deref() {
None => RouterCallReasoning::default(),
Some(value) => RouterCallReasoning::parse(value, &name)?,
};
Ok(Self {
name,
description: doc.description,
schema_revision: doc.schema_revision,
provider,
model,
call_reasoning,
})
}
/// Load one profile from a labelled search root set.
///
/// A bare name present under more than one origin is **ambiguous**: a
/// personal `~/.codewhale` Router silently shadowing a project Router would
/// change which provider sees every task's routing summary. Naming the
/// origin (`codewhale_home/fast`) resolves it.
pub fn load_by_name(
name: &str,
search_roots: &[FleetSearchRoot],
) -> Result<(Self, QualifiedRouterId), ReasoningRouterError> {
let (requested_origin, bare) = split_qualified(name);
if bare.is_empty() {
return Err(ReasoningRouterError::InvalidToken {
field: "router reference".to_string(),
value: name.trim().to_string(),
});
}
let file_name = format!("{bare}.toml");
let mut candidates: Vec<(&FleetSearchRoot, PathBuf)> = Vec::new();
for root in search_roots {
if let Some(origin) = requested_origin
&& !root.origin.eq_ignore_ascii_case(origin)
{
continue;
}
let path = root.root.join(REASONING_ROUTER_DIR).join(&file_name);
if path.is_file() {
candidates.push((root, path));
}
}
let Some((first_root, first_path)) = candidates.first() else {
return Err(ReasoningRouterError::NotFound {
name: name.trim().to_string(),
});
};
if candidates.len() > 1 {
// Unlike legacy Fleet role maps, there is no first-hit-wins fallback
// here: every Router profile names an exact provider/model, so a
// shadowed one always changes behavior.
return Err(ReasoningRouterError::AmbiguousRouter {
name: bare.to_string(),
origins: candidates
.iter()
.map(|(root, _)| format!("{}/{bare}", root.origin))
.collect(),
});
}
let text =
std::fs::read_to_string(first_path).map_err(|error| ReasoningRouterError::Io {
path: first_path.display().to_string(),
message: error.to_string(),
})?;
let profile = Self::parse(&text)?;
if profile.name != bare {
return Err(ReasoningRouterError::NameMismatch {
declared: profile.name.clone(),
expected: bare.to_string(),
});
}
let id = QualifiedRouterId {
name: profile.name.clone(),
origin: first_root.origin.clone(),
};
Ok((profile, id))
}
}
/// The Router service as frozen into a Workflow snapshot.
///
/// Whether it came from a named profile or from the legacy inline member, this
/// is the single runtime representation. Nothing downstream branches on which
/// form the operator wrote.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapturedReasoningRouter {
/// Always [`REASONING_ROUTER_SERVICE_KIND`]. Recorded explicitly so a
/// receipt states what kind of thing this is instead of implying it.
#[serde(default = "default_service_kind")]
pub service_kind: String,
/// The Router's id: the profile name, or the inline member's id.
pub id: String,
/// The origin the definition came from, or [`LEGACY_INLINE_ROUTER_ORIGIN`].
#[serde(default = "default_router_origin")]
pub origin: String,
/// True when this was written inline in the Fleet file rather than saved as
/// a reusable profile.
#[serde(default)]
pub legacy_inline: bool,
/// The Router's own exact provider/model.
pub route: FrozenRoute,
/// What the operator configured this Router's call to run at.
#[serde(default)]
pub requested_call_reasoning: RouterCallReasoning,
/// Always `false`. Stated rather than implied.
#[serde(default)]
pub dispatchable: bool,
/// Always [`PermissionCeiling::ROUTER`].
#[serde(default = "router_permissions")]
pub permissions: PermissionCeiling,
}
fn default_service_kind() -> String {
REASONING_ROUTER_SERVICE_KIND.to_string()
}
fn default_router_origin() -> String {
LEGACY_INLINE_ROUTER_ORIGIN.to_string()
}
fn router_permissions() -> PermissionCeiling {
PermissionCeiling::ROUTER
}
impl CapturedReasoningRouter {
/// Capture a saved, reusable profile.
#[must_use]
pub fn from_profile(profile: &ReasoningRouterProfile, origin: impl Into<String>) -> Self {
Self {
service_kind: default_service_kind(),
id: profile.name.clone(),
origin: origin.into(),
legacy_inline: false,
route: FrozenRoute {
provider: profile.provider.clone(),
model: profile.model.clone(),
},
requested_call_reasoning: profile.call_reasoning,
dispatchable: false,
permissions: PermissionCeiling::ROUTER,
}
}
/// Normalize the prototype inline form into the same captured service.
///
/// The inline member's own `reasoning` was a full [`ReasoningTier`]; it is
/// mapped onto the cheap call ceiling here, and anything above `low` is
/// rejected by the Fleet parser rather than clamped silently.
#[must_use]
pub fn from_legacy_inline(member: &RouterMember) -> Self {
Self {
service_kind: default_service_kind(),
id: member.id.clone(),
origin: default_router_origin(),
legacy_inline: true,
route: member.frozen_route(),
requested_call_reasoning: member.call_reasoning,
dispatchable: false,
permissions: PermissionCeiling::ROUTER,
}
}
/// `origin/id` — the stable display form a receipt prints.
#[must_use]
pub fn qualified(&self) -> String {
format!("{}/{}", self.origin, self.id)
}
/// A Router is never a worker. Constant, not a policy lookup.
#[must_use]
pub const fn is_dispatchable(&self) -> bool {
false
}
/// The Router's tool surface is empty, always.
#[must_use]
pub const fn tool_surface(&self) -> &'static [&'static str] {
&[]
}
}
/// How a Fleet points at its Router.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum FleetRouterRef {
/// A saved, reusable profile named by `reasoning_router = "<name>"`.
Profile { name: String },
/// The prototype inline `[[members]] kind = "router"` form.
LegacyInline(Box<RouterMember>),
}
fn split_qualified(name: &str) -> (Option<&str>, &str) {
let trimmed = name.trim();
match trimmed.split_once('/') {
Some((origin, bare)) if !origin.trim().is_empty() && !bare.trim().is_empty() => {
(Some(origin.trim()), bare.trim())
}
_ => (None, trimmed),
}
}
fn exact_token(value: &str, router: &str, field: &str) -> Result<String, ReasoningRouterError> {
let trimmed = value.trim();
if trimmed.is_empty()
|| trimmed
.chars()
.any(|ch| ch.is_whitespace() || matches!(ch, '"' | '\'' | '`' | '='))
{
return Err(ReasoningRouterError::InvalidToken {
field: format!("{router}.{field}"),
value: trimmed.to_string(),
});
}
Ok(trimmed.to_string())
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RouterProfileToml {
name: String,
#[serde(default)]
description: Option<String>,
schema: String,
#[serde(default = "default_router_revision")]
schema_revision: u32,
provider: String,
model: String,
#[serde(default, alias = "reasoning")]
call_reasoning: Option<String>,
}
const fn default_router_revision() -> u32 {
REASONING_ROUTER_SCHEMA_REVISION
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ReasoningRouterError {
#[error("failed to parse reasoning router profile: {0}")]
Parse(String),
#[error("failed to read reasoning router profile `{path}`: {message}")]
Io { path: String, message: String },
#[error("reasoning router `{name}` was not found in any configured origin")]
NotFound { name: String },
#[error(
"reasoning router `{name}` is defined in more than one place ({}); a router names an \
exact provider/model, so shadowing would silently change which provider sees every \
routing summary. Name one explicitly as `origin/{name}`.",
origins.join(", ")
)]
AmbiguousRouter { name: String, origins: Vec<String> },
#[error("unknown reasoning router schema `{schema}`; expected `reasoning_router`")]
UnknownSchema { schema: String },
#[error(
"reasoning router schema revision {revision} is not supported (this build reads {supported})"
)]
UnsupportedRevision { revision: u32, supported: u32 },
#[error("{field} must be a non-empty token without whitespace, quotes, or `=` (got `{value}`)")]
InvalidToken { field: String, value: String },
#[error("reasoning router name mismatch: file declares `{declared}`, expected `{expected}`")]
NameMismatch { declared: String, expected: String },
#[error(
"reasoning router `{router}` requests call reasoning `{value}`; a router may only run at \
`off` or `low`. This is rejected rather than clamped: a router answers one tiny JSON \
object per task, and running it at `{value}` would spend your tokens on thinking nobody \
asked for. Set `call_reasoning` to `off` or `low`."
)]
CallReasoningTooExpensive { router: String, value: String },
#[error(
"reasoning router `{router}` has invalid call reasoning `{value}`; expected off or low"
)]
InvalidCallReasoning { router: String, value: String },
}
#[cfg(test)]
mod tests {
use super::*;
const LUNA: &str = r#"
name = "luna-low"
description = "GPT-5.6 Luna, called at low"
schema = "reasoning_router"
schema_revision = 1
provider = "openai"
model = "gpt-5.6-luna"
call_reasoning = "low"
"#;
#[test]
fn a_profile_parses_its_exact_route_and_cheap_call_tier() {
let profile = ReasoningRouterProfile::parse(LUNA).expect("parse");
assert_eq!(profile.name, "luna-low");
assert_eq!(profile.provider, "openai");
assert_eq!(profile.model, "gpt-5.6-luna");
assert_eq!(profile.call_reasoning, RouterCallReasoning::Low);
assert_eq!(profile.schema_revision, REASONING_ROUTER_SCHEMA_REVISION);
}
#[test]
fn call_reasoning_defaults_to_off_when_unset() {
let text = LUNA.replace("call_reasoning = \"low\"\n", "");
let profile = ReasoningRouterProfile::parse(&text).expect("parse");
assert_eq!(profile.call_reasoning, RouterCallReasoning::Off);
}
/// The whole point of the cheap ceiling: an expensive tier is an error the
/// operator can see, never a clamp they cannot.
#[test]
fn medium_high_and_max_are_rejected_not_clamped() {
for value in ["medium", "high", "max", "xhigh"] {
let text = LUNA.replace("\"low\"", &format!("\"{value}\""));
let err = ReasoningRouterProfile::parse(&text)
.expect_err("an expensive router tier must be rejected");
assert!(
matches!(err, ReasoningRouterError::CallReasoningTooExpensive { .. }),
"value={value} err={err:?}"
);
let message = err.to_string();
assert!(message.contains("off"), "{message}");
assert!(message.contains("low"), "{message}");
assert!(
!message.contains("clamped to"),
"the error must not describe a clamp: {message}"
);
}
}
#[test]
fn an_unknown_tier_is_its_own_error() {
let text = LUNA.replace("\"low\"", "\"turbo\"");
assert!(matches!(
ReasoningRouterProfile::parse(&text).expect_err("garbage"),
ReasoningRouterError::InvalidCallReasoning { .. }
));
}
#[test]
fn an_unknown_schema_or_future_revision_fails_closed() {
let wrong_schema = LUNA.replace("\"reasoning_router\"", "\"exact\"");
assert!(matches!(
ReasoningRouterProfile::parse(&wrong_schema).expect_err("schema"),
ReasoningRouterError::UnknownSchema { .. }
));
let future = LUNA.replace("schema_revision = 1", "schema_revision = 99");
assert!(matches!(
ReasoningRouterProfile::parse(&future).expect_err("revision"),
ReasoningRouterError::UnsupportedRevision { revision: 99, .. }
));
}
#[test]
fn a_captured_profile_holds_no_authority_and_is_never_dispatchable() {
let profile = ReasoningRouterProfile::parse(LUNA).expect("parse");
let captured = CapturedReasoningRouter::from_profile(&profile, "workspace");
assert_eq!(captured.qualified(), "workspace/luna-low");
assert_eq!(captured.service_kind, REASONING_ROUTER_SERVICE_KIND);
assert!(!captured.legacy_inline);
assert!(!captured.is_dispatchable());
assert!(captured.tool_surface().is_empty());
assert!(!captured.permissions.tools);
assert!(!captured.permissions.write);
assert!(!captured.permissions.network_tool);
assert_eq!(captured.permissions.delegation_depth, 0);
}
/// One saved profile, two Fleets. The service is referenced, not owned.
#[test]
fn one_saved_profile_serves_more_than_one_fleet() {
let tmp = tempfile::tempdir().expect("tmp");
std::fs::create_dir_all(tmp.path().join(REASONING_ROUTER_DIR)).expect("dir");
std::fs::write(
tmp.path().join(REASONING_ROUTER_DIR).join("luna-low.toml"),
LUNA,
)
.expect("write");
let roots = vec![FleetSearchRoot::new("workspace", tmp.path())];
let (first, first_id) =
ReasoningRouterProfile::load_by_name("luna-low", &roots).expect("load");
let (second, second_id) =
ReasoningRouterProfile::load_by_name("workspace/luna-low", &roots).expect("qualified");
assert_eq!(first, second);
assert_eq!(first_id, second_id);
assert_eq!(first_id.qualified(), "workspace/luna-low");
// Two independent captures of the same saved service agree exactly.
let a = CapturedReasoningRouter::from_profile(&first, &first_id.origin);
let b = CapturedReasoningRouter::from_profile(&second, &second_id.origin);
assert_eq!(a, b);
}
/// Bare-name ambiguity across origins must fail; a qualified origin works.
#[test]
fn a_bare_name_defined_in_two_origins_is_ambiguous_until_qualified() {
let tmp = tempfile::tempdir().expect("tmp");
let home = tmp.path().join("home");
let workspace = tmp.path().join("workspace");
for root in [&home, &workspace] {
std::fs::create_dir_all(root.join(REASONING_ROUTER_DIR)).expect("dir");
}
std::fs::write(
home.join(REASONING_ROUTER_DIR).join("luna-low.toml"),
LUNA.replace("gpt-5.6-luna", "gpt-5.6-luna-mini"),
)
.expect("home");
std::fs::write(
workspace.join(REASONING_ROUTER_DIR).join("luna-low.toml"),
LUNA,
)
.expect("workspace");
let roots = vec![
FleetSearchRoot::new("codewhale_home", &home),
FleetSearchRoot::new("workspace", &workspace),
];
let err = ReasoningRouterProfile::load_by_name("luna-low", &roots)
.expect_err("bare name must not be resolved by shadowing");
assert!(
matches!(err, ReasoningRouterError::AmbiguousRouter { .. }),
"{err:?}"
);
let message = err.to_string();
assert!(message.contains("codewhale_home"), "{message}");
assert!(message.contains("workspace"), "{message}");
let (workspace_profile, id) =
ReasoningRouterProfile::load_by_name("workspace/luna-low", &roots).expect("qualified");
assert_eq!(id.qualified(), "workspace/luna-low");
assert_eq!(workspace_profile.model, "gpt-5.6-luna");
let (home_profile, home_id) =
ReasoningRouterProfile::load_by_name("codewhale_home/luna-low", &roots)
.expect("qualified");
assert_eq!(home_id.qualified(), "codewhale_home/luna-low");
assert_eq!(home_profile.model, "gpt-5.6-luna-mini");
}
#[test]
fn a_missing_profile_is_a_named_error() {
let tmp = tempfile::tempdir().expect("tmp");
let roots = vec![FleetSearchRoot::new("workspace", tmp.path())];
assert!(matches!(
ReasoningRouterProfile::load_by_name("nope", &roots).expect_err("missing"),
ReasoningRouterError::NotFound { .. }
));
}
/// A captured router serializes into a durable snapshot with no secrets and
/// no paths, and older records without the newer fields still read.
#[test]
fn a_captured_router_is_durable_and_backward_compatible() {
let profile = ReasoningRouterProfile::parse(LUNA).expect("parse");
let captured = CapturedReasoningRouter::from_profile(&profile, "workspace");
let json = serde_json::to_string(&captured).expect("serialize");
let lowered = json.to_ascii_lowercase();
for forbidden in ["api_key", "secret", "token", "bearer", "/users/", ".toml"] {
assert!(!lowered.contains(forbidden), "{forbidden} in {json}");
}
let back: CapturedReasoningRouter = serde_json::from_str(&json).expect("round-trip");
assert_eq!(back, captured);
let older = r#"{"id":"router","route":{"provider":"zai","model":"glm-5-turbo"}}"#;
let legacy: CapturedReasoningRouter = serde_json::from_str(older).expect("serde defaults");
assert_eq!(legacy.service_kind, REASONING_ROUTER_SERVICE_KIND);
assert_eq!(legacy.origin, LEGACY_INLINE_ROUTER_ORIGIN);
assert_eq!(legacy.requested_call_reasoning, RouterCallReasoning::Off);
assert!(!legacy.dispatchable);
}
}
+922
View File
@@ -0,0 +1,922 @@
//! Redaction for anything that can reach a log, a span, or a durable receipt.
//!
//! Two classes of content are stripped before text is allowed to leave the
//! process — either onto a provider's wire or into a journal:
//!
//! 1. **Absolute paths.** `/Users/hunter/src/app`, `/home/x/...`, `C:\Users\…`
//! and `~/…` carry the operator's username, home directory, and machine
//! layout. A journal travels further than the machine that wrote it.
//! 2. **Repo-relative paths.** `crates/tui/src/main.rs`, `./deploy.sh`,
//! `../../secret/notes.md` — and their escaped spellings, `crates\/tui\/…`
//! and `crates\\tui\\…`. An absolute path discloses the machine; a relative
//! one discloses the private tree's shape, which is exactly as much as the
//! reader of a routing summary at another provider needs to reconstruct it.
//! The rule is deliberately conservative — see [`looks_relative`] — because
//! the failure it must not trade for is mangling ordinary prose or a
//! `provider/model` label.
//! 3. **Secret-shaped tokens.** Provider keys, bearer tokens, and
//! `SOMETHING_KEY=value` assignments. These have no business in a routing
//! summary and must never be persisted next to one.
//!
//! Redaction is *recorded*, not silent: [`Redaction::kinds`] names what was
//! removed so a receipt can disclose the fact without disclosing the content.
use std::collections::BTreeSet;
/// A redaction kind, as it appears on a disclosure. These are stable labels —
/// receipts persist them.
pub const REDACTION_ABSOLUTE_PATH: &str = "absolute_path";
pub const REDACTION_RELATIVE_PATH: &str = "relative_path";
pub const REDACTION_SECRET: &str = "secret";
/// Placeholder substituted for a removed absolute path.
const PATH_PLACEHOLDER: &str = "<path>";
/// Placeholder substituted for a removed secret-shaped token.
const SECRET_PLACEHOLDER: &str = "<redacted>";
/// Case-insensitive substrings that mark an identifier as secret-bearing.
const SECRET_NAME_MARKERS: &[&str] = &[
"api_key",
"apikey",
"secret",
"token",
"password",
"passwd",
"credential",
"authorization",
"auth_token",
"access_key",
"private_key",
"session_key",
];
/// Prefixes that are themselves a credential, whatever they are attached to.
///
/// Every entry here must be *unambiguously* a credential prefix. A prefix that
/// is also an ordinary English word — `bearer`, `asia` — belongs nowhere near
/// this list: it would redact prose and, worse, would report a `secret`
/// redaction kind on a receipt that removed nothing but a word. The AWS key ids
/// that motivated `asia`/`akia` are handled by
/// [`looks_like_aws_access_key`], which requires the full shape.
const SECRET_VALUE_PREFIXES: &[&str] = &[
"sk-",
"sk_",
"ghp_",
"gho_",
"ghs_",
"github_pat_",
"xoxb-",
"xoxp-",
"xapp-",
];
/// HTTP authorization scheme keywords, in their canonical HTTP capitalization.
///
/// These are *never* the secret — the secret is the token that follows them.
/// Redacting the keyword and stopping there is the failure this list exists to
/// prevent: `Authorization: Bearer <token>` would keep the token and still
/// claim on the receipt that a secret had been removed.
///
/// The capitalization is stored, not normalized away, because it carries
/// evidence: `Bearer` is HTTP syntax and `bearer` is an English noun. See
/// [`is_canonical_auth_scheme`].
const AUTH_SCHEMES: &[&str] = &["Bearer", "Basic", "Digest", "Token"];
/// The one scheme keyword whose canonical capitalization is, by itself, enough
/// to treat the following token as a credential.
///
/// The asymmetry is deliberate and is the whole of the false-positive story.
/// `Basic`, `Digest`, and `Token` are ordinary capitalized English words —
/// "Basic auth is enabled", "Token holders vote", "Digest the results" — and
/// arming on them would redact the next word of perfectly ordinary prose. `Bearer`
/// capitalized is, in a technical corpus, the HTTP scheme essentially every
/// time. So `Bearer qqq` loses `qqq` even though a three-letter lowercase token
/// looks like nothing at all, while the other three need header context first.
///
/// The residual cost is stated plainly: `Bearer tokens are rotated weekly`
/// redacts `tokens`. That is a capitalized-`Bearer` sentence, which is header
/// syntax by shape; ordinary prose says "bearer", and lowercase never arms on
/// its own.
const SELF_EVIDENT_AUTH_SCHEME: &str = "Bearer";
/// AWS access-key-id prefixes. Matched case-sensitively and only against the
/// full 20-character shape, so the word `Asia` is prose and `ASIA…` is a key.
const AWS_KEY_ID_PREFIXES: &[&str] = &[
"AKIA", "ASIA", "AGPA", "AIDA", "AROA", "ANPA", "ANVA", "ASCA", "ABIA", "ACCA",
];
/// Minimum length of an AWS access key id (`AKIA` + 16).
const AWS_KEY_ID_LEN: usize = 20;
/// Minimum length before a bare token is treated as a credential value.
const CREDENTIAL_VALUE_MIN_LEN: usize = 16;
/// The result of redacting one string.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Redaction {
text: String,
kinds: BTreeSet<String>,
}
impl Redaction {
/// The redacted text.
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
/// Consume the redaction, yielding the redacted text.
#[must_use]
pub fn into_text(self) -> String {
self.text
}
/// Whether anything was removed.
#[must_use]
pub fn redacted(&self) -> bool {
!self.kinds.is_empty()
}
/// Which classes of content were removed — never the content itself.
#[must_use]
pub fn kinds(&self) -> Vec<String> {
self.kinds.iter().cloned().collect()
}
}
/// How strongly the preceding tokens claim that the *next* token is a
/// credential.
///
/// A credential is routinely written as two or three tokens
/// (`Authorization: Bearer <token>`), so the decision cannot be made per token.
/// But the evidence for "a credential follows" is not uniform, and collapsing it
/// to a boolean is what produces either a leak or a mangled sentence:
///
/// - `Authorization: Bearer qqq` — the credential is `qqq`, three lowercase
/// letters, indistinguishable in shape from a word. Only the *context* says
/// it is a secret, and the context says so unambiguously.
/// - `authorization: bearer shares responsibility` — the same context markers,
/// lowercase, and the next token is an English verb. Redacting `shares` would
/// destroy a sentence and put a false `secret` kind on a receipt.
///
/// So the arming carries its own strength, and the shape test is applied only
/// where the context is weak enough to need it.
#[derive(Clone, Copy, PartialEq, Eq)]
enum CredentialArm {
/// Nothing is expected; the next token is judged on its own.
None,
/// The next token is the credential only if it also *looks* like one.
/// Lowercase scheme words and bare `Authorization:` land here.
IfShaped,
/// The next token is the credential whatever it looks like. Reached by
/// canonical HTTP capitalization — `Authorization: Bearer …`, or a bare
/// `Bearer` — where the syntax alone settles it.
Certain,
}
impl CredentialArm {
const fn is_armed(self) -> bool {
!matches!(self, Self::None)
}
}
/// What one token resolved to, and whether it armed the *next* token.
struct TokenOutcome {
/// `None` keeps the token verbatim.
text: Option<String>,
/// Whether — and how strongly — the following token carries the credential
/// this one introduced. `Authorization:` and a bare `Bearer` reveal nothing
/// themselves; the value after them is the whole secret.
arm: CredentialArm,
}
impl TokenOutcome {
const fn keep() -> Self {
Self {
text: None,
arm: CredentialArm::None,
}
}
const fn keep_and_arm(arm: CredentialArm) -> Self {
Self { text: None, arm }
}
fn replace(text: String) -> Self {
Self {
text: Some(text),
arm: CredentialArm::None,
}
}
}
/// Redact absolute paths and secret-shaped tokens from `input`.
///
/// Operates token-by-token over whitespace-free runs, which is enough for the
/// bounded, already whitespace-collapsed text this crate transmits and keeps
/// the rule set auditable without a regex dependency.
///
/// One piece of state crosses token boundaries, and it has to: a credential is
/// routinely written as *two* tokens (`Authorization: Bearer <token>`), so a
/// purely per-token rule either leaks the value or redacts the English word
/// `bearer`. Carrying "the next token is the credential" forward — and *how
/// certainly*, see [`CredentialArm`] — is what lets this do neither.
#[must_use]
pub fn redact_for_disclosure(input: &str) -> Redaction {
let mut kinds = BTreeSet::new();
let tokens: Vec<&str> = input.split(' ').collect();
let mut out: Vec<String> = Vec::with_capacity(tokens.len());
let mut arm = CredentialArm::None;
for (index, token) in tokens.iter().enumerate() {
if token.is_empty() {
out.push(String::new());
continue;
}
if arm.is_armed() {
// `Authorization: Bearer <token>` — the scheme keyword is not the
// secret, so it survives and the arming carries past it. A
// canonically capitalized keyword also *upgrades* the arming: the
// header name alone left the shape in doubt, and `Bearer` settles
// it.
if is_auth_scheme(token) {
if is_canonical_auth_scheme(token) {
arm = CredentialArm::Certain;
}
out.push((*token).to_string());
continue;
}
// Weak arming still defers to shape, so an `authorization:` that
// introduces a sentence rather than a secret leaves the sentence
// intact — and leaves the receipt honest about having removed
// nothing.
if arm == CredentialArm::Certain || looks_like_credential_value(token) {
kinds.insert(REDACTION_SECRET.to_string());
out.push(SECRET_PLACEHOLDER.to_string());
arm = CredentialArm::None;
continue;
}
// Fall through: the token is judged on its own merits, and the
// arming state is replaced wholesale by `redact_token` below.
}
let next = tokens[index + 1..]
.iter()
.copied()
.find(|candidate| !candidate.is_empty());
let outcome = redact_token(token, next, &mut kinds);
arm = outcome.arm;
out.push(outcome.text.unwrap_or_else(|| (*token).to_string()));
}
Redaction {
text: out.join(" "),
kinds,
}
}
/// Redact one whitespace-free token, recording what was removed.
///
/// `next` is the following non-empty token, used only to decide whether a bare
/// authorization scheme keyword is introducing a credential or is just a word.
fn redact_token(token: &str, next: Option<&str>, kinds: &mut BTreeSet<String>) -> TokenOutcome {
// `NAME=value` / `NAME:value` — a secret-bearing name redacts its value and
// keeps the name, which is the useful half.
for separator in ['=', ':'] {
let Some((name, value)) = token.split_once(separator) else {
continue;
};
let lowered = name.to_ascii_lowercase();
let secret_name = SECRET_NAME_MARKERS
.iter()
.any(|marker| lowered.contains(marker));
if secret_name {
// `Authorization:Bearer` carries no secret of its own; the
// credential is the next token. Canonical capitalization on the
// scheme makes that certain; `authorization:bearer` does not.
if is_auth_scheme(value) {
return TokenOutcome::keep_and_arm(if is_canonical_auth_scheme(value) {
CredentialArm::Certain
} else {
CredentialArm::IfShaped
});
}
// A bare `Authorization:` only introduces a credential when one
// actually follows. `authorization: needed before merge` is a
// sentence, and redacting `needed` would report a secret that was
// never there. The arming stays weak here even when a scheme word
// follows — the scheme token itself decides, on the next pass,
// whether its capitalization upgrades it.
if value.is_empty() {
return if next
.is_some_and(|next| is_auth_scheme(next) || looks_like_credential_value(next))
{
TokenOutcome::keep_and_arm(CredentialArm::IfShaped)
} else {
TokenOutcome::keep()
};
}
kinds.insert(REDACTION_SECRET.to_string());
return TokenOutcome::replace(format!("{name}{separator}{SECRET_PLACEHOLDER}"));
}
// A path assigned to a variable is still a path.
if let Some(kind) = classify_path(value) {
kinds.insert(kind.to_string());
return TokenOutcome::replace(format!("{name}{separator}{PATH_PLACEHOLDER}"));
}
}
// A bare `Bearer` in canonical HTTP capitalization introduces a credential
// on its own — that is what makes `Bearer qqq` lose `qqq`, which no shape
// test could ever do. Any other scheme keyword, and any other
// capitalization, needs something credential-shaped to actually follow:
// `bearer of bad news` and `Token holders vote` are prose.
if unwrap_token(token) == SELF_EVIDENT_AUTH_SCHEME {
return TokenOutcome::keep_and_arm(CredentialArm::Certain);
}
if is_auth_scheme(token) && next.is_some_and(looks_like_credential_value) {
return TokenOutcome::keep_and_arm(CredentialArm::IfShaped);
}
let lowered = token.to_ascii_lowercase();
if SECRET_VALUE_PREFIXES
.iter()
.any(|prefix| lowered.starts_with(prefix))
|| looks_like_aws_access_key(token)
{
kinds.insert(REDACTION_SECRET.to_string());
return TokenOutcome::replace(SECRET_PLACEHOLDER.to_string());
}
if let Some(kind) = classify_path(token) {
kinds.insert(kind.to_string());
return TokenOutcome::replace(PATH_PLACEHOLDER.to_string());
}
TokenOutcome::keep()
}
/// Strip the punctuation a token picks up from surrounding prose.
fn unwrap_token(token: &str) -> &str {
token
.trim_start_matches(['(', '[', '"', '\'', '<'])
.trim_end_matches([',', '.', ';', ':', ')', ']', '"', '\'', '>', '!', '?'])
}
/// Whether a token is an HTTP authorization scheme keyword (and nothing else),
/// in any capitalization.
fn is_auth_scheme(token: &str) -> bool {
let word = unwrap_token(token);
!word.is_empty()
&& word.chars().all(|ch| ch.is_ascii_alphabetic())
&& AUTH_SCHEMES
.iter()
.any(|scheme| scheme.eq_ignore_ascii_case(word))
}
/// Whether a token is a scheme keyword spelled the way HTTP spells it —
/// `Bearer`, not `bearer` or `BEARER`.
///
/// This is the evidence that separates syntax from prose. It is a weak signal
/// read honestly: capitalization is *suggestive*, so it upgrades an arming that
/// header context already established, and stands alone only for
/// [`SELF_EVIDENT_AUTH_SCHEME`].
fn is_canonical_auth_scheme(token: &str) -> bool {
AUTH_SCHEMES.contains(&unwrap_token(token))
}
/// Whether a token has the shape of an opaque credential value.
///
/// Deliberately conservative: long, punctuation-free-ish, and mixing letters
/// with digits. Ordinary words — however long — never qualify, which is what
/// keeps `bearer of responsibility` out of the redactor.
fn looks_like_credential_value(token: &str) -> bool {
let value = unwrap_token(token);
if value.chars().count() < CREDENTIAL_VALUE_MIN_LEN {
return false;
}
let mut has_digit = false;
let mut has_alpha = false;
for ch in value.chars() {
if ch.is_ascii_digit() {
has_digit = true;
} else if ch.is_ascii_alphabetic() {
has_alpha = true;
} else if !matches!(ch, '-' | '_' | '.' | '=' | '+' | '/' | '~') {
return false;
}
}
has_digit && has_alpha
}
/// Whether a token is an AWS access key id.
///
/// Requires the exact uppercase prefix *and* the full length, so `Asia` and
/// `ASIA` (the continent, in prose or in a shouted heading) are left alone
/// while `ASIA` + 16 key characters is removed.
fn looks_like_aws_access_key(token: &str) -> bool {
let value = unwrap_token(token);
if value.len() < AWS_KEY_ID_LEN {
return false;
}
if !AWS_KEY_ID_PREFIXES
.iter()
.any(|prefix| value.starts_with(prefix))
{
return false;
}
value
.chars()
.all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit())
}
/// Whether a token is an absolute or home-relative filesystem path.
///
/// A lone `/` or `~` is punctuation, not a path; a Windows drive letter needs
/// its `:\` to count.
fn looks_absolute(token: &str) -> bool {
let trimmed = token.trim_start_matches(['(', '[', '"', '\'']);
if trimmed.len() < 2 {
return false;
}
if let Some(rest) = trimmed.strip_prefix('/') {
return rest.starts_with(|ch: char| ch.is_ascii_alphanumeric() || ch == '.' || ch == '_');
}
if trimmed.starts_with("~/") || trimmed.starts_with("~\\") {
return true;
}
if trimmed.starts_with("\\\\") {
return true;
}
let mut chars = trimmed.chars();
matches!(
(chars.next(), chars.next(), chars.next()),
(Some(drive), Some(':'), Some('\\' | '/')) if drive.is_ascii_alphabetic()
)
}
/// Which path kind a token is, if any — the single decision both the bare-token
/// and the `NAME=value` rules ask, so an assignment can never be classified
/// differently from the same value standing alone.
///
/// Escaped spellings are resolved first: a path that arrives inside a JSON
/// string is written `crates\/tui\/src\/main.rs` or `crates\\tui\\src\\main.rs`,
/// and reading only the literal characters would let either spelling through
/// while the receipt claimed nothing was removed.
///
/// A URL is not a filesystem path and is left to the URL-bearing-input guard
/// that already refuses such a task, so a token carrying a scheme is declined
/// here rather than silently reclassified.
fn classify_path(token: &str) -> Option<&'static str> {
let raw = trim_path_punctuation(token);
let unescaped = unescape_path(raw);
let candidate = trim_path_punctuation(&unescaped);
if candidate.contains("://") {
return None;
}
// Both spellings are asked, because unescaping is lossy in one direction
// that matters: a UNC share is *literally* `\\host\share`, and collapsing
// its leading pair would demote a machine-identifying path to a relative
// one.
if looks_absolute(raw) || looks_absolute(candidate) {
return Some(REDACTION_ABSOLUTE_PATH);
}
if looks_relative(candidate) {
return Some(REDACTION_RELATIVE_PATH);
}
None
}
/// Strip the punctuation a path picks up from surrounding prose, including the
/// escaped quotes it picks up from a JSON string.
fn trim_path_punctuation(token: &str) -> &str {
token
.trim_start_matches(['(', '[', '{', '"', '\'', '<', '`'])
.trim_end_matches([
',', ';', ':', '.', ')', ']', '}', '"', '\'', '>', '`', '!', '?',
])
}
/// Resolve `\/` and `\\` to the separator they escape, and drop escaped quotes.
///
/// Returns an owned string only because most tokens need no work; the borrowed
/// fast path is not worth a second code path in a function this small.
fn unescape_path(token: &str) -> String {
let mut out = String::with_capacity(token.len());
let mut chars = token.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '\\' {
out.push(ch);
continue;
}
match chars.peek() {
Some('/') => {
out.push('/');
chars.next();
}
Some('\\') => {
out.push('\\');
chars.next();
}
Some('"') | Some('\'') => {
chars.next();
}
// A lone backslash is a separator in its own right (`C:\Users`).
_ => out.push('\\'),
}
}
out
}
/// Whether a token is a repo-relative filesystem path, judged conservatively.
///
/// Two signals, and nothing else, because the cost of a false positive here is
/// paid in the operator's own summary: a redacted word plus a `relative_path`
/// kind on a receipt that removed prose.
///
/// 1. **Explicit relative syntax** — `./x`, `../x`, and their backslash forms.
/// Nothing but a path is spelled that way.
/// 2. **A file extension on the last segment** of a multi-segment token: stem
/// plus 18 ASCII *alphabetic* characters. The alphabetic requirement is
/// what keeps `zai/glm-5.2` a model label rather than a file, and the
/// multi-segment requirement is what keeps every bare `provider/model` pair
/// — `deepseek/deepseek-v4-flash`, `anthropic/claude-opus-5` — intact.
///
/// The deliberate gap is an extension-less directory (`crates/tui/src`), which
/// stays. Catching it would need a rule that cannot tell a directory from
/// `read/write/execute`, and shredding prose to hide a directory name is the
/// worse trade.
fn looks_relative(candidate: &str) -> bool {
if !candidate.contains(['/', '\\']) {
return false;
}
let explicit_prefix = ["./", "../", ".\\", "..\\"]
.iter()
.any(|prefix| candidate.starts_with(prefix));
if explicit_prefix {
return true;
}
let trimmed = candidate.trim_end_matches(['/', '\\']);
let segments: Vec<&str> = trimmed.split(['/', '\\']).collect();
if segments.len() < 2 || segments.iter().any(|segment| segment.is_empty()) {
return false;
}
let last = segments[segments.len() - 1];
let Some((stem, extension)) = last.rsplit_once('.') else {
return false;
};
!stem.is_empty()
&& (1..=8).contains(&extension.chars().count())
&& extension.chars().all(|ch| ch.is_ascii_alphabetic())
}
/// Whether a string contains anything this module would redact.
///
/// Used by assertions and by durable-write guards that must fail loudly rather
/// than persist a path or a key.
#[must_use]
#[cfg(test)]
pub fn contains_redactable(input: &str) -> bool {
redact_for_disclosure(input).redacted()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn absolute_paths_are_replaced_and_recorded() {
let redaction = redact_for_disclosure("fix /Users/hunter/src/app/main.rs and ~/notes.md");
assert!(!redaction.text().contains("/Users/"));
assert!(!redaction.text().contains("~/"));
assert!(redaction.text().contains(PATH_PLACEHOLDER));
assert!(redaction.redacted());
assert_eq!(redaction.kinds(), vec![REDACTION_ABSOLUTE_PATH.to_string()]);
}
#[test]
fn windows_paths_and_unc_shares_count_as_absolute() {
for token in ["C:\\Users\\hunter\\app", "\\\\share\\team\\notes"] {
let redaction = redact_for_disclosure(token);
assert!(redaction.redacted(), "{token} must be redacted");
assert_eq!(redaction.text(), PATH_PLACEHOLDER);
}
}
#[test]
fn secret_shaped_tokens_and_assignments_are_replaced() {
let redaction = redact_for_disclosure("use sk-live-abc123 and ZAI_API_KEY=zzz");
assert!(!redaction.text().contains("sk-live-abc123"));
assert!(!redaction.text().contains("zzz"));
assert!(
redaction.text().contains("ZAI_API_KEY=<redacted>"),
"the name stays, the value goes: {}",
redaction.text()
);
assert_eq!(redaction.kinds(), vec![REDACTION_SECRET.to_string()]);
}
/// The credential in an `Authorization` header is a *separate token* from
/// the header name and from the scheme keyword. Redacting only the keyword
/// leaves the secret in the clear while the receipt claims a secret was
/// removed — the exact failure this covers.
#[test]
fn a_multi_token_authorization_header_loses_its_credential() {
for header in [
"Authorization: Bearer sk-live-abc123def456",
"authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
"-H Authorization:Bearer abcdef0123456789abcdef",
] {
let redaction = redact_for_disclosure(header);
let text = redaction.text();
assert!(redaction.redacted(), "{header} must be redacted");
assert!(
text.contains(SECRET_PLACEHOLDER),
"{header} must carry a placeholder: {text}"
);
for leaked in [
"sk-live-abc123def456",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
"abcdef0123456789abcdef",
] {
assert!(!text.contains(leaked), "{leaked} leaked through: {text}");
}
assert_eq!(redaction.kinds(), vec![REDACTION_SECRET.to_string()]);
}
}
/// A bare scheme keyword introduces a credential only when a
/// credential-shaped token actually follows it.
#[test]
fn a_bare_bearer_token_is_removed_but_the_scheme_word_survives() {
let redaction = redact_for_disclosure("send Bearer 9f8e7d6c5b4a3f2e1d0c9b8a and retry");
let text = redaction.text();
assert!(
text.contains("Bearer"),
"the scheme keyword is not a secret"
);
assert!(!text.contains("9f8e7d6c5b4a3f2e1d0c9b8a"), "{text}");
assert!(text.ends_with("and retry"), "{text}");
}
/// Ordinary words that merely *look* like credential prefixes must survive,
/// and must not report a `secret` redaction kind. `Asia`, `bearer`, and any
/// identifier containing them are prose, not keys.
#[test]
fn ordinary_words_and_identifiers_are_not_mistaken_for_secrets() {
for text in [
"ship the Asia region rollout",
"ASIA is a continent, not a key",
"the bearer of this note may enter",
"authorization: needed before merge",
"rename bearer_token_header to auth_header_name",
"aws_region defaults to us-east-1",
"pk_display is a public identifier",
] {
let redaction = redact_for_disclosure(text);
assert!(!redaction.redacted(), "{text} must survive: {redaction:?}");
assert_eq!(redaction.text(), text);
}
}
/// The adversarial prose set. Every line here is ordinary English that the
/// scheme/prefix rules could plausibly mistake for credential syntax, and
/// every one of them must come back byte-identical with an empty `kinds`.
///
/// A false positive is not a harmless over-redaction: it mangles the routing
/// summary a human reads *and* writes `secret` onto a durable receipt that
/// removed nothing, which makes the disclosure a lie in the safe direction.
#[test]
fn adversarial_prose_survives_the_credential_state_machine() {
for text in [
// The lowercase scheme word, in every position that could arm it.
"bearer shares responsibility for the rollout",
"the bearer of bad news is rarely thanked",
"bearer",
"each bearer token header is rewritten downstream",
// Capitalized scheme words that are ordinary English. These are why
// canonical capitalization arms only for `Bearer`.
"Token holders vote on the proposal",
"Basic auth is enabled for the staging endpoint",
"Digest the results before the review",
// Weak header context introducing a sentence, not a secret.
"authorization: needed before merge",
"authorization: bearer shares responsibility",
// Identifiers and prefixes that resemble key material.
"variables like aws_region and pk_display stay readable",
"aws_ prefixed variables are documented in the runbook",
// `pk_` is a *public* key prefix and carries nothing; it is
// deliberately absent from SECRET_VALUE_PREFIXES. (`sk_` is not
// listed here because it genuinely is a secret prefix and redacting
// it is correct.)
"pk_ and pub_ are conventions, not values",
"asia and akia are four letter strings",
"the variables were renamed in the same commit",
// Long lowercase words are still words: shape alone must not fire.
"internationalization is spelled with eighteen letters",
] {
let redaction = redact_for_disclosure(text);
assert!(
!redaction.redacted(),
"{text:?} is prose and must survive untouched: {redaction:?}"
);
assert_eq!(redaction.text(), text);
assert!(
redaction.kinds().is_empty(),
"{text:?} must not claim a redaction it did not make"
);
}
}
/// The adversarial credential set. A real credential is often short,
/// lowercase, punctuated, or otherwise shapeless — `Bearer qqq` is the
/// canonical example, and no shape test could ever catch it. Context has to.
#[test]
fn adversarial_credentials_lose_the_whole_value() {
for (text, leaked) in [
// The short, shapeless credential. This is the leak the evidence
// model exists to close.
("Bearer qqq", "qqq"),
("Authorization: Bearer qqq", "qqq"),
("authorization: Bearer qqq", "qqq"),
// Punctuated and quoted header forms.
("Authorization: Bearer qqq.", "qqq"),
("-H \"Authorization: Bearer qqq\"", "qqq"),
("Authorization:Bearer qqq", "qqq"),
// The scheme keyword may not absorb the redaction and leave the
// value behind.
("send Bearer hunter2 now", "hunter2"),
(
"curl -H Authorization: Bearer sk-live-0000 -X POST",
"sk-live-0000",
),
] {
let redaction = redact_for_disclosure(text);
let redacted_text = redaction.text();
assert!(
redaction.redacted(),
"{text:?} carries a credential and must be redacted"
);
assert!(
!redacted_text.split(' ').any(|token| token == leaked
|| token.trim_end_matches(['.', ',', '"', '\'']) == leaked),
"{leaked:?} leaked through {text:?}: {redacted_text}"
);
assert!(
redacted_text.contains(SECRET_PLACEHOLDER),
"{text:?} must carry a placeholder: {redacted_text}"
);
assert!(
redaction.kinds().contains(&REDACTION_SECRET.to_string()),
"{text:?} must disclose the secret kind"
);
// The scheme keyword is not the secret and must still be readable,
// so a reader can tell *what* was removed.
assert!(
redacted_text.to_ascii_lowercase().contains("bearer"),
"the scheme keyword must survive: {redacted_text}"
);
}
}
/// The one documented false positive of the capitalization rule, pinned so
/// it stays deliberate rather than becoming a surprise. Capitalized `Bearer`
/// followed by a word is treated as header syntax; ordinary prose spells it
/// lowercase, which the test above covers.
#[test]
fn capitalized_bearer_arms_even_in_prose_and_that_is_the_known_cost() {
let redaction = redact_for_disclosure("Bearer tokens are rotated weekly");
assert_eq!(redaction.text(), "Bearer <redacted> are rotated weekly");
// The lowercase spelling — what prose actually uses — is untouched.
let prose = redact_for_disclosure("bearer tokens are rotated weekly");
assert!(!prose.redacted());
}
/// The real AWS shape still goes, so dropping the bare `asia`/`akia`
/// prefixes did not trade a false positive for a false negative.
#[test]
fn full_aws_access_key_ids_are_still_removed() {
for key in ["AKIAIOSFODNN7EXAMPLE", "ASIAIOSFODNN7EXAMPLE"] {
let redaction = redact_for_disclosure(&format!("creds {key} rotated"));
assert!(!redaction.text().contains(key), "{}", redaction.text());
assert_eq!(redaction.kinds(), vec![REDACTION_SECRET.to_string()]);
}
}
#[test]
fn ordinary_prose_is_left_alone() {
let redaction = redact_for_disclosure("refactor the parser and add a regression test");
assert!(!redaction.redacted());
assert_eq!(
redaction.text(),
"refactor the parser and add a regression test"
);
assert!(redaction.kinds().is_empty());
}
/// A repo-relative path discloses the private tree's shape to whatever
/// provider the routing summary reaches, and is persisted next to it. Every
/// spelling one arrives in — bare, quoted, JSON-escaped with `\/` or `\\`,
/// explicitly relative, assigned to a name, trailing prose punctuation —
/// must lose the path *and* say so on the receipt.
#[test]
fn repo_relative_paths_are_redacted_in_every_spelling_and_disclosed() {
for token in [
"crates/tui/src/main.rs",
"src/lib.rs",
"web/lib/deploy-preflight.test.ts",
".github/workflows/web.yml",
"crates\\tui\\src\\main.rs",
"crates\\/tui\\/src\\/main.rs",
"\\\"crates/tui/src/main.rs\\\"",
"\"crates/tui/src/main.rs\"",
"(crates/tui/src/main.rs)",
"./deploy.sh",
"../../secret/notes.md",
"..\\secret\\notes.md",
] {
let redaction = redact_for_disclosure(token);
assert!(redaction.redacted(), "{token} must be redacted");
assert!(
!redaction.text().contains("main.rs")
&& !redaction.text().contains("notes.md")
&& !redaction.text().contains("deploy"),
"{token} leaked: {}",
redaction.text()
);
assert!(
redaction
.kinds()
.contains(&REDACTION_RELATIVE_PATH.to_string()),
"{token} must disclose the relative_path kind: {:?}",
redaction.kinds()
);
}
// In a sentence, and as a value: the name survives, the path does not.
let sentence = redact_for_disclosure("patch crates/tui/src/main.rs, then path=src/lib.rs");
assert_eq!(
sentence.text(),
"patch <path> then path=<path>",
"prose keeps its shape around the placeholder"
);
assert_eq!(
sentence.kinds(),
vec![REDACTION_RELATIVE_PATH.to_string()],
"one kind, honestly reported"
);
}
/// The other half of the same rule: it must not shred ordinary prose,
/// `provider/model` labels, or bare punctuation, because a false positive
/// here costs the operator their own summary *and* puts a redaction kind on
/// a receipt that removed nothing.
#[test]
fn prose_labels_and_bare_punctuation_are_not_paths() {
for token in [
// Provider/model labels — the exact shape a Fleet receipt carries.
"deepseek/deepseek-v4-flash",
"zai/glm-5.2",
"anthropic/claude-opus-5",
"workspace/glm-pair",
// Prose that happens to carry separators.
"a/b",
"and/or",
"read/write/execute",
"TODO/FIXME",
"provider/model/reasoning",
// Bare punctuation and non-paths.
"/",
"~",
"5:30",
"v0.9.2",
// A URL is not a filesystem path; the URL-bearing-input guard owns
// it, and reclassifying it here would be a silent behavior change.
"https://example.test/a/b.rs",
] {
let redaction = redact_for_disclosure(token);
assert!(!redaction.redacted(), "{token} must not be redacted");
assert_eq!(redaction.text(), token, "{token} must survive verbatim");
}
}
/// The documented gap, pinned so it stays a decision rather than a
/// surprise: an extension-less directory survives, because no rule can
/// separate it from `read/write/execute` without shredding prose.
#[test]
fn an_extension_less_directory_is_the_known_residual() {
let redaction = redact_for_disclosure("look in crates/tui/src");
assert!(!redaction.redacted());
}
#[test]
fn contains_redactable_matches_the_redactor() {
assert!(contains_redactable("/Users/hunter"));
assert!(contains_redactable("token=abc"));
assert!(!contains_redactable("land a fix in the workflow crate"));
}
}
@@ -0,0 +1,676 @@
//! End-to-end vertical for an exact named Fleet at the crate boundary:
//! parse → attach a reusable Reasoning Router → freeze routes → router decision
//! → reasoning resolution → immutable Workflow snapshot.
//!
//! No live provider calls: the router's response is a fixture string.
use codewhale_workflow::{
CapturedReasoningRouter, CredentialReadiness, EffectiveReasoning, EffectiveReasoningSource,
EndpointIdentity, FleetDocument, FleetSearchRoot, FleetSnapshot, NamedFleetError,
PermissionCeiling, PreflightedRoute, ProviderEffectiveReasoning, QualifiedFleetId,
REASONING_ROUTER_DIR, REASONING_ROUTER_SERVICE_KIND, ReasoningCapability, ReasoningRouterError,
ReasoningRouterProfile, ReasoningTier, RequestedReasoning, RouterAvailability,
RouterCallReasoning, RouterIdentity, ShellCeiling, bounded_routing_payload,
parse_router_decision, resolve_exact_member_reasoning, router_call_plan, router_system_prompt,
router_user_message,
};
const GLM_FLEET: &str = r#"
name = "glm-pair"
description = "GLM workers with a shared GPT-5.6 Luna reasoning router"
schema = "exact"
schema_revision = 1
reasoning_router = "luna-low"
[[members]]
id = "implementer"
role = "builder"
provider = "zai"
model = "glm-5"
reasoning = "auto"
permissions = "read_write"
[[members]]
id = "auditor"
role = "reviewer"
provider = "zai"
model = "glm-5"
reasoning = "high"
permissions = "read_only"
"#;
/// The user's example: GPT-5.6 Luna, called at `low`.
const LUNA: &str = r#"
name = "luna-low"
schema = "reasoning_router"
schema_revision = 1
provider = "openai"
model = "gpt-5.6-luna"
call_reasoning = "low"
"#;
fn fleet_id(name: &str) -> QualifiedFleetId {
QualifiedFleetId {
name: name.to_string(),
origin: "workspace".to_string(),
}
}
fn luna() -> CapturedReasoningRouter {
let profile = ReasoningRouterProfile::parse(LUNA).expect("router profile");
CapturedReasoningRouter::from_profile(&profile, "workspace")
}
fn route(member: &str, provider: &str, model: &str) -> PreflightedRoute {
PreflightedRoute {
member_id: member.to_string(),
provider_id: provider.to_string(),
provider_kind: provider.to_string(),
declared_model: model.to_string(),
wire_model: model.to_string(),
endpoint: EndpointIdentity::from_base_url("https://api.example.test/v1"),
credential: CredentialReadiness::Configured,
capability: ReasoningCapability::tiered(),
}
}
/// A workspace holding one Fleet and one Router profile, plus its search roots.
fn workspace_with(fleet: &str, router: Option<&str>) -> (tempfile::TempDir, Vec<FleetSearchRoot>) {
let tmp = tempfile::tempdir().expect("tmp");
std::fs::create_dir_all(tmp.path().join("fleets")).expect("fleets dir");
std::fs::write(tmp.path().join("fleets/glm-pair.toml"), fleet).expect("fleet");
if let Some(router) = router {
std::fs::create_dir_all(tmp.path().join(REASONING_ROUTER_DIR)).expect("routers dir");
std::fs::write(
tmp.path().join(REASONING_ROUTER_DIR).join("luna-low.toml"),
router,
)
.expect("router");
}
let roots = vec![FleetSearchRoot::new("workspace", tmp.path())];
(tmp, roots)
}
#[test]
fn a_fleet_and_its_referenced_router_resolve_reasoning_without_moving_the_route() {
let (_tmp, roots) = workspace_with(GLM_FLEET, Some(LUNA));
let (document, id) = FleetDocument::load_by_name("glm-pair", &roots).expect("fleet loads");
let exact = document.exact().expect("exact");
// The Router is a *reference* to a separately saved service.
assert_eq!(exact.reasoning_router.as_deref(), Some("luna-low"));
assert!(exact.legacy_inline_router().is_none());
let (profile, router_id) =
ReasoningRouterProfile::load_by_name("luna-low", &roots).expect("router loads");
assert_eq!(router_id.qualified(), "workspace/luna-low");
let captured = CapturedReasoningRouter::from_profile(&profile, router_id.origin);
let snapshot = FleetSnapshot::capture(id, &document, "2026-07-26T00:00:00Z", Some(captured))
.expect("capture");
// Routes are frozen from the snapshot, before any reasoning resolution.
let member = snapshot.member("implementer").expect("member");
let frozen = member.route.clone();
assert_eq!(frozen.provider, "zai");
assert_eq!(frozen.model, "glm-5");
// The Router is a non-dispatchable service with no authority.
let router = snapshot.router().expect("router service");
assert_eq!(router.service_kind, REASONING_ROUTER_SERVICE_KIND);
assert_eq!(router.route.model, "gpt-5.6-luna");
assert_eq!(router.requested_call_reasoning, RouterCallReasoning::Low);
assert!(!router.dispatchable);
assert!(!router.permissions.tools);
let decision = parse_router_decision(
r#"```json
{"reasoning":"max"}
```"#,
)
.expect("router decision parses");
let identity = RouterIdentity::from_captured(
router,
Some(&route("router", "openai", "gpt-5.6-luna")),
Some(
router_call_plan(
router.requested_call_reasoning,
&ReasoningCapability::tiered(),
)
.disclosure,
),
);
let resolved = resolve_exact_member_reasoning(
&member.id,
&frozen,
member.requested_reasoning,
&ReasoningCapability::tiered(),
&RouterAvailability::Ready,
Some(&decision),
Some(&identity),
)
.expect("ready router resolves auto");
assert_eq!(resolved.requested(), RequestedReasoning::Auto);
assert_eq!(
resolved.effective(),
EffectiveReasoning::Tier(ReasoningTier::Max)
);
assert_eq!(resolved.source(), EffectiveReasoningSource::FleetRouter);
// The router's own call ran at the configured `low`, and says so.
let call = resolved
.router()
.expect("router identity")
.call
.as_ref()
.expect("call disclosure");
assert_eq!(call.requested, "low");
assert_eq!(call.effective, "low");
// The worker's provider/model did not move.
assert_eq!(snapshot.member("implementer").unwrap().route, frozen);
}
/// One saved Router profile, referenced by two different Fleets.
#[test]
fn one_router_profile_serves_two_fleets() {
let tmp = tempfile::tempdir().expect("tmp");
std::fs::create_dir_all(tmp.path().join("fleets")).expect("fleets dir");
std::fs::create_dir_all(tmp.path().join(REASONING_ROUTER_DIR)).expect("routers dir");
std::fs::write(
tmp.path().join(REASONING_ROUTER_DIR).join("luna-low.toml"),
LUNA,
)
.expect("router");
std::fs::write(tmp.path().join("fleets/glm-pair.toml"), GLM_FLEET).expect("first");
std::fs::write(
tmp.path().join("fleets/glm-solo.toml"),
GLM_FLEET.replace("name = \"glm-pair\"", "name = \"glm-solo\""),
)
.expect("second");
let roots = vec![FleetSearchRoot::new("workspace", tmp.path())];
let mut snapshots = Vec::new();
for name in ["glm-pair", "glm-solo"] {
let (document, id) = FleetDocument::load_by_name(name, &roots).expect("fleet loads");
let reference = document
.exact()
.expect("exact")
.reasoning_router
.clone()
.expect("router reference");
let (profile, router_id) =
ReasoningRouterProfile::load_by_name(&reference, &roots).expect("router loads");
snapshots.push(
FleetSnapshot::capture(
id,
&document,
"2026-07-26T00:00:00Z",
Some(CapturedReasoningRouter::from_profile(
&profile,
router_id.origin,
)),
)
.expect("capture"),
);
}
assert_eq!(
snapshots[0].router(),
snapshots[1].router(),
"both fleets attach the identical captured router service"
);
assert_ne!(snapshots[0].fleet(), snapshots[1].fleet());
assert_eq!(
snapshots[0].router().expect("router").qualified(),
"workspace/luna-low"
);
}
/// A bare Router name defined in two origins is ambiguous; a qualified origin
/// resolves it. Shadowing would silently change which provider sees every
/// routing summary.
#[test]
fn a_router_defined_in_two_origins_is_ambiguous_until_qualified() {
let tmp = tempfile::tempdir().expect("tmp");
let home = tmp.path().join("home");
let workspace = tmp.path().join("workspace");
for root in [&home, &workspace] {
std::fs::create_dir_all(root.join(REASONING_ROUTER_DIR)).expect("routers dir");
}
std::fs::write(
home.join(REASONING_ROUTER_DIR).join("luna-low.toml"),
LUNA.replace("gpt-5.6-luna", "gpt-5.6-luna-mini"),
)
.expect("home");
std::fs::write(
workspace.join(REASONING_ROUTER_DIR).join("luna-low.toml"),
LUNA,
)
.expect("workspace");
let roots = vec![
FleetSearchRoot::new("codewhale_home", &home),
FleetSearchRoot::new("workspace", &workspace),
];
let err = ReasoningRouterProfile::load_by_name("luna-low", &roots)
.expect_err("a bare name must not be resolved by shadowing");
assert!(
matches!(err, ReasoningRouterError::AmbiguousRouter { .. }),
"{err:?}"
);
assert_eq!(
ReasoningRouterProfile::load_by_name("workspace/luna-low", &roots)
.expect("qualified")
.0
.model,
"gpt-5.6-luna"
);
assert_eq!(
ReasoningRouterProfile::load_by_name("codewhale_home/luna-low", &roots)
.expect("qualified")
.0
.model,
"gpt-5.6-luna-mini"
);
}
/// A Router may only run at `off` or `low`. `medium`/`high`/`max` are rejected,
/// not silently clamped — the operator must see that their setting was refused.
#[test]
fn an_expensive_router_call_tier_is_rejected_rather_than_clamped() {
for value in ["medium", "high", "max"] {
let text = LUNA.replace("\"low\"", &format!("\"{value}\""));
let err = ReasoningRouterProfile::parse(&text).expect_err("expensive tier");
assert!(
matches!(err, ReasoningRouterError::CallReasoningTooExpensive { .. }),
"value={value} err={err:?}"
);
}
// And `low` is honored end to end, never forced to `off` behind the label.
let profile = ReasoningRouterProfile::parse(LUNA).expect("parse");
let plan = router_call_plan(profile.call_reasoning, &ReasoningCapability::tiered());
assert_eq!(plan.tier, ReasoningTier::Low);
assert_eq!(plan.disclosure.effective, "low");
assert_eq!(plan.disclosure.provider_effective, "low");
}
/// A manual tier consults no Router at all.
#[test]
fn a_non_auto_member_never_consults_the_router() {
let document = FleetDocument::parse(GLM_FLEET).expect("parse");
let exact = document.exact().expect("exact");
let auditor = exact.member("auditor").expect("auditor");
let resolved = resolve_exact_member_reasoning(
&auditor.id,
&auditor.frozen_route(),
auditor.reasoning,
&ReasoningCapability::tiered(),
// Deliberately absent — an explicit tier must not need a router.
&RouterAvailability::Absent,
None,
None,
)
.expect("explicit tier resolves");
assert_eq!(
resolved.effective(),
EffectiveReasoning::Tier(ReasoningTier::High)
);
assert_eq!(resolved.source(), EffectiveReasoningSource::MemberExplicit);
assert!(resolved.router().is_none(), "no router was involved");
}
#[test]
fn an_auto_member_in_a_router_less_fleet_fails_before_work_starts() {
let router_less = GLM_FLEET.replace("reasoning_router = \"luna-low\"\n", "");
let document = FleetDocument::parse(&router_less).expect("parse");
let exact = document.exact().expect("exact");
assert!(exact.router_ref().is_none());
let member = exact.member("implementer").expect("member");
let err = resolve_exact_member_reasoning(
&member.id,
&member.frozen_route(),
member.reasoning,
&ReasoningCapability::tiered(),
&RouterAvailability::Absent,
None,
None,
)
.expect_err("auto without a router must fail closed");
let message = err.to_string();
assert!(message.contains("implementer"), "{message}");
assert!(message.contains("reasoning_router"), "{message}");
}
/// A Fleet that references a Router profile which is not installed cannot be
/// resolved — decided locally, with no provider contacted.
#[test]
fn a_missing_router_profile_is_a_local_load_failure() {
let (_tmp, roots) = workspace_with(GLM_FLEET, None);
let (document, _id) = FleetDocument::load_by_name("glm-pair", &roots).expect("fleet loads");
let reference = document
.exact()
.expect("exact")
.reasoning_router
.clone()
.expect("reference");
assert!(matches!(
ReasoningRouterProfile::load_by_name(&reference, &roots).expect_err("missing"),
ReasoningRouterError::NotFound { .. }
));
}
#[test]
fn member_ceilings_clamp_against_a_read_only_session_posture() {
let document = FleetDocument::parse(GLM_FLEET).expect("parse");
let snapshot = FleetSnapshot::capture(
fleet_id("glm-pair"),
&document,
"2026-07-26T00:00:00Z",
Some(luna()),
)
.expect("capture");
let session = PermissionCeiling {
write: false,
network_tool: false,
shell: ShellCeiling::ReadOnly,
delegation_depth: 0,
tools: true,
};
let implementer = snapshot.member("implementer").expect("member");
assert!(
implementer.permissions.write,
"the saved ceiling allows writes"
);
let effective = implementer.permissions.clamp_to(session);
assert!(
!effective.write,
"a saved fleet must never raise the active session posture"
);
assert_eq!(effective.shell, ShellCeiling::ReadOnly);
}
/// The bounded routing summary is transmitted exactly once, and the receipt's
/// count and hash describe exactly those bytes.
#[test]
fn the_routing_summary_is_transmitted_once_and_disclosed_without_content() {
let payload = bounded_routing_payload("refactor the parser in /Users/hunter/app");
let disclosure = payload.disclosure().clone();
let input = codewhale_workflow::RouterCallInput {
fleet: "workspace/glm-pair".to_string(),
member_id: "implementer".to_string(),
frozen: codewhale_workflow::FrozenRoute {
provider: "zai".to_string(),
model: "glm-5".to_string(),
},
payload,
};
let system = router_system_prompt(&input);
let user = router_user_message(&input);
assert!(!system.contains("refactor the parser"), "{system}");
assert!(!user.contains("/Users/"), "paths are redacted: {user}");
// The disclosed count and hash describe exactly the bytes that were sent —
// and the summary appears exactly once across the whole request.
assert_eq!(disclosure.transmitted_bytes, user.len());
assert_eq!(disclosure.transmitted_chars, user.chars().count());
assert_eq!(
format!("{system}\n{user}").matches(user.as_str()).count(),
1,
"the bounded summary must be transmitted once, not duplicated"
);
assert!(disclosure.redacted);
assert!(disclosure.redactions.contains(&"absolute_path".to_string()));
}
/// Legacy role-map fleets keep loading through the same store.
#[test]
fn legacy_fleet_files_still_load_through_the_same_store() {
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..");
let (document, id) =
FleetDocument::load_by_name("stopship", &[FleetSearchRoot::new("workspace", root)])
.expect("workspace legacy fleet loads");
assert!(document.is_legacy());
assert_eq!(document.schema_kind(), "legacy");
assert_eq!(id.qualified(), "workspace/stopship");
let legacy = document.legacy().expect("legacy body");
legacy.validate_stopship_roles().expect("required roles");
assert_eq!(legacy.resolve("release_lead").unwrap(), "manager");
}
/// A personal `~/.codewhale` Fleet must not silently shadow — or be shadowed
/// by — a project Fleet of the same name.
#[test]
fn an_exact_fleet_defined_in_two_origins_is_ambiguous_until_qualified() {
let tmp = tempfile::tempdir().expect("tmp");
let home = tmp.path().join("home");
let workspace = tmp.path().join("workspace");
for root in [&home, &workspace] {
std::fs::create_dir_all(root.join("fleets")).expect("fleets dir");
}
std::fs::write(
home.join("fleets/glm-pair.toml"),
GLM_FLEET.replace("model = \"glm-5\"", "model = \"glm-4\""),
)
.expect("home fleet");
std::fs::write(workspace.join("fleets/glm-pair.toml"), GLM_FLEET).expect("workspace fleet");
let roots = vec![
FleetSearchRoot::new("codewhale_home", &home),
FleetSearchRoot::new("workspace", &workspace),
];
let err = FleetDocument::load_by_name("glm-pair", &roots)
.expect_err("an exact fleet must not be resolved by shadowing");
assert!(
matches!(err, NamedFleetError::AmbiguousFleet { .. }),
"{err:?}"
);
let (document, id) =
FleetDocument::load_by_name("workspace/glm-pair", &roots).expect("qualified load");
assert_eq!(id.qualified(), "workspace/glm-pair");
assert_eq!(
document
.exact()
.expect("exact")
.member("implementer")
.expect("member")
.model,
"glm-5"
);
let (home_document, home_id) =
FleetDocument::load_by_name("codewhale_home/glm-pair", &roots).expect("qualified load");
assert_eq!(home_id.qualified(), "codewhale_home/glm-pair");
assert_eq!(
home_document
.exact()
.expect("exact")
.member("implementer")
.expect("member")
.model,
"glm-4"
);
}
/// Legacy role maps keep their historic first-hit-wins behavior: a role map
/// resolves through the same profile store from either origin.
#[test]
fn legacy_fleets_in_two_origins_keep_first_hit_wins() {
let tmp = tempfile::tempdir().expect("tmp");
let home = tmp.path().join("home");
let workspace = tmp.path().join("workspace");
for root in [&home, &workspace] {
std::fs::create_dir_all(root.join("fleets")).expect("fleets dir");
}
std::fs::write(
home.join("fleets/pair.toml"),
"name = \"pair\"\n\n[roles]\nscout = \"home-scout\"\n",
)
.expect("home fleet");
std::fs::write(
workspace.join("fleets/pair.toml"),
"name = \"pair\"\n\n[roles]\nscout = \"workspace-scout\"\n",
)
.expect("workspace fleet");
let (document, id) = FleetDocument::load_by_name(
"pair",
&[
FleetSearchRoot::new("codewhale_home", &home),
FleetSearchRoot::new("workspace", &workspace),
],
)
.expect("legacy collisions stay resolvable");
assert!(document.is_legacy());
assert_eq!(id.origin, "codewhale_home");
assert_eq!(
document.legacy().expect("legacy").resolve("scout").unwrap(),
"home-scout"
);
}
/// A broken file in a *shadowed* origin must not fail a legacy load that has
/// always worked.
#[test]
fn a_malformed_shadowed_sibling_does_not_regress_legacy_first_hit() {
let tmp = tempfile::tempdir().expect("tmp");
let home = tmp.path().join("home");
let workspace = tmp.path().join("workspace");
for root in [&home, &workspace] {
std::fs::create_dir_all(root.join("fleets")).expect("fleets dir");
}
std::fs::write(
home.join("fleets/pair.toml"),
"name = \"pair\"\n\n[roles]\nscout = \"home-scout\"\n",
)
.expect("home fleet");
std::fs::write(
workspace.join("fleets/pair.toml"),
"name = \"pair\"\n[roles\nscout = = = \"\"\"broken\n",
)
.expect("workspace fleet");
let (document, id) = FleetDocument::load_by_name(
"pair",
&[
FleetSearchRoot::new("codewhale_home", &home),
FleetSearchRoot::new("workspace", &workspace),
],
)
.expect("a broken shadowed sibling must not break first-hit-wins");
assert_eq!(id.origin, "codewhale_home");
assert_eq!(
document.legacy().expect("legacy").resolve("scout").unwrap(),
"home-scout"
);
}
/// Roster invariants hold at the crate boundary, not just inside the parser.
#[test]
fn duplicate_roles_and_reserved_router_identities_are_rejected_at_load() {
let duplicate_role = GLM_FLEET.replace("role = \"reviewer\"", "role = \"builder\"");
assert!(
FleetDocument::parse(&duplicate_role).is_err(),
"two members must not share the role `builder`"
);
let worker_router = GLM_FLEET.replace("role = \"reviewer\"", "role = \"router\"");
assert!(
FleetDocument::parse(&worker_router).is_err(),
"a worker must not claim the reserved role `router`"
);
}
/// The legacy inline Router form still parses and normalizes into the same
/// captured service — one runtime representation, whichever way it was written.
#[test]
fn a_legacy_inline_router_still_works_and_normalizes() {
let inline = format!(
"{}\n[[members]]\nid = \"router\"\nkind = \"router\"\nprovider = \"zai\"\nmodel = \
\"glm-5-turbo\"\n",
GLM_FLEET.replace("reasoning_router = \"luna-low\"\n", "")
);
let document = FleetDocument::parse(&inline).expect("legacy inline parses");
let exact = document.exact().expect("exact");
let captured = codewhale_workflow::captured_legacy_inline_router(exact).expect("inline router");
assert!(captured.legacy_inline);
assert_eq!(captured.service_kind, REASONING_ROUTER_SERVICE_KIND);
assert_eq!(captured.route.model, "glm-5-turbo");
assert_eq!(captured.requested_call_reasoning, RouterCallReasoning::Off);
assert!(!captured.is_dispatchable());
let snapshot = FleetSnapshot::capture(
fleet_id("glm-pair"),
&document,
"2026-07-26T00:00:00Z",
Some(captured),
)
.expect("capture");
assert!(snapshot.member("router").is_none());
assert_eq!(snapshot.members().len(), 2);
}
/// The provider-effective control is reported separately from the selector
/// tier: a Z.AI GLM route expresses only thinking on/off, so `high` and `max`
/// must not be presented as two distinct provider-effective tiers.
#[test]
fn glm_receipts_do_not_invent_distinct_high_and_max_provider_tiers() {
let document = FleetDocument::parse(GLM_FLEET).expect("parse");
let exact = document.exact().expect("exact");
let auditor = exact.member("auditor").expect("auditor");
let glm = ReasoningCapability::enabled_disabled();
let high = resolve_exact_member_reasoning(
&auditor.id,
&auditor.frozen_route(),
RequestedReasoning::High,
&glm,
&RouterAvailability::Absent,
None,
None,
)
.expect("resolve");
let max = resolve_exact_member_reasoning(
&auditor.id,
&auditor.frozen_route(),
RequestedReasoning::Max,
&glm,
&RouterAvailability::Absent,
None,
None,
)
.expect("resolve");
assert_eq!(
high.provider_effective(),
ProviderEffectiveReasoning::Enabled
);
assert_eq!(
max.provider_effective(),
ProviderEffectiveReasoning::Enabled
);
assert_ne!(high.effective(), max.effective());
}