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 救援槽保留,不受日常副控生命周期影响。
|
- 独立 Linux 救援槽保留,不受日常副控生命周期影响。
|
||||||
|
|
||||||
|
第一份源码合同位于 `guanghu-os/crates/supervisor`。它已经把请求、策略、能力白名单、授权、
|
||||||
|
回滚检查点、救援槽、状态迁移、目标读回和失败回收绑定为一个确定性闭环。发生唤醒尝试后,
|
||||||
|
任何失败都必须回收;只有再次读回副控为 `DORMANT` 才能解除锁定。该合同已有隔离测试,
|
||||||
|
但尚未接入真实 Linux 虚拟机或容器后端,也没有取得京东整机启动权。
|
||||||
|
|
||||||
### 阶段 F:裸机研究后端
|
### 阶段 F:裸机研究后端
|
||||||
|
|
||||||
- 现有 GOSK/GHAL 候选继续作为研究和专用设备后端;
|
- 现有 GOSK/GHAL 候选继续作为研究和专用设备后端;
|
||||||
|
|
@ -119,6 +124,7 @@
|
||||||
- 第一条类型化协议执行桥;
|
- 第一条类型化协议执行桥;
|
||||||
- systemd 状态与重启动作的白名单、授权、回滚和目标读回合同;
|
- systemd 状态与重启动作的白名单、授权、回滚和目标读回合同;
|
||||||
- 光湖语言主控的世界启动门、系统服务依赖、公共入口验收和服务器回执合同;
|
- 光湖语言主控的世界启动门、系统服务依赖、公共入口验收和服务器回执合同;
|
||||||
|
- 光湖监督器与 Linux 副控生命周期的类型化源码合同、失败回收和锁死语义;
|
||||||
- 京东节点已将 `guanghu-language-primary.target` 设为默认目标,并完成一次真实重启后
|
- 京东节点已将 `guanghu-language-primary.target` 设为默认目标,并完成一次真实重启后
|
||||||
`PASS_100` 回读;本次启动总耗时 36.146 秒;
|
`PASS_100` 回读;本次启动总耗时 36.146 秒;
|
||||||
- 对应单元和集成测试。
|
- 对应单元和集成测试。
|
||||||
|
|
@ -129,7 +135,8 @@
|
||||||
- 全部注册协议的工程实现;
|
- 全部注册协议的工程实现;
|
||||||
- 普通 Linux 管理入口的紧急维护边界收紧。
|
- 普通 Linux 管理入口的紧急维护边界收紧。
|
||||||
- 光湖独立先启动的监督器;
|
- 光湖独立先启动的监督器;
|
||||||
- Linux 平时休眠、按需唤醒、目标读回和任务后收回的完整生命周期。
|
- 监督器到真实隔离 Linux 后端的接线;
|
||||||
|
- Linux 平时休眠、按需唤醒、目标读回和任务后收回的实体生命周期。
|
||||||
|
|
||||||
因此,当前京东有界语言服务控制层为100;最终整机光湖 OS 主控为0。Linux-free 或删除
|
因此,当前京东有界语言服务控制层为100;最终整机光湖 OS 主控为0。Linux-free 或删除
|
||||||
Linux 不在最终完成公式中。
|
Linux 不在最终完成公式中。
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,13 @@ dependencies = [
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "guanghu-supervisor"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,6 @@ members = [
|
||||||
"crates/ghctl",
|
"crates/ghctl",
|
||||||
"crates/hldp-native-compiler",
|
"crates/hldp-native-compiler",
|
||||||
"crates/hldp-runtime",
|
"crates/hldp-runtime",
|
||||||
|
"crates/supervisor",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,27 @@ The registered translation chain is:
|
||||||
TCS -> HLDP -> GLC -> GIR -> BTCP -> GOSK -> GHAL -> hardware
|
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
|
## Language-primary boot target
|
||||||
|
|
||||||
`guanghu-language-primary.target` makes the accepted cognitive-control model
|
`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_library \
|
||||||
--test compiler_command \
|
--test compiler_command \
|
||||||
--test world_manifest \
|
--test world_manifest \
|
||||||
|
--test supervisor_lifecycle \
|
||||||
--no-report
|
--no-report
|
||||||
cargo llvm-cov report --manifest-path "$1/Cargo.toml" \
|
cargo llvm-cov report --manifest-path "$1/Cargo.toml" \
|
||||||
--ignore-filename-regex "/src/main\\.rs$" \
|
--ignore-filename-regex "/src/main\\.rs$" \
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"schema": "guanghu.os-control-architecture/v1",
|
"schema": "guanghu.os-control-architecture/v1",
|
||||||
"record_id": "HLP-GUANGHU-OS-CONTROL-001",
|
"record_id": "HLP-GUANGHU-OS-CONTROL-001",
|
||||||
"version": "2026-08-10.1",
|
"version": "2026-08-15.1",
|
||||||
"state": "CURRENT_CANONICAL",
|
"state": "CURRENT_CANONICAL",
|
||||||
"final_topology": "GUANGHU_MASTER_WITH_ON_DEMAND_LINUX_SUBCONTROL_AND_RESCUE",
|
"final_topology": "GUANGHU_MASTER_WITH_ON_DEMAND_LINUX_SUBCONTROL_AND_RESCUE",
|
||||||
"control_ownership": {
|
"control_ownership": {
|
||||||
|
|
@ -39,7 +39,16 @@
|
||||||
"linux_on_demand_subcontrol": 0,
|
"linux_on_demand_subcontrol": 0,
|
||||||
"linux_rescue_preserved": 100,
|
"linux_rescue_preserved": 100,
|
||||||
"final_guanghu_os_master": 0,
|
"final_guanghu_os_master": 0,
|
||||||
"next_engineering_gate": "IMPLEMENT_AND_ISOLATION_TEST_GUANGHU_SUPERVISOR_AND_LINUX_SUBCONTROL_LIFECYCLE_BEFORE_REAL_BOOT_CUTOVER"
|
"next_engineering_gate": "INTEGRATE_A_REAL_ISOLATED_LINUX_BACKEND_AND_GUANGHU_FIRST_BOOT_SUPERVISOR_BEFORE_JD_PHYSICAL_CUTOVER"
|
||||||
|
},
|
||||||
|
"implementation": {
|
||||||
|
"guanghu_supervisor_lifecycle_contract_source": 100,
|
||||||
|
"typed_policy_authorization_and_rollback_binding": 100,
|
||||||
|
"target_readback_and_mandatory_reclaim_state_machine": 100,
|
||||||
|
"declared_supervisor_core_line_and_function_coverage": 100,
|
||||||
|
"real_isolated_linux_backend": 0,
|
||||||
|
"independent_guanghu_first_boot_supervisor": 0,
|
||||||
|
"jd_physical_deployment": 0
|
||||||
},
|
},
|
||||||
"historical_evidence_policy": {
|
"historical_evidence_policy": {
|
||||||
"bare_metal_gosk_ghal_receipts_remain_valid_for_observed_capabilities": true,
|
"bare_metal_gosk_ghal_receipts_remain_valid_for_observed_capabilities": true,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,16 @@ test("current JD state stays transitional and cannot impersonate final master co
|
||||||
assert.equal(contract.jd_fd_primary.final_guanghu_os_master, 0);
|
assert.equal(contract.jd_fd_primary.final_guanghu_os_master, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("source lifecycle progress does not impersonate backend, boot, or JD deployment", () => {
|
||||||
|
assert.equal(contract.version, "2026-08-15.1");
|
||||||
|
assert.equal(contract.implementation.guanghu_supervisor_lifecycle_contract_source, 100);
|
||||||
|
assert.equal(contract.implementation.target_readback_and_mandatory_reclaim_state_machine, 100);
|
||||||
|
assert.equal(contract.implementation.declared_supervisor_core_line_and_function_coverage, 100);
|
||||||
|
assert.equal(contract.implementation.real_isolated_linux_backend, 0);
|
||||||
|
assert.equal(contract.implementation.independent_guanghu_first_boot_supervisor, 0);
|
||||||
|
assert.equal(contract.implementation.jd_physical_deployment, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test("current architecture and global engineering rules project the same control contract", () => {
|
test("current architecture and global engineering rules project the same control contract", () => {
|
||||||
assert.equal(architecture.version, "2026-08-14.1");
|
assert.equal(architecture.version, "2026-08-14.1");
|
||||||
assert.equal(architecture.server_os_control.machine_projection, "routing/guanghu-os-control-architecture.json");
|
assert.equal(architecture.server_os_control.machine_projection, "routing/guanghu-os-control-architecture.json");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue