1722 lines
68 KiB
Rust
1722 lines
68 KiB
Rust
//! HoloLake GLS 原生协议内核。
|
|
//!
|
|
//! P1-P6 的协议在这里成为确定性的消息、身份、上下文、状态机和回执规则;
|
|
//! P7 只暴露真实节点能力装配状态,没有目标侧证据时永远不冒充物理能力。
|
|
|
|
use ring::digest::{digest, SHA256};
|
|
use rusqlite::{params, Connection, OptionalExtension, Transaction, TransactionBehavior};
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{Map, Value};
|
|
use std::collections::{BTreeMap, HashSet};
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Mutex;
|
|
use std::time::Duration;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
use tauri::{AppHandle, Manager};
|
|
use uuid::Uuid;
|
|
|
|
use crate::direct_local_session::{
|
|
authenticate_context_at, direct_session_root, AuthenticateSessionInput,
|
|
};
|
|
|
|
const EMBEDDED_KERNEL: &str = include_str!("../../contracts/gls-native-runtime-kernel.json");
|
|
const KERNEL_SCHEMA: &str = "hololake.gls-native-runtime-kernel/v1";
|
|
const REQUEST_SCHEMA: &str = "hololake.protocol-decision-request/v1";
|
|
const RECEIPT_SCHEMA: &str = "hololake.protocol-decision-receipt/v1";
|
|
const SOURCE_COMMIT: &str = "104a5d73162bdf4a529701e65898e2bc2863ea9e";
|
|
const MAX_TEXT_BYTES: usize = 512;
|
|
static LEDGER_SCHEMA_LOCK: Mutex<()> = Mutex::new(());
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
struct KernelContract {
|
|
schema: String,
|
|
record_id: String,
|
|
source_commit: String,
|
|
receipt_schema: String,
|
|
decision_set: Vec<String>,
|
|
runtime_boundaries: RuntimeBoundaries,
|
|
stages: Vec<KernelStage>,
|
|
p7_node_assemblies: Vec<NodeAssembly>,
|
|
lifecycle_states: Vec<String>,
|
|
broadcast_actions: Vec<String>,
|
|
work_order_stages: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
struct RuntimeBoundaries {
|
|
identity_is_authority: bool,
|
|
model_is_persona: bool,
|
|
model_can_override_decision: bool,
|
|
raw_protocol_text_executed: bool,
|
|
arbitrary_external_code_executed: bool,
|
|
last_write_wins_on_concurrent_state: bool,
|
|
target_evidence_required_for_completion: bool,
|
|
every_decision_writes_receipt: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
pub struct KernelStage {
|
|
pub id: String,
|
|
pub state: String,
|
|
pub protocols: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct NodeAssembly {
|
|
protocol_id: String,
|
|
target: String,
|
|
state: String,
|
|
source_evidence_node: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
struct ProtocolDecisionInput {
|
|
session: AuthenticateSessionInput,
|
|
request: ProtocolDecisionRequest,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
struct ProtocolDecisionRequest {
|
|
schema: String,
|
|
request_id: String,
|
|
idempotency_key: String,
|
|
event_kind: String,
|
|
human_controller_id: String,
|
|
subject: ProtocolIdentityReference,
|
|
target: ProtocolIdentityReference,
|
|
context: ProtocolContext,
|
|
issued_at_unix_ms: u64,
|
|
expires_at_unix_ms: u64,
|
|
payload_sha256: String,
|
|
state_before_digest: String,
|
|
state_after_digest: String,
|
|
#[serde(default)]
|
|
evidence_refs: Vec<String>,
|
|
operation: Value,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
struct ProtocolIdentityReference {
|
|
canonical_id: String,
|
|
subject_kind: String,
|
|
instance_id: String,
|
|
domain_id: String,
|
|
registry_ref: String,
|
|
proof_ref: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
struct ProtocolContext {
|
|
world_id: String,
|
|
domain_id: String,
|
|
channel_id: String,
|
|
task_id: String,
|
|
authorization_ref: String,
|
|
protocol_bundle_commit: String,
|
|
visibility_scope: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProtocolDecisionReceipt {
|
|
pub schema: String,
|
|
pub receipt_id: String,
|
|
pub sequence: u64,
|
|
pub previous_receipt_sha256: String,
|
|
pub request_id: String,
|
|
pub event_kind: String,
|
|
pub human_controller_id: String,
|
|
pub subject_id: String,
|
|
pub target_id: String,
|
|
pub protocol_bundle_commit: String,
|
|
pub protocol_bundle_sha256: String,
|
|
pub protocol_set: Vec<String>,
|
|
pub adapter_id: String,
|
|
pub input_digest: String,
|
|
pub decision: String,
|
|
pub reason_codes: Vec<String>,
|
|
pub state_before_digest: String,
|
|
pub state_after_digest: String,
|
|
pub evidence_refs: Vec<String>,
|
|
pub time_authority: String,
|
|
pub observed_at_unix_ms: u64,
|
|
pub idempotency_key: String,
|
|
pub signer: String,
|
|
pub receipt_sha256: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProtocolKernelSnapshot {
|
|
pub state: String,
|
|
pub contract_id: String,
|
|
pub source_commit: String,
|
|
pub executable_protocol_count: usize,
|
|
pub implemented_stage_count: usize,
|
|
pub decision_receipt_count: u64,
|
|
pub allow_count: u64,
|
|
pub deny_count: u64,
|
|
pub ambiguous_count: u64,
|
|
pub unverified_count: u64,
|
|
pub last_receipt_sha256: String,
|
|
pub stages: Vec<KernelStage>,
|
|
pub p7_node_assemblies: Vec<NodeAssembly>,
|
|
pub p7_verified_physical_capability_count: usize,
|
|
pub raw_protocol_text_executed: bool,
|
|
pub model_can_override_decision: bool,
|
|
pub bootstrap_compiler_self_check: String,
|
|
pub bootstrap_golden_gir_sha256: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct CompileHldpResult {
|
|
pub decision_receipt: ProtocolDecisionReceipt,
|
|
pub gir: Option<crate::gls_bootstrap_compiler::GirProgram>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct Decision {
|
|
adapter: &'static str,
|
|
decision: &'static str,
|
|
reasons: Vec<String>,
|
|
}
|
|
|
|
fn load_contract() -> Result<KernelContract, String> {
|
|
let contract: KernelContract = serde_json::from_str(EMBEDDED_KERNEL)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_KERNEL_CONTRACT_INVALID: {error}"))?;
|
|
validate_contract(&contract)?;
|
|
Ok(contract)
|
|
}
|
|
|
|
fn validate_contract(contract: &KernelContract) -> Result<(), String> {
|
|
let stages = contract
|
|
.stages
|
|
.iter()
|
|
.map(|stage| stage.id.as_str())
|
|
.collect::<Vec<_>>();
|
|
let decisions = contract
|
|
.decision_set
|
|
.iter()
|
|
.map(String::as_str)
|
|
.collect::<HashSet<_>>();
|
|
if contract.schema != KERNEL_SCHEMA
|
|
|| contract.record_id != "HLP-GLS-NATIVE-RUNTIME-KERNEL-001"
|
|
|| contract.source_commit != SOURCE_COMMIT
|
|
|| contract.receipt_schema != RECEIPT_SCHEMA
|
|
|| decisions != HashSet::from(["ALLOW", "DENY", "AMBIGUOUS", "UNVERIFIED"])
|
|
|| stages != ["P1", "P2", "P3", "P4", "P5", "P6", "P7"]
|
|
|| contract.stages[..6]
|
|
.iter()
|
|
.any(|stage| !stage.state.starts_with("IMPLEMENTED"))
|
|
|| contract.p7_node_assemblies.len() != 11
|
|
|| contract.runtime_boundaries.identity_is_authority
|
|
|| contract.runtime_boundaries.model_is_persona
|
|
|| contract.runtime_boundaries.model_can_override_decision
|
|
|| contract.runtime_boundaries.raw_protocol_text_executed
|
|
|| contract.runtime_boundaries.arbitrary_external_code_executed
|
|
|| contract
|
|
.runtime_boundaries
|
|
.last_write_wins_on_concurrent_state
|
|
|| !contract
|
|
.runtime_boundaries
|
|
.target_evidence_required_for_completion
|
|
|| !contract.runtime_boundaries.every_decision_writes_receipt
|
|
|| contract.lifecycle_states.len() != 14
|
|
|| contract.broadcast_actions.len() != 9
|
|
|| contract.work_order_stages != ["REGISTERED", "TESTED", "PUBLISHED", "DEPLOYED"]
|
|
{
|
|
return Err("HOLOLAKE_GLS_KERNEL_BOUNDARY_INVALID".into());
|
|
}
|
|
if contract.p7_node_assemblies.iter().any(|assembly| {
|
|
assembly.target != "DESKTOP_HOLOLAKE"
|
|
|| assembly.source_evidence_node.is_empty()
|
|
|| !assembly.protocol_id.starts_with("GLS-")
|
|
|| assembly.state == "ACTIVE_HEALTHY"
|
|
}) {
|
|
return Err("HOLOLAKE_GLS_P7_FALSE_PHYSICAL_CAPABILITY".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn now_unix_ms() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|value| value.as_millis() as u64)
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn sha256(bytes: &[u8]) -> String {
|
|
digest(&SHA256, bytes)
|
|
.as_ref()
|
|
.iter()
|
|
.map(|byte| format!("{byte:02x}"))
|
|
.collect()
|
|
}
|
|
|
|
fn is_digest(value: &str) -> bool {
|
|
value.len() == 64 && value.chars().all(|character| character.is_ascii_hexdigit())
|
|
}
|
|
|
|
fn valid_text(value: &str) -> bool {
|
|
!value.trim().is_empty()
|
|
&& value.len() <= MAX_TEXT_BYTES
|
|
&& !value.chars().any(char::is_control)
|
|
}
|
|
|
|
fn operation_digest(operation: &Value) -> Result<String, String> {
|
|
serde_json::to_vec(operation)
|
|
.map(|bytes| sha256(&bytes))
|
|
.map_err(|error| format!("HOLOLAKE_GLS_OPERATION_SERIALIZE_FAILED: {error}"))
|
|
}
|
|
|
|
fn value_string<'a>(object: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
|
|
object
|
|
.get(key)
|
|
.and_then(Value::as_str)
|
|
.filter(|value| valid_text(value))
|
|
}
|
|
|
|
fn value_u64(object: &Map<String, Value>, key: &str) -> Option<u64> {
|
|
object.get(key).and_then(Value::as_u64)
|
|
}
|
|
|
|
fn allowed_operation_fields(event: &str) -> &'static [&'static str] {
|
|
match event {
|
|
"MESSAGE_VALIDATE" | "IDENTITY_VERIFY" | "CONTEXT_RESOLVE" | "DECISION_RECEIPT" => &[],
|
|
"HEARTBEAT_OBSERVE" => &["leaseExpiresAtUnixMs", "healthDigest"],
|
|
"WORK_ORDER_TRANSITION" => &["proposerId", "approverId", "fromStage", "toStage"],
|
|
"LIVE_SESSION_OBSERVE" => &["connected", "serverReceiptRef"],
|
|
"WITNESS_APPEND" => &["targetEvidenceRef"],
|
|
"MEMORY_SYNC" | "STATE_SYNC" => &["causalRelation", "localVersion", "remoteVersion"],
|
|
"TIME_CONTINUITY" => &[
|
|
"previousSequence",
|
|
"nextSequence",
|
|
"primaryInstanceId",
|
|
"leaseExpiresAtUnixMs",
|
|
],
|
|
"MODULE_ADMIT" => &[
|
|
"moduleId",
|
|
"immutableVersion",
|
|
"contentDigest",
|
|
"provenanceRef",
|
|
"signatureRef",
|
|
],
|
|
"LIFECYCLE_TRANSITION" => &["fromState", "toState"],
|
|
"RUNWAY_ASSIGN" => &[
|
|
"cpuUnits",
|
|
"memoryBytes",
|
|
"channelIsolation",
|
|
"isolationKey",
|
|
],
|
|
"RUNWAY_RELEASE" => &["resourcesReleased", "targetEvidenceRef", "isolationKey"],
|
|
"BROADCAST_TRANSITION" => &["epoch", "controllerInstanceId", "fromAction", "toAction"],
|
|
"EXTERNAL_ADAPTER_TRANSLATE" => &[
|
|
"externalKind",
|
|
"normalizedDigest",
|
|
"sourceRef",
|
|
"requestsExecutionAuthority",
|
|
],
|
|
"MODEL_ROUTE" => &[
|
|
"modelId",
|
|
"modelIsPersona",
|
|
"grantsWorldAuthority",
|
|
"privacyClass",
|
|
"healthReceiptRef",
|
|
],
|
|
"TEMPORARY_CAPABILITY" => &[
|
|
"sandboxed",
|
|
"permanentInstall",
|
|
"autoPublish",
|
|
"autoDeploy",
|
|
"cleanupPlanDigest",
|
|
],
|
|
"HLDP_NP_VALIDATE" | "GIR_VALIDATE" | "COMPILE_HLDP" => &[
|
|
"sourceDigest",
|
|
"unresolvedNaturalLanguage",
|
|
"unresolvedPermissions",
|
|
"goldenVectorDigest",
|
|
],
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
fn adapter_for(event: &str) -> Option<&'static str> {
|
|
match event {
|
|
"MESSAGE_VALIDATE" => Some("glp-envelope-codec"),
|
|
"IDENTITY_VERIFY" => Some("glp-identity-reference"),
|
|
"CONTEXT_RESOLVE" => Some("glp-context-guard"),
|
|
"DECISION_RECEIPT" => Some("glp-decision-kernel"),
|
|
"HEARTBEAT_OBSERVE"
|
|
| "WORK_ORDER_TRANSITION"
|
|
| "LIVE_SESSION_OBSERVE"
|
|
| "WITNESS_APPEND" => Some("glp-live-coordination"),
|
|
"MEMORY_SYNC" | "STATE_SYNC" | "TIME_CONTINUITY" => Some("glp-continuity-kernel"),
|
|
"MODULE_ADMIT"
|
|
| "LIFECYCLE_TRANSITION"
|
|
| "RUNWAY_ASSIGN"
|
|
| "RUNWAY_RELEASE"
|
|
| "BROADCAST_TRANSITION" => Some("gls-execution-control"),
|
|
"EXTERNAL_ADAPTER_TRANSLATE" | "MODEL_ROUTE" | "TEMPORARY_CAPABILITY" => {
|
|
Some("gls-external-resource-boundary")
|
|
}
|
|
"HLDP_NP_VALIDATE" | "GIR_VALIDATE" | "COMPILE_HLDP" => Some("gls-bootstrap-compiler"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn decide(
|
|
request: &ProtocolDecisionRequest,
|
|
session_account: &str,
|
|
session_id: &str,
|
|
observed_at: u64,
|
|
) -> Decision {
|
|
let deny = |code: &str| Decision {
|
|
adapter: adapter_for(&request.event_kind).unwrap_or("glp-decision-kernel"),
|
|
decision: "DENY",
|
|
reasons: vec![code.into()],
|
|
};
|
|
let ambiguous = |code: &str| Decision {
|
|
adapter: adapter_for(&request.event_kind).unwrap_or("glp-decision-kernel"),
|
|
decision: "AMBIGUOUS",
|
|
reasons: vec![code.into()],
|
|
};
|
|
let unverified = |code: &str| Decision {
|
|
adapter: adapter_for(&request.event_kind).unwrap_or("glp-decision-kernel"),
|
|
decision: "UNVERIFIED",
|
|
reasons: vec![code.into()],
|
|
};
|
|
let allow = |code: &str| Decision {
|
|
adapter: adapter_for(&request.event_kind).unwrap_or("glp-decision-kernel"),
|
|
decision: "ALLOW",
|
|
reasons: vec![code.into()],
|
|
};
|
|
|
|
if request.schema != REQUEST_SCHEMA
|
|
|| !valid_text(&request.request_id)
|
|
|| !valid_text(&request.idempotency_key)
|
|
{
|
|
return deny("GLP_ENVELOPE_INCOMPLETE");
|
|
}
|
|
if request.human_controller_id != session_account {
|
|
return deny("GLP_IDENTITY_SESSION_CONFLICT");
|
|
}
|
|
for identity in [&request.subject, &request.target] {
|
|
if !valid_text(&identity.canonical_id)
|
|
|| !valid_text(&identity.subject_kind)
|
|
|| !valid_text(&identity.instance_id)
|
|
|| !valid_text(&identity.domain_id)
|
|
|| !valid_text(&identity.registry_ref)
|
|
|| !valid_text(&identity.proof_ref)
|
|
{
|
|
return deny("GLP_IDENTITY_REFERENCE_INCOMPLETE");
|
|
}
|
|
}
|
|
if request.context.domain_id != request.target.domain_id
|
|
|| request.subject.domain_id != request.context.domain_id
|
|
{
|
|
return ambiguous("GLP_CONTEXT_DOMAIN_CONFLICT");
|
|
}
|
|
if !valid_text(&request.context.world_id)
|
|
|| !valid_text(&request.context.channel_id)
|
|
|| !valid_text(&request.context.task_id)
|
|
|| !valid_text(&request.context.visibility_scope)
|
|
{
|
|
return ambiguous("GLP_CONTEXT_INCOMPLETE");
|
|
}
|
|
if request.context.protocol_bundle_commit != SOURCE_COMMIT {
|
|
return deny("GLP_PROTOCOL_BUNDLE_DRIFT");
|
|
}
|
|
if request.issued_at_unix_ms > observed_at.saturating_add(300_000)
|
|
|| request.expires_at_unix_ms <= observed_at
|
|
|| request.expires_at_unix_ms <= request.issued_at_unix_ms
|
|
{
|
|
return deny("GLP_ENVELOPE_EXPIRED_OR_TIME_INVALID");
|
|
}
|
|
if !is_digest(&request.payload_sha256)
|
|
|| operation_digest(&request.operation).ok().as_deref()
|
|
!= Some(request.payload_sha256.as_str())
|
|
|| !is_digest(&request.state_before_digest)
|
|
|| !is_digest(&request.state_after_digest)
|
|
{
|
|
return deny("GLP_INPUT_OR_STATE_DIGEST_DRIFT");
|
|
}
|
|
let Some(adapter) = adapter_for(&request.event_kind) else {
|
|
return deny("GLP_EVENT_KIND_UNKNOWN");
|
|
};
|
|
let Some(object) = request.operation.as_object() else {
|
|
return deny("GLP_OPERATION_OBJECT_REQUIRED");
|
|
};
|
|
let allowed_fields = allowed_operation_fields(&request.event_kind);
|
|
if object
|
|
.keys()
|
|
.any(|key| !allowed_fields.contains(&key.as_str()))
|
|
{
|
|
return deny("GLP_OPERATION_FIELD_UNKNOWN");
|
|
}
|
|
if !matches!(
|
|
request.event_kind.as_str(),
|
|
"MESSAGE_VALIDATE"
|
|
| "IDENTITY_VERIFY"
|
|
| "CONTEXT_RESOLVE"
|
|
| "HEARTBEAT_OBSERVE"
|
|
| "LIVE_SESSION_OBSERVE"
|
|
) && request.context.authorization_ref != format!("direct-session://{session_id}")
|
|
{
|
|
return unverified("GLP_AUTHORIZATION_EVIDENCE_MISSING");
|
|
}
|
|
|
|
let result = match request.event_kind.as_str() {
|
|
"MESSAGE_VALIDATE" | "IDENTITY_VERIFY" | "CONTEXT_RESOLVE" | "DECISION_RECEIPT" => {
|
|
allow("GLP_CONTRACT_VALID")
|
|
}
|
|
"HEARTBEAT_OBSERVE" => match (
|
|
value_u64(object, "leaseExpiresAtUnixMs"),
|
|
value_string(object, "healthDigest"),
|
|
) {
|
|
(Some(expiry), Some(hash)) if expiry > observed_at && is_digest(hash) => {
|
|
allow("GLP_HEARTBEAT_CURRENT")
|
|
}
|
|
_ => unverified("GLP_HEARTBEAT_STALE_OR_UNPROVEN"),
|
|
},
|
|
"WORK_ORDER_TRANSITION" => {
|
|
let proposer = value_string(object, "proposerId");
|
|
let approver = value_string(object, "approverId");
|
|
let from = value_string(object, "fromStage");
|
|
let to = value_string(object, "toStage");
|
|
if proposer.is_some() && proposer == approver {
|
|
deny("GLP_WORK_ORDER_SELF_APPROVAL_DENIED")
|
|
} else if approver.is_none() {
|
|
unverified("GLP_WORK_ORDER_APPROVER_MISSING")
|
|
} else if !matches!(
|
|
(from, to),
|
|
(Some("REGISTERED"), Some("TESTED"))
|
|
| (Some("TESTED"), Some("PUBLISHED"))
|
|
| (Some("PUBLISHED"), Some("DEPLOYED"))
|
|
) {
|
|
deny("GLP_WORK_ORDER_STAGE_TRANSITION_INVALID")
|
|
} else {
|
|
allow("GLP_WORK_ORDER_SEPARATION_OF_DUTIES_VALID")
|
|
}
|
|
}
|
|
"LIVE_SESSION_OBSERVE" => {
|
|
if object.get("connected").and_then(Value::as_bool) == Some(true)
|
|
&& value_string(object, "serverReceiptRef").is_some()
|
|
{
|
|
allow("HLSP_CURRENT_SERVER_SESSION_VERIFIED")
|
|
} else {
|
|
unverified("HLSP_OFFLINE_CACHE_IS_NOT_LIVE_STATE")
|
|
}
|
|
}
|
|
"WITNESS_APPEND" => {
|
|
if value_string(object, "targetEvidenceRef").is_some() {
|
|
allow("GLOW_TARGET_EVIDENCE_APPENDED")
|
|
} else {
|
|
unverified("GLOW_TARGET_EVIDENCE_MISSING")
|
|
}
|
|
}
|
|
"MEMORY_SYNC" | "STATE_SYNC" => {
|
|
let local = value_u64(object, "localVersion");
|
|
let remote = value_u64(object, "remoteVersion");
|
|
match (value_string(object, "causalRelation"), local, remote) {
|
|
(Some("CONCURRENT"), Some(_), Some(_)) => {
|
|
ambiguous("GLP_CONCURRENT_VERSIONS_PRESERVED")
|
|
}
|
|
(Some("EQUAL"), Some(left), Some(right)) if left == right => {
|
|
allow("GLP_CAUSAL_SYNC_VALID")
|
|
}
|
|
(Some("BEFORE"), Some(left), Some(right)) if left < right => {
|
|
allow("GLP_CAUSAL_SYNC_VALID")
|
|
}
|
|
(Some("AFTER"), Some(left), Some(right)) if left > right => {
|
|
allow("GLP_CAUSAL_SYNC_VALID")
|
|
}
|
|
_ => deny("GLP_CAUSAL_SYNC_RELATION_INVALID"),
|
|
}
|
|
}
|
|
"TIME_CONTINUITY" => {
|
|
let previous = value_u64(object, "previousSequence");
|
|
let next = value_u64(object, "nextSequence");
|
|
let primary = value_string(object, "primaryInstanceId");
|
|
let lease = value_u64(object, "leaseExpiresAtUnixMs");
|
|
if previous
|
|
.zip(next)
|
|
.map_or(true, |(left, right)| right != left.saturating_add(1))
|
|
{
|
|
deny("PTCP_NON_MONOTONIC_EVENT_SEQUENCE")
|
|
} else if primary != Some(request.subject.instance_id.as_str()) {
|
|
deny("PTCP_DOUBLE_PRIMARY_OR_OWNER_CONFLICT")
|
|
} else if lease.map_or(true, |expiry| expiry <= observed_at) {
|
|
unverified("PTCP_PRIMARY_LEASE_EXPIRED")
|
|
} else {
|
|
allow("PTCP_MONOTONIC_SINGLE_PRIMARY_VALID")
|
|
}
|
|
}
|
|
"MODULE_ADMIT" => {
|
|
let content = value_string(object, "contentDigest");
|
|
if content.map_or(true, |value| !is_digest(value))
|
|
|| value_string(object, "moduleId").is_none()
|
|
|| value_string(object, "immutableVersion").is_none()
|
|
|| value_string(object, "provenanceRef").is_none()
|
|
{
|
|
deny("GMP_MODULE_BACKPACK_INCOMPLETE")
|
|
} else if value_string(object, "signatureRef").is_none() {
|
|
unverified("GMP_MODULE_SIGNATURE_UNVERIFIED")
|
|
} else {
|
|
allow("GMP_IMMUTABLE_MODULE_ADMITTED")
|
|
}
|
|
}
|
|
"LIFECYCLE_TRANSITION" => {
|
|
let contract = load_contract().ok();
|
|
let from = value_string(object, "fromState");
|
|
let to = value_string(object, "toState");
|
|
let adjacent = contract.as_ref().is_some_and(|contract| {
|
|
from.zip(to).is_some_and(|(left, right)| {
|
|
let left_index = contract
|
|
.lifecycle_states
|
|
.iter()
|
|
.position(|state| state == left);
|
|
let right_index = contract
|
|
.lifecycle_states
|
|
.iter()
|
|
.position(|state| state == right);
|
|
matches!((left_index, right_index), (Some(a), Some(b)) if b == a + 1)
|
|
|| matches!(
|
|
(left, right),
|
|
("REGISTERED", "RESIDENT")
|
|
| ("REGISTERED", "DORMANT")
|
|
| ("RETURNED", "DORMANT")
|
|
)
|
|
})
|
|
});
|
|
if adjacent {
|
|
allow("PALP_TRANSITION_VALID")
|
|
} else {
|
|
deny("PALP_TRANSITION_INVALID")
|
|
}
|
|
}
|
|
"RUNWAY_ASSIGN" => {
|
|
if value_u64(object, "cpuUnits").is_some_and(|value| value > 0)
|
|
&& value_u64(object, "memoryBytes").is_some_and(|value| value > 0)
|
|
&& object.get("channelIsolation").and_then(Value::as_bool) == Some(true)
|
|
&& value_string(object, "isolationKey").is_some()
|
|
{
|
|
allow("GRSP_ISOLATED_RUNWAY_ASSIGNED")
|
|
} else {
|
|
deny("GRSP_RESOURCE_OR_ISOLATION_INVALID")
|
|
}
|
|
}
|
|
"RUNWAY_RELEASE" => {
|
|
if object.get("resourcesReleased").and_then(Value::as_bool) == Some(true)
|
|
&& value_string(object, "targetEvidenceRef").is_some()
|
|
&& value_string(object, "isolationKey").is_some()
|
|
{
|
|
allow("GRSP_RESOURCES_RELEASED")
|
|
} else {
|
|
unverified("GRSP_RESOURCE_CLEANUP_UNVERIFIED")
|
|
}
|
|
}
|
|
"BROADCAST_TRANSITION" => {
|
|
let contract = load_contract().ok();
|
|
let from = value_string(object, "fromAction");
|
|
let to = value_string(object, "toAction");
|
|
let adjacent = contract.as_ref().is_some_and(|contract| {
|
|
from.zip(to).is_some_and(|(left, right)| {
|
|
contract
|
|
.broadcast_actions
|
|
.iter()
|
|
.position(|state| state == left)
|
|
.zip(
|
|
contract
|
|
.broadcast_actions
|
|
.iter()
|
|
.position(|state| state == right),
|
|
)
|
|
.is_some_and(|(a, b)| b == a + 1)
|
|
})
|
|
});
|
|
if value_u64(object, "epoch").map_or(true, |value| value == 0)
|
|
|| value_string(object, "controllerInstanceId")
|
|
!= Some(request.subject.instance_id.as_str())
|
|
{
|
|
deny("BTCP_CONTROL_EPOCH_OR_OWNER_INVALID")
|
|
} else if !adjacent {
|
|
deny("BTCP_TRANSITION_INVALID")
|
|
} else {
|
|
allow("BTCP_UNIQUE_CONTROL_EPOCH_TRANSITION_VALID")
|
|
}
|
|
}
|
|
"EXTERNAL_ADAPTER_TRANSLATE" => {
|
|
if object
|
|
.get("requestsExecutionAuthority")
|
|
.and_then(Value::as_bool)
|
|
!= Some(false)
|
|
{
|
|
deny("UAP_CANNOT_REQUEST_EXECUTION_AUTHORITY")
|
|
} else if value_string(object, "normalizedDigest")
|
|
.map_or(true, |value| !is_digest(value))
|
|
|| value_string(object, "sourceRef").is_none()
|
|
{
|
|
deny("UAP_NORMALIZED_SOURCE_INVALID")
|
|
} else {
|
|
allow("UAP_SEMANTIC_TRANSLATION_VALID")
|
|
}
|
|
}
|
|
"MODEL_ROUTE" => {
|
|
if object.get("modelIsPersona").and_then(Value::as_bool) != Some(false)
|
|
|| object.get("grantsWorldAuthority").and_then(Value::as_bool) != Some(false)
|
|
{
|
|
deny("GMRP_MODEL_PERSONA_OR_AUTHORITY_CONFUSION")
|
|
} else if value_string(object, "modelId").is_none()
|
|
|| value_string(object, "healthReceiptRef").is_none()
|
|
{
|
|
unverified("GMRP_MODEL_HEALTH_UNVERIFIED")
|
|
} else {
|
|
allow("GMRP_REPLACEABLE_MODEL_ROUTE_VALID")
|
|
}
|
|
}
|
|
"TEMPORARY_CAPABILITY" => {
|
|
if object.get("sandboxed").and_then(Value::as_bool) != Some(true)
|
|
|| object.get("permanentInstall").and_then(Value::as_bool) != Some(false)
|
|
|| object.get("autoPublish").and_then(Value::as_bool) != Some(false)
|
|
|| object.get("autoDeploy").and_then(Value::as_bool) != Some(false)
|
|
{
|
|
deny("PEN_EPHEMERAL_BOUNDARY_VIOLATION")
|
|
} else if value_string(object, "cleanupPlanDigest")
|
|
.map_or(true, |value| !is_digest(value))
|
|
{
|
|
deny("PEN_CLEANUP_PLAN_REQUIRED")
|
|
} else {
|
|
allow("PEN_SANDBOXED_TEMPORARY_CAPABILITY_VALID")
|
|
}
|
|
}
|
|
"HLDP_NP_VALIDATE" | "GIR_VALIDATE" | "COMPILE_HLDP" => {
|
|
let source = value_string(object, "sourceDigest");
|
|
let golden = value_string(object, "goldenVectorDigest");
|
|
let unresolved_language = object
|
|
.get("unresolvedNaturalLanguage")
|
|
.and_then(Value::as_array);
|
|
let unresolved_permissions = object
|
|
.get("unresolvedPermissions")
|
|
.and_then(Value::as_array);
|
|
if source.map_or(true, |value| !is_digest(value))
|
|
|| golden.map_or(true, |value| !is_digest(value))
|
|
{
|
|
deny("GLC_SOURCE_OR_GOLDEN_VECTOR_DIGEST_INVALID")
|
|
} else if unresolved_language.map_or(true, |items| !items.is_empty())
|
|
|| unresolved_permissions.map_or(true, |items| !items.is_empty())
|
|
{
|
|
deny("GLC_UNRESOLVED_LANGUAGE_OR_PERMISSION_FORBIDDEN")
|
|
} else {
|
|
allow("GLC_BOOTSTRAP_GIR_CONTRACT_VALID")
|
|
}
|
|
}
|
|
_ => deny("GLP_EVENT_KIND_UNKNOWN"),
|
|
};
|
|
Decision { adapter, ..result }
|
|
}
|
|
|
|
fn ledger_path(app: &AppHandle) -> Result<PathBuf, String> {
|
|
Ok(app
|
|
.path()
|
|
.app_data_dir()
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_ROOT_FAILED: {error}"))?
|
|
.join("protocol-runtime")
|
|
.join("protocol-decisions.sqlite3"))
|
|
}
|
|
|
|
pub(crate) fn start_on_application_open(app: &AppHandle) -> Result<(), String> {
|
|
load_contract()?;
|
|
crate::gls_bootstrap_compiler::bootstrap_self_check()?;
|
|
for (adapter, event) in [
|
|
("glp-decision-kernel", "DECISION_RECEIPT"),
|
|
("glp-live-coordination", "LIVE_SESSION_OBSERVE"),
|
|
("glp-continuity-kernel", "TIME_CONTINUITY"),
|
|
("gls-execution-control", "BROADCAST_TRANSITION"),
|
|
("gls-external-resource-boundary", "TEMPORARY_CAPABILITY"),
|
|
("gls-bootstrap-compiler", "COMPILE_HLDP"),
|
|
] {
|
|
crate::gls_protocol_runtime::require_adapter(adapter, event)?;
|
|
}
|
|
open_ledger(&ledger_path(app)?)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn open_ledger(path: &Path) -> Result<Connection, String> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_CREATE_FAILED: {error}"))?;
|
|
}
|
|
// 多个协议请求可能在首次启动的同一瞬间进入。SQLite 的 WAL 切换与建表必须先在
|
|
// 进程内串行完成,之后各请求再依靠 IMMEDIATE 事务并发竞争,避免把首启抢锁误报
|
|
// 成协议内核不可用。
|
|
let _schema_guard = LEDGER_SCHEMA_LOCK
|
|
.lock()
|
|
.map_err(|_| "HOLOLAKE_GLS_LEDGER_SCHEMA_LOCK_POISONED".to_string())?;
|
|
let connection = Connection::open(path)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_OPEN_FAILED: {error}"))?;
|
|
connection
|
|
.busy_timeout(Duration::from_secs(5))
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_BUSY_TIMEOUT_FAILED: {error}"))?;
|
|
connection.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
|
|
CREATE TABLE IF NOT EXISTS protocol_receipts (sequence INTEGER PRIMARY KEY, idempotency_key TEXT NOT NULL, input_digest TEXT NOT NULL, decision TEXT NOT NULL, receipt_sha256 TEXT NOT NULL, receipt_json TEXT NOT NULL);
|
|
CREATE TABLE IF NOT EXISTS protocol_state (state_key TEXT PRIMARY KEY, state_json TEXT NOT NULL, updated_by_idempotency_key TEXT NOT NULL);
|
|
CREATE TABLE IF NOT EXISTS protocol_conflicts (conflict_sha256 TEXT PRIMARY KEY, state_key TEXT NOT NULL, conflict_json TEXT NOT NULL, observed_at_unix_ms INTEGER NOT NULL);"
|
|
)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_SCHEMA_FAILED: {error}"))?;
|
|
Ok(connection)
|
|
}
|
|
|
|
fn state_value(transaction: &Transaction<'_>, key: &str) -> Result<Option<Value>, String> {
|
|
let json: Option<String> = transaction
|
|
.query_row(
|
|
"SELECT state_json FROM protocol_state WHERE state_key=?1",
|
|
params![key],
|
|
|row| row.get(0),
|
|
)
|
|
.optional()
|
|
.map_err(|error| format!("HOLOLAKE_GLS_STATE_READ_FAILED: {error}"))?;
|
|
json.map(|value| {
|
|
serde_json::from_str(&value)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_STATE_RECORD_INVALID: {error}"))
|
|
})
|
|
.transpose()
|
|
}
|
|
|
|
fn write_state(
|
|
transaction: &Transaction<'_>,
|
|
key: &str,
|
|
value: &Value,
|
|
idempotency_key: &str,
|
|
) -> Result<(), String> {
|
|
let json = serde_json::to_string(value)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_STATE_SERIALIZE_FAILED: {error}"))?;
|
|
transaction
|
|
.execute(
|
|
"INSERT INTO protocol_state(state_key,state_json,updated_by_idempotency_key) VALUES(?1,?2,?3) ON CONFLICT(state_key) DO UPDATE SET state_json=excluded.state_json,updated_by_idempotency_key=excluded.updated_by_idempotency_key",
|
|
params![key, json, idempotency_key],
|
|
)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_STATE_WRITE_FAILED: {error}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn deny_receipt(receipt: &mut ProtocolDecisionReceipt, code: &str) {
|
|
receipt.decision = "DENY".into();
|
|
receipt.reason_codes = vec![code.into()];
|
|
}
|
|
|
|
fn project_state_digests(
|
|
receipt: &mut ProtocolDecisionReceipt,
|
|
before: &Value,
|
|
after: &Value,
|
|
) -> Result<(), String> {
|
|
receipt.state_before_digest = sha256(
|
|
&serde_json::to_vec(before)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_STATE_SERIALIZE_FAILED: {error}"))?,
|
|
);
|
|
receipt.state_after_digest = sha256(
|
|
&serde_json::to_vec(after)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_STATE_SERIALIZE_FAILED: {error}"))?,
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn enforce_persistent_state(
|
|
transaction: &Transaction<'_>,
|
|
receipt: &mut ProtocolDecisionReceipt,
|
|
request: &ProtocolDecisionRequest,
|
|
) -> Result<(), String> {
|
|
let Some(operation) = request.operation.as_object() else {
|
|
return Ok(());
|
|
};
|
|
if receipt.decision == "AMBIGUOUS"
|
|
&& matches!(request.event_kind.as_str(), "MEMORY_SYNC" | "STATE_SYNC")
|
|
{
|
|
let key = format!(
|
|
"sync:{}:{}",
|
|
request.event_kind, request.subject.canonical_id
|
|
);
|
|
let before = state_value(transaction, &key)?.unwrap_or(Value::Null);
|
|
let conflict = serde_json::json!({"before":before,"incoming":request.operation,"reason":"CONCURRENT_VERSIONS_PRESERVED"});
|
|
let bytes = serde_json::to_vec(&conflict)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_CONFLICT_SERIALIZE_FAILED: {error}"))?;
|
|
let conflict_hash = sha256(&bytes);
|
|
transaction.execute(
|
|
"INSERT OR IGNORE INTO protocol_conflicts(conflict_sha256,state_key,conflict_json,observed_at_unix_ms) VALUES(?1,?2,?3,?4)",
|
|
params![conflict_hash, key, String::from_utf8_lossy(&bytes), receipt.observed_at_unix_ms],
|
|
).map_err(|error| format!("HOLOLAKE_GLS_CONFLICT_WRITE_FAILED: {error}"))?;
|
|
project_state_digests(receipt, &before, &conflict)?;
|
|
return Ok(());
|
|
}
|
|
if receipt.decision != "ALLOW" {
|
|
return Ok(());
|
|
}
|
|
|
|
let (key, before, after) = match request.event_kind.as_str() {
|
|
"WORK_ORDER_TRANSITION" => {
|
|
let key = format!("work-order:{}", request.context.task_id);
|
|
let before = state_value(transaction, &key)?
|
|
.unwrap_or_else(|| serde_json::json!({"stage":"REGISTERED"}));
|
|
let current = before
|
|
.get("stage")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("INVALID");
|
|
let expected = value_string(operation, "fromStage").unwrap_or("MISSING");
|
|
if current != expected {
|
|
deny_receipt(receipt, "GLP_WORK_ORDER_PERSISTED_STAGE_MISMATCH");
|
|
return Ok(());
|
|
}
|
|
let after = serde_json::json!({"stage":value_string(operation,"toStage").unwrap_or("INVALID"),"approverId":value_string(operation,"approverId").unwrap_or("UNKNOWN")});
|
|
(key, before, after)
|
|
}
|
|
"MEMORY_SYNC" | "STATE_SYNC" => {
|
|
let key = format!(
|
|
"sync:{}:{}",
|
|
request.event_kind, request.subject.canonical_id
|
|
);
|
|
let before = state_value(transaction, &key)?.unwrap_or(Value::Null);
|
|
let after = request.operation.clone();
|
|
(key, before, after)
|
|
}
|
|
"TIME_CONTINUITY" => {
|
|
let key = format!("time:{}", request.subject.canonical_id);
|
|
let before = state_value(transaction, &key)?.unwrap_or_else(|| serde_json::json!({"sequence":0,"primaryInstanceId":Value::Null,"leaseExpiresAtUnixMs":0}));
|
|
let current_sequence = before.get("sequence").and_then(Value::as_u64).unwrap_or(0);
|
|
let current_owner = before.get("primaryInstanceId").and_then(Value::as_str);
|
|
let current_lease = before
|
|
.get("leaseExpiresAtUnixMs")
|
|
.and_then(Value::as_u64)
|
|
.unwrap_or(0);
|
|
let requested_previous = value_u64(operation, "previousSequence").unwrap_or(u64::MAX);
|
|
let requested_owner = value_string(operation, "primaryInstanceId").unwrap_or("MISSING");
|
|
if requested_previous != current_sequence {
|
|
deny_receipt(receipt, "PTCP_PERSISTED_SEQUENCE_MISMATCH");
|
|
return Ok(());
|
|
}
|
|
if current_lease > receipt.observed_at_unix_ms
|
|
&& current_owner.is_some()
|
|
&& current_owner != Some(requested_owner)
|
|
{
|
|
deny_receipt(receipt, "PTCP_PERSISTED_PRIMARY_LEASE_CONFLICT");
|
|
return Ok(());
|
|
}
|
|
let after = serde_json::json!({"sequence":value_u64(operation,"nextSequence").unwrap_or(0),"primaryInstanceId":requested_owner,"leaseExpiresAtUnixMs":value_u64(operation,"leaseExpiresAtUnixMs").unwrap_or(0)});
|
|
(key, before, after)
|
|
}
|
|
"MODULE_ADMIT" => {
|
|
let module_id = value_string(operation, "moduleId").unwrap_or("MISSING");
|
|
let key = format!("module:{module_id}");
|
|
let before = state_value(transaction, &key)?.unwrap_or(Value::Null);
|
|
if before != Value::Null
|
|
&& before.get("contentDigest") != operation.get("contentDigest")
|
|
{
|
|
deny_receipt(receipt, "GMP_IMMUTABLE_MODULE_DIGEST_CONFLICT");
|
|
return Ok(());
|
|
}
|
|
(key, before, request.operation.clone())
|
|
}
|
|
"LIFECYCLE_TRANSITION" => {
|
|
let key = format!("lifecycle:{}", request.subject.instance_id);
|
|
let before = state_value(transaction, &key)?
|
|
.unwrap_or_else(|| serde_json::json!({"state":"REGISTERED"}));
|
|
let current = before
|
|
.get("state")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("INVALID");
|
|
if current != value_string(operation, "fromState").unwrap_or("MISSING") {
|
|
deny_receipt(receipt, "PALP_PERSISTED_STATE_MISMATCH");
|
|
return Ok(());
|
|
}
|
|
let after =
|
|
serde_json::json!({"state":value_string(operation,"toState").unwrap_or("INVALID")});
|
|
(key, before, after)
|
|
}
|
|
"RUNWAY_ASSIGN" => {
|
|
let isolation = value_string(operation, "isolationKey").unwrap_or("MISSING");
|
|
let key = format!("runway:{}:{isolation}", request.context.channel_id);
|
|
let before = state_value(transaction, &key)?.unwrap_or(Value::Null);
|
|
if before.get("state").and_then(Value::as_str) == Some("ASSIGNED") {
|
|
deny_receipt(receipt, "GRSP_RUNWAY_ALREADY_ASSIGNED");
|
|
return Ok(());
|
|
}
|
|
let after = serde_json::json!({"state":"ASSIGNED","ownerInstanceId":request.subject.instance_id,"channelId":request.context.channel_id,"resources":request.operation});
|
|
(key, before, after)
|
|
}
|
|
"RUNWAY_RELEASE" => {
|
|
let evidence = value_string(operation, "targetEvidenceRef").unwrap_or("MISSING");
|
|
let isolation = value_string(operation, "isolationKey").unwrap_or("MISSING");
|
|
let key = format!("runway:{}:{isolation}", request.context.channel_id);
|
|
let before = state_value(transaction, &key)?.unwrap_or(Value::Null);
|
|
if before.get("state").and_then(Value::as_str) != Some("ASSIGNED")
|
|
|| before.get("ownerInstanceId").and_then(Value::as_str)
|
|
!= Some(request.subject.instance_id.as_str())
|
|
{
|
|
deny_receipt(receipt, "GRSP_PERSISTED_RUNWAY_OWNER_MISMATCH");
|
|
return Ok(());
|
|
}
|
|
let after = serde_json::json!({"state":"RELEASED","targetEvidenceRef":evidence,"isolationKey":isolation});
|
|
(key, before, after)
|
|
}
|
|
"BROADCAST_TRANSITION" => {
|
|
let key = format!("broadcast:{}", request.context.domain_id);
|
|
let before = state_value(transaction, &key)?.unwrap_or_else(|| serde_json::json!({"action":"REGISTER","epoch":0,"controllerInstanceId":Value::Null}));
|
|
let current_action = before
|
|
.get("action")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("INVALID");
|
|
let current_epoch = before.get("epoch").and_then(Value::as_u64).unwrap_or(0);
|
|
let current_owner = before.get("controllerInstanceId").and_then(Value::as_str);
|
|
let requested_epoch = value_u64(operation, "epoch").unwrap_or(0);
|
|
let requested_owner =
|
|
value_string(operation, "controllerInstanceId").unwrap_or("MISSING");
|
|
if current_action != value_string(operation, "fromAction").unwrap_or("MISSING")
|
|
|| requested_epoch < current_epoch
|
|
{
|
|
deny_receipt(receipt, "BTCP_PERSISTED_ACTION_OR_EPOCH_MISMATCH");
|
|
return Ok(());
|
|
}
|
|
if requested_epoch == current_epoch
|
|
&& current_epoch > 0
|
|
&& current_owner != Some(requested_owner)
|
|
{
|
|
deny_receipt(receipt, "BTCP_DOUBLE_CONTROLLER_EPOCH_CONFLICT");
|
|
return Ok(());
|
|
}
|
|
let after = serde_json::json!({"action":value_string(operation,"toAction").unwrap_or("INVALID"),"epoch":requested_epoch,"controllerInstanceId":requested_owner});
|
|
(key, before, after)
|
|
}
|
|
_ => return Ok(()),
|
|
};
|
|
project_state_digests(receipt, &before, &after)?;
|
|
write_state(transaction, &key, &after, &receipt.idempotency_key)
|
|
}
|
|
|
|
fn persist_receipt(
|
|
path: &Path,
|
|
mut receipt: ProtocolDecisionReceipt,
|
|
request: Option<&ProtocolDecisionRequest>,
|
|
) -> Result<ProtocolDecisionReceipt, String> {
|
|
let mut connection = open_ledger(path)?;
|
|
let transaction = connection
|
|
.transaction_with_behavior(TransactionBehavior::Immediate)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_LOCK_FAILED: {error}"))?;
|
|
let existing: Option<(String, String)> = transaction.query_row(
|
|
"SELECT input_digest, receipt_json FROM protocol_receipts WHERE idempotency_key=?1 ORDER BY sequence ASC LIMIT 1",
|
|
params![receipt.idempotency_key],
|
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
).optional().map_err(|error| format!("HOLOLAKE_GLS_LEDGER_READ_FAILED: {error}"))?;
|
|
if let Some((input_digest, json)) = existing {
|
|
if input_digest == receipt.input_digest {
|
|
return serde_json::from_str(&json)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_RECEIPT_REPLAY_INVALID: {error}"));
|
|
}
|
|
receipt.decision = "DENY".into();
|
|
receipt.reason_codes = vec!["GLP_IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_INPUT".into()];
|
|
}
|
|
if let Some(request) = request {
|
|
enforce_persistent_state(&transaction, &mut receipt, request)?;
|
|
}
|
|
let previous: Option<(u64, String)> = transaction
|
|
.query_row(
|
|
"SELECT sequence, receipt_sha256 FROM protocol_receipts ORDER BY sequence DESC LIMIT 1",
|
|
[],
|
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
)
|
|
.optional()
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_TAIL_FAILED: {error}"))?;
|
|
receipt.sequence = previous.as_ref().map_or(1, |(sequence, _)| sequence + 1);
|
|
receipt.previous_receipt_sha256 = previous.map_or_else(|| "GENESIS".into(), |(_, hash)| hash);
|
|
receipt.receipt_id = format!("GLP-RCP-{:012}-{}", receipt.sequence, Uuid::new_v4());
|
|
receipt.receipt_sha256.clear();
|
|
receipt.receipt_sha256 = sha256(
|
|
&serde_json::to_vec(&receipt)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_RECEIPT_SERIALIZE_FAILED: {error}"))?,
|
|
);
|
|
let json = serde_json::to_string(&receipt)
|
|
.map_err(|error| format!("HOLOLAKE_GLS_RECEIPT_SERIALIZE_FAILED: {error}"))?;
|
|
transaction.execute("INSERT INTO protocol_receipts(sequence,idempotency_key,input_digest,decision,receipt_sha256,receipt_json) VALUES(?1,?2,?3,?4,?5,?6)", params![receipt.sequence, receipt.idempotency_key, receipt.input_digest, receipt.decision, receipt.receipt_sha256, json])
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_APPEND_FAILED: {error}"))?;
|
|
transaction
|
|
.commit()
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_COMMIT_FAILED: {error}"))?;
|
|
Ok(receipt)
|
|
}
|
|
|
|
fn build_receipt(
|
|
value: &Value,
|
|
parsed: Option<&ProtocolDecisionInput>,
|
|
decision: Decision,
|
|
protocols: Vec<String>,
|
|
observed_at: u64,
|
|
) -> ProtocolDecisionReceipt {
|
|
let request = parsed.map(|input| &input.request);
|
|
let text = |pointer: &str| {
|
|
value
|
|
.pointer(pointer)
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("UNKNOWN")
|
|
.to_string()
|
|
};
|
|
ProtocolDecisionReceipt {
|
|
schema: RECEIPT_SCHEMA.into(),
|
|
receipt_id: String::new(),
|
|
sequence: 0,
|
|
previous_receipt_sha256: String::new(),
|
|
request_id: request.map_or_else(
|
|
|| text("/request/requestId"),
|
|
|item| item.request_id.clone(),
|
|
),
|
|
event_kind: request.map_or_else(
|
|
|| text("/request/eventKind"),
|
|
|item| item.event_kind.clone(),
|
|
),
|
|
human_controller_id: request.map_or_else(
|
|
|| text("/request/humanControllerId"),
|
|
|item| item.human_controller_id.clone(),
|
|
),
|
|
subject_id: request.map_or_else(
|
|
|| text("/request/subject/canonicalId"),
|
|
|item| item.subject.canonical_id.clone(),
|
|
),
|
|
target_id: request.map_or_else(
|
|
|| text("/request/target/canonicalId"),
|
|
|item| item.target.canonical_id.clone(),
|
|
),
|
|
protocol_bundle_commit: request.map_or_else(
|
|
|| "UNKNOWN".into(),
|
|
|item| item.context.protocol_bundle_commit.clone(),
|
|
),
|
|
protocol_bundle_sha256: sha256(EMBEDDED_KERNEL.as_bytes()),
|
|
protocol_set: protocols,
|
|
adapter_id: decision.adapter.into(),
|
|
input_digest: sha256(&serde_json::to_vec(value).unwrap_or_default()),
|
|
decision: decision.decision.into(),
|
|
reason_codes: decision.reasons,
|
|
state_before_digest: request
|
|
.map_or_else(|| "UNKNOWN".into(), |item| item.state_before_digest.clone()),
|
|
state_after_digest: request
|
|
.map_or_else(|| "UNKNOWN".into(), |item| item.state_after_digest.clone()),
|
|
evidence_refs: request.map_or_else(Vec::new, |item| item.evidence_refs.clone()),
|
|
time_authority: format!("HOLOLAKE_GUANGHU_TIME_AUTHORITY@{observed_at}"),
|
|
observed_at_unix_ms: observed_at,
|
|
idempotency_key: request.map_or_else(
|
|
|| text("/request/idempotencyKey"),
|
|
|item| item.idempotency_key.clone(),
|
|
),
|
|
signer: "HOLOLAKE_NATIVE_PROTOCOL_KERNEL_LOCAL_HASH_CHAIN".into(),
|
|
receipt_sha256: String::new(),
|
|
}
|
|
}
|
|
|
|
fn decide_value_at(
|
|
ledger: &Path,
|
|
session_root: &Path,
|
|
value: Value,
|
|
observed_at: u64,
|
|
) -> Result<ProtocolDecisionReceipt, String> {
|
|
let parsed = match serde_json::from_value::<ProtocolDecisionInput>(value.clone()) {
|
|
Ok(input) => input,
|
|
Err(_) => {
|
|
return persist_receipt(
|
|
ledger,
|
|
build_receipt(
|
|
&value,
|
|
None,
|
|
Decision {
|
|
adapter: "glp-envelope-codec",
|
|
decision: "DENY",
|
|
reasons: vec!["GLP_STRICT_CODEC_REJECTED_MALFORMED_INPUT".into()],
|
|
},
|
|
vec!["GLS-0301".into(), "GLS-0306".into()],
|
|
observed_at,
|
|
),
|
|
None,
|
|
)
|
|
}
|
|
};
|
|
let authenticated = authenticate_context_at(session_root, &parsed.session);
|
|
let decision = match authenticated {
|
|
Ok(_) => decide(
|
|
&parsed.request,
|
|
&parsed.session.account_id,
|
|
&parsed.session.session_id,
|
|
observed_at,
|
|
),
|
|
Err(_) => Decision {
|
|
adapter: "glp-identity-reference",
|
|
decision: "DENY",
|
|
reasons: vec!["GLP_SESSION_AUTHENTICATION_FAILED".into()],
|
|
},
|
|
};
|
|
let protocols =
|
|
crate::gls_protocol_runtime::require_adapter(decision.adapter, &parsed.request.event_kind)
|
|
.or_else(|_| {
|
|
crate::gls_protocol_runtime::require_adapter(
|
|
"glp-decision-kernel",
|
|
"DECISION_RECEIPT",
|
|
)
|
|
})?;
|
|
let receipt = build_receipt(&value, Some(&parsed), decision, protocols, observed_at);
|
|
persist_receipt(ledger, receipt, Some(&parsed.request))
|
|
}
|
|
|
|
pub async fn decide_gls_protocol(
|
|
app: AppHandle,
|
|
input: Value,
|
|
) -> Result<ProtocolDecisionReceipt, String> {
|
|
let ledger = ledger_path(&app)?;
|
|
let sessions = direct_session_root(&app)?;
|
|
tauri::async_runtime::spawn_blocking(move || {
|
|
decide_value_at(&ledger, &sessions, input, now_unix_ms())
|
|
})
|
|
.await
|
|
.map_err(|error| format!("HOLOLAKE_GLS_DECISION_JOIN_FAILED: {error}"))?
|
|
}
|
|
|
|
pub async fn compile_gls_hldp_program(
|
|
app: AppHandle,
|
|
input: Value,
|
|
) -> Result<CompileHldpResult, String> {
|
|
let Some(root) = input.as_object() else {
|
|
return Err("HOLOLAKE_GLC_COMMAND_OBJECT_REQUIRED".into());
|
|
};
|
|
if root
|
|
.keys()
|
|
.any(|key| !matches!(key.as_str(), "session" | "request" | "program"))
|
|
{
|
|
return Err("HOLOLAKE_GLC_COMMAND_FIELD_UNKNOWN".into());
|
|
}
|
|
let program_value = root.get("program").cloned().unwrap_or(Value::Null);
|
|
let compiled = crate::gls_bootstrap_compiler::compile_hldp_program(program_value.clone());
|
|
let golden = crate::gls_bootstrap_compiler::bootstrap_self_check()?;
|
|
let mut request = root
|
|
.get("request")
|
|
.cloned()
|
|
.unwrap_or_else(|| Value::Object(Map::new()));
|
|
let request_object = request
|
|
.as_object_mut()
|
|
.ok_or("HOLOLAKE_GLC_REQUEST_OBJECT_REQUIRED")?;
|
|
let (source_digest, unresolved_language, unresolved_permissions) = match &compiled {
|
|
Ok(gir) => (
|
|
gir.source_sha256.clone(),
|
|
Vec::<String>::new(),
|
|
Vec::<String>::new(),
|
|
),
|
|
Err(error) => (
|
|
sha256(&serde_json::to_vec(&program_value).unwrap_or_default()),
|
|
vec![error.clone()],
|
|
vec!["STRICT_COMPILER_INPUT_REJECTED".into()],
|
|
),
|
|
};
|
|
let operation = serde_json::json!({
|
|
"sourceDigest": source_digest,
|
|
"unresolvedNaturalLanguage": unresolved_language,
|
|
"unresolvedPermissions": unresolved_permissions,
|
|
"goldenVectorDigest": golden,
|
|
});
|
|
request_object.insert("schema".into(), Value::from(REQUEST_SCHEMA));
|
|
request_object.insert("eventKind".into(), Value::from("COMPILE_HLDP"));
|
|
request_object.insert(
|
|
"payloadSha256".into(),
|
|
Value::from(operation_digest(&operation)?),
|
|
);
|
|
request_object.insert("operation".into(), operation);
|
|
let decision_value = serde_json::json!({"session": root.get("session").cloned().unwrap_or(Value::Null), "request": request});
|
|
let ledger = ledger_path(&app)?;
|
|
let sessions = direct_session_root(&app)?;
|
|
let decision_receipt = tauri::async_runtime::spawn_blocking(move || {
|
|
decide_value_at(&ledger, &sessions, decision_value, now_unix_ms())
|
|
})
|
|
.await
|
|
.map_err(|error| format!("HOLOLAKE_GLC_DECISION_JOIN_FAILED: {error}"))??;
|
|
Ok(CompileHldpResult {
|
|
gir: if decision_receipt.decision == "ALLOW" {
|
|
compiled.ok()
|
|
} else {
|
|
None
|
|
},
|
|
decision_receipt,
|
|
})
|
|
}
|
|
|
|
fn counts(connection: &Connection) -> Result<(u64, BTreeMap<String, u64>, String), String> {
|
|
let total = connection
|
|
.query_row("SELECT COUNT(*) FROM protocol_receipts", [], |row| {
|
|
row.get(0)
|
|
})
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_COUNT_FAILED: {error}"))?;
|
|
let mut by_decision = BTreeMap::new();
|
|
let mut statement = connection
|
|
.prepare("SELECT decision, COUNT(*) FROM protocol_receipts GROUP BY decision")
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_COUNT_FAILED: {error}"))?;
|
|
let rows = statement
|
|
.query_map([], |row| {
|
|
Ok((row.get::<_, String>(0)?, row.get::<_, u64>(1)?))
|
|
})
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_COUNT_FAILED: {error}"))?;
|
|
for row in rows {
|
|
let (decision, count) =
|
|
row.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_COUNT_FAILED: {error}"))?;
|
|
by_decision.insert(decision, count);
|
|
}
|
|
let tail = connection
|
|
.query_row(
|
|
"SELECT receipt_sha256 FROM protocol_receipts ORDER BY sequence DESC LIMIT 1",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.optional()
|
|
.map_err(|error| format!("HOLOLAKE_GLS_LEDGER_TAIL_FAILED: {error}"))?
|
|
.unwrap_or_else(|| "GENESIS".into());
|
|
Ok((total, by_decision, tail))
|
|
}
|
|
|
|
pub async fn get_gls_protocol_kernel(app: AppHandle) -> Result<ProtocolKernelSnapshot, String> {
|
|
let contract = load_contract()?;
|
|
let connection = open_ledger(&ledger_path(&app)?)?;
|
|
let (total, decision_counts, tail) = counts(&connection)?;
|
|
let executable_protocol_count = contract.stages[..6]
|
|
.iter()
|
|
.map(|stage| stage.protocols.len())
|
|
.sum();
|
|
Ok(ProtocolKernelSnapshot {
|
|
state: "P1_TO_P6_NATIVE_P7_FAIL_CLOSED".into(),
|
|
contract_id: contract.record_id,
|
|
source_commit: contract.source_commit,
|
|
executable_protocol_count,
|
|
implemented_stage_count: 6,
|
|
decision_receipt_count: total,
|
|
allow_count: *decision_counts.get("ALLOW").unwrap_or(&0),
|
|
deny_count: *decision_counts.get("DENY").unwrap_or(&0),
|
|
ambiguous_count: *decision_counts.get("AMBIGUOUS").unwrap_or(&0),
|
|
unverified_count: *decision_counts.get("UNVERIFIED").unwrap_or(&0),
|
|
last_receipt_sha256: tail,
|
|
stages: contract.stages,
|
|
p7_verified_physical_capability_count: 0,
|
|
p7_node_assemblies: contract.p7_node_assemblies,
|
|
raw_protocol_text_executed: contract.runtime_boundaries.raw_protocol_text_executed,
|
|
model_can_override_decision: contract.runtime_boundaries.model_can_override_decision,
|
|
bootstrap_compiler_self_check: "PASS_DETERMINISTIC_DOUBLE_COMPILE".into(),
|
|
bootstrap_golden_gir_sha256: crate::gls_bootstrap_compiler::bootstrap_self_check()?,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::direct_local_session::{
|
|
issue_ticket_at, open_at, IssueDiscoveryTicketInput, OpenSessionInput,
|
|
};
|
|
use tempfile::TempDir;
|
|
|
|
fn digest_value(value: &Value) -> String {
|
|
operation_digest(value).unwrap()
|
|
}
|
|
|
|
fn authenticated_input(
|
|
root: &Path,
|
|
event: &str,
|
|
operation: Value,
|
|
now: u64,
|
|
key: &str,
|
|
) -> Value {
|
|
let account_id = format!("ICE-GL-{key}");
|
|
let ticket = issue_ticket_at(
|
|
root,
|
|
IssueDiscoveryTicketInput {
|
|
account_id: account_id.clone(),
|
|
lane_id: "personal-channel".into(),
|
|
client_instance_id: "test-instance".into(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
let session = open_at(
|
|
root,
|
|
OpenSessionInput {
|
|
account_id: account_id.clone(),
|
|
lane_id: "personal-channel".into(),
|
|
client_instance_id: "test-instance".into(),
|
|
discovery_ticket: ticket.discovery_ticket,
|
|
},
|
|
)
|
|
.unwrap();
|
|
serde_json::json!({
|
|
"session":{"accountId":account_id,"sessionId":session.session_id,"resumeSecret":session.resume_secret.unwrap()},
|
|
"request":{"schema":REQUEST_SCHEMA,"requestId":format!("REQ-{key}"),"idempotencyKey":key,"eventKind":event,"humanControllerId":format!("ICE-GL-{key}"),
|
|
"subject":{"canonicalId":"ICE-P-ZY001","subjectKind":"AGE_PERSONA","instanceId":"persona-primary","domainId":"FIFTH_DOMAIN","registryRef":"registry://persona/ICE-P-ZY001","proofRef":"proof://persona/current"},
|
|
"target":{"canonicalId":"JD-FD-PRIMARY","subjectKind":"NODE","instanceId":"jd-primary","domainId":"FIFTH_DOMAIN","registryRef":"registry://node/JD-FD-PRIMARY","proofRef":"proof://node/current"},
|
|
"context":{"worldId":"GUANGHU_WORLD","domainId":"FIFTH_DOMAIN","channelId":"personal-channel","taskId":"TASK-1","authorizationRef":format!("direct-session://{}",session.session_id),"protocolBundleCommit":SOURCE_COMMIT,"visibilityScope":"PRIVATE_SUBJECT"},
|
|
"issuedAtUnixMs":now-1,"expiresAtUnixMs":now+60_000,"payloadSha256":digest_value(&operation),"stateBeforeDigest":sha256(b"before"),"stateAfterDigest":sha256(b"after"),"evidenceRefs":[],"operation":operation}
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
fn kernel_contract_covers_p1_to_p7_without_claiming_physical_capability() {
|
|
let contract = load_contract().unwrap();
|
|
assert_eq!(contract.stages.len(), 7);
|
|
assert_eq!(
|
|
contract.stages[..6]
|
|
.iter()
|
|
.map(|stage| stage.protocols.len())
|
|
.sum::<usize>(),
|
|
21
|
|
);
|
|
assert!(contract
|
|
.p7_node_assemblies
|
|
.iter()
|
|
.all(|assembly| assembly.state != "ACTIVE_HEALTHY"));
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_and_expired_inputs_fail_closed_and_still_write_receipts() {
|
|
let temp = TempDir::new().unwrap();
|
|
let ledger = temp.path().join("ledger.sqlite3");
|
|
let malformed = decide_value_at(
|
|
&ledger,
|
|
temp.path(),
|
|
serde_json::json!({"request":{"idempotencyKey":"bad"}}),
|
|
1000,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(malformed.decision, "DENY");
|
|
let mut expired = authenticated_input(
|
|
temp.path(),
|
|
"MESSAGE_VALIDATE",
|
|
serde_json::json!({}),
|
|
2000,
|
|
"expired",
|
|
);
|
|
expired["request"]["expiresAtUnixMs"] = Value::from(1999_u64);
|
|
let receipt = decide_value_at(&ledger, temp.path(), expired, 2000).unwrap();
|
|
assert_eq!(receipt.decision, "DENY");
|
|
assert_eq!(
|
|
open_ledger(&ledger)
|
|
.unwrap()
|
|
.query_row("SELECT COUNT(*) FROM protocol_receipts", [], |row| row
|
|
.get::<_, u64>(0))
|
|
.unwrap(),
|
|
2
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn every_stage_rule_is_deterministic_and_fail_closed() {
|
|
let temp = TempDir::new().unwrap();
|
|
let ledger = temp.path().join("ledger.sqlite3");
|
|
let now = 10_000;
|
|
let cases = [
|
|
(
|
|
"HEARTBEAT_OBSERVE",
|
|
serde_json::json!({"leaseExpiresAtUnixMs":20_000,"healthDigest":sha256(b"health")}),
|
|
"ALLOW",
|
|
),
|
|
(
|
|
"WORK_ORDER_TRANSITION",
|
|
serde_json::json!({"proposerId":"ICE-P-ZY001","approverId":"ICE-GL∞","fromStage":"REGISTERED","toStage":"TESTED"}),
|
|
"ALLOW",
|
|
),
|
|
(
|
|
"MEMORY_SYNC",
|
|
serde_json::json!({"causalRelation":"CONCURRENT","localVersion":2,"remoteVersion":2}),
|
|
"AMBIGUOUS",
|
|
),
|
|
(
|
|
"TIME_CONTINUITY",
|
|
serde_json::json!({"previousSequence":0,"nextSequence":1,"primaryInstanceId":"persona-primary","leaseExpiresAtUnixMs":20_000}),
|
|
"ALLOW",
|
|
),
|
|
(
|
|
"MODULE_ADMIT",
|
|
serde_json::json!({"moduleId":"GMP-1","immutableVersion":"1.0.0","contentDigest":sha256(b"module"),"provenanceRef":"repo://fixed","signatureRef":"sig://1"}),
|
|
"ALLOW",
|
|
),
|
|
(
|
|
"RUNWAY_ASSIGN",
|
|
serde_json::json!({"cpuUnits":1,"memoryBytes":1024,"channelIsolation":true,"isolationKey":"channel-1"}),
|
|
"ALLOW",
|
|
),
|
|
(
|
|
"EXTERNAL_ADAPTER_TRANSLATE",
|
|
serde_json::json!({"externalKind":"API","normalizedDigest":sha256(b"normalized"),"sourceRef":"api://fixed","requestsExecutionAuthority":false}),
|
|
"ALLOW",
|
|
),
|
|
(
|
|
"MODEL_ROUTE",
|
|
serde_json::json!({"modelId":"replaceable-model","modelIsPersona":false,"grantsWorldAuthority":false,"privacyClass":"PRIVATE","healthReceiptRef":"receipt://health/1"}),
|
|
"ALLOW",
|
|
),
|
|
(
|
|
"TEMPORARY_CAPABILITY",
|
|
serde_json::json!({"sandboxed":true,"permanentInstall":false,"autoPublish":false,"autoDeploy":false,"cleanupPlanDigest":sha256(b"cleanup")}),
|
|
"ALLOW",
|
|
),
|
|
(
|
|
"COMPILE_HLDP",
|
|
serde_json::json!({"sourceDigest":sha256(b"program"),"unresolvedNaturalLanguage":[],"unresolvedPermissions":[],"goldenVectorDigest":sha256(b"golden")}),
|
|
"ALLOW",
|
|
),
|
|
];
|
|
for (index, (event, operation, expected)) in cases.into_iter().enumerate() {
|
|
let input =
|
|
authenticated_input(temp.path(), event, operation, now, &format!("case-{index}"));
|
|
assert_eq!(
|
|
decide_value_at(&ledger, temp.path(), input, now)
|
|
.unwrap()
|
|
.decision,
|
|
expected
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn receipt_chain_is_idempotent_and_detects_key_reuse() {
|
|
let temp = TempDir::new().unwrap();
|
|
let ledger = temp.path().join("ledger.sqlite3");
|
|
let input = authenticated_input(
|
|
temp.path(),
|
|
"MESSAGE_VALIDATE",
|
|
serde_json::json!({}),
|
|
1000,
|
|
"same",
|
|
);
|
|
let first = decide_value_at(&ledger, temp.path(), input.clone(), 1000).unwrap();
|
|
let replay = decide_value_at(&ledger, temp.path(), input, 1001).unwrap();
|
|
assert_eq!(first.receipt_sha256, replay.receipt_sha256);
|
|
let mut changed = authenticated_input(
|
|
temp.path(),
|
|
"IDENTITY_VERIFY",
|
|
serde_json::json!({}),
|
|
1000,
|
|
"changed-session",
|
|
);
|
|
changed["request"]["idempotencyKey"] = Value::from("same");
|
|
let conflict = decide_value_at(&ledger, temp.path(), changed, 1000).unwrap();
|
|
assert_eq!(conflict.decision, "DENY");
|
|
assert_eq!(conflict.previous_receipt_sha256, first.receipt_sha256);
|
|
}
|
|
|
|
#[test]
|
|
fn concurrent_exact_replay_produces_one_receipt() {
|
|
let temp = TempDir::new().unwrap();
|
|
let ledger = temp.path().join("ledger.sqlite3");
|
|
let sessions = temp.path().join("sessions");
|
|
let input = authenticated_input(
|
|
&sessions,
|
|
"MESSAGE_VALIDATE",
|
|
serde_json::json!({}),
|
|
1_000,
|
|
"concurrent",
|
|
);
|
|
let handles = (0..8)
|
|
.map(|_| {
|
|
let ledger = ledger.clone();
|
|
let sessions = sessions.clone();
|
|
let input = input.clone();
|
|
std::thread::spawn(move || {
|
|
decide_value_at(&ledger, &sessions, input, 1_000).unwrap()
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let receipts = handles
|
|
.into_iter()
|
|
.map(|handle| handle.join().unwrap())
|
|
.collect::<Vec<_>>();
|
|
assert!(receipts
|
|
.iter()
|
|
.all(|receipt| receipt.receipt_sha256 == receipts[0].receipt_sha256));
|
|
assert_eq!(
|
|
open_ledger(&ledger)
|
|
.unwrap()
|
|
.query_row("SELECT COUNT(*) FROM protocol_receipts", [], |row| row
|
|
.get::<_, u64>(0))
|
|
.unwrap(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn protocol_state_machines_are_atomic_persistent_and_fail_closed() {
|
|
let temp = TempDir::new().unwrap();
|
|
let ledger = temp.path().join("ledger.sqlite3");
|
|
let now = 10_000;
|
|
|
|
let decide = |key: &str, event: &str, operation: Value| {
|
|
decide_value_at(
|
|
&ledger,
|
|
temp.path(),
|
|
authenticated_input(temp.path(), event, operation, now, key),
|
|
now,
|
|
)
|
|
.unwrap()
|
|
};
|
|
|
|
assert_eq!(
|
|
decide(
|
|
"work-1",
|
|
"WORK_ORDER_TRANSITION",
|
|
serde_json::json!({"proposerId":"proposer","approverId":"approver","fromStage":"REGISTERED","toStage":"TESTED"}),
|
|
)
|
|
.decision,
|
|
"ALLOW"
|
|
);
|
|
assert_eq!(
|
|
decide(
|
|
"work-stale",
|
|
"WORK_ORDER_TRANSITION",
|
|
serde_json::json!({"proposerId":"proposer","approverId":"approver","fromStage":"REGISTERED","toStage":"TESTED"}),
|
|
)
|
|
.reason_codes,
|
|
["GLP_WORK_ORDER_PERSISTED_STAGE_MISMATCH"]
|
|
);
|
|
|
|
assert_eq!(
|
|
decide(
|
|
"time-1",
|
|
"TIME_CONTINUITY",
|
|
serde_json::json!({"previousSequence":0,"nextSequence":1,"primaryInstanceId":"persona-primary","leaseExpiresAtUnixMs":20_000}),
|
|
)
|
|
.decision,
|
|
"ALLOW"
|
|
);
|
|
let mut competing_time = authenticated_input(
|
|
temp.path(),
|
|
"TIME_CONTINUITY",
|
|
serde_json::json!({"previousSequence":1,"nextSequence":2,"primaryInstanceId":"persona-secondary","leaseExpiresAtUnixMs":20_000}),
|
|
now,
|
|
"time-double-primary",
|
|
);
|
|
competing_time["request"]["subject"]["instanceId"] = Value::from("persona-secondary");
|
|
assert_eq!(
|
|
decide_value_at(&ledger, temp.path(), competing_time, now)
|
|
.unwrap()
|
|
.reason_codes,
|
|
["PTCP_PERSISTED_PRIMARY_LEASE_CONFLICT"]
|
|
);
|
|
|
|
assert_eq!(
|
|
decide(
|
|
"module-1",
|
|
"MODULE_ADMIT",
|
|
serde_json::json!({"moduleId":"GMP-PERSISTENT","immutableVersion":"1.0.0","contentDigest":sha256(b"module-a"),"provenanceRef":"repo://fixed","signatureRef":"sig://1"}),
|
|
)
|
|
.decision,
|
|
"ALLOW"
|
|
);
|
|
assert_eq!(
|
|
decide(
|
|
"module-conflict",
|
|
"MODULE_ADMIT",
|
|
serde_json::json!({"moduleId":"GMP-PERSISTENT","immutableVersion":"1.0.0","contentDigest":sha256(b"module-b"),"provenanceRef":"repo://fixed","signatureRef":"sig://1"}),
|
|
)
|
|
.reason_codes,
|
|
["GMP_IMMUTABLE_MODULE_DIGEST_CONFLICT"]
|
|
);
|
|
|
|
assert_eq!(
|
|
decide(
|
|
"lifecycle-1",
|
|
"LIFECYCLE_TRANSITION",
|
|
serde_json::json!({"fromState":"REGISTERED","toState":"DORMANT"}),
|
|
)
|
|
.decision,
|
|
"ALLOW"
|
|
);
|
|
assert_eq!(
|
|
decide(
|
|
"lifecycle-stale",
|
|
"LIFECYCLE_TRANSITION",
|
|
serde_json::json!({"fromState":"REGISTERED","toState":"DORMANT"}),
|
|
)
|
|
.reason_codes,
|
|
["PALP_PERSISTED_STATE_MISMATCH"]
|
|
);
|
|
|
|
assert_eq!(
|
|
decide(
|
|
"runway-assign",
|
|
"RUNWAY_ASSIGN",
|
|
serde_json::json!({"cpuUnits":1,"memoryBytes":1024,"channelIsolation":true,"isolationKey":"isolated-runway"}),
|
|
)
|
|
.decision,
|
|
"ALLOW"
|
|
);
|
|
let mut wrong_release = authenticated_input(
|
|
temp.path(),
|
|
"RUNWAY_RELEASE",
|
|
serde_json::json!({"resourcesReleased":true,"targetEvidenceRef":"receipt://cleanup/wrong","isolationKey":"isolated-runway"}),
|
|
now,
|
|
"runway-wrong-owner",
|
|
);
|
|
wrong_release["request"]["subject"]["instanceId"] = Value::from("persona-secondary");
|
|
assert_eq!(
|
|
decide_value_at(&ledger, temp.path(), wrong_release, now)
|
|
.unwrap()
|
|
.reason_codes,
|
|
["GRSP_PERSISTED_RUNWAY_OWNER_MISMATCH"]
|
|
);
|
|
assert_eq!(
|
|
decide(
|
|
"runway-release",
|
|
"RUNWAY_RELEASE",
|
|
serde_json::json!({"resourcesReleased":true,"targetEvidenceRef":"receipt://cleanup/ok","isolationKey":"isolated-runway"}),
|
|
)
|
|
.decision,
|
|
"ALLOW"
|
|
);
|
|
|
|
assert_eq!(
|
|
decide(
|
|
"broadcast-1",
|
|
"BROADCAST_TRANSITION",
|
|
serde_json::json!({"fromAction":"REGISTER","toAction":"SUMMON","epoch":1,"controllerInstanceId":"persona-primary"}),
|
|
)
|
|
.decision,
|
|
"ALLOW"
|
|
);
|
|
let mut competing_controller = authenticated_input(
|
|
temp.path(),
|
|
"BROADCAST_TRANSITION",
|
|
serde_json::json!({"fromAction":"SUMMON","toAction":"ASSIGN","epoch":1,"controllerInstanceId":"persona-secondary"}),
|
|
now,
|
|
"broadcast-double-controller",
|
|
);
|
|
competing_controller["request"]["subject"]["instanceId"] = Value::from("persona-secondary");
|
|
assert_eq!(
|
|
decide_value_at(&ledger, temp.path(), competing_controller, now)
|
|
.unwrap()
|
|
.reason_codes,
|
|
["BTCP_DOUBLE_CONTROLLER_EPOCH_CONFLICT"]
|
|
);
|
|
|
|
assert_eq!(
|
|
decide(
|
|
"memory-conflict",
|
|
"MEMORY_SYNC",
|
|
serde_json::json!({"causalRelation":"CONCURRENT","localVersion":2,"remoteVersion":3}),
|
|
)
|
|
.decision,
|
|
"AMBIGUOUS"
|
|
);
|
|
let connection = open_ledger(&ledger).unwrap();
|
|
assert_eq!(
|
|
connection
|
|
.query_row("SELECT COUNT(*) FROM protocol_conflicts", [], |row| row
|
|
.get::<_, u64>(0))
|
|
.unwrap(),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
connection
|
|
.query_row("SELECT COUNT(*) FROM protocol_state", [], |row| row
|
|
.get::<_, u64>(0))
|
|
.unwrap(),
|
|
6
|
|
);
|
|
}
|
|
}
|