feat: admit signed channel workbench module

This commit is contained in:
冰朔 2026-08-19 02:22:32 +08:00
commit bafaf464c2
25 changed files with 1886 additions and 44 deletions

View file

@ -0,0 +1,497 @@
//! Signed channel document and spreadsheet workbench adapter.
//!
//! The signed package only selects this host-owned adapter. User content stays in
//! the authenticated account root and every accepted save is committed together
//! with a hash-chained receipt. Unmounting the package never removes that data.
use ring::digest::{digest, SHA256};
use rusqlite::{params, Connection, OptionalExtension, Transaction};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::AppHandle;
use uuid::Uuid;
const MODULE_NUMBER: &str = "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001";
const ADAPTER: &str = "channel-workbench-v1";
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
const MAX_BODY_BYTES: usize = 2 * 1024 * 1024;
const MAX_COLUMNS: usize = 64;
const MAX_ROWS: usize = 5_000;
const MAX_CELL_BYTES: usize = 64 * 1024;
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WorkbenchColumn {
pub column_id: String,
pub title: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WorkbenchRow {
pub row_id: String,
pub cells: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelDocument {
document_id: String,
title: String,
body: String,
revision: u64,
updated_at_unix_ms: u64,
content_sha256: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelSpreadsheet {
table_id: String,
title: String,
columns: Vec<WorkbenchColumn>,
rows: Vec<WorkbenchRow>,
revision: u64,
updated_at_unix_ms: u64,
content_sha256: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelWorkbenchSnapshot {
schema: &'static str,
state: &'static str,
module_number: &'static str,
document: ChannelDocument,
spreadsheet: ChannelSpreadsheet,
receipt_count: u64,
latest_receipt_hash: String,
user_data_survives_unmount: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SaveChannelDocumentInput {
document_id: String,
title: String,
body: String,
expected_revision: u64,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SaveChannelSpreadsheetInput {
table_id: String,
title: String,
columns: Vec<WorkbenchColumn>,
rows: Vec<WorkbenchRow>,
expected_revision: u64,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkbenchSaveResult {
schema: &'static str,
state: &'static str,
entity_kind: &'static str,
entity_id: String,
revision: u64,
content_sha256: String,
receipt_sha256: String,
observed_at_unix_ms: u64,
}
fn now_unix_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or(0)
}
fn sha256_hex(bytes: &[u8]) -> String {
digest(&SHA256, bytes)
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn storage_root(app: &AppHandle) -> Result<PathBuf, String> {
crate::authenticated_storage::account_storage_root(app, "channel-workbench-v1")
}
fn require_active(app: &AppHandle) -> Result<(), String> {
crate::module_package_runtime::require_active_module_adapter(app, MODULE_NUMBER, ADAPTER)
}
fn open_db(root: &Path) -> Result<Connection, String> {
fs::create_dir_all(root)
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_ROOT_FAILED: {error}"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(root, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_PERMISSION_FAILED: {error}"))?;
}
let connection = Connection::open(root.join("channel-workbench.sqlite3"))
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_DB_OPEN_FAILED: {error}"))?;
connection
.busy_timeout(std::time::Duration::from_secs(5))
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_DB_TIMEOUT_FAILED: {error}"))?;
connection
.execute_batch(
"PRAGMA journal_mode=WAL;
PRAGMA foreign_keys=ON;
CREATE TABLE IF NOT EXISTS documents(
document_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL,
revision INTEGER NOT NULL,
updated_at_unix_ms INTEGER NOT NULL,
content_sha256 TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS spreadsheets(
table_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
columns_json TEXT NOT NULL,
rows_json TEXT NOT NULL,
revision INTEGER NOT NULL,
updated_at_unix_ms INTEGER NOT NULL,
content_sha256 TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS workbench_receipts(
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
receipt_id TEXT NOT NULL UNIQUE,
event TEXT NOT NULL,
entity_kind TEXT NOT NULL,
entity_id TEXT NOT NULL,
revision INTEGER NOT NULL,
content_sha256 TEXT NOT NULL,
previous_receipt_hash TEXT NOT NULL,
receipt_hash TEXT NOT NULL UNIQUE,
observed_at_unix_ms INTEGER NOT NULL
);
CREATE TRIGGER IF NOT EXISTS workbench_receipts_no_update
BEFORE UPDATE ON workbench_receipts BEGIN SELECT RAISE(ABORT,'HOLOLAKE_WORKBENCH_RECEIPT_APPEND_ONLY'); END;
CREATE TRIGGER IF NOT EXISTS workbench_receipts_no_delete
BEFORE DELETE ON workbench_receipts BEGIN SELECT RAISE(ABORT,'HOLOLAKE_WORKBENCH_RECEIPT_APPEND_ONLY'); END;",
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_DB_INIT_FAILED: {error}"))?;
Ok(connection)
}
fn valid_object_id(value: &str, prefix: &str) -> bool {
value.starts_with(prefix)
&& value.len() <= 96
&& value
.chars()
.all(|item| item.is_ascii_uppercase() || item.is_ascii_digit() || item == '-')
}
fn valid_title(value: &str) -> bool {
!value.trim().is_empty() && value.len() <= 120 && !value.chars().any(char::is_control)
}
fn receipt_hash(
event: &str,
entity_kind: &str,
entity_id: &str,
revision: u64,
content_sha256: &str,
previous: &str,
observed: u64,
) -> String {
sha256_hex(
format!(
"{event}\n{entity_kind}\n{entity_id}\n{revision}\n{content_sha256}\n{previous}\n{observed}"
)
.as_bytes(),
)
}
fn verify_receipts(connection: &Connection) -> Result<(u64, String), String> {
let mut statement = connection
.prepare("SELECT sequence,event,entity_kind,entity_id,revision,content_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms FROM workbench_receipts ORDER BY sequence")
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
let rows = statement
.query_map([], |row| {
Ok((
row.get::<_, u64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, String>(3)?,
row.get::<_, u64>(4)?,
row.get::<_, String>(5)?,
row.get::<_, String>(6)?,
row.get::<_, String>(7)?,
row.get::<_, u64>(8)?,
))
})
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
let mut count = 0;
let mut previous = ZERO_HASH.to_string();
for row in rows {
let (sequence, event, kind, id, revision, content, stored_previous, stored_hash, observed) =
row.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
if sequence != count + 1
|| stored_previous != previous
|| stored_hash != receipt_hash(&event, &kind, &id, revision, &content, &previous, observed)
{
return Err("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID".into());
}
count = sequence;
previous = stored_hash;
}
Ok((count, previous))
}
fn append_receipt(
tx: &Transaction<'_>,
event: &str,
kind: &str,
id: &str,
revision: u64,
content: &str,
observed: u64,
) -> Result<String, String> {
let previous: String = tx
.query_row(
"SELECT COALESCE((SELECT receipt_hash FROM workbench_receipts ORDER BY sequence DESC LIMIT 1),?1)",
params![ZERO_HASH],
|row| row.get(0),
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_HEAD_FAILED: {error}"))?;
let hash = receipt_hash(event, kind, id, revision, content, &previous, observed);
tx.execute(
"INSERT INTO workbench_receipts(receipt_id,event,entity_kind,entity_id,revision,content_sha256,previous_receipt_hash,receipt_hash,observed_at_unix_ms) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
params![format!("HLP-WB-RCP-{}", Uuid::new_v4().simple()), event, kind, id, revision, content, previous, hash, observed],
)
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_WRITE_FAILED: {error}"))?;
Ok(hash)
}
fn empty_document() -> ChannelDocument {
ChannelDocument {
document_id: "HLP-WB-DOC-0001".into(),
title: "频道笔记".into(),
body: "".into(),
revision: 0,
updated_at_unix_ms: 0,
content_sha256: sha256_hex(b""),
}
}
fn empty_spreadsheet() -> ChannelSpreadsheet {
ChannelSpreadsheet {
table_id: "HLP-WB-TBL-0001".into(),
title: "频道工作表".into(),
columns: vec![
WorkbenchColumn { column_id: "HLP-WB-COL-0001".into(), title: "事项".into() },
WorkbenchColumn { column_id: "HLP-WB-COL-0002".into(), title: "状态".into() },
],
rows: Vec::new(),
revision: 0,
updated_at_unix_ms: 0,
content_sha256: sha256_hex(b""),
}
}
fn snapshot_at(root: &Path) -> Result<ChannelWorkbenchSnapshot, String> {
let connection = open_db(root)?;
let (receipt_count, latest_receipt_hash) = verify_receipts(&connection)?;
let document = connection
.query_row(
"SELECT document_id,title,body,revision,updated_at_unix_ms,content_sha256 FROM documents ORDER BY updated_at_unix_ms DESC LIMIT 1",
[],
|row| Ok(ChannelDocument { document_id: row.get(0)?, title: row.get(1)?, body: row.get(2)?, revision: row.get(3)?, updated_at_unix_ms: row.get(4)?, content_sha256: row.get(5)? }),
)
.optional()
.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))?
.unwrap_or_else(empty_document);
let spreadsheet = connection
.query_row(
"SELECT table_id,title,columns_json,rows_json,revision,updated_at_unix_ms,content_sha256 FROM spreadsheets ORDER BY updated_at_unix_ms DESC LIMIT 1",
[],
|row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, String>(2)?, row.get::<_, String>(3)?, row.get::<_, u64>(4)?, row.get::<_, u64>(5)?, row.get::<_, String>(6)?)),
)
.optional()
.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))?
.map(|(table_id, title, columns, rows, revision, updated_at_unix_ms, content_sha256)| {
Ok::<ChannelSpreadsheet, String>(ChannelSpreadsheet {
table_id,
title,
columns: serde_json::from_str(&columns).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_COLUMNS_INVALID".to_string())?,
rows: serde_json::from_str(&rows).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_ROWS_INVALID".to_string())?,
revision,
updated_at_unix_ms,
content_sha256,
})
})
.transpose()?
.unwrap_or_else(empty_spreadsheet);
if document.revision > 0 {
let actual = sha256_hex(
serde_json::to_string(&(document.title.trim(), &document.body))
.map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")?
.as_bytes(),
);
if actual != document.content_sha256 {
return Err("HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID".into());
}
}
if spreadsheet.revision > 0 {
let actual = sha256_hex(
serde_json::to_string(&(
spreadsheet.title.trim(),
&spreadsheet.columns,
&spreadsheet.rows,
))
.map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?
.as_bytes(),
);
if actual != spreadsheet.content_sha256 {
return Err("HOLOLAKE_CHANNEL_SPREADSHEET_CONTENT_INTEGRITY_INVALID".into());
}
}
let mut latest = connection
.prepare("SELECT entity_kind,entity_id,revision,content_sha256 FROM workbench_receipts WHERE sequence IN (SELECT MAX(sequence) FROM workbench_receipts GROUP BY entity_kind,entity_id)")
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
let heads = latest
.query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?, row.get::<_, u64>(2)?, row.get::<_, String>(3)?)))
.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
for head in heads {
let (kind, id, revision, content) = head.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_READ_FAILED: {error}"))?;
let matches = match kind.as_str() {
"DOCUMENT" => document.document_id == id && document.revision == revision && document.content_sha256 == content,
"SPREADSHEET" => spreadsheet.table_id == id && spreadsheet.revision == revision && spreadsheet.content_sha256 == content,
_ => false,
};
if !matches { return Err("HOLOLAKE_CHANNEL_WORKBENCH_CURRENT_STATE_INVALID".into()); }
}
Ok(ChannelWorkbenchSnapshot {
schema: "hololake.channel-workbench-snapshot/v1",
state: "ACTIVE_LOCAL_ACCOUNT_DATA",
module_number: MODULE_NUMBER,
document,
spreadsheet,
receipt_count,
latest_receipt_hash,
user_data_survives_unmount: true,
})
}
fn save_document_at(root: &Path, input: SaveChannelDocumentInput, observed: u64) -> Result<WorkbenchSaveResult, String> {
if !valid_object_id(&input.document_id, "HLP-WB-DOC-")
|| !valid_title(&input.title)
|| input.body.len() > MAX_BODY_BYTES
{
return Err("HOLOLAKE_CHANNEL_DOCUMENT_INPUT_INVALID".into());
}
let mut connection = open_db(root)?;
verify_receipts(&connection)?;
let transaction = connection.transaction().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_TRANSACTION_FAILED: {error}"))?;
let current: Option<u64> = transaction.query_row("SELECT revision FROM documents WHERE document_id=?1", params![input.document_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_READ_FAILED: {error}"))?;
if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT".into()); }
let revision = input.expected_revision + 1;
let content = sha256_hex(serde_json::to_string(&(input.title.trim(), &input.body)).map_err(|_| "HOLOLAKE_CHANNEL_DOCUMENT_SERIALIZE_FAILED")?.as_bytes());
transaction.execute(
"INSERT INTO documents VALUES (?1,?2,?3,?4,?5,?6) ON CONFLICT(document_id) DO UPDATE SET title=excluded.title,body=excluded.body,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256",
params![input.document_id, input.title.trim(), input.body, revision, observed, content],
).map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_WRITE_FAILED: {error}"))?;
let receipt = append_receipt(&transaction, if current.is_some() { "UPDATE" } else { "CREATE" }, "DOCUMENT", &input.document_id, revision, &content, observed)?;
transaction.commit().map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_COMMIT_FAILED: {error}"))?;
Ok(WorkbenchSaveResult { schema: "hololake.channel-workbench-save/v1", state: "COMMITTED", entity_kind: "DOCUMENT", entity_id: input.document_id, revision, content_sha256: content, receipt_sha256: receipt, observed_at_unix_ms: observed })
}
fn validate_spreadsheet(input: &SaveChannelSpreadsheetInput) -> Result<(), String> {
if !valid_object_id(&input.table_id, "HLP-WB-TBL-")
|| !valid_title(&input.title)
|| input.columns.is_empty()
|| input.columns.len() > MAX_COLUMNS
|| input.rows.len() > MAX_ROWS
|| input.columns.iter().any(|column| !valid_object_id(&column.column_id, "HLP-WB-COL-") || !valid_title(&column.title))
|| input.rows.iter().any(|row| !valid_object_id(&row.row_id, "HLP-WB-ROW-") || row.cells.len() > input.columns.len() || row.cells.iter().any(|cell| cell.len() > MAX_CELL_BYTES))
{
return Err("HOLOLAKE_CHANNEL_SPREADSHEET_INPUT_INVALID".into());
}
Ok(())
}
fn save_spreadsheet_at(root: &Path, input: SaveChannelSpreadsheetInput, observed: u64) -> Result<WorkbenchSaveResult, String> {
validate_spreadsheet(&input)?;
let mut connection = open_db(root)?;
verify_receipts(&connection)?;
let transaction = connection.transaction().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_TRANSACTION_FAILED: {error}"))?;
let current: Option<u64> = transaction.query_row("SELECT revision FROM spreadsheets WHERE table_id=?1", params![input.table_id], |row| row.get(0)).optional().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_READ_FAILED: {error}"))?;
if current.unwrap_or(0) != input.expected_revision { return Err("HOLOLAKE_CHANNEL_SPREADSHEET_REVISION_CONFLICT".into()); }
let revision = input.expected_revision + 1;
let columns = serde_json::to_string(&input.columns).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
let rows = serde_json::to_string(&input.rows).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?;
let content = sha256_hex(serde_json::to_string(&(input.title.trim(), &input.columns, &input.rows)).map_err(|_| "HOLOLAKE_CHANNEL_SPREADSHEET_SERIALIZE_FAILED")?.as_bytes());
transaction.execute(
"INSERT INTO spreadsheets VALUES (?1,?2,?3,?4,?5,?6,?7) ON CONFLICT(table_id) DO UPDATE SET title=excluded.title,columns_json=excluded.columns_json,rows_json=excluded.rows_json,revision=excluded.revision,updated_at_unix_ms=excluded.updated_at_unix_ms,content_sha256=excluded.content_sha256",
params![input.table_id, input.title.trim(), columns, rows, revision, observed, content],
).map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_WRITE_FAILED: {error}"))?;
let receipt = append_receipt(&transaction, if current.is_some() { "UPDATE" } else { "CREATE" }, "SPREADSHEET", &input.table_id, revision, &content, observed)?;
transaction.commit().map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_COMMIT_FAILED: {error}"))?;
Ok(WorkbenchSaveResult { schema: "hololake.channel-workbench-save/v1", state: "COMMITTED", entity_kind: "SPREADSHEET", entity_id: input.table_id, revision, content_sha256: content, receipt_sha256: receipt, observed_at_unix_ms: observed })
}
pub async fn get_channel_workbench_snapshot(app: AppHandle) -> Result<ChannelWorkbenchSnapshot, String> {
require_active(&app)?;
let root = storage_root(&app)?;
tauri::async_runtime::spawn_blocking(move || snapshot_at(&root)).await.map_err(|error| format!("HOLOLAKE_CHANNEL_WORKBENCH_JOIN_FAILED: {error}"))?
}
pub async fn save_channel_document(app: AppHandle, input: SaveChannelDocumentInput) -> Result<WorkbenchSaveResult, String> {
require_active(&app)?;
let root = storage_root(&app)?;
tauri::async_runtime::spawn_blocking(move || save_document_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_DOCUMENT_JOIN_FAILED: {error}"))?
}
pub async fn save_channel_spreadsheet(app: AppHandle, input: SaveChannelSpreadsheetInput) -> Result<WorkbenchSaveResult, String> {
require_active(&app)?;
let root = storage_root(&app)?;
tauri::async_runtime::spawn_blocking(move || save_spreadsheet_at(&root, input, now_unix_ms())).await.map_err(|error| format!("HOLOLAKE_CHANNEL_SPREADSHEET_JOIN_FAILED: {error}"))?
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn document_and_spreadsheet_survive_reopen_with_receipt_chain() {
let root = tempfile::tempdir().unwrap();
let document = save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "真实笔记".into(), body: "第一段真实内容".into(), expected_revision: 0 }, 101).unwrap();
let sheet = save_spreadsheet_at(root.path(), SaveChannelSpreadsheetInput { table_id: "HLP-WB-TBL-0001".into(), title: "真实工作表".into(), columns: vec![WorkbenchColumn { column_id: "HLP-WB-COL-0001".into(), title: "数量".into() }], rows: vec![WorkbenchRow { row_id: "HLP-WB-ROW-0001".into(), cells: vec!["=1+2".into()] }], expected_revision: 0 }, 102).unwrap();
let snapshot = snapshot_at(root.path()).unwrap();
assert_eq!(document.revision, 1);
assert_eq!(sheet.revision, 1);
assert_eq!(snapshot.document.body, "第一段真实内容");
assert_eq!(snapshot.spreadsheet.rows[0].cells[0], "=1+2");
assert_eq!(snapshot.receipt_count, 2);
assert_eq!(snapshot.latest_receipt_hash, sheet.receipt_sha256);
}
#[test]
fn stale_revision_and_tampered_receipt_fail_closed() {
let root = tempfile::tempdir().unwrap();
let input = SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "正文".into(), expected_revision: 0 };
save_document_at(root.path(), input, 101).unwrap();
assert_eq!(save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "过期写入".into(), expected_revision: 0 }, 102).unwrap_err(), "HOLOLAKE_CHANNEL_DOCUMENT_REVISION_CONFLICT");
let connection = open_db(root.path()).unwrap();
connection.execute_batch("DROP TRIGGER workbench_receipts_no_update; UPDATE workbench_receipts SET receipt_hash='bad'").unwrap();
assert_eq!(snapshot_at(root.path()).unwrap_err(), "HOLOLAKE_CHANNEL_WORKBENCH_RECEIPT_CHAIN_INVALID");
}
#[test]
fn content_mutation_without_a_receipt_fails_closed() {
let root = tempfile::tempdir().unwrap();
save_document_at(root.path(), SaveChannelDocumentInput { document_id: "HLP-WB-DOC-0001".into(), title: "笔记".into(), body: "可信正文".into(), expected_revision: 0 }, 101).unwrap();
let connection = open_db(root.path()).unwrap();
connection.execute("UPDATE documents SET body='被改写' WHERE document_id='HLP-WB-DOC-0001'", []).unwrap();
assert_eq!(snapshot_at(root.path()).unwrap_err(), "HOLOLAKE_CHANNEL_DOCUMENT_CONTENT_INTEGRITY_INVALID");
}
}

View file

@ -1,5 +1,6 @@
mod authenticated_storage;
mod circular_lake_membrane;
mod channel_workbench;
mod code_channel;
mod code_repo_login;
mod direct_local_broker;

View file

@ -31,6 +31,32 @@ const NATIVE_COMPOSITION_PACKAGE: &[u8] = include_bytes!(
const NATIVE_COMPOSITION_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-NATIVE-COMPOSITION-0001-0.1.0.ghmod.sig"
);
const CHANNEL_WORKBENCH_NUMBER: &str = "HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001";
const CHANNEL_WORKBENCH_PACKAGE: &[u8] = include_bytes!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod"
);
const CHANNEL_WORKBENCH_SIGNATURE: &str = include_str!(
"../../fixtures/module-packages/HLP-MOD-LOCAL-CHANNEL-WORKBENCH-0001-0.1.0.ghmod.sig"
);
struct BundledModuleSource {
module_number: &'static str,
package: &'static [u8],
signature: &'static str,
}
const BUNDLED_MODULES: &[BundledModuleSource] = &[
BundledModuleSource {
module_number: NATIVE_COMPOSITION_NUMBER,
package: NATIVE_COMPOSITION_PACKAGE,
signature: NATIVE_COMPOSITION_SIGNATURE,
},
BundledModuleSource {
module_number: CHANNEL_WORKBENCH_NUMBER,
package: CHANNEL_WORKBENCH_PACKAGE,
signature: CHANNEL_WORKBENCH_SIGNATURE,
},
];
#[derive(Debug, Deserialize)]
struct RuntimeContract {
@ -379,10 +405,11 @@ fn read_verified_package_bytes(
}
fn bundled_package(module_number: &str) -> Result<(&'static [u8], &'static str), String> {
match module_number {
NATIVE_COMPOSITION_NUMBER => Ok((NATIVE_COMPOSITION_PACKAGE, NATIVE_COMPOSITION_SIGNATURE)),
_ => Err("HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into()),
}
BUNDLED_MODULES
.iter()
.find(|source| source.module_number == module_number)
.map(|source| (source.package, source.signature))
.ok_or_else(|| "HOLOLAKE_BUNDLED_MODULE_UNKNOWN".into())
}
fn runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
@ -673,25 +700,35 @@ pub async fn get_bundled_module_catalog(
) -> Result<Vec<BundledModuleDescriptor>, String> {
let root = runtime_root(&app)?;
let snapshot = snapshot_at(&root)?;
let (bytes, signature) = bundled_package(NATIVE_COMPOSITION_NUMBER)?;
let (_, package, package_sha256) =
read_verified_package_bytes(bytes, signature, RELEASE_TRUST_RAW)?;
let installed_state = snapshot
.modules
BUNDLED_MODULES
.iter()
.find(|module| module.module_number == NATIVE_COMPOSITION_NUMBER)
.map(|module| module.state.clone())
.unwrap_or_else(|| "NOT_INSTALLED".into());
Ok(vec![BundledModuleDescriptor {
module_number: package.manifest.module_number,
display_name: package.manifest.display_name,
version: package.manifest.version,
adapter: package.manifest.adapter,
permissions: package.manifest.permissions,
package_sha256,
installed_state,
signature_verified: true,
}])
.map(|source| {
let (_, package, package_sha256) = read_verified_package_bytes(
source.package,
source.signature,
RELEASE_TRUST_RAW,
)?;
if package.manifest.module_number != source.module_number {
return Err("HOLOLAKE_BUNDLED_MODULE_IDENTITY_INVALID".into());
}
let installed_state = snapshot
.modules
.iter()
.find(|module| module.module_number == source.module_number)
.map(|module| module.state.clone())
.unwrap_or_else(|| "NOT_INSTALLED".into());
Ok(BundledModuleDescriptor {
module_number: package.manifest.module_number,
display_name: package.manifest.display_name,
version: package.manifest.version,
adapter: package.manifest.adapter,
permissions: package.manifest.permissions,
package_sha256,
installed_state,
signature_verified: true,
})
})
.collect()
}
pub async fn activate_bundled_module(
@ -1455,6 +1492,21 @@ mod tests {
assert_eq!(snapshot.receipt_count, 3);
}
#[test]
fn bundled_channel_workbench_has_an_independent_verified_identity() {
let (_, package, digest) = read_verified_package_bytes(
CHANNEL_WORKBENCH_PACKAGE,
CHANNEL_WORKBENCH_SIGNATURE,
RELEASE_TRUST_RAW,
)
.unwrap();
assert_eq!(package.manifest.module_number, CHANNEL_WORKBENCH_NUMBER);
assert_eq!(package.manifest.adapter, "channel-workbench-v1");
assert_eq!(package.manifest.permissions.len(), 4);
assert!(is_sha256(&digest));
assert_eq!(BUNDLED_MODULES.len(), 2);
}
#[cfg(unix)]
#[test]
fn symlinked_package_input_is_rejected_before_signature_processing() {

View file

@ -53,7 +53,7 @@ fn validate_tree() -> Result<(), String> {
|| tree.record_id != "HLP-UNIFIED-NUMBER-TREE-001"
|| tree.state != "MACHINE_COMPILED_STARTUP_ENFORCED"
|| tree.root_number != "HLP-NUMBER-WORLD-ROOT-001"
|| tree.route_count != 95
|| tree.route_count != 98
|| tree.routes.len() != tree.route_count
|| !tree.invariants.number_is_stable_coordinate_not_authority
|| !tree.invariants.path_is_unique_navigation

View file

@ -934,7 +934,7 @@ mod tests {
#[test]
fn registry_is_closed_and_contains_every_migrated_command() {
let registry = load_registry().unwrap();
assert_eq!(registry.operations.len(), 73);
assert_eq!(registry.operations.len(), 76);
assert!(!registry.runtime.legacy_direct_commands_allowed);
}

View file

@ -133,6 +133,15 @@ pub(crate) async fn dispatch(
crate::native_composition::execute_knowledge_native_composition(app, input(&payload)?)
.await?,
),
"channel_workbench::get_channel_workbench_snapshot" => {
json(crate::channel_workbench::get_channel_workbench_snapshot(app).await?)
}
"channel_workbench::save_channel_document" => json(
crate::channel_workbench::save_channel_document(app, input(&payload)?).await?,
),
"channel_workbench::save_channel_spreadsheet" => json(
crate::channel_workbench::save_channel_spreadsheet(app, input(&payload)?).await?,
),
"local_development_bridge::acquire_development_write_lane" => json(
crate::local_development_bridge::acquire_development_write_lane(app, input(&payload)?)
.await?,