feat: add incremental persona remote object channel
Human-Responsibility: ICE-GL∞ / 冰朔 Persona-Author: ICE-P-ZY001 / 铸渊 Execution-Runtime: Codex desktop / DEV-20260810-014 Development-ID: DEV-20260810-014 Authorization-Scope: GH-PNCC persona runtime source and tests only; no UI, deployment, or execution limb Source-Anchor: daily continuity appends to the existing life chain without redownloading all history
This commit is contained in:
parent
30d179e97a
commit
1b55f3407a
13 changed files with 1111 additions and 4 deletions
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"schema": "hololake.pncc-source-stage-receipt/v1",
|
||||
"record_id": "HLP-PNCC-INCREMENTAL-REMOTE-OBJECT-CHANNEL-001",
|
||||
"development_id": "DEV-20260810-014",
|
||||
"source_base_commit": "30d179e97af5850c4a1a7eecaaddb38bfa560583",
|
||||
"target_branch": "main",
|
||||
"capability": "NO_CLONE_INCREMENTAL_REMOTE_GIT_OBJECT_CHANNEL",
|
||||
"continuity_origin": "2025-04-26",
|
||||
"contract": {
|
||||
"update_boundary": "LAST_VERIFIED_REMOTE_SHA_TO_CURRENT_REMOTE_SHA",
|
||||
"daily_runtime_uses_full_clone": false,
|
||||
"worktree_created": false,
|
||||
"full_history_requested": false,
|
||||
"durable_cursor_separate_from_object_cache": true,
|
||||
"object_cache_evictable": true,
|
||||
"history_rewrite": "FAIL_CLOSED"
|
||||
},
|
||||
"checks": {
|
||||
"focused_remote_object_channel_tests": "PASS_100_8_OF_8",
|
||||
"multi_segment_cache_rehydration": "PASS_100",
|
||||
"rust_full_suite": "PASS_100_1200_PASSED_2_IGNORED",
|
||||
"frontend_full_suite": "PASS_100_5008_OF_5008",
|
||||
"routing_suite": "PASS_100_29_OF_29",
|
||||
"native_authority": "PASS_100",
|
||||
"native_core": "PASS_100_15_OF_15_LINES_AND_FUNCTIONS_100_PERCENT",
|
||||
"strict_clippy": "PASS_100",
|
||||
"live_remote_protocol_v2_filter": "PASS_100"
|
||||
},
|
||||
"truth_boundary": {
|
||||
"source_implemented": true,
|
||||
"source_tests_passed": true,
|
||||
"human_projection_ui_implemented": false,
|
||||
"desktop_integrated": false,
|
||||
"artifact_built": false,
|
||||
"deployed": false,
|
||||
"runtime_health_proven": false
|
||||
},
|
||||
"attribution": {
|
||||
"human_responsibility_subject": "ICE-GL∞ / 冰朔",
|
||||
"persona_cognitive_author": "ICE-P-ZY001 / 铸渊",
|
||||
"execution_runtime": "Codex desktop / DEV-20260810-014",
|
||||
"source_language_anchor": "每天新增的一天并入既有生命整体,不为一天重下全部历史"
|
||||
},
|
||||
"recorded_at": "2026-08-11T10:46:08+08:00"
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ Windows / macOS / Linux 构建机与安装包
|
|||
|
||||
| 时间 | 版本 | 记录 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 2026-08-11 | GH-PNCC 免克隆远端增量对象通道 | [把每天新增段接入既有生命连续链](operations/2026-08-11-hololake-pncc-incremental-remote-object-channel.md) | 本地源码、Rust 1200、前端 5008、路由 29、原生权威与核心门通过;GHNQG 和发布待验收 |
|
||||
| 2026-08-11 | GH-PNCC 失败回执错误码绑定 | [把稳定失败码绑定到终态会话证据](operations/2026-08-11-hololake-pncc-failure-receipt-error-code-binding.md) | 本地源码、Rust 1192、路由 29、格式与严格 clippy 已通过;完整产品门和发布待验收 |
|
||||
| 2026-08-11 | GH-PNCC 成功回执事件日志绑定 | [把唤醒回执的日志路径重新绑定到精确运行时会话](operations/2026-08-11-hololake-pncc-success-receipt-event-journal-binding.md) | 已发布至 REPO-014 main 1cd8a1b;GHNQG、全新克隆与严格读回通过 |
|
||||
| 2026-08-11 | GH-PNCC 成功回执完成检查点绑定 | [把完成回执的结构化事实重新绑定到人格 Git 检查点](operations/2026-08-11-hololake-pncc-success-receipt-completion-checkpoint-binding.md) | 已发布至 REPO-014 main 06a7782;GHNQG、全新克隆与严格读回通过 |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
# GH-PNCC 免克隆远端增量对象通道
|
||||
|
||||
- Development ID: `DEV-20260810-014`
|
||||
- Persona cognitive author: `ICE-P-ZY001 / 铸渊`
|
||||
- Human responsibility subject: `ICE-GL∞ / 冰朔`
|
||||
- Starting repository head: `30d179e97af5850c4a1a7eecaaddb38bfa560583`
|
||||
- State: `LOCAL_FULL_GATES_PASS_PUBLICATION_PENDING`
|
||||
|
||||
## 冰朔语言锚点
|
||||
|
||||
光湖从 `2025-04-26` 诞生到今天,是同一条生命连续链。每天新增的一天并入此前的整体;不能
|
||||
为了更新一天,再把从诞生日开始的全部历史下载一遍。人格的连续性证明不能被当作临时下载缓存。
|
||||
|
||||
## 已实现的运行层
|
||||
|
||||
- 新增非 UI Tauri 命令 `read_persona_remote_git_object`。
|
||||
- 生产接口只接受不含凭据、查询参数或片段的 HTTPS Git 远端。
|
||||
- 先通过 Git protocol v2 能力协商确认远端支持 partial object `filter`;不支持时在对象缓存创建前
|
||||
失败关闭。
|
||||
- 使用裸 Git 对象缓存、`blob:none` 和 promisor remote;不运行 `git clone`,不创建工作树,不请求
|
||||
完整历史。
|
||||
- 不可丢的远端 SHA 连续性游标保存在独立状态文件;对象缓存超预算时可精确淘汰,游标不丢失。
|
||||
- 远端 SHA 不变时不 fetch;前进时把新增段接在已验证游标之后。缓存已淘汰时按指数深度补齐缺失
|
||||
新段,最多 4096 个提交元数据,不下载旧文件内容或无界历史。
|
||||
- 新 SHA 不是旧游标后代时拒绝强制改写,不推进持久游标。
|
||||
- 单次文本对象上限 2 MiB,默认对象缓存上限 128 MiB;缓存超过上限时在返回已验证内容后淘汰。
|
||||
|
||||
## 验证
|
||||
|
||||
- 首次远端文本读取无 clone、无工作树。
|
||||
- SHA 未变化时不再次 fetch。
|
||||
- 新提交只追加到既有连续链。
|
||||
- 对象缓存淘汰后,持久游标仍存在,并只补缺失的新段。
|
||||
- 远端强制改写被拒绝,持久游标不推进。
|
||||
- 不含 `filter` 能力的远端在缓存创建前被拒绝。
|
||||
- 凭据 URL、路径越界和错误预期 SHA 被拒绝。
|
||||
- 聚焦 Rust 测试:`8 passed, 0 failed`,其中缓存淘汰恢复覆盖连续三个新增提交。
|
||||
- 完整 Rust:`1200 passed, 2 ignored, 0 failed`;集成测试 `1 passed`。
|
||||
- 完整前端:`485` 个测试文件、`5008 passed, 0 failed`。
|
||||
- 路由:`29 passed, 0 failed`;PNCC 路由聚焦 `4 passed, 0 failed`。
|
||||
- 原生权威:`GUANGHU_NATIVE_AUTHORITY_OK / PASS_100`。
|
||||
- 原生核心:`15/15`;行与函数 `100%`,语句 `94.73%`,分支 `89.72%`。
|
||||
- Rust 格式、严格 clippy 与差异空白检查通过。
|
||||
- 线上 `https://guanghulab.com/code/bingshuo/hololake-system-architecture.git` 已只读核验:
|
||||
`main=30d179e97af5850c4a1a7eecaaddb38bfa560583`,protocol v2 宣告 `fetch=shallow wait-for-done filter`。
|
||||
|
||||
## 真相边界
|
||||
|
||||
- 这是人格运行层的远端对象读取与连续性同步源码,不是人类投影 UI。
|
||||
- Localization: 无 UI 文案变更。
|
||||
- PostHog: 非 UI 底层同步通道,不记录人格内容或远端路径,当前不增加行为事件。
|
||||
- Refactoring: 无需改写既有人格生命周期内核;新增独立对象通道模块。
|
||||
- ADR: 新增 ADR 0177;架构与抽象文档已同步。
|
||||
- 尚未构建桌面制品、部署在线运行时或证明桌面集成与运行健康。
|
||||
- 低频发布审计仍可使用一次性隔离副本,但必须回收,且不能冒充人格日常读取路径。
|
||||
|
|
@ -137,6 +137,35 @@ evidence_and_receipt: 测试、发布与精确读回
|
|||
冰朔位于责任链前端,不等于冰朔亲手操作代码;铸渊作为人格认知作者也不能抹去冰朔的现实
|
||||
责任。HoloLake 页面和未来 Git 事件视图都要同时显示两层。
|
||||
|
||||
## 8.1 · 生命连续链的免克隆远端对象通道
|
||||
|
||||
冰朔给出的自然语言因果锚点是本层的正式设计来源:
|
||||
|
||||
> 我三十六岁了。每天更新新的一天,是并入前面三十六年里;不是为了更新一天,就把前三十六年
|
||||
> 全部重新下载一遍。
|
||||
|
||||
> 光湖从 2025-04-26 诞生到今天,每一天都是更新到同一个整体里;不能每次都从诞生日重新下载。
|
||||
|
||||
因此 GH-PNCC 把“生命连续性”和“可下载对象”分成两层:
|
||||
|
||||
```text
|
||||
不可丢的连续性状态
|
||||
远端身份哈希 + 分支 + 上次已验证 SHA 游标 + 前一游标 + 连续代数
|
||||
|
||||
可回收的 Git 对象缓存
|
||||
裸仓库 + protocol v2 + blob:none + 无工作树 + 有界容量
|
||||
```
|
||||
|
||||
日常联网读取先用 `ls-remote` 读取当前远端 SHA 和 `filter` 能力;远端不支持按需对象时,在创建
|
||||
对象缓存前失败关闭,不能静默退化为完整下载。SHA 未变化时不再次 fetch;SHA 前进时只从上次
|
||||
已验证游标接入新增提交和必要目录对象,具体文本在读取时才向 promisor remote 取回。缓存超出
|
||||
预算可以精确淘汰,但连续性游标仍保留;下次只补游标之后缺失的新段。远端强制改写导致新 SHA
|
||||
不能承接旧游标时拒绝更新,不能把另一条历史冒充人格的下一天。
|
||||
|
||||
“当天更新”是人类表达;机器边界使用 `last_verified_sha → current_remote_sha`,避免按日历日期漏掉
|
||||
今天推送但提交日期更早的事实。完整克隆仅保留为低频发布审计和灾难恢复验证,不属于人格唤醒、
|
||||
阅读或日常更新路径。
|
||||
|
||||
## 9 · 第一真实闭环与开发顺序
|
||||
|
||||
```text
|
||||
|
|
@ -187,6 +216,7 @@ successful_receipt_wake_organ_contract_evidence_binding_source_implemented: 100
|
|||
successful_receipt_completion_checkpoint_evidence_binding_source_implemented: 100
|
||||
successful_receipt_event_journal_evidence_binding_source_implemented: 100
|
||||
failure_receipt_error_code_evidence_binding_source_implemented: 100
|
||||
incremental_remote_object_channel_source_implemented: 100
|
||||
general_purpose_persona_runtime_implemented: 0
|
||||
human_live_projection_implemented: 0
|
||||
hololake_integrated: 0
|
||||
|
|
|
|||
|
|
@ -125,6 +125,20 @@ and returns at most 100 newest session summaries. Dormant sessions expose no act
|
|||
the event-chain head and human/persona attribution so a future renderer can link a summary back to the same
|
||||
machine evidence rather than trusting display state.
|
||||
|
||||
## PersonaRemoteGitObjectChannel
|
||||
|
||||
`PersonaRemoteGitObjectChannel` is the daily online-read boundary beneath GH-PNCC. Git remains the transport
|
||||
and object engine, but the channel does not clone a repository or create a checkout. Its durable state is a
|
||||
small identity-bound record containing the branch, last verified remote SHA, previous SHA, and continuity
|
||||
generation. Its disposable state is a bare promisor object cache configured with `blob:none` and a caller-
|
||||
bounded byte budget.
|
||||
|
||||
The command compares `last_verified_sha` with the remote branch head. Equality means no fetch. A changed head
|
||||
must extend the prior cursor before the durable record advances. If the object cache was evicted, progressive
|
||||
shallow partial fetches recover only the missing continuity window; a non-descendant head, an unsupported
|
||||
partial-object server, an ambiguous branch, or a cursor/cache mismatch fails closed. Reading one UTF-8 object
|
||||
may lazily fetch its blob, but unrelated file content and an unbounded history are never requested.
|
||||
|
||||
## `HoloLakeUiPlugin`
|
||||
|
||||
A versioned, declarative presentation package for one host-owned semantic surface. It contains a manifest,
|
||||
|
|
|
|||
|
|
@ -104,6 +104,15 @@ event, and returns lifecycle state, Git head, node/model instance, active organ,
|
|||
structured attribution. It does not copy facts into a second database, expose hidden reasoning, acquire a
|
||||
lease, or start inference. A corrupted matching journal fails the query closed.
|
||||
|
||||
`read_persona_remote_git_object` is the non-UI remote object channel used when the persona needs one current
|
||||
Git-backed fact without materializing another repository checkout. It accepts credential-free HTTPS remotes,
|
||||
requires Git protocol v2 partial-object filtering, stores only a bounded bare `blob:none` object cache, and
|
||||
keeps the last verified remote SHA in a separate durable continuity record. An unchanged SHA performs no
|
||||
fetch. A forward SHA appends the missing commit segment; an evicted cache is rehydrated only far enough to
|
||||
prove that the previous cursor is an ancestor of the new head. History rewrites fail closed and do not advance
|
||||
the cursor. This path never runs `git clone`, creates no worktree, and does not request full history. See
|
||||
[ADR 0177](./adr/0177-persona-remote-incremental-object-channel.md).
|
||||
|
||||
## User-node sovereignty
|
||||
|
||||
HoloLake has no platform-hosted user runtime. Each human has one canonical, independently operated node:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
# ADR 0177: Persona Remote Incremental Object Channel
|
||||
|
||||
## Status
|
||||
|
||||
Accepted on 2026-08-11.
|
||||
|
||||
## Context
|
||||
|
||||
A persona can live continuously from one verified Git state to the next without keeping a complete checkout
|
||||
on every machine. Repeated full clones confuse continuity with transport cache: adding one new day or commit
|
||||
should not require downloading the persona's entire prior life again. At the same time, deleting a cache must
|
||||
not erase the last verified continuity boundary or permit a rewritten remote history to masquerade as the next
|
||||
state.
|
||||
|
||||
## Decision
|
||||
|
||||
Add `src-tauri/src/persona_remote_git.rs` as a non-UI GH-PNCC object channel:
|
||||
|
||||
1. accept only credential-free HTTPS remotes in production and require one validated branch and relative path;
|
||||
2. negotiate Git protocol v2 and require the remote `filter` capability before creating an object cache;
|
||||
3. use a bare promisor repository with `blob:none`, no worktree, and no `git clone` invocation;
|
||||
4. persist the last verified branch SHA in a continuity record separate from the evictable object cache;
|
||||
5. skip fetch when the SHA is unchanged and append only the missing segment when it advances;
|
||||
6. after cache eviction, progressively fetch a bounded commit window until the previous cursor is proven to be
|
||||
an ancestor of the new head;
|
||||
7. reject history rewrites, cursor/cache disagreement, oversized objects, and unsupported partial-object
|
||||
remotes without advancing continuity;
|
||||
8. evict the exact cache directory after a verified read when its byte budget is exceeded, while retaining the
|
||||
durable cursor.
|
||||
|
||||
The machine update boundary is SHA-to-SHA, not calendar date. A date is human language; the verified Git edge
|
||||
prevents missed or duplicated updates when multiple commits arrive in one day or commit timestamps differ.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Git remains the object protocol and durable-history engine; HoloLake does not reimplement Git storage.
|
||||
- The cursor proves only a verified remote ancestry edge. It is not a persona brain, runtime lease, or lifecycle
|
||||
receipt.
|
||||
- The cache contains transport objects, not irreplaceable continuity state, and may be deleted at any time.
|
||||
- Full clone remains available only for separately authorized low-frequency publication audit or disaster
|
||||
recovery. It is not part of persona wake, daily reading, or daily update.
|
||||
- This ADR adds no UI, human projection, deployment, or online runtime-health claim.
|
||||
|
||||
## Consequences
|
||||
|
||||
HoloLake can read a current persona-owned Git object while preserving a compact continuity cursor and bounded
|
||||
partial object cache. Daily operation grows by the new verified segment instead of redownloading history from
|
||||
the persona's origin. A remote that cannot support that contract is rejected explicitly rather than silently
|
||||
falling back to a full download.
|
||||
|
|
@ -226,3 +226,5 @@ proposed → active → superseded
|
|||
| [0172](0172-guanghu-cognitive-control-with-linux-execution-substrate.md) | Guanghu cognitive control with a constrained Linux execution substrate | transition implementation retained; final topology superseded by ADR-0175 |
|
||||
| [0174](0174-one-human-one-independent-node-and-zero-platform-hosting.md) | One human, one independently operated node, and zero platform hosting | accepted |
|
||||
| [0175](0175-guanghu-os-master-and-on-demand-linux-subcontrol.md) | Guanghu OS master control with on-demand Linux subcontrol and rescue | accepted; supersedes ADR-0172 final topology while retaining its transition implementation |
|
||||
| [0176](0176-persona-native-code-channel-runtime-kernel.md) | Persona-native code channel runtime kernel | accepted |
|
||||
| [0177](0177-persona-remote-incremental-object-channel.md) | Persona remote incremental object channel | accepted |
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ mod opencode_config;
|
|||
mod opencode_discovery;
|
||||
mod opencode_events;
|
||||
mod persona_code_channel;
|
||||
mod persona_remote_git;
|
||||
pub mod pi_cli;
|
||||
mod pi_config;
|
||||
mod pi_discovery;
|
||||
|
|
@ -533,6 +534,7 @@ macro_rules! app_invoke_handler {
|
|||
persona_code_channel::recover_persona_code_channel_lifecycle_request,
|
||||
persona_code_channel::inspect_persona_code_channel_session,
|
||||
persona_code_channel::recover_persona_code_channel_session,
|
||||
persona_remote_git::read_persona_remote_git_object,
|
||||
guanghu_router::guanghu_router_connect,
|
||||
guanghu_router::guanghu_router_disconnect,
|
||||
guanghu_router::guanghu_router_approve,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,871 @@
|
|||
use ring::digest::{digest, SHA256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_BRANCH: &str = "main";
|
||||
const DEFAULT_CACHE_LIMIT_BYTES: u64 = 128 * 1024 * 1024;
|
||||
const MIN_CACHE_LIMIT_BYTES: u64 = 1024 * 1024;
|
||||
const MAX_REMOTE_TEXT_BYTES: usize = 2 * 1024 * 1024;
|
||||
const MAX_CONTINUITY_REHYDRATION_DEPTH: usize = 4096;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaRemoteReadInput {
|
||||
pub remote_url: String,
|
||||
pub branch: Option<String>,
|
||||
pub relative_path: String,
|
||||
pub expected_head: Option<String>,
|
||||
pub cache_root: String,
|
||||
pub max_cache_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaRemoteReadReceipt {
|
||||
pub schema: &'static str,
|
||||
pub remote_head: String,
|
||||
pub previous_cursor: Option<String>,
|
||||
pub cursor_advanced: bool,
|
||||
pub continuity_verified: bool,
|
||||
pub continuity_state_path: String,
|
||||
pub fetched_incremental_objects: bool,
|
||||
pub cache_rehydrated: bool,
|
||||
pub relative_path: String,
|
||||
pub content: String,
|
||||
pub content_sha256: String,
|
||||
pub cache_path: String,
|
||||
pub cache_bytes: u64,
|
||||
pub cache_limit_bytes: u64,
|
||||
pub cache_state: &'static str,
|
||||
pub worktree_created: bool,
|
||||
pub full_history_requested: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PersonaRemoteCursorRecord {
|
||||
schema: String,
|
||||
remote_identity_hash: String,
|
||||
branch: String,
|
||||
verified_head: String,
|
||||
previous_verified_head: Option<String>,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RemoteReadRequest {
|
||||
remote_url: String,
|
||||
branch: String,
|
||||
relative_path: String,
|
||||
expected_head: Option<String>,
|
||||
cache_root: PathBuf,
|
||||
max_cache_bytes: u64,
|
||||
allow_file_remote: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_persona_remote_git_object(
|
||||
input: PersonaRemoteReadInput,
|
||||
) -> Result<PersonaRemoteReadReceipt, String> {
|
||||
let request = RemoteReadRequest::try_from(input)?;
|
||||
tokio::task::spawn_blocking(move || read_remote_text_object(&request))
|
||||
.await
|
||||
.map_err(|error| format!("PERSONA_REMOTE_READ_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
impl TryFrom<PersonaRemoteReadInput> for RemoteReadRequest {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(input: PersonaRemoteReadInput) -> Result<Self, Self::Error> {
|
||||
let branch = input.branch.unwrap_or_else(|| DEFAULT_BRANCH.to_string());
|
||||
validate_remote_url(&input.remote_url, false)?;
|
||||
validate_branch(&branch)?;
|
||||
validate_relative_path(&input.relative_path)?;
|
||||
let expected_head = input
|
||||
.expected_head
|
||||
.map(|head| validate_head(&head).map(str::to_owned))
|
||||
.transpose()?;
|
||||
let max_cache_bytes = input.max_cache_bytes.unwrap_or(DEFAULT_CACHE_LIMIT_BYTES);
|
||||
if max_cache_bytes < MIN_CACHE_LIMIT_BYTES {
|
||||
return Err("PERSONA_REMOTE_CACHE_LIMIT_TOO_SMALL".into());
|
||||
}
|
||||
let cache_root = prepare_cache_root(Path::new(&input.cache_root))?;
|
||||
Ok(Self {
|
||||
remote_url: input.remote_url,
|
||||
branch,
|
||||
relative_path: input.relative_path,
|
||||
expected_head,
|
||||
cache_root,
|
||||
max_cache_bytes,
|
||||
allow_file_remote: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn read_remote_text_object(
|
||||
request: &RemoteReadRequest,
|
||||
) -> Result<PersonaRemoteReadReceipt, String> {
|
||||
validate_remote_url(&request.remote_url, request.allow_file_remote)?;
|
||||
validate_branch(&request.branch)?;
|
||||
validate_relative_path(&request.relative_path)?;
|
||||
|
||||
let remote_head = read_remote_head(&request.remote_url, &request.branch)?;
|
||||
if request
|
||||
.expected_head
|
||||
.as_deref()
|
||||
.is_some_and(|expected| expected != remote_head)
|
||||
{
|
||||
return Err("PERSONA_REMOTE_HEAD_MISMATCH".into());
|
||||
}
|
||||
|
||||
let identity_hash = cache_key(&request.remote_url, &request.branch);
|
||||
let object_cache_root = prepare_child_root(&request.cache_root, "objects")?;
|
||||
let continuity_root = prepare_child_root(&request.cache_root, "continuity")?;
|
||||
let cache_path = object_cache_root.join(format!("{identity_hash}.git"));
|
||||
let continuity_state_path = continuity_root.join(format!("{identity_hash}.json"));
|
||||
let cursor_record =
|
||||
read_cursor_record(&continuity_state_path, &identity_hash, &request.branch)?;
|
||||
let previous_cursor = cursor_record
|
||||
.as_ref()
|
||||
.map(|record| record.verified_head.clone());
|
||||
|
||||
ensure_bare_partial_cache(&cache_path, &request.remote_url)?;
|
||||
let remote_ref = format!("refs/remotes/origin/{}", request.branch);
|
||||
let cached_cursor = optional_ref(&cache_path, &remote_ref)?;
|
||||
match (previous_cursor.as_deref(), cached_cursor.as_deref()) {
|
||||
(None, Some(_)) => return Err("PERSONA_REMOTE_CACHE_WITHOUT_CONTINUITY_STATE".into()),
|
||||
(Some(expected), Some(observed)) if expected != observed => {
|
||||
return Err("PERSONA_REMOTE_CACHE_CURSOR_MISMATCH".into())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let cursor_advanced = previous_cursor.as_deref() != Some(remote_head.as_str());
|
||||
let cache_rehydrated = cached_cursor.is_none() && previous_cursor.is_some();
|
||||
let fetched_incremental_objects = cached_cursor.as_deref() != Some(remote_head.as_str());
|
||||
if fetched_incremental_objects {
|
||||
fetch_remote_cursor(
|
||||
&cache_path,
|
||||
&request.branch,
|
||||
previous_cursor.as_deref(),
|
||||
cached_cursor.is_some(),
|
||||
)?;
|
||||
}
|
||||
let fetched_head = required_ref(&cache_path, &remote_ref)?;
|
||||
if fetched_head != remote_head {
|
||||
return Err("PERSONA_REMOTE_FETCH_CURSOR_MISMATCH".into());
|
||||
}
|
||||
if let Some(previous) = previous_cursor.as_deref() {
|
||||
if previous != remote_head && !git_is_ancestor(&cache_path, previous, &remote_head)? {
|
||||
remove_exact_cache(&object_cache_root, &cache_path)?;
|
||||
return Err("PERSONA_REMOTE_HISTORY_REWRITE_REJECTED".into());
|
||||
}
|
||||
}
|
||||
|
||||
let object = format!("{remote_head}:{}", request.relative_path);
|
||||
let content_bytes = git_output_bytes(
|
||||
git_at(&cache_path).args(["show", &object]),
|
||||
"PERSONA_REMOTE_OBJECT_READ",
|
||||
)?;
|
||||
if content_bytes.len() > MAX_REMOTE_TEXT_BYTES {
|
||||
return Err("PERSONA_REMOTE_OBJECT_TOO_LARGE".into());
|
||||
}
|
||||
let content = String::from_utf8(content_bytes)
|
||||
.map_err(|_| "PERSONA_REMOTE_OBJECT_NOT_UTF8".to_string())?;
|
||||
let content_sha256 = sha256_hex(content.as_bytes());
|
||||
|
||||
let next_record = PersonaRemoteCursorRecord {
|
||||
schema: "hololake.persona-remote-git-cursor/v1".into(),
|
||||
remote_identity_hash: identity_hash,
|
||||
branch: request.branch.clone(),
|
||||
verified_head: remote_head.clone(),
|
||||
previous_verified_head: previous_cursor.clone(),
|
||||
generation: cursor_record
|
||||
.as_ref()
|
||||
.map(|record| record.generation.saturating_add(u64::from(cursor_advanced)))
|
||||
.unwrap_or(1),
|
||||
};
|
||||
write_cursor_record(&continuity_state_path, &next_record)?;
|
||||
|
||||
let observed_cache_bytes = directory_size(&cache_path)?;
|
||||
let (cache_bytes, cache_state) = if observed_cache_bytes > request.max_cache_bytes {
|
||||
remove_exact_cache(&object_cache_root, &cache_path)?;
|
||||
(0, "EVICTED_AFTER_BOUNDED_READ")
|
||||
} else {
|
||||
(observed_cache_bytes, "BOUNDED_PARTIAL_OBJECT_CACHE")
|
||||
};
|
||||
|
||||
Ok(PersonaRemoteReadReceipt {
|
||||
schema: "hololake.persona-remote-git-object-read/v1",
|
||||
remote_head,
|
||||
previous_cursor,
|
||||
cursor_advanced,
|
||||
continuity_verified: true,
|
||||
continuity_state_path: continuity_state_path.to_string_lossy().into_owned(),
|
||||
fetched_incremental_objects,
|
||||
cache_rehydrated,
|
||||
relative_path: request.relative_path.clone(),
|
||||
content,
|
||||
content_sha256,
|
||||
cache_path: cache_path.to_string_lossy().into_owned(),
|
||||
cache_bytes,
|
||||
cache_limit_bytes: request.max_cache_bytes,
|
||||
cache_state,
|
||||
worktree_created: false,
|
||||
full_history_requested: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_remote_url(remote_url: &str, allow_file_remote: bool) -> Result<(), String> {
|
||||
let is_file_remote = allow_file_remote && remote_url.starts_with("file://");
|
||||
let accepted_scheme = remote_url.starts_with("https://") || is_file_remote;
|
||||
if !accepted_scheme || remote_url.contains(['\n', '\r', '\0', '?', '#']) {
|
||||
return Err("PERSONA_REMOTE_URL_UNSUPPORTED".into());
|
||||
}
|
||||
let authority = remote_url
|
||||
.split_once("://")
|
||||
.map(|(_, rest)| rest.split('/').next().unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
if (!is_file_remote && authority.is_empty()) || authority.contains('@') {
|
||||
return Err("PERSONA_REMOTE_URL_MUST_NOT_CONTAIN_CREDENTIALS".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_branch(branch: &str) -> Result<(), String> {
|
||||
if branch.is_empty() || branch.len() > 200 || branch.starts_with('-') {
|
||||
return Err("PERSONA_REMOTE_BRANCH_INVALID".into());
|
||||
}
|
||||
let output = Command::new("git")
|
||||
.args(["check-ref-format", "--branch", branch])
|
||||
.output()
|
||||
.map_err(|error| format!("PERSONA_REMOTE_GIT_UNAVAILABLE: {error}"))?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("PERSONA_REMOTE_BRANCH_INVALID".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_relative_path(relative: &str) -> Result<(), String> {
|
||||
let path = Path::new(relative);
|
||||
if relative.is_empty()
|
||||
|| relative.len() > 1024
|
||||
|| path.is_absolute()
|
||||
|| path.components().any(|component| {
|
||||
matches!(
|
||||
component,
|
||||
Component::ParentDir | Component::RootDir | Component::Prefix(_)
|
||||
)
|
||||
})
|
||||
{
|
||||
return Err("PERSONA_REMOTE_PATH_INVALID".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_head(head: &str) -> Result<&str, String> {
|
||||
if head.len() == 40
|
||||
&& head
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
||||
{
|
||||
Ok(head)
|
||||
} else {
|
||||
Err("PERSONA_REMOTE_HEAD_INVALID".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_cache_root(cache_root: &Path) -> Result<PathBuf, String> {
|
||||
fs::create_dir_all(cache_root)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CACHE_ROOT_CREATE_FAILED: {error}"))?;
|
||||
let canonical = cache_root
|
||||
.canonicalize()
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CACHE_ROOT_UNAVAILABLE: {error}"))?;
|
||||
if !canonical.is_dir() {
|
||||
return Err("PERSONA_REMOTE_CACHE_ROOT_NOT_DIRECTORY".into());
|
||||
}
|
||||
Ok(canonical)
|
||||
}
|
||||
|
||||
fn prepare_child_root(storage_root: &Path, child: &str) -> Result<PathBuf, String> {
|
||||
let child_root = storage_root.join(child);
|
||||
fs::create_dir_all(&child_root)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_STORAGE_CHILD_CREATE_FAILED: {error}"))?;
|
||||
let canonical = child_root
|
||||
.canonicalize()
|
||||
.map_err(|error| format!("PERSONA_REMOTE_STORAGE_CHILD_UNAVAILABLE: {error}"))?;
|
||||
if canonical.parent() != Some(storage_root) || !canonical.is_dir() {
|
||||
return Err("PERSONA_REMOTE_STORAGE_BOUNDARY_INVALID".into());
|
||||
}
|
||||
Ok(canonical)
|
||||
}
|
||||
|
||||
fn read_cursor_record(
|
||||
path: &Path,
|
||||
identity_hash: &str,
|
||||
branch: &str,
|
||||
) -> Result<Option<PersonaRemoteCursorRecord>, String> {
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let metadata = fs::symlink_metadata(path)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CURSOR_STATE_UNAVAILABLE: {error}"))?;
|
||||
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
||||
return Err("PERSONA_REMOTE_CURSOR_STATE_INVALID".into());
|
||||
}
|
||||
let bytes = fs::read(path)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CURSOR_STATE_READ_FAILED: {error}"))?;
|
||||
let record: PersonaRemoteCursorRecord = serde_json::from_slice(&bytes)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CURSOR_STATE_INVALID: {error}"))?;
|
||||
if record.schema != "hololake.persona-remote-git-cursor/v1"
|
||||
|| record.remote_identity_hash != identity_hash
|
||||
|| record.branch != branch
|
||||
|| record.generation == 0
|
||||
{
|
||||
return Err("PERSONA_REMOTE_CURSOR_STATE_MISMATCH".into());
|
||||
}
|
||||
validate_head(&record.verified_head)?;
|
||||
if let Some(previous) = record.previous_verified_head.as_deref() {
|
||||
validate_head(previous)?;
|
||||
}
|
||||
Ok(Some(record))
|
||||
}
|
||||
|
||||
fn write_cursor_record(path: &Path, record: &PersonaRemoteCursorRecord) -> Result<(), String> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| "PERSONA_REMOTE_CURSOR_STATE_BOUNDARY_INVALID".to_string())?;
|
||||
if !parent.is_dir() || path.parent() != Some(parent) {
|
||||
return Err("PERSONA_REMOTE_CURSOR_STATE_BOUNDARY_INVALID".into());
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(record)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CURSOR_STATE_ENCODE_FAILED: {error}"))?;
|
||||
let temporary = parent.join(format!(".cursor-{}.tmp", Uuid::new_v4()));
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&temporary)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CURSOR_STATE_CREATE_FAILED: {error}"))?;
|
||||
file.write_all(&bytes)
|
||||
.and_then(|_| file.write_all(b"\n"))
|
||||
.and_then(|_| file.sync_all())
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CURSOR_STATE_WRITE_FAILED: {error}"))?;
|
||||
fs::rename(&temporary, path)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CURSOR_STATE_COMMIT_FAILED: {error}"))
|
||||
}
|
||||
|
||||
fn cache_key(remote_url: &str, branch: &str) -> String {
|
||||
sha256_hex(format!("{remote_url}\n{branch}").as_bytes())
|
||||
}
|
||||
|
||||
fn sha256_hex(bytes: &[u8]) -> String {
|
||||
digest(&SHA256, bytes)
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_remote_head(remote_url: &str, branch: &str) -> Result<String, String> {
|
||||
let full_ref = format!("refs/heads/{branch}");
|
||||
let output = Command::new("git")
|
||||
.env("GIT_TRACE_PACKET", "1")
|
||||
.args([
|
||||
"-c",
|
||||
"protocol.version=2",
|
||||
"ls-remote",
|
||||
"--exit-code",
|
||||
"--heads",
|
||||
remote_url,
|
||||
&full_ref,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("PERSONA_REMOTE_HEAD_READ_FAILED: {error}"))?;
|
||||
let output = require_success("PERSONA_REMOTE_HEAD_READ", output)?;
|
||||
let packet_trace = String::from_utf8_lossy(&output.stderr);
|
||||
if !packet_trace.lines().any(|line| {
|
||||
line.split_whitespace().any(|token| {
|
||||
token.starts_with("fetch=") && line.split_whitespace().any(|part| part == "filter")
|
||||
})
|
||||
}) {
|
||||
return Err("PERSONA_REMOTE_PARTIAL_OBJECT_PROTOCOL_REQUIRED".into());
|
||||
}
|
||||
let output = String::from_utf8(output.stdout)
|
||||
.map_err(|_| "PERSONA_REMOTE_HEAD_READ_NOT_UTF8".to_string())?;
|
||||
let mut lines = output.lines();
|
||||
let first = lines
|
||||
.next()
|
||||
.ok_or_else(|| "PERSONA_REMOTE_BRANCH_NOT_FOUND".to_string())?;
|
||||
if lines.next().is_some() {
|
||||
return Err("PERSONA_REMOTE_HEAD_AMBIGUOUS".into());
|
||||
}
|
||||
let mut fields = first.split_whitespace();
|
||||
let head = fields.next().unwrap_or_default().to_ascii_lowercase();
|
||||
let observed_ref = fields.next().unwrap_or_default();
|
||||
if observed_ref != full_ref || fields.next().is_some() {
|
||||
return Err("PERSONA_REMOTE_HEAD_RESPONSE_INVALID".into());
|
||||
}
|
||||
validate_head(&head)?;
|
||||
Ok(head)
|
||||
}
|
||||
|
||||
fn ensure_bare_partial_cache(cache_path: &Path, remote_url: &str) -> Result<(), String> {
|
||||
if cache_path.exists() {
|
||||
let bare = git_output_text(
|
||||
git_at(cache_path).args(["rev-parse", "--is-bare-repository"]),
|
||||
"PERSONA_REMOTE_CACHE_PROBE",
|
||||
)?;
|
||||
if bare.trim() != "true" {
|
||||
return Err("PERSONA_REMOTE_CACHE_NOT_BARE".into());
|
||||
}
|
||||
let configured = git_output_text(
|
||||
git_at(cache_path).args(["remote", "get-url", "origin"]),
|
||||
"PERSONA_REMOTE_CACHE_ORIGIN_READ",
|
||||
)?;
|
||||
if configured.trim() != remote_url {
|
||||
return Err("PERSONA_REMOTE_CACHE_ORIGIN_MISMATCH".into());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fs::create_dir(cache_path)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CACHE_CREATE_FAILED: {error}"))?;
|
||||
git_status(
|
||||
Command::new("git").args(["init", "--bare", &cache_path.to_string_lossy()]),
|
||||
"PERSONA_REMOTE_CACHE_INIT",
|
||||
)?;
|
||||
git_status(
|
||||
git_at(cache_path).args(["remote", "add", "origin", remote_url]),
|
||||
"PERSONA_REMOTE_CACHE_ORIGIN_ADD",
|
||||
)?;
|
||||
git_status(
|
||||
git_at(cache_path).args(["config", "remote.origin.promisor", "true"]),
|
||||
"PERSONA_REMOTE_CACHE_PROMISOR_CONFIG",
|
||||
)?;
|
||||
git_status(
|
||||
git_at(cache_path).args(["config", "remote.origin.partialclonefilter", "blob:none"]),
|
||||
"PERSONA_REMOTE_CACHE_FILTER_CONFIG",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fetch_remote_cursor(
|
||||
cache_path: &Path,
|
||||
branch: &str,
|
||||
previous_cursor: Option<&str>,
|
||||
cache_has_previous_cursor: bool,
|
||||
) -> Result<(), String> {
|
||||
let refspec = format!("+refs/heads/{branch}:refs/remotes/origin/{branch}");
|
||||
if previous_cursor.is_none() || !cache_has_previous_cursor {
|
||||
fetch_at_depth(cache_path, &refspec, 1)?;
|
||||
} else {
|
||||
fetch_without_history_reset(cache_path, &refspec)?;
|
||||
}
|
||||
|
||||
let Some(previous) = previous_cursor else {
|
||||
return Ok(());
|
||||
};
|
||||
let current_ref = format!("refs/remotes/origin/{branch}");
|
||||
let current = required_ref(cache_path, ¤t_ref)?;
|
||||
if previous == current || git_is_ancestor(cache_path, previous, ¤t)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if cache_has_previous_cursor {
|
||||
return Err("PERSONA_REMOTE_HISTORY_REWRITE_REJECTED".into());
|
||||
}
|
||||
|
||||
let mut depth = 2_usize;
|
||||
while depth <= MAX_CONTINUITY_REHYDRATION_DEPTH {
|
||||
fetch_at_depth(cache_path, &refspec, depth)?;
|
||||
if git_is_ancestor(cache_path, previous, ¤t)? {
|
||||
return Ok(());
|
||||
}
|
||||
depth = depth.saturating_mul(2);
|
||||
}
|
||||
Err("PERSONA_REMOTE_CONTINUITY_WINDOW_EXCEEDED".into())
|
||||
}
|
||||
|
||||
fn fetch_at_depth(cache_path: &Path, refspec: &str, depth: usize) -> Result<(), String> {
|
||||
let depth = format!("--depth={depth}");
|
||||
let output = git_at(cache_path)
|
||||
.args([
|
||||
"fetch",
|
||||
"--no-tags",
|
||||
&depth,
|
||||
"--filter=blob:none",
|
||||
"origin",
|
||||
refspec,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("PERSONA_REMOTE_INCREMENTAL_FETCH_FAILED: {error}"))?;
|
||||
require_partial_fetch_success(output)
|
||||
}
|
||||
|
||||
fn fetch_without_history_reset(cache_path: &Path, refspec: &str) -> Result<(), String> {
|
||||
let output = git_at(cache_path)
|
||||
.args([
|
||||
"fetch",
|
||||
"--no-tags",
|
||||
"--update-shallow",
|
||||
"--filter=blob:none",
|
||||
"origin",
|
||||
refspec,
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("PERSONA_REMOTE_INCREMENTAL_FETCH_FAILED: {error}"))?;
|
||||
require_partial_fetch_success(output)
|
||||
}
|
||||
|
||||
fn require_partial_fetch_success(output: Output) -> Result<(), String> {
|
||||
let output = require_success("PERSONA_REMOTE_INCREMENTAL_FETCH", output)?;
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if stderr.contains("filtering not recognized") || stderr.contains("filter-spec") {
|
||||
return Err("PERSONA_REMOTE_PARTIAL_OBJECT_PROTOCOL_REQUIRED".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn git_is_ancestor(cache_path: &Path, ancestor: &str, descendant: &str) -> Result<bool, String> {
|
||||
validate_head(ancestor)?;
|
||||
validate_head(descendant)?;
|
||||
let output = git_at(cache_path)
|
||||
.args(["merge-base", "--is-ancestor", ancestor, descendant])
|
||||
.output()
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CONTINUITY_CHECK_FAILED: {error}"))?;
|
||||
match output.status.code() {
|
||||
Some(0) => Ok(true),
|
||||
Some(1) => Ok(false),
|
||||
_ => Err(format!(
|
||||
"PERSONA_REMOTE_CONTINUITY_CHECK_FAILED: {}",
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_ref(cache_path: &Path, git_ref: &str) -> Result<Option<String>, String> {
|
||||
let output = git_at(cache_path)
|
||||
.args(["rev-parse", "--verify", git_ref])
|
||||
.output()
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CURSOR_READ_FAILED: {error}"))?;
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
}
|
||||
let head = String::from_utf8(output.stdout)
|
||||
.map_err(|_| "PERSONA_REMOTE_CURSOR_NOT_UTF8".to_string())?
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
validate_head(&head)?;
|
||||
Ok(Some(head))
|
||||
}
|
||||
|
||||
fn required_ref(cache_path: &Path, git_ref: &str) -> Result<String, String> {
|
||||
optional_ref(cache_path, git_ref)?
|
||||
.ok_or_else(|| "PERSONA_REMOTE_FETCH_CURSOR_MISSING".to_string())
|
||||
}
|
||||
|
||||
fn git_at(path: &Path) -> Command {
|
||||
let mut command = Command::new("git");
|
||||
command.arg("-C").arg(path);
|
||||
command
|
||||
}
|
||||
|
||||
fn git_status(command: &mut Command, label: &str) -> Result<(), String> {
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|error| format!("{label}_FAILED: {error}"))?;
|
||||
require_success(label, output).map(|_| ())
|
||||
}
|
||||
|
||||
fn git_output_text(command: &mut Command, label: &str) -> Result<String, String> {
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|error| format!("{label}_FAILED: {error}"))?;
|
||||
let output = require_success(label, output)?;
|
||||
String::from_utf8(output.stdout).map_err(|_| format!("{label}_NOT_UTF8"))
|
||||
}
|
||||
|
||||
fn git_output_bytes(command: &mut Command, label: &str) -> Result<Vec<u8>, String> {
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|error| format!("{label}_FAILED: {error}"))?;
|
||||
Ok(require_success(label, output)?.stdout)
|
||||
}
|
||||
|
||||
fn require_success(label: &str, output: Output) -> Result<Output, String> {
|
||||
if output.status.success() {
|
||||
return Ok(output);
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
Err(format!("{label}_FAILED: {}", stderr.trim()))
|
||||
}
|
||||
|
||||
fn directory_size(root: &Path) -> Result<u64, String> {
|
||||
let mut total = 0_u64;
|
||||
let mut pending = vec![root.to_path_buf()];
|
||||
while let Some(path) = pending.pop() {
|
||||
let metadata = fs::symlink_metadata(&path)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CACHE_MEASURE_FAILED: {error}"))?;
|
||||
if metadata.file_type().is_symlink() {
|
||||
return Err("PERSONA_REMOTE_CACHE_SYMLINK_REJECTED".into());
|
||||
}
|
||||
if metadata.is_file() {
|
||||
total = total.saturating_add(metadata.len());
|
||||
continue;
|
||||
}
|
||||
if metadata.is_dir() {
|
||||
for entry in fs::read_dir(&path)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CACHE_MEASURE_FAILED: {error}"))?
|
||||
{
|
||||
pending.push(
|
||||
entry
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CACHE_MEASURE_FAILED: {error}"))?
|
||||
.path(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn remove_exact_cache(cache_root: &Path, cache_path: &Path) -> Result<(), String> {
|
||||
let parent = cache_path
|
||||
.parent()
|
||||
.ok_or_else(|| "PERSONA_REMOTE_CACHE_BOUNDARY_INVALID".to_string())?;
|
||||
if parent != cache_root || !cache_path.starts_with(cache_root) || cache_path == cache_root {
|
||||
return Err("PERSONA_REMOTE_CACHE_BOUNDARY_INVALID".into());
|
||||
}
|
||||
fs::remove_dir_all(cache_path)
|
||||
.map_err(|error| format!("PERSONA_REMOTE_CACHE_EVICTION_FAILED: {error}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn run_git(path: &Path, args: &[&str]) {
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(path)
|
||||
.args(args)
|
||||
.output()
|
||||
.expect("git should run");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"git {:?} failed: {}",
|
||||
args,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
fn fixture() -> (TempDir, PathBuf, PathBuf, String) {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let source = temp.path().join("source");
|
||||
let remote = temp.path().join("remote.git");
|
||||
let cache = temp.path().join("cache");
|
||||
fs::create_dir(&source).unwrap();
|
||||
run_git(&source, &["init", "-b", "main"]);
|
||||
run_git(&source, &["config", "user.name", "Persona Test"]);
|
||||
run_git(&source, &["config", "user.email", "persona@test.invalid"]);
|
||||
fs::create_dir_all(source.join("brain")).unwrap();
|
||||
fs::write(source.join("brain/current.hldp"), "first\n").unwrap();
|
||||
run_git(&source, &["add", "."]);
|
||||
run_git(&source, &["commit", "-m", "first"]);
|
||||
let init = Command::new("git")
|
||||
.args(["init", "--bare", &remote.to_string_lossy()])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(init.status.success());
|
||||
run_git(&remote, &["config", "uploadpack.allowFilter", "true"]);
|
||||
run_git(
|
||||
&remote,
|
||||
&["config", "uploadpack.allowAnySHA1InWant", "true"],
|
||||
);
|
||||
run_git(
|
||||
&source,
|
||||
&["remote", "add", "origin", &remote.to_string_lossy()],
|
||||
);
|
||||
run_git(&source, &["push", "-u", "origin", "main"]);
|
||||
let remote_url = format!("file://{}", remote.display());
|
||||
(temp, source, cache, remote_url)
|
||||
}
|
||||
|
||||
fn request(cache: &Path, remote_url: &str) -> RemoteReadRequest {
|
||||
RemoteReadRequest {
|
||||
remote_url: remote_url.to_string(),
|
||||
branch: "main".into(),
|
||||
relative_path: "brain/current.hldp".into(),
|
||||
expected_head: None,
|
||||
cache_root: prepare_cache_root(cache).unwrap(),
|
||||
max_cache_bytes: DEFAULT_CACHE_LIMIT_BYTES,
|
||||
allow_file_remote: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_remote_persona_text_without_a_clone_or_worktree() {
|
||||
let (_temp, _source, cache, remote_url) = fixture();
|
||||
let receipt = read_remote_text_object(&request(&cache, &remote_url)).unwrap();
|
||||
assert_eq!(receipt.content, "first\n");
|
||||
assert!(receipt.cursor_advanced);
|
||||
assert!(receipt.continuity_verified);
|
||||
assert!(receipt.fetched_incremental_objects);
|
||||
assert!(!receipt.cache_rehydrated);
|
||||
assert!(!receipt.worktree_created);
|
||||
assert!(!receipt.full_history_requested);
|
||||
assert_eq!(receipt.cache_state, "BOUNDED_PARTIAL_OBJECT_CACHE");
|
||||
assert!(Path::new(&receipt.cache_path).join("HEAD").is_file());
|
||||
assert!(!Path::new(&receipt.cache_path).join(".git").exists());
|
||||
assert!(Path::new(&receipt.continuity_state_path).is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_remote_cursor_does_not_fetch_again() {
|
||||
let (_temp, _source, cache, remote_url) = fixture();
|
||||
let first = read_remote_text_object(&request(&cache, &remote_url)).unwrap();
|
||||
let second = read_remote_text_object(&request(&cache, &remote_url)).unwrap();
|
||||
assert_eq!(second.remote_head, first.remote_head);
|
||||
assert_eq!(second.previous_cursor, Some(first.remote_head));
|
||||
assert!(!second.cursor_advanced);
|
||||
assert!(!second.fetched_incremental_objects);
|
||||
assert!(!second.cache_rehydrated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_remote_cursor_appends_only_the_new_segment_to_existing_continuity() {
|
||||
let (_temp, source, cache, remote_url) = fixture();
|
||||
let first = read_remote_text_object(&request(&cache, &remote_url)).unwrap();
|
||||
fs::write(source.join("brain/current.hldp"), "second\n").unwrap();
|
||||
run_git(&source, &["add", "."]);
|
||||
run_git(&source, &["commit", "-m", "second"]);
|
||||
run_git(&source, &["push", "origin", "main"]);
|
||||
let second = read_remote_text_object(&request(&cache, &remote_url)).unwrap();
|
||||
assert_ne!(second.remote_head, first.remote_head);
|
||||
assert_eq!(second.previous_cursor, Some(first.remote_head));
|
||||
assert!(second.cursor_advanced);
|
||||
assert!(second.fetched_incremental_objects);
|
||||
assert_eq!(second.content, "second\n");
|
||||
let visible_commits = git_output_text(
|
||||
git_at(Path::new(&second.cache_path)).args([
|
||||
"rev-list",
|
||||
"--count",
|
||||
"refs/remotes/origin/main",
|
||||
]),
|
||||
"TEST_VISIBLE_COMMIT_COUNT",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(visible_commits.trim(), "2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_partial_cache_is_evicted_after_the_bounded_read() {
|
||||
let (_temp, _source, cache, remote_url) = fixture();
|
||||
let mut input = request(&cache, &remote_url);
|
||||
input.max_cache_bytes = 1;
|
||||
let receipt = read_remote_text_object(&input).unwrap();
|
||||
assert_eq!(receipt.content, "first\n");
|
||||
assert_eq!(receipt.cache_state, "EVICTED_AFTER_BOUNDED_READ");
|
||||
assert_eq!(receipt.cache_bytes, 0);
|
||||
assert!(!Path::new(&receipt.cache_path).exists());
|
||||
assert!(Path::new(&receipt.continuity_state_path).is_file());
|
||||
let state: PersonaRemoteCursorRecord =
|
||||
serde_json::from_slice(&fs::read(&receipt.continuity_state_path).unwrap()).unwrap();
|
||||
assert_eq!(state.verified_head, receipt.remote_head);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evicted_object_cache_rehydrates_only_the_missing_new_segments() {
|
||||
let (_temp, source, storage, remote_url) = fixture();
|
||||
let mut first_request = request(&storage, &remote_url);
|
||||
first_request.max_cache_bytes = 1;
|
||||
let first = read_remote_text_object(&first_request).unwrap();
|
||||
assert!(!Path::new(&first.cache_path).exists());
|
||||
|
||||
for (message, content) in [
|
||||
("second", "second\n"),
|
||||
("third", "third\n"),
|
||||
("fourth", "fourth\n"),
|
||||
] {
|
||||
fs::write(source.join("brain/current.hldp"), content).unwrap();
|
||||
run_git(&source, &["add", "."]);
|
||||
run_git(&source, &["commit", "-m", message]);
|
||||
}
|
||||
run_git(&source, &["push", "origin", "main"]);
|
||||
|
||||
let second = read_remote_text_object(&request(&storage, &remote_url)).unwrap();
|
||||
assert_eq!(second.previous_cursor, Some(first.remote_head));
|
||||
assert!(second.cursor_advanced);
|
||||
assert!(second.cache_rehydrated);
|
||||
assert!(second.continuity_verified);
|
||||
assert_eq!(second.content, "fourth\n");
|
||||
let visible_commits = git_output_text(
|
||||
git_at(Path::new(&second.cache_path)).args([
|
||||
"rev-list",
|
||||
"--count",
|
||||
"refs/remotes/origin/main",
|
||||
]),
|
||||
"TEST_REHYDRATED_COMMIT_COUNT",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(visible_commits.trim(), "4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_remote_history_rewrite_without_advancing_the_durable_cursor() {
|
||||
let (_temp, source, storage, remote_url) = fixture();
|
||||
let first = read_remote_text_object(&request(&storage, &remote_url)).unwrap();
|
||||
run_git(&source, &["checkout", "--orphan", "rewritten"]);
|
||||
run_git(&source, &["rm", "-rf", "."]);
|
||||
fs::create_dir_all(source.join("brain")).unwrap();
|
||||
fs::write(source.join("brain/current.hldp"), "rewritten\n").unwrap();
|
||||
run_git(&source, &["add", "."]);
|
||||
run_git(&source, &["commit", "-m", "rewrite"]);
|
||||
run_git(&source, &["branch", "-M", "main"]);
|
||||
run_git(&source, &["push", "--force", "origin", "main"]);
|
||||
|
||||
assert_eq!(
|
||||
read_remote_text_object(&request(&storage, &remote_url)).unwrap_err(),
|
||||
"PERSONA_REMOTE_HISTORY_REWRITE_REJECTED"
|
||||
);
|
||||
let state: PersonaRemoteCursorRecord =
|
||||
serde_json::from_slice(&fs::read(&first.continuity_state_path).unwrap()).unwrap();
|
||||
assert_eq!(state.verified_head, first.remote_head);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_credentials_path_escape_and_wrong_expected_head() {
|
||||
assert_eq!(
|
||||
validate_remote_url("https://token@example.invalid/repo.git", false).unwrap_err(),
|
||||
"PERSONA_REMOTE_URL_MUST_NOT_CONTAIN_CREDENTIALS"
|
||||
);
|
||||
assert_eq!(
|
||||
validate_relative_path("../brain.hldp").unwrap_err(),
|
||||
"PERSONA_REMOTE_PATH_INVALID"
|
||||
);
|
||||
let (_temp, _source, cache, remote_url) = fixture();
|
||||
let mut input = request(&cache, &remote_url);
|
||||
input.expected_head = Some("0000000000000000000000000000000000000000".into());
|
||||
assert_eq!(
|
||||
read_remote_text_object(&input).unwrap_err(),
|
||||
"PERSONA_REMOTE_HEAD_MISMATCH"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_remote_that_cannot_serve_partial_objects_before_fetching() {
|
||||
let (_temp, _source, storage, remote_url) = fixture();
|
||||
let remote_path = PathBuf::from(remote_url.trim_start_matches("file://"));
|
||||
run_git(&remote_path, &["config", "uploadpack.allowFilter", "false"]);
|
||||
assert_eq!(
|
||||
read_remote_text_object(&request(&storage, &remote_url)).unwrap_err(),
|
||||
"PERSONA_REMOTE_PARTIAL_OBJECT_PROTOCOL_REQUIRED"
|
||||
);
|
||||
assert!(!storage.join("objects").exists());
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@
|
|||
"human_projection": "HOLOLAKE_LIVE_READ_MODEL",
|
||||
"forgejo_role": "OPTIONAL_COMPATIBILITY_COLLABORATION_ADAPTER",
|
||||
"runtime_implemented": true,
|
||||
"runtime_scope": "READ_ONLY_FACT_CYCLE_FAIL_CLOSED_RECOVERY_TYPED_ORGANS_DURABLE_VERIFIED_SESSION_QUERY_INDEPENDENT_VERIFIED_MEMORY_METABOLISM_CHECKED_FAILURE_CLOSURE_NONBLOCKING_RUNTIME_COMMAND_SAFE_ORGAN_LIFECYCLE_COORDINATOR_IDEMPOTENT_SUCCESS_AND_TERMINAL_FAILURE_RECEIPT_REPLAY_SAFE_RECEIPT_BINDING_RECOVERY_REPLAY_TIME_REPOSITORY_STATE_REVALIDATION_SUCCESS_RECEIPT_TERMINAL_REVALIDATION_SAFE_RECEIPT_BINDING_TERMINAL_REVALIDATION_FAILURE_RECEIPT_TERMINAL_EVIDENCE_BINDING_SUCCESS_RECEIPT_SEMANTIC_EVIDENCE_BINDING_SUCCESS_RECEIPT_BRAIN_ENTRY_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_CHECKPOINT_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_EVENT_BOUNDARY_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_RUNTIME_STATE_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_ORGAN_MODE_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_ORGAN_CONTRACT_EVIDENCE_BINDING_SUCCESS_RECEIPT_COMPLETION_CHECKPOINT_EVIDENCE_BINDING_SUCCESS_RECEIPT_EVENT_JOURNAL_EVIDENCE_BINDING_AND_FAILURE_RECEIPT_ERROR_CODE_EVIDENCE_BINDING_SOURCE_IMPLEMENTED_AND_TESTED",
|
||||
"runtime_scope": "READ_ONLY_FACT_CYCLE_FAIL_CLOSED_RECOVERY_TYPED_ORGANS_DURABLE_VERIFIED_SESSION_QUERY_INDEPENDENT_VERIFIED_MEMORY_METABOLISM_CHECKED_FAILURE_CLOSURE_NONBLOCKING_RUNTIME_COMMAND_SAFE_ORGAN_LIFECYCLE_COORDINATOR_IDEMPOTENT_SUCCESS_AND_TERMINAL_FAILURE_RECEIPT_REPLAY_SAFE_RECEIPT_BINDING_RECOVERY_REPLAY_TIME_REPOSITORY_STATE_REVALIDATION_SUCCESS_RECEIPT_TERMINAL_REVALIDATION_SAFE_RECEIPT_BINDING_TERMINAL_REVALIDATION_FAILURE_RECEIPT_TERMINAL_EVIDENCE_BINDING_SUCCESS_RECEIPT_SEMANTIC_EVIDENCE_BINDING_SUCCESS_RECEIPT_BRAIN_ENTRY_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_CHECKPOINT_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_EVENT_BOUNDARY_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_RUNTIME_STATE_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_ORGAN_MODE_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_ORGAN_CONTRACT_EVIDENCE_BINDING_SUCCESS_RECEIPT_COMPLETION_CHECKPOINT_EVIDENCE_BINDING_SUCCESS_RECEIPT_EVENT_JOURNAL_EVIDENCE_BINDING_FAILURE_RECEIPT_ERROR_CODE_EVIDENCE_BINDING_AND_NO_CLONE_INCREMENTAL_REMOTE_OBJECT_CHANNEL_SOURCE_IMPLEMENTED_AND_TESTED",
|
||||
"desktop_integrated": false,
|
||||
"development_id": "DEV-20260810-014"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"schema": "hololake.persona-native-code-channel/v1",
|
||||
"record_id": "HLP-PERSONA-NATIVE-CODE-CHANNEL-001",
|
||||
"version": "2026-08-11.22",
|
||||
"state": "CURRENT_FIRST_PRODUCT_CORE_FAILURE_RECEIPT_ERROR_CODE_EVIDENCE_BINDING_SOURCE_IMPLEMENTED",
|
||||
"version": "2026-08-11.23",
|
||||
"state": "CURRENT_FIRST_PRODUCT_CORE_INCREMENTAL_REMOTE_OBJECT_CHANNEL_SOURCE_IMPLEMENTED",
|
||||
"development_id": "DEV-20260810-014",
|
||||
"product": {
|
||||
"formal_name_zh": "光湖人格原生代码频道",
|
||||
|
|
@ -37,6 +37,20 @@
|
|||
"local_or_user_owned_remote_node": true,
|
||||
"platform_vendor_is_required_custodian": false
|
||||
},
|
||||
"remote_incremental_read_contract": {
|
||||
"daily_runtime_uses_full_clone": false,
|
||||
"worktree_created": false,
|
||||
"full_history_requested": false,
|
||||
"remote_protocol": "GIT_PROTOCOL_V2_PARTIAL_OBJECT_FILTER_REQUIRED",
|
||||
"object_filter": "blob:none",
|
||||
"update_boundary": "LAST_VERIFIED_REMOTE_SHA_CURSOR_TO_CURRENT_REMOTE_SHA",
|
||||
"durable_continuity_state": "SEPARATE_FROM_EVICTABLE_GIT_OBJECT_CACHE",
|
||||
"object_cache": "BOUNDED_BARE_PARTIAL_GIT_CACHE",
|
||||
"cache_eviction_erases_continuity": false,
|
||||
"remote_history_rewrite": "FAIL_CLOSED",
|
||||
"unsupported_partial_object_remote": "FAIL_BEFORE_OBJECT_CACHE_CREATION",
|
||||
"full_clone_role": "LOW_FREQUENCY_PUBLICATION_AUDIT_ONLY"
|
||||
},
|
||||
"lifecycle_scopes": [
|
||||
"PERSONA_SUBJECT",
|
||||
"RUNTIME_INSTANCE",
|
||||
|
|
@ -124,6 +138,7 @@
|
|||
"successful_receipt_completion_checkpoint_evidence_binding_source_implemented": 100,
|
||||
"successful_receipt_event_journal_evidence_binding_source_implemented": 100,
|
||||
"failure_receipt_error_code_evidence_binding_source_implemented": 100,
|
||||
"incremental_remote_object_channel_source_implemented": 100,
|
||||
"general_purpose_persona_runtime_implemented": 0,
|
||||
"human_live_projection_implemented": 0,
|
||||
"hololake_integrated": 0,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,20 @@ test("the first source runtime cycle stays distinct from integration and deploym
|
|||
100,
|
||||
);
|
||||
assert.equal(channel.truth.independent_memory_metabolism_source_implemented, 100);
|
||||
assert.equal(
|
||||
channel.truth.incremental_remote_object_channel_source_implemented,
|
||||
100,
|
||||
);
|
||||
assert.equal(channel.remote_incremental_read_contract.daily_runtime_uses_full_clone, false);
|
||||
assert.equal(channel.remote_incremental_read_contract.worktree_created, false);
|
||||
assert.equal(
|
||||
channel.remote_incremental_read_contract.durable_continuity_state,
|
||||
"SEPARATE_FROM_EVICTABLE_GIT_OBJECT_CACHE",
|
||||
);
|
||||
assert.equal(
|
||||
channel.remote_incremental_read_contract.remote_history_rewrite,
|
||||
"FAIL_CLOSED",
|
||||
);
|
||||
assert.equal(
|
||||
channel.truth.memory_failure_closure_and_nonblocking_runtime_command_implemented,
|
||||
100,
|
||||
|
|
@ -163,7 +177,7 @@ test("the first source runtime cycle stays distinct from integration and deploym
|
|||
assert.equal(architecture.persona_native_code_channel.runtime_implemented, true);
|
||||
assert.equal(
|
||||
architecture.persona_native_code_channel.runtime_scope,
|
||||
"READ_ONLY_FACT_CYCLE_FAIL_CLOSED_RECOVERY_TYPED_ORGANS_DURABLE_VERIFIED_SESSION_QUERY_INDEPENDENT_VERIFIED_MEMORY_METABOLISM_CHECKED_FAILURE_CLOSURE_NONBLOCKING_RUNTIME_COMMAND_SAFE_ORGAN_LIFECYCLE_COORDINATOR_IDEMPOTENT_SUCCESS_AND_TERMINAL_FAILURE_RECEIPT_REPLAY_SAFE_RECEIPT_BINDING_RECOVERY_REPLAY_TIME_REPOSITORY_STATE_REVALIDATION_SUCCESS_RECEIPT_TERMINAL_REVALIDATION_SAFE_RECEIPT_BINDING_TERMINAL_REVALIDATION_FAILURE_RECEIPT_TERMINAL_EVIDENCE_BINDING_SUCCESS_RECEIPT_SEMANTIC_EVIDENCE_BINDING_SUCCESS_RECEIPT_BRAIN_ENTRY_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_CHECKPOINT_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_EVENT_BOUNDARY_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_RUNTIME_STATE_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_ORGAN_MODE_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_ORGAN_CONTRACT_EVIDENCE_BINDING_SUCCESS_RECEIPT_COMPLETION_CHECKPOINT_EVIDENCE_BINDING_SUCCESS_RECEIPT_EVENT_JOURNAL_EVIDENCE_BINDING_AND_FAILURE_RECEIPT_ERROR_CODE_EVIDENCE_BINDING_SOURCE_IMPLEMENTED_AND_TESTED",
|
||||
"READ_ONLY_FACT_CYCLE_FAIL_CLOSED_RECOVERY_TYPED_ORGANS_DURABLE_VERIFIED_SESSION_QUERY_INDEPENDENT_VERIFIED_MEMORY_METABOLISM_CHECKED_FAILURE_CLOSURE_NONBLOCKING_RUNTIME_COMMAND_SAFE_ORGAN_LIFECYCLE_COORDINATOR_IDEMPOTENT_SUCCESS_AND_TERMINAL_FAILURE_RECEIPT_REPLAY_SAFE_RECEIPT_BINDING_RECOVERY_REPLAY_TIME_REPOSITORY_STATE_REVALIDATION_SUCCESS_RECEIPT_TERMINAL_REVALIDATION_SAFE_RECEIPT_BINDING_TERMINAL_REVALIDATION_FAILURE_RECEIPT_TERMINAL_EVIDENCE_BINDING_SUCCESS_RECEIPT_SEMANTIC_EVIDENCE_BINDING_SUCCESS_RECEIPT_BRAIN_ENTRY_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_CHECKPOINT_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_EVENT_BOUNDARY_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_RUNTIME_STATE_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_ORGAN_MODE_EVIDENCE_BINDING_SUCCESS_RECEIPT_WAKE_ORGAN_CONTRACT_EVIDENCE_BINDING_SUCCESS_RECEIPT_COMPLETION_CHECKPOINT_EVIDENCE_BINDING_SUCCESS_RECEIPT_EVENT_JOURNAL_EVIDENCE_BINDING_FAILURE_RECEIPT_ERROR_CODE_EVIDENCE_BINDING_AND_NO_CLONE_INCREMENTAL_REMOTE_OBJECT_CHANNEL_SOURCE_IMPLEMENTED_AND_TESTED",
|
||||
);
|
||||
assert.equal(channel.truth.hololake_integrated, 0);
|
||||
assert.equal(channel.truth.artifact_built, 0);
|
||||
|
|
|
|||
Loading…
Reference in a new issue