use serde::{Deserialize, Serialize};
use super::root::{CapabilityGrant, CapabilityRef, KnowledgeEntry};
use super::scalar::{CallId, WireU64};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum HostCommand {
Cancel(CancelCommand),
ForceCompact(ForceCompactCommand),
UpdateTask(UpdateTaskCommand),
ApplyCapabilityPatch(ApplyCapabilityPatchCommand),
ApplyKnowledgeMutation(ApplyKnowledgeMutationCommand),
SeedKnowledge(SeedKnowledgeCommand),
ApplySkillActivation(ApplySkillActivationCommand),
ApplyPolicyPatch(ApplyPolicyPatchCommand),
UpdateDeadline(UpdateDeadlineCommand),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CancelCommand {
pub reason: CancellationReason,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pending_call_ids: Vec<CallId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CancellationReason {
User,
Deadline,
LeaseLost,
HostShutdown,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ForceCompactCommand {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UpdateTaskCommand {
pub update: TaskUpdate,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TaskUpdate {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plan: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_step: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progress: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scratchpad: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocked_on: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preserved_refs: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub directives: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApplyCapabilityPatchCommand {
pub patch: CapabilityPatch,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CapabilityPatch {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mount: Vec<CapabilityGrant>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unmount: Vec<CapabilityRef>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApplyKnowledgeMutationCommand {
pub mutation: KnowledgeMutation,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KnowledgeMutation {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub upsert: Vec<KnowledgeEntry>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub remove: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SeedKnowledgeCommand {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entries: Vec<KnowledgeEntry>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApplySkillActivationCommand {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub activate: Vec<SkillActivation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deactivate: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SkillActivation {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lease_turns: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ApplyPolicyPatchCommand {
pub expected_revision: WireU64,
pub patch: LivePolicyPatch,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LivePolicyPatch {
ReplaceSignalPolicy(ReplaceSignalPolicy),
ReplaceGovernancePolicy(ReplaceGovernancePolicy),
TightenResourceQuota(TightenResourceQuota),
ReplaceRecoveryPolicy(ReplaceRecoveryPolicy),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReplaceSignalPolicy {
pub policy: SignalPolicy,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SignalPolicy {
pub queue_max: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl_ms: Option<WireU64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deadline_escalation: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReplaceGovernancePolicy {
pub policy: GovernancePolicy,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GovernancePolicy {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_action: Option<PolicyAction>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<PolicyRule>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub vetoed_tools: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rate_limits: Vec<RateLimitSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub constraints: Vec<ParamConstraint>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RateLimitSpec {
pub tool: String,
pub max_calls: u32,
pub window_ms: WireU64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ParamConstraint {
Required(RequiredParam),
Enum(EnumParam),
Range(RangeParam),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RequiredParam {
pub tool: String,
pub param_path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EnumParam {
pub tool: String,
pub param_path: String,
pub values: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RangeParam {
pub tool: String,
pub param_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_micros: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_micros: Option<i64>,
}
impl ParamConstraint {
pub fn tool(&self) -> &str {
match self {
Self::Required(c) => &c.tool,
Self::Enum(c) => &c.tool,
Self::Range(c) => &c.tool,
}
}
pub fn param_path(&self) -> &str {
match self {
Self::Required(c) => &c.param_path,
Self::Enum(c) => &c.param_path,
Self::Range(c) => &c.param_path,
}
}
pub fn validate(&self) -> Result<(), String> {
if self.tool().is_empty() {
return Err("governance constraint tool must not be empty".to_string());
}
if self.param_path().is_empty() {
return Err("governance constraint param_path must not be empty".to_string());
}
match self {
Self::Required(_) => Ok(()),
Self::Enum(c) => {
if c.values.is_empty() {
return Err(format!(
"enum constraint on {}.{} lists no permitted value, so every call is denied",
c.tool, c.param_path
));
}
Ok(())
}
Self::Range(c) => match (c.min_micros, c.max_micros) {
(None, None) => Err(format!(
"range constraint on {}.{} bounds nothing",
c.tool, c.param_path
)),
(Some(min), Some(max)) if min > max => Err(format!(
"range constraint on {}.{} has min_micros {min} above max_micros {max}",
c.tool, c.param_path
)),
_ => Ok(()),
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PolicyAction {
Allow,
Deny,
AskUser,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PolicyRule {
pub tool_pattern: String,
pub action: PolicyAction,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TightenResourceQuota {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_concurrent_subagents: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_total_subagents: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_spawn_depth: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_workflow_nodes: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReplaceRecoveryPolicy {
pub policy: RecoveryPolicy,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RecoveryPolicy {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_recovery_attempts: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_recovery_attempts: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tail_bounds: Option<TailBoundsPolicy>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TailBoundsPolicy {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub soft_records: Option<WireU64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hard_records: Option<WireU64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub soft_bytes: Option<WireU64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hard_bytes: Option<WireU64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UpdateDeadlineCommand {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deadline_ms: Option<WireU64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LivePolicyState {
revision: WireU64,
config: super::config::ResolvedOperationConfig,
}
impl LivePolicyState {
pub fn new(config: super::config::ResolvedOperationConfig) -> Self {
Self {
revision: WireU64::ZERO,
config,
}
}
pub fn restore(revision: WireU64, config: super::config::ResolvedOperationConfig) -> Self {
Self { revision, config }
}
pub fn revision(&self) -> WireU64 {
self.revision
}
pub fn config(&self) -> &super::config::ResolvedOperationConfig {
&self.config
}
pub fn apply(
&mut self,
command: &ApplyPolicyPatchCommand,
) -> Result<WireU64, super::envelope::WireRejection> {
use super::envelope::{WireRejection, WireRejectionKind};
if command.expected_revision != self.revision {
return Err(WireRejection::new(
WireRejectionKind::PolicyViolation,
format!(
"policy revision mismatch: patch expects {}, the operation is at {}; \
re-read the policy and rebase the patch",
command.expected_revision, self.revision
),
));
}
let next = command.patch.apply_to(&self.config)?;
self.config = next;
self.revision = WireU64::new(self.revision.get().saturating_add(1));
Ok(self.revision)
}
}
impl LivePolicyPatch {
pub fn apply_to(
&self,
current: &super::config::ResolvedOperationConfig,
) -> Result<super::config::ResolvedOperationConfig, super::envelope::WireRejection> {
use super::config::{
ResolvedRecoveryPolicy, ResolvedSignalPolicy, validate_quota, validate_recovery,
validate_signal,
};
let mut next = current.clone();
match self {
Self::ReplaceSignalPolicy(patch) => {
next.signal_policy = ResolvedSignalPolicy {
queue_max: patch.policy.queue_max,
ttl_ms: patch.policy.ttl_ms,
deadline_escalation: patch
.policy
.deadline_escalation
.unwrap_or(current.signal_policy.deadline_escalation),
};
validate_signal(&next.signal_policy)?;
}
Self::ReplaceGovernancePolicy(patch) => {
let policy = &patch.policy;
next.governance_policy.default_action = policy
.default_action
.unwrap_or(current.governance_policy.default_action);
next.governance_policy.rules = policy.rules.clone();
next.governance_policy.vetoed_tools = policy.vetoed_tools.clone();
next.governance_policy.rate_limits = policy.rate_limits.clone();
next.governance_policy.constraints = policy.constraints.clone();
super::config::validate_governance(
&next.governance_policy,
current.kernel_limits.collection_limits.governance_rules,
)?;
}
Self::TightenResourceQuota(patch) => {
next.resource_quota = patch.tighten(¤t.resource_quota)?;
validate_quota(&next.resource_quota)?;
}
Self::ReplaceRecoveryPolicy(patch) => {
if patch.policy.tail_bounds.is_some() {
return Err(super::envelope::WireRejection::new(
super::envelope::WireRejectionKind::PolicyViolation,
"recovery_policy.tail_bounds is boot-only: it is frozen in the genesis \
record and cannot be patched live",
));
}
next.recovery_policy = ResolvedRecoveryPolicy {
provider_recovery_attempts: patch
.policy
.provider_recovery_attempts
.unwrap_or(current.recovery_policy.provider_recovery_attempts),
output_recovery_attempts: patch
.policy
.output_recovery_attempts
.unwrap_or(current.recovery_policy.output_recovery_attempts),
tail_bounds: current.recovery_policy.tail_bounds,
};
validate_recovery(&next.recovery_policy)?;
}
}
Ok(next)
}
}
impl TightenResourceQuota {
pub fn tighten(
&self,
current: &super::config::ResourceQuota,
) -> Result<super::config::ResourceQuota, super::envelope::WireRejection> {
use super::envelope::{WireRejection, WireRejectionKind};
fn narrow(
label: &str,
requested: Option<u32>,
current: Option<u32>,
) -> Result<Option<u32>, WireRejection> {
match (requested, current) {
(None, current) => Ok(current),
(Some(requested), Some(current)) if requested > current => Err(WireRejection::new(
WireRejectionKind::PolicyViolation,
format!(
"policy patch raises {label} from {current} to {requested}; \
a live quota change may only tighten — growing one is a reservation \
fact and needs its own contract"
),
)),
(Some(requested), _) => Ok(Some(requested)),
}
}
Ok(super::config::ResourceQuota {
max_concurrent_subagents: narrow(
"max_concurrent_subagents",
self.max_concurrent_subagents,
current.max_concurrent_subagents,
)?,
max_total_subagents: narrow(
"max_total_subagents",
self.max_total_subagents,
current.max_total_subagents,
)?,
max_spawn_depth: narrow(
"max_spawn_depth",
self.max_spawn_depth,
current.max_spawn_depth,
)?,
max_workflow_nodes: narrow(
"max_workflow_nodes",
self.max_workflow_nodes,
current.max_workflow_nodes,
)?,
memory_writes_per_window: current.memory_writes_per_window.clone(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::kernel::wire::config::{
ConfigDefaults, HostEffectSupport, OperationConfig, ResolvedOperationConfig, ResourceQuota,
};
use crate::runtime::kernel::wire::effect::EffectKindTag;
use crate::runtime::kernel::wire::envelope::WireRejectionKind;
use serde_json::json;
fn resolved(quota: Option<ResourceQuota>) -> ResolvedOperationConfig {
OperationConfig {
resource_quota: quota,
host_effect_support: HostEffectSupport::new([
EffectKindTag::CallProvider,
EffectKindTag::SpawnTasks,
EffectKindTag::PreemptTasks,
]),
..OperationConfig::default()
}
.resolve(&ConfigDefaults::default())
.expect("baseline config resolves")
}
fn quota() -> ResourceQuota {
ResourceQuota {
max_concurrent_subagents: Some(4),
max_total_subagents: Some(16),
max_spawn_depth: Some(3),
max_workflow_nodes: Some(64),
memory_writes_per_window: None,
}
}
fn patch(expected_revision: u64, patch: LivePolicyPatch) -> ApplyPolicyPatchCommand {
ApplyPolicyPatchCommand {
expected_revision: WireU64::new(expected_revision),
patch,
}
}
fn tighten(quota: TightenResourceQuota) -> LivePolicyPatch {
LivePolicyPatch::TightenResourceQuota(quota)
}
#[test]
fn a_patch_at_the_current_revision_applies_and_advances_it() {
let mut state = LivePolicyState::new(resolved(Some(quota())));
assert_eq!(state.revision(), WireU64::ZERO);
let next = state
.apply(&patch(
0,
tighten(TightenResourceQuota {
max_concurrent_subagents: Some(2),
..TightenResourceQuota::default()
}),
))
.expect("a patch at the current revision applies");
assert_eq!(next, WireU64::new(1));
assert_eq!(state.revision(), WireU64::new(1));
assert_eq!(
state.config().resource_quota.max_concurrent_subagents,
Some(2)
);
assert_eq!(state.config().resource_quota.max_total_subagents, Some(16));
}
#[test]
fn a_stale_revision_is_refused_and_changes_nothing() {
let mut state = LivePolicyState::new(resolved(Some(quota())));
state
.apply(&patch(
0,
tighten(TightenResourceQuota {
max_spawn_depth: Some(2),
..TightenResourceQuota::default()
}),
))
.unwrap();
let before = state.clone();
let rejection = state
.apply(&patch(
0,
tighten(TightenResourceQuota {
max_spawn_depth: Some(1),
..TightenResourceQuota::default()
}),
))
.expect_err("a stale patch must not silently overwrite");
assert_eq!(rejection.kind, WireRejectionKind::PolicyViolation);
assert!(rejection.message.contains("revision mismatch"));
assert_eq!(state, before, "a refused patch left state behind");
assert!(
state
.apply(&patch(
1,
tighten(TightenResourceQuota {
max_spawn_depth: Some(1),
..TightenResourceQuota::default()
})
))
.is_ok()
);
}
#[test]
fn a_future_revision_is_refused_too() {
let mut state = LivePolicyState::new(resolved(Some(quota())));
let rejection = state
.apply(&patch(9, tighten(TightenResourceQuota::default())))
.expect_err("a patch from the future is not a valid rebase either");
assert!(rejection.message.contains("revision mismatch"));
assert_eq!(state.revision(), WireU64::ZERO);
}
#[test]
fn the_revision_is_mandatory_on_the_wire() {
assert!(
serde_json::from_value::<ApplyPolicyPatchCommand>(json!({
"patch": { "kind": "replace_signal_policy", "policy": { "queue_max": 8 } },
}))
.is_err(),
"a patch without a revision silently overwrites another writer"
);
}
#[test]
fn no_policy_carries_a_version_of_its_own() {
assert!(
serde_json::from_value::<SignalPolicy>(json!({ "queue_max": 8, "version": 1 }))
.is_err()
);
assert!(serde_json::from_value::<GovernancePolicy>(json!({ "version": 1 })).is_err());
assert!(serde_json::from_value::<RecoveryPolicy>(json!({ "version": 1 })).is_err());
}
#[test]
fn a_live_quota_change_may_only_tighten() {
let mut state = LivePolicyState::new(resolved(Some(quota())));
for (label, widening) in [
(
"max_concurrent_subagents",
TightenResourceQuota {
max_concurrent_subagents: Some(99),
..TightenResourceQuota::default()
},
),
(
"max_total_subagents",
TightenResourceQuota {
max_total_subagents: Some(99),
..TightenResourceQuota::default()
},
),
(
"max_spawn_depth",
TightenResourceQuota {
max_spawn_depth: Some(9),
..TightenResourceQuota::default()
},
),
(
"max_workflow_nodes",
TightenResourceQuota {
max_workflow_nodes: Some(999),
..TightenResourceQuota::default()
},
),
] {
let before = state.clone();
let rejection = state
.apply(&patch(0, tighten(widening)))
.expect_err("widening must be refused, not clamped");
assert!(
rejection.message.contains("may only tighten"),
"{label}: unexpected message {}",
rejection.message
);
assert!(rejection.message.contains(label));
assert_eq!(state, before, "{label}: a refused patch left state behind");
}
}
#[test]
fn capping_a_previously_uncapped_axis_is_a_tightening() {
let mut state = LivePolicyState::new(resolved(None));
assert_eq!(state.config().resource_quota.max_spawn_depth, None);
state
.apply(&patch(
0,
tighten(TightenResourceQuota {
max_spawn_depth: Some(2),
..TightenResourceQuota::default()
}),
))
.expect("uncapped ⇒ capped narrows the surface");
assert_eq!(state.config().resource_quota.max_spawn_depth, Some(2));
}
#[test]
fn an_absent_axis_means_unchanged_not_cleared() {
let mut state = LivePolicyState::new(resolved(Some(quota())));
state
.apply(&patch(0, tighten(TightenResourceQuota::default())))
.unwrap();
assert_eq!(state.config().resource_quota, quota());
}
#[test]
fn a_patch_that_fails_validation_leaves_the_revision_alone() {
let mut state = LivePolicyState::new(resolved(Some(quota())));
let before = state.clone();
let rejection = state
.apply(&patch(
0,
LivePolicyPatch::ReplaceSignalPolicy(ReplaceSignalPolicy {
policy: SignalPolicy {
queue_max: 0,
ttl_ms: None,
deadline_escalation: None,
},
}),
))
.expect_err("a zero-length signal queue drops every signal");
assert!(rejection.message.contains("queue_max"));
assert_eq!(state, before);
}
#[test]
fn replacing_the_governance_policy_reuses_the_boot_validator() {
let mut state = LivePolicyState::new(resolved(None));
let rejection = state
.apply(&patch(
0,
LivePolicyPatch::ReplaceGovernancePolicy(ReplaceGovernancePolicy {
policy: GovernancePolicy {
constraints: vec![ParamConstraint::Enum(EnumParam {
tool: "write".to_string(),
param_path: "mode".to_string(),
values: Vec::new(),
})],
..GovernancePolicy::default()
},
}),
))
.expect_err("an enum constraint with no permitted value denies every call");
assert!(rejection.message.contains("no permitted value"));
state
.apply(&patch(
0,
LivePolicyPatch::ReplaceGovernancePolicy(ReplaceGovernancePolicy {
policy: GovernancePolicy {
default_action: Some(PolicyAction::Deny),
rules: vec![PolicyRule {
tool_pattern: "read.*".to_string(),
action: PolicyAction::Allow,
}],
..GovernancePolicy::default()
},
}),
))
.expect("a well-formed governance replacement applies");
assert_eq!(
state.config().governance_policy.default_action,
PolicyAction::Deny
);
}
#[test]
fn recovery_ladders_stay_inside_their_ceiling_live_too() {
let mut state = LivePolicyState::new(resolved(None));
let rejection = state
.apply(&patch(
0,
LivePolicyPatch::ReplaceRecoveryPolicy(ReplaceRecoveryPolicy {
policy: RecoveryPolicy {
provider_recovery_attempts: Some(64),
output_recovery_attempts: None,
tail_bounds: None,
},
}),
))
.expect_err("an unbounded recovery ladder is a livelock");
assert!(rejection.message.contains("provider_recovery_attempts"));
}
#[test]
fn the_tail_bound_is_boot_only_even_though_its_policy_is_live() {
let mut state = LivePolicyState::new(resolved(None));
let frozen = state.config().recovery_policy.tail_bounds;
let rejection = state
.apply(&patch(
0,
LivePolicyPatch::ReplaceRecoveryPolicy(ReplaceRecoveryPolicy {
policy: RecoveryPolicy {
provider_recovery_attempts: None,
output_recovery_attempts: None,
tail_bounds: Some(TailBoundsPolicy {
hard_records: Some(WireU64::new(1_000_000)),
..TailBoundsPolicy::default()
}),
},
}),
))
.expect_err("the tail bound is not live-mutable");
assert!(rejection.message.contains("boot-only"), "{rejection}");
assert_eq!(
state.config().recovery_policy.tail_bounds,
frozen,
"a refused patch changes nothing"
);
state
.apply(&patch(
0,
LivePolicyPatch::ReplaceRecoveryPolicy(ReplaceRecoveryPolicy {
policy: RecoveryPolicy {
provider_recovery_attempts: Some(3),
output_recovery_attempts: None,
tail_bounds: None,
},
}),
))
.expect("the semantic ladders are live-mutable");
assert_eq!(state.config().recovery_policy.provider_recovery_attempts, 3);
assert_eq!(state.config().recovery_policy.tail_bounds, frozen);
}
#[test]
fn the_live_policy_union_is_closed_to_boot_only_policies() {
for unlisted in [
"replace_context_policy",
"replace_execution_policy",
"replace_scheduler_policy",
"replace_payload_policy",
"replace_feature_policy",
"set_tools",
"set_knowledge_budget",
"set_scheduler_budget",
"set_memory_policy",
"set_tokenizer",
] {
let error = serde_json::from_value::<LivePolicyPatch>(json!({ "kind": unlisted }))
.expect_err("only the four §13.2 patches exist");
assert!(
error.to_string().contains("unknown variant"),
"{unlisted}: {error}"
);
}
}
#[test]
fn every_live_command_is_a_13_2_capability() {
let commands = [
json!({ "kind": "cancel", "reason": "user" }),
json!({ "kind": "update_deadline", "deadline_ms": "1700000000000" }),
json!({ "kind": "update_task", "update": { "progress": "halfway" } }),
json!({ "kind": "apply_capability_patch", "patch": {} }),
json!({ "kind": "apply_knowledge_mutation", "mutation": {} }),
json!({ "kind": "force_compact" }),
json!({ "kind": "apply_skill_activation", "activate": [{ "name": "research" }] }),
json!({
"kind": "apply_policy_patch",
"expected_revision": "0",
"patch": { "kind": "replace_signal_policy", "policy": { "queue_max": 8 } },
}),
json!({ "kind": "seed_knowledge", "entries": [] }),
];
for command in commands {
serde_json::from_value::<HostCommand>(command.clone())
.unwrap_or_else(|e| panic!("{command}: {e}"));
}
for boot_only in [
json!({ "kind": "configure_run", "config": {} }),
json!({ "kind": "set_tools", "tools": [] }),
json!({ "kind": "load_governance_policy" }),
json!({ "kind": "set_resource_quota", "quota": {} }),
json!({ "kind": "set_scheduler_budget", "max_wall_ms": "1" }),
json!({ "kind": "set_memory_policy", "memory_path": "/tmp" }),
json!({ "kind": "resume" }),
json!({ "kind": "spawn_sub_agent" }),
json!({ "kind": "page_in", "entries": [] }),
] {
assert!(
serde_json::from_value::<HostCommand>(boot_only.clone()).is_err(),
"{boot_only} must not decode as a live command"
);
}
}
#[test]
fn host_and_model_task_updates_do_not_share_a_wire_variant() {
use crate::runtime::kernel::wire::syscall::SyscallRequest;
let update = json!({ "plan": ["a", "b"], "current_step": 1 });
let host: HostCommand =
serde_json::from_value(json!({ "kind": "update_task", "update": update })).unwrap();
let model: SyscallRequest =
serde_json::from_value(json!({ "kind": "update_task", "update": update })).unwrap();
match (host, model) {
(HostCommand::UpdateTask(host), SyscallRequest::UpdateTask(model)) => {
assert_eq!(host.update, model.update);
}
other => panic!("unexpected decode: {other:?}"),
}
}
#[test]
fn range_constraints_are_fixed_point_not_floats() {
assert!(
serde_json::from_value::<ParamConstraint>(json!({
"kind": "range", "tool": "sample", "param_path": "temperature", "min": 0.0, "max": 1.0,
}))
.is_err(),
"an authoritative range bound must not be a language-default float"
);
let parsed: ParamConstraint = serde_json::from_value(json!({
"kind": "range", "tool": "sample", "param_path": "temperature",
"min_micros": 0, "max_micros": 1_000_000,
}))
.unwrap();
assert!(parsed.validate().is_ok());
}
#[test]
fn a_range_constraint_that_can_never_hold_is_rejected() {
for constraint in [
ParamConstraint::Range(RangeParam {
tool: "sample".to_string(),
param_path: "t".to_string(),
min_micros: None,
max_micros: None,
}),
ParamConstraint::Range(RangeParam {
tool: "sample".to_string(),
param_path: "t".to_string(),
min_micros: Some(2_000_000),
max_micros: Some(1_000_000),
}),
ParamConstraint::Required(RequiredParam {
tool: String::new(),
param_path: "t".to_string(),
}),
] {
assert!(constraint.validate().is_err());
}
}
#[test]
fn cancel_does_not_repeat_the_operation_id() {
assert!(
serde_json::from_value::<CancelCommand>(json!({
"reason": "user", "operation_id": "op-1",
}))
.is_err(),
"the envelope owns the operation id; repeating it forced three SDKs to special-case cancel"
);
}
}