feat: add Guanghu Linux subcontrol supervisor contract
This commit is contained in:
parent
ebe2ecf262
commit
36996be39a
10 changed files with 803 additions and 3 deletions
|
|
@ -105,6 +105,11 @@
|
|||
- 执行后目标读回,随后停止或冻结副控环境并登记资源回收回执;
|
||||
- 独立 Linux 救援槽保留,不受日常副控生命周期影响。
|
||||
|
||||
第一份源码合同位于 `guanghu-os/crates/supervisor`。它已经把请求、策略、能力白名单、授权、
|
||||
回滚检查点、救援槽、状态迁移、目标读回和失败回收绑定为一个确定性闭环。发生唤醒尝试后,
|
||||
任何失败都必须回收;只有再次读回副控为 `DORMANT` 才能解除锁定。该合同已有隔离测试,
|
||||
但尚未接入真实 Linux 虚拟机或容器后端,也没有取得京东整机启动权。
|
||||
|
||||
### 阶段 F:裸机研究后端
|
||||
|
||||
- 现有 GOSK/GHAL 候选继续作为研究和专用设备后端;
|
||||
|
|
@ -119,6 +124,7 @@
|
|||
- 第一条类型化协议执行桥;
|
||||
- systemd 状态与重启动作的白名单、授权、回滚和目标读回合同;
|
||||
- 光湖语言主控的世界启动门、系统服务依赖、公共入口验收和服务器回执合同;
|
||||
- 光湖监督器与 Linux 副控生命周期的类型化源码合同、失败回收和锁死语义;
|
||||
- 京东节点已将 `guanghu-language-primary.target` 设为默认目标,并完成一次真实重启后
|
||||
`PASS_100` 回读;本次启动总耗时 36.146 秒;
|
||||
- 对应单元和集成测试。
|
||||
|
|
@ -129,7 +135,8 @@
|
|||
- 全部注册协议的工程实现;
|
||||
- 普通 Linux 管理入口的紧急维护边界收紧。
|
||||
- 光湖独立先启动的监督器;
|
||||
- Linux 平时休眠、按需唤醒、目标读回和任务后收回的完整生命周期。
|
||||
- 监督器到真实隔离 Linux 后端的接线;
|
||||
- Linux 平时休眠、按需唤醒、目标读回和任务后收回的实体生命周期。
|
||||
|
||||
因此,当前京东有界语言服务控制层为100;最终整机光湖 OS 主控为0。Linux-free 或删除
|
||||
Linux 不在最终完成公式中。
|
||||
|
|
|
|||
|
|
@ -40,6 +40,13 @@ dependencies = [
|
|||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guanghu-supervisor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ members = [
|
|||
"crates/ghctl",
|
||||
"crates/hldp-native-compiler",
|
||||
"crates/hldp-runtime",
|
||||
"crates/supervisor",
|
||||
]
|
||||
resolver = "2"
|
||||
|
|
|
|||
|
|
@ -57,6 +57,27 @@ The registered translation chain is:
|
|||
TCS -> HLDP -> GLC -> GIR -> BTCP -> GOSK -> GHAL -> hardware
|
||||
```
|
||||
|
||||
## Guanghu supervisor and Linux subcontrol lifecycle
|
||||
|
||||
`crates/supervisor` implements the first fail-closed source contract for
|
||||
ADR-0175. A typed, policy-bound capability request can advance only through:
|
||||
|
||||
```text
|
||||
DORMANT -> STARTING -> READY -> EXECUTING -> VERIFYING -> RECLAIMING -> DORMANT
|
||||
```
|
||||
|
||||
The request must bind the exact node, subject, capability, authorization,
|
||||
rollback checkpoint, backend, required protocol chain, and preserved rescue
|
||||
slot. Every failure after a wake attempt enters reclaim. A receipt can report
|
||||
`PASS_100` only after target-side readback and a second observation proving the
|
||||
Linux subcontrol returned to `DORMANT`; a failed reclaim is
|
||||
`FAIL_0_LOCKED`.
|
||||
|
||||
The deterministic contract and isolation harness are implemented and covered
|
||||
at 100% of declared source lines and functions. No production Linux
|
||||
subcontrol backend, independent Guanghu boot supervisor, JD boot change, or
|
||||
physical cutover is implied by that source result.
|
||||
|
||||
## Language-primary boot target
|
||||
|
||||
`guanghu-language-primary.target` makes the accepted cognitive-control model
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "guanghu-supervisor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
description = "Fail-closed lifecycle contract for Guanghu-controlled on-demand Linux subcontrol"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const REQUEST_SCHEMA: &str = "guanghu.linux-subcontrol-request/v1";
|
||||
pub const POLICY_SCHEMA: &str = "guanghu.supervisor-policy/v1";
|
||||
|
||||
pub const REQUIRED_PROTOCOL_CHAIN: [&str; 12] = [
|
||||
"GLS-0301", // message envelope
|
||||
"GLS-0302", // identity
|
||||
"GLS-0303", // context
|
||||
"GLS-0306", // receipt
|
||||
"GLS-0309", // work order and authorization
|
||||
"GLS-0311", // witness
|
||||
"GLS-0130", // compiler
|
||||
"GLS-0131", // deterministic representation
|
||||
"GLS-0709", // adapter
|
||||
"GLS-0710", // immutable module
|
||||
"GLS-0803", // bounded execution lifecycle
|
||||
"GLS-0819", // resource reclaim
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct LifecycleRequest {
|
||||
pub schema: String,
|
||||
pub request_id: String,
|
||||
pub subject_id: String,
|
||||
pub target_node_id: String,
|
||||
pub protocol_chain: Vec<String>,
|
||||
pub capability: String,
|
||||
pub authorization_id: String,
|
||||
pub rollback_checkpoint_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct CapabilityGrant {
|
||||
pub capability: String,
|
||||
pub backend_id: String,
|
||||
pub authorization_id: String,
|
||||
pub rollback_checkpoint_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SupervisorPolicy {
|
||||
pub schema: String,
|
||||
pub policy_id: String,
|
||||
pub target_node_id: String,
|
||||
pub rescue_slot_id: String,
|
||||
pub capability_grants: Vec<CapabilityGrant>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct LifecyclePlan {
|
||||
pub request_id: String,
|
||||
pub subject_id: String,
|
||||
pub target_node_id: String,
|
||||
pub policy_id: String,
|
||||
pub capability: String,
|
||||
pub backend_id: String,
|
||||
pub authorization_id: String,
|
||||
pub rollback_checkpoint_id: String,
|
||||
pub rescue_slot_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ObservedSubcontrolState {
|
||||
Dormant,
|
||||
Ready,
|
||||
Active,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SupervisorState {
|
||||
Dormant,
|
||||
Starting,
|
||||
Ready,
|
||||
Executing,
|
||||
Verifying,
|
||||
Reclaiming,
|
||||
FailedLocked,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct LifecycleReceipt {
|
||||
pub schema: String,
|
||||
pub request_id: String,
|
||||
pub subject_id: String,
|
||||
pub target_node_id: String,
|
||||
pub policy_id: String,
|
||||
pub capability: String,
|
||||
pub backend_id: String,
|
||||
pub authorization_id: String,
|
||||
pub rollback_checkpoint_id: String,
|
||||
pub rescue_slot_id: String,
|
||||
pub rescue_verified: bool,
|
||||
pub target_state_verified: bool,
|
||||
pub reclaim_attempted: bool,
|
||||
pub reclaim_verified: bool,
|
||||
pub final_state: String,
|
||||
pub failure_stage: Option<String>,
|
||||
pub failure_detail: Option<String>,
|
||||
pub transitions: Vec<SupervisorState>,
|
||||
}
|
||||
|
||||
pub trait LifecycleBackend {
|
||||
fn rescue_available(&self, rescue_slot_id: &str) -> Result<bool, String>;
|
||||
fn observe_state(&self, backend_id: &str) -> Result<ObservedSubcontrolState, String>;
|
||||
fn wake(&self, backend_id: &str) -> Result<(), String>;
|
||||
fn execute(&self, backend_id: &str, capability: &str) -> Result<(), String>;
|
||||
fn verify_target(&self, backend_id: &str, capability: &str) -> Result<bool, String>;
|
||||
fn reclaim(&self, backend_id: &str) -> Result<(), String>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SupervisorError(String);
|
||||
|
||||
impl SupervisorError {
|
||||
fn new(message: impl Into<String>) -> Self {
|
||||
Self(message.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SupervisorError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for SupervisorError {}
|
||||
|
||||
pub fn compile_request(
|
||||
request: &LifecycleRequest,
|
||||
policy: &SupervisorPolicy,
|
||||
) -> Result<LifecyclePlan, SupervisorError> {
|
||||
require(
|
||||
request.schema == REQUEST_SCHEMA,
|
||||
"unsupported request schema",
|
||||
)?;
|
||||
require(policy.schema == POLICY_SCHEMA, "unsupported policy schema")?;
|
||||
require(
|
||||
!request.request_id.trim().is_empty(),
|
||||
"request_id is required",
|
||||
)?;
|
||||
require(
|
||||
!request.subject_id.trim().is_empty(),
|
||||
"subject_id is required",
|
||||
)?;
|
||||
require(
|
||||
request.target_node_id == policy.target_node_id,
|
||||
"request target does not match policy target",
|
||||
)?;
|
||||
require(
|
||||
!policy.rescue_slot_id.trim().is_empty(),
|
||||
"an independent Linux rescue slot is required",
|
||||
)?;
|
||||
|
||||
for protocol in REQUIRED_PROTOCOL_CHAIN {
|
||||
require(
|
||||
request
|
||||
.protocol_chain
|
||||
.iter()
|
||||
.any(|candidate| candidate == protocol),
|
||||
format!("required protocol is missing: {protocol}"),
|
||||
)?;
|
||||
}
|
||||
|
||||
let grant = policy
|
||||
.capability_grants
|
||||
.iter()
|
||||
.find(|grant| {
|
||||
grant.capability == request.capability
|
||||
&& grant.authorization_id == request.authorization_id
|
||||
&& grant.rollback_checkpoint_id == request.rollback_checkpoint_id
|
||||
})
|
||||
.ok_or_else(|| SupervisorError::new("capability mutation is not granted by policy"))?;
|
||||
require(
|
||||
!grant.backend_id.trim().is_empty(),
|
||||
"backend_id is required",
|
||||
)?;
|
||||
|
||||
Ok(LifecyclePlan {
|
||||
request_id: request.request_id.clone(),
|
||||
subject_id: request.subject_id.clone(),
|
||||
target_node_id: request.target_node_id.clone(),
|
||||
policy_id: policy.policy_id.clone(),
|
||||
capability: request.capability.clone(),
|
||||
backend_id: grant.backend_id.clone(),
|
||||
authorization_id: request.authorization_id.clone(),
|
||||
rollback_checkpoint_id: request.rollback_checkpoint_id.clone(),
|
||||
rescue_slot_id: policy.rescue_slot_id.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_lifecycle(
|
||||
plan: &LifecyclePlan,
|
||||
backend: &dyn LifecycleBackend,
|
||||
) -> Result<LifecycleReceipt, SupervisorError> {
|
||||
let rescue_verified = backend
|
||||
.rescue_available(&plan.rescue_slot_id)
|
||||
.map_err(|error| SupervisorError::new(format!("cannot verify rescue slot: {error}")))?;
|
||||
require(
|
||||
rescue_verified,
|
||||
"independent Linux rescue slot is unavailable",
|
||||
)?;
|
||||
|
||||
let initial_state = backend.observe_state(&plan.backend_id).map_err(|error| {
|
||||
SupervisorError::new(format!("cannot observe Linux subcontrol: {error}"))
|
||||
})?;
|
||||
require(
|
||||
initial_state == ObservedSubcontrolState::Dormant,
|
||||
"Linux subcontrol is not dormant before wake",
|
||||
)?;
|
||||
|
||||
let mut transitions = vec![SupervisorState::Dormant, SupervisorState::Starting];
|
||||
if let Err(error) = backend.wake(&plan.backend_id) {
|
||||
return Ok(reclaim_after(
|
||||
plan,
|
||||
backend,
|
||||
rescue_verified,
|
||||
false,
|
||||
"wake",
|
||||
error,
|
||||
transitions,
|
||||
));
|
||||
}
|
||||
|
||||
match backend.observe_state(&plan.backend_id) {
|
||||
Ok(ObservedSubcontrolState::Ready) => transitions.push(SupervisorState::Ready),
|
||||
Ok(state) => {
|
||||
return Ok(reclaim_after(
|
||||
plan,
|
||||
backend,
|
||||
rescue_verified,
|
||||
false,
|
||||
"wake_readiness",
|
||||
format!("unexpected state after wake: {state:?}"),
|
||||
transitions,
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
return Ok(reclaim_after(
|
||||
plan,
|
||||
backend,
|
||||
rescue_verified,
|
||||
false,
|
||||
"wake_readiness",
|
||||
error,
|
||||
transitions,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
transitions.push(SupervisorState::Executing);
|
||||
if let Err(error) = backend.execute(&plan.backend_id, &plan.capability) {
|
||||
return Ok(reclaim_after(
|
||||
plan,
|
||||
backend,
|
||||
rescue_verified,
|
||||
false,
|
||||
"execution",
|
||||
error,
|
||||
transitions,
|
||||
));
|
||||
}
|
||||
|
||||
transitions.push(SupervisorState::Verifying);
|
||||
match backend.verify_target(&plan.backend_id, &plan.capability) {
|
||||
Ok(true) => Ok(reclaim_complete(
|
||||
plan,
|
||||
backend,
|
||||
rescue_verified,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
transitions,
|
||||
)),
|
||||
Ok(false) => Ok(reclaim_after(
|
||||
plan,
|
||||
backend,
|
||||
rescue_verified,
|
||||
false,
|
||||
"target_readback",
|
||||
"target-side readback did not reach the expected state".to_owned(),
|
||||
transitions,
|
||||
)),
|
||||
Err(error) => Ok(reclaim_after(
|
||||
plan,
|
||||
backend,
|
||||
rescue_verified,
|
||||
false,
|
||||
"target_readback",
|
||||
error,
|
||||
transitions,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn reclaim_after(
|
||||
plan: &LifecyclePlan,
|
||||
backend: &dyn LifecycleBackend,
|
||||
rescue_verified: bool,
|
||||
target_state_verified: bool,
|
||||
failure_stage: &str,
|
||||
failure_detail: String,
|
||||
transitions: Vec<SupervisorState>,
|
||||
) -> LifecycleReceipt {
|
||||
reclaim_complete(
|
||||
plan,
|
||||
backend,
|
||||
rescue_verified,
|
||||
target_state_verified,
|
||||
Some(failure_stage.to_owned()),
|
||||
Some(failure_detail),
|
||||
transitions,
|
||||
)
|
||||
}
|
||||
|
||||
fn reclaim_complete(
|
||||
plan: &LifecyclePlan,
|
||||
backend: &dyn LifecycleBackend,
|
||||
rescue_verified: bool,
|
||||
target_state_verified: bool,
|
||||
failure_stage: Option<String>,
|
||||
failure_detail: Option<String>,
|
||||
mut transitions: Vec<SupervisorState>,
|
||||
) -> LifecycleReceipt {
|
||||
transitions.push(SupervisorState::Reclaiming);
|
||||
let reclaim_verified = backend.reclaim(&plan.backend_id).is_ok()
|
||||
&& matches!(
|
||||
backend.observe_state(&plan.backend_id),
|
||||
Ok(ObservedSubcontrolState::Dormant)
|
||||
);
|
||||
if reclaim_verified {
|
||||
transitions.push(SupervisorState::Dormant);
|
||||
} else {
|
||||
transitions.push(SupervisorState::FailedLocked);
|
||||
}
|
||||
|
||||
let passed = target_state_verified && reclaim_verified && failure_stage.is_none();
|
||||
LifecycleReceipt {
|
||||
schema: "guanghu.linux-subcontrol-receipt/v1".to_owned(),
|
||||
request_id: plan.request_id.clone(),
|
||||
subject_id: plan.subject_id.clone(),
|
||||
target_node_id: plan.target_node_id.clone(),
|
||||
policy_id: plan.policy_id.clone(),
|
||||
capability: plan.capability.clone(),
|
||||
backend_id: plan.backend_id.clone(),
|
||||
authorization_id: plan.authorization_id.clone(),
|
||||
rollback_checkpoint_id: plan.rollback_checkpoint_id.clone(),
|
||||
rescue_slot_id: plan.rescue_slot_id.clone(),
|
||||
rescue_verified,
|
||||
target_state_verified,
|
||||
reclaim_attempted: true,
|
||||
reclaim_verified,
|
||||
final_state: if passed {
|
||||
"PASS_100"
|
||||
} else if reclaim_verified {
|
||||
"FAIL_0"
|
||||
} else {
|
||||
"FAIL_0_LOCKED"
|
||||
}
|
||||
.to_owned(),
|
||||
failure_stage,
|
||||
failure_detail,
|
||||
transitions,
|
||||
}
|
||||
}
|
||||
|
||||
fn require(condition: bool, message: impl Into<String>) -> Result<(), SupervisorError> {
|
||||
if condition {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SupervisorError::new(message))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,357 @@
|
|||
use std::{cell::RefCell, collections::VecDeque};
|
||||
|
||||
use guanghu_supervisor::{
|
||||
compile_request, run_lifecycle, CapabilityGrant, LifecycleBackend, LifecycleRequest,
|
||||
ObservedSubcontrolState, SupervisorPolicy, SupervisorState, POLICY_SCHEMA, REQUEST_SCHEMA,
|
||||
REQUIRED_PROTOCOL_CHAIN,
|
||||
};
|
||||
|
||||
fn request() -> LifecycleRequest {
|
||||
LifecycleRequest {
|
||||
schema: REQUEST_SCHEMA.to_owned(),
|
||||
request_id: "REQ-JD-SUBCONTROL-001".to_owned(),
|
||||
subject_id: "ICE-P-ZY001".to_owned(),
|
||||
target_node_id: "JD-FD-PRIMARY".to_owned(),
|
||||
protocol_chain: REQUIRED_PROTOCOL_CHAIN
|
||||
.iter()
|
||||
.map(|protocol| (*protocol).to_owned())
|
||||
.collect(),
|
||||
capability: "public-entry-readback".to_owned(),
|
||||
authorization_id: "AUTH-JD-SUBCONTROL-001".to_owned(),
|
||||
rollback_checkpoint_id: "CHECKPOINT-JD-LINUX-RESCUE-001".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn policy() -> SupervisorPolicy {
|
||||
SupervisorPolicy {
|
||||
schema: POLICY_SCHEMA.to_owned(),
|
||||
policy_id: "JD-GUANGHU-SUPERVISOR-001".to_owned(),
|
||||
target_node_id: "JD-FD-PRIMARY".to_owned(),
|
||||
rescue_slot_id: "ubuntu-maintenance".to_owned(),
|
||||
capability_grants: vec![CapabilityGrant {
|
||||
capability: "public-entry-readback".to_owned(),
|
||||
backend_id: "isolated-linux-fixture".to_owned(),
|
||||
authorization_id: "AUTH-JD-SUBCONTROL-001".to_owned(),
|
||||
rollback_checkpoint_id: "CHECKPOINT-JD-LINUX-RESCUE-001".to_owned(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compiles_only_an_exact_authorized_capability() {
|
||||
let plan = compile_request(&request(), &policy()).expect("compile exact grant");
|
||||
|
||||
assert_eq!(plan.backend_id, "isolated-linux-fixture");
|
||||
assert_eq!(plan.capability, "public-entry-readback");
|
||||
assert_eq!(plan.rescue_slot_id, "ubuntu-maintenance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_protocol_and_invented_authority() {
|
||||
let mut missing_protocol = request();
|
||||
missing_protocol
|
||||
.protocol_chain
|
||||
.retain(|protocol| protocol != "GLS-0819");
|
||||
let error =
|
||||
compile_request(&missing_protocol, &policy()).expect_err("missing lifecycle protocol");
|
||||
assert!(error.to_string().contains("GLS-0819"));
|
||||
|
||||
let mut invented = request();
|
||||
invented.authorization_id = "AUTH-INVENTED".to_owned();
|
||||
let error = compile_request(&invented, &policy()).expect_err("invented grant");
|
||||
assert!(error.to_string().contains("not granted"));
|
||||
}
|
||||
|
||||
struct FakeBackend {
|
||||
rescue_result: Result<bool, String>,
|
||||
states: RefCell<VecDeque<Result<ObservedSubcontrolState, String>>>,
|
||||
wake_result: Result<(), String>,
|
||||
execute_result: Result<(), String>,
|
||||
readback_result: Result<bool, String>,
|
||||
reclaim_result: Result<(), String>,
|
||||
calls: RefCell<Vec<String>>,
|
||||
}
|
||||
|
||||
impl FakeBackend {
|
||||
fn successful() -> Self {
|
||||
Self {
|
||||
rescue_result: Ok(true),
|
||||
states: RefCell::new(
|
||||
[
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
Ok(ObservedSubcontrolState::Ready),
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
]
|
||||
.into(),
|
||||
),
|
||||
wake_result: Ok(()),
|
||||
execute_result: Ok(()),
|
||||
readback_result: Ok(true),
|
||||
reclaim_result: Ok(()),
|
||||
calls: RefCell::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LifecycleBackend for FakeBackend {
|
||||
fn rescue_available(&self, rescue_slot_id: &str) -> Result<bool, String> {
|
||||
self.calls
|
||||
.borrow_mut()
|
||||
.push(format!("rescue:{rescue_slot_id}"));
|
||||
self.rescue_result.clone()
|
||||
}
|
||||
|
||||
fn observe_state(&self, backend_id: &str) -> Result<ObservedSubcontrolState, String> {
|
||||
self.calls
|
||||
.borrow_mut()
|
||||
.push(format!("observe:{backend_id}"));
|
||||
self.states
|
||||
.borrow_mut()
|
||||
.pop_front()
|
||||
.unwrap_or_else(|| Err("unexpected state observation".to_owned()))
|
||||
}
|
||||
|
||||
fn wake(&self, backend_id: &str) -> Result<(), String> {
|
||||
self.calls.borrow_mut().push(format!("wake:{backend_id}"));
|
||||
self.wake_result.clone()
|
||||
}
|
||||
|
||||
fn execute(&self, backend_id: &str, capability: &str) -> Result<(), String> {
|
||||
self.calls
|
||||
.borrow_mut()
|
||||
.push(format!("execute:{backend_id}:{capability}"));
|
||||
self.execute_result.clone()
|
||||
}
|
||||
|
||||
fn verify_target(&self, backend_id: &str, capability: &str) -> Result<bool, String> {
|
||||
self.calls
|
||||
.borrow_mut()
|
||||
.push(format!("verify:{backend_id}:{capability}"));
|
||||
self.readback_result.clone()
|
||||
}
|
||||
|
||||
fn reclaim(&self, backend_id: &str) -> Result<(), String> {
|
||||
self.calls
|
||||
.borrow_mut()
|
||||
.push(format!("reclaim:{backend_id}"));
|
||||
self.reclaim_result.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_wake_execute_verify_and_reclaim_to_dormant() {
|
||||
let plan = compile_request(&request(), &policy()).expect("compile");
|
||||
let backend = FakeBackend::successful();
|
||||
let receipt = run_lifecycle(&plan, &backend).expect("run lifecycle");
|
||||
|
||||
assert_eq!(receipt.final_state, "PASS_100");
|
||||
assert!(receipt.target_state_verified);
|
||||
assert!(receipt.reclaim_verified);
|
||||
assert!(receipt.rescue_verified);
|
||||
assert_eq!(
|
||||
receipt.transitions,
|
||||
[
|
||||
SupervisorState::Dormant,
|
||||
SupervisorState::Starting,
|
||||
SupervisorState::Ready,
|
||||
SupervisorState::Executing,
|
||||
SupervisorState::Verifying,
|
||||
SupervisorState::Reclaiming,
|
||||
SupervisorState::Dormant,
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
backend.calls.borrow().last().unwrap(),
|
||||
"observe:isolated-linux-fixture"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_target_readback_is_fail_zero_but_reclaims_linux() {
|
||||
let plan = compile_request(&request(), &policy()).expect("compile");
|
||||
let mut backend = FakeBackend::successful();
|
||||
backend.readback_result = Ok(false);
|
||||
let receipt = run_lifecycle(&plan, &backend).expect("bounded failure receipt");
|
||||
|
||||
assert_eq!(receipt.final_state, "FAIL_0");
|
||||
assert!(!receipt.target_state_verified);
|
||||
assert!(receipt.reclaim_attempted);
|
||||
assert!(receipt.reclaim_verified);
|
||||
assert_eq!(receipt.failure_stage.as_deref(), Some("target_readback"));
|
||||
assert_eq!(receipt.transitions.last(), Some(&SupervisorState::Dormant));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reclaim_failure_locks_the_supervisor_instead_of_claiming_safety() {
|
||||
let plan = compile_request(&request(), &policy()).expect("compile");
|
||||
let mut backend = FakeBackend::successful();
|
||||
backend.readback_result = Ok(false);
|
||||
backend.reclaim_result = Err("cannot stop subordinate".to_owned());
|
||||
backend.states = RefCell::new(
|
||||
[
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
Ok(ObservedSubcontrolState::Ready),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
let receipt = run_lifecycle(&plan, &backend).expect("locked failure receipt");
|
||||
|
||||
assert_eq!(receipt.final_state, "FAIL_0_LOCKED");
|
||||
assert!(!receipt.reclaim_verified);
|
||||
assert_eq!(
|
||||
receipt.transitions.last(),
|
||||
Some(&SupervisorState::FailedLocked)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_rescue_or_non_dormant_linux_prevents_wake() {
|
||||
let plan = compile_request(&request(), &policy()).expect("compile");
|
||||
let mut no_rescue = FakeBackend::successful();
|
||||
no_rescue.rescue_result = Ok(false);
|
||||
let error = run_lifecycle(&plan, &no_rescue).expect_err("rescue is mandatory");
|
||||
assert!(error.to_string().contains("rescue"));
|
||||
assert!(!no_rescue
|
||||
.calls
|
||||
.borrow()
|
||||
.iter()
|
||||
.any(|call| call.starts_with("wake:")));
|
||||
|
||||
let mut active = FakeBackend::successful();
|
||||
active.states = RefCell::new([Ok(ObservedSubcontrolState::Ready)].into());
|
||||
let error = run_lifecycle(&plan, &active).expect_err("must begin dormant");
|
||||
assert!(error.to_string().contains("not dormant"));
|
||||
assert!(!active
|
||||
.calls
|
||||
.borrow()
|
||||
.iter()
|
||||
.any(|call| call.starts_with("wake:")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_validation_fails_closed_for_every_required_boundary() {
|
||||
let mut invalid_request = request();
|
||||
invalid_request.schema = "wrong".to_owned();
|
||||
assert!(compile_request(&invalid_request, &policy()).is_err());
|
||||
|
||||
let mut invalid_policy = policy();
|
||||
invalid_policy.schema = "wrong".to_owned();
|
||||
assert!(compile_request(&request(), &invalid_policy).is_err());
|
||||
|
||||
let mut missing_request_id = request();
|
||||
missing_request_id.request_id = " ".to_owned();
|
||||
assert!(compile_request(&missing_request_id, &policy()).is_err());
|
||||
|
||||
let mut missing_subject = request();
|
||||
missing_subject.subject_id.clear();
|
||||
assert!(compile_request(&missing_subject, &policy()).is_err());
|
||||
|
||||
let mut wrong_target = request();
|
||||
wrong_target.target_node_id = "OTHER-NODE".to_owned();
|
||||
assert!(compile_request(&wrong_target, &policy()).is_err());
|
||||
|
||||
let mut missing_rescue = policy();
|
||||
missing_rescue.rescue_slot_id.clear();
|
||||
assert!(compile_request(&request(), &missing_rescue).is_err());
|
||||
|
||||
let mut wrong_capability = request();
|
||||
wrong_capability.capability = "unregistered".to_owned();
|
||||
assert!(compile_request(&wrong_capability, &policy()).is_err());
|
||||
|
||||
let mut wrong_rollback = request();
|
||||
wrong_rollback.rollback_checkpoint_id = "CHECKPOINT-INVENTED".to_owned();
|
||||
assert!(compile_request(&wrong_rollback, &policy()).is_err());
|
||||
|
||||
let mut empty_backend = policy();
|
||||
empty_backend.capability_grants[0].backend_id.clear();
|
||||
assert!(compile_request(&request(), &empty_backend).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observation_errors_before_wake_do_not_mutate_linux() {
|
||||
let plan = compile_request(&request(), &policy()).expect("compile");
|
||||
let mut rescue_error = FakeBackend::successful();
|
||||
rescue_error.rescue_result = Err("rescue probe unavailable".to_owned());
|
||||
let error = run_lifecycle(&plan, &rescue_error).expect_err("rescue probe error");
|
||||
assert!(error.to_string().contains("cannot verify rescue"));
|
||||
|
||||
let mut state_error = FakeBackend::successful();
|
||||
state_error.states = RefCell::new([Err("state probe unavailable".to_owned())].into());
|
||||
let error = run_lifecycle(&plan, &state_error).expect_err("state probe error");
|
||||
assert!(error.to_string().contains("cannot observe"));
|
||||
assert!(!state_error
|
||||
.calls
|
||||
.borrow()
|
||||
.iter()
|
||||
.any(|call| call.starts_with("wake:")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_post_wake_failure_attempts_reclaim() {
|
||||
let plan = compile_request(&request(), &policy()).expect("compile");
|
||||
|
||||
let mut wake_failure = FakeBackend::successful();
|
||||
wake_failure.wake_result = Err("wake failed".to_owned());
|
||||
wake_failure.states = RefCell::new(
|
||||
[
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
let receipt = run_lifecycle(&plan, &wake_failure).expect("wake failure receipt");
|
||||
assert_eq!(receipt.failure_stage.as_deref(), Some("wake"));
|
||||
assert!(receipt.reclaim_verified);
|
||||
|
||||
let mut not_ready = FakeBackend::successful();
|
||||
not_ready.states = RefCell::new(
|
||||
[
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
Ok(ObservedSubcontrolState::Active),
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
let receipt = run_lifecycle(&plan, ¬_ready).expect("not-ready failure receipt");
|
||||
assert_eq!(receipt.failure_stage.as_deref(), Some("wake_readiness"));
|
||||
|
||||
let mut readiness_error = FakeBackend::successful();
|
||||
readiness_error.states = RefCell::new(
|
||||
[
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
Err("readiness probe failed".to_owned()),
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
let receipt = run_lifecycle(&plan, &readiness_error).expect("readiness failure receipt");
|
||||
assert_eq!(receipt.failure_stage.as_deref(), Some("wake_readiness"));
|
||||
|
||||
let mut execution_error = FakeBackend::successful();
|
||||
execution_error.execute_result = Err("capability failed".to_owned());
|
||||
let receipt = run_lifecycle(&plan, &execution_error).expect("execution failure receipt");
|
||||
assert_eq!(receipt.failure_stage.as_deref(), Some("execution"));
|
||||
|
||||
let mut readback_error = FakeBackend::successful();
|
||||
readback_error.readback_result = Err("readback probe failed".to_owned());
|
||||
let receipt = run_lifecycle(&plan, &readback_error).expect("readback failure receipt");
|
||||
assert_eq!(receipt.failure_stage.as_deref(), Some("target_readback"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reclaim_requires_a_final_dormant_readback() {
|
||||
let plan = compile_request(&request(), &policy()).expect("compile");
|
||||
let mut backend = FakeBackend::successful();
|
||||
backend.states = RefCell::new(
|
||||
[
|
||||
Ok(ObservedSubcontrolState::Dormant),
|
||||
Ok(ObservedSubcontrolState::Ready),
|
||||
Ok(ObservedSubcontrolState::Failed),
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
|
||||
let receipt = run_lifecycle(&plan, &backend).expect("locked receipt");
|
||||
assert_eq!(receipt.final_state, "FAIL_0_LOCKED");
|
||||
assert!(!receipt.reclaim_verified);
|
||||
}
|
||||
|
|
@ -98,6 +98,7 @@ run_gate auditable_line_coverage_100_percent \
|
|||
--test compiler_library \
|
||||
--test compiler_command \
|
||||
--test world_manifest \
|
||||
--test supervisor_lifecycle \
|
||||
--no-report
|
||||
cargo llvm-cov report --manifest-path "$1/Cargo.toml" \
|
||||
--ignore-filename-regex "/src/main\\.rs$" \
|
||||
|
|
|
|||
Loading…
Reference in a new issue