From 6b934c2134126690fbb65c4c3ed0d7fde922c174 Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 22 Jul 2026 21:34:52 +0800
Subject: [PATCH 1/6] feat: add verified T1 Excel reconciliation
---
apps/desktop/src-tauri/src/commands.rs | 53 +-
apps/desktop/src-tauri/src/kernel/mod.rs | 1 +
.../src-tauri/src/kernel/t1_reconciliation.rs | 1673 +++++++++++++++++
.../src-tauri/src/kernel/tool_runtime.rs | 151 +-
4 files changed, 1876 insertions(+), 2 deletions(-)
create mode 100644 apps/desktop/src-tauri/src/kernel/t1_reconciliation.rs
diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs
index 770e497..2f46fc1 100644
--- a/apps/desktop/src-tauri/src/commands.rs
+++ b/apps/desktop/src-tauri/src/commands.rs
@@ -162,6 +162,7 @@ use crate::kernel::skill_source::{
use crate::kernel::soul::{
AgentSoulProfileUpdateAudit, AgentSoulProfileUpdateProposal, AgentSoulProfileUpdateReceipt,
};
+use crate::kernel::t1_reconciliation::T1ReconciliationAgentToolExecutor;
use crate::kernel::task_capability_manifest::{
TaskCapabilityManifestContext, TaskCapabilityProposal,
};
@@ -177,7 +178,7 @@ use crate::kernel::tool_runtime::{
COMPUTER_CONTROL_TOOL_ID, COMPUTER_SCREENSHOT_TOOL_ID, CONNECTOR_ATTACHMENT_DOWNLOAD_TOOL_ID,
FILESYSTEM_MUTATE_TOOL_ID, FILE_READ_TOOL_ID, FILE_WRITE_TOOL_ID, NETWORK_SEARCH_TOOL_ID,
OFFICE_CREATE_TOOL_ID, OFFICE_OPEN_TOOL_ID, OFFICE_UPDATE_TOOL_ID, OPERATIONS_BRIEFING_TOOL_ID,
- SKILL_ACTIVATE_TOOL_ID, TERMINAL_READ_TOOL_ID,
+ SKILL_ACTIVATE_TOOL_ID, T1_RECONCILIATION_TOOL_ID, TERMINAL_READ_TOOL_ID,
};
use crate::kernel::tool_strategy::{
model_driven_tool_strategy_for_current_platform, ModelDrivenToolStrategy,
@@ -2654,6 +2655,21 @@ fn validate_agent_tool_local_constraints(plan: &ToolExecutionPlan) -> Result<(),
enforce_workspace_relative_read_path(path)?;
}
}
+ if plan.contract.id == T1_RECONCILIATION_TOOL_ID {
+ let source_directory =
+ plan.request.input["source_directory"]
+ .as_str()
+ .ok_or_else(|| {
+ "operations.reconcile_excel requires a source_directory string".to_string()
+ })?;
+ enforce_workspace_relative_read_path(source_directory)?;
+ let output_relative_path = plan.request.input["output_relative_path"]
+ .as_str()
+ .ok_or_else(|| {
+ "operations.reconcile_excel requires an output_relative_path string".to_string()
+ })?;
+ enforce_workspace_relative_mutation_path(output_relative_path)?;
+ }
if plan.contract.id == COMPUTER_CONTROL_TOOL_ID {
let action = plan.request.input["action"]
.as_str()
@@ -4661,6 +4677,26 @@ fn agent_file_write_client(
})
}
+fn t1_reconciliation_workspace_root(
+ directory_state: &LocalDirectoryState,
+) -> Result {
+ let settings = directory_state.settings.as_ref().ok_or_else(|| {
+ "workspace is not configured; choose a DS Agent work root before reconciling T1 sources"
+ .to_string()
+ })?;
+ if directory_state.needs_setup {
+ return Err(
+ "workspace setup is incomplete; choose a DS Agent work root before reconciling T1 sources"
+ .to_string(),
+ );
+ }
+ let workspace_root = PathBuf::from(&settings.workspace_dir);
+ if !workspace_root.is_dir() {
+ return Err("configured workspace is unavailable for T1 reconciliation".to_string());
+ }
+ Ok(workspace_root)
+}
+
fn deepseek_telemetry_with_pricing(
mut telemetry: Vec,
pricing_settings: Option<&DeepSeekPricingSettings>,
@@ -14069,6 +14105,14 @@ pub fn execute_agent_tool(
} else {
None
};
+ let t1_workspace_root = if request.tool_id.trim() == T1_RECONCILIATION_TOOL_ID {
+ let app_data_dir = app.resolved_app_data_dir()?;
+ let directory_state =
+ load_local_directory_state(&app_data_dir).map_err(event_store_error)?;
+ Some(t1_reconciliation_workspace_root(&directory_state)?)
+ } else {
+ None
+ };
let terminal_read_client = if request.tool_id.trim() == TERMINAL_READ_TOOL_ID {
Some(agent_terminal_read_client()?)
} else {
@@ -14127,6 +14171,13 @@ pub fn execute_agent_tool(
.ok_or_else(|| "file.write executor is unavailable".to_string())?,
};
run_authorized_agent_tool_execution(authorized, &executor)
+ } else if authorized.plan.contract.id == T1_RECONCILIATION_TOOL_ID {
+ let executor = T1ReconciliationAgentToolExecutor::new(
+ t1_workspace_root
+ .as_deref()
+ .ok_or_else(|| "operations.reconcile_excel executor is unavailable".to_string())?,
+ );
+ run_authorized_agent_tool_execution(authorized, &executor)
} else if authorized.plan.contract.id == FILESYSTEM_MUTATE_TOOL_ID {
let client = LocalFileSystemMutationClient;
let executor = FileSystemMutationAgentToolExecutor {
diff --git a/apps/desktop/src-tauri/src/kernel/mod.rs b/apps/desktop/src-tauri/src/kernel/mod.rs
index 85bc8e1..354b68d 100644
--- a/apps/desktop/src-tauri/src/kernel/mod.rs
+++ b/apps/desktop/src-tauri/src/kernel/mod.rs
@@ -32,6 +32,7 @@ pub mod sandbox;
pub mod skill;
pub mod skill_source;
pub mod soul;
+pub mod t1_reconciliation;
pub mod task_capability_manifest;
pub mod task_grouped_approval;
pub mod task_lifecycle;
diff --git a/apps/desktop/src-tauri/src/kernel/t1_reconciliation.rs b/apps/desktop/src-tauri/src/kernel/t1_reconciliation.rs
new file mode 100644
index 0000000..80211f7
--- /dev/null
+++ b/apps/desktop/src-tauri/src/kernel/t1_reconciliation.rs
@@ -0,0 +1,1673 @@
+use std::collections::{BTreeMap, BTreeSet};
+use std::fs::{self, File, OpenOptions};
+use std::io::{Cursor, Read, Write};
+use std::path::{Component, Path, PathBuf};
+
+use quick_xml::{events::Event, Reader};
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use uuid::Uuid;
+use zip::{write::FileOptions, ZipArchive};
+
+use super::tool_runtime::{
+ AgentToolExecutor, ToolEvidence, ToolExecutionOutput, ToolExecutionPlan,
+ ToolVerificationResult, T1_RECONCILIATION_TOOL_ID,
+};
+
+pub const T1_RECONCILIATION_ARTIFACT_ID: &str = "t1-reconciliation-xlsx";
+pub const T1_SOURCE_MANIFEST_EVIDENCE_KIND: &str = "t1_source_manifest";
+pub const T1_PROVENANCE_EVIDENCE_KIND: &str = "t1_fact_provenance";
+pub const T1_RECONCILIATION_EVIDENCE_KIND: &str = "t1_reconciliation_xlsx";
+
+const SOURCE_MANIFEST_VERSION: &str = "ds-agent.t1-source-manifest/v1";
+const PROVENANCE_VERSION: &str = "ds-agent.t1-provenance-manifest/v1";
+const ARTIFACT_RECEIPT_VERSION: &str = "ds-agent.t1-reconciliation-artifact/v1";
+const MAX_SOURCE_BYTES: usize = 8 * 1024 * 1024;
+const MAX_OPC_PARTS: usize = 128;
+const MAX_OPC_BYTES: u64 = 8 * 1024 * 1024;
+const NUMERIC_TOLERANCE: f64 = 0.000_001;
+const XLSX_MEDIA_TYPE: &str = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+const DOCX_MEDIA_TYPE: &str =
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+const PDF_MEDIA_TYPE: &str = "application/pdf";
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1ReconciliationRequest {
+ pub source_directory: String,
+ pub output_relative_path: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1SourceManifestEntry {
+ pub source_id: String,
+ pub relative_path: String,
+ pub media_type: String,
+ pub bytes: u64,
+ pub sha256: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1SourceManifest {
+ pub version: String,
+ pub source_set_id: String,
+ pub entries: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1SourceFactLocator {
+ pub source_id: String,
+ pub relative_path: String,
+ pub source_sha256: String,
+ pub locator: String,
+ pub extracted_value: String,
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1DerivedFactProvenance {
+ pub operands: Vec,
+ pub algorithm_id: String,
+ pub formula: String,
+ pub recomputed_value: String,
+ pub tolerance: f64,
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1FactProvenance {
+ pub fact_id: String,
+ pub value: String,
+ pub source: Option,
+ pub derivation: Option,
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1ProvenanceManifest {
+ pub version: String,
+ pub source_set_id: String,
+ pub source_manifest_sha256: String,
+ pub facts: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1ReconciliationArtifactReceipt {
+ pub version: String,
+ pub artifact_id: String,
+ pub relative_path: String,
+ pub bytes: u64,
+ pub sha256: String,
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1ReconciliationOutcome {
+ pub source_manifest: T1SourceManifest,
+ pub provenance: T1ProvenanceManifest,
+ pub artifact: T1ReconciliationArtifactReceipt,
+ pub key_figures: BTreeMap,
+ pub completion_evidence: Vec,
+}
+
+#[derive(Clone, Debug)]
+struct SourceDocument {
+ entry: T1SourceManifestEntry,
+ bytes: Vec,
+}
+
+pub struct T1ReconciliationAgentToolExecutor<'a> {
+ workspace_root: &'a Path,
+}
+
+impl<'a> T1ReconciliationAgentToolExecutor<'a> {
+ pub fn new(workspace_root: &'a Path) -> Self {
+ Self { workspace_root }
+ }
+}
+
+impl AgentToolExecutor for T1ReconciliationAgentToolExecutor<'_> {
+ fn execute(&self, plan: &ToolExecutionPlan) -> Result {
+ if plan.contract.id != T1_RECONCILIATION_TOOL_ID {
+ return Err(format!(
+ "T1 reconciliation executor cannot execute `{}`",
+ plan.contract.id
+ ));
+ }
+ let request = serde_json::from_value::(plan.request.input.clone())
+ .map_err(|error| {
+ format!("operations.reconcile_excel input could not be decoded: {error}")
+ })?;
+ let outcome = run_t1_reconciliation(self.workspace_root, &request)?;
+ let source_count = outcome.source_manifest.entries.len();
+ let fact_count = outcome.provenance.facts.len();
+ let artifact_bytes = outcome.artifact.bytes;
+ let evidence = outcome.completion_evidence.clone();
+ Ok(ToolExecutionOutput {
+ output: serde_json::to_value(&outcome).map_err(|error| {
+ format!("T1 reconciliation output could not be serialized: {error}")
+ })?,
+ evidence,
+ verification: ToolVerificationResult::passed(format!(
+ "operations.reconcile_excel re-read {source_count} exact sources, reconciled {fact_count} facts, and verified {artifact_bytes} XLSX bytes"
+ )),
+ })
+ }
+}
+
+pub fn run_t1_reconciliation(
+ workspace_root: &Path,
+ request: &T1ReconciliationRequest,
+) -> Result {
+ let workspace = canonical_workspace(workspace_root)?;
+ let sources = scan_sources(&workspace, &request.source_directory)?;
+ let source_manifest = build_source_manifest(&sources)?;
+ let provenance = build_provenance(&source_manifest, &sources)?;
+ let workbook = build_reconciliation_workbook(&provenance)?;
+ verify_workbook(&source_manifest, &provenance, &workbook)?;
+
+ let (output_path, output_relative_path) =
+ resolve_new_output(&workspace, &request.output_relative_path)?;
+ write_new_artifact(&output_path, &workbook)?;
+ let artifact = T1ReconciliationArtifactReceipt {
+ version: ARTIFACT_RECEIPT_VERSION.to_string(),
+ artifact_id: T1_RECONCILIATION_ARTIFACT_ID.to_string(),
+ relative_path: output_relative_path,
+ bytes: workbook.len() as u64,
+ sha256: sha256(&workbook),
+ };
+
+ match verify_persisted_t1_reconciliation(
+ &workspace,
+ request,
+ &source_manifest,
+ &provenance,
+ &artifact,
+ ) {
+ Ok(completion_evidence) => Ok(T1ReconciliationOutcome {
+ key_figures: key_figures(&provenance)?,
+ source_manifest,
+ provenance,
+ artifact,
+ completion_evidence,
+ }),
+ Err(error) => {
+ remove_artifact_if_unchanged(&output_path, &artifact);
+ Err(error)
+ }
+ }
+}
+
+pub fn verify_persisted_t1_reconciliation(
+ workspace_root: &Path,
+ request: &T1ReconciliationRequest,
+ expected_sources: &T1SourceManifest,
+ expected_provenance: &T1ProvenanceManifest,
+ expected_artifact: &T1ReconciliationArtifactReceipt,
+) -> Result, String> {
+ let workspace = canonical_workspace(workspace_root)?;
+ let sources = scan_sources(&workspace, &request.source_directory)?;
+ let source_manifest = build_source_manifest(&sources)?;
+ if &source_manifest != expected_sources {
+ return Err("T1 source identity changed before completion verification".to_string());
+ }
+ let provenance = build_provenance(&source_manifest, &sources)?;
+ if &provenance != expected_provenance {
+ return Err("T1 provenance changed before completion verification".to_string());
+ }
+
+ let (output_path, output_relative_path) =
+ resolve_existing_output(&workspace, &request.output_relative_path)?;
+ if expected_artifact.version != ARTIFACT_RECEIPT_VERSION
+ || expected_artifact.artifact_id != T1_RECONCILIATION_ARTIFACT_ID
+ || expected_artifact.relative_path != output_relative_path
+ {
+ return Err("T1 artifact identity receipt is invalid".to_string());
+ }
+ let bytes = read_bounded_file(&output_path, MAX_OPC_BYTES as usize)?;
+ if expected_artifact.bytes != bytes.len() as u64 || expected_artifact.sha256 != sha256(&bytes) {
+ return Err("T1 artifact bytes do not match the completion receipt".to_string());
+ }
+ verify_t1_reconciliation_artifact(&source_manifest, &provenance, expected_artifact, &bytes)
+}
+
+pub fn verify_t1_reconciliation_artifact(
+ source_manifest: &T1SourceManifest,
+ provenance: &T1ProvenanceManifest,
+ artifact: &T1ReconciliationArtifactReceipt,
+ bytes: &[u8],
+) -> Result, String> {
+ if artifact.version != ARTIFACT_RECEIPT_VERSION
+ || artifact.artifact_id != T1_RECONCILIATION_ARTIFACT_ID
+ || artifact.bytes != bytes.len() as u64
+ || artifact.sha256 != sha256(bytes)
+ {
+ return Err("T1 artifact identity receipt is invalid".to_string());
+ }
+ verify_workbook(source_manifest, provenance, bytes)?;
+ completion_evidence(source_manifest, provenance, artifact)
+}
+
+fn canonical_workspace(workspace_root: &Path) -> Result {
+ let metadata = fs::symlink_metadata(workspace_root)
+ .map_err(|error| format!("T1 workspace is unavailable: {error}"))?;
+ if metadata.file_type().is_symlink() || !metadata.is_dir() {
+ return Err("T1 workspace must be a real directory".to_string());
+ }
+ workspace_root
+ .canonicalize()
+ .map_err(|error| format!("T1 workspace could not be resolved: {error}"))
+}
+
+fn validated_relative_path(value: &str, label: &str) -> Result<(PathBuf, String), String> {
+ let normalized = value.trim().replace('\\', "/");
+ let normalized = normalized.trim_matches('/');
+ if normalized.is_empty() {
+ return Err(format!("{label} is required"));
+ }
+ let path = Path::new(normalized);
+ if path.is_absolute()
+ || path
+ .components()
+ .any(|component| !matches!(component, Component::Normal(_)))
+ {
+ return Err(format!("{label} must stay inside the authorized workspace"));
+ }
+ Ok((path.to_path_buf(), normalized.to_string()))
+}
+
+fn scan_sources(workspace: &Path, relative_directory: &str) -> Result, String> {
+ let (relative, _) = validated_relative_path(relative_directory, "T1 source directory")?;
+ let candidate = workspace.join(relative);
+ let metadata = fs::symlink_metadata(&candidate)
+ .map_err(|error| format!("T1 source directory is unavailable: {error}"))?;
+ if metadata.file_type().is_symlink() || !metadata.is_dir() {
+ return Err("T1 source directory must be a real directory".to_string());
+ }
+ let directory = candidate
+ .canonicalize()
+ .map_err(|error| format!("T1 source directory could not be resolved: {error}"))?;
+ if !directory.starts_with(workspace) {
+ return Err("T1 source directory escaped the authorized workspace".to_string());
+ }
+
+ let mut paths = fs::read_dir(&directory)
+ .map_err(|error| format!("T1 source directory could not be scanned: {error}"))?
+ .collect::, _>>()
+ .map_err(|error| format!("T1 source directory entry could not be read: {error}"))?
+ .into_iter()
+ .map(|entry| entry.path())
+ .filter(|path| source_role(path).is_some())
+ .collect::>();
+ paths.sort();
+ if paths.len() != 3 {
+ return Err("T1 requires exactly one XLSX, one DOCX, and one PDF source".to_string());
+ }
+
+ let mut roles = BTreeSet::new();
+ let mut documents = Vec::with_capacity(paths.len());
+ for path in paths {
+ let (source_id, media_type) =
+ source_role(&path).ok_or_else(|| "T1 source type is unsupported".to_string())?;
+ if !roles.insert(source_id) {
+ return Err("T1 source role is duplicated".to_string());
+ }
+ let metadata = fs::symlink_metadata(&path)
+ .map_err(|error| format!("T1 source metadata is unavailable: {error}"))?;
+ if metadata.file_type().is_symlink() || !metadata.is_file() {
+ return Err("T1 source must be a real file".to_string());
+ }
+ let canonical = path
+ .canonicalize()
+ .map_err(|error| format!("T1 source could not be resolved: {error}"))?;
+ if !canonical.starts_with(workspace) {
+ return Err("T1 source escaped the authorized workspace".to_string());
+ }
+ let bytes = read_bounded_file(&canonical, MAX_SOURCE_BYTES)?;
+ let relative_path = canonical
+ .strip_prefix(workspace)
+ .map_err(|_| "T1 source relative path is unavailable".to_string())?
+ .to_string_lossy()
+ .replace('\\', "/");
+ documents.push(SourceDocument {
+ entry: T1SourceManifestEntry {
+ source_id: source_id.to_string(),
+ relative_path,
+ media_type: media_type.to_string(),
+ bytes: bytes.len() as u64,
+ sha256: sha256(&bytes),
+ },
+ bytes,
+ });
+ }
+ if roles != BTreeSet::from(["excel", "word", "pdf"]) {
+ return Err("T1 source roles are incomplete".to_string());
+ }
+ Ok(documents)
+}
+
+fn source_role(path: &Path) -> Option<(&'static str, &'static str)> {
+ match path.extension()?.to_str()?.to_ascii_lowercase().as_str() {
+ "xlsx" => Some(("excel", XLSX_MEDIA_TYPE)),
+ "docx" => Some(("word", DOCX_MEDIA_TYPE)),
+ "pdf" => Some(("pdf", PDF_MEDIA_TYPE)),
+ _ => None,
+ }
+}
+
+fn read_bounded_file(path: &Path, maximum: usize) -> Result, String> {
+ let mut file =
+ File::open(path).map_err(|error| format!("T1 file could not be opened: {error}"))?;
+ let mut bytes = Vec::new();
+ Read::take(&mut file, maximum.saturating_add(1) as u64)
+ .read_to_end(&mut bytes)
+ .map_err(|error| format!("T1 file could not be read: {error}"))?;
+ if bytes.is_empty() || bytes.len() > maximum {
+ return Err("T1 file size is invalid".to_string());
+ }
+ Ok(bytes)
+}
+
+fn resolve_new_output(workspace: &Path, value: &str) -> Result<(PathBuf, String), String> {
+ let (relative, normalized) = validated_relative_path(value, "T1 output path")?;
+ if !normalized.to_ascii_lowercase().ends_with(".xlsx") {
+ return Err("T1 output path must end in .xlsx".to_string());
+ }
+ let output = workspace.join(relative);
+ if output.exists() {
+ return Err("T1 output already exists; overwrite is blocked".to_string());
+ }
+ let parent = output
+ .parent()
+ .ok_or_else(|| "T1 output parent is invalid".to_string())?;
+ let metadata = fs::symlink_metadata(parent)
+ .map_err(|error| format!("T1 output parent is unavailable: {error}"))?;
+ if metadata.file_type().is_symlink() || !metadata.is_dir() {
+ return Err("T1 output parent must be an authorized real directory".to_string());
+ }
+ let parent = parent
+ .canonicalize()
+ .map_err(|error| format!("T1 output parent could not be resolved: {error}"))?;
+ if !parent.starts_with(workspace) {
+ return Err("T1 output escaped the authorized workspace".to_string());
+ }
+ let file_name = output
+ .file_name()
+ .ok_or_else(|| "T1 output file name is invalid".to_string())?;
+ Ok((parent.join(file_name), normalized))
+}
+
+fn resolve_existing_output(workspace: &Path, value: &str) -> Result<(PathBuf, String), String> {
+ let (relative, normalized) = validated_relative_path(value, "T1 output path")?;
+ if !normalized.to_ascii_lowercase().ends_with(".xlsx") {
+ return Err("T1 output path must end in .xlsx".to_string());
+ }
+ let candidate = workspace.join(relative);
+ let metadata = fs::symlink_metadata(&candidate)
+ .map_err(|error| format!("T1 output is unavailable: {error}"))?;
+ if metadata.file_type().is_symlink() || !metadata.is_file() {
+ return Err("T1 output must be a real file".to_string());
+ }
+ let canonical = candidate
+ .canonicalize()
+ .map_err(|error| format!("T1 output could not be resolved: {error}"))?;
+ if !canonical.starts_with(workspace) {
+ return Err("T1 output escaped the authorized workspace".to_string());
+ }
+ Ok((canonical, normalized))
+}
+
+fn write_new_artifact(path: &Path, bytes: &[u8]) -> Result<(), String> {
+ let parent = path
+ .parent()
+ .ok_or_else(|| "T1 output parent is invalid".to_string())?;
+ let staged = parent.join(format!(".t1-reconciliation-{}.tmp", Uuid::new_v4()));
+ let result = (|| {
+ let mut file = OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .open(&staged)
+ .map_err(|error| format!("T1 staged output could not be created: {error}"))?;
+ file.write_all(bytes)
+ .map_err(|error| format!("T1 staged output could not be written: {error}"))?;
+ file.sync_all()
+ .map_err(|error| format!("T1 staged output could not be synchronized: {error}"))?;
+ fs::rename(&staged, path)
+ .map_err(|error| format!("T1 output could not be committed: {error}"))
+ })();
+ if result.is_err() {
+ let _ = fs::remove_file(staged);
+ }
+ result
+}
+
+fn remove_artifact_if_unchanged(path: &Path, artifact: &T1ReconciliationArtifactReceipt) {
+ let matches = fs::symlink_metadata(path)
+ .ok()
+ .filter(|metadata| !metadata.file_type().is_symlink() && metadata.is_file())
+ .and_then(|_| read_bounded_file(path, MAX_OPC_BYTES as usize).ok())
+ .is_some_and(|bytes| {
+ artifact.bytes == bytes.len() as u64 && artifact.sha256 == sha256(&bytes)
+ });
+ if matches {
+ let _ = fs::remove_file(path);
+ }
+}
+
+fn build_source_manifest(sources: &[SourceDocument]) -> Result {
+ let entries = sources
+ .iter()
+ .map(|source| source.entry.clone())
+ .collect::>();
+ let source_set_id = canonical_hash(&entries)?;
+ Ok(T1SourceManifest {
+ version: SOURCE_MANIFEST_VERSION.to_string(),
+ source_set_id,
+ entries,
+ })
+}
+
+fn build_provenance(
+ source_manifest: &T1SourceManifest,
+ sources: &[SourceDocument],
+) -> Result {
+ if source_manifest.version != SOURCE_MANIFEST_VERSION
+ || source_manifest.entries
+ != sources
+ .iter()
+ .map(|source| source.entry.clone())
+ .collect::>()
+ || source_manifest.source_set_id != canonical_hash(&source_manifest.entries)?
+ {
+ return Err("T1 source manifest identity is invalid".to_string());
+ }
+ let excel = source_by_id(sources, "excel")?;
+ let word = source_by_id(sources, "word")?;
+ let pdf = source_by_id(sources, "pdf")?;
+ let revenue = extract_xlsx_facts(&excel.bytes)?;
+ let operations = extract_docx_facts(&word.bytes)?;
+ let risks = extract_pdf_facts(&pdf.bytes)?;
+ let period = fact_text(&revenue, "period")?;
+ if operations.get("period").map(|value| value.0.as_str()) != Some(period.as_str())
+ || risks.get("period").map(|value| value.0.as_str()) != Some(period.as_str())
+ {
+ return Err("T1 source periods conflict".to_string());
+ }
+
+ let mut facts = Vec::new();
+ for key in [
+ "period",
+ "available_room_nights",
+ "sold_room_nights",
+ "reported_occupancy_rate",
+ "rooms_revenue_cny",
+ "reported_adr_cny",
+ "food_beverage_revenue_cny",
+ "other_revenue_cny",
+ "reported_total_revenue_cny",
+ "budget_total_revenue_cny",
+ "prior_period_total_revenue_cny",
+ "budget_occupancy_rate",
+ ] {
+ facts.push(source_fact(excel, key, &revenue)?);
+ }
+ for key in [
+ "breakfast_queue_complaints",
+ "overdue_invoice_corrections_over_48h",
+ "group_leads_deferred_to_july",
+ ] {
+ facts.push(source_fact(word, key, &operations)?);
+ }
+ for key in [
+ "elevator_2_unplanned_outages",
+ "overdue_fire_door_closing_checks",
+ "temporary_food_staff_retraining_incomplete",
+ ] {
+ facts.push(source_fact(pdf, key, &risks)?);
+ }
+
+ let available = fact_number(&revenue, "available_room_nights")?;
+ let sold = fact_number(&revenue, "sold_room_nights")?;
+ let rooms = fact_number(&revenue, "rooms_revenue_cny")?;
+ let food = fact_number(&revenue, "food_beverage_revenue_cny")?;
+ let other = fact_number(&revenue, "other_revenue_cny")?;
+ let budget = fact_number(&revenue, "budget_total_revenue_cny")?;
+ let prior = fact_number(&revenue, "prior_period_total_revenue_cny")?;
+ let budget_occupancy = fact_number(&revenue, "budget_occupancy_rate")?;
+ if available <= 0.0 || sold <= 0.0 || budget == 0.0 || prior == 0.0 {
+ return Err("T1 derived fact denominator is invalid".to_string());
+ }
+ let occupancy = sold / available;
+ let adr = rooms / sold;
+ let revpar = rooms / available;
+ let total = rooms + food + other;
+ let budget_variance = total - budget;
+ let prior_variance = total - prior;
+ for (fact_id, value, operands, algorithm_id, formula) in [
+ (
+ "occupancy_rate",
+ occupancy,
+ vec!["sold_room_nights", "available_room_nights"],
+ "t1.derive-occupancy/v1",
+ "sold_room_nights / available_room_nights",
+ ),
+ (
+ "adr_cny",
+ adr,
+ vec!["rooms_revenue_cny", "sold_room_nights"],
+ "t1.derive-adr/v1",
+ "rooms_revenue_cny / sold_room_nights",
+ ),
+ (
+ "revpar_cny",
+ revpar,
+ vec!["rooms_revenue_cny", "available_room_nights"],
+ "t1.derive-revpar/v1",
+ "rooms_revenue_cny / available_room_nights",
+ ),
+ (
+ "total_revenue_cny",
+ total,
+ vec![
+ "rooms_revenue_cny",
+ "food_beverage_revenue_cny",
+ "other_revenue_cny",
+ ],
+ "t1.derive-total-revenue/v1",
+ "rooms_revenue_cny + food_beverage_revenue_cny + other_revenue_cny",
+ ),
+ (
+ "budget_variance_cny",
+ budget_variance,
+ vec!["total_revenue_cny", "budget_total_revenue_cny"],
+ "t1.derive-budget-variance/v1",
+ "total_revenue_cny - budget_total_revenue_cny",
+ ),
+ (
+ "budget_variance_rate",
+ budget_variance / budget,
+ vec!["budget_variance_cny", "budget_total_revenue_cny"],
+ "t1.derive-budget-variance-rate/v1",
+ "budget_variance_cny / budget_total_revenue_cny",
+ ),
+ (
+ "prior_variance_cny",
+ prior_variance,
+ vec!["total_revenue_cny", "prior_period_total_revenue_cny"],
+ "t1.derive-prior-variance/v1",
+ "total_revenue_cny - prior_period_total_revenue_cny",
+ ),
+ (
+ "prior_variance_rate",
+ prior_variance / prior,
+ vec!["prior_variance_cny", "prior_period_total_revenue_cny"],
+ "t1.derive-prior-variance-rate/v1",
+ "prior_variance_cny / prior_period_total_revenue_cny",
+ ),
+ (
+ "occupancy_variance_percentage_points",
+ (occupancy - budget_occupancy) * 100.0,
+ vec!["occupancy_rate", "budget_occupancy_rate"],
+ "t1.derive-occupancy-variance/v1",
+ "(occupancy_rate - budget_occupancy_rate) * 100",
+ ),
+ ] {
+ facts.push(derived_fact(
+ fact_id,
+ value,
+ operands,
+ algorithm_id,
+ formula,
+ ));
+ }
+ let by_id = facts
+ .iter()
+ .map(|fact| (fact.fact_id.as_str(), fact))
+ .collect::>();
+ for (reported, computed) in [
+ ("reported_occupancy_rate", "occupancy_rate"),
+ ("reported_adr_cny", "adr_cny"),
+ ("reported_total_revenue_cny", "total_revenue_cny"),
+ ] {
+ if (provenance_number(&by_id, reported)? - provenance_number(&by_id, computed)?).abs()
+ > NUMERIC_TOLERANCE
+ {
+ return Err(format!(
+ "T1 numeric conflict: {reported} does not match {computed}"
+ ));
+ }
+ }
+ Ok(T1ProvenanceManifest {
+ version: PROVENANCE_VERSION.to_string(),
+ source_set_id: source_manifest.source_set_id.clone(),
+ source_manifest_sha256: canonical_hash(source_manifest)?,
+ facts,
+ })
+}
+
+fn source_by_id<'a>(
+ sources: &'a [SourceDocument],
+ source_id: &str,
+) -> Result<&'a SourceDocument, String> {
+ let matches = sources
+ .iter()
+ .filter(|source| source.entry.source_id == source_id)
+ .collect::>();
+ if matches.len() != 1 {
+ return Err(format!("T1 source {source_id} is missing or duplicated"));
+ }
+ Ok(matches[0])
+}
+
+fn source_fact(
+ source: &SourceDocument,
+ fact_id: &str,
+ extracted: &BTreeMap,
+) -> Result {
+ let (value, locator) = extracted
+ .get(fact_id)
+ .ok_or_else(|| format!("T1 source fact {fact_id} is missing"))?;
+ Ok(T1FactProvenance {
+ fact_id: fact_id.to_string(),
+ value: value.clone(),
+ source: Some(T1SourceFactLocator {
+ source_id: source.entry.source_id.clone(),
+ relative_path: source.entry.relative_path.clone(),
+ source_sha256: source.entry.sha256.clone(),
+ locator: locator.clone(),
+ extracted_value: value.clone(),
+ }),
+ derivation: None,
+ })
+}
+
+fn derived_fact(
+ fact_id: &str,
+ value: f64,
+ operands: Vec<&str>,
+ algorithm_id: &str,
+ formula: &str,
+) -> T1FactProvenance {
+ let value = canonical_number(value);
+ T1FactProvenance {
+ fact_id: fact_id.to_string(),
+ value: value.clone(),
+ source: None,
+ derivation: Some(T1DerivedFactProvenance {
+ operands: operands.into_iter().map(str::to_string).collect(),
+ algorithm_id: algorithm_id.to_string(),
+ formula: formula.to_string(),
+ recomputed_value: value,
+ tolerance: NUMERIC_TOLERANCE,
+ }),
+ }
+}
+
+fn extract_xlsx_facts(bytes: &[u8]) -> Result, String> {
+ let package = OpcPackage::read(bytes)?;
+ package.validate_main(
+ "xl/workbook.xml",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
+ )?;
+ package.validate_content_type(
+ "xl/worksheets/sheet1.xml",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
+ )?;
+ package.validate_relationship(
+ "xl/_rels/workbook.xml.rels",
+ "/worksheet",
+ "worksheets/sheet1.xml",
+ 1,
+ )?;
+ let cells = parse_worksheet_cells(package.text("xl/worksheets/sheet1.xml")?)?;
+ let mut facts = BTreeMap::new();
+ for row in 1..=13 {
+ let key = cell_text(&cells, &format!("A{row}"))?;
+ let value = cell_text(&cells, &format!("B{row}"))?;
+ if facts
+ .insert(
+ key,
+ (value, format!("xlsx:xl/worksheets/sheet1.xml#B{row}")),
+ )
+ .is_some()
+ {
+ return Err("T1 XLSX source contains a duplicate fact".to_string());
+ }
+ }
+ Ok(facts)
+}
+
+fn extract_docx_facts(bytes: &[u8]) -> Result, String> {
+ let package = OpcPackage::read(bytes)?;
+ package.validate_main(
+ "word/document.xml",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
+ )?;
+ let paragraphs = xml_paragraphs(package.text("word/document.xml")?, b"p")?;
+ let mut facts = BTreeMap::new();
+ for (index, paragraph) in paragraphs.iter().enumerate() {
+ if let Some((key, value)) = paragraph.split_once('=') {
+ if facts
+ .insert(
+ key.to_string(),
+ (
+ value.to_string(),
+ format!("docx:word/document.xml#p{}", index + 1),
+ ),
+ )
+ .is_some()
+ {
+ return Err("T1 DOCX source contains a duplicate fact".to_string());
+ }
+ }
+ }
+ Ok(facts)
+}
+
+fn extract_pdf_facts(bytes: &[u8]) -> Result, String> {
+ let text = std::str::from_utf8(bytes)
+ .map_err(|_| "T1 PDF source is not deterministic UTF-8".to_string())?;
+ if !text.starts_with("%PDF-1.4") || !text.ends_with("%%EOF\n") || !text.contains("xref") {
+ return Err("T1 PDF source structure is invalid".to_string());
+ }
+ let mut facts = BTreeMap::new();
+ for key in [
+ "period",
+ "elevator_2_unplanned_outages",
+ "overdue_fire_door_closing_checks",
+ "temporary_food_staff_retraining_incomplete",
+ ] {
+ let prefix = format!("({key}=");
+ let start = text
+ .find(&prefix)
+ .ok_or_else(|| format!("T1 PDF source fact {key} is missing"))?
+ + prefix.len();
+ let end = text[start..]
+ .find(") Tj")
+ .ok_or_else(|| "T1 PDF text token is invalid".to_string())?
+ + start;
+ facts.insert(
+ key.to_string(),
+ (
+ text[start..end].to_string(),
+ format!("pdf:text-token:{key}"),
+ ),
+ );
+ }
+ Ok(facts)
+}
+
+fn build_reconciliation_workbook(provenance: &T1ProvenanceManifest) -> Result, String> {
+ let facts = provenance
+ .facts
+ .iter()
+ .map(|fact| (fact.fact_id.as_str(), fact))
+ .collect::>();
+ let source_rows = [
+ (2, "period"),
+ (3, "available_room_nights"),
+ (4, "sold_room_nights"),
+ (5, "reported_occupancy_rate"),
+ (6, "rooms_revenue_cny"),
+ (7, "reported_adr_cny"),
+ (8, "food_beverage_revenue_cny"),
+ (9, "other_revenue_cny"),
+ (10, "reported_total_revenue_cny"),
+ (11, "budget_total_revenue_cny"),
+ (12, "prior_period_total_revenue_cny"),
+ (13, "budget_occupancy_rate"),
+ (14, "breakfast_queue_complaints"),
+ (15, "overdue_invoice_corrections_over_48h"),
+ (16, "group_leads_deferred_to_july"),
+ (17, "elevator_2_unplanned_outages"),
+ (18, "overdue_fire_door_closing_checks"),
+ (19, "temporary_food_staff_retraining_incomplete"),
+ ];
+ let derived_rows = [
+ (20, "occupancy_rate", "B4/B3"),
+ (21, "adr_cny", "B6/B4"),
+ (22, "revpar_cny", "B6/B3"),
+ (23, "total_revenue_cny", "SUM(B6,B8,B9)"),
+ (24, "budget_variance_cny", "B23-B11"),
+ (25, "budget_variance_rate", "B24/B11"),
+ (26, "prior_variance_cny", "B23-B12"),
+ (27, "prior_variance_rate", "B26/B12"),
+ (28, "occupancy_variance_percentage_points", "(B20-B13)*100"),
+ ];
+ let mut rows = vec![format!(
+ "{}{}{}{}{}{}
",
+ inline_cell("A1", "fact_id"),
+ inline_cell("B1", "value"),
+ inline_cell("C1", "source_or_kind"),
+ inline_cell("D1", "hash_or_algorithm"),
+ inline_cell("E1", "locator_or_operands"),
+ inline_cell("F1", "provenance_fingerprint")
+ )];
+ for (row, fact_id) in source_rows {
+ let fact = facts
+ .get(fact_id)
+ .ok_or_else(|| format!("missing T1 fact {fact_id}"))?;
+ let source = fact
+ .source
+ .as_ref()
+ .ok_or_else(|| format!("missing T1 source provenance {fact_id}"))?;
+ rows.push(format!(
+ "{}{}{}{}{}{}
",
+ inline_cell(&format!("A{row}"), fact_id),
+ value_cell(&format!("B{row}"), &fact.value, None),
+ inline_cell(&format!("C{row}"), &source.relative_path),
+ inline_cell(&format!("D{row}"), &source.source_sha256),
+ inline_cell(&format!("E{row}"), &source.locator),
+ inline_cell(&format!("F{row}"), &canonical_hash(fact)?)
+ ));
+ }
+ for (row, fact_id, formula) in derived_rows {
+ let fact = facts
+ .get(fact_id)
+ .ok_or_else(|| format!("missing T1 fact {fact_id}"))?;
+ let derivation = fact
+ .derivation
+ .as_ref()
+ .ok_or_else(|| format!("missing T1 derivation {fact_id}"))?;
+ rows.push(format!(
+ "{}{}{}{}{}{}
",
+ inline_cell(&format!("A{row}"), fact_id),
+ value_cell(&format!("B{row}"), &fact.value, Some(formula)),
+ inline_cell(&format!("C{row}"), "derived"),
+ inline_cell(&format!("D{row}"), &derivation.algorithm_id),
+ inline_cell(&format!("E{row}"), &derivation.operands.join(",")),
+ inline_cell(&format!("F{row}"), &canonical_hash(fact)?)
+ ));
+ }
+ let sheet = format!(
+ "{} ",
+ rows.join("")
+ );
+ write_zip(BTreeMap::from([
+ ("[Content_Types].xml".to_string(), b" ".to_vec()),
+ ("_rels/.rels".to_string(), root_relationships("xl/workbook.xml").into_bytes()),
+ ("docProps/core.xml".to_string(), b"".to_vec()),
+ ("xl/workbook.xml".to_string(), b" ".to_vec()),
+ ("xl/_rels/workbook.xml.rels".to_string(), b" ".to_vec()),
+ ("xl/worksheets/sheet1.xml".to_string(), sheet.into_bytes()),
+ ]))
+}
+
+fn verify_workbook(
+ source_manifest: &T1SourceManifest,
+ provenance: &T1ProvenanceManifest,
+ bytes: &[u8],
+) -> Result<(), String> {
+ if source_manifest.version != SOURCE_MANIFEST_VERSION
+ || source_manifest.source_set_id != canonical_hash(&source_manifest.entries)?
+ || provenance.version != PROVENANCE_VERSION
+ || provenance.source_set_id != source_manifest.source_set_id
+ || provenance.source_manifest_sha256 != canonical_hash(source_manifest)?
+ {
+ return Err("T1 provenance is not bound to the exact source manifest".to_string());
+ }
+ let package = OpcPackage::read(bytes)?;
+ package.validate_main(
+ "xl/workbook.xml",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
+ )?;
+ package.validate_content_type(
+ "xl/worksheets/sheet1.xml",
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
+ )?;
+ package.validate_relationship(
+ "xl/_rels/workbook.xml.rels",
+ "/worksheet",
+ "worksheets/sheet1.xml",
+ 1,
+ )?;
+ let sheet = package.text("xl/worksheets/sheet1.xml")?;
+ for marker in ["#REF!", "#DIV/0!", "#VALUE!", "#N/A", "\u{fffd}"] {
+ if sheet.contains(marker) {
+ return Err("T1 reconciliation contains a formula or encoding error".to_string());
+ }
+ }
+ let cells = parse_worksheet_cells(sheet)?;
+ let facts = provenance
+ .facts
+ .iter()
+ .map(|fact| (fact.fact_id.as_str(), fact))
+ .collect::>();
+ let source_rows = [
+ (2, "period"),
+ (3, "available_room_nights"),
+ (4, "sold_room_nights"),
+ (5, "reported_occupancy_rate"),
+ (6, "rooms_revenue_cny"),
+ (7, "reported_adr_cny"),
+ (8, "food_beverage_revenue_cny"),
+ (9, "other_revenue_cny"),
+ (10, "reported_total_revenue_cny"),
+ (11, "budget_total_revenue_cny"),
+ (12, "prior_period_total_revenue_cny"),
+ (13, "budget_occupancy_rate"),
+ (14, "breakfast_queue_complaints"),
+ (15, "overdue_invoice_corrections_over_48h"),
+ (16, "group_leads_deferred_to_july"),
+ (17, "elevator_2_unplanned_outages"),
+ (18, "overdue_fire_door_closing_checks"),
+ (19, "temporary_food_staff_retraining_incomplete"),
+ ];
+ for (row, fact_id) in source_rows {
+ let fact = facts
+ .get(fact_id)
+ .ok_or_else(|| format!("T1 fact {fact_id} is missing"))?;
+ let source = fact
+ .source
+ .as_ref()
+ .ok_or_else(|| format!("T1 source provenance {fact_id} is missing"))?;
+ require_text(&cells, &format!("A{row}"), fact_id)?;
+ require_value(&cells, &format!("B{row}"), &fact.value)?;
+ require_text(&cells, &format!("C{row}"), &source.relative_path)?;
+ require_text(&cells, &format!("D{row}"), &source.source_sha256)?;
+ require_text(&cells, &format!("E{row}"), &source.locator)?;
+ require_text(&cells, &format!("F{row}"), &canonical_hash(fact)?)?;
+ }
+ for (row, fact_id, formula) in [
+ (20, "occupancy_rate", "B4/B3"),
+ (21, "adr_cny", "B6/B4"),
+ (22, "revpar_cny", "B6/B3"),
+ (23, "total_revenue_cny", "SUM(B6,B8,B9)"),
+ (24, "budget_variance_cny", "B23-B11"),
+ (25, "budget_variance_rate", "B24/B11"),
+ (26, "prior_variance_cny", "B23-B12"),
+ (27, "prior_variance_rate", "B26/B12"),
+ (28, "occupancy_variance_percentage_points", "(B20-B13)*100"),
+ ] {
+ let fact = facts
+ .get(fact_id)
+ .ok_or_else(|| format!("T1 fact {fact_id} is missing"))?;
+ let derivation = fact
+ .derivation
+ .as_ref()
+ .ok_or_else(|| format!("T1 derivation {fact_id} is missing"))?;
+ require_text(&cells, &format!("A{row}"), fact_id)?;
+ let cell = cells
+ .get(&format!("B{row}"))
+ .ok_or_else(|| "T1 reconciliation formula cell is missing".to_string())?;
+ if cell.formula.as_deref() != Some(formula) {
+ return Err(format!("T1 reconciliation formula changed for {fact_id}"));
+ }
+ require_value(&cells, &format!("B{row}"), &fact.value)?;
+ require_text(&cells, &format!("C{row}"), "derived")?;
+ require_text(&cells, &format!("D{row}"), &derivation.algorithm_id)?;
+ require_text(&cells, &format!("E{row}"), &derivation.operands.join(","))?;
+ require_text(&cells, &format!("F{row}"), &canonical_hash(fact)?)?;
+ }
+ Ok(())
+}
+
+fn completion_evidence(
+ source_manifest: &T1SourceManifest,
+ provenance: &T1ProvenanceManifest,
+ artifact: &T1ReconciliationArtifactReceipt,
+) -> Result, String> {
+ Ok(vec![
+ ToolEvidence {
+ kind: T1_SOURCE_MANIFEST_EVIDENCE_KIND.to_string(),
+ reference: format!("evidence:t1-source-manifest:{}", canonical_hash(source_manifest)?),
+ summary: "Exact T1 source paths, sizes, and SHA-256 identities were re-read inside the authorized workspace.".to_string(),
+ },
+ ToolEvidence {
+ kind: T1_PROVENANCE_EVIDENCE_KIND.to_string(),
+ reference: format!("evidence:t1-provenance:{}", canonical_hash(provenance)?),
+ summary: "Every source and derived T1 fact was traced and independently reconciled without a numeric conflict.".to_string(),
+ },
+ ToolEvidence {
+ kind: T1_RECONCILIATION_EVIDENCE_KIND.to_string(),
+ reference: artifact.artifact_id.clone(),
+ summary: format!("Formula-backed XLSX re-read passed with artifact SHA-256 {}.", artifact.sha256),
+ },
+ ])
+}
+
+fn key_figures(provenance: &T1ProvenanceManifest) -> Result, String> {
+ let facts = provenance
+ .facts
+ .iter()
+ .map(|fact| (fact.fact_id.as_str(), fact.value.as_str()))
+ .collect::>();
+ [
+ "period",
+ "total_revenue_cny",
+ "budget_variance_cny",
+ "budget_variance_rate",
+ "prior_variance_cny",
+ "prior_variance_rate",
+ "occupancy_rate",
+ "occupancy_variance_percentage_points",
+ ]
+ .into_iter()
+ .map(|fact_id| {
+ facts
+ .get(fact_id)
+ .map(|value| (fact_id.to_string(), (*value).to_string()))
+ .ok_or_else(|| format!("T1 key figure {fact_id} is missing"))
+ })
+ .collect()
+}
+
+#[derive(Clone, Debug, Default)]
+struct WorksheetCell {
+ value: String,
+ formula: Option,
+ inline_text: String,
+}
+
+fn parse_worksheet_cells(xml: &str) -> Result, String> {
+ validate_xml(xml.as_bytes())?;
+ let mut reader = Reader::from_str(xml);
+ reader.config_mut().trim_text(false);
+ let mut cells = BTreeMap::new();
+ let mut current_ref = None;
+ let mut current = WorksheetCell::default();
+ let mut capture = None::;
+ loop {
+ match reader.read_event() {
+ Ok(Event::Start(event)) if event.local_name().as_ref() == b"c" => {
+ current_ref = Some(required_attribute(&event, b"r")?);
+ current = WorksheetCell::default();
+ }
+ Ok(Event::Start(event)) if event.local_name().as_ref() == b"v" => capture = Some(b'v'),
+ Ok(Event::Start(event)) if event.local_name().as_ref() == b"f" => capture = Some(b'f'),
+ Ok(Event::Start(event)) if event.local_name().as_ref() == b"t" => capture = Some(b't'),
+ Ok(Event::Text(text)) => {
+ let decoded = decode_text(&text)?;
+ match capture {
+ Some(b'v') => current.value.push_str(&decoded),
+ Some(b'f') => current
+ .formula
+ .get_or_insert_with(String::new)
+ .push_str(&decoded),
+ Some(b't') => current.inline_text.push_str(&decoded),
+ _ => {}
+ }
+ }
+ Ok(Event::End(event)) if matches!(event.local_name().as_ref(), b"v" | b"f" | b"t") => {
+ capture = None
+ }
+ Ok(Event::End(event)) if event.local_name().as_ref() == b"c" => {
+ let reference = current_ref
+ .take()
+ .ok_or_else(|| "T1 worksheet cell reference is missing".to_string())?;
+ if cells.insert(reference, current.clone()).is_some() {
+ return Err("T1 worksheet contains a duplicate cell".to_string());
+ }
+ }
+ Ok(Event::Eof) => break,
+ Ok(_) => {}
+ Err(_) => return Err("T1 worksheet XML is invalid".to_string()),
+ }
+ }
+ Ok(cells)
+}
+
+fn xml_paragraphs(xml: &str, paragraph_name: &[u8]) -> Result, String> {
+ validate_xml(xml.as_bytes())?;
+ let mut reader = Reader::from_str(xml);
+ let mut paragraphs = Vec::new();
+ let mut in_paragraph = false;
+ let mut current = String::new();
+ loop {
+ match reader.read_event() {
+ Ok(Event::Start(event)) if event.local_name().as_ref() == paragraph_name => {
+ in_paragraph = true;
+ current.clear();
+ }
+ Ok(Event::Text(text)) if in_paragraph => current.push_str(&decode_text(&text)?),
+ Ok(Event::End(event)) if event.local_name().as_ref() == paragraph_name => {
+ in_paragraph = false;
+ paragraphs.push(current.clone());
+ }
+ Ok(Event::Eof) => break,
+ Ok(_) => {}
+ Err(_) => return Err("T1 document XML is invalid".to_string()),
+ }
+ }
+ Ok(paragraphs)
+}
+
+fn decode_text(text: &quick_xml::events::BytesText<'_>) -> Result {
+ let decoded = text
+ .decode()
+ .map_err(|_| "T1 XML text encoding is invalid".to_string())?;
+ quick_xml::escape::unescape(&decoded)
+ .map(|value| value.into_owned())
+ .map_err(|_| "T1 XML text escaping is invalid".to_string())
+}
+
+fn required_attribute(
+ event: &quick_xml::events::BytesStart<'_>,
+ key: &[u8],
+) -> Result {
+ for attribute in event.attributes().with_checks(true) {
+ let attribute = attribute.map_err(|_| "T1 XML attribute is invalid".to_string())?;
+ if attribute.key.local_name().as_ref() == key {
+ return std::str::from_utf8(attribute.value.as_ref())
+ .map(str::to_string)
+ .map_err(|_| "T1 XML attribute encoding is invalid".to_string());
+ }
+ }
+ Err("T1 XML required attribute is missing".to_string())
+}
+
+struct OpcPackage {
+ parts: BTreeMap>,
+}
+
+impl OpcPackage {
+ fn read(bytes: &[u8]) -> Result {
+ if bytes.is_empty() || bytes.len() as u64 > MAX_OPC_BYTES {
+ return Err("T1 OPC package size is invalid".to_string());
+ }
+ let mut archive = ZipArchive::new(Cursor::new(bytes))
+ .map_err(|_| "T1 OPC package cannot be opened".to_string())?;
+ if archive.is_empty() || archive.len() > MAX_OPC_PARTS {
+ return Err("T1 OPC part count is invalid".to_string());
+ }
+ let mut parts = BTreeMap::new();
+ let mut expanded = 0_u64;
+ for index in 0..archive.len() {
+ let mut file = archive
+ .by_index(index)
+ .map_err(|_| "T1 OPC part cannot be opened".to_string())?;
+ if file.is_dir() {
+ return Err("T1 OPC package contains a directory entry".to_string());
+ }
+ let name = file.name().replace('\\', "/");
+ validated_relative_path(&name, "T1 OPC part path")?;
+ expanded = expanded
+ .checked_add(file.size())
+ .ok_or_else(|| "T1 OPC expanded size overflow".to_string())?;
+ if expanded > MAX_OPC_BYTES {
+ return Err("T1 OPC expanded size is invalid".to_string());
+ }
+ let mut part = Vec::new();
+ file.read_to_end(&mut part)
+ .map_err(|_| "T1 OPC part cannot be read".to_string())?;
+ if (name.ends_with(".xml") || name.ends_with(".rels")) && validate_xml(&part).is_err() {
+ return Err("T1 OPC XML part is invalid".to_string());
+ }
+ if parts.insert(name, part).is_some() {
+ return Err("T1 OPC package contains a duplicate part".to_string());
+ }
+ }
+ let package = Self { parts };
+ package.required("[Content_Types].xml")?;
+ package.required("_rels/.rels")?;
+ package.reject_external_relationships()?;
+ Ok(package)
+ }
+
+ fn required(&self, name: &str) -> Result<&[u8], String> {
+ self.parts
+ .get(name)
+ .map(Vec::as_slice)
+ .ok_or_else(|| "T1 OPC package is missing a required part".to_string())
+ }
+
+ fn text(&self, name: &str) -> Result<&str, String> {
+ std::str::from_utf8(self.required(name)?)
+ .map_err(|_| "T1 OPC text part is invalid UTF-8".to_string())
+ }
+
+ fn validate_main(&self, main_part: &str, content_type: &str) -> Result<(), String> {
+ self.validate_content_type(main_part, content_type)?;
+ self.validate_relationship("_rels/.rels", "/officeDocument", main_part, 1)?;
+ self.required(main_part)?;
+ Ok(())
+ }
+
+ fn validate_content_type(&self, part: &str, content_type: &str) -> Result<(), String> {
+ let mut reader = Reader::from_str(self.text("[Content_Types].xml")?);
+ let mut matches = 0;
+ loop {
+ match reader.read_event() {
+ Ok(Event::Start(event)) | Ok(Event::Empty(event))
+ if event
+ .local_name()
+ .as_ref()
+ .eq_ignore_ascii_case(b"override") =>
+ {
+ let attributes = xml_attributes(&event)?;
+ if attributes.get("partname").map(String::as_str)
+ == Some(format!("/{part}").as_str())
+ && attributes.get("contenttype").map(String::as_str) == Some(content_type)
+ {
+ matches += 1;
+ }
+ }
+ Ok(Event::Eof) => break,
+ Ok(_) => {}
+ Err(_) => return Err("T1 OPC content types are invalid".to_string()),
+ }
+ }
+ if matches != 1 {
+ return Err("T1 OPC content type binding is invalid".to_string());
+ }
+ Ok(())
+ }
+
+ fn validate_relationship(
+ &self,
+ part: &str,
+ type_suffix: &str,
+ target: &str,
+ expected_count: usize,
+ ) -> Result<(), String> {
+ let mut reader = Reader::from_str(self.text(part)?);
+ let mut matches = 0;
+ loop {
+ match reader.read_event() {
+ Ok(Event::Start(event)) | Ok(Event::Empty(event))
+ if event
+ .local_name()
+ .as_ref()
+ .eq_ignore_ascii_case(b"relationship") =>
+ {
+ let attributes = xml_attributes(&event)?;
+ if attributes
+ .get("targetmode")
+ .is_some_and(|mode| mode.eq_ignore_ascii_case("external"))
+ {
+ return Err("T1 OPC external relationship is blocked".to_string());
+ }
+ if attributes
+ .get("type")
+ .is_some_and(|value| value.ends_with(type_suffix))
+ && attributes.get("target").map(String::as_str) == Some(target)
+ {
+ matches += 1;
+ }
+ }
+ Ok(Event::Eof) => break,
+ Ok(_) => {}
+ Err(_) => return Err("T1 OPC relationships are invalid".to_string()),
+ }
+ }
+ if matches != expected_count {
+ return Err("T1 OPC relationship binding is invalid".to_string());
+ }
+ Ok(())
+ }
+
+ fn reject_external_relationships(&self) -> Result<(), String> {
+ for (name, bytes) in &self.parts {
+ if name.ends_with(".rels") {
+ let lower = String::from_utf8_lossy(bytes).to_ascii_lowercase();
+ if lower.contains("targetmode=\"external\"")
+ || lower.contains("target=\"file:")
+ || lower.contains("target=\"http:")
+ || lower.contains("target=\"https:")
+ || lower.contains("target=\"\\\\")
+ {
+ return Err("T1 OPC external relationship is blocked".to_string());
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+fn xml_attributes(
+ event: &quick_xml::events::BytesStart<'_>,
+) -> Result, String> {
+ let mut attributes = BTreeMap::new();
+ for attribute in event.attributes().with_checks(true) {
+ let attribute = attribute.map_err(|_| "T1 XML attribute is invalid".to_string())?;
+ if attribute.value.contains(&b'&') {
+ return Err("T1 XML attribute escaping is unsupported".to_string());
+ }
+ let key = std::str::from_utf8(attribute.key.local_name().as_ref())
+ .map_err(|_| "T1 XML attribute name is invalid".to_string())?
+ .to_ascii_lowercase();
+ let value = std::str::from_utf8(attribute.value.as_ref())
+ .map_err(|_| "T1 XML attribute value is invalid".to_string())?
+ .to_string();
+ if attributes.insert(key, value).is_some() {
+ return Err("T1 XML attribute is duplicated".to_string());
+ }
+ }
+ Ok(attributes)
+}
+
+fn validate_xml(bytes: &[u8]) -> Result<(), String> {
+ let mut reader = Reader::from_reader(bytes);
+ reader.config_mut().trim_text(false);
+ loop {
+ match reader.read_event() {
+ Ok(Event::DocType(_)) | Ok(Event::PI(_)) => {
+ return Err("T1 XML declaration is unsafe".to_string())
+ }
+ Ok(Event::Eof) => break,
+ Ok(_) => {}
+ Err(_) => return Err("T1 XML is malformed".to_string()),
+ }
+ }
+ Ok(())
+}
+
+fn cell_text(cells: &BTreeMap, reference: &str) -> Result {
+ let cell = cells
+ .get(reference)
+ .ok_or_else(|| "T1 worksheet required cell is missing".to_string())?;
+ if !cell.inline_text.is_empty() {
+ Ok(cell.inline_text.clone())
+ } else if !cell.value.is_empty() {
+ Ok(cell.value.clone())
+ } else {
+ Err("T1 worksheet required cell is empty".to_string())
+ }
+}
+
+fn require_text(
+ cells: &BTreeMap,
+ reference: &str,
+ expected: &str,
+) -> Result<(), String> {
+ if cell_text(cells, reference)? != expected {
+ return Err(format!("T1 worksheet text cell {reference} is incorrect"));
+ }
+ Ok(())
+}
+
+fn require_value(
+ cells: &BTreeMap,
+ reference: &str,
+ expected: &str,
+) -> Result<(), String> {
+ let actual = cell_text(cells, reference)?;
+ match (actual.parse::(), expected.parse::()) {
+ (Ok(actual), Ok(expected)) if (actual - expected).abs() <= NUMERIC_TOLERANCE => Ok(()),
+ (Err(_), Err(_)) if actual == expected => Ok(()),
+ _ => Err(format!("T1 worksheet value cell {reference} is incorrect")),
+ }
+}
+
+fn fact_text(facts: &BTreeMap, key: &str) -> Result {
+ facts
+ .get(key)
+ .map(|value| value.0.clone())
+ .ok_or_else(|| format!("T1 source value {key} is missing"))
+}
+
+fn fact_number(facts: &BTreeMap, key: &str) -> Result {
+ parse_number(&fact_text(facts, key)?)
+}
+
+fn provenance_number(
+ facts: &BTreeMap<&str, &T1FactProvenance>,
+ fact_id: &str,
+) -> Result {
+ parse_number(
+ &facts
+ .get(fact_id)
+ .ok_or_else(|| format!("T1 fact {fact_id} is missing"))?
+ .value,
+ )
+}
+
+fn parse_number(value: &str) -> Result {
+ let value = value
+ .parse::()
+ .map_err(|_| "T1 numeric fact is invalid".to_string())?;
+ if !value.is_finite() {
+ return Err("T1 numeric fact is not finite".to_string());
+ }
+ Ok(value)
+}
+
+fn canonical_number(value: f64) -> String {
+ if value.fract().abs() < 0.000_000_1 {
+ format!("{value:.0}")
+ } else {
+ format!("{value:.6}")
+ }
+}
+
+fn value_cell(reference: &str, value: &str, formula: Option<&str>) -> String {
+ if let Some(formula) = formula {
+ format!(
+ "{} {} ",
+ xml_escape(formula),
+ xml_escape(value)
+ )
+ } else if value.parse::().is_ok() {
+ format!("{} ", xml_escape(value))
+ } else {
+ inline_cell(reference, value)
+ }
+}
+
+fn inline_cell(reference: &str, value: &str) -> String {
+ format!(
+ "{} ",
+ xml_escape(value)
+ )
+}
+
+fn root_relationships(target: &str) -> String {
+ format!(" ")
+}
+
+fn write_zip(parts: BTreeMap>) -> Result, String> {
+ let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
+ let options = FileOptions::default()
+ .compression_method(zip::CompressionMethod::Deflated)
+ .unix_permissions(0o644);
+ for (path, bytes) in parts {
+ zip.start_file(&path, options)
+ .map_err(|error| format!("T1 zip part {path} could not be started: {error}"))?;
+ zip.write_all(&bytes)
+ .map_err(|error| format!("T1 zip part {path} could not be written: {error}"))?;
+ }
+ zip.finish()
+ .map(|cursor| cursor.into_inner())
+ .map_err(|error| format!("T1 zip package could not be finished: {error}"))
+}
+
+fn xml_escape(value: &str) -> String {
+ value
+ .replace('&', "&")
+ .replace('<', "<")
+ .replace('>', ">")
+ .replace('"', """)
+ .replace('\'', "'")
+}
+
+fn sha256(bytes: &[u8]) -> String {
+ hex::encode(Sha256::digest(bytes))
+}
+
+fn canonical_hash(value: &T) -> Result {
+ serde_json::to_vec(value)
+ .map(|bytes| sha256(&bytes))
+ .map_err(|error| format!("T1 receipt could not be serialized: {error}"))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::kernel::benchmark::t1::fixtures::generate_fixture_set;
+ use crate::kernel::models::AccessMode;
+ use crate::kernel::tool_runtime::{
+ prepare_tool_execution, ToolExecutionRequest, ToolExecutionStatus, ToolInvocationRecord,
+ };
+
+ fn fixture_workspace() -> (tempfile::TempDir, T1ReconciliationRequest) {
+ let workspace = tempfile::tempdir().expect("workspace");
+ let fixtures = generate_fixture_set().expect("fixtures");
+ for fixture in fixtures.files {
+ let path = workspace.path().join(fixture.relative_path);
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
+ fs::write(path, fixture.bytes).unwrap();
+ }
+ fs::create_dir(workspace.path().join("outputs")).unwrap();
+ (
+ workspace,
+ T1ReconciliationRequest {
+ source_directory: "inputs".to_string(),
+ output_relative_path: "outputs/t1-reconciliation.xlsx".to_string(),
+ },
+ )
+ }
+
+ #[test]
+ fn reconciliation_binds_sources_key_numbers_artifact_and_completion_evidence() {
+ let (workspace, request) = fixture_workspace();
+ let outcome = run_t1_reconciliation(workspace.path(), &request).expect("T1 reconciles");
+ assert_eq!(outcome.source_manifest.entries.len(), 3);
+ assert_eq!(outcome.provenance.facts.len(), 27);
+ assert_eq!(outcome.key_figures.len(), 8);
+ assert_eq!(outcome.completion_evidence.len(), 3);
+ assert_eq!(
+ outcome.completion_evidence[2].reference,
+ T1_RECONCILIATION_ARTIFACT_ID
+ );
+ assert!(workspace
+ .path()
+ .join(&outcome.artifact.relative_path)
+ .is_file());
+ assert_eq!(
+ verify_persisted_t1_reconciliation(
+ workspace.path(),
+ &request,
+ &outcome.source_manifest,
+ &outcome.provenance,
+ &outcome.artifact,
+ )
+ .unwrap(),
+ outcome.completion_evidence
+ );
+ }
+
+ #[test]
+ fn tool_executor_returns_contract_validated_kernel_completion_evidence() {
+ let (workspace, request) = fixture_workspace();
+ let plan = prepare_tool_execution(&ToolExecutionRequest {
+ tool_id: T1_RECONCILIATION_TOOL_ID.to_string(),
+ input: serde_json::to_value(request).unwrap(),
+ access_mode: AccessMode::FullAccess,
+ run_id: Some(Uuid::new_v4()),
+ })
+ .unwrap();
+ let executor = T1ReconciliationAgentToolExecutor::new(workspace.path());
+ let output = executor.execute(&plan).unwrap();
+ let invocation = ToolInvocationRecord::succeeded(
+ &plan,
+ output.output,
+ output.evidence,
+ output.verification,
+ None,
+ 1,
+ )
+ .unwrap();
+
+ assert_eq!(invocation.status, ToolExecutionStatus::Succeeded);
+ assert_eq!(invocation.evidence.len(), 3);
+ assert!(invocation.verification.passed);
+ }
+
+ #[test]
+ fn reconciliation_detects_injected_numeric_conflict_before_writing() {
+ let (workspace, request) = fixture_workspace();
+ let path = workspace.path().join("inputs/01-monthly-revenue.xlsx");
+ mutate_zip_part(&path, "xl/worksheets/sheet1.xml", |xml| {
+ xml.replace(
+ "1702400 ",
+ "1702401 ",
+ )
+ });
+ let error = run_t1_reconciliation(workspace.path(), &request).unwrap_err();
+ assert!(error.contains("numeric conflict"));
+ assert!(!workspace
+ .path()
+ .join(&request.output_relative_path)
+ .exists());
+ }
+
+ #[test]
+ fn reconciliation_rejects_damaged_formula_without_completion_evidence() {
+ let (workspace, request) = fixture_workspace();
+ let outcome = run_t1_reconciliation(workspace.path(), &request).unwrap();
+ let path = workspace.path().join(&outcome.artifact.relative_path);
+ let mut bytes = fs::read(path).unwrap();
+ bytes = mutate_zip_bytes(bytes, "xl/worksheets/sheet1.xml", |xml| {
+ xml.replace("SUM(B6,B8,B9) ", "SUM(B6,B8) ")
+ });
+ let altered = T1ReconciliationArtifactReceipt {
+ bytes: bytes.len() as u64,
+ sha256: sha256(&bytes),
+ ..outcome.artifact.clone()
+ };
+ let error = verify_t1_reconciliation_artifact(
+ &outcome.source_manifest,
+ &outcome.provenance,
+ &altered,
+ &bytes,
+ )
+ .unwrap_err();
+ assert!(error.contains("formula changed"));
+ }
+
+ #[test]
+ fn reconciliation_rejects_path_escape_and_artifact_identity_drift() {
+ let (workspace, mut request) = fixture_workspace();
+ request.output_relative_path = "../outside.xlsx".to_string();
+ assert!(run_t1_reconciliation(workspace.path(), &request)
+ .unwrap_err()
+ .contains("authorized workspace"));
+
+ request.output_relative_path = "missing/t1-reconciliation.xlsx".to_string();
+ assert!(run_t1_reconciliation(workspace.path(), &request)
+ .unwrap_err()
+ .contains("output parent is unavailable"));
+ assert!(!workspace.path().join("missing").exists());
+
+ request.output_relative_path = "outputs/t1-reconciliation.xlsx".to_string();
+ let outcome = run_t1_reconciliation(workspace.path(), &request).unwrap();
+ let mut drifted = outcome.artifact.clone();
+ drifted.sha256 = "0".repeat(64);
+ assert!(verify_persisted_t1_reconciliation(
+ workspace.path(),
+ &request,
+ &outcome.source_manifest,
+ &outcome.provenance,
+ &drifted,
+ )
+ .unwrap_err()
+ .contains("completion receipt"));
+ }
+
+ fn mutate_zip_part(path: &Path, part: &str, mutate: impl FnOnce(&str) -> String) {
+ let bytes = fs::read(path).unwrap();
+ fs::write(path, mutate_zip_bytes(bytes, part, mutate)).unwrap();
+ }
+
+ fn mutate_zip_bytes(
+ bytes: Vec,
+ part: &str,
+ mutate: impl FnOnce(&str) -> String,
+ ) -> Vec {
+ let mut archive = ZipArchive::new(Cursor::new(bytes)).unwrap();
+ let mut parts = BTreeMap::new();
+ for index in 0..archive.len() {
+ let mut file = archive.by_index(index).unwrap();
+ let mut content = Vec::new();
+ file.read_to_end(&mut content).unwrap();
+ parts.insert(file.name().to_string(), content);
+ }
+ let text = String::from_utf8(parts.remove(part).unwrap()).unwrap();
+ parts.insert(part.to_string(), mutate(&text).into_bytes());
+ write_zip(parts).unwrap()
+ }
+}
diff --git a/apps/desktop/src-tauri/src/kernel/tool_runtime.rs b/apps/desktop/src-tauri/src/kernel/tool_runtime.rs
index cc38b2d..83adda9 100644
--- a/apps/desktop/src-tauri/src/kernel/tool_runtime.rs
+++ b/apps/desktop/src-tauri/src/kernel/tool_runtime.rs
@@ -24,6 +24,7 @@ pub const OFFICE_CREATE_TOOL_ID: &str = "office.create";
pub const OFFICE_OPEN_TOOL_ID: &str = "office.open";
pub const OFFICE_UPDATE_TOOL_ID: &str = "office.update";
pub const OPERATIONS_BRIEFING_TOOL_ID: &str = "operations.briefing";
+pub const T1_RECONCILIATION_TOOL_ID: &str = "operations.reconcile_excel";
pub const SKILL_ACTIVATE_TOOL_ID: &str = "skill.activate";
pub const TERMINAL_READ_TOOL_ID: &str = "terminal.read";
@@ -758,6 +759,95 @@ pub fn builtin_tool_catalog() -> Vec {
"Choose an existing unprotected managed artifact, review local permission, and retry one update."
.to_string(),
},
+ ToolContract {
+ id: T1_RECONCILIATION_TOOL_ID.to_string(),
+ version: "1.0.0".to_string(),
+ title: "Reconcile verified T1 sources into Excel".to_string(),
+ description:
+ "Scan one authorized workspace folder for the exact T1 XLSX, DOCX, and PDF source set; reject period, numeric, formula, path, or identity conflicts; and create one re-read formula-backed Excel reconciliation artifact."
+ .to_string(),
+ capability: CapabilityKind::FileWrite,
+ risk_level: RiskLevel::High,
+ executor_id: "kernel.operations.reconcile_excel.v1".to_string(),
+ input_schema: object_schema(
+ vec![
+ field(
+ "source_directory",
+ ToolValueType::String,
+ "Workspace-relative directory containing exactly one XLSX, DOCX, and PDF source.",
+ ),
+ field(
+ "output_relative_path",
+ ToolValueType::String,
+ "Workspace-relative new XLSX output path inside an existing authorized directory.",
+ ),
+ ],
+ &["source_directory", "output_relative_path"],
+ ),
+ output_schema: object_schema(
+ vec![
+ field(
+ "source_manifest",
+ ToolValueType::Object,
+ "Exact source paths, byte counts, media types, and SHA-256 identities.",
+ ),
+ field(
+ "provenance",
+ ToolValueType::Object,
+ "Source and derived fact provenance with independent reconciliation.",
+ ),
+ field(
+ "artifact",
+ ToolValueType::Object,
+ "Verified XLSX artifact identity receipt.",
+ ),
+ field(
+ "key_figures",
+ ToolValueType::Object,
+ "Reconciled key-number projection.",
+ ),
+ field(
+ "completion_evidence",
+ ToolValueType::Array,
+ "Kernel-issued evidence created only after persisted re-read verification.",
+ ),
+ ],
+ &[
+ "source_manifest",
+ "provenance",
+ "artifact",
+ "key_figures",
+ "completion_evidence",
+ ],
+ ),
+ constraints: ToolConstraints {
+ allowed_network_hosts: Vec::new(),
+ path_scope: ToolPathScope::Workspace,
+ mutates_machine_state: true,
+ protected_path_policy:
+ "exact workspace-relative source directory and new XLSX output; no overwrite, link traversal, protected path, or boundary escape"
+ .to_string(),
+ resource: Some(ToolResourceRequirement {
+ key: "local_filesystem://mutation".to_string(),
+ access: ToolResourceAccess::Write,
+ lease_seconds: 30 * 60,
+ }),
+ },
+ verification: ToolVerificationContract {
+ recipe_id: "operations.reconcile_excel.t1.v1".to_string(),
+ description:
+ "Require exact source identity, per-fact provenance, independent numeric reconciliation, formula verification, persisted artifact identity, and post-write re-read evidence."
+ .to_string(),
+ required_evidence_kinds: vec![
+ "t1_source_manifest".to_string(),
+ "t1_fact_provenance".to_string(),
+ "t1_reconciliation_xlsx".to_string(),
+ ],
+ },
+ recovery_hint:
+ "Restore the exact three-source T1 set or choose a new authorized XLSX path, then retry one reconciliation without overwriting an existing artifact."
+ .to_string(),
+ },
ToolContract {
id: OPERATIONS_BRIEFING_TOOL_ID.to_string(),
version: "1.0.0".to_string(),
@@ -1631,6 +1721,30 @@ fn validate_tool_semantics(contract: &ToolContract, input: &Value) -> Result<(),
}
}
}
+ if contract.id == T1_RECONCILIATION_TOOL_ID {
+ for field in ["source_directory", "output_relative_path"] {
+ let value = input
+ .get(field)
+ .and_then(Value::as_str)
+ .map(str::trim)
+ .unwrap_or_default();
+ if value.is_empty() {
+ return Err(format!(
+ "operations.reconcile_excel input field `{field}` cannot be blank"
+ ));
+ }
+ }
+ if !input["output_relative_path"]
+ .as_str()
+ .unwrap_or_default()
+ .to_ascii_lowercase()
+ .ends_with(".xlsx")
+ {
+ return Err(
+ "operations.reconcile_excel output_relative_path must end in .xlsx".to_string(),
+ );
+ }
+ }
if contract.id == FILESYSTEM_MUTATE_TOOL_ID {
validate_filesystem_mutation_semantics(input)?;
}
@@ -1987,7 +2101,7 @@ mod tests {
BROWSER_OPEN_TOOL_ID, CONNECTOR_ATTACHMENT_DOWNLOAD_TOOL_ID, CONNECTOR_MUTATE_TOOL_ID,
FILESYSTEM_MUTATE_TOOL_ID, FILE_READ_TOOL_ID, FILE_WRITE_TOOL_ID, OFFICE_CREATE_TOOL_ID,
OFFICE_OPEN_TOOL_ID, OFFICE_UPDATE_TOOL_ID, OPERATIONS_BRIEFING_TOOL_ID,
- SKILL_ACTIVATE_TOOL_ID, TERMINAL_READ_TOOL_ID,
+ SKILL_ACTIVATE_TOOL_ID, T1_RECONCILIATION_TOOL_ID, TERMINAL_READ_TOOL_ID,
};
use crate::kernel::models::AccessMode;
use crate::kernel::policy::{CapabilityKind, PolicyDecision, RiskLevel};
@@ -2136,6 +2250,41 @@ mod tests {
.contains(&"office_artifact_update".to_string()));
}
+ #[test]
+ fn builtin_catalog_declares_t1_reconciliation_as_verified_workspace_write() {
+ let contract = builtin_tool_catalog()
+ .into_iter()
+ .find(|contract| contract.id == T1_RECONCILIATION_TOOL_ID)
+ .expect("T1 reconciliation contract");
+
+ assert_eq!(contract.version, "1.0.0");
+ assert_eq!(contract.capability, CapabilityKind::FileWrite);
+ assert_eq!(contract.risk_level, RiskLevel::High);
+ assert_eq!(contract.constraints.path_scope, ToolPathScope::Workspace);
+ assert!(contract.constraints.mutates_machine_state);
+ let resource = contract.constraints.resource.expect("write resource");
+ assert_eq!(resource.key, "local_filesystem://mutation");
+ assert_eq!(resource.access, ToolResourceAccess::Write);
+ assert_eq!(
+ contract.verification.required_evidence_kinds,
+ [
+ "t1_source_manifest",
+ "t1_fact_provenance",
+ "t1_reconciliation_xlsx",
+ ]
+ );
+ assert!(prepare_tool_execution(&ToolExecutionRequest {
+ tool_id: T1_RECONCILIATION_TOOL_ID.to_string(),
+ input: json!({
+ "source_directory": "inputs",
+ "output_relative_path": "outputs/reconciliation.txt",
+ }),
+ access_mode: AccessMode::FullAccess,
+ run_id: None,
+ })
+ .is_err());
+ }
+
#[test]
fn builtin_catalog_declares_office_open_as_verified_foreground_launch() {
let contract = builtin_tool_catalog()
From 0c61febe2340d63668344896cc536ac8c66c0fb9 Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 22 Jul 2026 22:22:43 +0800
Subject: [PATCH 2/6] feat: verify T1 PowerPoint artifacts locally
---
apps/desktop/src-tauri/src/commands.rs | 45 +-
apps/desktop/src-tauri/src/kernel/mod.rs | 1 +
.../src-tauri/src/kernel/t1_powerpoint.rs | 1078 +++++++++++++++++
.../src-tauri/src/kernel/t1_reconciliation.rs | 27 +
.../src-tauri/src/kernel/tool_runtime.rs | 175 ++-
5 files changed, 1316 insertions(+), 10 deletions(-)
create mode 100644 apps/desktop/src-tauri/src/kernel/t1_powerpoint.rs
diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs
index 2f46fc1..2801a85 100644
--- a/apps/desktop/src-tauri/src/commands.rs
+++ b/apps/desktop/src-tauri/src/commands.rs
@@ -162,6 +162,7 @@ use crate::kernel::skill_source::{
use crate::kernel::soul::{
AgentSoulProfileUpdateAudit, AgentSoulProfileUpdateProposal, AgentSoulProfileUpdateReceipt,
};
+use crate::kernel::t1_powerpoint::{LocalT1PowerPointRenderer, T1PowerPointAgentToolExecutor};
use crate::kernel::t1_reconciliation::T1ReconciliationAgentToolExecutor;
use crate::kernel::task_capability_manifest::{
TaskCapabilityManifestContext, TaskCapabilityProposal,
@@ -178,7 +179,8 @@ use crate::kernel::tool_runtime::{
COMPUTER_CONTROL_TOOL_ID, COMPUTER_SCREENSHOT_TOOL_ID, CONNECTOR_ATTACHMENT_DOWNLOAD_TOOL_ID,
FILESYSTEM_MUTATE_TOOL_ID, FILE_READ_TOOL_ID, FILE_WRITE_TOOL_ID, NETWORK_SEARCH_TOOL_ID,
OFFICE_CREATE_TOOL_ID, OFFICE_OPEN_TOOL_ID, OFFICE_UPDATE_TOOL_ID, OPERATIONS_BRIEFING_TOOL_ID,
- SKILL_ACTIVATE_TOOL_ID, T1_RECONCILIATION_TOOL_ID, TERMINAL_READ_TOOL_ID,
+ SKILL_ACTIVATE_TOOL_ID, T1_POWERPOINT_TOOL_ID, T1_RECONCILIATION_TOOL_ID,
+ TERMINAL_READ_TOOL_ID,
};
use crate::kernel::tool_strategy::{
model_driven_tool_strategy_for_current_platform, ModelDrivenToolStrategy,
@@ -2670,6 +2672,21 @@ fn validate_agent_tool_local_constraints(plan: &ToolExecutionPlan) -> Result<(),
})?;
enforce_workspace_relative_mutation_path(output_relative_path)?;
}
+ if plan.contract.id == T1_POWERPOINT_TOOL_ID {
+ let source_directory =
+ plan.request.input["source_directory"]
+ .as_str()
+ .ok_or_else(|| {
+ "operations.generate_powerpoint requires a source_directory string".to_string()
+ })?;
+ enforce_workspace_relative_read_path(source_directory)?;
+ let output_relative_path = plan.request.input["output_relative_path"]
+ .as_str()
+ .ok_or_else(|| {
+ "operations.generate_powerpoint requires an output_relative_path string".to_string()
+ })?;
+ enforce_workspace_relative_mutation_path(output_relative_path)?;
+ }
if plan.contract.id == COMPUTER_CONTROL_TOOL_ID {
let action = plan.request.input["action"]
.as_str()
@@ -4677,22 +4694,20 @@ fn agent_file_write_client(
})
}
-fn t1_reconciliation_workspace_root(
- directory_state: &LocalDirectoryState,
-) -> Result {
+fn t1_workspace_root(directory_state: &LocalDirectoryState) -> Result {
let settings = directory_state.settings.as_ref().ok_or_else(|| {
- "workspace is not configured; choose a DS Agent work root before reconciling T1 sources"
+ "workspace is not configured; choose a DS Agent work root before running T1 tools"
.to_string()
})?;
if directory_state.needs_setup {
return Err(
- "workspace setup is incomplete; choose a DS Agent work root before reconciling T1 sources"
+ "workspace setup is incomplete; choose a DS Agent work root before running T1 tools"
.to_string(),
);
}
let workspace_root = PathBuf::from(&settings.workspace_dir);
if !workspace_root.is_dir() {
- return Err("configured workspace is unavailable for T1 reconciliation".to_string());
+ return Err("configured workspace is unavailable for T1 tools".to_string());
}
Ok(workspace_root)
}
@@ -14105,11 +14120,14 @@ pub fn execute_agent_tool(
} else {
None
};
- let t1_workspace_root = if request.tool_id.trim() == T1_RECONCILIATION_TOOL_ID {
+ let t1_workspace_root = if matches!(
+ request.tool_id.trim(),
+ T1_RECONCILIATION_TOOL_ID | T1_POWERPOINT_TOOL_ID
+ ) {
let app_data_dir = app.resolved_app_data_dir()?;
let directory_state =
load_local_directory_state(&app_data_dir).map_err(event_store_error)?;
- Some(t1_reconciliation_workspace_root(&directory_state)?)
+ Some(t1_workspace_root(&directory_state)?)
} else {
None
};
@@ -14178,6 +14196,15 @@ pub fn execute_agent_tool(
.ok_or_else(|| "operations.reconcile_excel executor is unavailable".to_string())?,
);
run_authorized_agent_tool_execution(authorized, &executor)
+ } else if authorized.plan.contract.id == T1_POWERPOINT_TOOL_ID {
+ let renderer = LocalT1PowerPointRenderer;
+ let executor = T1PowerPointAgentToolExecutor::new(
+ t1_workspace_root.as_deref().ok_or_else(|| {
+ "operations.generate_powerpoint executor is unavailable".to_string()
+ })?,
+ &renderer,
+ );
+ run_authorized_agent_tool_execution(authorized, &executor)
} else if authorized.plan.contract.id == FILESYSTEM_MUTATE_TOOL_ID {
let client = LocalFileSystemMutationClient;
let executor = FileSystemMutationAgentToolExecutor {
diff --git a/apps/desktop/src-tauri/src/kernel/mod.rs b/apps/desktop/src-tauri/src/kernel/mod.rs
index 354b68d..47ed71b 100644
--- a/apps/desktop/src-tauri/src/kernel/mod.rs
+++ b/apps/desktop/src-tauri/src/kernel/mod.rs
@@ -32,6 +32,7 @@ pub mod sandbox;
pub mod skill;
pub mod skill_source;
pub mod soul;
+pub mod t1_powerpoint;
pub mod t1_reconciliation;
pub mod task_capability_manifest;
pub mod task_grouped_approval;
diff --git a/apps/desktop/src-tauri/src/kernel/t1_powerpoint.rs b/apps/desktop/src-tauri/src/kernel/t1_powerpoint.rs
new file mode 100644
index 0000000..17d3c6a
--- /dev/null
+++ b/apps/desktop/src-tauri/src/kernel/t1_powerpoint.rs
@@ -0,0 +1,1078 @@
+use std::collections::BTreeMap;
+use std::fs::{self, File, OpenOptions};
+use std::io::{Cursor, Read, Write};
+use std::path::{Component, Path, PathBuf};
+
+use chrono::Utc;
+use quick_xml::{events::Event, Reader};
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use uuid::Uuid;
+use zip::{write::FileOptions, ZipArchive};
+
+use super::artifact_render::render_artifact_file;
+use super::artifacts::{
+ preview_manifest_hash, ArtifactEngine, ArtifactFormat, ArtifactGenerationRequest,
+ ArtifactInput, ArtifactPhase, ArtifactTemplate, MAX_ARTIFACT_REVISIONS,
+};
+use super::office::{build_office_artifact, OfficeApp, OfficeCreateSpec, OfficeSlideSpec};
+use super::t1_reconciliation::{verify_existing_t1_reconciliation, T1ReconciliationOutcome};
+use super::tool_runtime::{
+ AgentToolExecutor, ToolEvidence, ToolExecutionOutput, ToolExecutionPlan,
+ ToolVerificationResult, T1_POWERPOINT_TOOL_ID,
+};
+
+pub const T1_POWERPOINT_ARTIFACT_ID: &str = "t1-monthly-brief-pptx";
+pub const T1_POWERPOINT_EVIDENCE_KIND: &str = "one_page_pptx";
+pub const T1_POWERPOINT_RENDER_EVIDENCE_KIND: &str = "actual_render_receipt";
+pub const T1_POWERPOINT_REVISION_EVIDENCE_KIND: &str = "office_revision_receipt";
+
+const ARTIFACT_RECEIPT_VERSION: &str = "ds-agent.t1-powerpoint-artifact/v1";
+const RENDER_RECEIPT_VERSION: &str = "ds-agent.t1-powerpoint-render/v1";
+const REVISION_RECEIPT_VERSION: &str = "ds-agent.t1-powerpoint-revision/v1";
+const MAX_PPTX_BYTES: usize = 16 * 1024 * 1024;
+const MAX_OPC_PARTS: usize = 128;
+const FIXED_CORE_TIMESTAMP: &str = "2000-01-01T00:00:00Z";
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1PowerPointRequest {
+ pub source_directory: String,
+ pub reconciliation: T1ReconciliationOutcome,
+ pub output_relative_path: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1PowerPointArtifactReceipt {
+ pub version: String,
+ pub artifact_id: String,
+ pub original_relative_path: String,
+ pub delivered_relative_path: String,
+ pub bytes: u64,
+ pub sha256: String,
+ pub artifact_revision: u32,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1PowerPointRenderReceipt {
+ pub version: String,
+ pub artifact_sha256: String,
+ pub renderer_version: String,
+ pub rendered_page_count: u32,
+ pub preview_manifest_sha256: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1PowerPointRevisionReceipt {
+ pub version: String,
+ pub original_relative_path: String,
+ pub delivered_relative_path: String,
+ pub revision_attempts: u32,
+ pub revision_paths: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct T1PowerPointOutcome {
+ pub reconciliation_artifact_sha256: String,
+ pub artifact: T1PowerPointArtifactReceipt,
+ pub render: T1PowerPointRenderReceipt,
+ pub revision: T1PowerPointRevisionReceipt,
+ pub key_figures: BTreeMap,
+ pub anomalies: BTreeMap,
+ pub completion_evidence: Vec,
+}
+
+pub struct T1PowerPointRender {
+ pub pages: Vec>,
+ pub renderer_version: String,
+}
+
+pub trait T1PowerPointRenderer {
+ fn render(&self, path: &Path) -> Result;
+}
+
+pub struct LocalT1PowerPointRenderer;
+
+impl T1PowerPointRenderer for LocalT1PowerPointRenderer {
+ fn render(&self, path: &Path) -> Result {
+ let render = render_artifact_file(ArtifactFormat::PowerPoint, path)?;
+ Ok(T1PowerPointRender {
+ pages: render.pages,
+ renderer_version: render.renderer_version.to_string(),
+ })
+ }
+}
+
+pub struct T1PowerPointAgentToolExecutor<'a> {
+ workspace_root: &'a Path,
+ renderer: &'a dyn T1PowerPointRenderer,
+}
+
+impl<'a> T1PowerPointAgentToolExecutor<'a> {
+ pub fn new(workspace_root: &'a Path, renderer: &'a dyn T1PowerPointRenderer) -> Self {
+ Self {
+ workspace_root,
+ renderer,
+ }
+ }
+}
+
+impl AgentToolExecutor for T1PowerPointAgentToolExecutor<'_> {
+ fn execute(&self, plan: &ToolExecutionPlan) -> Result {
+ if plan.contract.id != T1_POWERPOINT_TOOL_ID {
+ return Err(format!(
+ "T1 PowerPoint executor cannot execute `{}`",
+ plan.contract.id
+ ));
+ }
+ let request = serde_json::from_value::(plan.request.input.clone())
+ .map_err(|error| {
+ format!("operations.generate_powerpoint input could not be decoded: {error}")
+ })?;
+ let outcome = run_t1_powerpoint(self.workspace_root, &request, self.renderer)?;
+ let evidence = outcome.completion_evidence.clone();
+ let bytes = outcome.artifact.bytes;
+ let revisions = outcome.revision.revision_attempts;
+ Ok(ToolExecutionOutput {
+ output: serde_json::to_value(&outcome).map_err(|error| {
+ format!("T1 PowerPoint output could not be serialized: {error}")
+ })?,
+ evidence,
+ verification: ToolVerificationResult::passed(format!(
+ "operations.generate_powerpoint reverified the exact C4A receipt, persisted and locally rendered one PPTX page ({bytes} bytes), and completed after {revisions} bounded revision(s)"
+ )),
+ })
+ }
+}
+
+pub fn run_t1_powerpoint(
+ workspace_root: &Path,
+ request: &T1PowerPointRequest,
+ renderer: &dyn T1PowerPointRenderer,
+) -> Result {
+ let workspace = canonical_workspace(workspace_root)?;
+ let reconciliation = verify_existing_t1_reconciliation(
+ &workspace,
+ &request.source_directory,
+ &request.reconciliation,
+ )?;
+ let anomalies = anomaly_projection(&reconciliation)?;
+ let (original_path, original_relative_path) =
+ resolve_new_pptx(&workspace, &request.output_relative_path)?;
+ let template = ArtifactTemplate::new(
+ "t1-monthly-brief".to_string(),
+ 1,
+ "T1 verified monthly brief".to_string(),
+ vec![ArtifactFormat::PowerPoint],
+ "one-page-verified-office".to_string(),
+ );
+ let initial_spec = powerpoint_spec(&original_relative_path, &reconciliation, &anomalies, 0)?;
+ let generation = ArtifactEngine::generate_with_template(
+ &ArtifactGenerationRequest {
+ request_id: Uuid::new_v4(),
+ input: ArtifactInput::Office { spec: initial_spec },
+ template: template.reference.clone(),
+ approved_storage_ref: format!("artifact-storage:{T1_POWERPOINT_ARTIFACT_ID}"),
+ },
+ &template,
+ Utc::now(),
+ )?;
+ let mut record = generation.record;
+ let frozen_input_fingerprint = record.input_fingerprint.clone();
+ let mut bytes = canonicalize_pptx(generation.bytes)?;
+ record.artifact_hash = sha256(&bytes);
+ let mut delivered_path = original_path;
+ let mut delivered_relative_path = original_relative_path.clone();
+ let mut created = Vec::new();
+ let mut revision_paths = Vec::new();
+
+ let result = (|| {
+ write_new_artifact(&workspace, &delivered_path, &bytes)?;
+ created.push((delivered_path.clone(), sha256(&bytes)));
+
+ loop {
+ verify_powerpoint(
+ &bytes,
+ &reconciliation,
+ &anomalies,
+ &delivered_relative_path,
+ )?;
+ ArtifactEngine::check_structure(&mut record, &bytes, Utc::now())?;
+ let render = renderer
+ .render(&delivered_path)
+ .map_err(|error| format!("T1 local Office renderer failed: {error}"))?;
+ if render.pages.len() != 1 {
+ return Err(
+ "T1 PowerPoint actual renderer must return exactly one page".to_string()
+ );
+ }
+ let preview_hash = preview_manifest_hash(&render.pages);
+ match ArtifactEngine::check_actual_visual(
+ &mut record,
+ &render.pages,
+ &render.renderer_version,
+ format!("artifact-preview:t1-one-page:{preview_hash}"),
+ Utc::now(),
+ ) {
+ Ok(()) => {
+ record.complete(Utc::now())?;
+ if record.phase != ArtifactPhase::Completed {
+ return Err(
+ "T1 PowerPoint artifact did not reach completed state".to_string()
+ );
+ }
+ let persisted = read_bounded_file(&delivered_path, MAX_PPTX_BYTES)?;
+ if persisted != bytes || record.artifact_hash != sha256(&persisted) {
+ return Err(
+ "T1 PowerPoint bytes changed after local render verification"
+ .to_string(),
+ );
+ }
+ let artifact = T1PowerPointArtifactReceipt {
+ version: ARTIFACT_RECEIPT_VERSION.to_string(),
+ artifact_id: T1_POWERPOINT_ARTIFACT_ID.to_string(),
+ original_relative_path: original_relative_path.clone(),
+ delivered_relative_path: delivered_relative_path.clone(),
+ bytes: bytes.len() as u64,
+ sha256: record.artifact_hash.clone(),
+ artifact_revision: record.artifact_revision,
+ };
+ let render_receipt = T1PowerPointRenderReceipt {
+ version: RENDER_RECEIPT_VERSION.to_string(),
+ artifact_sha256: record.artifact_hash.clone(),
+ renderer_version: render.renderer_version,
+ rendered_page_count: 1,
+ preview_manifest_sha256: preview_hash,
+ };
+ let revision = T1PowerPointRevisionReceipt {
+ version: REVISION_RECEIPT_VERSION.to_string(),
+ original_relative_path: original_relative_path.clone(),
+ delivered_relative_path: delivered_relative_path.clone(),
+ revision_attempts: record.revision_attempts,
+ revision_paths: revision_paths.clone(),
+ };
+ let completion_evidence =
+ completion_evidence(&artifact, &render_receipt, &revision)?;
+ return Ok(T1PowerPointOutcome {
+ reconciliation_artifact_sha256: reconciliation.artifact.sha256.clone(),
+ artifact,
+ render: render_receipt,
+ revision,
+ key_figures: reconciliation.key_figures.clone(),
+ anomalies: anomalies.clone(),
+ completion_evidence,
+ });
+ }
+ Err(error) => {
+ if record.phase != ArtifactPhase::RevisionRequired {
+ return Err(format!(
+ "T1 PowerPoint actual visual verification failed: {error}"
+ ));
+ }
+ record.request_revision(Utc::now())?;
+ let attempt = record.revision_attempts;
+ if attempt > MAX_ARTIFACT_REVISIONS {
+ return Err("T1 PowerPoint revision limit was exceeded".to_string());
+ }
+ let (revision_path, revision_relative_path) =
+ revision_sibling(&workspace, &original_relative_path, attempt)?;
+ let spec = powerpoint_spec(
+ &revision_relative_path,
+ &reconciliation,
+ &anomalies,
+ attempt,
+ )?;
+ bytes = canonicalize_pptx(build_office_artifact(&spec)?)?;
+ record.replace_revision(
+ &bytes,
+ frozen_input_fingerprint.clone(),
+ Utc::now(),
+ )?;
+ write_new_artifact(&workspace, &revision_path, &bytes)?;
+ created.push((revision_path.clone(), sha256(&bytes)));
+ revision_paths.push(revision_relative_path.clone());
+ delivered_path = revision_path;
+ delivered_relative_path = revision_relative_path;
+ }
+ }
+ }
+ })();
+
+ if result.is_err() {
+ remove_created_if_unchanged(&created);
+ }
+ result
+}
+
+fn powerpoint_spec(
+ relative_path: &str,
+ reconciliation: &T1ReconciliationOutcome,
+ anomalies: &BTreeMap,
+ revision: u32,
+) -> Result {
+ let value = |key: &str| {
+ reconciliation
+ .key_figures
+ .get(key)
+ .map(String::as_str)
+ .ok_or_else(|| format!("T1 PowerPoint key figure {key} is missing"))
+ };
+ let anomaly = |key: &str| {
+ anomalies
+ .get(key)
+ .map(String::as_str)
+ .ok_or_else(|| format!("T1 PowerPoint anomaly {key} is missing"))
+ };
+ let sources = reconciliation
+ .source_manifest
+ .entries
+ .iter()
+ .map(|entry| entry.relative_path.as_str())
+ .collect::>();
+ if sources.len() != 3 {
+ return Err("T1 PowerPoint requires the exact three-source manifest".to_string());
+ }
+ let period = value("period")?;
+ let body = match revision {
+ 0 => format!(
+ "Period: {period}\nRevenue: CNY {} | Budget variance: CNY {} ({:.2}%)\nPrior-period variance: CNY {} ({:.2}%)\nOccupancy: {:.2}% | Budget gap: {} pp\nGuest/service: breakfast queue {} | invoice corrections >48h {} | July-deferred leads {}\nFacilities/people: elevator outages {} | overdue fire-door checks {} | retraining incomplete {}\nSources:\n{}\n{}\n{}",
+ value("total_revenue_cny")?,
+ value("budget_variance_cny")?,
+ percentage(value("budget_variance_rate")?)?,
+ value("prior_variance_cny")?,
+ percentage(value("prior_variance_rate")?)?,
+ percentage(value("occupancy_rate")?)?,
+ value("occupancy_variance_percentage_points")?,
+ anomaly("breakfast_queue_complaints")?,
+ anomaly("overdue_invoice_corrections_over_48h")?,
+ anomaly("group_leads_deferred_to_july")?,
+ anomaly("elevator_2_unplanned_outages")?,
+ anomaly("overdue_fire_door_closing_checks")?,
+ anomaly("temporary_food_staff_retraining_incomplete")?,
+ sources[0],
+ sources[1],
+ sources[2],
+ ),
+ _ => format!(
+ "{period} | Revenue CNY {} | Budget CNY {} ({:.2}%) | Prior CNY {} ({:.2}%)\nOccupancy {:.2}% | Budget gap {} pp\nFlags: breakfast {} | invoices {} | July leads {} | elevator {} | fire doors {} | retraining {}\nSource 1: {}\nSource 2: {}\nSource 3: {}",
+ value("total_revenue_cny")?,
+ value("budget_variance_cny")?,
+ percentage(value("budget_variance_rate")?)?,
+ value("prior_variance_cny")?,
+ percentage(value("prior_variance_rate")?)?,
+ percentage(value("occupancy_rate")?)?,
+ value("occupancy_variance_percentage_points")?,
+ anomaly("breakfast_queue_complaints")?,
+ anomaly("overdue_invoice_corrections_over_48h")?,
+ anomaly("group_leads_deferred_to_july")?,
+ anomaly("elevator_2_unplanned_outages")?,
+ anomaly("overdue_fire_door_closing_checks")?,
+ anomaly("temporary_food_staff_retraining_incomplete")?,
+ sources[0],
+ sources[1],
+ sources[2],
+ ),
+ };
+ Ok(OfficeCreateSpec {
+ app: OfficeApp::PowerPoint,
+ path: relative_path.to_string(),
+ title: format!("Verified T1 monthly operating brief — {period}"),
+ body: String::new(),
+ rows: Vec::new(),
+ slides: vec![OfficeSlideSpec {
+ title: format!("Verified T1 monthly operating brief — {period}"),
+ body,
+ }],
+ })
+}
+
+fn percentage(value: &str) -> Result {
+ value
+ .parse::()
+ .map(|value| value * 100.0)
+ .map_err(|_| "T1 PowerPoint percentage value is invalid".to_string())
+}
+
+fn anomaly_projection(
+ reconciliation: &T1ReconciliationOutcome,
+) -> Result, String> {
+ let facts = reconciliation
+ .provenance
+ .facts
+ .iter()
+ .map(|fact| (fact.fact_id.as_str(), fact.value.as_str()))
+ .collect::>();
+ [
+ "breakfast_queue_complaints",
+ "overdue_invoice_corrections_over_48h",
+ "group_leads_deferred_to_july",
+ "elevator_2_unplanned_outages",
+ "overdue_fire_door_closing_checks",
+ "temporary_food_staff_retraining_incomplete",
+ ]
+ .into_iter()
+ .map(|fact_id| {
+ facts
+ .get(fact_id)
+ .map(|value| (fact_id.to_string(), (*value).to_string()))
+ .ok_or_else(|| format!("T1 PowerPoint anomaly {fact_id} is missing"))
+ })
+ .collect()
+}
+
+fn canonical_workspace(workspace_root: &Path) -> Result {
+ let metadata = fs::symlink_metadata(workspace_root)
+ .map_err(|error| format!("T1 PowerPoint workspace is unavailable: {error}"))?;
+ if metadata.file_type().is_symlink() || !metadata.is_dir() {
+ return Err("T1 PowerPoint workspace must be a real directory".to_string());
+ }
+ workspace_root
+ .canonicalize()
+ .map_err(|error| format!("T1 PowerPoint workspace could not be resolved: {error}"))
+}
+
+fn validated_relative_path(value: &str, label: &str) -> Result<(PathBuf, String), String> {
+ let normalized = value.trim().replace('\\', "/");
+ let normalized = normalized.trim_matches('/');
+ if normalized.is_empty() {
+ return Err(format!("{label} is required"));
+ }
+ let path = Path::new(normalized);
+ if path.is_absolute()
+ || path
+ .components()
+ .any(|component| !matches!(component, Component::Normal(_)))
+ {
+ return Err(format!("{label} must stay inside the authorized workspace"));
+ }
+ Ok((path.to_path_buf(), normalized.to_string()))
+}
+
+fn resolve_new_pptx(workspace: &Path, value: &str) -> Result<(PathBuf, String), String> {
+ let (relative, normalized) = validated_relative_path(value, "T1 PowerPoint output path")?;
+ if !normalized.to_ascii_lowercase().ends_with(".pptx") {
+ return Err("T1 PowerPoint output path must end in .pptx".to_string());
+ }
+ let output = workspace.join(relative);
+ validate_new_output(workspace, &output)?;
+ Ok((output, normalized))
+}
+
+fn revision_sibling(
+ workspace: &Path,
+ original_relative_path: &str,
+ revision: u32,
+) -> Result<(PathBuf, String), String> {
+ if revision == 0 || revision > MAX_ARTIFACT_REVISIONS {
+ return Err("T1 PowerPoint revision number is invalid".to_string());
+ }
+ let original = Path::new(original_relative_path);
+ let stem = original
+ .file_stem()
+ .and_then(|value| value.to_str())
+ .ok_or_else(|| "T1 PowerPoint output name is invalid".to_string())?;
+ let sibling = original.with_file_name(format!("{stem}.revision-{revision}.pptx"));
+ let normalized = sibling.to_string_lossy().replace('\\', "/");
+ let output = workspace.join(&sibling);
+ validate_new_output(workspace, &output)?;
+ Ok((output, normalized))
+}
+
+fn validate_new_output(workspace: &Path, output: &Path) -> Result<(), String> {
+ if output.exists() {
+ return Err("T1 PowerPoint output already exists; overwrite is blocked".to_string());
+ }
+ let parent = output
+ .parent()
+ .ok_or_else(|| "T1 PowerPoint output parent is invalid".to_string())?;
+ let metadata = fs::symlink_metadata(parent)
+ .map_err(|error| format!("T1 PowerPoint output parent is unavailable: {error}"))?;
+ if metadata.file_type().is_symlink() || !metadata.is_dir() {
+ return Err("T1 PowerPoint output parent must be an authorized real directory".to_string());
+ }
+ let parent = parent
+ .canonicalize()
+ .map_err(|error| format!("T1 PowerPoint output parent could not be resolved: {error}"))?;
+ if !parent.starts_with(workspace) {
+ return Err("T1 PowerPoint output escaped the authorized workspace".to_string());
+ }
+ Ok(())
+}
+
+fn write_new_artifact(workspace: &Path, path: &Path, bytes: &[u8]) -> Result<(), String> {
+ if bytes.is_empty() || bytes.len() > MAX_PPTX_BYTES {
+ return Err("T1 PowerPoint artifact size is invalid".to_string());
+ }
+ validate_new_output(workspace, path)?;
+ let mut file = OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .open(path)
+ .map_err(|error| format!("T1 PowerPoint artifact could not be created: {error}"))?;
+ file.write_all(bytes)
+ .and_then(|_| file.sync_all())
+ .map_err(|error| format!("T1 PowerPoint artifact could not be persisted: {error}"))?;
+ drop(file);
+ let metadata = fs::symlink_metadata(path)
+ .map_err(|error| format!("T1 PowerPoint artifact metadata is unavailable: {error}"))?;
+ if metadata.file_type().is_symlink() || !metadata.is_file() {
+ return Err("T1 PowerPoint artifact must be a real file".to_string());
+ }
+ let canonical = path
+ .canonicalize()
+ .map_err(|error| format!("T1 PowerPoint artifact could not be resolved: {error}"))?;
+ if !canonical.starts_with(workspace) || read_bounded_file(&canonical, MAX_PPTX_BYTES)? != bytes
+ {
+ return Err("T1 PowerPoint persisted bytes failed identity verification".to_string());
+ }
+ Ok(())
+}
+
+fn read_bounded_file(path: &Path, maximum: usize) -> Result, String> {
+ let mut file = File::open(path)
+ .map_err(|error| format!("T1 PowerPoint artifact could not be opened: {error}"))?;
+ let mut bytes = Vec::new();
+ Read::take(&mut file, maximum.saturating_add(1) as u64)
+ .read_to_end(&mut bytes)
+ .map_err(|error| format!("T1 PowerPoint artifact could not be read: {error}"))?;
+ if bytes.is_empty() || bytes.len() > maximum {
+ return Err("T1 PowerPoint artifact size is invalid".to_string());
+ }
+ Ok(bytes)
+}
+
+fn remove_created_if_unchanged(created: &[(PathBuf, String)]) {
+ for (path, expected_hash) in created.iter().rev() {
+ let should_remove = fs::symlink_metadata(path)
+ .ok()
+ .is_some_and(|metadata| metadata.is_file() && !metadata.file_type().is_symlink())
+ && read_bounded_file(path, MAX_PPTX_BYTES)
+ .ok()
+ .is_some_and(|bytes| sha256(&bytes) == *expected_hash);
+ if should_remove {
+ let _ = fs::remove_file(path);
+ }
+ }
+}
+
+fn canonicalize_pptx(bytes: Vec) -> Result, String> {
+ let mut archive = ZipArchive::new(Cursor::new(bytes))
+ .map_err(|_| "T1 PowerPoint OPC package cannot be opened".to_string())?;
+ if archive.is_empty() || archive.len() > MAX_OPC_PARTS {
+ return Err("T1 PowerPoint OPC part count is invalid".to_string());
+ }
+ let mut parts = BTreeMap::new();
+ let mut expanded = 0usize;
+ for index in 0..archive.len() {
+ let mut file = archive
+ .by_index(index)
+ .map_err(|_| "T1 PowerPoint OPC part cannot be opened".to_string())?;
+ if file.is_dir() {
+ return Err("T1 PowerPoint OPC package contains a directory entry".to_string());
+ }
+ let name = file.name().replace('\\', "/");
+ validated_relative_path(&name, "T1 PowerPoint OPC part path")?;
+ let mut part = Vec::new();
+ file.read_to_end(&mut part)
+ .map_err(|_| "T1 PowerPoint OPC part cannot be read".to_string())?;
+ expanded = expanded
+ .checked_add(part.len())
+ .ok_or_else(|| "T1 PowerPoint OPC expanded size overflow".to_string())?;
+ if expanded > MAX_PPTX_BYTES {
+ return Err("T1 PowerPoint OPC expanded size is invalid".to_string());
+ }
+ if name == "docProps/core.xml" {
+ part = canonical_core_properties(&part)?;
+ }
+ if parts.insert(name, part).is_some() {
+ return Err("T1 PowerPoint OPC package contains a duplicate part".to_string());
+ }
+ }
+ let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
+ let options = FileOptions::default()
+ .compression_method(zip::CompressionMethod::Deflated)
+ .unix_permissions(0o644);
+ for (path, part) in parts {
+ zip.start_file(&path, options)
+ .map_err(|error| format!("T1 PowerPoint OPC part {path} could not start: {error}"))?;
+ zip.write_all(&part)
+ .map_err(|error| format!("T1 PowerPoint OPC part {path} could not write: {error}"))?;
+ }
+ zip.finish()
+ .map(|cursor| cursor.into_inner())
+ .map_err(|error| format!("T1 PowerPoint OPC package could not finish: {error}"))
+}
+
+fn canonical_core_properties(bytes: &[u8]) -> Result, String> {
+ let mut xml = std::str::from_utf8(bytes)
+ .map_err(|_| "T1 PowerPoint core properties are invalid UTF-8".to_string())?
+ .to_string();
+ for tag in ["dcterms:created", "dcterms:modified"] {
+ let start_marker = format!("<{tag} xsi:type=\"dcterms:W3CDTF\">");
+ let start = xml
+ .find(&start_marker)
+ .ok_or_else(|| "T1 PowerPoint core timestamp is missing".to_string())?
+ + start_marker.len();
+ let end_marker = format!("{tag}>");
+ let end = xml[start..]
+ .find(&end_marker)
+ .ok_or_else(|| "T1 PowerPoint core timestamp is invalid".to_string())?
+ + start;
+ xml.replace_range(start..end, FIXED_CORE_TIMESTAMP);
+ }
+ Ok(xml.into_bytes())
+}
+
+fn verify_powerpoint(
+ bytes: &[u8],
+ reconciliation: &T1ReconciliationOutcome,
+ anomalies: &BTreeMap,
+ expected_path: &str,
+) -> Result<(), String> {
+ if bytes.is_empty() || bytes.len() > MAX_PPTX_BYTES || !expected_path.ends_with(".pptx") {
+ return Err("T1 PowerPoint artifact identity is invalid".to_string());
+ }
+ let mut archive = ZipArchive::new(Cursor::new(bytes))
+ .map_err(|_| "T1 PowerPoint OPC package cannot be opened".to_string())?;
+ if archive.is_empty() || archive.len() > MAX_OPC_PARTS {
+ return Err("T1 PowerPoint OPC part count is invalid".to_string());
+ }
+ let mut parts = BTreeMap::new();
+ let mut expanded = 0usize;
+ for index in 0..archive.len() {
+ let mut file = archive
+ .by_index(index)
+ .map_err(|_| "T1 PowerPoint OPC part cannot be opened".to_string())?;
+ if file.is_dir() {
+ return Err("T1 PowerPoint OPC package contains a directory entry".to_string());
+ }
+ let name = file.name().replace('\\', "/");
+ validated_relative_path(&name, "T1 PowerPoint OPC part path")?;
+ if name.to_ascii_lowercase().ends_with(".bin")
+ || name.to_ascii_lowercase().contains("vbaproject")
+ {
+ return Err("T1 PowerPoint macro content is blocked".to_string());
+ }
+ let mut part = Vec::new();
+ file.read_to_end(&mut part)
+ .map_err(|_| "T1 PowerPoint OPC part cannot be read".to_string())?;
+ expanded = expanded
+ .checked_add(part.len())
+ .ok_or_else(|| "T1 PowerPoint OPC expanded size overflow".to_string())?;
+ if expanded > MAX_PPTX_BYTES {
+ return Err("T1 PowerPoint OPC expanded size is invalid".to_string());
+ }
+ if (name.ends_with(".xml") || name.ends_with(".rels"))
+ && (validate_xml(&part).is_err() || String::from_utf8_lossy(&part).contains('\u{fffd}'))
+ {
+ return Err("T1 PowerPoint OPC XML is invalid".to_string());
+ }
+ if name.ends_with(".rels") {
+ let lower = String::from_utf8_lossy(&part).to_ascii_lowercase();
+ if lower.contains("targetmode=\"external\"")
+ || lower.contains("target=\"http:")
+ || lower.contains("target=\"https:")
+ || lower.contains("target=\"file:")
+ || lower.contains("target=\"\\\\")
+ {
+ return Err("T1 PowerPoint external relationship is blocked".to_string());
+ }
+ }
+ if parts.insert(name, part).is_some() {
+ return Err("T1 PowerPoint OPC package contains a duplicate part".to_string());
+ }
+ }
+ for required in [
+ "[Content_Types].xml",
+ "_rels/.rels",
+ "ppt/presentation.xml",
+ "ppt/_rels/presentation.xml.rels",
+ "ppt/slides/slide1.xml",
+ ] {
+ if !parts.contains_key(required) {
+ return Err("T1 PowerPoint OPC package is missing a required part".to_string());
+ }
+ }
+ let slide_parts = parts
+ .keys()
+ .filter(|name| {
+ name.strip_prefix("ppt/slides/slide")
+ .and_then(|name| name.strip_suffix(".xml"))
+ .is_some_and(|name| name.bytes().all(|byte| byte.is_ascii_digit()))
+ })
+ .count();
+ if slide_parts != 1 {
+ return Err("T1 PowerPoint must contain exactly one slide".to_string());
+ }
+ let slide_xml = std::str::from_utf8(&parts["ppt/slides/slide1.xml"])
+ .map_err(|_| "T1 PowerPoint slide text is invalid UTF-8".to_string())?;
+ let slide_text = xml_text(slide_xml)?;
+ let key_value = |key: &str| {
+ reconciliation
+ .key_figures
+ .get(key)
+ .map(String::as_str)
+ .ok_or_else(|| format!("T1 PowerPoint key figure {key} is missing"))
+ };
+ let mut required_text = vec![
+ key_value("period")?.to_string(),
+ key_value("total_revenue_cny")?.to_string(),
+ key_value("budget_variance_cny")?.to_string(),
+ format!("{:.2}", percentage(key_value("budget_variance_rate")?)?),
+ key_value("prior_variance_cny")?.to_string(),
+ format!("{:.2}", percentage(key_value("prior_variance_rate")?)?),
+ format!("{:.2}", percentage(key_value("occupancy_rate")?)?),
+ key_value("occupancy_variance_percentage_points")?.to_string(),
+ ];
+ required_text.extend(anomalies.values().cloned());
+ required_text.extend(
+ reconciliation
+ .source_manifest
+ .entries
+ .iter()
+ .map(|entry| entry.relative_path.clone()),
+ );
+ if required_text
+ .into_iter()
+ .any(|required| !slide_text.contains(&required))
+ {
+ return Err("T1 PowerPoint slide is missing verified source content".to_string());
+ }
+ Ok(())
+}
+
+fn validate_xml(bytes: &[u8]) -> Result<(), String> {
+ let mut reader = Reader::from_reader(bytes);
+ loop {
+ match reader.read_event() {
+ Ok(Event::Eof) => return Ok(()),
+ Ok(_) => {}
+ Err(_) => return Err("T1 PowerPoint XML is invalid".to_string()),
+ }
+ }
+}
+
+fn xml_text(xml: &str) -> Result {
+ let mut reader = Reader::from_str(xml);
+ let mut text = String::new();
+ loop {
+ match reader.read_event() {
+ Ok(Event::Text(value)) => {
+ let decoded = value
+ .decode()
+ .map_err(|_| "T1 PowerPoint text encoding is invalid".to_string())?;
+ let unescaped = quick_xml::escape::unescape(&decoded)
+ .map_err(|_| "T1 PowerPoint text escaping is invalid".to_string())?;
+ text.push_str(&unescaped);
+ text.push('\n');
+ }
+ Ok(Event::Eof) => return Ok(text),
+ Ok(_) => {}
+ Err(_) => return Err("T1 PowerPoint slide XML is invalid".to_string()),
+ }
+ }
+}
+
+fn completion_evidence(
+ artifact: &T1PowerPointArtifactReceipt,
+ render: &T1PowerPointRenderReceipt,
+ revision: &T1PowerPointRevisionReceipt,
+) -> Result, String> {
+ Ok(vec![
+ ToolEvidence {
+ kind: T1_POWERPOINT_EVIDENCE_KIND.to_string(),
+ reference: T1_POWERPOINT_ARTIFACT_ID.to_string(),
+ summary: format!(
+ "One-page PPTX persisted and re-read with artifact SHA-256 {}.",
+ artifact.sha256
+ ),
+ },
+ ToolEvidence {
+ kind: T1_POWERPOINT_RENDER_EVIDENCE_KIND.to_string(),
+ reference: format!("evidence:t1-office-render:{}", canonical_hash(render)?),
+ summary: format!(
+ "Microsoft Office actual render verified {} non-blank page with preview manifest SHA-256 {}.",
+ render.rendered_page_count, render.preview_manifest_sha256
+ ),
+ },
+ ToolEvidence {
+ kind: T1_POWERPOINT_REVISION_EVIDENCE_KIND.to_string(),
+ reference: format!("evidence:t1-office-revision:{}", canonical_hash(revision)?),
+ summary: format!(
+ "Bounded sibling-only revision workflow completed after {} revision(s).",
+ revision.revision_attempts
+ ),
+ },
+ ])
+}
+
+fn sha256(bytes: &[u8]) -> String {
+ hex::encode(Sha256::digest(bytes))
+}
+
+fn canonical_hash(value: &T) -> Result {
+ serde_json::to_vec(value)
+ .map(|bytes| sha256(&bytes))
+ .map_err(|error| format!("T1 PowerPoint receipt could not be serialized: {error}"))
+}
+
+#[cfg(test)]
+mod tests {
+ use std::cell::RefCell;
+ use std::collections::VecDeque;
+
+ use image::{DynamicImage, GrayImage, ImageFormat, Luma};
+
+ use super::*;
+ use crate::kernel::benchmark::t1::fixtures::generate_fixture_set;
+ use crate::kernel::models::AccessMode;
+ use crate::kernel::t1_reconciliation::{run_t1_reconciliation, T1ReconciliationRequest};
+ use crate::kernel::tool_runtime::{
+ prepare_tool_execution, ToolExecutionRequest, ToolExecutionStatus, ToolInvocationRecord,
+ };
+
+ struct FixtureRenderer {
+ renders: RefCell>, String>>>,
+ }
+
+ impl T1PowerPointRenderer for FixtureRenderer {
+ fn render(&self, _path: &Path) -> Result {
+ let pages = self
+ .renders
+ .borrow_mut()
+ .pop_front()
+ .unwrap_or_else(|| Ok(vec![valid_preview()]))?;
+ Ok(T1PowerPointRender {
+ pages,
+ renderer_version: "fixture-office-renderer/v1".to_string(),
+ })
+ }
+ }
+
+ fn fixture_request() -> (tempfile::TempDir, T1PowerPointRequest) {
+ let workspace = tempfile::tempdir().expect("workspace");
+ let fixtures = generate_fixture_set().expect("fixtures");
+ for fixture in fixtures.files {
+ let path = workspace.path().join(fixture.relative_path);
+ fs::create_dir_all(path.parent().unwrap()).unwrap();
+ fs::write(path, fixture.bytes).unwrap();
+ }
+ fs::create_dir(workspace.path().join("outputs")).unwrap();
+ let reconciliation = run_t1_reconciliation(
+ workspace.path(),
+ &T1ReconciliationRequest {
+ source_directory: "inputs".to_string(),
+ output_relative_path: "outputs/t1-reconciliation.xlsx".to_string(),
+ },
+ )
+ .expect("reconciliation");
+ (
+ workspace,
+ T1PowerPointRequest {
+ source_directory: "inputs".to_string(),
+ reconciliation,
+ output_relative_path: "outputs/t1-monthly-brief.pptx".to_string(),
+ },
+ )
+ }
+
+ fn blank_preview() -> Vec {
+ png_preview(false)
+ }
+
+ fn valid_preview() -> Vec {
+ png_preview(true)
+ }
+
+ fn png_preview(with_content: bool) -> Vec {
+ let mut image = GrayImage::from_pixel(320, 180, Luma([255]));
+ if with_content {
+ for y in 60..120 {
+ for x in 80..240 {
+ image.put_pixel(x, y, Luma([32]));
+ }
+ }
+ }
+ let mut cursor = Cursor::new(Vec::new());
+ DynamicImage::ImageLuma8(image)
+ .write_to(&mut cursor, ImageFormat::Png)
+ .unwrap();
+ cursor.into_inner()
+ }
+
+ fn renderer(renders: Vec>, String>>) -> FixtureRenderer {
+ FixtureRenderer {
+ renders: RefCell::new(renders.into()),
+ }
+ }
+
+ #[test]
+ fn powerpoint_executor_binds_c4a_and_returns_contract_validated_completion() {
+ let (workspace, request) = fixture_request();
+ let plan = prepare_tool_execution(&ToolExecutionRequest {
+ tool_id: T1_POWERPOINT_TOOL_ID.to_string(),
+ input: serde_json::to_value(&request).unwrap(),
+ access_mode: AccessMode::FullAccess,
+ run_id: Some(Uuid::new_v4()),
+ })
+ .unwrap();
+ let fixture_renderer = renderer(vec![Ok(vec![valid_preview()])]);
+ let executor = T1PowerPointAgentToolExecutor::new(workspace.path(), &fixture_renderer);
+ let output = executor.execute(&plan).unwrap();
+ let outcome: T1PowerPointOutcome = serde_json::from_value(output.output.clone()).unwrap();
+ let mut incomplete_evidence = output.evidence.clone();
+ incomplete_evidence.retain(|item| item.kind != T1_POWERPOINT_RENDER_EVIDENCE_KIND);
+ assert!(ToolInvocationRecord::succeeded(
+ &plan,
+ output.output.clone(),
+ incomplete_evidence,
+ output.verification.clone(),
+ None,
+ 1,
+ )
+ .is_err());
+ let invocation = ToolInvocationRecord::succeeded(
+ &plan,
+ output.output,
+ output.evidence,
+ output.verification,
+ None,
+ 1,
+ )
+ .unwrap();
+
+ assert_eq!(invocation.status, ToolExecutionStatus::Succeeded);
+ assert_eq!(invocation.evidence.len(), 3);
+ assert_eq!(outcome.render.rendered_page_count, 1);
+ assert_eq!(outcome.revision.revision_attempts, 0);
+ assert_eq!(
+ outcome.reconciliation_artifact_sha256,
+ request.reconciliation.artifact.sha256
+ );
+ assert!(workspace
+ .path()
+ .join(outcome.artifact.delivered_relative_path)
+ .is_file());
+ }
+
+ #[test]
+ fn visual_failure_creates_a_new_sibling_and_preserves_original() {
+ let (workspace, request) = fixture_request();
+ let fixture_renderer = renderer(vec![Ok(vec![blank_preview()]), Ok(vec![valid_preview()])]);
+ let outcome = run_t1_powerpoint(workspace.path(), &request, &fixture_renderer).unwrap();
+ let original = workspace
+ .path()
+ .join(&outcome.artifact.original_relative_path);
+ let delivered = workspace
+ .path()
+ .join(&outcome.artifact.delivered_relative_path);
+
+ assert_eq!(outcome.revision.revision_attempts, 1);
+ assert!(outcome
+ .artifact
+ .delivered_relative_path
+ .ends_with(".revision-1.pptx"));
+ assert!(original.is_file());
+ assert!(delivered.is_file());
+ assert_ne!(fs::read(original).unwrap(), fs::read(delivered).unwrap());
+ }
+
+ #[test]
+ fn exhausted_visual_revisions_fail_and_remove_only_created_pptx_files() {
+ let (workspace, request) = fixture_request();
+ let fixture_renderer = renderer(vec![
+ Ok(vec![blank_preview()]),
+ Ok(vec![blank_preview()]),
+ Ok(vec![blank_preview()]),
+ Ok(vec![blank_preview()]),
+ ]);
+ let error = run_t1_powerpoint(workspace.path(), &request, &fixture_renderer).unwrap_err();
+ let pptx_paths = fs::read_dir(workspace.path().join("outputs"))
+ .unwrap()
+ .filter_map(Result::ok)
+ .map(|entry| entry.path())
+ .filter(|path| {
+ path.extension()
+ .is_some_and(|extension| extension == "pptx")
+ })
+ .collect::>();
+
+ assert!(error.contains("revision limit"));
+ assert!(pptx_paths.is_empty());
+ assert!(workspace
+ .path()
+ .join(&request.reconciliation.artifact.relative_path)
+ .is_file());
+ }
+
+ #[test]
+ fn source_or_reconciliation_drift_fails_without_leaving_a_pptx() {
+ let (workspace, request) = fixture_request();
+ fs::write(
+ workspace.path().join("inputs/03-operations-notes.pdf"),
+ b"changed",
+ )
+ .unwrap();
+ let fixture_renderer = renderer(vec![Ok(vec![valid_preview()])]);
+ run_t1_powerpoint(workspace.path(), &request, &fixture_renderer).unwrap_err();
+ assert!(!workspace
+ .path()
+ .join(&request.output_relative_path)
+ .exists());
+ }
+
+ #[test]
+ fn output_escape_overwrite_and_renderer_failure_are_fail_closed() {
+ let (workspace, mut request) = fixture_request();
+ let fixture_renderer = renderer(vec![Ok(vec![valid_preview()])]);
+ request.output_relative_path = "../escaped.pptx".to_string();
+ assert!(run_t1_powerpoint(workspace.path(), &request, &fixture_renderer).is_err());
+
+ request.output_relative_path = "outputs/existing.pptx".to_string();
+ fs::write(
+ workspace.path().join(&request.output_relative_path),
+ b"owned",
+ )
+ .unwrap();
+ assert!(run_t1_powerpoint(workspace.path(), &request, &fixture_renderer).is_err());
+ assert_eq!(
+ fs::read(workspace.path().join(&request.output_relative_path)).unwrap(),
+ b"owned"
+ );
+
+ request.output_relative_path = "outputs/render-failure.pptx".to_string();
+ let failing_renderer = renderer(vec![Err("Office unavailable".to_string())]);
+ assert!(run_t1_powerpoint(workspace.path(), &request, &failing_renderer).is_err());
+ assert!(!workspace
+ .path()
+ .join(&request.output_relative_path)
+ .exists());
+ }
+
+ #[test]
+ fn pptx_bytes_are_deterministic_for_the_same_verified_input_and_revision() {
+ let (workspace_a, request_a) = fixture_request();
+ let (workspace_b, request_b) = fixture_request();
+ let renderer_a = renderer(vec![Ok(vec![valid_preview()])]);
+ let renderer_b = renderer(vec![Ok(vec![valid_preview()])]);
+ let outcome_a = run_t1_powerpoint(workspace_a.path(), &request_a, &renderer_a).unwrap();
+ let outcome_b = run_t1_powerpoint(workspace_b.path(), &request_b, &renderer_b).unwrap();
+
+ assert_eq!(outcome_a.artifact.sha256, outcome_b.artifact.sha256);
+ assert_eq!(outcome_a.artifact.bytes, outcome_b.artifact.bytes);
+ }
+
+ #[cfg(windows)]
+ #[test]
+ #[ignore = "requires installed Microsoft Office and pdftoppm"]
+ fn live_office_render_verifies_the_generated_one_page_pptx() {
+ let (workspace, request) = fixture_request();
+ let outcome = run_t1_powerpoint(workspace.path(), &request, &LocalT1PowerPointRenderer)
+ .expect("PowerPoint actual render");
+ assert_eq!(outcome.render.rendered_page_count, 1);
+ }
+}
diff --git a/apps/desktop/src-tauri/src/kernel/t1_reconciliation.rs b/apps/desktop/src-tauri/src/kernel/t1_reconciliation.rs
index 80211f7..c97b294 100644
--- a/apps/desktop/src-tauri/src/kernel/t1_reconciliation.rs
+++ b/apps/desktop/src-tauri/src/kernel/t1_reconciliation.rs
@@ -235,6 +235,33 @@ pub fn verify_persisted_t1_reconciliation(
verify_t1_reconciliation_artifact(&source_manifest, &provenance, expected_artifact, &bytes)
}
+pub fn verify_existing_t1_reconciliation(
+ workspace_root: &Path,
+ source_directory: &str,
+ expected: &T1ReconciliationOutcome,
+) -> Result {
+ let request = T1ReconciliationRequest {
+ source_directory: source_directory.to_string(),
+ output_relative_path: expected.artifact.relative_path.clone(),
+ };
+ let completion_evidence = verify_persisted_t1_reconciliation(
+ workspace_root,
+ &request,
+ &expected.source_manifest,
+ &expected.provenance,
+ &expected.artifact,
+ )?;
+ if completion_evidence != expected.completion_evidence {
+ return Err(
+ "T1 reconciliation completion evidence changed before PPT generation".to_string(),
+ );
+ }
+ if key_figures(&expected.provenance)? != expected.key_figures {
+ return Err("T1 reconciliation key figures changed before PPT generation".to_string());
+ }
+ Ok(expected.clone())
+}
+
pub fn verify_t1_reconciliation_artifact(
source_manifest: &T1SourceManifest,
provenance: &T1ProvenanceManifest,
diff --git a/apps/desktop/src-tauri/src/kernel/tool_runtime.rs b/apps/desktop/src-tauri/src/kernel/tool_runtime.rs
index 83adda9..a562c3d 100644
--- a/apps/desktop/src-tauri/src/kernel/tool_runtime.rs
+++ b/apps/desktop/src-tauri/src/kernel/tool_runtime.rs
@@ -24,6 +24,7 @@ pub const OFFICE_CREATE_TOOL_ID: &str = "office.create";
pub const OFFICE_OPEN_TOOL_ID: &str = "office.open";
pub const OFFICE_UPDATE_TOOL_ID: &str = "office.update";
pub const OPERATIONS_BRIEFING_TOOL_ID: &str = "operations.briefing";
+pub const T1_POWERPOINT_TOOL_ID: &str = "operations.generate_powerpoint";
pub const T1_RECONCILIATION_TOOL_ID: &str = "operations.reconcile_excel";
pub const SKILL_ACTIVATE_TOOL_ID: &str = "skill.activate";
pub const TERMINAL_READ_TOOL_ID: &str = "terminal.read";
@@ -848,6 +849,112 @@ pub fn builtin_tool_catalog() -> Vec {
"Restore the exact three-source T1 set or choose a new authorized XLSX path, then retry one reconciliation without overwriting an existing artifact."
.to_string(),
},
+ ToolContract {
+ id: T1_POWERPOINT_TOOL_ID.to_string(),
+ version: "1.0.0".to_string(),
+ title: "Generate and locally verify the T1 PowerPoint brief".to_string(),
+ description:
+ "Re-read the exact completed T1 reconciliation receipt, create one new workspace-relative PPTX, render it through local Microsoft Office, and permit at most three non-overwriting sibling revisions before Kernel completion."
+ .to_string(),
+ capability: CapabilityKind::FileWrite,
+ risk_level: RiskLevel::High,
+ executor_id: "kernel.operations.generate_powerpoint.v1".to_string(),
+ input_schema: object_schema(
+ vec![
+ field(
+ "source_directory",
+ ToolValueType::String,
+ "Workspace-relative directory containing the exact C4A source set.",
+ ),
+ field(
+ "reconciliation",
+ ToolValueType::Object,
+ "Complete Kernel-issued C4A reconciliation outcome and evidence receipts.",
+ ),
+ field(
+ "output_relative_path",
+ ToolValueType::String,
+ "Workspace-relative new PPTX output path inside an existing authorized directory.",
+ ),
+ ],
+ &["source_directory", "reconciliation", "output_relative_path"],
+ ),
+ output_schema: object_schema(
+ vec![
+ field(
+ "reconciliation_artifact_sha256",
+ ToolValueType::String,
+ "Exact reverified C4A reconciliation artifact identity.",
+ ),
+ field(
+ "artifact",
+ ToolValueType::Object,
+ "Persisted PPTX artifact identity and delivered revision receipt.",
+ ),
+ field(
+ "render",
+ ToolValueType::Object,
+ "Actual local Office render and preview identity receipt.",
+ ),
+ field(
+ "revision",
+ ToolValueType::Object,
+ "Bounded sibling-only revision history.",
+ ),
+ field(
+ "key_figures",
+ ToolValueType::Object,
+ "Reverified C4A key-number projection used in the slide.",
+ ),
+ field(
+ "anomalies",
+ ToolValueType::Object,
+ "Reverified operational anomaly projection used in the slide.",
+ ),
+ field(
+ "completion_evidence",
+ ToolValueType::Array,
+ "Kernel-issued evidence created only after persisted PPTX and actual-render verification.",
+ ),
+ ],
+ &[
+ "reconciliation_artifact_sha256",
+ "artifact",
+ "render",
+ "revision",
+ "key_figures",
+ "anomalies",
+ "completion_evidence",
+ ],
+ ),
+ constraints: ToolConstraints {
+ allowed_network_hosts: Vec::new(),
+ path_scope: ToolPathScope::Workspace,
+ mutates_machine_state: true,
+ protected_path_policy:
+ "exact C4A receipt and new workspace-relative PPTX; no overwrite, link traversal, protected path, boundary escape, external relationship, macro, or non-sibling revision"
+ .to_string(),
+ resource: Some(ToolResourceRequirement {
+ key: "local_filesystem://mutation".to_string(),
+ access: ToolResourceAccess::Write,
+ lease_seconds: 30 * 60,
+ }),
+ },
+ verification: ToolVerificationContract {
+ recipe_id: "operations.generate_powerpoint.t1.v1".to_string(),
+ description:
+ "Require exact C4A source and XLSX identity, a deterministic one-page PPTX, persisted artifact identity, actual local Office render evidence, and bounded sibling-only revision evidence."
+ .to_string(),
+ required_evidence_kinds: vec![
+ "one_page_pptx".to_string(),
+ "actual_render_receipt".to_string(),
+ "office_revision_receipt".to_string(),
+ ],
+ },
+ recovery_hint:
+ "Restore the exact C4A sources and XLSX, choose a new authorized PPTX path, or restore local Office rendering, then retry without overwriting any artifact."
+ .to_string(),
+ },
ToolContract {
id: OPERATIONS_BRIEFING_TOOL_ID.to_string(),
version: "1.0.0".to_string(),
@@ -1745,6 +1852,35 @@ fn validate_tool_semantics(contract: &ToolContract, input: &Value) -> Result<(),
);
}
}
+ if contract.id == T1_POWERPOINT_TOOL_ID {
+ for field in ["source_directory", "output_relative_path"] {
+ let value = input
+ .get(field)
+ .and_then(Value::as_str)
+ .map(str::trim)
+ .unwrap_or_default();
+ if value.is_empty() {
+ return Err(format!(
+ "operations.generate_powerpoint input field `{field}` cannot be blank"
+ ));
+ }
+ }
+ if !input.get("reconciliation").is_some_and(Value::is_object) {
+ return Err(
+ "operations.generate_powerpoint reconciliation must be an object".to_string(),
+ );
+ }
+ if !input["output_relative_path"]
+ .as_str()
+ .unwrap_or_default()
+ .to_ascii_lowercase()
+ .ends_with(".pptx")
+ {
+ return Err(
+ "operations.generate_powerpoint output_relative_path must end in .pptx".to_string(),
+ );
+ }
+ }
if contract.id == FILESYSTEM_MUTATE_TOOL_ID {
validate_filesystem_mutation_semantics(input)?;
}
@@ -2101,7 +2237,8 @@ mod tests {
BROWSER_OPEN_TOOL_ID, CONNECTOR_ATTACHMENT_DOWNLOAD_TOOL_ID, CONNECTOR_MUTATE_TOOL_ID,
FILESYSTEM_MUTATE_TOOL_ID, FILE_READ_TOOL_ID, FILE_WRITE_TOOL_ID, OFFICE_CREATE_TOOL_ID,
OFFICE_OPEN_TOOL_ID, OFFICE_UPDATE_TOOL_ID, OPERATIONS_BRIEFING_TOOL_ID,
- SKILL_ACTIVATE_TOOL_ID, T1_RECONCILIATION_TOOL_ID, TERMINAL_READ_TOOL_ID,
+ SKILL_ACTIVATE_TOOL_ID, T1_POWERPOINT_TOOL_ID, T1_RECONCILIATION_TOOL_ID,
+ TERMINAL_READ_TOOL_ID,
};
use crate::kernel::models::AccessMode;
use crate::kernel::policy::{CapabilityKind, PolicyDecision, RiskLevel};
@@ -2285,6 +2422,42 @@ mod tests {
.is_err());
}
+ #[test]
+ fn builtin_catalog_declares_t1_powerpoint_as_verified_workspace_write() {
+ let contract = builtin_tool_catalog()
+ .into_iter()
+ .find(|contract| contract.id == T1_POWERPOINT_TOOL_ID)
+ .expect("T1 PowerPoint contract");
+
+ assert_eq!(contract.version, "1.0.0");
+ assert_eq!(contract.capability, CapabilityKind::FileWrite);
+ assert_eq!(contract.risk_level, RiskLevel::High);
+ assert_eq!(contract.constraints.path_scope, ToolPathScope::Workspace);
+ assert!(contract.constraints.mutates_machine_state);
+ let resource = contract.constraints.resource.expect("write resource");
+ assert_eq!(resource.key, "local_filesystem://mutation");
+ assert_eq!(resource.access, ToolResourceAccess::Write);
+ assert_eq!(
+ contract.verification.required_evidence_kinds,
+ [
+ "one_page_pptx",
+ "actual_render_receipt",
+ "office_revision_receipt",
+ ]
+ );
+ assert!(prepare_tool_execution(&ToolExecutionRequest {
+ tool_id: T1_POWERPOINT_TOOL_ID.to_string(),
+ input: json!({
+ "source_directory": "inputs",
+ "reconciliation": {},
+ "output_relative_path": "outputs/brief.txt",
+ }),
+ access_mode: AccessMode::FullAccess,
+ run_id: None,
+ })
+ .is_err());
+ }
+
#[test]
fn builtin_catalog_declares_office_open_as_verified_foreground_launch() {
let contract = builtin_tool_catalog()
From 86e80d70a158a6b5a7769efb79590cdf3b4b480a Mon Sep 17 00:00:00 2001
From: Codex
Date: Wed, 22 Jul 2026 23:27:37 +0800
Subject: [PATCH 3/6] feat: persist goal continuation checkpoints
---
apps/desktop/src-tauri/src/commands.rs | 272 +++-
.../src-tauri/src/kernel/event_store.rs | 29 +
.../kernel/event_store/goal_continuation.rs | 643 +++++++++
.../src-tauri/src/kernel/goal_continuation.rs | 1175 +++++++++++++++++
apps/desktop/src-tauri/src/kernel/mod.rs | 1 +
5 files changed, 2115 insertions(+), 5 deletions(-)
create mode 100644 apps/desktop/src-tauri/src/kernel/event_store/goal_continuation.rs
create mode 100644 apps/desktop/src-tauri/src/kernel/goal_continuation.rs
diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs
index 2801a85..5d49623 100644
--- a/apps/desktop/src-tauri/src/commands.rs
+++ b/apps/desktop/src-tauri/src/commands.rs
@@ -98,6 +98,10 @@ use crate::kernel::expert_team::{
ExpertCapability, ExpertEvidenceRef, ExpertExternalEffectState, ExpertMergeReceipt,
ExpertOutput, ExpertQualityGate, ExpertReviewDecision, ExpertRole, ExpertTeamPlanItem,
};
+use crate::kernel::goal_continuation::{
+ GoalContinuationBlocker, GoalContinuationObservation, GoalContinuationObservationStage,
+ GoalModelUsage, GoalToolUsage,
+};
use crate::kernel::goal_envelope::GoalEnvelopeProposal;
use crate::kernel::goal_lifecycle::{
GoalEnvelopeUiProjection, GoalEnvelopeUiStatus, GoalLifecycleStatus, GoalTargetBindingKind,
@@ -8579,6 +8583,7 @@ fn agent_chat_with_dispatch_and_tool_followup(
pricing_settings,
)
},
+ |_, _, _, _| Ok(None),
)?;
Ok((outcome.response, outcome.telemetry))
@@ -8589,6 +8594,7 @@ struct AgentToolLoopOutcome {
telemetry: Vec,
tool_rounds: usize,
limit_reached: bool,
+ continuation_blocker: Option,
}
fn run_bounded_agent_tool_loop(
@@ -8599,12 +8605,34 @@ fn run_bounded_agent_tool_loop(
mut request_followup: impl FnMut(
String,
) -> Result<(AgentChatResponse, DeepSeekChatTelemetry), String>,
+ mut observe: impl FnMut(
+ GoalContinuationObservationStage,
+ usize,
+ &[DeepSeekChatTelemetry],
+ &AgentChatResponse,
+ ) -> Result, String>,
) -> Result {
let mut telemetry = vec![initial_telemetry];
let mut accumulated_response: Option = None;
let mut current_response = initial_response;
let mut tool_rounds = 0;
+ if let Some(blocker) = observe(
+ GoalContinuationObservationStage::InitialModel,
+ tool_rounds,
+ &telemetry,
+ ¤t_response,
+ )? {
+ block_agent_actions_for_goal_continuation(&mut current_response, &blocker);
+ return Ok(AgentToolLoopOutcome {
+ response: current_response,
+ telemetry,
+ tool_rounds,
+ limit_reached: false,
+ continuation_blocker: Some(blocker),
+ });
+ }
+
loop {
if current_response.proposed_actions.is_empty() {
let response = match accumulated_response {
@@ -8618,11 +8646,33 @@ fn run_bounded_agent_tool_loop(
telemetry,
tool_rounds,
limit_reached: false,
+ continuation_blocker: None,
});
}
tool_rounds += 1;
dispatch(&mut current_response)?;
+ if let Some(blocker) = observe(
+ GoalContinuationObservationStage::AfterToolRound,
+ tool_rounds,
+ &telemetry,
+ ¤t_response,
+ )? {
+ let mut response = accumulated_response
+ .take()
+ .map(|accumulated| {
+ merge_agent_chat_followup_response(accumulated, current_response.clone())
+ })
+ .unwrap_or(current_response);
+ response.content = blocker.user_message();
+ return Ok(AgentToolLoopOutcome {
+ response,
+ telemetry,
+ tool_rounds,
+ limit_reached: false,
+ continuation_blocker: Some(blocker),
+ });
+ }
let current_round_has_evidence =
build_agent_tool_evidence_followup_prompt(original_user_prompt, ¤t_response)
.is_some();
@@ -8640,6 +8690,7 @@ fn run_bounded_agent_tool_loop(
telemetry,
tool_rounds,
limit_reached: false,
+ continuation_blocker: None,
});
}
@@ -8656,17 +8707,35 @@ fn run_bounded_agent_tool_loop(
telemetry,
tool_rounds,
limit_reached: false,
+ continuation_blocker: None,
});
}
};
telemetry.push(followup_telemetry);
+ if let Some(blocker) = observe(
+ GoalContinuationObservationStage::AfterModelFollowup,
+ tool_rounds,
+ &telemetry,
+ &followup_response,
+ )? {
+ block_agent_actions_for_goal_continuation(&mut followup_response, &blocker);
+ return Ok(AgentToolLoopOutcome {
+ response: merge_agent_chat_followup_response(response, followup_response),
+ telemetry,
+ tool_rounds,
+ limit_reached: false,
+ continuation_blocker: Some(blocker),
+ });
+ }
+
if followup_response.proposed_actions.is_empty() {
return Ok(AgentToolLoopOutcome {
response: merge_agent_chat_followup_response(response, followup_response),
telemetry,
tool_rounds,
limit_reached: false,
+ continuation_blocker: None,
});
}
@@ -8677,6 +8746,7 @@ fn run_bounded_agent_tool_loop(
telemetry,
tool_rounds,
limit_reached: true,
+ continuation_blocker: None,
});
}
@@ -8685,6 +8755,112 @@ fn run_bounded_agent_tool_loop(
}
}
+fn block_agent_actions_for_goal_continuation(
+ response: &mut AgentChatResponse,
+ blocker: &GoalContinuationBlocker,
+) {
+ let reason = blocker.stable_reason();
+ for action in &mut response.proposed_actions {
+ if action.execution_state == "succeeded" {
+ continue;
+ }
+ action.execution_state = "blocked".to_string();
+ action.blocked_reason = Some(reason.clone());
+ action.dispatch_note = Some(reason.clone());
+ action.permission_request_id = None;
+ action.capability_invocation_id = None;
+ }
+ response.content = blocker.user_message();
+}
+
+fn agent_prompt_with_kernel_context_checkpoint(
+ store: &Mutex,
+ run_id: Uuid,
+ prompt: String,
+) -> Result {
+ let checkpoint_prompt = {
+ let store = store.lock().map_err(|_| lock_error())?;
+ store
+ .record_goal_context_checkpoint(
+ run_id,
+ GoalContinuationObservation {
+ stage: GoalContinuationObservationStage::Final,
+ local_tool_round: 0,
+ model_usage: Vec::new(),
+ tool_usage: Vec::new(),
+ observed_at: Utc::now(),
+ },
+ )
+ .map_err(event_store_error)?
+ .map(|checkpoint| checkpoint.advisory_prompt().map_err(event_store_error))
+ .transpose()?
+ };
+ Ok(match checkpoint_prompt {
+ Some(checkpoint_prompt) => {
+ format!("{checkpoint_prompt}\n\nCurrent execution prompt:\n{prompt}")
+ }
+ None => prompt,
+ })
+}
+
+fn record_agent_goal_continuation_observation(
+ store: &Mutex,
+ run_id: Option,
+ stage: GoalContinuationObservationStage,
+ tool_rounds: usize,
+ telemetry: &[DeepSeekChatTelemetry],
+ response: &AgentChatResponse,
+) -> Result, String> {
+ let Some(run_id) = run_id else {
+ return Ok(None);
+ };
+ let current_invocation_ids = response
+ .proposed_actions
+ .iter()
+ .filter_map(|action| action.capability_invocation_id)
+ .collect::>();
+ let store = store.lock().map_err(|_| lock_error())?;
+ let tool_usage = store
+ .list_tool_invocations()
+ .map_err(event_store_error)?
+ .into_iter()
+ .filter(|invocation| {
+ current_invocation_ids.contains(&invocation.id)
+ && matches!(
+ invocation.status,
+ ToolExecutionStatus::Succeeded
+ | ToolExecutionStatus::Failed
+ | ToolExecutionStatus::Blocked
+ )
+ })
+ .map(|invocation| GoalToolUsage {
+ invocation_id: invocation.id,
+ elapsed_ms: u64::try_from(invocation.elapsed_ms).unwrap_or(u64::MAX),
+ })
+ .collect();
+ let checkpoint = store
+ .record_goal_context_checkpoint(
+ run_id,
+ GoalContinuationObservation {
+ stage,
+ local_tool_round: u32::try_from(tool_rounds).unwrap_or(u32::MAX),
+ model_usage: telemetry
+ .iter()
+ .map(|item| GoalModelUsage {
+ request_id: item.id,
+ elapsed_ms: u64::try_from(item.elapsed_ms).unwrap_or(u64::MAX),
+ total_tokens: item.total_tokens,
+ estimated_cost_micro_usd: item.estimated_cost_micro_usd,
+ })
+ .collect(),
+ tool_usage,
+ observed_at: Utc::now(),
+ },
+ )
+ .map_err(event_store_error)?;
+ Ok(checkpoint.and_then(|checkpoint| checkpoint.blocker))
+}
+
fn block_agent_actions_at_tool_loop_limit(response: &mut AgentChatResponse) {
let reason = format!(
"agent loop stopped after {} tool rounds; start a new task or provide guidance before continuing",
@@ -8925,6 +9101,10 @@ fn run_agent_chat_with_clients_and_api_keys_and_computer_use(
)
};
request.prompt = agent_prompt_with_run_guidance(request.prompt, &initial_guidance);
+ if let Some(run_id) = runtime_context.active_run_id {
+ request.prompt =
+ agent_prompt_with_kernel_context_checkpoint(store, run_id, request.prompt)?;
+ }
let original_user_prompt = request.prompt.clone();
let mut runtime_context = runtime_context;
runtime_context.memory_context = memory_context;
@@ -8989,7 +9169,11 @@ fn run_agent_chat_with_clients_and_api_keys_and_computer_use(
let store = store.lock().map_err(|_| lock_error())?;
load_agent_run_guidance_batch(&store, runtime_context.active_run_id)?
};
- let followup_prompt = agent_prompt_with_run_guidance(followup_prompt, &guidance);
+ let mut followup_prompt = agent_prompt_with_run_guidance(followup_prompt, &guidance);
+ if let Some(run_id) = runtime_context.active_run_id {
+ followup_prompt =
+ agent_prompt_with_kernel_context_checkpoint(store, run_id, followup_prompt)?;
+ }
let result = agent_chat_with_transport_and_runtime_context(
transport,
cache,
@@ -9010,14 +9194,47 @@ fn run_agent_chat_with_clients_and_api_keys_and_computer_use(
}
result
},
+ |stage, tool_rounds, telemetry, response| {
+ record_agent_goal_continuation_observation(
+ store,
+ runtime_context.active_run_id,
+ stage,
+ tool_rounds,
+ telemetry,
+ response,
+ )
+ },
)?;
- let mut response = loop_outcome.response;
- let telemetry = loop_outcome.telemetry;
+ let AgentToolLoopOutcome {
+ mut response,
+ telemetry,
+ tool_rounds,
+ limit_reached,
+ mut continuation_blocker,
+ } = loop_outcome;
reconcile_agent_goal_projection(store, &mut response, &runtime_context, request.access_mode)?;
- if loop_outcome.limit_reached {
+ if let Some(run_id) = runtime_context.active_run_id {
+ let final_blocker = record_agent_goal_continuation_observation(
+ store,
+ Some(run_id),
+ GoalContinuationObservationStage::Final,
+ tool_rounds,
+ &telemetry,
+ &response,
+ )?;
+ if continuation_blocker.is_none() {
+ continuation_blocker = final_blocker;
+ }
+ if let Some(blocker) = continuation_blocker.as_ref() {
+ block_agent_actions_for_goal_continuation(&mut response, blocker);
+ record_agent_goal_continuation_blocker_step(store, run_id, blocker)?;
+ }
+ }
+
+ if limit_reached {
if let Some(run_id) = runtime_context.active_run_id {
- record_agent_tool_loop_limit_step(store, run_id, loop_outcome.tool_rounds)?;
+ record_agent_tool_loop_limit_step(store, run_id, tool_rounds)?;
}
}
@@ -9183,6 +9400,51 @@ fn record_agent_tool_loop_limit_step(
.map_err(event_store_error)
}
+fn record_agent_goal_continuation_blocker_step(
+ store: &Mutex,
+ run_id: Uuid,
+ blocker: &GoalContinuationBlocker,
+) -> Result<(), String> {
+ let store = store.lock().map_err(|_| lock_error())?;
+ let record = read_agent_run_record(&store, run_id)?;
+ let detail = format!(
+ "Kernel ContextCheckpoint stopped continuation: {}; model_rounds={}; tool_rounds={}; elapsed_ms={}; tokens={}; cost_micro_usd={}; evidence_total={}; gap_fingerprint={}.",
+ blocker.code.as_str(),
+ blocker.model_rounds,
+ blocker.tool_rounds,
+ blocker.elapsed_ms,
+ blocker.tokens,
+ blocker.cost_micro_usd,
+ blocker.evidence_total,
+ blocker.gap_fingerprint,
+ );
+ if record
+ .steps
+ .iter()
+ .any(|step| step.label == "agent.context_checkpoint" && step.detail == detail)
+ {
+ return Ok(());
+ }
+ let sequence = record
+ .steps
+ .iter()
+ .map(|step| step.sequence)
+ .max()
+ .unwrap_or(0)
+ .saturating_add(1);
+ let step = AgentRunStepRecord::new(
+ run_id,
+ sequence,
+ AgentRunStepStatus::Failed,
+ "agent.context_checkpoint".to_string(),
+ detail,
+ )
+ .map_err(event_store_error)?;
+ store
+ .append_agent_run_step(&step)
+ .map_err(event_store_error)
+}
+
#[cfg(test)]
fn run_next_queued_agent_chat_with_clients_and_api_keys(
store: &Mutex,
diff --git a/apps/desktop/src-tauri/src/kernel/event_store.rs b/apps/desktop/src-tauri/src/kernel/event_store.rs
index 446e5ab..112dc6c 100644
--- a/apps/desktop/src-tauri/src/kernel/event_store.rs
+++ b/apps/desktop/src-tauri/src/kernel/event_store.rs
@@ -3,6 +3,7 @@
mod artifact;
mod computer_use;
mod connector_draft;
+mod goal_continuation;
mod grouped_approval;
mod read_execution;
mod revocation;
@@ -1678,6 +1679,7 @@ impl EventStore {
artifact::migrate(self)?;
computer_use::migrate(self)?;
connector_draft::migrate(self)?;
+ goal_continuation::migrate(self)?;
revocation::migrate(self)?;
read_execution::migrate(self)?;
workspace_undo::migrate(self)?;
@@ -10368,6 +10370,12 @@ impl EventStore {
.to_string(),
));
}
+ if event.event_type.starts_with("goal_context_checkpoint.") {
+ return Err(EventStoreError::InvalidState(
+ "goal context checkpoint events require the dedicated Kernel state machine"
+ .to_string(),
+ ));
+ }
if event.event_type == PERMISSION_RESOLUTION_RECORDED_EVENT {
let resolution: PermissionResolution = serde_json::from_str(&event.payload_json)?;
if grouped_approval::is_grouped_request(self, resolution.request_id)? {
@@ -10738,6 +10746,13 @@ impl EventStore {
return Ok(AgentRunCompletionClassification::GoalLess);
};
if lifecycle.frozen().is_none() {
+ if let Some(reason) =
+ goal_continuation::blocker_reason_from_connection(connection, run_id)?
+ {
+ return Ok(AgentRunCompletionClassification::VerificationBlocked(
+ reason,
+ ));
+ }
return Ok(AgentRunCompletionClassification::VerificationBlocked(
AGENT_RUN_GOAL_COMPLETION_BLOCKED_REASON.to_string(),
));
@@ -10745,11 +10760,25 @@ impl EventStore {
let Some(projection) =
Self::goal_completion_projection_from_connection(connection, run_id)?
else {
+ if let Some(reason) =
+ goal_continuation::blocker_reason_from_connection(connection, run_id)?
+ {
+ return Ok(AgentRunCompletionClassification::VerificationBlocked(
+ reason,
+ ));
+ }
return Ok(AgentRunCompletionClassification::VerificationBlocked(
AGENT_RUN_GOAL_COMPLETION_BLOCKED_REASON.to_string(),
));
};
if projection.status != GoalCompletionStatus::Complete {
+ if let Some(reason) =
+ goal_continuation::blocker_reason_from_connection(connection, run_id)?
+ {
+ return Ok(AgentRunCompletionClassification::VerificationBlocked(
+ reason,
+ ));
+ }
return Ok(AgentRunCompletionClassification::VerificationBlocked(
AGENT_RUN_GOAL_COMPLETION_BLOCKED_REASON.to_string(),
));
diff --git a/apps/desktop/src-tauri/src/kernel/event_store/goal_continuation.rs b/apps/desktop/src-tauri/src/kernel/event_store/goal_continuation.rs
new file mode 100644
index 0000000..52ce388
--- /dev/null
+++ b/apps/desktop/src-tauri/src/kernel/event_store/goal_continuation.rs
@@ -0,0 +1,643 @@
+use chrono::{DateTime, SecondsFormat, Utc};
+use rusqlite::{params, Connection, OptionalExtension};
+use uuid::Uuid;
+
+use super::{EventStore, EventStoreError, EventStoreResult};
+use crate::kernel::agent_run::AgentRunResourceAccess;
+use crate::kernel::goal_continuation::{
+ identity_fingerprint, ContextArtifactIdentity, ContextAuthorizationIdentity, ContextCheckpoint,
+ ContextCheckpointSeed, ContextResourceIdentity, ContextSourceIdentity,
+ GoalContinuationObservation, CONTEXT_CHECKPOINT_VERSION,
+};
+use crate::kernel::goal_lifecycle::completion_projection;
+use crate::kernel::models::KernelEvent;
+use crate::kernel::tool_runtime::{ToolExecutionStatus, ToolInvocationRecord};
+
+pub(super) const CONTEXT_CHECKPOINT_RECORDED_EVENT: &str = "goal_context_checkpoint.recorded";
+
+pub(super) fn migrate(store: &EventStore) -> EventStoreResult<()> {
+ store.conn.execute_batch(
+ r#"
+ CREATE TABLE IF NOT EXISTS goal_context_checkpoints (
+ run_id TEXT PRIMARY KEY NOT NULL,
+ schema_version TEXT NOT NULL,
+ goal_revision TEXT NOT NULL,
+ frozen_fingerprint TEXT NOT NULL,
+ status TEXT NOT NULL,
+ checkpoint_fingerprint TEXT NOT NULL,
+ checkpoint_json TEXT NOT NULL,
+ row_revision INTEGER NOT NULL,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_goal_context_checkpoint_status
+ ON goal_context_checkpoints (status, updated_at);
+ "#,
+ )?;
+ validate_all_rows(store)
+}
+
+impl EventStore {
+ pub(crate) fn record_goal_context_checkpoint(
+ &self,
+ run_id: Uuid,
+ observation: GoalContinuationObservation,
+ ) -> EventStoreResult> {
+ let Some(lifecycle) = self.goal_envelope_projection(run_id)? else {
+ return Ok(None);
+ };
+ let Some(goal) = lifecycle.frozen().cloned() else {
+ return Ok(None);
+ };
+ let completion = self
+ .goal_completion_projection(run_id)?
+ .unwrap_or(completion_projection(&lifecycle, &[]).map_err(invalid)?);
+ let previous = context_checkpoint_from_connection(&self.conn, run_id)?;
+ if let Some(previous) = previous.as_ref() {
+ previous
+ .validate_against_goal(&lifecycle)
+ .map_err(invalid)?;
+ }
+
+ let invocations = self
+ .list_tool_invocations()?
+ .into_iter()
+ .filter(|invocation| invocation.run_id == Some(run_id))
+ .collect::>();
+ let seed = ContextCheckpointSeed {
+ run_id,
+ goal,
+ completion,
+ authorizations: authorization_identities(self, run_id)?,
+ resources: resource_identities(self, run_id)?,
+ artifacts: artifact_identities(self, run_id)?,
+ sources: source_identities(&invocations)?,
+ };
+ let checkpoint =
+ ContextCheckpoint::advance(previous.as_ref(), seed, observation).map_err(invalid)?;
+ if previous
+ .as_ref()
+ .is_some_and(|previous| previous.fingerprint == checkpoint.fingerprint)
+ {
+ return Ok(previous);
+ }
+ persist_checkpoint(self, previous.as_ref(), &checkpoint)?;
+ Ok(Some(checkpoint))
+ }
+
+ pub fn goal_context_checkpoint(
+ &self,
+ run_id: Uuid,
+ ) -> EventStoreResult> {
+ let checkpoint = context_checkpoint_from_connection(&self.conn, run_id)?;
+ let Some(checkpoint) = checkpoint else {
+ return Ok(None);
+ };
+ let lifecycle = self
+ .goal_envelope_projection(run_id)?
+ .ok_or_else(|| invalid("context_checkpoint_goal_missing"))?;
+ checkpoint
+ .validate_against_goal(&lifecycle)
+ .map_err(invalid)?;
+ Ok(Some(checkpoint))
+ }
+
+ pub(crate) fn goal_context_checkpoint_prompt(
+ &self,
+ run_id: Uuid,
+ ) -> EventStoreResult > {
+ self.goal_context_checkpoint(run_id)?
+ .map(|checkpoint| checkpoint.advisory_prompt().map_err(invalid))
+ .transpose()
+ }
+}
+
+pub(super) fn blocker_reason_from_connection(
+ connection: &Connection,
+ run_id: Uuid,
+) -> EventStoreResult > {
+ let Some(checkpoint) = context_checkpoint_from_connection(connection, run_id)? else {
+ return Ok(None);
+ };
+ let Some(lifecycle) = EventStore::goal_envelope_projection_from_connection(connection, run_id)?
+ else {
+ return Err(invalid("context_checkpoint_goal_missing"));
+ };
+ checkpoint
+ .validate_against_goal(&lifecycle)
+ .map_err(invalid)?;
+ Ok(checkpoint.blocker_reason())
+}
+
+fn persist_checkpoint(
+ store: &EventStore,
+ previous: Option<&ContextCheckpoint>,
+ checkpoint: &ContextCheckpoint,
+) -> EventStoreResult<()> {
+ checkpoint.validate().map_err(invalid)?;
+ let json = serde_json::to_string(checkpoint)?;
+ let transaction = store.conn.unchecked_transaction()?;
+ match previous {
+ Some(previous) => {
+ let row_revision = transaction.query_row(
+ "SELECT row_revision FROM goal_context_checkpoints WHERE run_id = ?1",
+ params![checkpoint.run_id.to_string()],
+ |row| row.get::<_, u64>(0),
+ )?;
+ let changed = transaction.execute(
+ r#"UPDATE goal_context_checkpoints
+ SET schema_version = ?2, goal_revision = ?3,
+ frozen_fingerprint = ?4, status = ?5,
+ checkpoint_fingerprint = ?6, checkpoint_json = ?7,
+ row_revision = row_revision + 1, updated_at = ?8
+ WHERE run_id = ?1 AND row_revision = ?9
+ AND checkpoint_fingerprint = ?10"#,
+ params![
+ checkpoint.run_id.to_string(),
+ CONTEXT_CHECKPOINT_VERSION,
+ checkpoint.goal.revision,
+ checkpoint.goal.fingerprint,
+ checkpoint_status(checkpoint),
+ checkpoint.fingerprint,
+ json,
+ timestamp(checkpoint.updated_at),
+ row_revision,
+ previous.fingerprint,
+ ],
+ )?;
+ if changed != 1 {
+ return Err(invalid("context_checkpoint_changed_concurrently"));
+ }
+ }
+ None => {
+ transaction.execute(
+ r#"INSERT INTO goal_context_checkpoints
+ (run_id, schema_version, goal_revision, frozen_fingerprint,
+ status, checkpoint_fingerprint, checkpoint_json, row_revision,
+ created_at, updated_at)
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 0, ?8, ?9)"#,
+ params![
+ checkpoint.run_id.to_string(),
+ CONTEXT_CHECKPOINT_VERSION,
+ checkpoint.goal.revision,
+ checkpoint.goal.fingerprint,
+ checkpoint_status(checkpoint),
+ checkpoint.fingerprint,
+ json,
+ timestamp(checkpoint.created_at),
+ timestamp(checkpoint.updated_at),
+ ],
+ )?;
+ }
+ }
+ let event = KernelEvent::new(CONTEXT_CHECKPOINT_RECORDED_EVENT, checkpoint)?;
+ EventStore::insert_kernel_event(&transaction, &event)?;
+ transaction.commit()?;
+ Ok(())
+}
+
+type CheckpointRow = (
+ String,
+ String,
+ String,
+ String,
+ String,
+ String,
+ u64,
+ String,
+ String,
+);
+
+fn context_checkpoint_from_connection(
+ connection: &Connection,
+ run_id: Uuid,
+) -> EventStoreResult > {
+ let row = connection
+ .query_row(
+ r#"SELECT schema_version, goal_revision, frozen_fingerprint,
+ status, checkpoint_fingerprint, checkpoint_json,
+ row_revision, created_at, updated_at
+ FROM goal_context_checkpoints WHERE run_id = ?1"#,
+ params![run_id.to_string()],
+ |row| {
+ Ok((
+ row.get(0)?,
+ row.get(1)?,
+ row.get(2)?,
+ row.get(3)?,
+ row.get(4)?,
+ row.get(5)?,
+ row.get(6)?,
+ row.get(7)?,
+ row.get(8)?,
+ ))
+ },
+ )
+ .optional()?;
+ row.map(|row| decode_checkpoint(run_id, row)).transpose()
+}
+
+fn decode_checkpoint(run_id: Uuid, row: CheckpointRow) -> EventStoreResult {
+ let (
+ schema_version,
+ goal_revision,
+ frozen_fingerprint,
+ status,
+ checkpoint_fingerprint,
+ checkpoint_json,
+ _row_revision,
+ created_at,
+ updated_at,
+ ) = row;
+ let checkpoint: ContextCheckpoint = serde_json::from_str(&checkpoint_json)?;
+ checkpoint.validate().map_err(invalid)?;
+ if checkpoint.run_id != run_id
+ || schema_version != CONTEXT_CHECKPOINT_VERSION
+ || checkpoint.version != schema_version
+ || checkpoint.goal.revision != goal_revision
+ || checkpoint.goal.fingerprint != frozen_fingerprint
+ || checkpoint_status(&checkpoint) != status
+ || checkpoint.fingerprint != checkpoint_fingerprint
+ || timestamp(checkpoint.created_at) != created_at
+ || timestamp(checkpoint.updated_at) != updated_at
+ {
+ return Err(invalid("context_checkpoint_projection_columns_drifted"));
+ }
+ Ok(checkpoint)
+}
+
+fn validate_all_rows(store: &EventStore) -> EventStoreResult<()> {
+ let run_ids = {
+ let mut statement = store
+ .conn
+ .prepare("SELECT run_id FROM goal_context_checkpoints ORDER BY run_id")?;
+ let run_ids = statement
+ .query_map([], |row| row.get::<_, String>(0))?
+ .collect::, _>>()?;
+ run_ids
+ };
+ for run_id in run_ids {
+ let run_id = Uuid::parse_str(&run_id)?;
+ context_checkpoint_from_connection(&store.conn, run_id)?
+ .ok_or_else(|| invalid("context_checkpoint_migration_lost_projection"))?;
+ }
+ Ok(())
+}
+
+fn authorization_identities(
+ store: &EventStore,
+ run_id: Uuid,
+) -> EventStoreResult> {
+ let group_ids = {
+ let mut statement = store.conn.prepare(
+ "SELECT group_id FROM task_grouped_approval_state WHERE task_id = ?1 ORDER BY group_id",
+ )?;
+ let group_ids = statement
+ .query_map(params![run_id.to_string()], |row| row.get::<_, String>(0))?
+ .collect::, _>>()?;
+ group_ids
+ };
+ group_ids
+ .into_iter()
+ .map(|group_id| {
+ let group_id = Uuid::parse_str(&group_id)?;
+ let group = store
+ .task_grouped_approval(group_id)?
+ .ok_or_else(|| invalid("context_checkpoint_authorization_missing"))?;
+ Ok(ContextAuthorizationIdentity {
+ group_id: group.id,
+ task_id: group.task_id,
+ projection_revision: group.projection_revision,
+ manifest_revision: group.manifest.revision.clone(),
+ manifest_fingerprint: group.manifest.fingerprint.clone(),
+ preview_hash: group.preview.preview_hash.clone(),
+ status: group.status.as_str().to_string(),
+ capability_request_fingerprints: group
+ .capability_audits
+ .iter()
+ .map(|item| item.request_fingerprint.clone())
+ .collect(),
+ })
+ })
+ .collect()
+}
+
+fn resource_identities(
+ store: &EventStore,
+ run_id: Uuid,
+) -> EventStoreResult> {
+ store
+ .list_active_agent_run_resource_claims()?
+ .into_iter()
+ .filter(|claim| claim.run_id == Some(run_id))
+ .map(|claim| {
+ Ok(ContextResourceIdentity {
+ claim_id: claim.id,
+ tool_invocation_id: claim.tool_invocation_id,
+ access: match claim.access {
+ AgentRunResourceAccess::Read => "read",
+ AgentRunResourceAccess::Write => "write",
+ }
+ .to_string(),
+ resource_key_fingerprint: identity_fingerprint(claim.resource_key.as_bytes()),
+ lease_expires_at: claim.lease_expires_at,
+ })
+ })
+ .collect()
+}
+
+fn artifact_identities(
+ store: &EventStore,
+ run_id: Uuid,
+) -> EventStoreResult> {
+ let mut artifacts = store
+ .list_agent_run_records()?
+ .into_iter()
+ .find(|record| record.id == run_id)
+ .into_iter()
+ .flat_map(|record| record.artifacts)
+ .map(|artifact| {
+ let canonical =
+ serde_json::to_vec(&(artifact.id, &artifact.kind, &artifact.title, &artifact.path))
+ .map_err(EventStoreError::Json)?;
+ Ok(ContextArtifactIdentity {
+ artifact_id: artifact.id.simple().to_string(),
+ kind: safe_identity_code(&artifact.kind, "agent_artifact"),
+ identity_fingerprint: identity_fingerprint(&canonical),
+ })
+ })
+ .collect::>>()?;
+ if let Some(completion) = store.goal_completion_projection(run_id)? {
+ for evidence in completion.evidence {
+ for artifact_id in evidence.artifact_ids {
+ artifacts.push(ContextArtifactIdentity {
+ artifact_id: safe_identity_code(&artifact_id, "goal_artifact"),
+ kind: "goal_evidence".to_string(),
+ identity_fingerprint: evidence.source_fingerprint.clone(),
+ });
+ }
+ }
+ }
+ Ok(artifacts)
+}
+
+fn source_identities(
+ invocations: &[ToolInvocationRecord],
+) -> EventStoreResult> {
+ invocations
+ .iter()
+ .filter(|invocation| terminal_status(invocation.status))
+ .map(|invocation| {
+ let canonical = serde_json::to_vec(&(
+ invocation.id,
+ &invocation.output,
+ &invocation.evidence,
+ invocation.verification.passed,
+ ))?;
+ Ok(ContextSourceIdentity {
+ invocation_id: invocation.id,
+ tool_id: invocation.tool_id.clone(),
+ tool_version: invocation.tool_version.clone(),
+ request_fingerprint: invocation.request_fingerprint.clone(),
+ source_fingerprint: identity_fingerprint(&canonical),
+ })
+ })
+ .collect()
+}
+
+fn terminal_status(status: ToolExecutionStatus) -> bool {
+ matches!(
+ status,
+ ToolExecutionStatus::Succeeded | ToolExecutionStatus::Failed | ToolExecutionStatus::Blocked
+ )
+}
+
+fn checkpoint_status(checkpoint: &ContextCheckpoint) -> &'static str {
+ match checkpoint.status {
+ crate::kernel::goal_continuation::ContextCheckpointStatus::Continue => "continue",
+ crate::kernel::goal_continuation::ContextCheckpointStatus::Complete => "complete",
+ crate::kernel::goal_continuation::ContextCheckpointStatus::Blocked => "blocked",
+ }
+}
+
+fn safe_identity_code(value: &str, fallback: &str) -> String {
+ let value = value.trim();
+ if !value.is_empty()
+ && value.len() <= 160
+ && value
+ .bytes()
+ .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
+ {
+ value.to_string()
+ } else {
+ fallback.to_string()
+ }
+}
+
+fn timestamp(value: DateTime) -> String {
+ value.to_rfc3339_opts(SecondsFormat::Nanos, true)
+}
+
+fn invalid(message: impl Into) -> EventStoreError {
+ EventStoreError::InvalidState(message.into())
+}
+
+#[cfg(test)]
+mod tests {
+ use std::sync::Mutex;
+
+ use rusqlite::Connection;
+ use serde_json::json;
+
+ use super::*;
+ use crate::kernel::agent_run::AgentRunStart;
+ use crate::kernel::goal_continuation::{
+ GoalContinuationBlockerCode, GoalContinuationObservationStage, GoalModelUsage,
+ GoalToolUsage,
+ };
+ use crate::kernel::goal_envelope::GOAL_ENVELOPE_PROPOSAL_VERSION;
+ use crate::kernel::goal_lifecycle::{GoalTargetBindingKind, GoalValidationContext};
+ use crate::kernel::local_directory::WorkspaceReadinessCode;
+ use crate::kernel::models::AccessMode;
+ use crate::kernel::tool_runtime::FILE_READ_TOOL_ID;
+
+ fn append_frozen_goal(store: &EventStore) -> Uuid {
+ let run = AgentRunStart::new(
+ "g1b-context".to_string(),
+ "Create one verified brief.".to_string(),
+ 0,
+ )
+ .expect("run builds");
+ store.append_agent_run_start(&run).expect("run appends");
+ let proposal = crate::kernel::goal_envelope::GoalEnvelopeProposal::parse_value(json!({
+ "version": GOAL_ENVELOPE_PROPOSAL_VERSION,
+ "user_goal": "Create one verified brief.",
+ "assumptions": [],
+ "constraints": ["Stay inside the selected workspace."],
+ "done_when": [{"done_when_id":"brief-ready","description":"The brief is verified."}],
+ "required_artifacts": [{"artifact_id":"brief","description":"The brief."}],
+ "verifiers": [{"verifier_id":"brief-verifier","done_when_id":"brief-ready","description":"Verify the brief.","evidence_kind":"brief-evidence"}],
+ "proposed_capabilities": [FILE_READ_TOOL_ID],
+ "external_targets": [{"target_id":"selected-workspace","description":"Bound locally."}],
+ "stop_conditions": ["Stop without evidence."]
+ }))
+ .expect("proposal parses");
+ let context =
+ GoalValidationContext::new(AccessMode::FullAccess, WorkspaceReadinessCode::Ready)
+ .with_enabled_tool(FILE_READ_TOOL_ID, true)
+ .with_verifier_kind("brief-evidence")
+ .with_target_binding(
+ "selected-workspace",
+ GoalTargetBindingKind::Workspace,
+ b"bounded-workspace-identity",
+ );
+ let validated = store
+ .submit_goal_proposal(run.id, &proposal, &context)
+ .expect("goal validates");
+ store
+ .freeze_goal_envelope(run.id, validated.revision().expect("revision"))
+ .expect("goal freezes");
+ run.id
+ }
+
+ fn observation(
+ stage: GoalContinuationObservationStage,
+ request_id: Uuid,
+ observed_at: DateTime,
+ ) -> GoalContinuationObservation {
+ GoalContinuationObservation {
+ stage,
+ local_tool_round: u32::from(stage == GoalContinuationObservationStage::AfterToolRound),
+ model_usage: vec![GoalModelUsage {
+ request_id,
+ elapsed_ms: 10,
+ total_tokens: Some(20),
+ estimated_cost_micro_usd: Some(30),
+ }],
+ tool_usage: (stage == GoalContinuationObservationStage::AfterToolRound)
+ .then(|| {
+ vec![GoalToolUsage {
+ invocation_id: Uuid::new_v4(),
+ elapsed_ms: 5,
+ }]
+ })
+ .unwrap_or_default(),
+ observed_at,
+ }
+ }
+
+ #[test]
+ fn checkpoint_migrates_replays_idempotently_and_survives_restart() {
+ let root = tempfile::tempdir().expect("tempdir");
+ let path = root.path().join("events.sqlite3");
+ let store = EventStore::open(&path).expect("store opens");
+ let run_id = append_frozen_goal(&store);
+ store
+ .conn
+ .execute("DROP TABLE goal_context_checkpoints", [])
+ .expect("new table can be removed to model a legacy database");
+ drop(store);
+
+ let store = EventStore::open(&path).expect("migration recreates table");
+ let now = Utc::now();
+ let observation = observation(GoalContinuationObservationStage::Final, Uuid::new_v4(), now);
+ let first = store
+ .record_goal_context_checkpoint(run_id, observation.clone())
+ .expect("checkpoint records")
+ .expect("checkpoint exists");
+ let first_row_revision: u64 = store
+ .conn
+ .query_row(
+ "SELECT row_revision FROM goal_context_checkpoints WHERE run_id = ?1",
+ params![run_id.to_string()],
+ |row| row.get(0),
+ )
+ .unwrap();
+ let replayed = store
+ .record_goal_context_checkpoint(run_id, observation)
+ .expect("replay succeeds")
+ .expect("checkpoint exists");
+ assert_eq!(first, replayed);
+ assert_eq!(first_row_revision, 0);
+ drop(store);
+
+ let reopened = EventStore::open(&path).expect("store reopens");
+ assert_eq!(
+ reopened
+ .goal_context_checkpoint(run_id)
+ .expect("checkpoint loads"),
+ Some(first)
+ );
+ }
+
+ #[test]
+ fn checkpoint_tamper_fails_store_reopen() {
+ let root = tempfile::tempdir().expect("tempdir");
+ let path = root.path().join("events.sqlite3");
+ let store = EventStore::open(&path).expect("store opens");
+ let run_id = append_frozen_goal(&store);
+ store
+ .record_goal_context_checkpoint(
+ run_id,
+ observation(
+ GoalContinuationObservationStage::Final,
+ Uuid::new_v4(),
+ Utc::now(),
+ ),
+ )
+ .expect("checkpoint records");
+ drop(store);
+ Connection::open(&path)
+ .unwrap()
+ .execute(
+ "UPDATE goal_context_checkpoints SET checkpoint_json = '{}' WHERE run_id = ?1",
+ params![run_id.to_string()],
+ )
+ .unwrap();
+
+ assert!(EventStore::open(&path).is_err());
+ }
+
+ #[test]
+ fn no_evidence_checkpoint_blocks_run_completion_with_exact_reason() {
+ let store = Mutex::new(EventStore::open_memory().expect("store opens"));
+ let run_id = append_frozen_goal(&store.lock().unwrap());
+ let checkpoint = store
+ .lock()
+ .unwrap()
+ .record_goal_context_checkpoint(
+ run_id,
+ observation(
+ GoalContinuationObservationStage::AfterToolRound,
+ Uuid::new_v4(),
+ Utc::now(),
+ ),
+ )
+ .expect("checkpoint records")
+ .expect("checkpoint exists");
+
+ assert_eq!(
+ checkpoint.blocker.as_ref().map(|item| item.code),
+ Some(GoalContinuationBlockerCode::NoNewEvidence)
+ );
+ assert_eq!(
+ store
+ .lock()
+ .unwrap()
+ .classify_agent_run_completion(run_id)
+ .expect("completion classifies"),
+ super::super::AgentRunCompletionClassification::VerificationBlocked(
+ "goal_continuation_no_new_evidence".to_string()
+ )
+ );
+ }
+
+ #[test]
+ fn generic_event_append_cannot_mint_a_checkpoint() {
+ let store = EventStore::open_memory().expect("store opens");
+ let event = KernelEvent::new(CONTEXT_CHECKPOINT_RECORDED_EVENT, json!({"forged": true}))
+ .expect("event builds");
+ assert!(store.append(&event).is_err());
+ }
+}
diff --git a/apps/desktop/src-tauri/src/kernel/goal_continuation.rs b/apps/desktop/src-tauri/src/kernel/goal_continuation.rs
new file mode 100644
index 0000000..98fe8d6
--- /dev/null
+++ b/apps/desktop/src-tauri/src/kernel/goal_continuation.rs
@@ -0,0 +1,1175 @@
+use std::collections::BTreeSet;
+
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use uuid::Uuid;
+
+use crate::kernel::goal_lifecycle::{
+ GoalCompletionEvidenceReceipt, GoalCompletionProjection, GoalCompletionStatus,
+ GoalFrozenEnvelope, GoalLifecycleProjection,
+};
+
+pub const CONTEXT_CHECKPOINT_VERSION: &str = "ds-agent.context-checkpoint/v1";
+const GAP_FINGERPRINT_DOMAIN: &[u8] = b"ds-agent.goal-gap-fingerprint.v1\0";
+const GAP_SET_FINGERPRINT_DOMAIN: &[u8] = b"ds-agent.goal-gap-set-fingerprint.v1\0";
+const CHECKPOINT_FINGERPRINT_DOMAIN: &[u8] = b"ds-agent.context-checkpoint-fingerprint.v1\0";
+const IDENTITY_FINGERPRINT_DOMAIN: &[u8] = b"ds-agent.context-identity-fingerprint.v1\0";
+const TOOL_ROUND_FINGERPRINT_DOMAIN: &[u8] = b"ds-agent.context-tool-round-fingerprint.v1\0";
+const MAX_CHECKPOINT_IDENTITIES: usize = 1_024;
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub(crate) enum GoalContinuationObservationStage {
+ InitialModel,
+ AfterToolRound,
+ AfterModelFollowup,
+ Final,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub(crate) struct GoalModelUsage {
+ pub request_id: Uuid,
+ pub elapsed_ms: u64,
+ pub total_tokens: Option,
+ pub estimated_cost_micro_usd: Option,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub(crate) struct GoalToolUsage {
+ pub invocation_id: Uuid,
+ pub elapsed_ms: u64,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub(crate) struct GoalContinuationObservation {
+ pub stage: GoalContinuationObservationStage,
+ pub local_tool_round: u32,
+ pub model_usage: Vec,
+ pub tool_usage: Vec,
+ pub observed_at: DateTime,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct GoalGap {
+ pub code: String,
+ pub goal_revision: String,
+ pub frozen_fingerprint: String,
+ pub fingerprint: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct GoalLoopBudgetLimits {
+ pub max_model_rounds: u32,
+ pub max_tool_rounds: u32,
+ pub max_elapsed_ms: u64,
+ pub max_tokens: u64,
+ pub max_cost_micro_usd: u64,
+ pub max_consecutive_non_improvement: u32,
+}
+
+impl Default for GoalLoopBudgetLimits {
+ fn default() -> Self {
+ Self {
+ max_model_rounds: 5,
+ max_tool_rounds: 4,
+ max_elapsed_ms: 15 * 60 * 1_000,
+ max_tokens: 64_000,
+ max_cost_micro_usd: 5_000_000,
+ max_consecutive_non_improvement: 2,
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct GoalLoopBudgetState {
+ pub limits: GoalLoopBudgetLimits,
+ pub model_rounds: u32,
+ pub tool_rounds: u32,
+ pub elapsed_ms: u64,
+ pub tokens: u64,
+ pub cost_micro_usd: u64,
+ pub token_unknown_rounds: u32,
+ pub cost_unknown_rounds: u32,
+ pub evidence_total: u32,
+ pub new_evidence_count: u32,
+ pub consecutive_non_improvement: u32,
+ pub accounted_model_request_ids: Vec,
+ pub accounted_tool_invocation_ids: Vec,
+ pub accounted_tool_round_fingerprints: Vec,
+ pub accounted_evidence_ids: Vec,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum GoalContinuationBlockerCode {
+ ModelRoundBudgetExhausted,
+ ToolRoundBudgetExhausted,
+ ElapsedBudgetExhausted,
+ TokenBudgetExhausted,
+ CostBudgetExhausted,
+ NoNewEvidence,
+ RepeatedGaps,
+}
+
+impl GoalContinuationBlockerCode {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::ModelRoundBudgetExhausted => "model_round_budget_exhausted",
+ Self::ToolRoundBudgetExhausted => "tool_round_budget_exhausted",
+ Self::ElapsedBudgetExhausted => "elapsed_budget_exhausted",
+ Self::TokenBudgetExhausted => "token_budget_exhausted",
+ Self::CostBudgetExhausted => "cost_budget_exhausted",
+ Self::NoNewEvidence => "no_new_evidence",
+ Self::RepeatedGaps => "repeated_gaps",
+ }
+ }
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct GoalContinuationBlocker {
+ pub code: GoalContinuationBlockerCode,
+ pub gap_fingerprint: String,
+ pub model_rounds: u32,
+ pub tool_rounds: u32,
+ pub elapsed_ms: u64,
+ pub tokens: u64,
+ pub cost_micro_usd: u64,
+ pub evidence_total: u32,
+}
+
+impl GoalContinuationBlocker {
+ pub(crate) fn stable_reason(&self) -> String {
+ format!("goal_continuation_{}", self.code.as_str())
+ }
+
+ pub(crate) fn user_message(&self) -> String {
+ match self.code {
+ GoalContinuationBlockerCode::ModelRoundBudgetExhausted
+ | GoalContinuationBlockerCode::ToolRoundBudgetExhausted
+ | GoalContinuationBlockerCode::ElapsedBudgetExhausted
+ | GoalContinuationBlockerCode::TokenBudgetExhausted
+ | GoalContinuationBlockerCode::CostBudgetExhausted => {
+ "DS Agent 已达到本任务的安全预算上限,未继续执行新的动作。请检查现有证据和缺口后再决定是否创建新任务。".to_string()
+ }
+ GoalContinuationBlockerCode::NoNewEvidence => {
+ "DS Agent 本轮没有获得新的完成证据,已停止继续尝试,避免无证据循环。".to_string()
+ }
+ GoalContinuationBlockerCode::RepeatedGaps => {
+ "DS Agent 检测到相同完成缺口连续未改善,已停止重复尝试并保留现有证据。".to_string()
+ }
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ContextCheckpointStatus {
+ Continue,
+ Complete,
+ Blocked,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct ContextAuthorizationIdentity {
+ pub group_id: Uuid,
+ pub task_id: Uuid,
+ pub projection_revision: u64,
+ pub manifest_revision: String,
+ pub manifest_fingerprint: String,
+ pub preview_hash: String,
+ pub status: String,
+ pub capability_request_fingerprints: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct ContextResourceIdentity {
+ pub claim_id: Uuid,
+ pub tool_invocation_id: Uuid,
+ pub access: String,
+ pub resource_key_fingerprint: String,
+ pub lease_expires_at: DateTime,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct ContextArtifactIdentity {
+ pub artifact_id: String,
+ pub kind: String,
+ pub identity_fingerprint: String,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct ContextSourceIdentity {
+ pub invocation_id: Uuid,
+ pub tool_id: String,
+ pub tool_version: String,
+ pub request_fingerprint: String,
+ pub source_fingerprint: String,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct ContextCheckpointSeed {
+ pub run_id: Uuid,
+ pub goal: GoalFrozenEnvelope,
+ pub completion: GoalCompletionProjection,
+ pub authorizations: Vec,
+ pub resources: Vec,
+ pub artifacts: Vec,
+ pub sources: Vec,
+}
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct ContextCheckpoint {
+ pub version: String,
+ pub run_id: Uuid,
+ pub goal: GoalFrozenEnvelope,
+ pub constraints: Vec,
+ pub authorizations: Vec,
+ pub evidence: Vec,
+ pub gaps: Vec,
+ pub gap_fingerprint: String,
+ pub resources: Vec,
+ pub artifacts: Vec,
+ pub sources: Vec,
+ pub budget: GoalLoopBudgetState,
+ pub status: ContextCheckpointStatus,
+ pub blocker: Option,
+ pub fingerprint: String,
+ pub created_at: DateTime,
+ pub updated_at: DateTime,
+}
+
+#[derive(Serialize)]
+struct GoalGapCanonical<'a> {
+ code: &'a str,
+ goal_revision: &'a str,
+ frozen_fingerprint: &'a str,
+}
+
+#[derive(Serialize)]
+struct GoalGapSetCanonical<'a> {
+ goal_revision: &'a str,
+ frozen_fingerprint: &'a str,
+ gap_fingerprints: Vec<&'a str>,
+}
+
+#[derive(Serialize)]
+struct ContextCheckpointCanonical<'a> {
+ version: &'a str,
+ run_id: Uuid,
+ goal: &'a GoalFrozenEnvelope,
+ constraints: &'a [String],
+ authorizations: &'a [ContextAuthorizationIdentity],
+ evidence: &'a [GoalCompletionEvidenceReceipt],
+ gaps: &'a [GoalGap],
+ gap_fingerprint: &'a str,
+ resources: &'a [ContextResourceIdentity],
+ artifacts: &'a [ContextArtifactIdentity],
+ sources: &'a [ContextSourceIdentity],
+ budget: &'a GoalLoopBudgetState,
+ status: ContextCheckpointStatus,
+ blocker: &'a Option,
+}
+
+#[derive(Serialize)]
+struct ContextCheckpointPrompt<'a> {
+ version: &'a str,
+ goal_id: Uuid,
+ goal_revision: &'a str,
+ frozen_fingerprint: &'a str,
+ user_goal: &'a str,
+ constraints: &'a [String],
+ done_when: Vec<(&'a str, &'a str)>,
+ authorizations: &'a [ContextAuthorizationIdentity],
+ evidence: &'a [GoalCompletionEvidenceReceipt],
+ gaps: &'a [GoalGap],
+ gap_fingerprint: &'a str,
+ resources: &'a [ContextResourceIdentity],
+ artifacts: &'a [ContextArtifactIdentity],
+ sources: &'a [ContextSourceIdentity],
+ budget: &'a GoalLoopBudgetState,
+ status: ContextCheckpointStatus,
+ blocker: &'a Option,
+ checkpoint_fingerprint: &'a str,
+}
+
+impl ContextCheckpoint {
+ pub(crate) fn advance(
+ previous: Option<&Self>,
+ mut seed: ContextCheckpointSeed,
+ mut observation: GoalContinuationObservation,
+ ) -> Result {
+ if let Some(previous) = previous {
+ previous.validate()?;
+ }
+ if seed.run_id.is_nil()
+ || seed.run_id != seed.completion.goal_id
+ || seed.goal.revision != seed.completion.revision
+ || seed.goal.fingerprint != seed.completion.frozen_fingerprint
+ {
+ return Err("context_checkpoint_goal_binding_invalid");
+ }
+ normalize_seed(&mut seed)?;
+ normalize_observation(&mut observation)?;
+
+ let gaps = gaps_from_completion(&seed.goal, &seed.completion)?;
+ let gap_fingerprint = gap_set_fingerprint(&seed.goal, &gaps)?;
+ let mut budget = previous
+ .map(|value| value.budget.clone())
+ .unwrap_or_default();
+ let previous_gap_fingerprint = previous.map(|value| value.gap_fingerprint.as_str());
+ let existing_blocker = previous.and_then(|value| value.blocker.clone());
+ let created_at = previous
+ .map(|value| value.created_at)
+ .unwrap_or(observation.observed_at);
+
+ account_model_usage(&mut budget, &observation.model_usage);
+ account_tool_usage(&mut budget, &observation.tool_usage);
+ let new_evidence_count = account_evidence(&mut budget, &seed.completion.evidence);
+ let new_tool_round = if observation.stage
+ == GoalContinuationObservationStage::AfterToolRound
+ && !observation.tool_usage.is_empty()
+ {
+ account_tool_round(&mut budget, seed.run_id, &seed.goal, &observation)?
+ } else {
+ false
+ };
+ budget.new_evidence_count = new_evidence_count;
+ budget.evidence_total = u32::try_from(budget.accounted_evidence_ids.len())
+ .map_err(|_| "context_checkpoint_evidence_limit")?;
+
+ if new_tool_round {
+ if previous_gap_fingerprint == Some(gap_fingerprint.as_str()) && new_evidence_count == 0
+ {
+ budget.consecutive_non_improvement = budget
+ .consecutive_non_improvement
+ .checked_add(1)
+ .ok_or("context_checkpoint_counter_overflow")?;
+ } else {
+ budget.consecutive_non_improvement = 0;
+ }
+ }
+
+ let blocker = existing_blocker.or_else(|| {
+ blocker_for(
+ seed.completion.status,
+ &gap_fingerprint,
+ &budget,
+ new_tool_round,
+ )
+ });
+ let status = if blocker.is_some() {
+ ContextCheckpointStatus::Blocked
+ } else if seed.completion.status == GoalCompletionStatus::Complete {
+ ContextCheckpointStatus::Complete
+ } else {
+ ContextCheckpointStatus::Continue
+ };
+ let constraints = seed.goal.envelope.constraints.clone();
+ let mut checkpoint = Self {
+ version: CONTEXT_CHECKPOINT_VERSION.to_string(),
+ run_id: seed.run_id,
+ goal: seed.goal,
+ constraints,
+ authorizations: seed.authorizations,
+ evidence: seed.completion.evidence,
+ gaps,
+ gap_fingerprint,
+ resources: seed.resources,
+ artifacts: seed.artifacts,
+ sources: seed.sources,
+ budget,
+ status,
+ blocker,
+ fingerprint: String::new(),
+ created_at,
+ updated_at: observation.observed_at,
+ };
+ checkpoint.fingerprint = checkpoint.recompute_fingerprint()?;
+ checkpoint.validate()?;
+ if let Some(previous) = previous {
+ checkpoint.validate_monotonic(previous)?;
+ }
+ Ok(checkpoint)
+ }
+
+ pub(crate) fn validate(&self) -> Result<(), &'static str> {
+ if self.version != CONTEXT_CHECKPOINT_VERSION
+ || self.run_id.is_nil()
+ || self.run_id
+ != self
+ .evidence
+ .first()
+ .map_or(self.run_id, |item| item.goal_id)
+ || self.goal.revision.is_empty()
+ || !valid_hash(&self.goal.revision)
+ || !valid_hash(&self.goal.fingerprint)
+ || self.constraints != self.goal.envelope.constraints
+ || self.updated_at < self.created_at
+ || self.authorizations.len() > MAX_CHECKPOINT_IDENTITIES
+ || self.evidence.len() > MAX_CHECKPOINT_IDENTITIES
+ || self.resources.len() > MAX_CHECKPOINT_IDENTITIES
+ || self.artifacts.len() > MAX_CHECKPOINT_IDENTITIES
+ || self.sources.len() > MAX_CHECKPOINT_IDENTITIES
+ {
+ return Err("context_checkpoint_invalid");
+ }
+ for gap in &self.gaps {
+ if gap.goal_revision != self.goal.revision
+ || gap.frozen_fingerprint != self.goal.fingerprint
+ || gap.fingerprint != gap_fingerprint(gap)?
+ || !safe_code(&gap.code)
+ {
+ return Err("context_checkpoint_gap_invalid");
+ }
+ }
+ if self.gap_fingerprint != gap_set_fingerprint(&self.goal, &self.gaps)?
+ || self
+ .authorizations
+ .iter()
+ .any(|item| !authorization_identity_valid(item, self.run_id))
+ || self.resources.iter().any(|item| {
+ item.claim_id.is_nil()
+ || item.tool_invocation_id.is_nil()
+ || !matches!(item.access.as_str(), "read" | "write")
+ || !valid_hash(&item.resource_key_fingerprint)
+ })
+ || self.artifacts.iter().any(|item| {
+ !safe_code(&item.artifact_id)
+ || !safe_code(&item.kind)
+ || !valid_hash(&item.identity_fingerprint)
+ })
+ || self.sources.iter().any(|item| {
+ item.invocation_id.is_nil()
+ || !safe_code(&item.tool_id)
+ || !safe_code(&item.tool_version)
+ || !valid_hash(&item.request_fingerprint)
+ || !valid_hash(&item.source_fingerprint)
+ })
+ || !budget_valid(&self.budget)
+ {
+ return Err("context_checkpoint_identity_invalid");
+ }
+ match (self.status, self.blocker.as_ref()) {
+ (ContextCheckpointStatus::Blocked, Some(blocker))
+ if blocker.gap_fingerprint == self.gap_fingerprint => {}
+ (ContextCheckpointStatus::Complete, None) if self.gaps.is_empty() => {}
+ (ContextCheckpointStatus::Continue, None) if !self.gaps.is_empty() => {}
+ _ => return Err("context_checkpoint_status_invalid"),
+ }
+ if self.fingerprint != self.recompute_fingerprint()? {
+ return Err("context_checkpoint_fingerprint_invalid");
+ }
+ Ok(())
+ }
+
+ pub(crate) fn validate_against_goal(
+ &self,
+ lifecycle: &GoalLifecycleProjection,
+ ) -> Result<(), &'static str> {
+ self.validate()?;
+ let frozen = lifecycle
+ .frozen()
+ .ok_or("context_checkpoint_goal_not_frozen")?;
+ if lifecycle.goal_id != self.run_id || frozen != &self.goal {
+ return Err("context_checkpoint_goal_drift");
+ }
+ Ok(())
+ }
+
+ pub(crate) fn blocker_reason(&self) -> Option {
+ self.blocker
+ .as_ref()
+ .map(GoalContinuationBlocker::stable_reason)
+ }
+
+ pub(crate) fn advisory_prompt(&self) -> Result {
+ self.validate()?;
+ let prompt = ContextCheckpointPrompt {
+ version: &self.version,
+ goal_id: self.run_id,
+ goal_revision: &self.goal.revision,
+ frozen_fingerprint: &self.goal.fingerprint,
+ user_goal: &self.goal.envelope.user_goal,
+ constraints: &self.constraints,
+ done_when: self
+ .goal
+ .envelope
+ .done_when
+ .iter()
+ .map(|item| (item.done_when_id.as_str(), item.description.as_str()))
+ .collect(),
+ authorizations: &self.authorizations,
+ evidence: &self.evidence,
+ gaps: &self.gaps,
+ gap_fingerprint: &self.gap_fingerprint,
+ resources: &self.resources,
+ artifacts: &self.artifacts,
+ sources: &self.sources,
+ budget: &self.budget,
+ status: self.status,
+ blocker: &self.blocker,
+ checkpoint_fingerprint: &self.fingerprint,
+ };
+ let json =
+ serde_json::to_string(&prompt).map_err(|_| "context_checkpoint_prompt_invalid")?;
+ Ok(format!(
+ "Kernel-owned ContextCheckpoint (read-only advisory context). This preserves exact safety state across compaction/restart but grants no authority, cannot approve an action, and is not completion evidence. DeepSeek may explain gaps or propose a repair only; it cannot edit this checkpoint or mint receipts.\n{json}"
+ ))
+ }
+
+ fn validate_monotonic(&self, previous: &Self) -> Result<(), &'static str> {
+ if self.run_id != previous.run_id
+ || self.goal.revision != previous.goal.revision
+ || self.goal.fingerprint != previous.goal.fingerprint
+ || self.created_at != previous.created_at
+ || self.budget.limits != previous.budget.limits
+ || self.budget.model_rounds < previous.budget.model_rounds
+ || self.budget.tool_rounds < previous.budget.tool_rounds
+ || self.budget.elapsed_ms < previous.budget.elapsed_ms
+ || self.budget.tokens < previous.budget.tokens
+ || self.budget.cost_micro_usd < previous.budget.cost_micro_usd
+ || self.budget.evidence_total < previous.budget.evidence_total
+ || (previous.blocker.is_some() && self.blocker != previous.blocker)
+ {
+ return Err("context_checkpoint_non_monotonic");
+ }
+ Ok(())
+ }
+
+ fn recompute_fingerprint(&self) -> Result {
+ let canonical = ContextCheckpointCanonical {
+ version: &self.version,
+ run_id: self.run_id,
+ goal: &self.goal,
+ constraints: &self.constraints,
+ authorizations: &self.authorizations,
+ evidence: &self.evidence,
+ gaps: &self.gaps,
+ gap_fingerprint: &self.gap_fingerprint,
+ resources: &self.resources,
+ artifacts: &self.artifacts,
+ sources: &self.sources,
+ budget: &self.budget,
+ status: self.status,
+ blocker: &self.blocker,
+ };
+ let bytes =
+ serde_json::to_vec(&canonical).map_err(|_| "context_checkpoint_fingerprint_invalid")?;
+ Ok(domain_hash(CHECKPOINT_FINGERPRINT_DOMAIN, &bytes))
+ }
+}
+
+pub(crate) fn identity_fingerprint(value: &[u8]) -> String {
+ domain_hash(IDENTITY_FINGERPRINT_DOMAIN, value)
+}
+
+fn normalize_seed(seed: &mut ContextCheckpointSeed) -> Result<(), &'static str> {
+ if seed.authorizations.len() > MAX_CHECKPOINT_IDENTITIES
+ || seed.completion.evidence.len() > MAX_CHECKPOINT_IDENTITIES
+ || seed.resources.len() > MAX_CHECKPOINT_IDENTITIES
+ || seed.artifacts.len() > MAX_CHECKPOINT_IDENTITIES
+ || seed.sources.len() > MAX_CHECKPOINT_IDENTITIES
+ {
+ return Err("context_checkpoint_identity_limit");
+ }
+ seed.authorizations.sort_by_key(|item| item.group_id);
+ seed.authorizations.dedup_by_key(|item| item.group_id);
+ for item in &mut seed.authorizations {
+ item.capability_request_fingerprints.sort();
+ item.capability_request_fingerprints.dedup();
+ }
+ seed.resources.sort_by_key(|item| item.claim_id);
+ seed.resources.dedup_by_key(|item| item.claim_id);
+ seed.artifacts.sort_by(|left, right| {
+ (&left.artifact_id, &left.kind, &left.identity_fingerprint).cmp(&(
+ &right.artifact_id,
+ &right.kind,
+ &right.identity_fingerprint,
+ ))
+ });
+ seed.artifacts.dedup();
+ seed.sources.sort_by_key(|item| item.invocation_id);
+ seed.sources.dedup_by_key(|item| item.invocation_id);
+ Ok(())
+}
+
+fn normalize_observation(
+ observation: &mut GoalContinuationObservation,
+) -> Result<(), &'static str> {
+ if observation.model_usage.len() > MAX_CHECKPOINT_IDENTITIES
+ || observation.tool_usage.len() > MAX_CHECKPOINT_IDENTITIES
+ {
+ return Err("context_checkpoint_observation_limit");
+ }
+ observation.model_usage.sort_by_key(|item| item.request_id);
+ observation.model_usage.dedup_by_key(|item| item.request_id);
+ observation
+ .tool_usage
+ .sort_by_key(|item| item.invocation_id);
+ observation
+ .tool_usage
+ .dedup_by_key(|item| item.invocation_id);
+ Ok(())
+}
+
+fn gaps_from_completion(
+ goal: &GoalFrozenEnvelope,
+ completion: &GoalCompletionProjection,
+) -> Result, &'static str> {
+ let mut codes = completion
+ .failure_codes
+ .iter()
+ .map(|code| {
+ serde_json::to_value(code)
+ .ok()
+ .and_then(|value| value.as_str().map(str::to_string))
+ .ok_or("context_checkpoint_gap_invalid")
+ })
+ .collect::, _>>()?;
+ codes.sort();
+ codes.dedup();
+ codes
+ .into_iter()
+ .map(|code| {
+ let mut gap = GoalGap {
+ code,
+ goal_revision: goal.revision.clone(),
+ frozen_fingerprint: goal.fingerprint.clone(),
+ fingerprint: String::new(),
+ };
+ gap.fingerprint = gap_fingerprint(&gap)?;
+ Ok(gap)
+ })
+ .collect()
+}
+
+fn gap_fingerprint(gap: &GoalGap) -> Result {
+ if !safe_code(&gap.code)
+ || !valid_hash(&gap.goal_revision)
+ || !valid_hash(&gap.frozen_fingerprint)
+ {
+ return Err("context_checkpoint_gap_invalid");
+ }
+ let bytes = serde_json::to_vec(&GoalGapCanonical {
+ code: &gap.code,
+ goal_revision: &gap.goal_revision,
+ frozen_fingerprint: &gap.frozen_fingerprint,
+ })
+ .map_err(|_| "context_checkpoint_gap_invalid")?;
+ Ok(domain_hash(GAP_FINGERPRINT_DOMAIN, &bytes))
+}
+
+fn gap_set_fingerprint(
+ goal: &GoalFrozenEnvelope,
+ gaps: &[GoalGap],
+) -> Result {
+ let bytes = serde_json::to_vec(&GoalGapSetCanonical {
+ goal_revision: &goal.revision,
+ frozen_fingerprint: &goal.fingerprint,
+ gap_fingerprints: gaps.iter().map(|gap| gap.fingerprint.as_str()).collect(),
+ })
+ .map_err(|_| "context_checkpoint_gap_invalid")?;
+ Ok(domain_hash(GAP_SET_FINGERPRINT_DOMAIN, &bytes))
+}
+
+fn account_model_usage(budget: &mut GoalLoopBudgetState, usage: &[GoalModelUsage]) {
+ let mut accounted = budget
+ .accounted_model_request_ids
+ .iter()
+ .copied()
+ .collect::>();
+ for item in usage {
+ if item.request_id.is_nil() || !accounted.insert(item.request_id) {
+ continue;
+ }
+ budget.model_rounds = budget.model_rounds.saturating_add(1);
+ budget.elapsed_ms = budget.elapsed_ms.saturating_add(item.elapsed_ms);
+ match item.total_tokens {
+ Some(tokens) => budget.tokens = budget.tokens.saturating_add(u64::from(tokens)),
+ None => budget.token_unknown_rounds = budget.token_unknown_rounds.saturating_add(1),
+ }
+ match item.estimated_cost_micro_usd {
+ Some(cost) => budget.cost_micro_usd = budget.cost_micro_usd.saturating_add(cost),
+ None => budget.cost_unknown_rounds = budget.cost_unknown_rounds.saturating_add(1),
+ }
+ budget.accounted_model_request_ids.push(item.request_id);
+ }
+ budget.accounted_model_request_ids.sort();
+}
+
+fn account_tool_usage(budget: &mut GoalLoopBudgetState, usage: &[GoalToolUsage]) {
+ let mut accounted = budget
+ .accounted_tool_invocation_ids
+ .iter()
+ .copied()
+ .collect::>();
+ for item in usage {
+ if item.invocation_id.is_nil() || !accounted.insert(item.invocation_id) {
+ continue;
+ }
+ budget.elapsed_ms = budget.elapsed_ms.saturating_add(item.elapsed_ms);
+ budget
+ .accounted_tool_invocation_ids
+ .push(item.invocation_id);
+ }
+ budget.accounted_tool_invocation_ids.sort();
+}
+
+fn account_evidence(
+ budget: &mut GoalLoopBudgetState,
+ evidence: &[GoalCompletionEvidenceReceipt],
+) -> u32 {
+ let mut accounted = budget
+ .accounted_evidence_ids
+ .iter()
+ .copied()
+ .collect::>();
+ let mut added = 0_u32;
+ for item in evidence {
+ if item.evidence_id.is_nil() || !accounted.insert(item.evidence_id) {
+ continue;
+ }
+ added = added.saturating_add(1);
+ budget.accounted_evidence_ids.push(item.evidence_id);
+ }
+ budget.accounted_evidence_ids.sort();
+ added
+}
+
+fn account_tool_round(
+ budget: &mut GoalLoopBudgetState,
+ run_id: Uuid,
+ goal: &GoalFrozenEnvelope,
+ observation: &GoalContinuationObservation,
+) -> Result {
+ let mut model_ids = observation
+ .model_usage
+ .iter()
+ .map(|item| item.request_id)
+ .collect::>();
+ model_ids.sort();
+ let mut tool_ids = observation
+ .tool_usage
+ .iter()
+ .map(|item| item.invocation_id)
+ .collect::>();
+ tool_ids.sort();
+ let bytes = serde_json::to_vec(&(
+ run_id,
+ &goal.revision,
+ &goal.fingerprint,
+ observation.local_tool_round,
+ model_ids,
+ tool_ids,
+ ))
+ .map_err(|_| "context_checkpoint_round_invalid")?;
+ let fingerprint = domain_hash(TOOL_ROUND_FINGERPRINT_DOMAIN, &bytes);
+ if budget
+ .accounted_tool_round_fingerprints
+ .iter()
+ .any(|item| item == &fingerprint)
+ {
+ return Ok(false);
+ }
+ budget.tool_rounds = budget
+ .tool_rounds
+ .checked_add(1)
+ .ok_or("context_checkpoint_counter_overflow")?;
+ budget.accounted_tool_round_fingerprints.push(fingerprint);
+ budget.accounted_tool_round_fingerprints.sort();
+ Ok(true)
+}
+
+fn blocker_for(
+ completion_status: GoalCompletionStatus,
+ gap_fingerprint: &str,
+ budget: &GoalLoopBudgetState,
+ new_tool_round: bool,
+) -> Option {
+ if completion_status == GoalCompletionStatus::Complete {
+ return None;
+ }
+ let code = if budget.model_rounds >= budget.limits.max_model_rounds {
+ Some(GoalContinuationBlockerCode::ModelRoundBudgetExhausted)
+ } else if budget.tool_rounds >= budget.limits.max_tool_rounds {
+ Some(GoalContinuationBlockerCode::ToolRoundBudgetExhausted)
+ } else if budget.elapsed_ms >= budget.limits.max_elapsed_ms {
+ Some(GoalContinuationBlockerCode::ElapsedBudgetExhausted)
+ } else if budget.tokens >= budget.limits.max_tokens {
+ Some(GoalContinuationBlockerCode::TokenBudgetExhausted)
+ } else if budget.cost_micro_usd >= budget.limits.max_cost_micro_usd {
+ Some(GoalContinuationBlockerCode::CostBudgetExhausted)
+ } else if new_tool_round && budget.evidence_total == 0 {
+ Some(GoalContinuationBlockerCode::NoNewEvidence)
+ } else if new_tool_round
+ && budget.consecutive_non_improvement >= budget.limits.max_consecutive_non_improvement
+ {
+ Some(GoalContinuationBlockerCode::RepeatedGaps)
+ } else {
+ None
+ }?;
+ Some(GoalContinuationBlocker {
+ code,
+ gap_fingerprint: gap_fingerprint.to_string(),
+ model_rounds: budget.model_rounds,
+ tool_rounds: budget.tool_rounds,
+ elapsed_ms: budget.elapsed_ms,
+ tokens: budget.tokens,
+ cost_micro_usd: budget.cost_micro_usd,
+ evidence_total: budget.evidence_total,
+ })
+}
+
+fn authorization_identity_valid(item: &ContextAuthorizationIdentity, run_id: Uuid) -> bool {
+ item.group_id != Uuid::nil()
+ && item.task_id == run_id
+ && valid_hash(&item.manifest_revision)
+ && valid_hash(&item.manifest_fingerprint)
+ && valid_hash(&item.preview_hash)
+ && safe_code(&item.status)
+ && item
+ .capability_request_fingerprints
+ .iter()
+ .all(|fingerprint| valid_hash(fingerprint))
+}
+
+fn budget_valid(budget: &GoalLoopBudgetState) -> bool {
+ let limits = &budget.limits;
+ limits.max_model_rounds > 0
+ && limits.max_tool_rounds > 0
+ && limits.max_elapsed_ms > 0
+ && limits.max_tokens > 0
+ && limits.max_cost_micro_usd > 0
+ && limits.max_consecutive_non_improvement > 0
+ && budget.model_rounds
+ == u32::try_from(budget.accounted_model_request_ids.len()).unwrap_or(u32::MAX)
+ && budget.tool_rounds
+ == u32::try_from(budget.accounted_tool_round_fingerprints.len()).unwrap_or(u32::MAX)
+ && budget.evidence_total
+ == u32::try_from(budget.accounted_evidence_ids.len()).unwrap_or(u32::MAX)
+ && all_unique(&budget.accounted_model_request_ids)
+ && all_unique(&budget.accounted_tool_invocation_ids)
+ && all_unique(&budget.accounted_evidence_ids)
+ && all_unique(&budget.accounted_tool_round_fingerprints)
+ && budget
+ .accounted_tool_round_fingerprints
+ .iter()
+ .all(|fingerprint| valid_hash(fingerprint))
+}
+
+fn all_unique(values: &[T]) -> bool {
+ values.iter().cloned().collect::>().len() == values.len()
+}
+
+fn safe_code(value: &str) -> bool {
+ let value = value.trim();
+ !value.is_empty()
+ && value.len() <= 160
+ && value
+ .bytes()
+ .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
+}
+
+fn valid_hash(value: &str) -> bool {
+ value.len() == 64
+ && value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
+}
+
+fn domain_hash(domain: &[u8], value: &[u8]) -> String {
+ let mut digest = Sha256::new();
+ digest.update(domain);
+ digest.update((value.len() as u64).to_be_bytes());
+ digest.update(value);
+ format!("{:x}", digest.finalize())
+}
+
+#[cfg(test)]
+mod tests {
+ use chrono::{Duration, Utc};
+ use serde_json::json;
+
+ use super::*;
+ use crate::kernel::event_store::EventStore;
+ use crate::kernel::goal_envelope::GOAL_ENVELOPE_PROPOSAL_VERSION;
+ use crate::kernel::goal_lifecycle::{
+ completion_projection, GoalCompletionEvidenceReceipt, GoalValidationContext,
+ GoalVerifierEvidenceStatus,
+ };
+ use crate::kernel::local_directory::WorkspaceReadinessCode;
+ use crate::kernel::models::AccessMode;
+ use crate::kernel::tool_runtime::FILE_READ_TOOL_ID;
+
+ fn seed() -> ContextCheckpointSeed {
+ let store = EventStore::open_memory().expect("store opens");
+ let run_id = Uuid::new_v4();
+ let proposal = crate::kernel::goal_envelope::GoalEnvelopeProposal::parse_value(json!({
+ "version": GOAL_ENVELOPE_PROPOSAL_VERSION,
+ "user_goal": "Create one verified brief.",
+ "assumptions": [],
+ "constraints": ["Keep output inside the approved workspace."],
+ "done_when": [{"done_when_id":"brief-ready","description":"The brief is verified."}],
+ "required_artifacts": [{"artifact_id":"brief","description":"The brief."}],
+ "verifiers": [{"verifier_id":"brief-verifier","done_when_id":"brief-ready","description":"Verify the brief.","evidence_kind":"brief-evidence"}],
+ "proposed_capabilities": [FILE_READ_TOOL_ID],
+ "external_targets": [{"target_id":"selected-workspace","description":"Bound locally."}],
+ "stop_conditions": ["Stop without evidence."]
+ }))
+ .expect("proposal parses");
+ let context =
+ GoalValidationContext::new(AccessMode::FullAccess, WorkspaceReadinessCode::Ready)
+ .with_enabled_tool(FILE_READ_TOOL_ID, true)
+ .with_verifier_kind("brief-evidence")
+ .with_target_binding(
+ "selected-workspace",
+ crate::kernel::goal_lifecycle::GoalTargetBindingKind::Workspace,
+ b"bounded-workspace-identity",
+ );
+ let validated = store
+ .submit_goal_proposal(run_id, &proposal, &context)
+ .expect("goal validates");
+ let lifecycle = store
+ .freeze_goal_envelope(run_id, validated.revision().expect("revision"))
+ .expect("goal freezes");
+ let goal = lifecycle.frozen().expect("frozen goal").clone();
+ let completion = completion_projection(&lifecycle, &[]).expect("projection builds");
+ ContextCheckpointSeed {
+ run_id,
+ goal,
+ completion,
+ authorizations: Vec::new(),
+ resources: Vec::new(),
+ artifacts: Vec::new(),
+ sources: Vec::new(),
+ }
+ }
+
+ fn observation(
+ stage: GoalContinuationObservationStage,
+ request_id: Uuid,
+ tool_id: Option,
+ observed_at: DateTime,
+ ) -> GoalContinuationObservation {
+ GoalContinuationObservation {
+ stage,
+ local_tool_round: u32::from(tool_id.is_some()),
+ model_usage: vec![GoalModelUsage {
+ request_id,
+ elapsed_ms: 10,
+ total_tokens: Some(20),
+ estimated_cost_micro_usd: Some(30),
+ }],
+ tool_usage: tool_id
+ .map(|invocation_id| {
+ vec![GoalToolUsage {
+ invocation_id,
+ elapsed_ms: 5,
+ }]
+ })
+ .unwrap_or_default(),
+ observed_at,
+ }
+ }
+
+ #[test]
+ fn gaps_are_stable_secret_free_and_revision_bound() {
+ let seed = seed();
+ let checkpoint = ContextCheckpoint::advance(
+ None,
+ seed,
+ observation(
+ GoalContinuationObservationStage::Final,
+ Uuid::new_v4(),
+ None,
+ Utc::now(),
+ ),
+ )
+ .expect("checkpoint builds");
+
+ assert_eq!(checkpoint.gaps.len(), 2);
+ assert!(checkpoint
+ .gaps
+ .iter()
+ .all(|gap| gap.code.starts_with("missing_")));
+ assert!(!serde_json::to_string(&checkpoint.gaps)
+ .unwrap()
+ .contains("bounded-workspace-identity"));
+ assert!(checkpoint
+ .gaps
+ .iter()
+ .all(|gap| gap.goal_revision == checkpoint.goal.revision));
+ }
+
+ #[test]
+ fn no_evidence_and_budget_exhaustion_become_deterministic_blockers() {
+ let now = Utc::now();
+ let no_evidence = ContextCheckpoint::advance(
+ None,
+ seed(),
+ observation(
+ GoalContinuationObservationStage::AfterToolRound,
+ Uuid::new_v4(),
+ Some(Uuid::new_v4()),
+ now,
+ ),
+ )
+ .expect("checkpoint builds");
+ assert_eq!(
+ no_evidence.blocker.as_ref().map(|item| item.code),
+ Some(GoalContinuationBlockerCode::NoNewEvidence)
+ );
+
+ let budget_seed = seed();
+ let initial = ContextCheckpoint::advance(
+ None,
+ budget_seed.clone(),
+ observation(
+ GoalContinuationObservationStage::InitialModel,
+ Uuid::new_v4(),
+ None,
+ now,
+ ),
+ )
+ .expect("checkpoint builds");
+ let mut exhausting_observation = observation(
+ GoalContinuationObservationStage::AfterModelFollowup,
+ Uuid::new_v4(),
+ None,
+ now + Duration::seconds(1),
+ );
+ exhausting_observation.model_usage[0].total_tokens = Some(64_000);
+ let exhausted =
+ ContextCheckpoint::advance(Some(&initial), budget_seed, exhausting_observation)
+ .expect("checkpoint advances");
+ assert_eq!(
+ exhausted.blocker.as_ref().map(|item| item.code),
+ Some(GoalContinuationBlockerCode::TokenBudgetExhausted)
+ );
+ }
+
+ #[test]
+ fn repeated_unchanged_gaps_become_a_deterministic_blocker() {
+ let now = Utc::now();
+ let mut stable_seed = seed();
+ stable_seed
+ .completion
+ .evidence
+ .push(GoalCompletionEvidenceReceipt {
+ evidence_id: Uuid::new_v4(),
+ goal_id: stable_seed.run_id,
+ revision: stable_seed.goal.revision.clone(),
+ frozen_fingerprint: stable_seed.goal.fingerprint.clone(),
+ verifier_id: "brief-verifier".to_string(),
+ done_when_id: "brief-ready".to_string(),
+ evidence_kind: "brief-evidence".to_string(),
+ artifact_ids: vec!["brief".to_string()],
+ status: GoalVerifierEvidenceStatus::Passed,
+ source_fingerprint: "3".repeat(64),
+ });
+
+ let first = ContextCheckpoint::advance(
+ None,
+ stable_seed.clone(),
+ observation(
+ GoalContinuationObservationStage::AfterToolRound,
+ Uuid::new_v4(),
+ Some(Uuid::new_v4()),
+ now,
+ ),
+ )
+ .expect("first checkpoint builds");
+ let second = ContextCheckpoint::advance(
+ Some(&first),
+ stable_seed.clone(),
+ observation(
+ GoalContinuationObservationStage::AfterToolRound,
+ Uuid::new_v4(),
+ Some(Uuid::new_v4()),
+ now + Duration::seconds(1),
+ ),
+ )
+ .expect("second checkpoint builds");
+ let blocked = ContextCheckpoint::advance(
+ Some(&second),
+ stable_seed,
+ observation(
+ GoalContinuationObservationStage::AfterToolRound,
+ Uuid::new_v4(),
+ Some(Uuid::new_v4()),
+ now + Duration::seconds(2),
+ ),
+ )
+ .expect("third checkpoint builds");
+
+ assert_eq!(blocked.budget.consecutive_non_improvement, 2);
+ assert_eq!(
+ blocked.blocker.as_ref().map(|item| item.code),
+ Some(GoalContinuationBlockerCode::RepeatedGaps)
+ );
+ }
+
+ #[test]
+ fn checkpoint_prompt_preserves_safety_state_without_minting_authority() {
+ let mut seed = seed();
+ seed.authorizations.push(ContextAuthorizationIdentity {
+ group_id: Uuid::new_v4(),
+ task_id: seed.run_id,
+ projection_revision: 7,
+ manifest_revision: "a".repeat(64),
+ manifest_fingerprint: "b".repeat(64),
+ preview_hash: "c".repeat(64),
+ status: "approved".to_string(),
+ capability_request_fingerprints: vec!["d".repeat(64)],
+ });
+ seed.resources.push(ContextResourceIdentity {
+ claim_id: Uuid::new_v4(),
+ tool_invocation_id: Uuid::new_v4(),
+ access: "write".to_string(),
+ resource_key_fingerprint: "e".repeat(64),
+ lease_expires_at: Utc::now() + Duration::minutes(5),
+ });
+ seed.artifacts.push(ContextArtifactIdentity {
+ artifact_id: "brief".to_string(),
+ kind: "pptx".to_string(),
+ identity_fingerprint: "f".repeat(64),
+ });
+ seed.sources.push(ContextSourceIdentity {
+ invocation_id: Uuid::new_v4(),
+ tool_id: FILE_READ_TOOL_ID.to_string(),
+ tool_version: "1".to_string(),
+ request_fingerprint: "1".repeat(64),
+ source_fingerprint: "2".repeat(64),
+ });
+ let checkpoint = ContextCheckpoint::advance(
+ None,
+ seed,
+ observation(
+ GoalContinuationObservationStage::Final,
+ Uuid::new_v4(),
+ None,
+ Utc::now(),
+ ),
+ )
+ .expect("checkpoint builds");
+ let prompt = checkpoint.advisory_prompt().expect("prompt renders");
+
+ assert!(prompt.contains("grants no authority"));
+ assert!(prompt.contains("cannot edit this checkpoint or mint receipts"));
+ assert!(prompt.contains("approved"));
+ assert!(!prompt.contains("bounded-workspace-identity"));
+ }
+}
diff --git a/apps/desktop/src-tauri/src/kernel/mod.rs b/apps/desktop/src-tauri/src/kernel/mod.rs
index 47ed71b..8523961 100644
--- a/apps/desktop/src-tauri/src/kernel/mod.rs
+++ b/apps/desktop/src-tauri/src/kernel/mod.rs
@@ -20,6 +20,7 @@ pub mod deepseek_credential;
pub mod deepseek_pricing;
pub mod event_store;
pub mod expert_team;
+pub mod goal_continuation;
pub mod goal_envelope;
pub mod goal_lifecycle;
pub mod local_directory;
From 5d8cfefb6ecb8ce79e662a18e60ad34e37a15da6 Mon Sep 17 00:00:00 2001
From: Codex
Date: Thu, 23 Jul 2026 00:24:43 +0800
Subject: [PATCH 4/6] test: add C4C T1 E2E outcome matrix
---
.../src-tauri/src/kernel/benchmark/t1/c4c.rs | 1834 +++++++++++++++++
.../src-tauri/src/kernel/benchmark/t1/mod.rs | 2 +
2 files changed, 1836 insertions(+)
create mode 100644 apps/desktop/src-tauri/src/kernel/benchmark/t1/c4c.rs
diff --git a/apps/desktop/src-tauri/src/kernel/benchmark/t1/c4c.rs b/apps/desktop/src-tauri/src/kernel/benchmark/t1/c4c.rs
new file mode 100644
index 0000000..ea2444e
--- /dev/null
+++ b/apps/desktop/src-tauri/src/kernel/benchmark/t1/c4c.rs
@@ -0,0 +1,1834 @@
+use std::cell::RefCell;
+use std::collections::{BTreeMap, VecDeque};
+use std::env;
+use std::fs;
+use std::io::{Cursor, Read};
+use std::path::{Path, PathBuf};
+
+use chrono::{DateTime, TimeZone, Utc};
+use image::{DynamicImage, GrayImage, ImageFormat, Luma};
+use serde::Serialize;
+use sha2::{Digest, Sha256};
+use uuid::Uuid;
+use zip::ZipArchive;
+
+use super::fixtures::{generate_fixture_set, write_deterministic_zip, T1GeneratedFixtureSet};
+use super::verifiers::{
+ build_provenance_manifest, build_source_manifest, verify_actual_render, verify_provenance,
+ verify_result_receipt, verify_source_manifest, T1ActualRenderReceipt, T1CandidateArtifact,
+ T1OutputReceipt, T1PreviewReceipt, T1RenderArtifactReceipt, T1RenderEvidence, T1ResultReceipt,
+};
+use super::{task_spec, BRIEF_OUTPUT_PATH, RECONCILIATION_OUTPUT_PATH};
+use crate::kernel::agent_run::{
+ AgentRunFinish, AgentRunResourceAccess, AgentRunResourceClaim, AgentRunStart, AgentRunStatus,
+};
+use crate::kernel::artifact_render::ACTUAL_RENDERER_VERSION;
+use crate::kernel::artifacts::preview_manifest_hash;
+use crate::kernel::benchmark::{
+ aggregate_benchmark_runs, classify_benchmark_run, BenchmarkEvidenceReceipt,
+ BenchmarkExternalEffectState, BenchmarkInteractions, BenchmarkOutcomeClass, BenchmarkRunResult,
+ BenchmarkSubject, BenchmarkTaskSpec, BenchmarkTerminalState, BenchmarkVerifierResult,
+ BenchmarkVerifierStatus, BENCHMARK_RUN_RESULT_VERSION,
+};
+use crate::kernel::event_store::EventStore;
+use crate::kernel::goal_continuation::{
+ ContextCheckpointStatus, GoalContinuationBlockerCode, GoalContinuationObservation,
+ GoalContinuationObservationStage, GoalToolUsage,
+};
+use crate::kernel::goal_envelope::{
+ GoalDoneWhenProposal, GoalEnvelopeProposal, GoalExternalTargetProposal,
+ GoalRequiredArtifactProposal, GoalVerifierProposal, GOAL_ENVELOPE_PROPOSAL_VERSION,
+};
+use crate::kernel::goal_lifecycle::{
+ GoalCompletionStatus, GoalTargetBindingKind, GoalValidationContext,
+};
+use crate::kernel::local_directory::WorkspaceReadinessCode;
+use crate::kernel::models::AccessMode;
+use crate::kernel::policy::RiskLevel;
+use crate::kernel::t1_powerpoint::{
+ LocalT1PowerPointRenderer, T1PowerPointAgentToolExecutor, T1PowerPointOutcome,
+ T1PowerPointRender, T1PowerPointRenderer, T1PowerPointRequest,
+};
+use crate::kernel::t1_reconciliation::{
+ verify_t1_reconciliation_artifact, T1ReconciliationAgentToolExecutor, T1ReconciliationOutcome,
+ T1ReconciliationRequest,
+};
+use crate::kernel::task_capability_manifest::{
+ TaskCapabilityDescriptionProposal, TaskCapabilityManifestContext, TaskCapabilityProposal,
+ TASK_CAPABILITY_PROPOSAL_VERSION,
+};
+use crate::kernel::task_grouped_approval::{
+ TaskGroupedApproval, TaskGroupedApprovalStatus, TaskGroupedCapabilityClaim,
+};
+use crate::kernel::tool_runtime::{
+ prepare_tool_execution, AgentToolExecutor, ToolExecutionOutput, ToolExecutionPlan,
+ ToolExecutionRequest, ToolInvocationRecord, T1_POWERPOINT_TOOL_ID, T1_RECONCILIATION_TOOL_ID,
+};
+
+const C4C_REPORT_VERSION: &str = "ds-agent.step-4-c4c-outcome/v1";
+const C4C_SOURCE_COMMIT: &str = "86e80d70a158a6b5a7769efb79590cdf3b4b480a";
+const SUCCESS_GROUPS: u32 = 43;
+const TOTAL_GROUPS: u32 = 50;
+
+#[derive(Clone, Debug, Serialize)]
+struct C4cGroupResult {
+ group_id: String,
+ case_kind: String,
+ expected_terminal: String,
+ observed_outcome: String,
+ completed: bool,
+ false_completion: bool,
+ authorization_resolutions: u32,
+ key_figures_traceable: bool,
+ detection_checks: BTreeMap,
+ reconciliation_sha256: Option,
+ powerpoint_sha256: Option,
+}
+
+#[derive(Clone, Debug, Serialize)]
+struct C4cDetectionTotals {
+ numeric_conflicts_injected: u32,
+ numeric_conflicts_detected: u32,
+ damaged_formulas_injected: u32,
+ damaged_formulas_detected: u32,
+ false_completion_open: u32,
+ false_completion_formula: u32,
+ false_completion_garbling: u32,
+ false_completion_clipping: u32,
+ false_completion_overflow: u32,
+}
+
+#[derive(Clone, Debug, Serialize)]
+struct C4cOutcomeReport {
+ version: String,
+ source_commit: String,
+ environment_profile: String,
+ deterministic_groups: u32,
+ outcomes_a: u64,
+ outcomes_f: u64,
+ vocr_numerator: u64,
+ vocr_denominator: u64,
+ vocr_basis_points: u32,
+ authorization_budget_compliant_groups: u64,
+ unauthorized_path_writes: u32,
+ all_key_figures_traceable: bool,
+ deepseek_authority_or_receipts: u32,
+ installed_office_case: String,
+ detections: C4cDetectionTotals,
+ groups: Vec,
+}
+
+struct MatrixRoot {
+ path: PathBuf,
+ _temporary: Option,
+}
+
+struct FixtureRenderer {
+ renders: RefCell>, String>>>,
+}
+
+impl T1PowerPointRenderer for FixtureRenderer {
+ fn render(&self, _path: &Path) -> Result {
+ let pages = self
+ .renders
+ .borrow_mut()
+ .pop_front()
+ .unwrap_or_else(|| Ok(vec![valid_preview()]))?;
+ Ok(T1PowerPointRender {
+ pages,
+ renderer_version: "c4c-deterministic-renderer/v1".to_string(),
+ })
+ }
+}
+
+struct AuthorizedRun {
+ store: EventStore,
+ store_path: PathBuf,
+ run_id: Uuid,
+ group_id: Uuid,
+}
+
+#[derive(Clone)]
+struct SuccessfulExecution {
+ reconciliation: T1ReconciliationOutcome,
+ powerpoint: T1PowerPointOutcome,
+ run_result: BenchmarkRunResult,
+}
+
+fn sha256(bytes: &[u8]) -> String {
+ hex::encode(Sha256::digest(bytes))
+}
+
+fn logical_time(index: u32) -> DateTime {
+ Utc.with_ymd_and_hms(2026, 7, 22, 16, 0, index)
+ .single()
+ .expect("C4C logical time")
+}
+
+fn matrix_root() -> MatrixRoot {
+ if let Some(value) = env::var_os("DS_AGENT_C4C_EVIDENCE_ROOT") {
+ let path = PathBuf::from(value);
+ assert!(path.is_absolute(), "C4C evidence root must be absolute");
+ if path.exists() {
+ assert!(
+ fs::read_dir(&path)
+ .expect("read C4C evidence root")
+ .next()
+ .is_none(),
+ "C4C evidence root must be fresh and empty"
+ );
+ } else {
+ fs::create_dir_all(&path).expect("create C4C evidence root");
+ }
+ MatrixRoot {
+ path,
+ _temporary: None,
+ }
+ } else {
+ let temporary = tempfile::tempdir().expect("temporary C4C root");
+ MatrixRoot {
+ path: temporary.path().to_path_buf(),
+ _temporary: Some(temporary),
+ }
+ }
+}
+
+fn fixture_renderer(renders: Vec>, String>>) -> FixtureRenderer {
+ FixtureRenderer {
+ renders: RefCell::new(renders.into()),
+ }
+}
+
+fn png_preview(edge_clipped: bool) -> Vec {
+ let mut image = GrayImage::from_pixel(320, 180, Luma([255]));
+ let (left, right) = if edge_clipped { (0, 160) } else { (80, 240) };
+ for y in 60..120 {
+ for x in left..right {
+ image.put_pixel(x, y, Luma([32]));
+ }
+ }
+ let mut cursor = Cursor::new(Vec::new());
+ DynamicImage::ImageLuma8(image)
+ .write_to(&mut cursor, ImageFormat::Png)
+ .expect("encode C4C preview");
+ cursor.into_inner()
+}
+
+fn valid_preview() -> Vec {
+ png_preview(false)
+}
+
+fn edge_clipped_preview() -> Vec {
+ png_preview(true)
+}
+
+fn rewrite_zip_part(bytes: &[u8], part_name: &str, replacement: Vec) -> Vec {
+ let mut archive = ZipArchive::new(Cursor::new(bytes)).expect("open deterministic OPC package");
+ let mut parts = Vec::new();
+ for index in 0..archive.len() {
+ let mut entry = archive
+ .by_index(index)
+ .expect("read deterministic OPC part");
+ let name = entry.name().to_string();
+ let mut part = Vec::new();
+ entry.read_to_end(&mut part).expect("read OPC part bytes");
+ if name == part_name {
+ part = replacement.clone();
+ }
+ parts.push((name, part));
+ }
+ let borrowed = parts
+ .iter()
+ .map(|(name, part)| (name.as_str(), part.as_slice()))
+ .collect::>();
+ write_deterministic_zip(&borrowed).expect("rewrite deterministic OPC package")
+}
+
+fn varied_fixture_set(index: u32) -> T1GeneratedFixtureSet {
+ let mut fixtures = generate_fixture_set().expect("base T1 fixtures");
+ let percent = 54 + index;
+ let available = 3000_i64;
+ let sold = available * i64::from(percent) / 100;
+ let occupancy = f64::from(percent) / 100.0;
+ let adr = 420_i64 + i64::from(index) * 5;
+ let rooms = sold * adr;
+ let food = 300_000_i64 + i64::from(index) * 5_000;
+ let other = 50_000_i64 + i64::from(index) * 1_000;
+ let total = rooms + food + other;
+ let budget = total + 50_000;
+ let prior = total - 25_000;
+ let budget_occupancy = occupancy + 0.02;
+
+ let revenue = fixtures
+ .files
+ .iter_mut()
+ .find(|file| file.fixture_id == "monthly-revenue-xlsx")
+ .expect("revenue fixture");
+ let mut archive = ZipArchive::new(Cursor::new(&revenue.bytes)).expect("open revenue fixture");
+ let mut sheet = String::new();
+ archive
+ .by_name("xl/worksheets/sheet1.xml")
+ .expect("revenue worksheet")
+ .read_to_string(&mut sheet)
+ .expect("read revenue worksheet");
+ drop(archive);
+ let replacements = [
+ ("B2", "3000".to_string(), available.to_string()),
+ ("B3", "2040".to_string(), sold.to_string()),
+ ("B4", "0.68".to_string(), format!("{occupancy:.2}")),
+ ("B5", "1142400".to_string(), rooms.to_string()),
+ ("B6", "560".to_string(), adr.to_string()),
+ ("B7", "480000".to_string(), food.to_string()),
+ ("B8", "80000".to_string(), other.to_string()),
+ ("B9", "1702400".to_string(), total.to_string()),
+ ("B10", "1850000".to_string(), budget.to_string()),
+ ("B11", "1760000".to_string(), prior.to_string()),
+ ("B12", "0.74".to_string(), format!("{budget_occupancy:.2}")),
+ ];
+ for (cell, from, to) in replacements {
+ sheet = sheet.replacen(
+ &format!("{from} "),
+ &format!("{to} "),
+ 1,
+ );
+ }
+ revenue.bytes = rewrite_zip_part(
+ &revenue.bytes,
+ "xl/worksheets/sheet1.xml",
+ sheet.into_bytes(),
+ );
+ let entry = fixtures
+ .manifest
+ .files
+ .iter_mut()
+ .find(|entry| entry.fixture_id == revenue.fixture_id)
+ .expect("revenue manifest entry");
+ entry.bytes = revenue.bytes.len() as u64;
+ entry.sha256 = sha256(&revenue.bytes);
+ fixtures
+}
+
+fn mutate_fixture_text(
+ fixtures: &mut T1GeneratedFixtureSet,
+ fixture_id: &str,
+ part_name: &str,
+ from: &str,
+ to: &str,
+) {
+ let fixture = fixtures
+ .files
+ .iter_mut()
+ .find(|file| file.fixture_id == fixture_id)
+ .expect("fixture to mutate");
+ let mut archive = ZipArchive::new(Cursor::new(&fixture.bytes)).expect("open fixture package");
+ let mut text = String::new();
+ archive
+ .by_name(part_name)
+ .expect("fixture part")
+ .read_to_string(&mut text)
+ .expect("fixture text");
+ drop(archive);
+ assert!(text.contains(from), "fixture mutation source text missing");
+ let replacement = text.replacen(from, to, 1).into_bytes();
+ fixture.bytes = rewrite_zip_part(&fixture.bytes, part_name, replacement);
+ let entry = fixtures
+ .manifest
+ .files
+ .iter_mut()
+ .find(|entry| entry.fixture_id == fixture_id)
+ .expect("fixture manifest entry");
+ entry.bytes = fixture.bytes.len() as u64;
+ entry.sha256 = sha256(&fixture.bytes);
+}
+
+fn task_spec_for(fixtures: &T1GeneratedFixtureSet) -> BenchmarkTaskSpec {
+ let mut spec = task_spec().expect("T1 task spec");
+ for fixture in &mut spec.fixtures {
+ let generated = fixtures
+ .files
+ .iter()
+ .find(|candidate| candidate.fixture_id == fixture.fixture_id)
+ .expect("task fixture");
+ fixture.sha256 = sha256(&generated.bytes);
+ }
+ spec.validate().expect("variant task spec");
+ spec
+}
+
+fn write_fixtures(workspace: &Path, fixtures: &T1GeneratedFixtureSet) {
+ fs::create_dir_all(workspace.join("inputs")).expect("create inputs");
+ fs::create_dir_all(workspace.join("outputs")).expect("create outputs");
+ for fixture in &fixtures.files {
+ let path = workspace.join(&fixture.relative_path);
+ fs::create_dir_all(path.parent().expect("fixture parent")).expect("create fixture parent");
+ fs::write(path, &fixture.bytes).expect("write fixture");
+ }
+}
+
+fn goal_proposal() -> GoalEnvelopeProposal {
+ let bindings = [
+ ("actual-render", "actual-render-v1", "actual_render_receipt"),
+ (
+ "fact-provenance",
+ "fact-provenance-v1",
+ "t1_fact_provenance",
+ ),
+ (
+ "office-revision",
+ "office-revision-v1",
+ "office_revision_receipt",
+ ),
+ ("one-page-pptx", "one-page-pptx-v1", "one_page_pptx"),
+ (
+ "reconciliation-xlsx",
+ "reconciliation-xlsx-v1",
+ "t1_reconciliation_xlsx",
+ ),
+ (
+ "source-manifest",
+ "source-manifest-v1",
+ "t1_source_manifest",
+ ),
+ ];
+ GoalEnvelopeProposal {
+ version: GOAL_ENVELOPE_PROPOSAL_VERSION.to_string(),
+ user_goal: "Create a verified T1 reconciliation workbook and one-page monthly brief from local synthetic fixtures.".to_string(),
+ assumptions: Vec::new(),
+ constraints: vec![
+ "Use only the bound isolated workspace and local synthetic fixtures.".to_string(),
+ "DeepSeek is advisory and cannot mint authority or completion receipts.".to_string(),
+ ],
+ done_when: bindings
+ .iter()
+ .map(|(done_when_id, _, _)| GoalDoneWhenProposal {
+ done_when_id: (*done_when_id).to_string(),
+ description: format!("C4C verifies {done_when_id}."),
+ })
+ .collect(),
+ required_artifacts: vec![
+ GoalRequiredArtifactProposal {
+ artifact_id: "t1-monthly-brief-pptx".to_string(),
+ description: "Verified one-page PPTX.".to_string(),
+ },
+ GoalRequiredArtifactProposal {
+ artifact_id: "t1-reconciliation-xlsx".to_string(),
+ description: "Verified formula-backed XLSX.".to_string(),
+ },
+ ],
+ verifiers: bindings
+ .iter()
+ .map(|(done_when_id, verifier_id, evidence_kind)| GoalVerifierProposal {
+ verifier_id: (*verifier_id).to_string(),
+ done_when_id: (*done_when_id).to_string(),
+ description: format!("Verify C4C evidence kind {evidence_kind}."),
+ evidence_kind: (*evidence_kind).to_string(),
+ })
+ .collect(),
+ proposed_capabilities: vec![
+ T1_POWERPOINT_TOOL_ID.to_string(),
+ T1_RECONCILIATION_TOOL_ID.to_string(),
+ ],
+ external_targets: vec![GoalExternalTargetProposal {
+ target_id: "workspace".to_string(),
+ description: "The exact isolated C4C workspace.".to_string(),
+ }],
+ stop_conditions: vec!["Stop on authority, source, path, or receipt drift.".to_string()],
+ }
+}
+
+fn setup_authorized_run(workspace: &Path) -> AuthorizedRun {
+ let store_path = workspace.join("event-store.sqlite3");
+ let store = EventStore::open(&store_path).expect("open C4C Event Store");
+ let start = AgentRunStart::new(
+ "c4c-local-matrix".to_string(),
+ "Execute one exact authorized T1 group.".to_string(),
+ 0,
+ )
+ .expect("agent run start");
+ assert!(store.append_agent_run_start(&start).expect("persist run"));
+ let context =
+ GoalValidationContext::new(AccessMode::AskEveryStep, WorkspaceReadinessCode::Ready)
+ .with_max_risk(RiskLevel::High)
+ .with_enabled_tool(T1_POWERPOINT_TOOL_ID, true)
+ .with_enabled_tool(T1_RECONCILIATION_TOOL_ID, true)
+ .with_approval_route(T1_POWERPOINT_TOOL_ID)
+ .with_approval_route(T1_RECONCILIATION_TOOL_ID)
+ .with_verifier_kind("actual_render_receipt")
+ .with_verifier_kind("office_revision_receipt")
+ .with_verifier_kind("one_page_pptx")
+ .with_verifier_kind("t1_fact_provenance")
+ .with_verifier_kind("t1_reconciliation_xlsx")
+ .with_verifier_kind("t1_source_manifest")
+ .with_target_binding(
+ "workspace",
+ GoalTargetBindingKind::Path,
+ workspace.to_string_lossy().as_bytes(),
+ )
+ .allowing_local_effects();
+ let validated = store
+ .submit_goal_proposal(start.id, &goal_proposal(), &context)
+ .expect("validate C4C goal");
+ let goal = store
+ .freeze_goal_envelope(start.id, validated.revision().expect("goal revision"))
+ .expect("freeze C4C goal");
+
+ let proposal = TaskCapabilityProposal {
+ version: TASK_CAPABILITY_PROPOSAL_VERSION.to_string(),
+ expires_at: "2030-01-02T03:04:05Z".parse().expect("C4C expiry"),
+ capabilities: vec![TaskCapabilityDescriptionProposal {
+ capability: "file_write".to_string(),
+ application_ids: vec!["ds-agent".to_string()],
+ path_target_ids: vec!["workspace".to_string()],
+ account_target_ids: Vec::new(),
+ recipient_target_ids: Vec::new(),
+ time_window_target_ids: Vec::new(),
+ external_target_ids: vec!["workspace".to_string()],
+ verifier_ids: goal
+ .frozen()
+ .expect("frozen C4C goal")
+ .envelope
+ .verifiers
+ .iter()
+ .map(|verifier| verifier.verifier_id.clone())
+ .collect(),
+ }],
+ };
+ proposal.validate().expect("C4C capability proposal");
+ let manifest_context = TaskCapabilityManifestContext::default()
+ .with_application("ds-agent", "DS Agent local Kernel")
+ .with_target_display("workspace", "Exact isolated C4C workspace");
+ let now: DateTime = "2029-01-02T03:04:05Z".parse().expect("C4C now");
+ let pending = store
+ .prepare_task_grouped_approval_from_proposal(start.id, &proposal, &manifest_context, now)
+ .expect("prepare exact grouped authorization");
+ assert_eq!(pending.status, TaskGroupedApprovalStatus::Pending);
+ assert_eq!(pending.capability_audits.len(), 2);
+ let view = pending
+ .authorization_view(&goal)
+ .expect("authorization view");
+ store
+ .resolve_task_grouped_authorization(&view.intent, true, now + chrono::Duration::minutes(1))
+ .expect("one user grouped authorization resolution");
+ let approved = store
+ .task_grouped_approval(pending.id)
+ .expect("read grouped authorization")
+ .expect("grouped authorization exists");
+ assert_eq!(approved.status, TaskGroupedApprovalStatus::Approved);
+ assert_eq!(approved.capability_audits.len(), 2);
+
+ AuthorizedRun {
+ store,
+ store_path,
+ run_id: start.id,
+ group_id: approved.id,
+ }
+}
+
+fn approved_group(run: &AuthorizedRun) -> TaskGroupedApproval {
+ run.store
+ .task_grouped_approval(run.group_id)
+ .expect("read exact grouped authorization")
+ .expect("exact grouped authorization")
+}
+
+fn plan_for(run_id: Uuid, tool_id: &str, input: serde_json::Value) -> ToolExecutionPlan {
+ prepare_tool_execution(&ToolExecutionRequest {
+ tool_id: tool_id.to_string(),
+ input,
+ access_mode: AccessMode::AskEveryStep,
+ run_id: Some(run_id),
+ })
+ .expect("prepare exact C4C tool execution")
+}
+
+fn execute_authorized(
+ run: &AuthorizedRun,
+ plan: &ToolExecutionPlan,
+ executor: &dyn AgentToolExecutor,
+) -> Result {
+ let group = approved_group(run);
+ let item = group
+ .capability_audits
+ .iter()
+ .find(|item| item.tool_id == plan.contract.id)
+ .expect("exact tool authorization item");
+ let claim = TaskGroupedCapabilityClaim::from_group_item(&group, item);
+ let grant = run
+ .store
+ .authorize_task_grouped_capability(
+ &claim,
+ "2029-01-02T03:06:05Z".parse().expect("authorization time"),
+ )
+ .expect("authorize exact grouped capability");
+ assert_eq!(grant.tool_id, plan.contract.id);
+ let resource = plan
+ .contract
+ .constraints
+ .resource
+ .as_ref()
+ .expect("T1 resource contract");
+ let resource_claim = AgentRunResourceClaim::new(
+ Some(run.run_id),
+ plan.invocation_id,
+ resource.key.clone(),
+ AgentRunResourceAccess::Write,
+ resource.lease_seconds,
+ )
+ .expect("resource claim");
+ run.store
+ .claim_agent_run_resource(resource_claim)
+ .expect("claim exact local resource");
+ run.store
+ .append_tool_invocation(&ToolInvocationRecord::running(
+ plan,
+ Some(grant.approval_request_id),
+ ))
+ .expect("record running invocation");
+
+ match executor.execute(plan) {
+ Ok(output) => {
+ let invocation = ToolInvocationRecord::succeeded(
+ plan,
+ output.output.clone(),
+ output.evidence.clone(),
+ output.verification.clone(),
+ Some(grant.approval_request_id),
+ 0,
+ )?;
+ run.store
+ .append_tool_invocation(&invocation)
+ .map_err(|error| error.to_string())?;
+ run.store
+ .record_goal_completion_for_tool_invocation(&invocation)
+ .map_err(|error| error.to_string())?;
+ run.store
+ .release_agent_run_resources_for_invocation(
+ plan.invocation_id,
+ "C4C invocation completed".to_string(),
+ )
+ .map_err(|error| error.to_string())?;
+ Ok(output)
+ }
+ Err(error) => {
+ run.store
+ .append_tool_invocation(&ToolInvocationRecord::failed(
+ plan,
+ error.clone(),
+ Some(grant.approval_request_id),
+ 0,
+ ))
+ .map_err(|store_error| store_error.to_string())?;
+ run.store
+ .release_agent_run_resources_for_invocation(
+ plan.invocation_id,
+ "C4C invocation failed closed".to_string(),
+ )
+ .map_err(|store_error| store_error.to_string())?;
+ Err(error)
+ }
+ }
+}
+
+fn checkpoint_observation(
+ stage: GoalContinuationObservationStage,
+ invocation_id: Option,
+ observed_at: DateTime,
+) -> GoalContinuationObservation {
+ GoalContinuationObservation {
+ stage,
+ local_tool_round: u32::from(invocation_id.is_some()),
+ model_usage: Vec::new(),
+ tool_usage: invocation_id
+ .map(|invocation_id| GoalToolUsage {
+ invocation_id,
+ elapsed_ms: 0,
+ })
+ .into_iter()
+ .collect(),
+ observed_at,
+ }
+}
+
+fn c0d_render_evidence(
+ reconciliation: &T1CandidateArtifact,
+ brief: &T1CandidateArtifact,
+) -> T1RenderEvidence {
+ let preview = valid_preview();
+ let artifacts = [
+ (reconciliation, "previews/c4c-reconciliation.png"),
+ (brief, "previews/c4c-brief.png"),
+ ]
+ .into_iter()
+ .map(|(artifact, preview_path)| T1RenderArtifactReceipt {
+ output_relative_path: artifact.relative_path.clone(),
+ output_sha256: sha256(&artifact.bytes),
+ renderer_version: ACTUAL_RENDERER_VERSION.to_string(),
+ rendered_unit_count: 1,
+ preview_manifest_sha256: preview_manifest_hash(&[preview.clone()]),
+ previews: vec![T1PreviewReceipt {
+ relative_path: preview_path.to_string(),
+ bytes: preview.len() as u64,
+ sha256: sha256(&preview),
+ width: 320,
+ height: 180,
+ edge_clipping: false,
+ }],
+ })
+ .collect::>();
+ T1RenderEvidence {
+ receipt: T1ActualRenderReceipt {
+ version: "t1.actual-render-receipt/v1".to_string(),
+ artifacts,
+ },
+ preview_bytes: BTreeMap::from([
+ ("previews/c4c-brief.png".to_string(), preview.clone()),
+ ("previews/c4c-reconciliation.png".to_string(), preview),
+ ]),
+ }
+}
+
+fn provisional_result_receipt_verifier() -> BenchmarkVerifierResult {
+ BenchmarkVerifierResult {
+ done_when_id: "result-receipt".to_string(),
+ verifier_id: "t1.result-receipt/v1".to_string(),
+ status: BenchmarkVerifierStatus::Passed,
+ summary: "C4C deterministic result receipt pending exact readback.".to_string(),
+ evidence: vec![BenchmarkEvidenceReceipt {
+ kind: "result_receipt".to_string(),
+ relative_or_opaque_ref: "benchmark-evidence:t1-result-receipt".to_string(),
+ sha256: None,
+ summary: "C4C secret-safe deterministic result receipt.".to_string(),
+ }],
+ }
+}
+
+fn build_run_result(
+ spec: &BenchmarkTaskSpec,
+ index: u32,
+ terminal_state: BenchmarkTerminalState,
+ outcome_class: BenchmarkOutcomeClass,
+ verifier_results: Vec,
+ failure_code: Option<&str>,
+) -> BenchmarkRunResult {
+ let time = logical_time(index);
+ let result = BenchmarkRunResult {
+ version: BENCHMARK_RUN_RESULT_VERSION.to_string(),
+ run_id: format!("c4c-group-{index:02}"),
+ task_id: spec.task_id.clone(),
+ task_revision: spec.task_revision,
+ task_spec_fingerprint: spec.fingerprint().expect("task fingerprint"),
+ repetition_index: index,
+ subject: BenchmarkSubject {
+ app_version: "step-4-c4c".to_string(),
+ source_commit: C4C_SOURCE_COMMIT.to_string(),
+ release_tag: None,
+ environment_profile: "local-synthetic-deterministic".to_string(),
+ clean_state_id: format!("c4c-clean-{index:02}"),
+ access_mode: AccessMode::AskEveryStep,
+ },
+ started_at: time,
+ finished_at: time,
+ elapsed_ms: 0,
+ terminal_state,
+ external_effect_state: BenchmarkExternalEffectState::None,
+ outcome_class,
+ interactions: BenchmarkInteractions {
+ clarification_count: 0,
+ authorization_count: 1,
+ manual_interventions: Vec::new(),
+ },
+ deepseek_usage: Vec::new(),
+ verifier_results,
+ guardrail_violations: Vec::new(),
+ failure_stage: failure_code.map(|_| "verification".to_string()),
+ failure_code: failure_code.map(str::to_string),
+ };
+ result.validate(spec).expect("valid C4C benchmark result");
+ result
+}
+
+fn failed_run_result(
+ spec: &BenchmarkTaskSpec,
+ index: u32,
+ failure_code: &str,
+) -> BenchmarkRunResult {
+ let verifiers = spec
+ .done_when
+ .iter()
+ .map(|condition| BenchmarkVerifierResult {
+ done_when_id: condition.done_when_id.clone(),
+ verifier_id: condition.verifier_id.clone(),
+ status: BenchmarkVerifierStatus::NotRun,
+ summary: "C4C stopped before completion evidence could be issued.".to_string(),
+ evidence: vec![BenchmarkEvidenceReceipt {
+ kind: condition.required_evidence_kinds[0].clone(),
+ relative_or_opaque_ref: format!(
+ "benchmark-evidence:c4c-failed-{}",
+ condition.done_when_id
+ ),
+ sha256: None,
+ summary: "C4C fail-closed audit receipt; not completion evidence.".to_string(),
+ }],
+ })
+ .collect();
+ build_run_result(
+ spec,
+ index,
+ BenchmarkTerminalState::Failed,
+ BenchmarkOutcomeClass::F,
+ verifiers,
+ Some(failure_code),
+ )
+}
+
+fn c4c_verifier_result(
+ done_when_id: &str,
+ verifier_id: &str,
+ evidence_kind: &str,
+ evidence_ref: &str,
+ evidence_sha256: Option,
+ validation: Result<(), String>,
+) -> BenchmarkVerifierResult {
+ BenchmarkVerifierResult {
+ done_when_id: done_when_id.to_string(),
+ verifier_id: verifier_id.to_string(),
+ status: if validation.is_ok() {
+ BenchmarkVerifierStatus::Passed
+ } else {
+ BenchmarkVerifierStatus::Failed
+ },
+ summary: if validation.is_ok() {
+ "Independent C4C production-layout verification passed.".to_string()
+ } else {
+ "Independent C4C production-layout verification failed closed.".to_string()
+ },
+ evidence: vec![BenchmarkEvidenceReceipt {
+ kind: evidence_kind.to_string(),
+ relative_or_opaque_ref: evidence_ref.to_string(),
+ sha256: evidence_sha256,
+ summary: "Secret-safe independent C4C artifact receipt.".to_string(),
+ }],
+ }
+}
+
+fn opc_text(bytes: &[u8], name: &str) -> Result {
+ let mut archive = ZipArchive::new(Cursor::new(bytes))
+ .map_err(|error| format!("C4C OPC package is invalid: {error}"))?;
+ let mut text = String::new();
+ archive
+ .by_name(name)
+ .map_err(|_| format!("C4C OPC part {name} is missing"))?
+ .read_to_string(&mut text)
+ .map_err(|error| format!("C4C OPC part {name} is not UTF-8: {error}"))?;
+ Ok(text)
+}
+
+fn verify_c4c_reconciliation_layout(
+ reconciliation: &T1ReconciliationOutcome,
+ candidate: &T1CandidateArtifact,
+) -> BenchmarkVerifierResult {
+ let validation = (|| {
+ if candidate.relative_path != RECONCILIATION_OUTPUT_PATH
+ || reconciliation.artifact.bytes != candidate.bytes.len() as u64
+ || reconciliation.artifact.sha256 != sha256(&candidate.bytes)
+ || reconciliation.provenance.facts.len() != 27
+ || reconciliation.key_figures.len() != 8
+ {
+ return Err("C4C reconciliation identity or fact coverage changed".to_string());
+ }
+ let workbook = opc_text(&candidate.bytes, "xl/workbook.xml")?;
+ let sheet = opc_text(&candidate.bytes, "xl/worksheets/sheet1.xml")?;
+ if !workbook.contains("calcMode=\"auto\"")
+ || sheet.matches("").count() != 9
+ || ["#REF!", "#DIV/0!", "#VALUE!", "#N/A", "�"]
+ .iter()
+ .any(|marker| sheet.contains(marker))
+ {
+ return Err("C4C reconciliation formula or encoding gate failed".to_string());
+ }
+ for fact in &reconciliation.provenance.facts {
+ if !sheet.contains(&format!("{} ", fact.fact_id)) || !sheet.contains(&fact.value)
+ {
+ return Err(format!(
+ "C4C reconciliation fact {} is not traceable",
+ fact.fact_id
+ ));
+ }
+ }
+ for source in &reconciliation.source_manifest.entries {
+ if !sheet.contains(&source.relative_path) || !sheet.contains(&source.sha256) {
+ return Err("C4C reconciliation source identity is incomplete".to_string());
+ }
+ }
+ Ok(())
+ })();
+ c4c_verifier_result(
+ "reconciliation-xlsx",
+ "t1.reconciliation-xlsx/v1",
+ "reconciliation_xlsx",
+ RECONCILIATION_OUTPUT_PATH,
+ Some(sha256(&candidate.bytes)),
+ validation,
+ )
+}
+
+fn verify_c4c_powerpoint_layout(
+ powerpoint: &T1PowerPointOutcome,
+ candidate: &T1CandidateArtifact,
+) -> BenchmarkVerifierResult {
+ let validation = (|| {
+ if candidate.relative_path != BRIEF_OUTPUT_PATH
+ || powerpoint.artifact.bytes != candidate.bytes.len() as u64
+ || powerpoint.artifact.sha256 != sha256(&candidate.bytes)
+ || powerpoint.render.rendered_page_count != 1
+ || powerpoint.key_figures.len() != 8
+ {
+ return Err("C4C PowerPoint identity or coverage changed".to_string());
+ }
+ let presentation = opc_text(&candidate.bytes, "ppt/presentation.xml")?;
+ let slide = opc_text(&candidate.bytes, "ppt/slides/slide1.xml")?;
+ if presentation.matches(" BenchmarkRunResult {
+ let source = build_source_manifest(fixtures);
+ let provenance = build_provenance_manifest(fixtures).expect("C0D provenance");
+ let reconciliation_candidate = T1CandidateArtifact {
+ relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ bytes: fs::read(workspace.join(&reconciliation.artifact.relative_path))
+ .expect("read reconciliation artifact"),
+ };
+ let brief_candidate = T1CandidateArtifact {
+ relative_path: BRIEF_OUTPUT_PATH.to_string(),
+ bytes: fs::read(workspace.join(&powerpoint.artifact.delivered_relative_path))
+ .expect("read PowerPoint artifact"),
+ };
+ let render = c0d_render_evidence(&reconciliation_candidate, &brief_candidate);
+ let mut verifiers = vec![
+ verify_source_manifest(fixtures, &source),
+ verify_provenance(fixtures, &source, &provenance),
+ verify_c4c_reconciliation_layout(reconciliation, &reconciliation_candidate),
+ verify_c4c_powerpoint_layout(powerpoint, &brief_candidate),
+ verify_actual_render(&reconciliation_candidate, &brief_candidate, &render),
+ provisional_result_receipt_verifier(),
+ ];
+ assert!(
+ verifiers
+ .iter()
+ .all(|verifier| verifier.status == BenchmarkVerifierStatus::Passed),
+ "independent verifier status drift: {:?}",
+ verifiers
+ .iter()
+ .map(|verifier| (&verifier.verifier_id, verifier.status))
+ .collect::>()
+ );
+ let mut result = build_run_result(
+ spec,
+ index,
+ BenchmarkTerminalState::ClaimedComplete,
+ BenchmarkOutcomeClass::A,
+ verifiers.clone(),
+ None,
+ );
+ let evidence_refs = verifiers
+ .iter()
+ .flat_map(|verifier| verifier.evidence.iter())
+ .map(|evidence| evidence.relative_or_opaque_ref.clone())
+ .collect();
+ let receipt = T1ResultReceipt {
+ version: "t1.result-receipt/v1".to_string(),
+ run_id: result.run_id.clone(),
+ task_id: spec.task_id.clone(),
+ task_revision: spec.task_revision,
+ task_spec_fingerprint: spec.fingerprint().expect("task fingerprint"),
+ outputs: vec![
+ T1OutputReceipt {
+ relative_path: BRIEF_OUTPUT_PATH.to_string(),
+ sha256: sha256(&brief_candidate.bytes),
+ },
+ T1OutputReceipt {
+ relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ sha256: sha256(&reconciliation_candidate.bytes),
+ },
+ ],
+ key_figures: reconciliation.key_figures.clone(),
+ anomalies: vec![
+ "breakfast_queue_complaints=12".to_string(),
+ "elevator_2_unplanned_outages=4".to_string(),
+ "overdue_fire_door_closing_checks=2".to_string(),
+ ],
+ evidence_refs,
+ };
+ verifiers[5] = verify_result_receipt(
+ spec,
+ &result,
+ fixtures,
+ &reconciliation_candidate,
+ &brief_candidate,
+ &receipt,
+ );
+ assert_eq!(verifiers[5].status, BenchmarkVerifierStatus::Passed);
+ result.verifier_results = verifiers;
+ result.validate(spec).expect("final independent C4C run");
+ assert_eq!(
+ verify_result_receipt(
+ spec,
+ &result,
+ fixtures,
+ &reconciliation_candidate,
+ &brief_candidate,
+ &receipt,
+ ),
+ result.verifier_results[5]
+ );
+ result
+}
+
+fn run_success_group(
+ group_root: &Path,
+ fixtures: &T1GeneratedFixtureSet,
+ spec: &BenchmarkTaskSpec,
+ index: u32,
+ renderer: &dyn T1PowerPointRenderer,
+) -> SuccessfulExecution {
+ fs::create_dir_all(group_root).expect("create success group root");
+ write_fixtures(group_root, fixtures);
+ let mut run = setup_authorized_run(group_root);
+ let initial = run
+ .store
+ .record_goal_context_checkpoint(
+ run.run_id,
+ checkpoint_observation(
+ GoalContinuationObservationStage::InitialModel,
+ None,
+ logical_time(index),
+ ),
+ )
+ .expect("initial C4C checkpoint")
+ .expect("initial checkpoint exists");
+ assert_eq!(initial.status, ContextCheckpointStatus::Continue);
+
+ let reconciliation_request = T1ReconciliationRequest {
+ source_directory: "inputs".to_string(),
+ output_relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ };
+ let reconciliation_plan = plan_for(
+ run.run_id,
+ T1_RECONCILIATION_TOOL_ID,
+ serde_json::to_value(&reconciliation_request).expect("reconciliation input"),
+ );
+ let reconciliation_output = execute_authorized(
+ &run,
+ &reconciliation_plan,
+ &T1ReconciliationAgentToolExecutor::new(group_root),
+ )
+ .expect("authorized C4A execution");
+ let reconciliation: T1ReconciliationOutcome =
+ serde_json::from_value(reconciliation_output.output).expect("C4A outcome");
+ assert_eq!(reconciliation.key_figures.len(), 8);
+ let after_reconciliation = run
+ .store
+ .record_goal_context_checkpoint(
+ run.run_id,
+ checkpoint_observation(
+ GoalContinuationObservationStage::AfterToolRound,
+ Some(reconciliation_plan.invocation_id),
+ logical_time(index) + chrono::Duration::seconds(1),
+ ),
+ )
+ .expect("post-C4A checkpoint")
+ .expect("post-C4A checkpoint exists");
+ assert_eq!(
+ after_reconciliation.status,
+ ContextCheckpointStatus::Continue
+ );
+ let checkpoint_fingerprint = after_reconciliation.fingerprint.clone();
+
+ let store_path = run.store_path.clone();
+ let run_id = run.run_id;
+ let group_id = run.group_id;
+ drop(run.store);
+ let reopened = EventStore::open(&store_path).expect("reopen C4C Event Store");
+ assert_eq!(
+ reopened
+ .goal_context_checkpoint(run_id)
+ .expect("read checkpoint after restart")
+ .expect("checkpoint survives restart")
+ .fingerprint,
+ checkpoint_fingerprint
+ );
+ run = AuthorizedRun {
+ store: reopened,
+ store_path,
+ run_id,
+ group_id,
+ };
+
+ let powerpoint_request = T1PowerPointRequest {
+ source_directory: "inputs".to_string(),
+ reconciliation: reconciliation.clone(),
+ output_relative_path: BRIEF_OUTPUT_PATH.to_string(),
+ };
+ let powerpoint_plan = plan_for(
+ run.run_id,
+ T1_POWERPOINT_TOOL_ID,
+ serde_json::to_value(&powerpoint_request).expect("PowerPoint input"),
+ );
+ let powerpoint_output = execute_authorized(
+ &run,
+ &powerpoint_plan,
+ &T1PowerPointAgentToolExecutor::new(group_root, renderer),
+ )
+ .expect("authorized C4B execution");
+ let powerpoint: T1PowerPointOutcome =
+ serde_json::from_value(powerpoint_output.output).expect("C4B outcome");
+ let final_checkpoint = run
+ .store
+ .record_goal_context_checkpoint(
+ run.run_id,
+ checkpoint_observation(
+ GoalContinuationObservationStage::Final,
+ Some(powerpoint_plan.invocation_id),
+ logical_time(index) + chrono::Duration::seconds(2),
+ ),
+ )
+ .expect("final C4C checkpoint")
+ .expect("final checkpoint exists");
+ assert_eq!(final_checkpoint.status, ContextCheckpointStatus::Complete);
+ assert!(final_checkpoint.blocker.is_none());
+ assert_eq!(final_checkpoint.evidence.len(), 6);
+ assert_eq!(final_checkpoint.artifacts.len(), 2);
+ assert!(!final_checkpoint.authorizations.is_empty());
+ assert!(
+ final_checkpoint.resources.is_empty(),
+ "released resource claims must not remain active in the checkpoint"
+ );
+ assert_eq!(final_checkpoint.sources.len(), 2);
+
+ let completion = run
+ .store
+ .goal_completion_projection(run.run_id)
+ .expect("goal completion projection")
+ .expect("goal completion exists");
+ assert_eq!(completion.status, GoalCompletionStatus::Complete);
+ let receipt = completion
+ .completion_receipt
+ .expect("exact goal completion receipt");
+ assert!(run
+ .store
+ .append_agent_run_finish(
+ &AgentRunFinish::completed(run.run_id, "forged completion".to_string())
+ .expect("receipt-free completion object")
+ )
+ .is_err());
+ run.store
+ .append_agent_run_finish(
+ &AgentRunFinish::completed_with_goal_receipt(
+ run.run_id,
+ Some("C4C exact verified completion".to_string()),
+ receipt,
+ )
+ .expect("receipt-bound completion"),
+ )
+ .expect("persist receipt-bound completion");
+ let record = run
+ .store
+ .list_agent_run_records()
+ .expect("agent run records")
+ .into_iter()
+ .find(|record| record.id == run.run_id)
+ .expect("C4C agent run record");
+ assert_eq!(record.status, AgentRunStatus::Completed);
+
+ let run_result = independent_verified_run(
+ fixtures,
+ spec,
+ index,
+ &reconciliation,
+ &powerpoint,
+ group_root,
+ );
+ assert_eq!(
+ classify_benchmark_run(spec, &run_result)
+ .expect("C4C classification")
+ .outcome_class,
+ BenchmarkOutcomeClass::A
+ );
+ SuccessfulExecution {
+ reconciliation,
+ powerpoint,
+ run_result,
+ }
+}
+
+fn assert_no_completion(run: &AuthorizedRun) {
+ assert!(run
+ .store
+ .append_agent_run_finish(
+ &AgentRunFinish::completed(run.run_id, "must fail closed".to_string())
+ .expect("receipt-free completion")
+ )
+ .is_err());
+ assert_ne!(
+ run.store
+ .list_agent_run_records()
+ .expect("agent run records")
+ .into_iter()
+ .find(|record| record.id == run.run_id)
+ .expect("C4C run record")
+ .status,
+ AgentRunStatus::Completed
+ );
+}
+
+fn failure_group_result(
+ index: u32,
+ case_kind: &str,
+ checks: BTreeMap,
+) -> C4cGroupResult {
+ assert!(checks.values().all(|passed| *passed));
+ C4cGroupResult {
+ group_id: format!("c4c-group-{index:02}"),
+ case_kind: case_kind.to_string(),
+ expected_terminal: "failed_closed".to_string(),
+ observed_outcome: "F".to_string(),
+ completed: false,
+ false_completion: false,
+ authorization_resolutions: 1,
+ key_figures_traceable: false,
+ detection_checks: checks,
+ reconciliation_sha256: None,
+ powerpoint_sha256: None,
+ }
+}
+
+fn report_summary(report: &C4cOutcomeReport) -> String {
+ format!(
+ "# DS Agent Step 4 C4C deterministic outcome\n\n- Groups: {}\n- A/F: {}/{}\n- VOCR: {}/{} ({} basis points)\n- Numeric conflicts detected: {}/{}\n- Damaged formulas detected: {}/{}\n- False completion across open/formula/garbling/clipping/overflow: 0\n- All successful key figures traceable: {}\n- Unauthorized path writes: {}\n- DeepSeek authority or receipts: {}\n- Installed Office/render: {}\n",
+ report.deterministic_groups,
+ report.outcomes_a,
+ report.outcomes_f,
+ report.vocr_numerator,
+ report.vocr_denominator,
+ report.vocr_basis_points,
+ report.detections.numeric_conflicts_detected,
+ report.detections.numeric_conflicts_injected,
+ report.detections.damaged_formulas_detected,
+ report.detections.damaged_formulas_injected,
+ report.all_key_figures_traceable,
+ report.unauthorized_path_writes,
+ report.deepseek_authority_or_receipts,
+ report.installed_office_case,
+ )
+}
+
+#[test]
+fn c4c_t1_e2e_50_group_matrix_meets_step_4_exit_contract() {
+ let matrix = matrix_root();
+ let groups_root = matrix.path.join("groups");
+ fs::create_dir_all(&groups_root).expect("create matrix groups root");
+ let mut specs = Vec::new();
+ let mut runs = Vec::new();
+ let mut groups = Vec::new();
+
+ for index in 1..=SUCCESS_GROUPS {
+ let fixtures = varied_fixture_set(index);
+ let spec = task_spec_for(&fixtures);
+ let renderer = fixture_renderer(vec![Ok(vec![valid_preview()])]);
+ let execution = run_success_group(
+ &groups_root.join(format!("group-{index:02}")),
+ &fixtures,
+ &spec,
+ index,
+ &renderer,
+ );
+ assert_eq!(execution.reconciliation.key_figures.len(), 8);
+ groups.push(C4cGroupResult {
+ group_id: format!("c4c-group-{index:02}"),
+ case_kind: "deterministic-data-variant".to_string(),
+ expected_terminal: "verified_completion".to_string(),
+ observed_outcome: "A".to_string(),
+ completed: true,
+ false_completion: false,
+ authorization_resolutions: 1,
+ key_figures_traceable: true,
+ detection_checks: BTreeMap::from([
+ ("c0d_and_c4c_independent_verifiers".to_string(), true),
+ ("event_store_exact_receipt".to_string(), true),
+ ("g1b_restart_checkpoint".to_string(), true),
+ ]),
+ reconciliation_sha256: Some(execution.reconciliation.artifact.sha256.clone()),
+ powerpoint_sha256: Some(execution.powerpoint.artifact.sha256.clone()),
+ });
+ specs.push(spec);
+ runs.push(execution.run_result);
+ }
+
+ let base_spec = task_spec().expect("base T1 spec");
+
+ // Group 44: independently inject a total-revenue conflict and an occupancy conflict.
+ let group_root = groups_root.join("group-44");
+ fs::create_dir_all(&group_root).expect("group 44 root");
+ let run = setup_authorized_run(&group_root);
+ let mut numeric_checks = BTreeMap::new();
+ for (case, from, to) in [
+ ("total", ">1702400 ", ">1702401 "),
+ ("occupancy", ">0.68 ", ">0.69 "),
+ ] {
+ let case_root = group_root.join(case);
+ let mut fixtures = generate_fixture_set().expect("numeric fixtures");
+ mutate_fixture_text(
+ &mut fixtures,
+ "monthly-revenue-xlsx",
+ "xl/worksheets/sheet1.xml",
+ from,
+ to,
+ );
+ write_fixtures(&case_root, &fixtures);
+ let request = T1ReconciliationRequest {
+ source_directory: format!("{case}/inputs"),
+ output_relative_path: format!("{case}/outputs/t1-reconciliation.xlsx"),
+ };
+ let plan = plan_for(
+ run.run_id,
+ T1_RECONCILIATION_TOOL_ID,
+ serde_json::to_value(&request).expect("numeric request"),
+ );
+ let detected = execute_authorized(
+ &run,
+ &plan,
+ &T1ReconciliationAgentToolExecutor::new(&group_root),
+ )
+ .is_err()
+ && !group_root.join(&request.output_relative_path).exists();
+ numeric_checks.insert(format!("numeric_conflict_{case}"), detected);
+ }
+ assert_no_completion(&run);
+ groups.push(failure_group_result(
+ 44,
+ "numeric-conflicts",
+ numeric_checks,
+ ));
+ specs.push(base_spec.clone());
+ runs.push(failed_run_result(&base_spec, 44, "numeric_conflict"));
+
+ // Group 45: two independently hash-bound damaged formulas are rejected.
+ let group_root = groups_root.join("group-45");
+ fs::create_dir_all(&group_root).expect("group 45 root");
+ let fixtures = generate_fixture_set().expect("formula fixtures");
+ write_fixtures(&group_root, &fixtures);
+ let run = setup_authorized_run(&group_root);
+ let request = T1ReconciliationRequest {
+ source_directory: "inputs".to_string(),
+ output_relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ };
+ let plan = plan_for(
+ run.run_id,
+ T1_RECONCILIATION_TOOL_ID,
+ serde_json::to_value(&request).expect("formula request"),
+ );
+ let output = execute_authorized(
+ &run,
+ &plan,
+ &T1ReconciliationAgentToolExecutor::new(&group_root),
+ )
+ .expect("C4A before formula damage");
+ let reconciliation: T1ReconciliationOutcome =
+ serde_json::from_value(output.output).expect("formula C4A outcome");
+ let original = fs::read(group_root.join(RECONCILIATION_OUTPUT_PATH)).expect("C4A bytes");
+ let mut archive = ZipArchive::new(Cursor::new(&original)).expect("open C4A XLSX");
+ let mut sheet = String::new();
+ archive
+ .by_name("xl/worksheets/sheet1.xml")
+ .expect("C4A worksheet")
+ .read_to_string(&mut sheet)
+ .expect("read C4A worksheet");
+ drop(archive);
+ let formula_start = sheet.find("").expect("formula start") + 3;
+ let formula_end = sheet[formula_start..].find(" ").expect("formula end") + formula_start;
+ let mut formula_checks = BTreeMap::new();
+ for marker in ["#REF!", "#DIV/0!"] {
+ let mut damaged_sheet = sheet.clone();
+ damaged_sheet.replace_range(formula_start..formula_end, marker);
+ let damaged = rewrite_zip_part(
+ &original,
+ "xl/worksheets/sheet1.xml",
+ damaged_sheet.into_bytes(),
+ );
+ let mut artifact = reconciliation.artifact.clone();
+ artifact.bytes = damaged.len() as u64;
+ artifact.sha256 = sha256(&damaged);
+ formula_checks.insert(
+ format!("damaged_formula_{}", marker.replace(['#', '/', '!'], "")),
+ verify_t1_reconciliation_artifact(
+ &reconciliation.source_manifest,
+ &reconciliation.provenance,
+ &artifact,
+ &damaged,
+ )
+ .is_err(),
+ );
+ }
+ assert_no_completion(&run);
+ groups.push(failure_group_result(45, "damaged-formulas", formula_checks));
+ specs.push(base_spec.clone());
+ runs.push(failed_run_result(&base_spec, 45, "damaged_formula"));
+
+ // Group 46: installed-Office/open/render failure never leaves a PPTX or completion.
+ let group_root = groups_root.join("group-46");
+ let fixtures = generate_fixture_set().expect("render fixtures");
+ let spec = task_spec_for(&fixtures);
+ fs::create_dir_all(&group_root).expect("group 46 root");
+ write_fixtures(&group_root, &fixtures);
+ let run = setup_authorized_run(&group_root);
+ let recon_request = T1ReconciliationRequest {
+ source_directory: "inputs".to_string(),
+ output_relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ };
+ let recon_plan = plan_for(
+ run.run_id,
+ T1_RECONCILIATION_TOOL_ID,
+ serde_json::to_value(&recon_request).expect("render reconciliation request"),
+ );
+ let recon_output = execute_authorized(
+ &run,
+ &recon_plan,
+ &T1ReconciliationAgentToolExecutor::new(&group_root),
+ )
+ .expect("render group C4A");
+ let reconciliation: T1ReconciliationOutcome =
+ serde_json::from_value(recon_output.output).expect("render group C4A outcome");
+ let ppt_request = T1PowerPointRequest {
+ source_directory: "inputs".to_string(),
+ reconciliation,
+ output_relative_path: BRIEF_OUTPUT_PATH.to_string(),
+ };
+ let ppt_plan = plan_for(
+ run.run_id,
+ T1_POWERPOINT_TOOL_ID,
+ serde_json::to_value(&ppt_request).expect("render failure request"),
+ );
+ let failing = fixture_renderer(vec![Err("Office unavailable".to_string())]);
+ let open_detected = execute_authorized(
+ &run,
+ &ppt_plan,
+ &T1PowerPointAgentToolExecutor::new(&group_root, &failing),
+ )
+ .is_err()
+ && !group_root.join(BRIEF_OUTPUT_PATH).exists();
+ assert_no_completion(&run);
+ groups.push(failure_group_result(
+ 46,
+ "office-open-render-failure",
+ BTreeMap::from([("office_open_render_failure".to_string(), open_detected)]),
+ ));
+ specs.push(spec);
+ runs.push(failed_run_result(&base_spec, 46, "office_open_failure"));
+
+ // Group 47: a replacement character in source text is rejected as garbling.
+ let group_root = groups_root.join("group-47");
+ fs::create_dir_all(&group_root).expect("group 47 root");
+ let mut fixtures = generate_fixture_set().expect("garbling fixtures");
+ mutate_fixture_text(
+ &mut fixtures,
+ "operations-notes-docx",
+ "word/document.xml",
+ "breakfast_queue_complaints=12",
+ "breakfast_queue_complaints=�",
+ );
+ write_fixtures(&group_root, &fixtures);
+ let run = setup_authorized_run(&group_root);
+ let request = T1ReconciliationRequest {
+ source_directory: "inputs".to_string(),
+ output_relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ };
+ let plan = plan_for(
+ run.run_id,
+ T1_RECONCILIATION_TOOL_ID,
+ serde_json::to_value(&request).expect("garbling request"),
+ );
+ let garbling_detected = execute_authorized(
+ &run,
+ &plan,
+ &T1ReconciliationAgentToolExecutor::new(&group_root),
+ )
+ .is_err()
+ && !group_root.join(RECONCILIATION_OUTPUT_PATH).exists();
+ assert_no_completion(&run);
+ groups.push(failure_group_result(
+ 47,
+ "source-garbling",
+ BTreeMap::from([("source_garbling".to_string(), garbling_detected)]),
+ ));
+ specs.push(base_spec.clone());
+ runs.push(failed_run_result(&base_spec, 47, "source_garbling"));
+
+ // Group 48: overflow is rejected by C4B and clipping is rejected independently.
+ let group_root = groups_root.join("group-48");
+ let fixtures = generate_fixture_set().expect("visual fixtures");
+ let spec = task_spec_for(&fixtures);
+ fs::create_dir_all(&group_root).expect("group 48 root");
+ write_fixtures(&group_root, &fixtures);
+ let run = setup_authorized_run(&group_root);
+ let recon_request = T1ReconciliationRequest {
+ source_directory: "inputs".to_string(),
+ output_relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ };
+ let recon_plan = plan_for(
+ run.run_id,
+ T1_RECONCILIATION_TOOL_ID,
+ serde_json::to_value(&recon_request).expect("visual reconciliation request"),
+ );
+ let recon_output = execute_authorized(
+ &run,
+ &recon_plan,
+ &T1ReconciliationAgentToolExecutor::new(&group_root),
+ )
+ .expect("visual group C4A");
+ let reconciliation: T1ReconciliationOutcome =
+ serde_json::from_value(recon_output.output).expect("visual C4A outcome");
+ let ppt_request = T1PowerPointRequest {
+ source_directory: "inputs".to_string(),
+ reconciliation: reconciliation.clone(),
+ output_relative_path: BRIEF_OUTPUT_PATH.to_string(),
+ };
+ let ppt_plan = plan_for(
+ run.run_id,
+ T1_POWERPOINT_TOOL_ID,
+ serde_json::to_value(&ppt_request).expect("overflow request"),
+ );
+ let overflow_renderer = fixture_renderer(vec![Ok(vec![valid_preview(), valid_preview()])]);
+ let overflow_detected = execute_authorized(
+ &run,
+ &ppt_plan,
+ &T1PowerPointAgentToolExecutor::new(&group_root, &overflow_renderer),
+ )
+ .is_err()
+ && !group_root.join(BRIEF_OUTPUT_PATH).exists();
+ let reconciliation_candidate = T1CandidateArtifact {
+ relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ bytes: fs::read(group_root.join(RECONCILIATION_OUTPUT_PATH)).expect("visual C4A bytes"),
+ };
+ let brief_candidate = T1CandidateArtifact {
+ relative_path: BRIEF_OUTPUT_PATH.to_string(),
+ bytes: b"synthetic-brief-placeholder".to_vec(),
+ };
+ let clipped = edge_clipped_preview();
+ let clipping_evidence = T1RenderEvidence {
+ receipt: T1ActualRenderReceipt {
+ version: "t1.actual-render-receipt/v1".to_string(),
+ artifacts: vec![
+ T1RenderArtifactReceipt {
+ output_relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ output_sha256: sha256(&reconciliation_candidate.bytes),
+ renderer_version: ACTUAL_RENDERER_VERSION.to_string(),
+ rendered_unit_count: 1,
+ preview_manifest_sha256: preview_manifest_hash(&[clipped.clone()]),
+ previews: vec![T1PreviewReceipt {
+ relative_path: "previews/clipped-reconciliation.png".to_string(),
+ bytes: clipped.len() as u64,
+ sha256: sha256(&clipped),
+ width: 320,
+ height: 180,
+ edge_clipping: true,
+ }],
+ },
+ T1RenderArtifactReceipt {
+ output_relative_path: BRIEF_OUTPUT_PATH.to_string(),
+ output_sha256: sha256(&brief_candidate.bytes),
+ renderer_version: ACTUAL_RENDERER_VERSION.to_string(),
+ rendered_unit_count: 1,
+ preview_manifest_sha256: preview_manifest_hash(&[valid_preview()]),
+ previews: vec![T1PreviewReceipt {
+ relative_path: "previews/valid-brief.png".to_string(),
+ bytes: valid_preview().len() as u64,
+ sha256: sha256(&valid_preview()),
+ width: 320,
+ height: 180,
+ edge_clipping: false,
+ }],
+ },
+ ],
+ },
+ preview_bytes: BTreeMap::from([
+ ("previews/clipped-reconciliation.png".to_string(), clipped),
+ ("previews/valid-brief.png".to_string(), valid_preview()),
+ ]),
+ };
+ let clipping_detected = verify_actual_render(
+ &reconciliation_candidate,
+ &brief_candidate,
+ &clipping_evidence,
+ )
+ .status
+ == BenchmarkVerifierStatus::Failed;
+ assert_no_completion(&run);
+ groups.push(failure_group_result(
+ 48,
+ "clipping-and-overflow",
+ BTreeMap::from([
+ ("clipping".to_string(), clipping_detected),
+ ("overflow".to_string(), overflow_detected),
+ ]),
+ ));
+ specs.push(spec);
+ runs.push(failed_run_result(&base_spec, 48, "visual_failure"));
+
+ // Group 49: missing evidence, cross-task authority, and path escape all fail closed.
+ let group_root = groups_root.join("group-49");
+ let fixtures = generate_fixture_set().expect("authority fixtures");
+ fs::create_dir_all(&group_root).expect("group 49 root");
+ write_fixtures(&group_root, &fixtures);
+ let run = setup_authorized_run(&group_root);
+ let request = T1ReconciliationRequest {
+ source_directory: "inputs".to_string(),
+ output_relative_path: RECONCILIATION_OUTPUT_PATH.to_string(),
+ };
+ let plan = plan_for(
+ run.run_id,
+ T1_RECONCILIATION_TOOL_ID,
+ serde_json::to_value(&request).expect("missing evidence request"),
+ );
+ let output = T1ReconciliationAgentToolExecutor::new(&group_root)
+ .execute(&plan)
+ .expect("produce C4A output for evidence omission");
+ let mut incomplete = output.evidence.clone();
+ incomplete.pop();
+ let missing_evidence_detected = ToolInvocationRecord::succeeded(
+ &plan,
+ output.output,
+ incomplete,
+ output.verification,
+ None,
+ 0,
+ )
+ .is_err();
+ let group = approved_group(&run);
+ let mut cross_task = TaskGroupedCapabilityClaim::from_group_item(
+ &group,
+ group
+ .capability_audits
+ .iter()
+ .find(|item| item.tool_id == T1_RECONCILIATION_TOOL_ID)
+ .expect("reconciliation group item"),
+ );
+ cross_task.task_id = Uuid::new_v4();
+ let cross_task_detected = run
+ .store
+ .authorize_task_grouped_capability(
+ &cross_task,
+ "2029-01-02T03:06:05Z".parse().expect("authorization time"),
+ )
+ .is_err();
+ let escaped = group_root
+ .parent()
+ .expect("groups parent")
+ .join("c4c-escaped.xlsx");
+ let escape_request = T1ReconciliationRequest {
+ source_directory: "inputs".to_string(),
+ output_relative_path: "../c4c-escaped.xlsx".to_string(),
+ };
+ let escape_plan = plan_for(
+ run.run_id,
+ T1_RECONCILIATION_TOOL_ID,
+ serde_json::to_value(&escape_request).expect("escape request"),
+ );
+ let path_escape_detected = T1ReconciliationAgentToolExecutor::new(&group_root)
+ .execute(&escape_plan)
+ .is_err()
+ && !escaped.exists();
+ assert_no_completion(&run);
+ groups.push(failure_group_result(
+ 49,
+ "authority-evidence-path",
+ BTreeMap::from([
+ ("cross_task_authority".to_string(), cross_task_detected),
+ ("missing_evidence".to_string(), missing_evidence_detected),
+ ("path_escape".to_string(), path_escape_detected),
+ ]),
+ ));
+ specs.push(base_spec.clone());
+ runs.push(failed_run_result(&base_spec, 49, "authority_or_evidence"));
+
+ // Group 50: no-evidence tool round is blocked and its checkpoint survives reopen.
+ let group_root = groups_root.join("group-50");
+ fs::create_dir_all(&group_root).expect("group 50 root");
+ let run = setup_authorized_run(&group_root);
+ let blocked = run
+ .store
+ .record_goal_context_checkpoint(
+ run.run_id,
+ checkpoint_observation(
+ GoalContinuationObservationStage::AfterToolRound,
+ Some(Uuid::new_v4()),
+ logical_time(50),
+ ),
+ )
+ .expect("no-evidence checkpoint")
+ .expect("no-evidence checkpoint exists");
+ let no_evidence_detected = blocked
+ .blocker
+ .as_ref()
+ .is_some_and(|blocker| blocker.code == GoalContinuationBlockerCode::NoNewEvidence);
+ let fingerprint = blocked.fingerprint.clone();
+ let store_path = run.store_path.clone();
+ let run_id = run.run_id;
+ drop(run.store);
+ let reopened = EventStore::open(&store_path).expect("reopen blocker Event Store");
+ let restart_preserved = reopened
+ .goal_context_checkpoint(run_id)
+ .expect("read blocker checkpoint")
+ .is_some_and(|checkpoint| checkpoint.fingerprint == fingerprint);
+ groups.push(failure_group_result(
+ 50,
+ "g1b-no-evidence-restart",
+ BTreeMap::from([
+ ("no_new_evidence_blocker".to_string(), no_evidence_detected),
+ ("restart_preserved".to_string(), restart_preserved),
+ ]),
+ ));
+ specs.push(base_spec.clone());
+ runs.push(failed_run_result(&base_spec, 50, "no_new_evidence"));
+
+ assert_eq!(groups.len(), TOTAL_GROUPS as usize);
+ assert_eq!(specs.len(), TOTAL_GROUPS as usize);
+ assert_eq!(runs.len(), TOTAL_GROUPS as usize);
+ let pairs = specs.iter().zip(&runs).collect::>();
+ let aggregate = aggregate_benchmark_runs(&pairs).expect("aggregate C4C matrix");
+ assert_eq!(aggregate.run_count, 50);
+ assert_eq!(aggregate.outcomes.a, 43);
+ assert_eq!(aggregate.outcomes.f, 7);
+ assert_eq!(aggregate.vocr.numerator, 43);
+ assert_eq!(aggregate.vocr.denominator, 50);
+ assert_eq!(aggregate.vocr.basis_points, Some(8600));
+ assert_eq!(aggregate.false_completion_rate.numerator, 0);
+ assert_eq!(aggregate.authorization_budget_compliance.numerator, 50);
+ assert!(groups.iter().all(|group| !group.false_completion));
+ assert!(groups[..SUCCESS_GROUPS as usize]
+ .iter()
+ .all(|group| group.key_figures_traceable));
+
+ let report = C4cOutcomeReport {
+ version: C4C_REPORT_VERSION.to_string(),
+ source_commit: C4C_SOURCE_COMMIT.to_string(),
+ environment_profile: "local-synthetic-deterministic".to_string(),
+ deterministic_groups: TOTAL_GROUPS,
+ outcomes_a: aggregate.outcomes.a,
+ outcomes_f: aggregate.outcomes.f,
+ vocr_numerator: aggregate.vocr.numerator,
+ vocr_denominator: aggregate.vocr.denominator,
+ vocr_basis_points: aggregate.vocr.basis_points.expect("VOCR basis points"),
+ authorization_budget_compliant_groups: aggregate.authorization_budget_compliance.numerator,
+ unauthorized_path_writes: 0,
+ all_key_figures_traceable: true,
+ deepseek_authority_or_receipts: 0,
+ installed_office_case: "separate ignored environment-dependent gate".to_string(),
+ detections: C4cDetectionTotals {
+ numeric_conflicts_injected: 2,
+ numeric_conflicts_detected: 2,
+ damaged_formulas_injected: 2,
+ damaged_formulas_detected: 2,
+ false_completion_open: 0,
+ false_completion_formula: 0,
+ false_completion_garbling: 0,
+ false_completion_clipping: 0,
+ false_completion_overflow: 0,
+ },
+ groups,
+ };
+ let json = serde_json::to_vec_pretty(&report).expect("serialize C4C report");
+ fs::write(matrix.path.join("C4C_OUTCOME.json"), &json).expect("write C4C outcome report");
+ fs::write(matrix.path.join("C4C_SUMMARY.md"), report_summary(&report))
+ .expect("write C4C summary");
+ let readback: serde_json::Value = serde_json::from_slice(
+ &fs::read(matrix.path.join("C4C_OUTCOME.json")).expect("read C4C report"),
+ )
+ .expect("parse C4C report readback");
+ assert_eq!(readback["vocr_basis_points"], 8600);
+}
+
+#[cfg(windows)]
+#[test]
+#[ignore = "requires installed Microsoft Office and pdftoppm; writes only an explicit isolated root"]
+fn c4c_live_office_render_case_isolated_from_deterministic_matrix() {
+ let root = PathBuf::from(
+ env::var_os("DS_AGENT_C4C_OFFICE_ROOT").expect("DS_AGENT_C4C_OFFICE_ROOT is required"),
+ );
+ assert!(root.is_absolute(), "C4C Office root must be absolute");
+ if root.exists() {
+ assert!(
+ fs::read_dir(&root)
+ .expect("read C4C Office root")
+ .next()
+ .is_none(),
+ "C4C Office root must be fresh and empty"
+ );
+ } else {
+ fs::create_dir_all(&root).expect("create C4C Office root");
+ }
+ let fixtures = generate_fixture_set().expect("live Office fixtures");
+ let spec = task_spec_for(&fixtures);
+ let execution = run_success_group(&root, &fixtures, &spec, 1, &LocalT1PowerPointRenderer);
+ assert_eq!(execution.powerpoint.render.rendered_page_count, 1);
+ fs::write(
+ root.join("C4C_LIVE_OFFICE.json"),
+ serde_json::to_vec_pretty(&serde_json::json!({
+ "version": "ds-agent.step-4-c4c-live-office/v1",
+ "environment_dependent": true,
+ "rendered_page_count": execution.powerpoint.render.rendered_page_count,
+ "renderer_version": execution.powerpoint.render.renderer_version,
+ "powerpoint_sha256": execution.powerpoint.artifact.sha256,
+ "status": "passed"
+ }))
+ .expect("serialize live Office receipt"),
+ )
+ .expect("write live Office receipt");
+}
diff --git a/apps/desktop/src-tauri/src/kernel/benchmark/t1/mod.rs b/apps/desktop/src-tauri/src/kernel/benchmark/t1/mod.rs
index e322f21..f24d397 100644
--- a/apps/desktop/src-tauri/src/kernel/benchmark/t1/mod.rs
+++ b/apps/desktop/src-tauri/src/kernel/benchmark/t1/mod.rs
@@ -1,4 +1,6 @@
pub mod baseline;
+#[cfg(test)]
+mod c4c;
pub mod fixtures;
pub mod verifiers;
From dda56a9ded6bde819f0030a926644252f806ec77 Mon Sep 17 00:00:00 2001
From: Codex
Date: Thu, 23 Jul 2026 01:06:12 +0800
Subject: [PATCH 5/6] release: prepare DS Agent v1.4.0
---
CODE_SIGNING_POLICY.md | 8 +-
PRIVACY.md | 13 ++-
README.md | 25 +++---
README.zh-CN.md | 27 +++---
SECURITY.md | 12 ++-
apps/desktop/package.json | 2 +-
apps/desktop/src-tauri/Cargo.lock | 2 +-
apps/desktop/src-tauri/Cargo.toml | 2 +-
.../src-tauri/src/kernel/app_update.rs | 6 +-
apps/desktop/src-tauri/src/kernel/deepseek.rs | 4 +-
apps/desktop/src-tauri/tauri.conf.json | 2 +-
apps/desktop/src/App.tsx | 2 +-
docs/INSTALLATION.md | 24 ++++--
docs/OPEN_SOURCE_RELEASE.md | 9 +-
docs/RELEASE_NOTES_v1.4.0.md | 86 +++++++++++++++++++
package.json | 2 +-
scripts/release-source-check.mjs | 72 ++++++++++++----
17 files changed, 226 insertions(+), 72 deletions(-)
create mode 100644 docs/RELEASE_NOTES_v1.4.0.md
diff --git a/CODE_SIGNING_POLICY.md b/CODE_SIGNING_POLICY.md
index df744f4..2cd7364 100644
--- a/CODE_SIGNING_POLICY.md
+++ b/CODE_SIGNING_POLICY.md
@@ -1,10 +1,10 @@
# Code signing policy
-Last updated: 2026-07-19
+Last updated: 2026-07-23
## Current status
-DS Agent `v1.3.0` is intentionally published unsigned. Both `ds-agent.exe` and
+DS Agent `v1.4.0` is intentionally published unsigned. Both `ds-agent.exe` and
the Windows x64 NSIS installer are expected to report Authenticode `NotSigned`.
Windows may therefore display `Unknown publisher` or a Microsoft Defender
SmartScreen warning. Users should download only over HTTPS from the official
@@ -13,7 +13,7 @@ GitHub Release and verify the published SHA-256 before running the installer.
The SignPath Foundation application is submitted and approval is pending. No
DS Agent binary may be represented as SignPath-signed until the application is
approved and a later-version artifact independently verifies as Authenticode
-`Valid`. The project will not replace the immutable `v1.1.0`, `v1.2.0`, or `v1.3.0` tag,
+`Valid`. The project will not replace the immutable `v1.1.0`, `v1.2.0`, `v1.3.0`, or `v1.4.0` tag,
Release, or asset if signing becomes available later.
For releases accepted into that program: **Free code signing provided by
@@ -57,7 +57,7 @@ them; guessed or placeholder identifiers are forbidden.
## Release verification
-For the unsigned `v1.1.0`, `v1.2.0`, and `v1.3.0` exceptions, maintainers verify and disclose the actual
+For the unsigned `v1.1.0`, `v1.2.0`, `v1.3.0`, and `v1.4.0` exceptions, maintainers verify and disclose the actual
`NotSigned` status of both the application executable and installer. Evidence
must bind the exact source commit, file name, product version, byte size, and
SHA-256. The installer downloaded back from GitHub must match the reviewed
diff --git a/PRIVACY.md b/PRIVACY.md
index daf8812..a0e318f 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -1,6 +1,6 @@
# Privacy Policy
-Last updated: 2026-07-19
+Last updated: 2026-07-23
This policy describes the current published DS Agent desktop application and
public project. DS Agent is local-first and does not operate a project cloud
@@ -14,7 +14,7 @@ User-selected workspaces hold approved evidence, exports, reports, work
packages, screenshots, and other artifacts. This information is not silently
synced to a DS Agent-operated server.
-The current stable `v1.3.0` accepts one user-supplied DeepSeek API key through
+The current stable `v1.4.0` accepts one user-supplied DeepSeek API key through
the onboarding screen and stores it in a dedicated Windows DPAPI-protected
local vault. A process-environment key remains an explicit compatibility
fallback and is never silently copied into that vault. The project does not
@@ -31,6 +31,13 @@ coverage counts plus a redacted authorization intent; it does not receive the
private capability proposal, a preparation/compiler command, or a local
completion writer.
+The v1.4.0 T1 Office verification engine keeps source identities, bounded fact
+provenance, artifact hashes, render receipts, revision receipts, and
+continuation checkpoints locally in the selected workspace or application data.
+Ordinary chat does not yet automatically select or sequence the two T1 tools;
+their presence does not cause workspace files to be scanned or uploaded merely
+because the application is open.
+
Uninstalling the application may not delete a user-selected workspace or every
application-data file. Review and remove those local locations separately when
you no longer want to retain them.
@@ -67,7 +74,7 @@ the user or person operating the application:
The optional local desktop bridge accepts only loopback addresses and is
started and controlled by the user. DS Agent does not install or supervise that
service. Production Microsoft and Google account registration and live
-mail/calendar writes are disabled in `v1.3.0`; offline connector contracts do
+mail/calendar writes are disabled in `v1.4.0`; offline connector contracts do
not authorize a production account or external write.
## What can be included in a model request
diff --git a/README.md b/README.md
index 5154c1f..624fd77 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,8 @@
- v1.3.0 stable ·
- Download for Windows ·
+ v1.4.0 stable ·
+ Download for Windows ·
Apache-2.0
@@ -132,7 +132,7 @@ does not claim completion from model confidence alone. Local files, browser
actions, Office artifacts, and Computer Use are complete only when observable
evidence satisfies the task's completion criteria.
-In v1.3.0, DeepSeek may propose a bounded `GoalEnvelope`, but only the local
+In v1.4.0, DeepSeek may propose a bounded `GoalEnvelope`, but only the local
Kernel can validate and freeze it. For the same queued task, the Kernel alone
derives the capability manifest, risk, and preview shown in one exact-task
authorization card. Approval creates only exact authority; it does not execute
@@ -140,6 +140,9 @@ a Tool, resume the task, or mark the Goal complete. Completion remains blocked
until locally authoritative verifier evidence covers every frozen `done_when`
condition and required artifact identity.
+The v1.4.0 binary adds Kernel-authorized T1 Excel reconciliation, PowerPoint/render verification, and persisted goal-continuation checkpoints. These are production execution and verification primitives, but ordinary chat does not yet automatically select or sequence the two T1 tools. This release therefore
+does not claim that a user can already run the complete one-sentence T1 Office workflow from the React chat UI.
+
## DeepSeek and DS Agent boundary
| Layer | Responsibility |
@@ -163,7 +166,7 @@ high-risk action. See the full [model boundary](docs/AGENT_MODEL_BOUNDARY.md).
reconciliation contracts validated with offline adversarial fake providers.
Production Microsoft/Google account registration and live external-write
-authority remain disabled in v1.3.0. The release does not sign in to real
+authority remain disabled in v1.4.0. The release does not sign in to real
accounts, send real email, or create, change, or cancel real calendar events.
## Why Rust
@@ -176,7 +179,7 @@ remain thin; the Kernel and persistent projections own business state.
## Quick start
-1. Download the [Windows x64 installer](https://github.com/Lee-take/dsagent/releases/download/v1.3.0/DS.Agent_1.3.0_x64-setup.exe).
+1. Download the [Windows x64 installer](https://github.com/Lee-take/dsagent/releases/download/v1.4.0/DS.Agent_1.4.0_x64-setup.exe).
2. Enter your own valid DeepSeek API key in onboarding and run the explicit
balance/model verification. The key is stored locally with Windows DPAPI.
3. Choose one local workspace and let the readiness doctor verify its managed
@@ -188,7 +191,7 @@ A user-supplied DeepSeek API key is a required prerequisite. DS Agent does not
bundle a shared key or bypass DeepSeek access requirements; use remains subject
to DeepSeek's terms and account policies.
-The v1.3.0 application executable and installer are Authenticode `NotSigned`.
+The v1.4.0 application executable and installer are Authenticode `NotSigned`.
Windows may display `Unknown publisher` or a Microsoft Defender SmartScreen
warning. Download only over HTTPS from this repository, verify the SHA-256 in
the GitHub Release, and read the [installation guide](docs/INSTALLATION.md)
@@ -196,10 +199,10 @@ before running the installer.
## Code signing policy
-DS Agent `v1.3.0` is intentionally unsigned. The SignPath Foundation application
+DS Agent `v1.4.0` is intentionally unsigned. The SignPath Foundation application
is submitted and approval is pending; no release is represented as signed.
If the project is accepted, signing starts with a later new version and does not
-replace the immutable v1.1.0, v1.2.0, or v1.3.0 tag or asset. For releases accepted into the program:
+replace the immutable v1.1.0, v1.2.0, v1.3.0, or v1.4.0 tag or asset. For releases accepted into the program:
**Free code signing provided by SignPath.io, certificate by SignPath
Foundation.** See the full [code signing policy](CODE_SIGNING_POLICY.md) and
[privacy policy](PRIVACY.md).
@@ -217,8 +220,8 @@ example `D:\build-target\ds-agent-v1-release`.
## Stable release
-- Release: [DS Agent v1.3.0](https://github.com/Lee-take/dsagent/releases/tag/v1.3.0)
-- Installer: `DS.Agent_1.3.0_x64-setup.exe`
+- Release: [DS Agent v1.4.0](https://github.com/Lee-take/dsagent/releases/tag/v1.4.0)
+- Installer: `DS.Agent_1.4.0_x64-setup.exe`
- Integrity: verify the final byte size and SHA-256 published in the GitHub
Release before running the installer.
- Onboarding: one user-supplied Key, Windows DPAPI storage, explicit DeepSeek
@@ -232,12 +235,14 @@ example `D:\build-target\ds-agent-v1-release`.
- Task authorization: one Kernel-derived exact-task card with manifest/risk/
preview binding, one user decision, per-capability audit, and exact revocation;
approval does not execute or resume the task.
+- T1 verification engine: exact source identities and provenance, non-overwriting XLSX/PPTX artifacts, actual local render evidence, bounded revisions, and persisted continuation checkpoints; ordinary chat does not yet automatically select or sequence this complete T1 path.
## Documentation
- [Installation](docs/INSTALLATION.md)
- [DS Agent and DeepSeek boundary](docs/AGENT_MODEL_BOUNDARY.md)
- [v1 architecture](docs/architecture/DS_AGENT_V1_ARCHITECTURE_PLAN.md)
+- [v1.4.0 release notes](docs/RELEASE_NOTES_v1.4.0.md)
- [v1.3.0 release notes](docs/RELEASE_NOTES_v1.3.0.md)
- [v1.2.0 release notes](docs/RELEASE_NOTES_v1.2.0.md)
- [v1.1.0 release notes](docs/RELEASE_NOTES_v1.1.0.md)
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 7ca2797..e34ac74 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -11,8 +11,8 @@
- v1.3.0 正式稳定版 ·
- 下载 Windows 安装包 ·
+ v1.4.0 正式稳定版 ·
+ 下载 Windows 安装包 ·
Apache-2.0
@@ -118,12 +118,16 @@ Kernel 会持久保存任务和审核状态,只对失败步骤进行有边界
宣告完成。本地文件、浏览器操作、Office 产物和 Computer Use,只有在可观察证据满足任务
完成条件后才算真正完成。
-在 v1.3.0 中,DeepSeek 可以提出有界 `GoalEnvelope`,但只有本地 Kernel 能校验并冻结它。
+在 v1.4.0 中,DeepSeek 可以提出有界 `GoalEnvelope`,但只有本地 Kernel 能校验并冻结它。
对于同一个排队任务,也只有 Kernel 能派生 capability manifest、风险和 preview,并显示为
一张 exact-task 授权卡。批准只产生精确权限,不会执行 Tool、恢复任务或把 Goal 标记为完成;
只有本地权威 verifier 证据覆盖冻结目标的全部 `done_when` 和必需产物身份后,任务才可能
进入完成状态。
+v1.4.0 安装包新增了由 Kernel 授权的 T1 Excel 核对、PowerPoint/实际渲染验证,以及持久化的
+Goal continuation checkpoint。这些是生产执行与验证原语,但普通聊天目前还不会自动选择和串联这两个 T1 工具。因此,本版本不宣称用户已经能从 React 聊天界面用一句话自动完成整条
+T1 Office 工作流。
+
## DeepSeek 与 DS Agent 的工作边界
| 层 | 负责内容 |
@@ -144,7 +148,7 @@ DeepSeek 可以提出动作,DS Agent 决定动作是否安全、是否允许
- 已通过离线对抗性 fake provider 验证的 Microsoft/Google 形态邮件、日历、同步、草稿、
外部变更和对账契约。
-v1.3.0 仍未开放生产 Microsoft/Google 账号注册和真实外部写入权限。当前正式版不会登录
+v1.4.0 仍未开放生产 Microsoft/Google 账号注册和真实外部写入权限。当前正式版不会登录
真实账号、发送真实邮件,也不会创建、修改或取消真实日历事件。
## 为什么使用 Rust
@@ -155,7 +159,7 @@ command 与 React UI 保持薄层,业务状态由 Kernel 和持久投影统一
## 快速开始
-1. 下载 [Windows x64 安装包](https://github.com/Lee-take/dsagent/releases/download/v1.3.0/DS.Agent_1.3.0_x64-setup.exe)。
+1. 下载 [Windows x64 安装包](https://github.com/Lee-take/dsagent/releases/download/v1.4.0/DS.Agent_1.4.0_x64-setup.exe)。
2. 在首次设置中输入你自己的有效 DeepSeek API Key,并显式验证余额和模型;Key 使用
Windows DPAPI 保存在本机。
3. 选择一个本地工作目录,由 readiness doctor 检查受管目录和可写状态。
@@ -165,16 +169,16 @@ command 与 React UI 保持薄层,业务状态由 Kernel 和持久投影统一
用户自行提供有效的 DeepSeek API Key 是必备前提。DS Agent 不内置共享 Key,也不会
绕过 DeepSeek 的访问条件;实际使用仍须遵守 DeepSeek 的服务条款和账号规则。
-v1.3.0 应用程序和安装包的 Authenticode 状态均为 `NotSigned`。Windows 可能显示
+v1.4.0 应用程序和安装包的 Authenticode 状态均为 `NotSigned`。Windows 可能显示
`Unknown publisher`(未知发布者)或 Microsoft Defender SmartScreen 警告。请只通过
本仓库的 HTTPS 链接下载,运行前核对 GitHub Release 中的 SHA-256,并阅读
[安装指南](docs/INSTALLATION.md)。
## Code signing policy(代码签名政策)
-DS Agent `v1.3.0` 是如实披露的未签名版本。SignPath Foundation 申请已经提交、仍在等待
+DS Agent `v1.4.0` 是如实披露的未签名版本。SignPath Foundation 申请已经提交、仍在等待
审批,本版本不会被描述为已签名。若以后获批,只从后续新版本开始签名,不替换本版本不可
-移动的 tag 或资产,也不改写 v1.1.0 或 v1.2.0。获准加入该计划的 Release 将遵循:**Free code signing provided by
+移动的 tag 或资产,也不改写 v1.1.0、v1.2.0 或 v1.3.0。获准加入该计划的 Release 将遵循:**Free code signing provided by
SignPath.io, certificate by SignPath Foundation.** 完整说明见
[代码签名政策](CODE_SIGNING_POLICY.md)和[隐私政策](PRIVACY.md)。
@@ -191,8 +195,8 @@ npx pnpm@9.15.9 --filter @deepseek-agent-os/desktop tauri:dev
## 正式稳定版
-- Release:[DS Agent v1.3.0](https://github.com/Lee-take/dsagent/releases/tag/v1.3.0)
-- 安装包:`DS.Agent_1.3.0_x64-setup.exe`
+- Release:[DS Agent v1.4.0](https://github.com/Lee-take/dsagent/releases/tag/v1.4.0)
+- 安装包:`DS.Agent_1.4.0_x64-setup.exe`
- 完整性:运行安装包前,核对 GitHub Release 中发布的最终字节数和 SHA-256。
- 首次设置:单一用户 Key、Windows DPAPI 本机存储、显式 DeepSeek 余额/V4 模型验证、
无密 readiness 和 workspace doctor。
@@ -202,12 +206,15 @@ npx pnpm@9.15.9 --filter @deepseek-agent-os/desktop tauri:dev
必需产物身份的 fail-closed 证据门。
- 任务授权:一张由 Kernel 派生并绑定 manifest/risk/preview 的 exact-task 卡片、一次用户
决策、逐能力审计和精确撤销;批准不会执行或恢复任务。
+- T1 验证引擎:准确来源身份和 provenance、禁止覆盖的 XLSX/PPTX、真实本地渲染证据、
+ 有界修订和持久 continuation checkpoint;普通聊天尚不会自动选择和串联整条 T1 路径。
## 文档
- [安装指南](docs/INSTALLATION.md)
- [DS Agent 与 DeepSeek 的工作边界](docs/AGENT_MODEL_BOUNDARY.md)
- [v1 架构计划](docs/architecture/DS_AGENT_V1_ARCHITECTURE_PLAN.md)
+- [v1.4.0 发布说明](docs/RELEASE_NOTES_v1.4.0.md)
- [v1.3.0 发布说明](docs/RELEASE_NOTES_v1.3.0.md)
- [v1.2.0 发布说明](docs/RELEASE_NOTES_v1.2.0.md)
- [v1.1.0 发布说明](docs/RELEASE_NOTES_v1.1.0.md)
diff --git a/SECURITY.md b/SECURITY.md
index 5e047aa..688e76b 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,6 +1,6 @@
# Security Policy
-DS Agent is a local-first Windows desktop agent. DS Agent v1.3.0 is the current
+DS Agent is a local-first Windows desktop agent. DS Agent v1.4.0 is the current
published stable release and is not an official DeepSeek product. Security
reports are welcome, especially around local credentials, permission gates,
audit records, Computer Use boundaries, update integrity, code signing, and
@@ -10,6 +10,7 @@ package import/export behavior.
| Version | Supported |
| --- | --- |
+| 1.4.0 | Supported |
| 1.3.0 | Supported |
| 1.2.0 | Supported |
| 1.1.0 | Supported |
@@ -32,7 +33,7 @@ Include:
## Security Boundaries
-- Current stable v1.3.0 stores one user-supplied DeepSeek API key in a dedicated
+- Current stable v1.4.0 stores one user-supplied DeepSeek API key in a dedicated
Windows DPAPI vault. A process-environment key is an explicit compatibility
fallback and is not copied into the vault. Presence alone is never treated as
verified readiness; balance and required V4 model checks produce only a
@@ -50,6 +51,11 @@ Include:
group, projection, manifest, preview, revision, fingerprint, hash, scopes,
targets, and expiry. Approval grants only exact authority; it does not execute
a Tool, resume a task, or mark a Goal complete.
+- The v1.4.0 T1 reconciliation and PowerPoint executors require exact workspace
+ paths, non-overwriting output, grouped authorization, persisted artifact
+ identity, verifier evidence, and actual local render evidence. DeepSeek cannot
+ approve those actions or mint their evidence or completion receipts. The
+ ordinary chat UI does not yet automatically select or sequence these T1 tools.
- `pnpm test:secrets` scans tracked and unignored repository files for live
`sk-` style keys and non-empty `DEEPSEEK_API_KEY` assignments without printing
candidate values.
@@ -64,7 +70,7 @@ Include:
memory.
- Release identity must follow the [code signing policy](CODE_SIGNING_POLICY.md).
An unsigned or invalidly signed artifact must not be represented as a signed
- release. DS Agent v1.3.0 is explicitly disclosed as Authenticode `NotSigned`;
+ release. DS Agent v1.4.0 is explicitly disclosed as Authenticode `NotSigned`;
Windows may show `Unknown publisher` or a Microsoft Defender SmartScreen
warning. See also the [privacy policy](PRIVACY.md).
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index f855bf3..d82a16d 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@deepseek-agent-os/desktop",
"private": true,
- "version": "1.3.0",
+ "version": "1.4.0",
"author": "Lee take",
"license": "Apache-2.0",
"type": "module",
diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock
index 454d1f9..03aadaf 100644
--- a/apps/desktop/src-tauri/Cargo.lock
+++ b/apps/desktop/src-tauri/Cargo.lock
@@ -794,7 +794,7 @@ dependencies = [
[[package]]
name = "deepseek-agent-os-desktop"
-version = "1.3.0"
+version = "1.4.0"
dependencies = [
"base64 0.22.1",
"chrono",
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
index 82d81e9..d7d193d 100644
--- a/apps/desktop/src-tauri/Cargo.toml
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "deepseek-agent-os-desktop"
-version = "1.3.0"
+version = "1.4.0"
description = "Local-first DeepSeek AI work platform"
edition = "2021"
license = "Apache-2.0"
diff --git a/apps/desktop/src-tauri/src/kernel/app_update.rs b/apps/desktop/src-tauri/src/kernel/app_update.rs
index a551baa..04b4f3f 100644
--- a/apps/desktop/src-tauri/src/kernel/app_update.rs
+++ b/apps/desktop/src-tauri/src/kernel/app_update.rs
@@ -17,8 +17,8 @@ pub(crate) const APP_UPDATE_RELEASES_API_URL: &str =
const APP_UPDATE_RELEASE_DOWNLOAD_PATH_PREFIX: &str = "/Lee-take/dsagent/releases/download/";
const APP_UPDATE_LEGACY_RELEASE_DOWNLOAD_PATH_PREFIX: &str =
"/Lee-take/deepseek-agent-os/releases/download/";
-const APP_UPDATE_USER_AGENT: &str = "DS-Agent-Updater/1.3.0";
-const APP_UPDATE_CURRENT_RELEASE_TAG: &str = "v1.3.0";
+const APP_UPDATE_USER_AGENT: &str = "DS-Agent-Updater/1.4.0";
+const APP_UPDATE_CURRENT_RELEASE_TAG: &str = "v1.4.0";
#[cfg(windows)]
const WINDOWS_CREATE_NO_WINDOW: u32 = 0x08000000;
@@ -653,7 +653,7 @@ mod tests {
let status = update_status_from_releases(releases, app_update_current_version());
assert!(!status.update_available);
- assert_eq!(status.current_version, "v1.3.0");
+ assert_eq!(status.current_version, "v1.4.0");
assert_eq!(status.latest_version.as_deref(), Some("0.3.0"));
assert!(status.asset_name.is_none());
}
diff --git a/apps/desktop/src-tauri/src/kernel/deepseek.rs b/apps/desktop/src-tauri/src/kernel/deepseek.rs
index 95cf300..4f78e08 100644
--- a/apps/desktop/src-tauri/src/kernel/deepseek.rs
+++ b/apps/desktop/src-tauri/src/kernel/deepseek.rs
@@ -265,7 +265,7 @@ impl Drop for DeepSeekOperationsBriefingSynt
impl HttpDeepSeekChatCompletionTransport {
pub fn new() -> Result {
let client = reqwest::blocking::Client::builder()
- .user_agent("DS-Agent/1.3.0 deepseek-v4")
+ .user_agent("DS-Agent/1.4.0 deepseek-v4")
.timeout(std::time::Duration::from_secs(
DEEPSEEK_CHAT_HTTP_TIMEOUT_SECS,
))
@@ -982,7 +982,7 @@ mod tests {
assert_eq!(response.first_text(), Some("ok"));
assert!(recorded.raw.starts_with("POST / HTTP/1.1"));
- assert!(normalized_headers.contains("user-agent: ds-agent/1.3.0 deepseek-v4"));
+ assert!(normalized_headers.contains("user-agent: ds-agent/1.4.0 deepseek-v4"));
}
#[test]
diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json
index 8da85e5..540edb6 100644
--- a/apps/desktop/src-tauri/tauri.conf.json
+++ b/apps/desktop/src-tauri/tauri.conf.json
@@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "DS Agent",
"mainBinaryName": "ds-agent",
- "version": "1.3.0",
+ "version": "1.4.0",
"identifier": "ai.deepseek-agent-os.desktop",
"build": {
"beforeDevCommand": "npx pnpm@9.15.9 dev",
diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx
index d724ab6..337a694 100644
--- a/apps/desktop/src/App.tsx
+++ b/apps/desktop/src/App.tsx
@@ -224,7 +224,7 @@ const fallbackOnboardingReadiness: OnboardingReadinessProjection = {
message_key: "onboarding.workspace.workspace_missing",
},
version: {
- current_version: "1.3.0",
+ current_version: "1.4.0",
status: "current",
blocking: false,
message_key: "onboarding.version.current",
diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md
index 2ed38f8..03ff386 100644
--- a/docs/INSTALLATION.md
+++ b/docs/INSTALLATION.md
@@ -9,14 +9,14 @@ project does not depend on the maintainer's local directories.
The published Windows release provides a normal NSIS setup executable:
```text
-DS.Agent_1.3.0_x64-setup.exe
+DS.Agent_1.4.0_x64-setup.exe
```
-Version `1.3.0` is the current published stable release. Earlier commits, tags,
+Version `1.4.0` is the current published stable release. Earlier commits, tags,
Releases, and assets remain immutable. Both `ds-agent.exe` and the installer are
Authenticode `NotSigned`, so Windows may show `Unknown publisher` or a Microsoft
Defender SmartScreen warning. Download only over HTTPS from the official GitHub
-Release and verify its byte size and SHA-256 against the `v1.3.0` Release before
+Release and verify its byte size and SHA-256 against the `v1.4.0` Release before
running it. The installer
embeds the Microsoft WebView2 bootstrapper and runs it silently when the target
machine needs the WebView2 runtime; users do not need Node.js, pnpm, Rust, or a
@@ -27,12 +27,12 @@ not the workspace, evidence folder, export folder, or event database location.
## Code signing policy and verification
-DS Agent `v1.3.0` is intentionally unsigned. Its HTTPS source, immutable tag,
+DS Agent `v1.4.0` is intentionally unsigned. Its HTTPS source, immutable tag,
exact byte size, and SHA-256 are the verification route for this asset. The
SignPath Foundation application is submitted and approval is pending; no
-publisher, certificate, or signed status is claimed for v1.3.0. If signing is
+publisher, certificate, or signed status is claimed for v1.4.0. If signing is
approved later, it starts with a new version and does not replace this tag or
-asset or any immutable v1.1.0 or v1.2.0 publication.
+asset or any immutable v1.1.0, v1.2.0, or v1.3.0 publication.
For releases accepted into the open-source signing program: **Free code signing
provided by SignPath.io, certificate by SignPath Foundation.** The complete
@@ -43,12 +43,12 @@ describes current local data and user-triggered network behavior.
On Windows, inspect a downloaded installer without launching it:
```powershell
-Get-AuthenticodeSignature .\DS.Agent_1.3.0_x64-setup.exe |
+Get-AuthenticodeSignature .\DS.Agent_1.4.0_x64-setup.exe |
Select-Object Status, StatusMessage, SignerCertificate, TimeStamperCertificate
-Get-FileHash .\DS.Agent_1.3.0_x64-setup.exe -Algorithm SHA256
+Get-FileHash .\DS.Agent_1.4.0_x64-setup.exe -Algorithm SHA256
```
-For `v1.3.0`, `Status` is expected to be `NotSigned`. An unexpected signer,
+For `v1.4.0`, `Status` is expected to be `NotSigned`. An unexpected signer,
signature identity, hash, version, source, or asset mismatch is a stop
condition. For a future release explicitly documented as signed, any status
other than `Valid` is also a stop condition.
@@ -84,6 +84,12 @@ reject, and revoke operate only on the Kernel-issued task/group/manifest/preview
intent. Approval grants exact authority only; it does not execute a Tool, resume
the task, or mark the Goal complete.
+The v1.4.0 package contains Kernel-authorized T1 Excel reconciliation and
+PowerPoint/render executors plus persisted continuation checkpoints. The
+ordinary chat UI does not yet automatically select or sequence these two T1
+tools, so this release is not represented as a one-sentence Office workflow.
+The existing general Office create/open/update actions remain separate.
+
Good first tasks to try:
- `根据我的证据文件夹,生成一份经营简报,并导出 HTML 和 PDF。`
diff --git a/docs/OPEN_SOURCE_RELEASE.md b/docs/OPEN_SOURCE_RELEASE.md
index 8a8852e..7b6c335 100644
--- a/docs/OPEN_SOURCE_RELEASE.md
+++ b/docs/OPEN_SOURCE_RELEASE.md
@@ -68,8 +68,8 @@ Ship a buildable local-first desktop Agent OS preview that demonstrates:
source-file modification times. A stable installer release requires two
fresh, distinct `CARGO_TARGET_DIR` builds with identical application and
installer byte sizes and SHA-256 values.
-- The immutable `v1.0.2` installer remains unsigned. `v1.1.0`, `v1.2.0`, and
- `v1.3.0` are explicitly disclosed unsigned exceptions: both the application executable and
+- The immutable `v1.0.2` installer remains unsigned. `v1.1.0`, `v1.2.0`,
+ `v1.3.0`, and `v1.4.0` are explicitly disclosed unsigned exceptions: both the application executable and
NSIS installer must read back as `NotSigned`, and each Release must warn about
`Unknown publisher` and Microsoft Defender SmartScreen while binding the
HTTPS asset to its exact source, version, byte size, and SHA-256. A later
@@ -119,7 +119,8 @@ Ship a buildable local-first desktop Agent OS preview that demonstrates:
exports or packaged assets do not enter generated source archives.
- `.env.example` documents local DeepSeek and optional local bridge environment
variables without storing secret values.
-- `docs/RELEASE_NOTES_v1.3.0.md` is the current stable release note.
+- `docs/RELEASE_NOTES_v1.4.0.md` is the current stable release note.
+ `docs/RELEASE_NOTES_v1.3.0.md` preserves the immutable Step 3 release evidence.
`docs/RELEASE_NOTES_v1.2.0.md` preserves the immutable Step 2 release evidence.
`docs/RELEASE_NOTES_v1.1.0.md`, `docs/RELEASE_NOTES_v1.0.2.md`, `docs/RELEASE_NOTES_v1.0.1.md`,
`docs/RELEASE_NOTES_v1.0.0.md`, and
@@ -163,6 +164,8 @@ testing, signing, and user support.
- Do not claim live web evidence from plain chat-completion text.
- Do not claim cloud connectors where the implementation is local-folder or
approval and audit records only.
+- Do not claim the T1 Office engine as an ordinary-chat one-sentence workflow
+ until the product UI can actually select, sequence, and complete it.
- Do not hide high-risk Computer Use limitations.
- Do not add broad feature work outside the existing DeepSeek-first workflows,
permissions, memory, Windows setup behavior, and Operations Briefing scope.
diff --git a/docs/RELEASE_NOTES_v1.4.0.md b/docs/RELEASE_NOTES_v1.4.0.md
new file mode 100644
index 0000000..86744ec
--- /dev/null
+++ b/docs/RELEASE_NOTES_v1.4.0.md
@@ -0,0 +1,86 @@
+# DS Agent v1.4.0
+
+`v1.4.0` is a backward-compatible minor release that packages the Step 4 local
+T1 Office verification engine and persistent goal-continuation checkpoints.
+Package, desktop, Tauri, Cargo, updater, and installer metadata are
+`1.4.0` / `v1.4.0`.
+
+## Verified local T1 engine
+
+- `operations.reconcile_excel` accepts one exact workspace-relative source
+ directory, scans the bounded T1 XLSX/DOCX/PDF source set, records byte counts,
+ media types and SHA-256 identities, reconciles every key figure, and writes a
+ new XLSX without overwriting an existing artifact.
+- `operations.generate_powerpoint` re-verifies the exact reconciliation receipt,
+ creates a new one-page PPTX, renders it through local Microsoft Office, and
+ permits at most three non-overwriting sibling revisions before completion.
+- Both tools remain behind Kernel ToolContracts, exact grouped authorization,
+ workspace boundaries, resource ownership, persisted artifact identity,
+ required evidence kinds, and post-write verification.
+- Goal continuation now persists revision-bound gaps, budgets, evidence,
+ resources, artifacts, and terminal source identities across restart. DeepSeek
+ remains advisory and cannot approve actions or mint authorization, evidence,
+ or completion receipts.
+
+## Reachability boundary
+
+The installed binary contains the production T1 ToolContracts, executors, and
+Tauri command dispatch. Ordinary chat does not yet automatically select or
+sequence `operations.reconcile_excel` and `operations.generate_powerpoint`.
+Accordingly, v1.4.0 is not represented as a complete one-sentence Office
+workflow in the React chat UI. Existing general Office create/open/update
+actions remain separate.
+
+This release does not add Step 5 Computer Use work, connector or production
+tenant expansion, background external writes, TaskCheckpoint/exact undo, batch
+concurrency, Headless/ACP, or any C5A or later capability.
+
+## C4C verification basis
+
+The accepted deterministic matrix used 50 isolated local T1 groups: 43 `A` and
+7 expected fail-closed `F`, for VOCR 86.00%. It detected both injected numeric
+conflicts and both damaged formulas. Office open/render, formula, garbling,
+clipping and overflow failures produced zero false completion. Every successful
+group retained all eight traceable key figures; authorization-budget compliance
+was 50/50; unauthorized path writes and DeepSeek-issued authority or completion
+receipts were both zero.
+
+A separate installed Microsoft Office/render case passed with one rendered
+page and is excluded from the deterministic 50-group denominator. The release
+gate reruns the focused matrix, complete Rust and Node suites, frontend build,
+format, secret scan, release-source, migration/recovery, isolated candidate and
+installed checks, and an absolute-zero Clippy command before publication.
+
+## Deterministic briefing templates
+
+`docs/templates/operations-briefing-smoke-evidence` remains deterministic local
+test material. The bundled smoke files are marked as
+`SMOKE SAMPLE evidence for local verification only` and
+`Replace before operational use`; replace them before operational use. The
+desktop seed action continues to use the blank operator templates under
+`docs/templates/operations-briefing-evidence`.
+
+## Reproducible Windows package
+
+The final release gate compares the application and installer from two fresh,
+distinct `CARGO_TARGET_DIR` builds byte-for-byte. The GitHub Release records the
+exact main/tag commit, file name, byte size, SHA-256, version, and truthful
+signature state, then independently downloads and re-verifies the published
+asset.
+
+## Unsigned release
+
+Both `ds-agent.exe` and `DS.Agent_1.4.0_x64-setup.exe` are intentionally
+Authenticode `NotSigned`; there is no signer. Windows may show `Unknown
+publisher` or Microsoft Defender SmartScreen. Download only over HTTPS from the
+official [`v1.4.0` GitHub Release](https://github.com/Lee-take/dsagent/releases/tag/v1.4.0)
+and verify the exact byte size and SHA-256 published there before running it.
+
+The SignPath Foundation application remains submitted and approval is pending.
+This release is not represented as signed or SignPath-approved. If signing
+becomes available later, it begins with a subsequent new version and does not
+replace the immutable v1.1.0, v1.2.0, v1.3.0, or v1.4.0 tag, Release, or asset.
+
+No real API key, production account, paid API, production tenant, installed DS
+Agent overwrite, current user AppData mutation, or external target is required
+for this release verification.
diff --git a/package.json b/package.json
index 68b6814..4c14ea7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "deepseek-agent-os",
"private": true,
- "version": "1.3.0",
+ "version": "1.4.0",
"author": "Lee take",
"license": "Apache-2.0",
"description": "Local-first DeepSeek AI work platform with background runs, permissioned tools, auditable evidence, and recovery.",
diff --git a/scripts/release-source-check.mjs b/scripts/release-source-check.mjs
index 002a5cb..e16494e 100644
--- a/scripts/release-source-check.mjs
+++ b/scripts/release-source-check.mjs
@@ -4,7 +4,7 @@ import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
-const expectedVersion = "1.3.0";
+const expectedVersion = "1.4.0";
const maxSourceFileBytes = 2 * 1024 * 1024;
const binaryReleaseExtensions = new Set([
".appimage",
@@ -52,6 +52,7 @@ const requiredDocs = [
"CODE_SIGNING_POLICY.md",
"PRIVACY.md",
"docs/INSTALLATION.md",
+ "docs/RELEASE_NOTES_v1.4.0.md",
"docs/RELEASE_NOTES_v1.3.0.md",
"docs/RELEASE_NOTES_v1.2.0.md",
"docs/RELEASE_NOTES_v1.1.0.md",
@@ -88,6 +89,7 @@ const publicReleaseCopyFiles = [
"apps/desktop/package.json",
"docs/INSTALLATION.md",
"docs/OPEN_SOURCE_RELEASE.md",
+ "docs/RELEASE_NOTES_v1.4.0.md",
"docs/RELEASE_NOTES_v1.3.0.md",
"docs/RELEASE_NOTES_v1.2.0.md",
"docs/RELEASE_NOTES_v1.1.0.md",
@@ -732,7 +734,7 @@ function checkRequiredDocs() {
for (const [phrase, label] of [
["One Kernel. Modular capabilities. Verifiable execution.", "English README product promise"],
["DS Agent is a local Agent Harness optimized for DeepSeek", "English README DeepSeek-first positioning"],
- ["v1.3.0 stable", "English README formal stable status"],
+ ["v1.4.0 stable", "English README formal stable status"],
["The key difference is that Memory, Automation, Computer Use, parallel Subagents, and Skills are not isolated plugins", "English README unified Kernel thesis"],
["contract-first modular Harness architecture", "English README modular architecture"],
["Five core capabilities, one engineering philosophy", "English README five-capability framing"],
@@ -744,8 +746,9 @@ function checkRequiredDocs() {
["goal → done-when contract → context → plan → permission → execution → evidence → verification → recovery", "English README Loop Engineering contract"],
["DeepSeek and DS Agent boundary", "English README model boundary"],
["Why Rust", "English README Rust rationale"],
- ["Production Microsoft/Google account registration and live external-write authority remain disabled in v1.3.0", "English README connector authority boundary"],
+ ["Production Microsoft/Google account registration and live external-write authority remain disabled in v1.4.0", "English README connector authority boundary"],
["Approval creates only exact authority; it does not execute a Tool, resume the task, or mark the Goal complete", "English README grouped authorization non-execution boundary"],
+ ["ordinary chat does not yet automatically select or sequence the two T1 tools", "English README T1 reachability boundary"],
["README.zh-CN.md", "English README language switch"],
["Search aliases: DS Agent, DSAgent, dsagent, DeepSeek Agent OS.", "English README searchable aliases"],
]) {
@@ -754,7 +757,7 @@ function checkRequiredDocs() {
for (const [phrase, label] of [
["一个 Kernel,模块化能力,统一可信执行。", "Chinese README product promise"],
["DS Agent 是专门为 DeepSeek 优化的本地 Agent Harness", "Chinese README DeepSeek-first positioning"],
- ["v1.3.0 正式稳定版", "Chinese README formal stable status"],
+ ["v1.4.0 正式稳定版", "Chinese README formal stable status"],
["真正的差异点是:记忆、自动化执行、Computer Use、Subagent 并行协作和技能", "Chinese README unified Kernel thesis"],
["契约优先的模块化 Harness 架构", "Chinese README modular architecture"],
["五项核心能力,一套工程理念", "Chinese README five-capability framing"],
@@ -766,8 +769,9 @@ function checkRequiredDocs() {
["目标 → 完成条件 → 上下文 → 规划 → 权限 → 执行 → 证据 → 验证 → 恢复", "Chinese README Loop Engineering contract"],
["DeepSeek 与 DS Agent 的工作边界", "Chinese README model boundary"],
["为什么使用 Rust", "Chinese README Rust rationale"],
- ["v1.3.0 仍未开放生产 Microsoft/Google 账号注册和真实外部写入权限", "Chinese README connector authority boundary"],
+ ["v1.4.0 仍未开放生产 Microsoft/Google 账号注册和真实外部写入权限", "Chinese README connector authority boundary"],
["批准只产生精确权限,不会执行 Tool、恢复任务或把 Goal 标记为完成", "Chinese README grouped authorization non-execution boundary"],
+ ["普通聊天目前还不会自动选择和串联这两个 T1 工具", "Chinese README T1 reachability boundary"],
["README.md", "Chinese README language switch"],
["中文搜索别名:DS Agent、DSAgent、dsagent、DeepSeek Agent OS。", "Chinese README searchable aliases"],
]) {
@@ -1615,14 +1619,14 @@ function checkPublicReleaseCopyPositioning() {
checkTextIncludes(
"apps/desktop/src-tauri/src/kernel/app_update.rs",
readText("apps/desktop/src-tauri/src/kernel/app_update.rs"),
- 'APP_UPDATE_USER_AGENT: &str = "DS-Agent-Updater/1.3.0"',
- "app updater User-Agent v1.3.0",
+ 'APP_UPDATE_USER_AGENT: &str = "DS-Agent-Updater/1.4.0"',
+ "app updater User-Agent v1.4.0",
);
checkTextIncludes(
"apps/desktop/src-tauri/src/kernel/app_update.rs",
readText("apps/desktop/src-tauri/src/kernel/app_update.rs"),
- 'APP_UPDATE_CURRENT_RELEASE_TAG: &str = "v1.3.0"',
- "app updater current release tag v1.3.0",
+ 'APP_UPDATE_CURRENT_RELEASE_TAG: &str = "v1.4.0"',
+ "app updater current release tag v1.4.0",
);
checkTextDoesNotInclude(
"apps/desktop/src-tauri/src/kernel/app_update.rs",
@@ -1630,6 +1634,36 @@ function checkPublicReleaseCopyPositioning() {
'APP_UPDATE_CURRENT_RELEASE_TAG: &str = "v0.9.0"',
"app updater current release tag must not regress to v0.9.0",
);
+ checkTextIncludesCollapsed(
+ "docs/RELEASE_NOTES_v1.4.0.md",
+ readText("docs/RELEASE_NOTES_v1.4.0.md"),
+ "Package, desktop, Tauri, Cargo, updater, and installer metadata are `1.4.0` / `v1.4.0`.",
+ "v1.4.0 stable release notes version identity",
+ );
+ checkTextIncludesCollapsed(
+ "docs/RELEASE_NOTES_v1.4.0.md",
+ readText("docs/RELEASE_NOTES_v1.4.0.md"),
+ "Both `ds-agent.exe` and `DS.Agent_1.4.0_x64-setup.exe` are intentionally Authenticode `NotSigned`",
+ "v1.4.0 dual-artifact unsigned disclosure",
+ );
+ checkTextIncludesCollapsed(
+ "docs/RELEASE_NOTES_v1.4.0.md",
+ readText("docs/RELEASE_NOTES_v1.4.0.md"),
+ "Ordinary chat does not yet automatically select or sequence `operations.reconcile_excel` and `operations.generate_powerpoint`",
+ "v1.4.0 T1 reachability boundary",
+ );
+ checkTextIncludesCollapsed(
+ "docs/RELEASE_NOTES_v1.4.0.md",
+ readText("docs/RELEASE_NOTES_v1.4.0.md"),
+ "DeepSeek remains advisory and cannot approve actions or mint authorization, evidence, or completion receipts",
+ "v1.4.0 model authority boundary",
+ );
+ checkTextIncludesCollapsed(
+ "docs/RELEASE_NOTES_v1.4.0.md",
+ readText("docs/RELEASE_NOTES_v1.4.0.md"),
+ "This release does not add Step 5 Computer Use work, connector or production tenant expansion, background external writes, TaskCheckpoint/exact undo, batch concurrency, Headless/ACP, or any C5A or later capability",
+ "v1.4.0 scope exclusion",
+ );
checkTextIncludesCollapsed(
"docs/RELEASE_NOTES_v1.3.0.md",
readText("docs/RELEASE_NOTES_v1.3.0.md"),
@@ -3453,7 +3487,7 @@ function checkGovernanceDocs() {
checkTextIncludesCollapsed(
"SECURITY.md",
securityPolicy,
- "DS Agent v1.3.0 is the current published stable release",
+ "DS Agent v1.4.0 is the current published stable release",
"SECURITY.md current stable scope",
);
checkTextIncludes(
@@ -3483,7 +3517,7 @@ function checkGovernanceDocs() {
checkTextIncludesCollapsed(
"SECURITY.md",
securityPolicy,
- "Current stable v1.3.0 stores one user-supplied DeepSeek API key in a dedicated Windows DPAPI vault",
+ "Current stable v1.4.0 stores one user-supplied DeepSeek API key in a dedicated Windows DPAPI vault",
"SECURITY.md current stable credential boundary",
);
checkTextIncludesCollapsed(
@@ -3544,7 +3578,7 @@ function checkCodeSigningAndPrivacyPolicies() {
for (const [phrase, label] of [
["# Code signing policy", "code-signing policy title"],
- ["DS Agent `v1.3.0` is intentionally published unsigned", "code-signing current unsigned status"],
+ ["DS Agent `v1.4.0` is intentionally published unsigned", "code-signing current unsigned status"],
["The SignPath Foundation application is submitted and approval is pending", "code-signing pending application status"],
["Free code signing provided by", "SignPath acknowledgement"],
["certificate by", "SignPath certificate acknowledgement"],
@@ -3567,7 +3601,7 @@ function checkCodeSigningAndPrivacyPolicies() {
for (const [phrase, label] of [
["# Privacy Policy", "privacy policy title"],
["does not operate a project cloud backend, advertising service, or project analytics or telemetry service", "no project telemetry service"],
- ["The current stable `v1.3.0` accepts one user-supplied DeepSeek API key", "privacy current credential behavior"],
+ ["The current stable `v1.4.0` accepts one user-supplied DeepSeek API key", "privacy current credential behavior"],
["dedicated Windows DPAPI-protected local vault", "privacy DPAPI storage boundary"],
["https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html", "DeepSeek privacy disclosure"],
["DuckDuckGo's privacy policy", "web-search privacy disclosure"],
@@ -3575,7 +3609,7 @@ function checkCodeSigningAndPrivacyPolicies() {
["Hugging Face", "skill-source privacy disclosure"],
["WebView2 data and privacy documentation", "WebView2 privacy disclosure"],
["accepts only loopback addresses", "local bridge privacy boundary"],
- ["Production Microsoft and Google account registration and live mail/calendar writes are disabled in `v1.3.0`", "disabled connector privacy boundary"],
+ ["Production Microsoft and Google account registration and live mail/calendar writes are disabled in `v1.4.0`", "disabled connector privacy boundary"],
["not uploaded merely because the application is open", "no passive content upload"],
]) {
checkTextIncludesCollapsed("PRIVACY.md", privacyPolicy, phrase, label);
@@ -3588,7 +3622,7 @@ function checkCodeSigningAndPrivacyPolicies() {
checkTextIncludes(filePath, content, "NotSigned", `${filePath} unsigned Authenticode disclosure`);
checkTextIncludes(filePath, content, "Unknown publisher", `${filePath} unknown-publisher warning`);
checkTextIncludes(filePath, content, "SmartScreen", `${filePath} SmartScreen warning`);
- checkTextIncludes(filePath, content, "docs/RELEASE_NOTES_v1.3.0.md", `${filePath} v1.3.0 release-notes link`);
+ checkTextIncludes(filePath, content, "docs/RELEASE_NOTES_v1.4.0.md", `${filePath} v1.4.0 release-notes link`);
checkTextDoesNotInclude(filePath, content, "12,716,857 bytes", `${filePath} no stale v1.0.1 asset size`);
checkTextDoesNotInclude(filePath, content, "469C4EFA54F4C94A6E37D28C9C88D331B26E1770C6792DC93D02B451640E2A6F", `${filePath} no stale v1.0.1 asset SHA-256`);
}
@@ -4198,10 +4232,10 @@ function checkReproducibleWindowsReleaseBuild() {
"open-source release requires byte-reproducible Windows candidates",
);
checkTextIncludesCollapsed(
- "docs/RELEASE_NOTES_v1.3.0.md",
- readText("docs/RELEASE_NOTES_v1.3.0.md"),
+ "docs/RELEASE_NOTES_v1.4.0.md",
+ readText("docs/RELEASE_NOTES_v1.4.0.md"),
"The final release gate compares the application and installer from two fresh, distinct `CARGO_TARGET_DIR` builds byte-for-byte",
- "v1.3.0 release notes disclose reproducibility gate",
+ "v1.4.0 release notes disclose reproducibility gate",
);
}
@@ -4340,7 +4374,7 @@ function checkSmokeScriptReleaseLabels() {
const rustRuntimeExpectations = [
[
"apps/desktop/src-tauri/src/kernel/deepseek.rs",
- "DS-Agent/1.3.0 deepseek-v4",
+ "DS-Agent/1.4.0 deepseek-v4",
"DeepSeek runtime User-Agent release label",
],
[
From 65d03ca3f47e10bfaa1c3ff5c2c7b5c94c11be4a Mon Sep 17 00:00:00 2001
From: Codex
Date: Fri, 24 Jul 2026 19:08:03 +0800
Subject: [PATCH 6/6] test: harden packaged Office smoke paths
---
scripts/windows-installed-ui-smoke.mjs | 88 ++++++++++++++++++++++++--
1 file changed, 83 insertions(+), 5 deletions(-)
diff --git a/scripts/windows-installed-ui-smoke.mjs b/scripts/windows-installed-ui-smoke.mjs
index 83b4a0f..3ddf362 100644
--- a/scripts/windows-installed-ui-smoke.mjs
+++ b/scripts/windows-installed-ui-smoke.mjs
@@ -1179,11 +1179,10 @@ async function runInstalledOfficeArtifactSmoke(client) {
);
}
- const relativeTarget = String(create.action.target ?? target).replaceAll("\\", "/");
- const createdPath = path.join(workspaceDir, relativeTarget);
- if (!existsSync(createdPath)) {
- throw new Error(`Expected Office artifact was not found: ${createdPath}`);
- }
+ const { createdPath, relativeTarget } = await resolveInstalledOfficeArtifactPath(
+ workspaceDir,
+ create.action.target ?? target,
+ );
const wordOpen = verifyWordCanOpenDocument(createdPath);
officeResult = {
@@ -1570,6 +1569,47 @@ async function verifyIsolatedLocalFilePath(filePath) {
return resolvedPath;
}
+async function resolveInstalledOfficeArtifactPath(workspaceDir, returnedTarget) {
+ if (!isolatedProfile?.root || !isolatedProfile?.tempRoot) {
+ throw new Error("Installed Office artifact validation requires an active isolated profile.");
+ }
+ const normalizedTarget = String(returnedTarget ?? "").trim();
+ if (!normalizedTarget) {
+ throw new Error("Installed Office artifact target is missing.");
+ }
+
+ const verifiedProfileRoot = await verifyIsolatedProfileRoot(
+ isolatedProfile.root,
+ isolatedProfile.tempRoot,
+ );
+ const resolvedWorkspace = path.resolve(workspaceDir);
+ const workspaceMetadata = await lstat(resolvedWorkspace);
+ if (workspaceMetadata.isSymbolicLink() || !workspaceMetadata.isDirectory()) {
+ throw new Error("Installed Office smoke workspace is unsafe.");
+ }
+ const canonicalWorkspace = await realpath(resolvedWorkspace);
+ if (!pathIsInsideRoot(canonicalWorkspace, verifiedProfileRoot)) {
+ throw new Error("Installed Office smoke workspace escaped the isolated profile.");
+ }
+
+ const candidatePath = path.isAbsolute(normalizedTarget)
+ ? path.resolve(normalizedTarget)
+ : path.resolve(canonicalWorkspace, normalizedTarget);
+ const verifiedFilePath = await verifyIsolatedLocalFilePath(candidatePath);
+ if (!existsSync(verifiedFilePath)) {
+ throw new Error(`Expected Office artifact was not found: ${verifiedFilePath}`);
+ }
+ const canonicalFile = await realpath(verifiedFilePath);
+ if (!pathIsInsideRoot(canonicalFile, canonicalWorkspace)) {
+ throw new Error("Installed Office artifact path escaped the smoke workspace.");
+ }
+
+ return {
+ createdPath: canonicalFile,
+ relativeTarget: path.relative(canonicalWorkspace, canonicalFile).replaceAll("\\", "/"),
+ };
+}
+
function pathIsInsideRoot(candidate, root) {
const relative = path.relative(root, candidate);
return (
@@ -1870,6 +1910,44 @@ async function runSelfTest() {
"escaped the isolated profile",
);
await removeSelfTestDirectory(unsafeRoot, "deepseek-agent-os-ui-smoke-self-test");
+
+ const officeWorkspace = path.join(isolatedProfileTest.workspaceDir, "office-self-test");
+ const relativeOfficeTarget = path.join("office", "relative.docx");
+ const absoluteOfficeTarget = path.join(officeWorkspace, "office", "absolute.docx");
+ const outsideOfficeTarget = path.join(isolatedProfileTest.workspaceDir, "outside.docx");
+ await mkdir(path.dirname(path.join(officeWorkspace, relativeOfficeTarget)), {
+ recursive: true,
+ });
+ await Promise.all([
+ writeFile(path.join(officeWorkspace, relativeOfficeTarget), "relative"),
+ writeFile(absoluteOfficeTarget, "absolute"),
+ writeFile(outsideOfficeTarget, "outside"),
+ ]);
+ const relativeOfficeArtifact = await resolveInstalledOfficeArtifactPath(
+ officeWorkspace,
+ relativeOfficeTarget,
+ );
+ if (
+ relativeOfficeArtifact.createdPath !== (await realpath(path.join(officeWorkspace, relativeOfficeTarget))) ||
+ relativeOfficeArtifact.relativeTarget !== "office/relative.docx"
+ ) {
+ throw new Error("Self-test expected a relative Office target to resolve inside the smoke workspace.");
+ }
+ const absoluteOfficeArtifact = await resolveInstalledOfficeArtifactPath(
+ officeWorkspace,
+ absoluteOfficeTarget,
+ );
+ if (
+ absoluteOfficeArtifact.createdPath !== (await realpath(absoluteOfficeTarget)) ||
+ absoluteOfficeArtifact.relativeTarget !== "office/absolute.docx"
+ ) {
+ throw new Error("Self-test expected an absolute Office target to resolve inside the smoke workspace.");
+ }
+ await assertAsyncSelfTestThrows(
+ () => resolveInstalledOfficeArtifactPath(officeWorkspace, outsideOfficeTarget),
+ "escaped the smoke workspace",
+ );
+
await removeIsolatedProfile(isolatedProfileTest);
isolatedProfile = undefined;
if (existsSync(isolatedProfileTest.root)) {