From 9673503e6a17b09c9d84de189fd3f9b02f873014 Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 07:16:05 +0800 Subject: [PATCH 01/19] feat(recovery): inspect full backups in isolated inert workspaces Signed-off-by: song --- .../dashboard/src/data/state-recovery.ts | 39 ++ .../configuration-backup-settings.tsx | 11 +- .../state-recovery-settings.css | 33 ++ .../state-recovery-settings.tsx | 122 ++++++ .../configuration-backup.mjs | 76 +++- loopx/cli_commands/support_control_backup.py | 26 ++ loopx/control_plane/effect_program.ts | 4 +- loopx/control_plane/effect_runtime.py | 2 + .../control_plane/effect_runtime_handlers.ts | 3 + loopx/control_plane/effect_runtime_server.ts | 2 + loopx/control_plane/runtime_decode.ts | 3 +- loopx/control_plane/state_recovery.ts | 286 ++++++++++++ .../control_plane/state_recovery_contract.ts | 81 ++++ loopx/control_plane/state_recovery_worker.ts | 20 + loopx/presentation/configuration_api.py | 8 + loopx/presentation/state_recovery_api.py | 152 +++++++ .../project_registry_io_manifest_v1.json | 2 +- loopx/semantics/vocabulary_v0.json | 31 +- loopx/state_recovery.py | 414 ++++++++++++++++++ tests/control_plane_ts/state_recovery.test.ts | 75 ++++ tests/test_state_recovery.py | 366 ++++++++++++++++ 21 files changed, 1742 insertions(+), 14 deletions(-) create mode 100644 apps/presentation/dashboard/src/data/state-recovery.ts create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.css create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx create mode 100644 loopx/control_plane/state_recovery.ts create mode 100644 loopx/control_plane/state_recovery_contract.ts create mode 100644 loopx/control_plane/state_recovery_worker.ts create mode 100644 loopx/presentation/state_recovery_api.py create mode 100644 loopx/state_recovery.py create mode 100644 tests/control_plane_ts/state_recovery.test.ts create mode 100644 tests/test_state_recovery.py diff --git a/apps/presentation/dashboard/src/data/state-recovery.ts b/apps/presentation/dashboard/src/data/state-recovery.ts new file mode 100644 index 0000000000..f19f05d44b --- /dev/null +++ b/apps/presentation/dashboard/src/data/state-recovery.ts @@ -0,0 +1,39 @@ +import {requestJson} from "./chat"; +import {decodeStateRecoveryAudit, type StateRecoveryAudit} from "../../../../../loopx/control_plane/state_recovery_contract"; + +export type StateRecoveryResult = StateRecoveryAudit & {checkpoint_ref: string; historical_readback?: boolean}; +export type RecoveryHistoryItem = { + checkpoint_ref: string; available: boolean; observed_at?: string; + summary?: StateRecoveryAudit["summary"]; +}; +const endpoint = "/api/chat/state-recovery"; + +export async function stateRecoveryProfile() { + const result = await requestJson<{supported: boolean; activation_available: false}>(`${endpoint}/profile`); + if (typeof result.supported !== "boolean" || result.activation_available !== false) throw new Error("Recovery profile is invalid"); + return result; +} + +export async function uploadRecoveryInput(file: File, kind: "archive" | "manifest") { + return requestJson<{input_ref: string; checkpoint_ref?: string}>(`${endpoint}/${kind}`, { + method: "POST", headers: {"Content-Type": "application/octet-stream"}, body: file, + }); +} +function result(value: unknown): StateRecoveryResult { + const audit = decodeStateRecoveryAudit(value); + if (typeof audit.checkpoint_ref !== "string" || !/^[a-f0-9]{32}$/.test(audit.checkpoint_ref)) { + throw new Error("Recovery checkpoint reference is invalid"); + } + return audit as StateRecoveryResult; +} +export async function inspectStateBackup(archiveRef: string, manifestRef: string) { + return result(await requestJson(`${endpoint}/verify`, {method: "POST", + body: JSON.stringify({archive_ref: archiveRef, manifest_ref: manifestRef, execute: true})})); +} +export async function readStateRecovery(checkpointRef: string) { + return result(await requestJson(`${endpoint}/read`, {method: "POST", + body: JSON.stringify({checkpoint_ref: checkpointRef})})); +} +export function listStateRecoveries() { + return requestJson<{items: RecoveryHistoryItem[]; has_more: boolean}>(endpoint); +} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/configuration-backup-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/configuration-backup-settings.tsx index 071078dce2..d5847f62ff 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/configuration-backup-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/configuration-backup-settings.tsx @@ -1,6 +1,7 @@ import {useEffect, useState} from "react"; import {exportConfigurationBackup, restoreConfigurationCheckpoint, type ConfigurationBackupResult} from "../../data/chat"; import {useWorkspaceI18n} from "./i18n"; +import {StateRecoverySettings} from "./state-recovery-settings"; export function ConfigurationBackupSettings({goalId}: {goalId: string | null}) { const {locale} = useWorkspaceI18n(); @@ -9,7 +10,8 @@ export function ConfigurationBackupSettings({goalId}: {goalId: string | null}) { const [result, setResult] = useState(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); - useEffect(() => {setBackup(null); setResult(null); setError("");}, [goalId]); + const [kind, setKind] = useState<"configuration" | "state">("configuration"); + useEffect(() => {setBackup(null); setResult(null); setError(""); setKind("configuration");}, [goalId]); async function perform(action: () => Promise) { setBusy(true); setError(""); try {await action();} catch (failure) {setError(String(failure));} @@ -17,6 +19,12 @@ export function ConfigurationBackupSettings({goalId}: {goalId: string | null}) { } return
{zh ? "配置备份与恢复" : "Configuration backup and recovery"} + {goalId === null ?
+ {zh ? "备份类型" : "Backup type"} + + +
: null} + {kind === "configuration" || goalId !== null ? <>

{zh ? "备份包含私有路径、Goal 设置及设备默认值,需自行保管和审查。凭据文件不包含在此组件中。恢复只建立隔离检查点;启用仍在原配置页面办理。" : "Keep and review this private backup: it includes Goal settings, paths and device defaults. Credential files are outside this component. Recovery creates an isolated checkpoint; activate settings through their existing editors."}

; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.css b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.css new file mode 100644 index 0000000000..e45cca2479 --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.css @@ -0,0 +1,33 @@ +.personal-recovery-kind { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin: 0.75rem 0; + padding: 0; + border: 0; +} +.personal-recovery-kind legend { margin-bottom: 0.5rem; } +.personal-recovery-kind label { display: inline-flex; align-items: center; gap: 0.375rem; min-height: 2.75rem; } +.personal-recovery { display: grid; gap: 0.75rem; min-width: 0; } +.personal-detail-card .personal-recovery p, +.personal-detail-card .personal-recovery label, +.personal-detail-card .personal-recovery button { font-size: var(--text-body); line-height: var(--leading-body); } +.personal-detail-card .personal-recovery p { margin: 0.375rem 0; color: var(--pw-text); } +.personal-recovery-files { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; } +.personal-recovery-files label { display: grid; gap: 0.5rem; min-width: 0; } +.personal-recovery-files input { width: 100%; max-width: 100%; min-width: 0; font: inherit; } +.personal-recovery-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; } +.personal-recovery-actions > button { width: auto; min-height: 2.75rem; padding: 0.5rem 0.75rem; } +.personal-recovery-result { display: grid; gap: 0.5rem; } +.personal-detail-card .personal-recovery-counts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin: 0.5rem 0; max-width: 28rem; } +.personal-detail-card .personal-recovery-counts > div { display: block; } +.personal-detail-card .personal-recovery-counts dt { color: var(--pw-muted); font-size: var(--text-caption); } +.personal-detail-card .personal-recovery-counts dd { margin: 0.25rem 0 0; font-size: var(--text-heading); font-weight: 600; } +.personal-recovery-components { list-style: none; margin: 0; padding: 0; } +.personal-recovery-components > li { padding: 0.75rem 0; border-top: 1px solid var(--pw-line); } +.personal-recovery code { overflow-wrap: anywhere; font-size: var(--text-caption); } +@media (max-width: 640px) { + .personal-recovery-files { grid-template-columns: minmax(0, 1fr); } + .personal-recovery-actions { display: grid; } + .personal-recovery-actions > button { width: 100%; } +} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx new file mode 100644 index 0000000000..677ece6b37 --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx @@ -0,0 +1,122 @@ +import {useEffect, useRef, useState} from "react"; +import {inspectStateBackup, listStateRecoveries, readStateRecovery, stateRecoveryProfile, uploadRecoveryInput, + type RecoveryHistoryItem, type StateRecoveryResult} from "../../data/state-recovery"; +import {useWorkspaceI18n} from "./i18n"; +import {ChatApiError} from "../../data/chat"; +import "./state-recovery-settings.css"; + +const ownerNames: Record = { + configuration: ["配置", "Configuration"], coordination_authority: ["Goal 工作与历史", "Goal work and history"], + sqlite_storage: ["数据库", "Database"], recovery: ["其他保存的状态", "Other saved state"], +}; +const nextActions: Record = { + retain_isolated_configuration: ["保留隔离配置,通过原设置页另行启用。", "Keep the isolated configuration; use its existing editor for any later activation."], + review_goal_identity_before_adoption: ["继续执行前,先由 Goal owner 核对身份与未完成工作。", "Before resuming, have the Goal owner review identity and unfinished work."], + inspect_captured_authority_with_owner: ["请存储 owner 检查保存的工作记录。", "Have the storage owner inspect the saved work records."], + use_provider_recovery_owner: ["通过该存储服务的恢复流程检查。", "Use this storage service's recovery procedure."], + retain_historical_database: ["保留数据库副本,暂不用于执行。", "Keep the database copy as historical data."], + inspect_database_with_owner: ["请数据库 owner 检查完整性和格式。", "Have the database owner check integrity and format."], + inspect_configuration_with_owner: ["请配置 owner 检查备份版本和内容。", "Have the configuration owner check the saved format and contents."], + classify_remaining_owner_state: ["启用前,需分别确认其他状态的归属、会话、租约和未决操作。", "Identify the remaining state owners, sessions, leases and pending operations before any activation."], +}; +const errors: Record = { + archive_digest_mismatch: ["归档与清单不匹配,请选择同一次备份的两个文件。", "The archive and manifest do not match. Choose the pair from the same backup."], + manifest_disagreement: ["内外清单不一致,请重新选择原始备份。", "The embedded and external manifests disagree. Choose the original backup pair."], + recovery_destination_exists: ["这份检查已有隔离副本,请读取历史结果。", "An isolated copy already exists for this inspection. Read its saved result."], + recovery_capacity_exceeded: ["备份超过当前检查容量,请使用较小的备份;未建立新副本。", "This backup exceeds the inspection limits. Use a smaller backup; no new copy was published."], + recovery_input_unavailable: ["上传文件已不可用,请重新选择归档与清单。", "The uploaded files are unavailable. Select the archive and manifest again."], + recovery_owner_timeout: ["离线检查超时,未发布副本;可使用较小的备份重试。", "Offline verification timed out without publishing a copy. Retry with a smaller backup."], + member_witness_mismatch: ["保存的文件与清单记录不符,已停止检查。", "Saved files differ from their manifest witnesses. Inspection stopped."], +}; + +export function StateRecoverySettings() { + const {locale} = useWorkspaceI18n(), zh = locale === "zh-CN", index = zh ? 0 : 1; + const [archive, setArchive] = useState(null); + const [manifest, setManifest] = useState(null); + const [audit, setAudit] = useState(null); + const [history, setHistory] = useState(null); + const [more, setMore] = useState(false); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + const [pending, setPending] = useState(null); + const [supported, setSupported] = useState(null); + const generation = useRef(0); + useEffect(() => { + let active = true; + void stateRecoveryProfile().then(profile => {if (active) setSupported(profile.supported);}) + .catch(() => {if (active) setSupported(false);}); + return () => {active = false;}; + }, []); + async function perform(operation: () => Promise) { + setBusy(true); setError(""); + try { await operation(); } catch (failure) { + const code = failure instanceof ChatApiError ? String(failure.payload.error_code ?? "") : ""; + setError(errors[code]?.[index] ?? (zh ? "检查未完成,请核对文件或稍后重试。" : "Inspection did not finish. Check the files or retry later.")); + } + finally { setBusy(false); } + } + function choose(file: File | null, kind: "archive" | "manifest") { + generation.current++; setAudit(null); setError(""); setPending(null); + if (kind === "archive") setArchive(file); else setManifest(file); + } + return
+ {supported === false ?

{zh ? "当前服务或平台不支持完整归档检查。配置快照仍可使用。" : "This service or platform does not support full archive inspection. Configuration snapshots remain available."}

: null} +

{zh ? "选择完整归档和配套清单,建立私有隔离副本并查看可读内容。此处不会恢复 Goal 运行;旧备份缺少一致性信息时会明确保留未知。" + : "Choose an archive and its matching manifest to inspect a private, isolated copy. This does not resume a Goal; missing consistency information stays unknown."}

+
+
+
+ + +
+ {error ?

{error}

: null} + {error && pending ? : null} + {audit ?
+ {audit.historical_readback + ? (zh ? "已读取历史检查记录;尚未重新验证副本。" : "Historical inspection loaded; the copy has not been reverified.") + : (zh ? "隔离副本已核对;Goal 尚未启用。" : "Isolated copy inspected; Goal execution remains inactive.")} +

{zh ? "检查时刻" : "Observed at"}:

+
+
{zh ? "可读组件" : "Readable components"}
{audit.summary.readable}
+
{zh ? "仅历史组件" : "Historical components"}
{audit.summary.historical_only}
+
{zh ? "未知组件" : "Unknown components"}
{audit.summary.unknown}
+
+

{zh ? "旧格式没有完整的捕获版本与一致性声明,以上结果只证明记录时的读取,不证明可以安全继续执行。" + : "This legacy archive has no complete capture-revision or consistency declaration. These observations do not authorize resuming work."}

+
    {audit.components.map(row =>
  • + {ownerNames[row.owner_kind]?.[index] ?? (zh ? "待核对组件" : "Component requiring review")} + {typeof row.facts.goal_id === "string" ? · {row.facts.goal_id} : null} + {" · "}{row.state === "readable" ? (zh ? "可读" : "Readable") : row.state === "historical_only" ? (zh ? "仅历史" : "Historical only") : (zh ? "未知" : "Unknown")} + {typeof row.facts.todo_count === "number" ?

    {zh ? "保存的任务" : "Recorded tasks"}: {row.facts.todo_count} · {zh ? "历史提交" : "Historical commits"}: {String(row.facts.history_commits)}

    : null} +

    {nextActions[row.next_action]?.[index] ?? (zh ? "请对应 owner 核对。" : "Ask the owning module to review this component.")}

    +
  • )}
+
{zh ? "核对信息" : "Verification details"}{audit.recovery_id} +
    {audit.components.map(row =>
  • {row.component_id}: {row.reason_code}
  • )}
+
+
: null} + {history ?
+ {history.length === 0 ?

{zh ? "尚无检查结果。" : "No saved inspection yet."}

: null} + {history.map(row =>

)} + {more ?

{zh ? "显示最近 10 份检查。" : "Showing the 10 latest inspections."}

: null} +
: null} +
; +} diff --git a/examples/personal-workspace-browser/configuration-backup.mjs b/examples/personal-workspace-browser/configuration-backup.mjs index 0c9bd8d366..f7a70ffa84 100644 --- a/examples/personal-workspace-browser/configuration-backup.mjs +++ b/examples/personal-workspace-browser/configuration-backup.mjs @@ -10,14 +10,27 @@ import {resolveTestPython} from "../../scripts/test-python.mjs"; // Only the backup routes use a real disposable server. Other workspace state // uses the shared browser fixture; no Host, model or live runtime is contacted. const serverCode = ` -import json,pathlib,sys,loopx +import json,pathlib,sys,loopx,subprocess from loopx.chat_server import ChatHTTPServer,ChatRequestHandler +from loopx.state_backup import build_state_backup_plan,execute_state_backup_plan +from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection r=pathlib.Path(sys.argv[1]); runtime=r/'runtime'; registry=r/'registry.json' registry.write_text(json.dumps({'common_runtime_root':str(runtime),'goals':[{'id':'fixture','repo':str(r),'control_plane':{'optional':{'context':'complete '*10000}}}]})) p=runtime/'machine/configuration.json';p.parent.mkdir(parents=True) p.write_text(json.dumps({'schema_version':'loopx_machine_configuration_v0','namespaces':{'goal_storage':{'schema_version':'loopx_goal_storage_defaults_v0','new_goal_provider':'sqlite'}}})) +source_project=r/'source-project';source_project.mkdir() +source_runtime=r/'source-runtime';source_registry=r/'source-registry.json' +(source_project/'ACTIVE.md').write_text('# Original Goal\\n\\nPreserve every unfinished commitment.\\n') +source_registry.write_text(json.dumps({'common_runtime_root':str(source_runtime),'goals':[{'id':'recovery-fixture','repo':str(source_project),'state_file':'ACTIVE.md','coordination':{'registered_agents':['worker']}}]})) +projection=build_todo_runtime_shadow_projection(goal_id='recovery-fixture',todos=[{'schema_version':'todo_item_v0','todo_id':'todo_recovery_work','role':'agent','status':'open','done':False,'archive_state':'active','source_section':'Agent Todo','text':'Preserve the original commitment','task_class':'advancement_task'}],handoff_mode='soft_claim') +provider=(pathlib.Path(loopx.__file__).parent/'control_plane/coordination/local_authority_provider.ts').as_uri() +script='import {selectLocalSqliteAuthority,openLocalAuthorityStore} from '+json.dumps(provider)+';let input="";for await(const c of process.stdin)input+=c;const r=JSON.parse(input);await selectLocalSqliteAuthority(r.root,r.goal,true);const s=await openLocalAuthorityStore(r.root,r.goal);const x=await s.commitAuthority({expected_provider_revision:null,operation_id:"original-work",events:[],receipts:[],next_projection:r.projection});if(x.status!=="applied")throw new Error(JSON.stringify(x));' +subprocess.run(['node','--no-warnings','--experimental-sqlite','--experimental-strip-types','--input-type=module','-e',script],input=json.dumps({'root':str(source_runtime),'goal':'recovery-fixture','projection':projection}),text=True,check=True,capture_output=True) +saved=execute_state_backup_plan(build_state_backup_plan(project=source_project,runtime_root=source_runtime,registry_path=source_registry,output_dir=r/'source-backup',backup_id='recovery-fixture',include_registry_projects=False,include_automations=False,include_skills=False)) +source_project.rename(r/'unavailable-project');source_runtime.rename(r/'unavailable-runtime');source_registry.rename(r/'unavailable-registry.json') +assert not source_project.exists() and not source_runtime.exists() and not source_registry.exists() s=ChatHTTPServer(('127.0.0.1',0),ChatRequestHandler);s.registry_path=registry;s.runtime_root=runtime;s.verbose=False -print(json.dumps({'port':s.server_address[1],'python':sys.executable,'module_path':loopx.__file__}),flush=True);s.serve_forever() +print(json.dumps({'port':s.server_address[1],'python':sys.executable,'module_path':loopx.__file__,'archive':saved['archive_path'],'manifest':saved['manifest_path'],'original_sources_absent':True}),flush=True);s.serve_forever() `; export const configurationBackupScenario = { @@ -70,6 +83,11 @@ export const configurationBackupScenario = { const response = await route.fetch({url: `http://127.0.0.1:${port}${path}`}); await route.fulfill({response}); }); + await page.route("**/api/chat/state-recovery**", async route => { + const path = new URL(route.request().url()).pathname; + const response = await route.fetch({url: `http://127.0.0.1:${port}${path}`}); + await route.fulfill({response}); + }); }}); const {page} = context; await page.getByRole("button", {name: "设置", exact: true}).click(); @@ -90,9 +108,61 @@ export const configurationBackupScenario = { await input.setInputFiles({name: "bad.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify({...backup, sha256: "changed"}))}); await panel.getByRole("alert").waitFor(); assert.equal(await panel.getByRole("button", {name: "恢复为隔离检查点"}).count(), 0); + await panel.getByRole("radio", {name: "完整归档检查", exact: true}).check(); + const recovery = panel.getByRole("region", {name: "完整备份检查"}); + await recovery.scrollIntoViewIfNeeded(); + await page.screenshot({path: resolve(outputDir, "state-recovery-ready.png"), animations: "disabled"}); + await recovery.getByLabel("完整归档", {exact: true}).setInputFiles(backend.archive); + await recovery.getByLabel("配套清单", {exact: true}).setInputFiles(backend.manifest); + await recovery.getByLabel("配套清单", {exact: true}).focus(); + await page.keyboard.press("Tab"); + assert.equal(await page.evaluate(() => document.activeElement.textContent), "验证并建立隔离副本"); + await page.keyboard.press("Enter"); + await recovery.getByText("隔离副本已核对;Goal 尚未启用。", {exact: true}).waitFor(); + await recovery.getByText("recovery-fixture", {exact: false}).first().waitFor(); + await recovery.getByText("保存的任务: 1", {exact: false}).waitFor(); + assert.equal(backend.original_sources_absent, true); + const registryAfter = JSON.parse(await readFile(join(root, "registry.json"), "utf8")); + assert.deepEqual(registryAfter.goals.map(goal => goal.id), ["fixture"], "inspection must not register the restored Goal"); + await recovery.getByRole("status").scrollIntoViewIfNeeded(); + await page.screenshot({path: resolve(outputDir, "state-recovery-audit.png"), animations: "disabled"}); + await recovery.getByLabel("完整归档", {exact: true}).setInputFiles({name: "damaged.tar.gz", mimeType: "application/gzip", buffer: Buffer.from("damaged")}); + await recovery.getByRole("button", {name: "验证并建立隔离副本", exact: true}).click(); + await recovery.getByRole("alert").waitFor(); + assert.equal(await recovery.getByText("保存的任务: 1", {exact: false}).count(), 0, "a changed input must clear the old success view"); + await recovery.getByRole("button", {name: "历史检查结果", exact: true}).click(); + const history = recovery.getByLabel("历史备份检查", {exact: true}); + await history.getByRole("button").click(); + await recovery.getByText("已读取历史检查记录;尚未重新验证副本。", {exact: true}).waitFor(); await page.setViewportSize({width: 390, height: 844}); assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth), false); - return {coverageEntries: context.coverageEntries, errors: context.errors, backend, verified: "Packaged download, full real-backend checkpoint recovery, damaged-file rejection and narrow viewport"}; + await recovery.getByRole("status").scrollIntoViewIfNeeded(); + await page.screenshot({path: resolve(outputDir, "state-recovery-mobile.png"), animations: "disabled"}); + await recovery.getByRole("button", {name: "历史检查结果", exact: true}).click(); + await recovery.getByLabel("历史备份检查", {exact: true}).getByRole("button").click(); + await page.setViewportSize({width: 1512, height: 982}); + await page.reload(); + await page.getByRole("button", {name: "设置", exact: true}).or(page.locator(".personal-settings-tabs")).first().waitFor(); + if (await page.getByRole("button", {name: "设置", exact: true}).count()) await page.getByRole("button", {name: "设置", exact: true}).click(); + await page.locator(".personal-settings-tabs").getByRole("button", {name: "能力中心", exact: true}).click(); + const reopened = page.locator("details").filter({has: page.getByText("配置备份与恢复", {exact: true})}); + await reopened.locator("summary").click(); + await reopened.getByRole("radio", {name: "完整归档检查", exact: true}).check(); + await reopened.getByRole("button", {name: "历史检查结果", exact: true}).click(); + await reopened.getByLabel("历史备份检查", {exact: true}).getByRole("button").click(); + await reopened.getByText("已读取历史检查记录;尚未重新验证副本。", {exact: true}).waitFor(); + await page.getByRole("button", {name: "语言", exact: true}).click(); + await page.getByRole("radio", {name: "English", exact: true}).check(); + await page.locator(".personal-settings-tabs").getByRole("button", {name: /Capabilit/}).click(); + const english = page.locator("details").filter({has: page.getByText("Configuration backup and recovery", {exact: true})}); + await english.locator("summary").click(); + await english.getByRole("radio", {name: "Full archive inspection", exact: true}).check(); + await english.getByRole("button", {name: "Previous inspections", exact: true}).click(); + await english.getByLabel("Previous backup inspections", {exact: true}).getByRole("button").click(); + await english.getByText("Historical inspection loaded; the copy has not been reverified.", {exact: true}).waitFor(); + await english.getByRole("status").scrollIntoViewIfNeeded(); + await page.screenshot({path: resolve(outputDir, "state-recovery-english.png"), animations: "disabled"}); + return {coverageEntries: context.coverageEntries, errors: context.errors, backend, verified: "Packaged configuration recovery and full archive inspection; real SQLite head/history after original-source loss, no Goal registration, keyboard, corrupt-input recovery, immutable history reload and narrow viewport"}; } finally { try {await context?.close();} finally { if (!closed) { diff --git a/loopx/cli_commands/support_control_backup.py b/loopx/cli_commands/support_control_backup.py index f584e18456..f3bcbed42f 100644 --- a/loopx/cli_commands/support_control_backup.py +++ b/loopx/cli_commands/support_control_backup.py @@ -30,6 +30,11 @@ def register_backup_state_command( help="Preview or create a private local archive of LoopX state.", ) add_subcommand_format(parser) + parser.add_argument("backup_action", nargs="?", choices=["verify"], + help="Verify a full archive and inspect an inert copy; never reactivate it.") + parser.add_argument("--archive", help="For verify, the private full-state tar.gz archive.") + parser.add_argument("--manifest", help="For verify, the matching private external manifest.") + parser.add_argument("--destination", help="For verify, a new inert directory below a physical parent.") parser.add_argument( "--project", default=".", @@ -76,6 +81,27 @@ def handle_backup_state_command( print_payload: PrintPayload, output_format: FormatSelector, ) -> int: + if getattr(args, "backup_action", None) == "verify": + from ..state_recovery import StateRecoveryError, render_state_recovery_markdown, verify_state_backup + + try: + if not all(getattr(args, key, None) for key in ("archive", "manifest", "destination")): + raise StateRecoveryError("recovery_inputs_required") + if args.output_dir or args.backup_id or args.no_automations or args.no_skills or args.current_project_only or args.project != ".": + raise StateRecoveryError("recovery_creation_options_invalid") + payload = verify_state_backup(archive=Path(args.archive).expanduser(), + manifest=Path(args.manifest).expanduser(), destination=Path(args.destination).expanduser().absolute(), + execute=bool(args.execute)) + except StateRecoveryError as exc: + payload = {"ok": False, "error_code": exc.code, "execution_authority_granted": False} + except OSError: + payload = {"ok": False, "error_code": "recovery_io_unavailable", "execution_authority_granted": False} + print_payload(payload, output_format(args), render_state_recovery_markdown) + return 0 if payload.get("ok") else 1 + if any(getattr(args, key, None) for key in ("archive", "manifest", "destination")): + payload = {"ok": False, "error": "archive, manifest and destination require backup-state verify"} + print_payload(payload, output_format(args), render_state_backup_markdown) + return 1 try: runtime_root = ( Path(args.runtime_root).expanduser() if args.runtime_root else None diff --git a/loopx/control_plane/effect_program.ts b/loopx/control_plane/effect_program.ts index 1c5cfe4402..13864e0dcc 100644 --- a/loopx/control_plane/effect_program.ts +++ b/loopx/control_plane/effect_program.ts @@ -1,5 +1,5 @@ import { EffectRuntimeRequestError } from "./effect_runtime_errors.ts"; -import { isStringLiteral } from "./runtime_decode.ts"; +import { isStringLiteral, type JsonObject } from "./runtime_decode.ts"; import { decodeInteractionContract } from "./work_items/interaction_contract.ts"; export { @@ -25,7 +25,7 @@ export const SETTLEMENT_PLAN_SCHEMA_VERSION = "quota_settlement_plan_v1"; export const SETTLEMENT_RECEIPT_SCHEMA_VERSION = "quota_settlement_receipt_v1"; -export type JsonObject = Record; +export type {JsonObject} from "./runtime_decode.ts"; export interface EffectRequest { kind: string; diff --git a/loopx/control_plane/effect_runtime.py b/loopx/control_plane/effect_runtime.py index 52a4068f12..e7f01fcfd5 100644 --- a/loopx/control_plane/effect_runtime.py +++ b/loopx/control_plane/effect_runtime.py @@ -48,6 +48,8 @@ "goal.checkpoint_read_context.commit", "goal.checkpoint_read_context.inspect_replay", "performance_diagnosis.inspect", + "state.recovery.archive.verify", + "state.recovery.manifest.verify", }) MAX_STARTUP_DIAGNOSTIC_BYTES = 8 * 1024 STARTUP_LOCK_TIMEOUT_SECONDS = 15.0 diff --git a/loopx/control_plane/effect_runtime_handlers.ts b/loopx/control_plane/effect_runtime_handlers.ts index 9558769848..ac9b4a6552 100644 --- a/loopx/control_plane/effect_runtime_handlers.ts +++ b/loopx/control_plane/effect_runtime_handlers.ts @@ -407,6 +407,9 @@ export function createEffectRuntimeHandlers( ["coordination.local_authority.promote", lazyHandler(() => import("./coordination/local_authority_runtime.ts"), ({promoteLocalCoordinationAuthority}) => promoteLocalCoordinationAuthority)], ["coordination.authority_archive.manage", lazyHandler(() => import("./coordination/local_authority_archive.ts"), ({manageLocalAuthorityArchive}) => manageLocalAuthorityArchive)], ["configuration.backup", lazyHandler(() => import("./configuration_backup.ts"), ({configurationBackupOperation}) => configurationBackupOperation)], + ["state.recovery.archive.verify", lazyHandler(() => import("./state_recovery.ts"), ({verifyStateRecoveryArchive}) => verifyStateRecoveryArchive)], + ["state.recovery.manifest.verify", lazyHandler(() => import("./state_recovery.ts"), ({verifyStateRecoveryManifest}) => verifyStateRecoveryManifest)], + ["state.recovery.audit.readback", lazyHandler(() => import("./state_recovery.ts"), ({verifySavedStateRecoveryAudit}) => verifySavedStateRecoveryAudit)], ["coordination.sqlite_backup.snapshot", lazyHandler(() => import("./coordination/sqlite_backup.ts"), ({snapshotSqliteBackup}) => snapshotSqliteBackup)], ["coordination.local_authority.new_goal_storage", lazyHandler(() => import("./coordination/local_authority_defaults.ts"), ({manageNewGoalStorage}) => manageNewGoalStorage)], ["coordination.local_authority.promotion_review", lazyHandler(() => Promise.all([import("./coordination/local_authority_runtime.ts"), import("./coordination/source_transfer.ts")]), ([{reviewLocalCoordinationAuthorityPromotion}, {withCoordinationSourceTransfer}]) => withCoordinationSourceTransfer("coordination.local_authority.promotion_review", reviewLocalCoordinationAuthorityPromotion))], diff --git a/loopx/control_plane/effect_runtime_server.ts b/loopx/control_plane/effect_runtime_server.ts index 39139eb382..8110bc3917 100644 --- a/loopx/control_plane/effect_runtime_server.ts +++ b/loopx/control_plane/effect_runtime_server.ts @@ -35,6 +35,8 @@ const LOCAL_SNAPSHOT_METHODS = new Set([ "goal.checkpoint_read_context.commit", "goal.checkpoint_read_context.inspect_replay", "performance_diagnosis.inspect", + "state.recovery.archive.verify", + "state.recovery.manifest.verify", ]); const DEFAULT_IDLE_MS = 5 * 60 * 1_000; // Bounds for LOOPX_EFFECT_RUNTIME_IDLE_MS. The upper bound is the largest diff --git a/loopx/control_plane/runtime_decode.ts b/loopx/control_plane/runtime_decode.ts index 8245e18c62..975b7766aa 100644 --- a/loopx/control_plane/runtime_decode.ts +++ b/loopx/control_plane/runtime_decode.ts @@ -1,6 +1,7 @@ -import type { JsonObject } from "./effect_program.ts"; import { EffectRuntimeRequestError } from "./effect_runtime_errors.ts"; +export type JsonObject = Record; + export function jsonObject(value: unknown): JsonObject | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as JsonObject) diff --git a/loopx/control_plane/state_recovery.ts b/loopx/control_plane/state_recovery.ts new file mode 100644 index 0000000000..468ce4351f --- /dev/null +++ b/loopx/control_plane/state_recovery.ts @@ -0,0 +1,286 @@ +/** Offline backup inspection. Existing owners decide readability, never activation. */ +import {createHash} from "node:crypto"; +import {lstat, readFile} from "node:fs/promises"; +import {join, relative} from "node:path"; +import type {JsonObject} from "./effect_program.ts"; +import {requireJsonObject} from "./runtime_decode.ts"; +import {canonicalAuthoritySha256, AuthorityStoreProtocolError} from "./coordination/authority_store_codec.ts"; +import {EffectRuntimeRequestError} from "./effect_runtime_errors.ts"; +import {verifyConfigurationBackup} from "./configuration_backup.ts"; +import {decodeLocalAuthoritySelection, localAuthorityProviderPaths, + openLocalAuthorityStoreHandle} from "./coordination/local_authority_provider.ts"; +import {sqliteAuthorityRuntime} from "./coordination/sqlite_runtime.ts"; +import {sqliteAuthorityPath} from "./coordination/sqlite_authority_store.ts"; +import {canonicalTodoCollection} from "./coordination/local_authority_read.ts"; +import {STATE_RECOVERY_AUDIT_SCHEMA, decodeStateRecoveryAudit, isRecoveryDigest, recoveryGoalLabel, + type RecoveryComponent, type RecoveryComponentState} from "./state_recovery_contract.ts"; + +const MAX_GOALS = 128; +const MAX_SQLITE_FILES = 128; +const MAX_COMMITS = 20_000; +const hash = (value: string) => createHash("sha256").update(value).digest("hex"); +const digest = isRecoveryDigest; +class RecoveryInputError extends Error { + readonly code: string; + constructor(code: string) { super(code); this.code = code; } +} +function check(value: unknown, code: string): asserts value { + if (!value) throw new RecoveryInputError(code); +} +function reject(code: string): JsonObject { + return {ok: false, error_code: code, execution_authority_granted: false}; +} +function manifestCore(value: JsonObject): JsonObject { + const execution = {...requireJsonObject(value.execution, "backup execution")}; + execution.archive_sha256 = null; + execution.archive_size_bytes = null; + delete execution.archive_to_logical_ratio; + return {...value, execution}; +} + +function externalEvidence(request: JsonObject): {external: JsonObject; execution: JsonObject} { + const external = requireJsonObject(request.external_manifest, "external backup manifest"); + check(external.schema_version === "loopx_state_backup_v0", "backup_schema_unsupported"); + check(external.dry_run === false && external.execute_requested === true && external.ok === true, + "backup_not_committed"); + check(typeof external.backup_id === "string" && external.backup_id.length > 0, "backup_identity_missing"); + const execution = requireJsonObject(external.execution, "backup execution"); + check(digest(request.archive_sha256) && execution.archive_sha256 === request.archive_sha256, + "archive_digest_mismatch"); + check(Number.isSafeInteger(request.archive_size_bytes) && execution.archive_size_bytes === request.archive_size_bytes, + "archive_size_mismatch"); + return {external, execution}; +} +function inputFailure(error: unknown): JsonObject { + if (error instanceof RecoveryInputError) return reject(error.code); + if (error instanceof EffectRuntimeRequestError || error instanceof AuthorityStoreProtocolError) { + return reject("backup_manifest_invalid"); + } + throw error; +} + +/** Reject a mismatched archive before its untrusted tar structure is decoded. */ +export function verifyStateRecoveryArchive(value: unknown): JsonObject { + try { + externalEvidence(requireJsonObject(value, "recovery archive verification")); + return {ok: true, archive_digest_verified: true, execution_authority_granted: false}; + } catch (error) { return inputFailure(error); } +} + +/** Host IO supplies observed bytes; input manifests never supply observed facts. */ +export function verifyStateRecoveryManifest(value: unknown): JsonObject { + try { + const request = requireJsonObject(value, "recovery manifest verification"); + const {external, execution} = externalEvidence(request); + const embedded = requireJsonObject(request.embedded_manifest, "embedded backup manifest"); + check(embedded.schema_version === external.schema_version, + "backup_schema_unsupported"); + check(canonicalAuthoritySha256(manifestCore(external)) === canonicalAuthoritySha256(manifestCore(embedded)), + "manifest_disagreement"); + check(Array.isArray(request.files), "member_inventory_invalid"); + check(Number.isSafeInteger(request.sqlite_member_count) && Number(request.sqlite_member_count) >= 0 + && Number(request.sqlite_member_count) <= MAX_SQLITE_FILES, "sqlite_inventory_capacity"); + const observed = new Map(); + for (const item of request.files) { + const file = requireJsonObject(item, "observed member"); + check(typeof file.archive_path === "string" && !observed.has(file.archive_path) + && digest(file.sha256) && Number.isSafeInteger(file.size_bytes) && Number(file.size_bytes) >= 0, + "member_inventory_invalid"); + observed.set(file.archive_path, file); + } + const witnesses = execution.file_members; + if (witnesses !== undefined) { + check(Array.isArray(witnesses) && witnesses.length === observed.size, "member_witness_mismatch"); + const seen = new Set(); + for (const item of witnesses) { + const witness = requireJsonObject(item, "member witness"); + const path = String(witness.archive_path); + check(!seen.has(path), "member_witness_mismatch"); + seen.add(path); + const file = observed.get(path); + check(file && file.sha256 === witness.sha256 && file.size_bytes === witness.size_bytes, + "member_witness_mismatch"); + } + } + check(digest(request.external_manifest_sha256), "manifest_digest_invalid"); + return {ok: true, schema_version: STATE_RECOVERY_AUDIT_SCHEMA, + recovery_id: hash(`${request.archive_sha256}:${request.external_manifest_sha256}`), + backup: {schema_version: external.schema_version, archive_sha256: request.archive_sha256, + external_manifest_sha256: request.external_manifest_sha256}, + file_count: observed.size, file_witnesses_verified: witnesses !== undefined, + manifest_verified: true, capture_profile: "legacy_best_effort", execution_authority_granted: false}; + } catch (error) { + return inputFailure(error); + } +} + +function component(id: string, owner: string, state: RecoveryComponentState, reason: string, + next: string, facts: JsonObject = {}, dependencies: string[] = []): RecoveryComponent { + return {component_id: id, owner_kind: owner, state, reason_code: reason, + next_action: next, facts, dependency_ids: dependencies}; +} +async function regular(path: string): Promise { + const stat = await lstat(path); + check(stat.isFile() && !stat.isSymbolicLink(), "owner_source_not_regular"); +} + +async function inspectGoal(root: string, goalId: string, standaloneSqlite: Set): Promise { + const id = `goal:${hash(goalId).slice(0, 20)}`; + const paths = localAuthorityProviderPaths(root, goalId); + const goalFacts = recoveryGoalLabel(goalId) === null ? {} : {goal_id: goalId}; + try { + let selection: ReturnType | null = null; + try { + await regular(paths.marker); + selection = decodeLocalAuthoritySelection(JSON.parse(await readFile(paths.marker, "utf8")), goalId); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + if (selection?.provider === "postgresql") return component(id, "coordination_authority", "unknown", + "offline_provider_unsupported", "use_provider_recovery_owner", goalFacts, ["configuration"]); + if (selection?.provider === "sqlite") check(standaloneSqlite.has(sqliteAuthorityPath(paths.sqlite, goalId)), + "sqlite_requires_owner_recovery"); + // The existing owner retains legacy File reads and rejects missing selected + // storage or a lost SQLite selector. existingOnly forbids initializing either. + const handle = await openLocalAuthorityStoreHandle(root, goalId, {}, {existingOnly: true}); + const loaded = await handle.store.loadAuthority(); + check(loaded.status === "loaded", "authority_head_unavailable"); + // Byte-valid storage alone is not a readable Goal work model. Reuse the + // same full-record, lease and acceptance decoder as canonical Todo reads. + const collection = canonicalTodoCollection(loaded.head, goalId, true); + let cursor: string | null = null; + let count = 0; + let final: JsonObject | null = null; + let finalRevision: string | null = null; + while (true) { + const page = await handle.store.scanCommitted(cursor, 200); + check(page.status === "page", "authority_history_unavailable"); + count += page.transactions.length; + check(count <= MAX_COMMITS, "authority_history_capacity"); + for (const row of page.transactions) { final = row.projection; finalRevision = row.provider_revision; } + if (!page.has_more) break; + check(page.next_cursor !== null && page.next_cursor !== cursor && page.transactions.length > 0, + "authority_history_cursor_invalid"); + cursor = page.next_cursor; + } + check(String(count) === loaded.cursor && final !== null && finalRevision === loaded.provider_revision + && canonicalAuthoritySha256(final) === canonicalAuthoritySha256(loaded.head), "authority_history_head_mismatch"); + const readback = await handle.store.loadAuthority(); + check(readback.status === "loaded" && readback.provider_revision === loaded.provider_revision, + "authority_changed_during_audit"); + return component(id, "coordination_authority", "readable", "historical_head_and_history_readable", + "review_goal_identity_before_adoption", {...goalFacts, provider: handle.provider, + head_digest: canonicalAuthoritySha256(loaded.head), history_commits: count, + todo_count: collection.projection.todo_ids.length, + historical_lease_count: collection.leaseIndex?.lease_todo_ids.length ?? 0}, ["configuration"]); + } catch (error) { + return component(id, "coordination_authority", "unknown", + error instanceof RecoveryInputError ? error.code : "authority_source_unavailable", + "inspect_captured_authority_with_owner", goalFacts, ["configuration"]); + } +} + +/** Reads only a host-validated inert directory; never registers or repairs a store. */ +async function auditOwners(value: unknown): Promise { + const request = requireJsonObject(value, "recovery owner audit"); + const basis = requireJsonObject(request.verified, "verified recovery basis"); + check(basis.ok === true && basis.manifest_verified === true && basis.schema_version === STATE_RECOVERY_AUDIT_SCHEMA, + "recovery_basis_invalid"); + check(typeof request.data_root === "string" && Array.isArray(request.sqlite_members), "recovery_owner_request_invalid"); + const dataRoot = request.data_root; + check(request.sqlite_members.length <= MAX_SQLITE_FILES, "sqlite_inventory_capacity"); + const components: RecoveryComponent[] = []; + let configuration: JsonObject | null = null; + try { + const path = join(dataRoot, "configuration-backup.json"); + await regular(path); + const candidate = requireJsonObject(JSON.parse(await readFile(path, "utf8")), "captured configuration"); + const verified = verifyConfigurationBackup(candidate); + configuration = candidate; + components.push(component("configuration", "configuration", "readable", "configuration_digest_verified", + "retain_isolated_configuration", {goal_count: verified.goal_count})); + } catch { + components.push(component("configuration", "configuration", "unknown", "configuration_unavailable", + "inspect_configuration_with_owner")); + } + check(Array.isArray(request.standalone_sqlite_members), "recovery_owner_request_invalid"); + const standaloneSqlite = new Set(request.standalone_sqlite_members.map(raw => join(dataRoot, String(raw)))); + const ownerReadableSqlite = new Set(); + for (const raw of request.sqlite_members) { + const path = String(raw); + // The IO adapter has already verified all names and refused symlink ancestors. + check(!path.startsWith("/") && !path.split("/").includes(".."), "sqlite_member_invalid"); + const id = `sqlite:${hash(path).slice(0, 20)}`; + try { + const fullPath = join(dataRoot, path); + check(relative(dataRoot, fullPath) === path && standaloneSqlite.has(fullPath), "sqlite_requires_owner_recovery"); + await regular(fullPath); + const {driver} = sqliteAuthorityRuntime(); + const limits = new driver.DatabaseSync(":memory:"); + try { + // SQLite's heap limit is process-wide; this module is invoked only in + // the disposable verifier, never in the shared Effect daemon. + limits.exec("PRAGMA hard_heap_limit=67108864"); + const actual = Number(limits.prepare("PRAGMA hard_heap_limit").get()?.hard_heap_limit); + check(actual > 0 && actual <= 64 * 1024 * 1024, "sqlite_memory_limit_unavailable"); + } finally { limits.close(); } + const database = new driver.DatabaseSync(fullPath, {readOnly: true}); + try { + database.exec("PRAGMA query_only=ON; PRAGMA trusted_schema=OFF"); + const tables = database.prepare("PRAGMA table_list").all(); + const physicalTablesOnly = tables.every(row => row.type === "table"); + check(physicalTablesOnly, "sqlite_schema_requires_owner_recovery"); + const rows = database.prepare("PRAGMA integrity_check").all(); + check(rows.length === 1 && rows[0]?.integrity_check === "ok", "sqlite_integrity_failed"); + // Live store readers assume their own physical tables. Never evaluate + // a restored view or virtual table as an authority table. + ownerReadableSqlite.add(fullPath); + } finally { database.close(); } + components.push(component(id, "sqlite_storage", "historical_only", "sqlite_bytes_verified", + "retain_historical_database")); + } catch (error) { + components.push(component(id, "sqlite_storage", "unknown", + error instanceof RecoveryInputError ? error.code : "sqlite_verification_failed", + "inspect_database_with_owner")); + } + } + if (configuration !== null) { + const goals = (configuration.data as JsonObject).goals as JsonObject[]; + check(goals.length <= MAX_GOALS, "goal_inventory_capacity"); + for (const goal of goals) components.push(await inspectGoal(join(dataRoot, "runtime-root"), String(goal.goal_id), ownerReadableSqlite)); + } + // A v0 file inventory does not identify every session/effect/extension owner. + // Retained bytes must not masquerade as a successful owner-local read. + components.push(component("remaining_state", "recovery", "unknown", "legacy_owner_inventory_incomplete", + "classify_remaining_owner_state")); + const audit = {...basis, status: "held", written: true, observed_at: new Date().toISOString(), + workspace: {isolation: "inert", registered: false}, components, + findings: [{reason_code: "legacy_manifest_incomplete", owner_kind: "backup_capture", + next_action: "review_capture_consistency"}], + summary: {readable: components.filter(row => row.state === "readable").length, + historical_only: components.filter(row => row.state === "historical_only").length, + unknown: components.filter(row => row.state === "unknown").length}, + activation_eligible: false, execution_authority_granted: false}; + return decodeStateRecoveryAudit({...audit, audit_sha256: canonicalAuthoritySha256(audit)}); +} + +export async function auditStateRecoveryOwners(value: unknown): Promise { + try { return await auditOwners(value); } + catch (error) { return inputFailure(error); } +} + +export function verifySavedStateRecoveryAudit(value: unknown): JsonObject { + try { + const audit = decodeStateRecoveryAudit(value); + const {audit_sha256, ...body} = audit; + check(canonicalAuthoritySha256(body) === audit_sha256, "recovery_audit_digest_mismatch"); + const backup = requireJsonObject(audit.backup, "recovery backup basis"); + check(audit.recovery_id === hash(`${backup.archive_sha256}:${backup.external_manifest_sha256}`), + "recovery_audit_identity_mismatch"); + return audit; + } catch (error) { + if (error instanceof RecoveryInputError) return reject(error.code); + if (error instanceof EffectRuntimeRequestError || error instanceof AuthorityStoreProtocolError) return reject("recovery_audit_invalid"); + throw error; + } +} diff --git a/loopx/control_plane/state_recovery_contract.ts b/loopx/control_plane/state_recovery_contract.ts new file mode 100644 index 0000000000..776c2c47c2 --- /dev/null +++ b/loopx/control_plane/state_recovery_contract.ts @@ -0,0 +1,81 @@ +/** Shared readback contract for the offline owner and packaged client. */ +import {EffectRuntimeRequestError} from "./effect_runtime_errors.ts"; +import {requireJsonObject, type JsonObject} from "./runtime_decode.ts"; + +export const STATE_RECOVERY_AUDIT_SCHEMA = "loopx_state_recovery_audit_v0"; +export const RECOVERY_COMPONENT_STATES = ["readable", "historical_only", "unknown"] as const; +export type RecoveryComponentState = typeof RECOVERY_COMPONENT_STATES[number]; +export interface RecoveryComponent extends JsonObject { + component_id: string; + owner_kind: string; + state: RecoveryComponentState; + reason_code: string; + next_action: string; + facts: JsonObject; + dependency_ids: string[]; +} +export interface StateRecoveryAudit extends JsonObject { + schema_version: typeof STATE_RECOVERY_AUDIT_SCHEMA; + recovery_id: string; + audit_sha256: string; + observed_at: string; + status: "held"; + execution_authority_granted: false; + activation_eligible: false; + components: RecoveryComponent[]; + findings: Array<{reason_code: string; owner_kind: string; next_action: string}>; + summary: Record; +} +function requireValue(condition: unknown): asserts condition { + if (!condition) throw new EffectRuntimeRequestError("State recovery audit contract is invalid", "recovery_audit_invalid"); +} +function token(value: unknown): boolean { + return typeof value === "string" && /^[a-z0-9_:.~-]{1,128}$/.test(value); +} +export function isRecoveryDigest(value: unknown): value is string { + return typeof value === "string" && /^[a-f0-9]{64}$/.test(value); +} +/** Display only; unsafe labels retain their opaque component identity. */ +export function recoveryGoalLabel(value: string): string | null { + return /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value) ? value : null; +} +export function decodeStateRecoveryAudit(value: unknown): StateRecoveryAudit { + const audit = requireJsonObject(value, "state recovery audit"); + requireValue(audit.ok === true && audit.schema_version === STATE_RECOVERY_AUDIT_SCHEMA + && audit.status === "held" && audit.written === true && audit.manifest_verified === true + && audit.execution_authority_granted === false && audit.activation_eligible === false + && isRecoveryDigest(audit.recovery_id) && isRecoveryDigest(audit.audit_sha256)); + requireValue(typeof audit.observed_at === "string" && audit.observed_at.length <= 40 + && Number.isFinite(Date.parse(audit.observed_at))); + const workspace = requireJsonObject(audit.workspace, "inert recovery workspace"); + requireValue(workspace.isolation === "inert" && workspace.registered === false); + const backup = requireJsonObject(audit.backup, "recovery backup basis"); + requireValue(backup.schema_version === "loopx_state_backup_v0" + && isRecoveryDigest(backup.archive_sha256) && isRecoveryDigest(backup.external_manifest_sha256)); + requireValue(Array.isArray(audit.components) && audit.components.length <= 512); + const ids = new Set(); + const counts = {readable: 0, historical_only: 0, unknown: 0}; + for (const value of audit.components) { + const row = requireJsonObject(value, "recovery component"); + requireValue(typeof row.component_id === "string" && token(row.component_id) && !ids.has(row.component_id) + && token(row.owner_kind) && token(row.reason_code) && token(row.next_action) + && RECOVERY_COMPONENT_STATES.includes(row.state as RecoveryComponentState) + && Array.isArray(row.dependency_ids) && row.dependency_ids.every(token)); + const facts = requireJsonObject(row.facts, "recovery component facts"); + requireValue(facts.goal_id === undefined || (typeof facts.goal_id === "string" && recoveryGoalLabel(facts.goal_id) === facts.goal_id)); + ids.add(row.component_id); + counts[row.state as RecoveryComponentState]++; + } + for (const value of audit.components) { + const row = value as RecoveryComponent; + requireValue(row.dependency_ids.every(id => ids.has(id) && id !== row.component_id)); + } + const summary = requireJsonObject(audit.summary, "recovery summary"); + requireValue(RECOVERY_COMPONENT_STATES.every(state => summary[state] === counts[state])); + requireValue(Array.isArray(audit.findings)); + for (const value of audit.findings) { + const finding = requireJsonObject(value, "recovery finding"); + requireValue(token(finding.reason_code) && token(finding.owner_kind) && token(finding.next_action)); + } + return audit as StateRecoveryAudit; +} diff --git a/loopx/control_plane/state_recovery_worker.ts b/loopx/control_plane/state_recovery_worker.ts new file mode 100644 index 0000000000..be0aaf590d --- /dev/null +++ b/loopx/control_plane/state_recovery_worker.ts @@ -0,0 +1,20 @@ +/** Disposable offline verifier: untrusted database work cannot stall the daemon. */ +import {auditStateRecoveryOwners} from "./state_recovery.ts"; + +try { + let size = 0; + const chunks: Buffer[] = []; + for await (const chunk of process.stdin) { + const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + size += data.length; + if (size > 2 * 1024 * 1024) throw new Error("input capacity"); + chunks.push(data); + } + const result = await auditStateRecoveryOwners(JSON.parse(Buffer.concat(chunks).toString("utf8"))); + process.stdout.write(JSON.stringify(result)); + process.exitCode = result.ok === true ? 0 : 1; +} catch { + process.stdout.write(JSON.stringify({ok: false, error_code: "recovery_owner_unavailable", + execution_authority_granted: false})); + process.exitCode = 1; +} diff --git a/loopx/presentation/configuration_api.py b/loopx/presentation/configuration_api.py index bfc3110351..be03234550 100644 --- a/loopx/presentation/configuration_api.py +++ b/loopx/presentation/configuration_api.py @@ -3,6 +3,7 @@ from collections.abc import Callable from . import configuration_backup_api as backup_api +from . import state_recovery_api as recovery_api from . import goal_ownership_api as ownership_api from . import goal_storage_api as storage_api from .. import chat_usage_statistics_api as usage_api @@ -21,11 +22,14 @@ class ChatConfigurationRequestMixin( machine_api.MachineConfigurationRequestMixin, operator_api.OperatorProviderRequestMixin, backup_api.ConfigurationBackupRequestMixin, + recovery_api.StateRecoveryRequestMixin, ): """Expose machine and Goal configuration through one route registry.""" def _configuration_get_routes(self) -> dict[str, Callable[[], None]]: return { + recovery_api.STATE_RECOVERY_PATH: self._state_recovery_list, + f"{recovery_api.STATE_RECOVERY_PATH}/profile": self._state_recovery_profile, storage_api.CHAT_GOAL_STORAGE_PATH: self._storage_inspect, ownership_api.CHAT_GOAL_OWNERSHIP_PATH: self._ownership_inspect, cadence_api.CHAT_AUTOMATION_CADENCE_PATH: self._cadence_read, @@ -37,6 +41,10 @@ def _configuration_get_routes(self) -> dict[str, Callable[[], None]]: def _configuration_post_routes(self) -> dict[str, Callable[[], None]]: return { + f"{recovery_api.STATE_RECOVERY_PATH}/archive": lambda: self._state_recovery_upload("archive"), + f"{recovery_api.STATE_RECOVERY_PATH}/manifest": lambda: self._state_recovery_upload("manifest"), + f"{recovery_api.STATE_RECOVERY_PATH}/verify": self._state_recovery_verify, + f"{recovery_api.STATE_RECOVERY_PATH}/read": self._state_recovery_read, f"{storage_api.CHAT_GOAL_STORAGE_PATH}/preview": lambda: self._storage_update(action="plan-migration"), f"{storage_api.CHAT_GOAL_STORAGE_PATH}/apply": lambda: self._storage_update(action="migrate"), f"{storage_api.CHAT_GOAL_STORAGE_PATH}/recover": lambda: self._storage_update(action="migration-readback"), diff --git a/loopx/presentation/state_recovery_api.py b/loopx/presentation/state_recovery_api.py new file mode 100644 index 0000000000..9f596399d9 --- /dev/null +++ b/loopx/presentation/state_recovery_api.py @@ -0,0 +1,152 @@ +"""Loopback-only backup upload and immutable inert-audit readback.""" +from __future__ import annotations + +import hashlib +import os +from pathlib import Path +import re +import shutil +import tempfile +import time +from uuid import uuid4 + +from ..state_recovery import ( + MAX_ARCHIVE_BYTES, MAX_MANIFEST_BYTES, StateRecoveryError, + read_state_recovery_audit, verify_state_backup, +) +from ..status_server import is_loopback_host + +STATE_RECOVERY_PATH = "/api/chat/state-recovery" +_UPLOAD_TTL_SECONDS = 3600 +_REF = re.compile(r"^(archive|manifest):([a-f0-9]{32})$") + + +class StateRecoveryRequestMixin: + def _recovery_root(self, *, create: bool = False) -> Path: + if os.name != "posix": + raise StateRecoveryError("recovery_platform_unqualified") + if not is_loopback_host(str(self.server.server_address[0])): + raise StateRecoveryError("recovery_requires_loopback_server") + root = self.server.runtime_root / "backups" / "state-recovery" + if root.absolute() != root.resolve(): + raise StateRecoveryError("recovery_storage_not_physical") + if create: + root.mkdir(parents=True, exist_ok=True, mode=0o700) + (root / "inputs").mkdir(exist_ok=True, mode=0o700) + (root / "checkpoints").mkdir(exist_ok=True, mode=0o700) + for directory in (root, root / "inputs", root / "checkpoints"): + if directory.exists() or directory.is_symlink(): + info = directory.lstat() + if directory.is_symlink() or not directory.is_dir() or info.st_uid != os.getuid() or info.st_mode & 0o077: + raise StateRecoveryError("recovery_storage_not_private") + return root + + def _state_recovery_profile(self) -> None: + supported = os.name == "posix" and is_loopback_host(str(self.server.server_address[0])) + self._send_json({"ok": True, "supported": supported, "activation_available": False, + "maximum_archive_bytes": MAX_ARCHIVE_BYTES, "maximum_manifest_bytes": MAX_MANIFEST_BYTES}) + + def _recovery_failure(self, error: Exception) -> None: + code = error.code if isinstance(error, StateRecoveryError) else "recovery_unavailable" + self._send_error("Backup inspection did not finish. Check the archive, manifest and available disk space.", + status=400 if isinstance(error, ValueError) else 503, error_code=code) + + def _state_recovery_upload(self, kind: str) -> None: + temporary: Path | None = None + previous_timeout = self.connection.gettimeout() + try: + if os.name != "posix": + raise StateRecoveryError("recovery_platform_unqualified") + maximum = MAX_ARCHIVE_BYTES if kind == "archive" else MAX_MANIFEST_BYTES + lengths = self.headers.get_all("Content-Length") or [] + if len(lengths) != 1 or not lengths[0].isdigit() or self.headers.get("Transfer-Encoding"): + raise StateRecoveryError("recovery_upload_length_invalid") + length = int(lengths[0]) + if not 0 < length <= maximum: + raise StateRecoveryError("recovery_capacity_exceeded") + root = self._recovery_root(create=True) + if shutil.disk_usage(root).free < length + MAX_MANIFEST_BYTES: + raise StateRecoveryError("recovery_disk_capacity") + inputs = root / "inputs" + # These are disposable upload inputs, not backups or audit receipts. + for old in inputs.iterdir(): + if re.fullmatch(r"(?:archive|manifest)-[a-f0-9]{32}", old.name) and not old.is_symlink(): + if old.stat().st_mtime < time.time() - _UPLOAD_TTL_SECONDS: + old.unlink() + fd, name = tempfile.mkstemp(prefix=".upload-", dir=inputs) + temporary = Path(name) + digest, remaining = hashlib.sha256(), length + self.connection.settimeout(30) + with os.fdopen(fd, "wb") as output: + while remaining: + chunk = self.rfile.read(min(256 * 1024, remaining)) + if not chunk: + raise StateRecoveryError("recovery_upload_incomplete") + output.write(chunk) + digest.update(chunk) + remaining -= len(chunk) + output.flush() + os.fsync(output.fileno()) + identifier = uuid4().hex + destination = inputs / f"{kind}-{identifier}" + os.link(temporary, destination) + self._send_json({"ok": True, "input_ref": f"{kind}:{identifier}", + "sha256": digest.hexdigest(), "size_bytes": length, + **({"checkpoint_ref": identifier} if kind == "archive" else {})}) + except (ValueError, OSError, RuntimeError) as error: + self.close_connection = True + self._recovery_failure(error) + finally: + self.connection.settimeout(previous_timeout) + if temporary is not None: + temporary.unlink(missing_ok=True) + + def _recovery_input(self, ref: object, kind: str) -> tuple[Path, str]: + match = _REF.fullmatch(ref) if isinstance(ref, str) else None + if match is None or match.group(1) != kind: + raise StateRecoveryError("recovery_input_ref_invalid") + root = self._recovery_root() + return root / "inputs" / f"{kind}-{match.group(2)}", match.group(2) + + def _state_recovery_verify(self) -> None: + try: + body = self._read_json() + if set(body) != {"archive_ref", "manifest_ref", "execute"} or type(body["execute"]) is not bool: + raise StateRecoveryError("recovery_request_invalid") + archive, identifier = self._recovery_input(body["archive_ref"], "archive") + manifest, _ = self._recovery_input(body["manifest_ref"], "manifest") + destination = self._recovery_root() / "checkpoints" / identifier + audit = verify_state_backup(archive=archive, manifest=manifest, destination=destination, + execute=body["execute"]) + self._send_json({**audit, "checkpoint_ref": identifier}) + except (ValueError, OSError, RuntimeError) as error: + self._recovery_failure(error) + + def _state_recovery_read(self) -> None: + try: + body = self._read_json() + ref = body.get("checkpoint_ref") + if set(body) != {"checkpoint_ref"} or not isinstance(ref, str) or not re.fullmatch(r"[a-f0-9]{32}", ref): + raise StateRecoveryError("recovery_checkpoint_ref_invalid") + audit = read_state_recovery_audit(self._recovery_root() / "checkpoints" / ref) + self._send_json({**audit, "checkpoint_ref": ref, "historical_readback": True}) + except (ValueError, OSError, RuntimeError) as error: + self._recovery_failure(error) + + def _state_recovery_list(self) -> None: + try: + parent = self._recovery_root() / "checkpoints" + candidates = [] if not parent.exists() else [path for path in parent.iterdir() + if re.fullmatch(r"[a-f0-9]{32}", path.name) and path.is_dir() and not path.is_symlink()] + candidates.sort(key=lambda path: path.stat().st_mtime_ns, reverse=True) + items = [] + for path in candidates[:10]: + try: + audit = read_state_recovery_audit(path) + items.append({"checkpoint_ref": path.name, "available": True, + "observed_at": audit["observed_at"], "summary": audit["summary"]}) + except (ValueError, OSError, RuntimeError): + items.append({"checkpoint_ref": path.name, "available": False}) + self._send_json({"ok": True, "items": items, "has_more": len(candidates) > 10}) + except (ValueError, OSError, RuntimeError) as error: + self._recovery_failure(error) diff --git a/loopx/semantics/project_registry_io_manifest_v1.json b/loopx/semantics/project_registry_io_manifest_v1.json index e076e23f56..14a6f75388 100644 --- a/loopx/semantics/project_registry_io_manifest_v1.json +++ b/loopx/semantics/project_registry_io_manifest_v1.json @@ -895,7 +895,7 @@ }, { "site": "loopx/cli_commands/support_control_backup.py::.handle_backup_state_command::codec_read:load_registry#1", - "line": 84, + "line": 110, "column": 49, "kind": "codec_read", "api": "load_registry", diff --git a/loopx/semantics/vocabulary_v0.json b/loopx/semantics/vocabulary_v0.json index 5b9a4df1d2..0a3fc1000d 100644 --- a/loopx/semantics/vocabulary_v0.json +++ b/loopx/semantics/vocabulary_v0.json @@ -13,7 +13,7 @@ "formal_model": { "schema_version": "loopx_semantic_formal_model_v0", "universes": { - "vocabularies": "V: registered vocabulary identifiers; Producers(V) ⊆ V is exactly the subset declaring a producers key (including compatibility-only entries with an empty list). Kernel(V) ⊆ Producers(V). Producers(V) contains 6 vocabularies: 5 kernel and 1 outside the kernel tier. The 20 cross_runtime vocabularies outside Producers(V) are unverified.", + "vocabularies": "V: registered vocabulary identifiers; Producers(V) ⊆ V is exactly the subset declaring a producers key (including compatibility-only entries with an empty list). Kernel(V) ⊆ Producers(V). Producers(V) contains 6 vocabularies: 5 kernel and 1 outside the kernel tier. The 21 cross_runtime vocabularies outside Producers(V) are unverified.", "values": "U(v): ambient runtime values; S(v): registered admitted values", "sites": "L: source locations that define, produce, consume, interpret, pass through, project, or persist values", "scopes": "Scope: global or bounded_context(context_id); ScopeDeclarations are the forked names the registry declares as bounded contexts", @@ -73,25 +73,25 @@ "invariants": [ { "id": "F1_producer_closedness", - "statement": "∀v ∈ Producers(V): Produced_scan(v) ⊆ S(v) ⊆ U(v), where Produced_scan(v) is the production the fixed forms observe inside the code-owned scan reach. Producers(V) contains 6 vocabularies: 5 kernel and 1 outside the kernel tier. The 20 cross_runtime vocabularies outside Producers(V) are unverified. Production outside the scan reach is unverified rather than proven closed.", + "statement": "∀v ∈ Producers(V): Produced_scan(v) ⊆ S(v) ⊆ U(v), where Produced_scan(v) is the production the fixed forms observe inside the code-owned scan reach. Producers(V) contains 6 vocabularies: 5 kernel and 1 outside the kernel tier. The 21 cross_runtime vocabularies outside Producers(V) are unverified. Production outside the scan reach is unverified rather than proven closed.", "enforcement": "m0_5", "evidence": "bounded production-form AST scan over the PRODUCER_ROOTS/PRODUCER_FILES reach in loopx/semantics/production.py; check_producers visits only vocabularies that declare producers, and validate_production claims no whole-program closedness; unresolved dynamic sites are reported, not treated as proven safe", "domain": { "quantifies_over": "vocabularies[producers].producers", "verified": 6, - "registered": 26, + "registered": 27, "evidence_bound": "producer_scan_reach" } }, { "id": "F2_canonical_value_liveness", - "statement": "∀v ∈ Producers(V): Canonical(v) ⊆ Produced_scan(v) ∪ CompatibilityOnly(v). Producers(V) contains 6 vocabularies: 5 kernel and 1 outside the kernel tier. The 20 cross_runtime vocabularies outside Producers(V) are unverified. Liveness outside the walked set is not proven.", + "statement": "∀v ∈ Producers(V): Canonical(v) ⊆ Produced_scan(v) ∪ CompatibilityOnly(v). Producers(V) contains 6 vocabularies: 5 kernel and 1 outside the kernel tier. The 21 cross_runtime vocabularies outside Producers(V) are unverified. Liveness outside the walked set is not proven.", "enforcement": "m0_5", "evidence": "every value of a vocabulary in Producers(V) has a producer recognised inside the scan reach, an executable input witness, or an explicit compatibility-only reason; a value whose only producer lies outside the reach would be reported as dead, not silently accepted", "domain": { "quantifies_over": "vocabularies[producers].producers", "verified": 6, - "registered": 26, + "registered": 27, "evidence_bound": "producer_scan_reach" } }, @@ -103,7 +103,7 @@ "domain": { "quantifies_over": "vocabularies[*]", "verified": 0, - "registered": 26, + "registered": 27, "evidence_bound": "inventory_only" } }, @@ -941,6 +941,25 @@ "control_repair": "Appended when the turn is routed to a control-plane repair instead of delivery: stall self-repair, a capability gate asking to repair the bridge, a workspace guard being present, or a required automation-prompt upgrade. The repair is control-plane work, so it displaces the delivery it would otherwise run alongside.", "delivery_not_allowed": "The catch-all appended last when normal delivery is off for a reason none of the four specific causes names, such as a monitor-only work mode, a settled receipt-bound monitor, a monitor-quiet skip or an external-evidence wait. Because it is appended last it only surfaces as the chosen reason when no more specific cause applies." } + }, + "state_recovery_component_state": { + "meaning": "Offline owner-local readability of a component in an inert recovery audit; never execution or adoption authority.", + "tier": "cross_runtime", + "status": "canonical", + "owners": { + "python": null, + "typescript": "loopx/control_plane/state_recovery_contract.ts::RECOVERY_COMPONENT_STATES" + }, + "values": [ + "readable", + "historical_only", + "unknown" + ], + "value_notes": { + "readable": "An existing configuration verifier or the selected File/SQLite authority and full Todo/history readers accepted the captured component. This is historical readability, not a current Goal identity, permission or execution grant.", + "historical_only": "A standalone SQLite copy passed its bounded offline structural integrity check, but its bytes have not been adopted by a current domain owner. Restored provider identities and leases remain historical data.", + "unknown": "No supported owner inventory exists for remaining v0 state, or a component failed format, availability, qualified-runtime or owner-read validation. Unknown never becomes readable merely because archive or file hashes matched." + } } }, "relations": { diff --git a/loopx/state_recovery.py b/loopx/state_recovery.py new file mode 100644 index 0000000000..b006e5635a --- /dev/null +++ b/loopx/state_recovery.py @@ -0,0 +1,414 @@ +"""Bounded tar/filesystem adapter for the typed, inert recovery audit owner. + +No restored Python, plugin, shell, credential or live registry is loaded. +Semantic manifest agreement and owner readback remain in TypeScript. +""" +from __future__ import annotations + +from contextlib import contextmanager +from collections.abc import Callable +import gzip +import hashlib +import io +import json +import os +from pathlib import Path, PurePosixPath +import shutil +import stat +import subprocess +import tarfile +import tempfile +import unicodedata +import zlib +from typing import Any, BinaryIO, Iterator + +from .control_plane.effect_runtime import _node_executable, effect_runtime_result, effect_runtime_request_scope + +MAX_ARCHIVE_BYTES = 128 * 1024 * 1024 +MAX_EXPANDED_BYTES = 512 * 1024 * 1024 +MAX_MANIFEST_BYTES = 16 * 1024 * 1024 +MAX_MEMBERS = 20_000 +MAX_EXPANSION_RATIO = 2000 +_CHUNK = 256 * 1024 +_OWNER_TIMEOUT_SECONDS = 90 + + +class StateRecoveryError(ValueError): + def __init__(self, code: str): + self.code = code + super().__init__(code) + + +def _require(condition: bool, code: str) -> None: + if not condition: + raise StateRecoveryError(code) + + +def _json(data: bytes) -> dict[str, Any]: + def pairs(items: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in items: + _require(key not in result, "manifest_duplicate_key") + result[key] = value + return result + + def constant(_value: str) -> None: + raise StateRecoveryError("manifest_invalid_json") + + try: + value = json.loads(data, object_pairs_hook=pairs, parse_constant=constant) + except (UnicodeError, json.JSONDecodeError, RecursionError) as exc: + raise StateRecoveryError("manifest_invalid_json") from exc + if not isinstance(value, dict): + raise StateRecoveryError("manifest_invalid_json") + pending = [(value, 0)] + while pending: + item, depth = pending.pop() + _require(depth <= 64, "recovery_capacity_exceeded") + if isinstance(item, dict): + pending.extend((child, depth + 1) for child in item.values()) + elif isinstance(item, list): + pending.extend((child, depth + 1) for child in item) + return value + + +def _runtime(method: str, request: dict[str, Any]) -> dict[str, Any]: + value = effect_runtime_result(method, request, timeout=90, + large_local_snapshot=method in {"state.recovery.archive.verify", "state.recovery.manifest.verify"}) + if not isinstance(value, dict): + raise StateRecoveryError("recovery_owner_result_invalid") + if value.get("ok") is not True: + code = value.get("error_code") + raise StateRecoveryError(code if isinstance(code, str) else "recovery_owner_result_invalid") + return value + + +def _audit_owners(request: dict[str, Any]) -> dict[str, Any]: + command = [_node_executable(), "--no-warnings", "--max-old-space-size=512", + "--experimental-sqlite", "--experimental-strip-types", + str(Path(__file__).parent / "control_plane/state_recovery_worker.ts")] + try: + completed = subprocess.run(command, input=json.dumps(request), text=True, encoding="utf-8", + capture_output=True, timeout=_OWNER_TIMEOUT_SECONDS, check=False) + except subprocess.TimeoutExpired as exc: + # subprocess.run kills and waits for this child only, never the daemon. + raise StateRecoveryError("recovery_owner_timeout") from exc + _require(bool(completed.stdout.strip()), "recovery_owner_unavailable") + _require(len(completed.stdout) <= 2 * 1024 * 1024, "recovery_owner_result_invalid") + result = _json(completed.stdout.encode()) + if result.get("ok") is not True: + raise StateRecoveryError(str(result.get("error_code") or "recovery_owner_unavailable")) + _require(completed.returncode == 0, "recovery_owner_unavailable") + return result + + +@contextmanager +def _regular_input(path: Path, limit: int) -> Iterator[BinaryIO]: + try: + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) + except OSError as exc: + raise StateRecoveryError("recovery_input_unavailable") from exc + with os.fdopen(fd, "rb") as handle: + info = os.fstat(handle.fileno()) + _require(stat.S_ISREG(info.st_mode), "recovery_input_not_regular") + _require(info.st_uid == os.getuid() and not info.st_mode & 0o077, "recovery_input_not_private") + _require(0 < info.st_size <= limit, "recovery_capacity_exceeded") + yield handle + + +def _sha(handle: BinaryIO) -> tuple[str, int]: + handle.seek(0) + digest, count = hashlib.sha256(), 0 + while chunk := handle.read(_CHUNK): + count += len(chunk) + _require(count <= MAX_ARCHIVE_BYTES, "recovery_capacity_exceeded") + digest.update(chunk) + handle.seek(0) + return digest.hexdigest(), count + + +class _BoundedGzip(io.RawIOBase): + def __init__(self, handle: BinaryIO, archive_size: int): + super().__init__() + self.source = gzip.GzipFile(fileobj=handle, mode="rb") + self.limit = min(MAX_EXPANDED_BYTES, archive_size * MAX_EXPANSION_RATIO) + self.total = 0 + + def read(self, size: int = -1) -> bytes: + _require(size >= 0, "archive_unbounded_read") + data = self.source.read(min(size, self.limit - self.total + 1)) + self.total += len(data) + _require(self.total <= self.limit, "recovery_capacity_exceeded") + return data + + +class _RecoveryTarInfo(tarfile.TarInfo): + def _proc_member(self, archive: tarfile.TarFile) -> tarfile.TarInfo | None: + # Bound extended metadata before tarfile allocates/decodes its body. + if self.type in (tarfile.XHDTYPE, tarfile.XGLTYPE, tarfile.GNUTYPE_LONGNAME, tarfile.GNUTYPE_LONGLINK): + _require(self.size <= 128 * 1024, "archive_metadata_too_large") + if isinstance(archive, _RecoveryTarFile): + archive.recovery_metadata_bytes += self.size + _require(archive.recovery_metadata_bytes <= MAX_MANIFEST_BYTES, "archive_metadata_too_large") + # The stdlib hook is absent from typeshed; keep its real bound signature + # here rather than weakening the public stream/return types. + process: Callable[[tarfile.TarFile], tarfile.TarInfo | None] = getattr(super(), "_proc_member") + return process(archive) + + +class _RecoveryTarFile(tarfile.TarFile): + recovery_metadata_bytes = 0 + + +def _member_name(raw: str) -> str: + _require(bool(raw) and len(raw) <= 1024 and "\\" not in raw and "\x00" not in raw, + "archive_path_invalid") + path = PurePosixPath(raw) + _require(bool(path.parts) and len(path.parts) <= 64 and not path.is_absolute() + and ".." not in path.parts and ":" not in path.parts[0] + and all(len(part.encode("utf-8")) <= 255 for part in path.parts), + "archive_path_invalid") + normalized = str(path) + _require(normalized not in ("", "."), "archive_path_invalid") + return normalized + + +def _physical_ancestors(destination: Path) -> None: + _require(destination.is_absolute() and str(destination) == os.path.normpath(destination), + "recovery_destination_invalid") + for parent in destination.parents: + try: + info = parent.lstat() + except OSError as exc: + raise StateRecoveryError("recovery_destination_parent_unavailable") from exc + _require(stat.S_ISDIR(info.st_mode) and not stat.S_ISLNK(info.st_mode), + "recovery_destination_parent_not_physical") + + +def _physical_parent(destination: Path) -> None: + _physical_ancestors(destination) + _require(not destination.exists() and not destination.is_symlink(), "recovery_destination_exists") + + +def _directories(root: Path, parts: tuple[str, ...]) -> Path: + current = root + for part in parts: + current /= part + current.mkdir(mode=0o700, exist_ok=True) + _require(current.is_dir() and not current.is_symlink(), "archive_parent_not_directory") + return current + + +def _check_links(links: list[tuple[str, str]]) -> None: + by_name = {unicodedata.normalize("NFC", name).casefold(): target for name, target in links} + for name, target in links: + pending = list(PurePosixPath(name).parent.parts) + list(PurePosixPath(target).parts) + resolved: list[str] = [] + hops = 0 + while pending: + part = pending.pop(0) + if part == "..": + _require(bool(resolved), "archive_link_escapes") + resolved.pop() + elif part != ".": + resolved.append(part) + key = unicodedata.normalize("NFC", "/".join(resolved)).casefold() + if key in by_name: + hops += 1 + _require(hops <= 40, "archive_link_cycle") + resolved.pop() + pending = list(PurePosixPath(by_name[key]).parts) + pending + + +def _scan(handle: BinaryIO, archive_size: int, output: Path | None = None) -> dict[str, Any]: + handle.seek(0) + stream = _BoundedGzip(handle, archive_size) + names: dict[str, str] = {} + folded: dict[str, str] = {} + ancestors: set[str] = set() + links: list[tuple[str, str]] = [] + files, sqlite_members, standalone = [], [], [] + embedded: dict[str, Any] | None = None + logical = 0 + try: + with _RecoveryTarFile.open(fileobj=stream, mode="r|", tarinfo=_RecoveryTarInfo) as archive: + for member in archive: + name = _member_name(member.name) + key = unicodedata.normalize("NFC", name).casefold() + _require(name not in names and key not in folded, "archive_name_collision") + _require(len(names) < MAX_MEMBERS, "recovery_capacity_exceeded") + _require(member.sparse is None, "archive_sparse_unsupported") + kind = "file" if member.isfile() else "directory" if member.isdir() else "symlink" if member.issym() else None + _require(kind is not None, "archive_member_type_unsupported") + names[name], folded[key] = str(kind), name + for parent in PurePosixPath(name).parents: + parent_key = unicodedata.normalize("NFC", str(parent)).casefold() + ancestors.add(parent_key) + _require(names.get(folded.get(parent_key, ""), "directory") == "directory", + "archive_parent_not_directory") + if kind != "directory": + _require(key not in ancestors, "archive_parent_not_directory") + if kind == "symlink": + target = member.linkname + _require(bool(target) and len(target) <= 1024 and "\x00" not in target + and not PurePosixPath(target).is_absolute() and "\\" not in target, + "archive_link_escapes") + resolved = os.path.normpath(str(PurePosixPath(name).parent / target)) + _require(resolved != ".." and not resolved.startswith("../"), "archive_link_escapes") + links.append((name, target)) + continue + if output is not None: + _directories(output, PurePosixPath(name).parts if kind == "directory" else PurePosixPath(name).parts[:-1]) + if kind == "directory": + continue + logical += member.size + _require(0 <= member.size <= MAX_EXPANDED_BYTES and logical <= MAX_EXPANDED_BYTES, + "recovery_capacity_exceeded") + if name == "manifest.json": + _require(member.size <= MAX_MANIFEST_BYTES, "archive_metadata_too_large") + source = archive.extractfile(member) + if source is None: + raise StateRecoveryError("archive_member_unreadable") + data, prefix = bytearray(), bytearray() + digest, size = hashlib.sha256(), 0 + destination = None + try: + if output is not None: + destination = open(output / name, "xb") + os.chmod(output / name, 0o600) + while chunk := source.read(_CHUNK): + size += len(chunk) + _require(size <= member.size, "archive_member_size_mismatch") + digest.update(chunk) + prefix.extend(chunk[:max(0, 100 - len(prefix))]) + if name == "manifest.json": + data.extend(chunk) + if destination is not None: + destination.write(chunk) + _require(size == member.size, "archive_member_size_mismatch") + if destination is not None: + destination.flush() + os.fsync(destination.fileno()) + finally: + source.close() + if destination is not None: + destination.close() + if name == "manifest.json": + embedded = _json(bytes(data)) + else: + files.append({"archive_path": name, "sha256": digest.hexdigest(), "size_bytes": size}) + if prefix[:16] == b"SQLite format 3\0": + sqlite_members.append(name) + if prefix[18:20] == b"\x01\x01": + standalone.append(name) + # tar EOF must not conceal an appended archive or unparsed payload. + while trailing := archive.fileobj.read(_CHUNK): + _require(not trailing.strip(b"\0"), "archive_trailing_data") + # Consume gzip EOF too: truncated CRC/trailing expansion is not a valid tar. + while stream.read(_CHUNK): + pass + except (tarfile.TarError, EOFError, gzip.BadGzipFile, OSError, zlib.error) as exc: + raise StateRecoveryError("archive_unreadable") from exc + finally: + stream.source.close() + _require(embedded is not None, "embedded_manifest_missing") + _check_links(links) + # Links are materialized last and no member may live beneath a link. + if output is not None: + for name, target in links: + _directories(output, PurePosixPath(name).parts[:-1]) + os.symlink(target, output / name) + return {"embedded_manifest": embedded, "files": files, "logical_bytes": logical, + "sqlite_members": sqlite_members, "standalone_sqlite_members": standalone, + "entry_count": len(names)} + + +@effect_runtime_request_scope() +def verify_state_backup( + *, archive: Path, manifest: Path, destination: Path, execute: bool = False, +) -> dict[str, Any]: + """Preview or publish one inert audit. This API has no live-adoption operation.""" + _require(os.name == "posix", "recovery_platform_unqualified") + _physical_parent(destination) + with _regular_input(manifest, MAX_MANIFEST_BYTES) as manifest_handle: + manifest_bytes = manifest_handle.read(MAX_MANIFEST_BYTES + 1) + _require(len(manifest_bytes) <= MAX_MANIFEST_BYTES, "recovery_capacity_exceeded") + external = _json(manifest_bytes) + with _regular_input(archive, MAX_ARCHIVE_BYTES) as archive_handle: + archive_sha, archive_size = _sha(archive_handle) + _runtime("state.recovery.archive.verify", {"external_manifest": external, + "archive_sha256": archive_sha, "archive_size_bytes": archive_size}) + observation = _scan(archive_handle, archive_size) + verified = _runtime("state.recovery.manifest.verify", { + "external_manifest": external, "embedded_manifest": observation["embedded_manifest"], + "archive_sha256": archive_sha, "archive_size_bytes": archive_size, + "external_manifest_sha256": hashlib.sha256(manifest_bytes).hexdigest(), + "files": observation["files"], + "sqlite_member_count": len(observation["sqlite_members"]), + }) + _require(shutil.disk_usage(destination.parent).free >= observation["logical_bytes"] + MAX_MANIFEST_BYTES, + "recovery_disk_capacity") + if not execute: + return {**verified, "status": "planned", "written": False, + "logical_bytes": observation["logical_bytes"], "entry_count": observation["entry_count"], + "activation_eligible": False} + staging = Path(tempfile.mkdtemp(prefix=".loopx-recovery-", dir=destination.parent)) + reserved = False + try: + data = staging / "data" + data.mkdir(mode=0o700) + copied = _scan(archive_handle, archive_size, data) + _require(copied == observation and _sha(archive_handle) == (archive_sha, archive_size), + "recovery_input_changed") + audit = _audit_owners({"data_root": str(data), "verified": verified, + "sqlite_members": observation["sqlite_members"], + "standalone_sqlite_members": observation["standalone_sqlite_members"]}) + audit = _runtime("state.recovery.audit.readback", audit) + for member in observation["files"]: + with (data / member["archive_path"]).open("rb") as handle: + digest = hashlib.file_digest(handle, "sha256").hexdigest() + _require(digest == member["sha256"], "recovery_owner_mutated_source") + audit_path = staging / "recovery-audit.json" + with audit_path.open("x", encoding="utf-8") as handle: + os.chmod(audit_path, 0o600) + json.dump(audit, handle, ensure_ascii=False, sort_keys=True, indent=2) + handle.write("\n") + handle.flush() + os.fsync(handle.fileno()) + _require(_json(audit_path.read_bytes()) == audit, "recovery_audit_readback_mismatch") + _physical_parent(destination) + destination.mkdir(mode=0o700) + reserved = True + os.rename(staging, destination) + reserved = False + return read_state_recovery_audit(destination) + finally: + if reserved: + destination.rmdir() + shutil.rmtree(staging, ignore_errors=True) + + +def read_state_recovery_audit(destination: Path) -> dict[str, Any]: + path = destination / "recovery-audit.json" + _physical_ancestors(path) + with _regular_input(path, MAX_MANIFEST_BYTES) as handle: + audit = _json(handle.read(MAX_MANIFEST_BYTES + 1)) + return _runtime("state.recovery.audit.readback", audit) + + +def render_state_recovery_markdown(payload: dict[str, Any]) -> str: + if payload.get("ok") is not True: + return f"Backup inspection failed: {payload.get('error_code', 'recovery_unavailable')}. No execution authority was granted." + if payload.get("status") == "planned": + return "\n".join([ + "Archive and manifest checks passed. No copy has been created and owner records have not been inspected.", + f"Saved files: {payload['file_count']}; logical bytes: {payload['logical_bytes']}", + "Repeat with --execute to create and inspect a new inert copy; this never activates a Goal.", + ]) + summary = payload.get("summary", {}) + return "\n".join([ + f"Backup inspection: {payload['status']}", + f"Recovery: {payload['recovery_id']}", + f"Readable: {summary.get('readable', 0)}; historical only: {summary.get('historical_only', 0)}; unknown: {summary.get('unknown', 0)}", + "The copy is inert. Goal execution and live settings were not activated.", + ]) diff --git a/tests/control_plane_ts/state_recovery.test.ts b/tests/control_plane_ts/state_recovery.test.ts new file mode 100644 index 0000000000..bd2eb40d66 --- /dev/null +++ b/tests/control_plane_ts/state_recovery.test.ts @@ -0,0 +1,75 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import {canonicalAuthoritySha256} from "../../loopx/control_plane/coordination/authority_store_codec.ts"; +import {createEffectRuntimeHandlers, dispatchEffectRuntimeMethod} from "../../loopx/control_plane/effect_runtime_handlers.ts"; +import {verifyStateRecoveryArchive, verifyStateRecoveryManifest} from "../../loopx/control_plane/state_recovery.ts"; +import {decodeStateRecoveryAudit, STATE_RECOVERY_AUDIT_SCHEMA} from "../../loopx/control_plane/state_recovery_contract.ts"; + +const member = {archive_path: "runtime-root/retained", size_bytes: 1, sha256: "c".repeat(64)}; +function request() { + const external = {schema_version: "loopx_state_backup_v0", ok: true, dry_run: false, + execute_requested: true, backup_id: "fixture", private_hint: "private-marker", + execution: {archive_sha256: "a".repeat(64), archive_size_bytes: 500, + file_members: [member], archive_to_logical_ratio: 0.5}}; + const embedded = structuredClone(external) as Record; + embedded.execution.archive_sha256 = embedded.execution.archive_size_bytes = null; + delete embedded.execution.archive_to_logical_ratio; + return {external_manifest: external, embedded_manifest: embedded, files: [member], + archive_sha256: "a".repeat(64), archive_size_bytes: 500, external_manifest_sha256: "b".repeat(64), + sqlite_member_count: 0}; +} + +test("archive and manifest checks bind observed bytes without projecting private capture metadata", () => { + const input = request(), before = structuredClone(input); + const first = verifyStateRecoveryArchive(input); + assert.equal(first.ok, true); + const verified = verifyStateRecoveryManifest(input); + assert.equal(verified.ok, true); + assert.equal(verified.file_witnesses_verified, true); + assert.equal(verified.manifest_verified, true); + assert.equal(verified.execution_authority_granted, false); + assert.equal(JSON.stringify(verified).includes("private-marker"), false); + assert.deepEqual(input, before); + assert.equal(verifyStateRecoveryArchive({...input, archive_sha256: "d".repeat(64)}).error_code, "archive_digest_mismatch"); + assert.equal(verifyStateRecoveryManifest({...input, files: [{...member, size_bytes: 2}]}).error_code, "member_witness_mismatch"); + assert.equal(verifyStateRecoveryManifest({...input, embedded_manifest: {...input.embedded_manifest, backup_id: "other"}}).error_code, "manifest_disagreement"); + assert.equal(verifyStateRecoveryManifest({...input, sqlite_member_count: 129}).error_code, "sqlite_inventory_capacity"); +}); + +test("legacy absent member witnesses stay unqualified rather than becoming verified by an empty list", () => { + const input = request(); + delete (input.external_manifest.execution as Record).file_members; + delete input.embedded_manifest.execution.file_members; + const result = verifyStateRecoveryManifest(input); + assert.equal(result.ok, true); + assert.equal(result.file_witnesses_verified, false); + assert.equal(result.capture_profile, "legacy_best_effort"); +}); + +function audit() { + const body = {ok: true, schema_version: STATE_RECOVERY_AUDIT_SCHEMA, + recovery_id: "e".repeat(64), status: "held", written: true, manifest_verified: true, + observed_at: "2026-10-12T00:00:00.000Z", execution_authority_granted: false, activation_eligible: false, + backup: {schema_version: "loopx_state_backup_v0", archive_sha256: "a".repeat(64), external_manifest_sha256: "b".repeat(64)}, + workspace: {isolation: "inert", registered: false}, + components: [{component_id: "configuration", owner_kind: "configuration", state: "readable", + reason_code: "configuration_digest_verified", next_action: "retain_isolated_configuration", facts: {}, dependency_ids: []}], + summary: {readable: 1, historical_only: 0, unknown: 0}, findings: []}; + return {...body, audit_sha256: canonicalAuthoritySha256(body)}; +} + +test("the packaged client and owner reject permission, unknown-state and summary contradictions", () => { + const input = audit(); + assert.doesNotThrow(() => decodeStateRecoveryAudit(input)); + for (const delta of [{execution_authority_granted: true}, {activation_eligible: true}, + {workspace: {isolation: "inert", registered: true}}, {summary: {...input.summary, readable: 2}}]) { + assert.throws(() => decodeStateRecoveryAudit({...input, ...delta})); + } + assert.throws(() => decodeStateRecoveryAudit({...input, components: [{...input.components[0], state: "active"}]})); + assert.throws(() => decodeStateRecoveryAudit({...input, components: [{...input.components[0], dependency_ids: ["missing"]}]})); +}); + +test("untrusted owner inspection has no shared-daemon dispatch", async () => { + const handlers = createEffectRuntimeHandlers({fingerprint: "recovery-isolation", requestShutdown() {}}); + await assert.rejects(() => dispatchEffectRuntimeMethod(handlers, "state.recovery.owners.audit", {}), /unsupported Effect runtime method/); +}); diff --git a/tests/test_state_recovery.py b/tests/test_state_recovery.py new file mode 100644 index 0000000000..49926c3d79 --- /dev/null +++ b/tests/test_state_recovery.py @@ -0,0 +1,366 @@ +from __future__ import annotations + +import hashlib +import io +import http.client +import json +import os +from pathlib import Path +import subprocess +import sqlite3 +import sys +import tarfile +import threading + +import pytest + +from loopx.state_backup import build_state_backup_plan, execute_state_backup_plan +from loopx.state_recovery import StateRecoveryError, read_state_recovery_audit, render_state_recovery_markdown, verify_state_backup +from tests.control_plane.canonical_authority_fixture import promoted_create_fixture + + +def _cli(*args: object) -> tuple[int, dict]: + result = subprocess.run([sys.executable, "-m", "loopx.cli", "--format", "json", *map(str, args)], + capture_output=True, text=True, env={**os.environ, "LOOPX_USAGE_PING": "0"}, timeout=90) + return result.returncode, json.loads(result.stdout) + + +def _backup(root: Path, provider: str | None = None) -> tuple[Path, Path]: + if provider: + registry, runtime, state = promoted_create_fixture(root, provider=provider) + project = state.parents[3] + code, result = _cli("--registry", registry, "--runtime-root", runtime, "todo", "add", + "--goal-id", "goal-a", "--role", "agent", "--claimed-by", "agent-a", + "--operation-id", "retained-work", "--text", "Keep this unfinished commitment") + assert code == 0, result + state.write_text(state.read_text() + "\n## Original commitments\nPreserve the full Goal, including unfinished acceptance.\n") + else: + project, runtime = root / "project", root / "runtime" + project.mkdir() + runtime.mkdir() + registry = root / "registry.json" + registry.write_text(json.dumps({"goals": []})) + (runtime / "retained.txt").write_text("saved data") + result = execute_state_backup_plan(build_state_backup_plan(project=project, runtime_root=runtime, + registry_path=registry, output_dir=root / "backups", backup_id="source", include_registry_projects=False, + include_automations=False, include_skills=False)) + return Path(result["archive_path"]), Path(result["manifest_path"]) + + +def _rewrite(archive: Path, manifest: Path, extra: tarfile.TarInfo | None = None, + body: bytes = b"", replacements: dict[str, bytes] | None = None) -> None: + with tarfile.open(archive, "r:gz") as source: + records = [(row, source.extractfile(row).read() if row.isfile() else None) for row in source] + with tarfile.open(archive, "w:gz") as target: + for row, data in records: + if replacements and row.name in replacements: + data = replacements[row.name] + row.size = len(data) + target.addfile(row, io.BytesIO(data) if data is not None else None) + if extra is not None: + target.addfile(extra, io.BytesIO(body) if extra.isfile() else None) + data = json.loads(manifest.read_text()) + data["execution"]["archive_sha256"] = hashlib.sha256(archive.read_bytes()).hexdigest() + data["execution"]["archive_size_bytes"] = archive.stat().st_size + manifest.write_text(json.dumps(data)) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_real_backup_recovers_readable_authority_after_original_sources_disappear(tmp_path, provider): + archive, manifest = _backup(tmp_path, provider) + (tmp_path / "project").rename(tmp_path / "unavailable-project") + (tmp_path / "runtime").rename(tmp_path / "unavailable-runtime") + (tmp_path / "registry.json").rename(tmp_path / "unavailable-registry.json") + original = archive.read_bytes(), manifest.read_bytes() + destination = tmp_path / "inert" + preview = verify_state_backup(archive=archive, manifest=manifest, destination=destination) + assert preview["status"] == "planned" and preview["written"] is False + preview_text = render_state_recovery_markdown(preview) + assert "owner records have not been inspected" in preview_text and "Readable: 0" not in preview_text + assert not destination.exists() + code, audit = _cli("backup-state", "verify", "--archive", archive, "--manifest", manifest, + "--destination", destination, "--execute") + assert code == 0, audit + assert audit["execution_authority_granted"] is False and audit["activation_eligible"] is False + assert audit["workspace"] == {"isolation": "inert", "registered": False} + assert audit["status"] == "held" + [goal] = [row for row in audit["components"] if row["owner_kind"] == "coordination_authority"] + assert goal["state"] == "readable", goal + assert goal["facts"]["todo_count"] == 1 + assert goal["facts"]["history_commits"] == 2 + assert goal["facts"]["provider"] == provider + captures = json.loads(manifest.read_text())["included"] + state_path = next(row["archive_path"] for row in captures if row["key"] == "registry_active_state:goal-a") + assert "Preserve the full Goal, including unfinished acceptance." in (destination / "data" / state_path).read_text() + assert (archive.read_bytes(), manifest.read_bytes()) == original + assert json.loads((destination / "recovery-audit.json").read_text()) == audit + assert str(tmp_path) not in json.dumps(audit) + assert not (destination / "registry.global.json").exists() + for path in [destination, destination / "data", destination / "recovery-audit.json"]: + assert path.stat().st_mode & 0o077 == 0 + before = (destination / "recovery-audit.json").read_bytes() + with pytest.raises(StateRecoveryError, match="recovery_destination_exists"): + verify_state_backup(archive=archive, manifest=manifest, destination=destination, execute=True) + assert (destination / "recovery-audit.json").read_bytes() == before + + +def test_valid_internal_symlink_stays_inert_and_chained_escape_is_refused(tmp_path): + archive, manifest = _backup(tmp_path) + link = tarfile.TarInfo("readable-link") + link.type, link.linkname = tarfile.SYMTYPE, "runtime-root/retained.txt" + _rewrite(archive, manifest, link) + result = verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "valid", execute=True) + assert result["execution_authority_granted"] is False + assert (tmp_path / "valid/data/readable-link").is_symlink() + alias = tarfile.TarInfo("alias") + alias.type, alias.linkname = tarfile.SYMTYPE, "." + _rewrite(archive, manifest, alias) + escape = tarfile.TarInfo("escape-link") + escape.type, escape.linkname = tarfile.SYMTYPE, "alias/../outside" + _rewrite(archive, manifest, escape) + with pytest.raises(StateRecoveryError, match="archive_link_escapes"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "invalid", execute=True) + assert not (tmp_path / "invalid").exists() + + +def test_capacity_and_private_input_fail_before_publication(tmp_path, monkeypatch): + import loopx.state_recovery as recovery + + archive, manifest = _backup(tmp_path) + original = archive.stat().st_mode + archive.chmod(0o644) + with pytest.raises(StateRecoveryError, match="input_not_private"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "invalid", execute=True) + archive.chmod(original) + monkeypatch.setattr(recovery, "MAX_MEMBERS", 1) + with pytest.raises(StateRecoveryError, match="capacity_exceeded"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "invalid", execute=True) + assert not (tmp_path / "invalid").exists() + + +def test_large_manifest_uses_existing_private_snapshot_transport(tmp_path): + archive, manifest = _backup(tmp_path) + external = json.loads(manifest.read_text()) + with tarfile.open(archive) as source: + embedded = json.load(source.extractfile("manifest.json")) + # Complete metadata exceeds the ordinary 2 MiB RPC; no wire ceiling is raised. + external["warnings"] = embedded["warnings"] = ["synthetic warning " * 150000] + manifest.write_text(json.dumps(external)) + _rewrite(archive, manifest, replacements={"manifest.json": json.dumps(embedded).encode()}) + result = verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "inert") + assert result["manifest_verified"] is True and not result["written"] + + +def test_changed_audit_and_true_execution_flag_are_not_trusted_on_readback(tmp_path): + archive, manifest = _backup(tmp_path) + destination = tmp_path / "inert" + result = verify_state_backup(archive=archive, manifest=manifest, destination=destination, execute=True) + path = destination / "recovery-audit.json" + changed = {**result, "observed_at": "2020-01-01T00:00:00.000Z"} + path.write_text(json.dumps(changed)) + with pytest.raises(StateRecoveryError, match="audit_digest_mismatch"): + read_state_recovery_audit(destination) + changed["execution_authority_granted"] = True + path.write_text(json.dumps(changed)) + with pytest.raises(StateRecoveryError, match="audit_invalid"): + read_state_recovery_audit(destination) + + +@pytest.mark.parametrize("provider", ["file", "sqlite"]) +def test_missing_selected_storage_is_unknown_without_creation_or_fallback(tmp_path, provider): + archive, manifest = _backup(tmp_path, provider) + destination = tmp_path / "inert" + original = verify_state_backup(archive=archive, manifest=manifest, destination=destination, execute=True) + from loopx.state_recovery import _audit_owners + + selected = destination / "data/runtime-root/authority" / f"{provider}-v0" + sqlite_files = [path.relative_to(destination / "data").as_posix() for path in selected.glob("*.sqlite")] + selected.rename(tmp_path / "unavailable-store") + observed = _audit_owners({"data_root": str(destination / "data"), + "verified": {key: original[key] for key in ("ok", "schema_version", "recovery_id", "backup", "file_count", + "file_witnesses_verified", "manifest_verified", "capture_profile", "execution_authority_granted")}, + "sqlite_members": sqlite_files, "standalone_sqlite_members": sqlite_files}) + [goal] = [row for row in observed["components"] if row["owner_kind"] == "coordination_authority"] + assert goal["state"] == "unknown" + assert observed["execution_authority_granted"] is False + assert not selected.exists() + if provider == "sqlite": + assert not (destination / "data/runtime-root/authority/file-v0").exists() + + +def test_restored_code_remains_unclassified_data_and_is_never_loaded(tmp_path): + archive, manifest = _backup(tmp_path) + # Re-capture through the real producer so the hostile file has valid witnesses. + marker = tmp_path / "executed" + (tmp_path / "runtime/untrusted_plugin.py").write_text(f"from pathlib import Path\nPath({str(marker)!r}).touch()\n") + result = execute_state_backup_plan(build_state_backup_plan(project=tmp_path / "project", runtime_root=tmp_path / "runtime", + registry_path=tmp_path / "registry.json", output_dir=tmp_path / "with-code", backup_id="code", + include_registry_projects=False, include_automations=False, include_skills=False)) + audit = verify_state_backup(archive=Path(result["archive_path"]), manifest=Path(result["manifest_path"]), + destination=tmp_path / "inert-code", execute=True) + assert not marker.exists() + [remaining] = [row for row in audit["components"] if row["component_id"] == "remaining_state"] + assert remaining["state"] == "unknown" and audit["execution_authority_granted"] is False + + +def test_sqlite_views_are_never_evaluated_as_authority_tables(tmp_path): + archive, manifest = _backup(tmp_path, "sqlite") + with tarfile.open(archive) as source: + database_member = next(row.name for row in source if row.name.endswith(".sqlite")) + database_bytes = source.extractfile(database_member).read() + embedded = json.load(source.extractfile("manifest.json")) + database_path = tmp_path / "view.sqlite" + database_path.write_bytes(database_bytes) + with sqlite3.connect(database_path) as database: + database.executescript("ALTER TABLE metadata RENAME TO saved_metadata; " + "CREATE VIEW metadata AS WITH RECURSIVE spin(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM spin) " + "SELECT *, (SELECT count(*) FROM spin) AS expensive FROM saved_metadata;") + changed = database_path.read_bytes() + external = json.loads(manifest.read_text()) + for envelope in (external, embedded): + witness = next(row for row in envelope["execution"]["file_members"] if row["archive_path"] == database_member) + witness.update(sha256=hashlib.sha256(changed).hexdigest(), size_bytes=len(changed)) + manifest.write_text(json.dumps(external)) + _rewrite(archive, manifest, replacements={database_member: changed, "manifest.json": json.dumps(embedded).encode()}) + result = verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "view-inert", execute=True) + [goal] = [row for row in result["components"] if row["owner_kind"] == "coordination_authority"] + assert goal["state"] == "unknown" + assert goal["reason_code"] == "sqlite_requires_owner_recovery" + + +def test_offline_worker_timeout_does_not_leave_a_copy_or_stop_shared_runtime(tmp_path, monkeypatch): + import loopx.state_recovery as recovery + from loopx.control_plane.effect_runtime import effect_runtime_result + + archive, manifest = _backup(tmp_path) + monkeypatch.setattr(recovery, "_OWNER_TIMEOUT_SECONDS", 0.001) + with pytest.raises(StateRecoveryError, match="recovery_owner_timeout"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "timed-out", execute=True) + assert not (tmp_path / "timed-out").exists() + assert not list(tmp_path.glob(".loopx-recovery-*")) + # This is still the real shared helper, not a fabricated availability result. + normal = effect_runtime_result("configuration.backup", {"action": "capture", "data": {"machine_configuration": None, "goals": []}}) + assert normal["activation_performed"] is False + + +def test_digest_refusal_precedes_tar_decoding_and_writes_nothing(tmp_path): + archive, manifest = _backup(tmp_path) + archive.write_bytes(b"not a tar archive") + with pytest.raises(StateRecoveryError, match="archive_digest_mismatch"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "inert", execute=True) + assert not (tmp_path / "inert").exists() + assert not list(tmp_path.glob(".loopx-recovery-*")) + + +def test_external_manifest_cannot_relabel_the_embedded_capture(tmp_path): + archive, manifest = _backup(tmp_path) + data = json.loads(manifest.read_text()) + data["backup_id"] = "different-backup" + manifest.write_text(json.dumps(data)) + with pytest.raises(StateRecoveryError, match="manifest_disagreement"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "inert", execute=True) + assert not (tmp_path / "inert").exists() + + +def test_recomputed_archive_digest_does_not_hide_member_corruption(tmp_path): + archive, manifest = _backup(tmp_path) + _rewrite(archive, manifest, replacements={"runtime-root/retained.txt": b"altered data"}) + with pytest.raises(StateRecoveryError, match="member_witness_mismatch"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "inert", execute=True) + + +@pytest.mark.parametrize("name,kind,target,reason", [ + ("../escape", tarfile.REGTYPE, "", "archive_path_invalid"), + ("/absolute", tarfile.REGTYPE, "", "archive_path_invalid"), + ("runtime-root/RETAINED.txt", tarfile.REGTYPE, "", "archive_name_collision"), + ("runtime-root/retained.txt", tarfile.REGTYPE, "", "archive_name_collision"), + ("fifo", tarfile.FIFOTYPE, "", "archive_member_type_unsupported"), + ("hardlink", tarfile.LNKTYPE, "runtime-root/retained.txt", "archive_member_type_unsupported"), + ("outside", tarfile.SYMTYPE, "../escape", "archive_link_escapes"), + ("runtime-root", tarfile.SYMTYPE, "project", "archive_name_collision"), +]) +def test_archive_paths_and_member_types_fail_before_destination_creation(tmp_path, name, kind, target, reason): + archive, manifest = _backup(tmp_path) + member = tarfile.TarInfo(name) + member.type, member.linkname = kind, target + _rewrite(archive, manifest, member) + with pytest.raises(StateRecoveryError, match=reason): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "inert", execute=True) + assert not (tmp_path / "inert").exists() + assert not (tmp_path.parent / "escape").exists() + + +def test_large_pax_metadata_is_refused_before_tarfile_decodes_its_body(tmp_path): + archive, manifest = _backup(tmp_path) + member = tarfile.TarInfo("extended-metadata") + member.pax_headers = {"comment": "x" * (128 * 1024 + 1)} + _rewrite(archive, manifest, member) + with pytest.raises(StateRecoveryError, match="archive_metadata_too_large"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "invalid", execute=True) + assert not (tmp_path / "invalid").exists() + + +def test_symlink_parent_and_occupied_directory_are_not_recovery_targets(tmp_path): + archive, manifest = _backup(tmp_path) + link = tmp_path / "alias" + link.symlink_to(tmp_path / "project", target_is_directory=True) + with pytest.raises(StateRecoveryError, match="parent_not_physical"): + verify_state_backup(archive=archive, manifest=manifest, destination=link / "new", execute=True) + occupied = tmp_path / "occupied" + occupied.mkdir() + (occupied / "keep").write_text("unchanged") + with pytest.raises(StateRecoveryError, match="destination_exists"): + verify_state_backup(archive=archive, manifest=manifest, destination=occupied, execute=True) + assert (occupied / "keep").read_text() == "unchanged" + + +def test_real_http_upload_verify_reload_and_corrupt_input_keep_live_state_unchanged(tmp_path): + from loopx.chat_server import ChatHTTPServer, ChatRequestHandler + + archive, manifest = _backup(tmp_path) + runtime = tmp_path / "replacement-runtime" + runtime.mkdir() + server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler) + server.runtime_root, server.registry_path, server.verbose = runtime, tmp_path / "absent-registry.json", False + worker = threading.Thread(target=server.serve_forever, daemon=True) + worker.start() + + def request(path, body=None, *, binary=False): + connection = http.client.HTTPConnection("127.0.0.1", server.server_address[1], timeout=90) + encoded = body if binary else json.dumps(body).encode() if body is not None else None + connection.request("POST" if body is not None else "GET", "/api/chat/state-recovery" + path, + body=encoded, headers={"Content-Type": "application/octet-stream" if binary else "application/json"}) + response = connection.getresponse() + payload = json.loads(response.read()) + status = response.status + connection.close() + return status, payload + + try: + assert request("")[1]["items"] == [] + assert not (runtime / "backups").exists() + _, first = request("/archive", archive.read_bytes(), binary=True) + _, second = request("/manifest", manifest.read_bytes(), binary=True) + inputs = {"archive_ref": first["input_ref"], "manifest_ref": second["input_ref"], "execute": True} + status, result = request("/verify", inputs) + assert status == 200, result + assert result["execution_authority_granted"] is False + assert not server.registry_path.exists() + status, rows = request("") + assert status == 200 and rows["items"][0]["checkpoint_ref"] == result["checkpoint_ref"] + read = request("/read", {"checkpoint_ref": result["checkpoint_ref"]})[1] + assert read["historical_readback"] is True and read["audit_sha256"] == result["audit_sha256"] + duplicate_status, duplicate = request("/verify", inputs) + assert duplicate_status == 400 and duplicate["error_code"] == "recovery_destination_exists" + checkpoint = runtime / "backups/state-recovery/checkpoints" / result["checkpoint_ref"] + before = (checkpoint / "recovery-audit.json").read_bytes() + _, damaged = request("/archive", b"damaged archive", binary=True) + status, rejected = request("/verify", {**inputs, "archive_ref": damaged["input_ref"]}) + assert status == 400 and rejected["error_code"] == "archive_digest_mismatch" + assert (checkpoint / "recovery-audit.json").read_bytes() == before + assert len(list((runtime / "backups/state-recovery/checkpoints").iterdir())) == 1 + assert not server.registry_path.exists() + finally: + server.shutdown() + worker.join(timeout=5) + server.server_close() From f1164455d12c596658ca1ed551d9f0b7384424fb Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 07:16:05 +0800 Subject: [PATCH 02/19] docs(recovery): describe offline inspection and retained owner holds Signed-off-by: song --- .../rfcs/complete-state-recovery-v0.md | 14 +++- .../rfcs/complete-state-recovery-v0.zh-CN.md | 11 ++- docs/reference/configuration-backup.md | 82 +++++++++++++++++++ docs/reference/glossary.md | 8 ++ 4 files changed, 113 insertions(+), 2 deletions(-) diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.md b/docs/architecture/rfcs/complete-state-recovery-v0.md index a3e23b57f7..a4be3504bf 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.md @@ -2,7 +2,7 @@ - **RFC status:** Accepted - **Supersedes / closes:** none -- **Delivery maturity:** Proposal; backup creation and configuration-only isolated recovery exist, complete state verification and reactivation do not +- **Delivery maturity:** Partial: bounded M1 full-archive verification and inert File/SQLite inspection implemented; controlled reactivation remains open - **Authors / owners:** Control-plane state, reliability, and affected domain maintainers - **Created:** 2026-10-08 - **Last normative revision:** 2026-10-08 @@ -24,6 +24,18 @@ transfer, or a default change. ## 1. Decision summary +### Current implementation checkpoint + +The bounded M1 path now has additive `backup-state verify`, loopback HTTP upload +and immutable report readback, and a packaged Settings/Capability Center journey. +It verifies real producer archives and reads captured configuration plus selected +File/SQLite Todo/history through existing owners after original sources become +unavailable. Every v0 audit remains held for incomplete capture/owner metadata; +unknown state is not relabeled recoverable, and execution authority is always +false. Limits, rollback and reproduction are in the [backup guide](../../reference/configuration-backup.md#inspect-a-complete-backup-without-activating-it). +This checkpoint does not qualify M2/M3, live adoption, remote providers or the G1 +team journey. No original identity or receipt is rewritten. + ### First supported user outcome The first bounded user promise is recovery of one local Goal after its source diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md index 712253f715..b3699465cc 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md @@ -2,7 +2,7 @@ - **RFC 状态:** 已接受 - **替代 / 关闭:** 无 -- **交付成熟度:** 提案;备份创建和仅配置的隔离恢复已存在,完整状态验证与重新激活尚不存在 +- **交付成熟度:** 部分实现:M1 完整归档验证与 File/SQLite 隔离检查已实现;受控重新启用仍开放 - **作者 / 负责人:** 控制面状态、可靠性及受影响领域的维护者 - **创建:** 2026-10-08 - **最近规范修订:** 2026-10-08 @@ -20,6 +20,15 @@ RFC 只授权有界实现切片,不授权真实恢复、provider 晋升、凭 ## 1. 决策摘要 +### 当前实现检查点 + +M1 的有界路径已包含增量 `backup-state verify`、loopback HTTP 上传及不可变报告读回, +以及打包设置/能力中心交互。真实生产归档在原来源不可达后,通过既有配置与选定的 +File/SQLite Todo/history owner 检查。v0 缺少完整捕获/owner 元数据,因此所有审计仍为 +held,未知状态不改称可恢复,执行权限始终为 false。限制与回退见[备份指南](../../reference/configuration-backup.md#inspect-a-complete-backup-without-activating-it)。 +本检查点不证明 M2/M3、live adoption、远端 provider 或 G1 团队验收,不重写原身份或回执。 + + ### 首个支持的用户结果 首个有界用户承诺是:一个本地 Goal 的来源工作区不可用或不再可信后,用户可以恢复 diff --git a/docs/reference/configuration-backup.md b/docs/reference/configuration-backup.md index e7568cf42c..cfbc64f09f 100644 --- a/docs/reference/configuration-backup.md +++ b/docs/reference/configuration-backup.md @@ -130,6 +130,88 @@ register it as a second live Goal or start a Host using an old grant. Explicit identity/path adoption, pending-effect disposition and authorized reactivation, including their App journey, remain separate acceptance work. +## Inspect a complete backup without activating it + +In **Settings → Capability Center → device defaults → Configuration backup and +recovery**, choose **Full archive inspection**. Select the original `.tar.gz` +and matching external `.manifest.json`, then choose **Verify and create isolated +copy**. The existing configuration-snapshot option remains separate. Full-archive +inspection is a machine-level operation; it is not shown in single-Goal scope. +Unsupported services/platforms keep the operation unavailable. + +The local server streams the two uploads into private temporary inputs. It checks +the archive digest before decoding tar data, then checks embedded/external +manifest agreement, paths, links, member hashes and extraction capacity. Only a +new inert checkpoint can be published. It reads configuration and selected +File/SQLite authority through their existing owners, including the complete Todo +read model and committed history. Original registry paths are never followed. + +The result separates **readable**, **historical only** and **unknown** components, +with an owner and one next action. SQLite bytes alone do not prove a readable Goal +work model. Remaining v0 state has no complete owner inventory, so it stays +unknown; every v0 audit carries `legacy_manifest_incomplete`, stays `held`, and +reports `execution_authority_granted=false` and `activation_eligible=false`. +No Goal is registered, no provider is selected, and no Host, lease, timer or +pending external operation is resumed. This is the M1 inspection path in the +[complete recovery RFC](../architecture/rfcs/complete-state-recovery-v0.md), +not controlled reactivation. + +Use **Previous inspections** after reloading to read the saved, digest-checked +report. A historical report describes its recorded observation time; reading it +again does not reverify copied data. A damaged new input clears the prior success +view and leaves existing checkpoints intact. If a response was lost, **Check for +an existing result** reads the original checkpoint; a repeated write to that +occupied target is refused. + +The additive CLI route preserves existing backup creation syntax: + +```bash +uv run --extra test loopx --format json backup-state verify \ + --archive BACKUP.tar.gz --manifest BACKUP.manifest.json \ + --destination NEW_PHYSICAL_DIRECTORY + +# Explicitly create and inspect that new inert copy. +uv run --extra test loopx --format json backup-state verify \ + --archive BACKUP.tar.gz --manifest BACKUP.manifest.json \ + --destination NEW_PHYSICAL_DIRECTORY --execute +``` + +Preview checks archive/manifest bytes and capacity; owner records are not yet +read and no zero-readability result is invented. Execution writes `data/` and +`recovery-audit.json` below the new destination. Source archive and manifest must +be private regular files; the destination must not exist and its ancestors must +be physical directories. Existing targets are never replaced. Browser callers +use opaque upload/checkpoint refs and cannot supply filesystem destinations. +Uploads expire after an hour and are cleaned by a later upload; completed audits +are retained. Removing these inert copies cannot undo or authorize live state. + +The first profile is POSIX with local File/SQLite state. Bounds are 128 MiB +compressed archive, 512 MiB expanded tar stream, 16 MiB external/embedded +manifest, 20,000 entries, expansion ratio 2,000, 128 KiB per extended tar header +and 16 MiB total extended metadata. Paths have at most 64 components, 1,024 +characters and 255 UTF-8 bytes per component; JSON nesting is bounded at 64. +Owner inspection permits 128 Goals, 128 SQLite files and 20,000 committed +transactions per Goal. Overflow refuses publication rather than truncating a +required inventory. Internal symlinks remain inert; escaping/cyclic links, +hard links, sparse or special files and normalized name collisions reject. + +Archive parsing never loads restored code. Database/domain inspection runs in a +disposable verifier with a 90-second deadline and a 512 MiB V8 heap ceiling. +SQLite gets a 64 MiB process-local [hard heap limit](https://www.sqlite.org/pragma.html#pragma_hard_heap_limit); +this is why that verifier is not dispatched in the shared Effect daemon. Views +and virtual tables cannot masquerade as authority tables. WAL-mode copies and +unsupported/service-owned providers remain unknown and need their existing +owner; PostgreSQL is never contacted. The normal 2 MiB RPC wire is unchanged; +only the two complete-manifest checks opt into the existing 64 MiB private-file +transport. These bounds qualify this inspection profile, not a production SLO. + +中文:在设置的能力中心选择设备范围,打开既有备份入口并切到“完整归档检查”, +选择同一份备份的归档与清单,一次明确操作建立隔离副本。可读、仅历史和未知分别 +显示;旧格式始终保留一致性/owner 缺口,不能据此启动 Goal。重载后可读历史报告, +但历史读回不等于重新验证副本。CLI 不带 `--execute` 只做计划,不虚构 owner 读取结果。 +当前支持本地 POSIX File/SQLite 检查;Windows、服务端 PostgreSQL、身份接纳和实际 +重新启用仍未取得此路径的资格。备份内可能包含私有数据和凭据,副本及上传始终私有。 + ## Privacy, consistency and limits Backups are **private by default**. Full Goal rows can contain private paths, diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index 3abbf0a74d..6e0ffd3307 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -184,6 +184,14 @@ Effect-program settlement step executed for one Turn. - typescript: [`SETTLEMENT_STEP_KINDS`](../../loopx/control_plane/effect_program.ts). - Values / 值: `validation`, `durable_writeback`, `quota_spend`, `terminal_closeout`. +## state_recovery_component_state + +Offline owner-local readability of a component in an inert recovery audit; never execution or adoption authority. + +- Tier / 层级: `cross_runtime`; status / 状态: `canonical`. +- typescript: [`RECOVERY_COMPONENT_STATES`](../../loopx/control_plane/state_recovery_contract.ts). +- Values / 值: `readable`, `historical_only`, `unknown`. + ## task_lease_lifecycle_operation Operation accepted by the shipped native lease lifecycle request decoder; acquisition has its own transaction. From 1bd828ba27e4d8947685c769bdae791c708f2041 Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 07:28:59 +0800 Subject: [PATCH 03/19] test(recovery): qualify installed File and SQLite inspection journeys Signed-off-by: song --- .../rfcs/loopx-overall-roadmap-v0.md | 13 ++++++++++--- .../rfcs/loopx-overall-roadmap-v0.zh-CN.md | 6 +++++- docs/reference/configuration-backup.md | 9 +++++++++ .../state-recovery/before-configuration.png | Bin 0 -> 132197 bytes .../state-recovery/historical-audit-phone.png | Bin 0 -> 60812 bytes .../state-recovery/inert-audit-desktop.png | Bin 0 -> 125930 bytes .../configuration-backup.mjs | 13 ++++++++----- 7 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 docs/reference/images/state-recovery/before-configuration.png create mode 100644 docs/reference/images/state-recovery/historical-audit-phone.png create mode 100644 docs/reference/images/state-recovery/inert-audit-desktop.png diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md index 9ac95f0b1c..957395f221 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.md @@ -1050,9 +1050,16 @@ bytes. Readback runs with the original project/runtime unreachable; missing selected-provider storage fails without replacement or File fallback. The pre-import backup separately retains original Markdown before later projection. This qualifies independent data/history recovery, not identity adoption, pending -effect disposition or live Host reactivation. Continue those boundaries and the -packaged App recovery journey through the existing owners. Do not retire the last -normal Markdown writer or claim the released SQLite default from this checkpoint. +effect disposition or live Host reactivation. The bounded complete-recovery M1 +inspection now adds `backup-state verify` and a packaged Settings journey: a new +inert copy, existing File/SQLite Todo/history readers, and explicit readable, +historical and unknown components. Corrupt inputs, occupied targets and unsafe +archive paths refuse; a bounded offline worker isolates database verification. +Source and installed-wheel journeys retain old Goal bytes and never register or +activate the copy. Legacy capture/owner gaps keep every v0 audit held. Continue +identity, pending-effect disposition and authorized adoption through the existing +owners; do not retire the last normal Markdown writer or claim the released +SQLite default from this inspection checkpoint. The qualified shadow→canonical archive→isolated File/SQLite restore path retains committed dependency archives, terminal lease facts and later canonical writes. diff --git a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md index 7a7ee368fb..a614fcba7c 100644 --- a/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md +++ b/docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md @@ -671,7 +671,11 @@ File/SQLite 完整 head、全部 committed journal、未引用的归档要求、 capture/rollback/已释放 lease 字节。回读前让原项目与 runtime 不可访问;选定 provider 存储缺失时明确失败,不新建替代库或回退 File。导入前备份另行保留投影前的原始 Markdown。这资格化独立数据与历史回读,不代表身份采用、pending effect 处置或 -Host 重新激活。继续既有 R5/T4 的这些边界和 packaged App 恢复旅程,通过后才退役 +Host 重新激活。有界的完整恢复 M1 检查现增加 `backup-state verify` 与打包设置旅程: +建立新隔离副本,复用 File/SQLite Todo/history reader,区分可读、仅历史与未知。 +损坏输入、已有目标及危险路径被拒绝,数据库验证在有时间/内存边界的独立进程内进行。 +源码与隔离 wheel 旅程保留原 Goal 字节,不注册或启用副本;v0 的 capture/owner 缺口使 +审计保持 held。继续既有 R5/T4 的身份、pending effect 与授权采用边界,通过后才退役 受影响旧 writer;D2 与发布默认值继续开放。详见 [全量状态恢复边界](../../reference/configuration-backup.md#full-state-recovery-after-cold-import)。 diff --git a/docs/reference/configuration-backup.md b/docs/reference/configuration-backup.md index cfbc64f09f..597a927c9e 100644 --- a/docs/reference/configuration-backup.md +++ b/docs/reference/configuration-backup.md @@ -156,6 +156,15 @@ pending external operation is resumed. This is the M1 inspection path in the [complete recovery RFC](../architecture/rfcs/complete-state-recovery-v0.md), not controlled reactivation. +The following public synthetic scene uses a built wheel and the real File +backend. Before this change, the entry only restored configuration snapshots. +The new path reads the saved Goal work after its original source was removed; +it does not register that Goal. The phone view is explicitly a historical report. + +| Before: configuration only | Inert full-archive inspection | Phone historical readback | +| --- | --- | --- | +| ![Configuration-only entry](images/state-recovery/before-configuration.png) | ![Inert backup audit](images/state-recovery/inert-audit-desktop.png) | ![Historical audit on phone](images/state-recovery/historical-audit-phone.png) | + Use **Previous inspections** after reloading to read the saved, digest-checked report. A historical report describes its recorded observation time; reading it again does not reverify copied data. A damaged new input clears the prior success diff --git a/docs/reference/images/state-recovery/before-configuration.png b/docs/reference/images/state-recovery/before-configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..3e958ae301750842bb9428f89330fc429a5f6af9 GIT binary patch literal 132197 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%0A~l?H=|o;fB!`| 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/state-recovery/historical-audit-phone.png b/docs/reference/images/state-recovery/historical-audit-phone.png new file mode 100644 index 0000000000000000000000000000000000000000..632b5186a6e6726f4b858353a6fec09aca709a85 GIT binary patch literal 60812 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/state-recovery/inert-audit-desktop.png b/docs/reference/images/state-recovery/inert-audit-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..fcfe6b6ffadd45b6b1b7cddd01a5c71e46c602a7 GIT binary patch literal 125930 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Ž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�M8|EsA0az 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 {ZEe+ejj(dW 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%0Afcm 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 document.documentElement.scrollWidth > window.innerWidth), false); await recovery.getByRole("status").scrollIntoViewIfNeeded(); await page.screenshot({path: resolve(outputDir, "state-recovery-mobile.png"), animations: "disabled"}); + await recovery.getByRole("status").evaluate(element => element.scrollIntoView({block: "start"})); + await page.screenshot({path: resolve(outputDir, "state-recovery-mobile-result.png"), animations: "disabled"}); await recovery.getByRole("button", {name: "历史检查结果", exact: true}).click(); await recovery.getByLabel("历史备份检查", {exact: true}).getByRole("button").click(); await page.setViewportSize({width: 1512, height: 982}); @@ -162,7 +165,7 @@ export const configurationBackupScenario = { await english.getByText("Historical inspection loaded; the copy has not been reverified.", {exact: true}).waitFor(); await english.getByRole("status").scrollIntoViewIfNeeded(); await page.screenshot({path: resolve(outputDir, "state-recovery-english.png"), animations: "disabled"}); - return {coverageEntries: context.coverageEntries, errors: context.errors, backend, verified: "Packaged configuration recovery and full archive inspection; real SQLite head/history after original-source loss, no Goal registration, keyboard, corrupt-input recovery, immutable history reload and narrow viewport"}; + return {coverageEntries: context.coverageEntries, errors: context.errors, backend, verified: `Packaged configuration recovery and full archive inspection; real ${backend.source_provider} head/history after original-source loss, no Goal registration, keyboard, corrupt-input recovery, immutable history reload and narrow viewport`}; } finally { try {await context?.close();} finally { if (!closed) { From 81cb60f5100d9551a6b5412afc7d63a03b1ef5d0 Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 07:35:21 +0800 Subject: [PATCH 04/19] docs(recovery): distinguish the design baseline from current inspection Signed-off-by: song --- docs/architecture/rfcs/complete-state-recovery-v0.md | 10 +++++----- .../rfcs/complete-state-recovery-v0.zh-CN.md | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.md b/docs/architecture/rfcs/complete-state-recovery-v0.md index a4be3504bf..d3d84ea6b7 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.md @@ -207,11 +207,11 @@ Other files and directories are traversed while the source may remain live. The command therefore has component-local SQLite snapshot semantics and best-effort file capture, not a cross-owner point-in-time guarantee. -`loopx backup-state` supports plan and create. It does not verify an existing -archive, safely materialize one, classify state owners, reconcile references, -or reactivate a runtime. Current tests extract with the Python tar API, validate -SQLite integrity and content, and verify publication/permission behavior. They -do not establish a restore contract. +At the design baseline, `loopx backup-state` supported plan/create only, and +tests manually extracted archives. The current bounded M1 route in Section 1 +adds verified inert extraction and existing-owner readback through CLI and the +packaged UI. It does not adopt identity, reconcile pending effects or reactivate +a runtime; those later owner boundaries remain open. Configuration backup separately supports export, verify, and isolated restore. Its checkpoint is inert: it creates no live registry, provider selection, diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md index b3699465cc..f6116a7ef1 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md @@ -172,10 +172,10 @@ digest 和大小,并排除 WAL/SHM sidecar。其他文件和目录可能在来 因此,该命令只具备组件本地 SQLite snapshot 语义和 best-effort 文件捕获,不具备 跨 owner point-in-time 保证。 -`loopx backup-state` 支持 plan 和 create。它不能验证已有归档、安全物化归档、 -分类状态 owner、reconcile reference 或重新激活 runtime。当前测试用 Python tar -API 提取,验证 SQLite integrity 与内容,并检查发布/权限行为;它们没有建立 -restore 契约。 +设计基线中的 `loopx backup-state` 只有 plan/create,测试通过 Python tar API 手工解包。 +第 1 节记录的当前有界 M1 路径已通过 CLI 和打包 UI 提供验证后的隔离解包及既有 owner +读回。它不采用身份、不处置 pending effect、不重新激活 runtime;这些后续 owner 边界 +仍保持开放。 配置备份另行支持 export、verify 和 isolated restore。其 checkpoint 是惰性的: 它不会创建 live registry、provider selection、writer fence、lease、Host session、 From 0078cc23073bd30e5da5ec8dd5ae0444750fb2bf Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 07:50:52 +0800 Subject: [PATCH 05/19] fix(recovery): reject sparse decoding and preserve failure cleanup Signed-off-by: song --- loopx/state_recovery.py | 29 +++++++++++---- tests/test_state_recovery.py | 70 ++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/loopx/state_recovery.py b/loopx/state_recovery.py index b006e5635a..20d63b307e 100644 --- a/loopx/state_recovery.py +++ b/loopx/state_recovery.py @@ -20,7 +20,7 @@ import tempfile import unicodedata import zlib -from typing import Any, BinaryIO, Iterator +from typing import Any, BinaryIO, Iterator, NoReturn from .control_plane.effect_runtime import _node_executable, effect_runtime_result, effect_runtime_request_scope @@ -143,9 +143,20 @@ def read(self, size: int = -1) -> bytes: class _RecoveryTarInfo(tarfile.TarInfo): + def _reject_sparse(self, *_args: object) -> NoReturn: + raise StateRecoveryError("archive_sparse_unsupported") + + # The stdlib invokes these hooks before yielding the final member. Refuse + # unsupported extents before their metadata/body is decoded or expanded. + _proc_gnusparse_00 = _reject_sparse + _proc_gnusparse_01 = _reject_sparse + _proc_gnusparse_10 = _reject_sparse + def _proc_member(self, archive: tarfile.TarFile) -> tarfile.TarInfo | None: + _require(self.type != tarfile.GNUTYPE_SPARSE, "archive_sparse_unsupported") # Bound extended metadata before tarfile allocates/decodes its body. - if self.type in (tarfile.XHDTYPE, tarfile.XGLTYPE, tarfile.GNUTYPE_LONGNAME, tarfile.GNUTYPE_LONGLINK): + if self.type in (tarfile.XHDTYPE, tarfile.XGLTYPE, tarfile.SOLARIS_XHDTYPE, + tarfile.GNUTYPE_LONGNAME, tarfile.GNUTYPE_LONGLINK): _require(self.size <= 128 * 1024, "archive_metadata_too_large") if isinstance(archive, _RecoveryTarFile): archive.recovery_metadata_bytes += self.size @@ -307,7 +318,10 @@ def _scan(handle: BinaryIO, archive_size: int, output: Path | None = None) -> di # Consume gzip EOF too: truncated CRC/trailing expansion is not a valid tar. while stream.read(_CHUNK): pass - except (tarfile.TarError, EOFError, gzip.BadGzipFile, OSError, zlib.error) as exc: + except StateRecoveryError: + raise + except (tarfile.TarError, EOFError, gzip.BadGzipFile, OSError, zlib.error, + ValueError, OverflowError, KeyError, RecursionError) as exc: raise StateRecoveryError("archive_unreadable") from exc finally: stream.source.close() @@ -383,9 +397,12 @@ def verify_state_backup( reserved = False return read_state_recovery_audit(destination) finally: - if reserved: - destination.rmdir() - shutil.rmtree(staging, ignore_errors=True) + try: + if reserved: + # Never recursively remove a destination occupied meanwhile. + destination.rmdir() + finally: + shutil.rmtree(staging, ignore_errors=True) def read_state_recovery_audit(destination: Path) -> dict[str, Any]: diff --git a/tests/test_state_recovery.py b/tests/test_state_recovery.py index 49926c3d79..53c01db42f 100644 --- a/tests/test_state_recovery.py +++ b/tests/test_state_recovery.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import gzip import io import http.client import json @@ -300,6 +301,75 @@ def test_large_pax_metadata_is_refused_before_tarfile_decodes_its_body(tmp_path) assert not (tmp_path / "invalid").exists() +@pytest.mark.parametrize("kind,headers,decoder", [ + (tarfile.GNUTYPE_SPARSE, {}, "_proc_sparse"), + (tarfile.REGTYPE, {"GNU.sparse.size": "invalid"}, "_proc_gnusparse_00"), + (tarfile.REGTYPE, {"GNU.sparse.map": "invalid"}, "_proc_gnusparse_01"), + (tarfile.REGTYPE, {"GNU.sparse.major": "1", "GNU.sparse.minor": "0"}, "_proc_gnusparse_10"), +]) +def test_sparse_records_are_refused_before_stdlib_extent_decoding(tmp_path, monkeypatch, kind, headers, decoder): + archive, manifest = _backup(tmp_path) + member = tarfile.TarInfo("unsupported-sparse") + member.type, member.pax_headers = kind, headers + _rewrite(archive, manifest, member) + + # Malformed sparse metadata must produce the same bounded refusal as a + # valid unsupported record, rather than leaking a decoder exception. + with pytest.raises(StateRecoveryError, match="^archive_sparse_unsupported$"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "inert", execute=True) + + def unexpected_decode(*_args): + pytest.fail("unsupported sparse extents reached the stdlib decoder") + + monkeypatch.setattr(tarfile.TarInfo, decoder, unexpected_decode) + with pytest.raises(StateRecoveryError, match="^archive_sparse_unsupported$"): + verify_state_backup(archive=archive, manifest=manifest, destination=tmp_path / "inert", execute=True) + assert not (tmp_path / "inert").exists() + assert not list(tmp_path.glob(".loopx-recovery-*")) + + +@pytest.mark.parametrize("kind", [tarfile.XHDTYPE, tarfile.XGLTYPE, tarfile.SOLARIS_XHDTYPE]) +def test_oversized_pax_variants_are_refused_without_reading_the_body(kind): + from loopx.state_recovery import _scan + + member = tarfile.TarInfo("oversized-pax") + member.type, member.size = kind, 128 * 1024 + 1 + compressed = gzip.compress(member.tobuf()) + with pytest.raises(StateRecoveryError, match="^archive_metadata_too_large$"): + _scan(io.BytesIO(compressed), len(compressed)) + + +def test_malformed_pax_record_length_is_a_bounded_archive_error(): + from loopx.state_recovery import _scan + + payload = b"9" * 5000 + b" comment=invalid\n" + member = tarfile.TarInfo("malformed-pax") + member.type, member.size = tarfile.XHDTYPE, len(payload) + raw = member.tobuf() + payload + b"\0" * (-len(payload) % 512 + 1024) + compressed = gzip.compress(raw) + with pytest.raises(StateRecoveryError, match="^archive_unreadable$"): + _scan(io.BytesIO(compressed), len(compressed)) + + +def test_failed_publication_cleans_staging_even_when_destination_becomes_occupied(tmp_path, monkeypatch): + import loopx.state_recovery as recovery + + archive, manifest = _backup(tmp_path) + destination = tmp_path / "inert" + + def failed_rename(source, target): + assert target == destination and (source / "recovery-audit.json").is_file() + (destination / "foreign-file").write_text("preserve this") + raise OSError("publication interrupted") + + monkeypatch.setattr(recovery.os, "rename", failed_rename) + with pytest.raises(OSError): + verify_state_backup(archive=archive, manifest=manifest, destination=destination, execute=True) + assert (destination / "foreign-file").read_text() == "preserve this" + assert not (destination / "recovery-audit.json").exists() + assert not list(tmp_path.glob(".loopx-recovery-*")) + + def test_symlink_parent_and_occupied_directory_are_not_recovery_targets(tmp_path): archive, manifest = _backup(tmp_path) link = tmp_path / "alias" From f5a8b76dd31d34dac43bccca9fd4ce35747724a1 Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 07:50:52 +0800 Subject: [PATCH 06/19] docs(recovery): retain the operational qualification boundary Signed-off-by: song --- docs/architecture/rfcs/complete-state-recovery-v0.md | 3 +++ docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md | 2 ++ docs/reference/configuration-backup.md | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.md b/docs/architecture/rfcs/complete-state-recovery-v0.md index d3d84ea6b7..5974767845 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.md @@ -33,6 +33,9 @@ File/SQLite Todo/history through existing owners after original sources become unavailable. Every v0 audit remains held for incomplete capture/owner metadata; unknown state is not relabeled recoverable, and execution authority is always false. Limits, rollback and reproduction are in the [backup guide](../../reference/configuration-backup.md#inspect-a-complete-backup-without-activating-it). +M1 operational qualification remains partial: per-audit verification duration +and data-size reporting, and measurements across the full capacity profile, +still belong to the recovery owner under Section 10. This checkpoint does not qualify M2/M3, live adoption, remote providers or the G1 team journey. No original identity or receipt is rewritten. diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md index f6116a7ef1..e5d95351ed 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md @@ -26,6 +26,8 @@ M1 的有界路径已包含增量 `backup-state verify`、loopback HTTP 上传 以及打包设置/能力中心交互。真实生产归档在原来源不可达后,通过既有配置与选定的 File/SQLite Todo/history owner 检查。v0 缺少完整捕获/owner 元数据,因此所有审计仍为 held,未知状态不改称可恢复,执行权限始终为 false。限制与回退见[备份指南](../../reference/configuration-backup.md#inspect-a-complete-backup-without-activating-it)。 +M1 的运行资格仍不完整:逐份审计的验证耗时和数据大小报告、完整容量范围测量, +继续由第 10 节的 recovery owner 承接。 本检查点不证明 M2/M3、live adoption、远端 provider 或 G1 团队验收,不重写原身份或回执。 diff --git a/docs/reference/configuration-backup.md b/docs/reference/configuration-backup.md index 597a927c9e..4f8fc0b46b 100644 --- a/docs/reference/configuration-backup.md +++ b/docs/reference/configuration-backup.md @@ -212,7 +212,9 @@ and virtual tables cannot masquerade as authority tables. WAL-mode copies and unsupported/service-owned providers remain unknown and need their existing owner; PostgreSQL is never contacted. The normal 2 MiB RPC wire is unchanged; only the two complete-manifest checks opt into the existing 64 MiB private-file -transport. These bounds qualify this inspection profile, not a production SLO. +transport. These bounds define this inspection profile; full-capacity +qualification and per-audit duration/data-size reporting remain open under the +recovery RFC's operational contract. No production SLO is claimed. 中文:在设置的能力中心选择设备范围,打开既有备份入口并切到“完整归档检查”, 选择同一份备份的归档与清单,一次明确操作建立隔离副本。可读、仅历史和未知分别 From f72de6facea12fca642e8eb95643b1d1d61f5f99 Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 08:05:38 +0800 Subject: [PATCH 07/19] feat(recovery): retain observed size and verification phase durations Signed-off-by: song --- .../state-recovery-settings.tsx | 3 +++ .../configuration-backup.mjs | 8 ++++++++ loopx/control_plane/state_recovery.ts | 6 +++++- loopx/control_plane/state_recovery_contract.ts | 17 +++++++++++++++++ loopx/state_recovery.py | 15 +++++++++++++-- tests/control_plane_ts/state_recovery.test.ts | 12 ++++++++++++ tests/test_state_recovery.py | 11 +++++++++++ 7 files changed, 69 insertions(+), 3 deletions(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx index 677ece6b37..a14b2dc852 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx @@ -107,6 +107,9 @@ export function StateRecoverySettings() {

{nextActions[row.next_action]?.[index] ?? (zh ? "请对应 owner 核对。" : "Ask the owning module to review this component.")}

)}
{zh ? "核对信息" : "Verification details"}{audit.recovery_id} + {audit.measurements ?

{zh ? "归档字节" : "Archive bytes"}: {audit.measurements.archive_bytes.toLocaleString(locale)} · {zh ? "普通文件字节" : "Regular file bytes"}: {audit.measurements.regular_file_bytes.toLocaleString(locale)}
+ {zh ? "归档检查与复制" : "Archive checks and copy"}: {audit.measurements.archive_processing_ms.toLocaleString(locale)} ms · {zh ? "记录读取" : "Owner reads"}: {audit.measurements.owner_read_ms.toLocaleString(locale)} ms

+ :

{zh ? "此历史报告未记录数据量和阶段耗时。" : "This historical report has no recorded size or phase durations."}

}
    {audit.components.map(row =>
  • {row.component_id}: {row.reason_code}
  • )}
: null} diff --git a/examples/personal-workspace-browser/configuration-backup.mjs b/examples/personal-workspace-browser/configuration-backup.mjs index d9c431bf88..a40e8b8014 100644 --- a/examples/personal-workspace-browser/configuration-backup.mjs +++ b/examples/personal-workspace-browser/configuration-backup.mjs @@ -122,6 +122,11 @@ export const configurationBackupScenario = { await recovery.getByText("隔离副本已核对;Goal 尚未启用。", {exact: true}).waitFor(); await recovery.getByText("recovery-fixture", {exact: false}).first().waitFor(); await recovery.getByText("保存的任务: 1", {exact: false}).waitFor(); + const verificationDetails = recovery.locator("details"); + await verificationDetails.locator("summary").click(); + await verificationDetails.getByText("归档字节:", {exact: false}).waitFor(); + const recordedMeasurements = await verificationDetails.locator("p").innerText(); + await verificationDetails.locator("summary").click(); assert.equal(backend.original_sources_absent, true); const registryAfter = JSON.parse(await readFile(join(root, "registry.json"), "utf8")); assert.deepEqual(registryAfter.goals.map(goal => goal.id), ["fixture"], "inspection must not register the restored Goal"); @@ -154,6 +159,9 @@ export const configurationBackupScenario = { await reopened.getByRole("button", {name: "历史检查结果", exact: true}).click(); await reopened.getByLabel("历史备份检查", {exact: true}).getByRole("button").click(); await reopened.getByText("已读取历史检查记录;尚未重新验证副本。", {exact: true}).waitFor(); + await reopened.locator(".personal-recovery-result details summary").click(); + assert.equal(await reopened.locator(".personal-recovery-result details p").innerText(), recordedMeasurements, + "historical readback must retain original measured values, not time the reload"); await page.getByRole("button", {name: "语言", exact: true}).click(); await page.getByRole("radio", {name: "English", exact: true}).check(); await page.locator(".personal-settings-tabs").getByRole("button", {name: /Capabilit/}).click(); diff --git a/loopx/control_plane/state_recovery.ts b/loopx/control_plane/state_recovery.ts index 468ce4351f..b089d5db80 100644 --- a/loopx/control_plane/state_recovery.ts +++ b/loopx/control_plane/state_recovery.ts @@ -12,7 +12,7 @@ import {decodeLocalAuthoritySelection, localAuthorityProviderPaths, import {sqliteAuthorityRuntime} from "./coordination/sqlite_runtime.ts"; import {sqliteAuthorityPath} from "./coordination/sqlite_authority_store.ts"; import {canonicalTodoCollection} from "./coordination/local_authority_read.ts"; -import {STATE_RECOVERY_AUDIT_SCHEMA, decodeStateRecoveryAudit, isRecoveryDigest, recoveryGoalLabel, +import {STATE_RECOVERY_AUDIT_SCHEMA, decodeRecoveryMeasurements, decodeStateRecoveryAudit, isRecoveryDigest, recoveryGoalLabel, type RecoveryComponent, type RecoveryComponentState} from "./state_recovery_contract.ts"; const MAX_GOALS = 128; @@ -182,7 +182,10 @@ async function inspectGoal(root: string, goalId: string, standaloneSqlite: Set { + const started = performance.now(); const request = requireJsonObject(value, "recovery owner audit"); + const measurements = decodeRecoveryMeasurements({ + ...requireJsonObject(request.measurements, "host recovery measurements"), owner_read_ms: 0}); const basis = requireJsonObject(request.verified, "verified recovery basis"); check(basis.ok === true && basis.manifest_verified === true && basis.schema_version === STATE_RECOVERY_AUDIT_SCHEMA, "recovery_basis_invalid"); @@ -254,6 +257,7 @@ async function auditOwners(value: unknown): Promise { components.push(component("remaining_state", "recovery", "unknown", "legacy_owner_inventory_incomplete", "classify_remaining_owner_state")); const audit = {...basis, status: "held", written: true, observed_at: new Date().toISOString(), + measurements: {...measurements, owner_read_ms: Math.floor(performance.now() - started)}, workspace: {isolation: "inert", registered: false}, components, findings: [{reason_code: "legacy_manifest_incomplete", owner_kind: "backup_capture", next_action: "review_capture_consistency"}], diff --git a/loopx/control_plane/state_recovery_contract.ts b/loopx/control_plane/state_recovery_contract.ts index 776c2c47c2..3e82d168ed 100644 --- a/loopx/control_plane/state_recovery_contract.ts +++ b/loopx/control_plane/state_recovery_contract.ts @@ -5,6 +5,12 @@ import {requireJsonObject, type JsonObject} from "./runtime_decode.ts"; export const STATE_RECOVERY_AUDIT_SCHEMA = "loopx_state_recovery_audit_v0"; export const RECOVERY_COMPONENT_STATES = ["readable", "historical_only", "unknown"] as const; export type RecoveryComponentState = typeof RECOVERY_COMPONENT_STATES[number]; +export interface RecoveryMeasurements extends JsonObject { + archive_bytes: number; + regular_file_bytes: number; + archive_processing_ms: number; + owner_read_ms: number; +} export interface RecoveryComponent extends JsonObject { component_id: string; owner_kind: string; @@ -25,6 +31,7 @@ export interface StateRecoveryAudit extends JsonObject { components: RecoveryComponent[]; findings: Array<{reason_code: string; owner_kind: string; next_action: string}>; summary: Record; + measurements?: RecoveryMeasurements; } function requireValue(condition: unknown): asserts condition { if (!condition) throw new EffectRuntimeRequestError("State recovery audit contract is invalid", "recovery_audit_invalid"); @@ -39,6 +46,13 @@ export function isRecoveryDigest(value: unknown): value is string { export function recoveryGoalLabel(value: string): string | null { return /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value) ? value : null; } +export function decodeRecoveryMeasurements(value: unknown): RecoveryMeasurements { + const measurements = requireJsonObject(value, "recovery measurements"); + for (const field of ["archive_bytes", "regular_file_bytes", "archive_processing_ms", "owner_read_ms"]) { + requireValue(Number.isSafeInteger(measurements[field]) && Number(measurements[field]) >= 0); + } + return measurements as RecoveryMeasurements; +} export function decodeStateRecoveryAudit(value: unknown): StateRecoveryAudit { const audit = requireJsonObject(value, "state recovery audit"); requireValue(audit.ok === true && audit.schema_version === STATE_RECOVERY_AUDIT_SCHEMA @@ -49,6 +63,9 @@ export function decodeStateRecoveryAudit(value: unknown): StateRecoveryAudit { && Number.isFinite(Date.parse(audit.observed_at))); const workspace = requireJsonObject(audit.workspace, "inert recovery workspace"); requireValue(workspace.isolation === "inert" && workspace.registered === false); + // Earlier reports remain historical observations; absent measurements are + // unknown, never synthesized from their timestamp or today's runtime. + if (audit.measurements !== undefined) decodeRecoveryMeasurements(audit.measurements); const backup = requireJsonObject(audit.backup, "recovery backup basis"); requireValue(backup.schema_version === "loopx_state_backup_v0" && isRecoveryDigest(backup.archive_sha256) && isRecoveryDigest(backup.external_manifest_sha256)); diff --git a/loopx/state_recovery.py b/loopx/state_recovery.py index 20d63b307e..8afa6fc959 100644 --- a/loopx/state_recovery.py +++ b/loopx/state_recovery.py @@ -18,6 +18,7 @@ import subprocess import tarfile import tempfile +import time import unicodedata import zlib from typing import Any, BinaryIO, Iterator, NoReturn @@ -342,6 +343,7 @@ def verify_state_backup( *, archive: Path, manifest: Path, destination: Path, execute: bool = False, ) -> dict[str, Any]: """Preview or publish one inert audit. This API has no live-adoption operation.""" + started = time.monotonic_ns() _require(os.name == "posix", "recovery_platform_unqualified") _physical_parent(destination) with _regular_input(manifest, MAX_MANIFEST_BYTES) as manifest_handle: @@ -365,6 +367,7 @@ def verify_state_backup( if not execute: return {**verified, "status": "planned", "written": False, "logical_bytes": observation["logical_bytes"], "entry_count": observation["entry_count"], + "archive_processing_ms": (time.monotonic_ns() - started) // 1_000_000, "activation_eligible": False} staging = Path(tempfile.mkdtemp(prefix=".loopx-recovery-", dir=destination.parent)) reserved = False @@ -375,6 +378,9 @@ def verify_state_backup( _require(copied == observation and _sha(archive_handle) == (archive_sha, archive_size), "recovery_input_changed") audit = _audit_owners({"data_root": str(data), "verified": verified, + "measurements": {"archive_bytes": archive_size, + "regular_file_bytes": copied["logical_bytes"], + "archive_processing_ms": (time.monotonic_ns() - started) // 1_000_000}, "sqlite_members": observation["sqlite_members"], "standalone_sqlite_members": observation["standalone_sqlite_members"]}) audit = _runtime("state.recovery.audit.readback", audit) @@ -423,9 +429,14 @@ def render_state_recovery_markdown(payload: dict[str, Any]) -> str: "Repeat with --execute to create and inspect a new inert copy; this never activates a Goal.", ]) summary = payload.get("summary", {}) - return "\n".join([ + lines = [ f"Backup inspection: {payload['status']}", f"Recovery: {payload['recovery_id']}", f"Readable: {summary.get('readable', 0)}; historical only: {summary.get('historical_only', 0)}; unknown: {summary.get('unknown', 0)}", "The copy is inert. Goal execution and live settings were not activated.", - ]) + ] + measurements = payload.get("measurements") + if isinstance(measurements, dict): + lines.append(f"Archive: {measurements['archive_bytes']} bytes; regular files: {measurements['regular_file_bytes']} bytes; " + f"archive checks/copy: {measurements['archive_processing_ms']} ms; owner reads: {measurements['owner_read_ms']} ms.") + return "\n".join(lines) diff --git a/tests/control_plane_ts/state_recovery.test.ts b/tests/control_plane_ts/state_recovery.test.ts index bd2eb40d66..f8ecc23265 100644 --- a/tests/control_plane_ts/state_recovery.test.ts +++ b/tests/control_plane_ts/state_recovery.test.ts @@ -73,3 +73,15 @@ test("untrusted owner inspection has no shared-daemon dispatch", async () => { const handlers = createEffectRuntimeHandlers({fingerprint: "recovery-isolation", requestShutdown() {}}); await assert.rejects(() => dispatchEffectRuntimeMethod(handlers, "state.recovery.owners.audit", {}), /unsupported Effect runtime method/); }); + +test("recorded measurements reject invalid values and absent historical measurements remain absent", () => { + const input = audit(); + const measurements = {archive_bytes: 500, regular_file_bytes: 256, archive_processing_ms: 3, owner_read_ms: 2}; + assert.equal(decodeStateRecoveryAudit(input).measurements, undefined); + assert.deepEqual(decodeStateRecoveryAudit({...input, measurements}).measurements, measurements); + for (const field of Object.keys(measurements)) { + for (const value of [-1, 1.5, "3", null, Number.NaN, Number.MAX_SAFE_INTEGER + 1]) { + assert.throws(() => decodeStateRecoveryAudit({...input, measurements: {...measurements, [field]: value}})); + } + } +}); diff --git a/tests/test_state_recovery.py b/tests/test_state_recovery.py index 53c01db42f..2aaa57749b 100644 --- a/tests/test_state_recovery.py +++ b/tests/test_state_recovery.py @@ -85,6 +85,11 @@ def test_real_backup_recovers_readable_authority_after_original_sources_disappea assert audit["execution_authority_granted"] is False and audit["activation_eligible"] is False assert audit["workspace"] == {"isolation": "inert", "registered": False} assert audit["status"] == "held" + assert audit["measurements"]["archive_bytes"] == archive.stat().st_size + assert audit["measurements"]["regular_file_bytes"] == preview["logical_bytes"] + assert all(type(value) is int and value >= 0 for value in audit["measurements"].values()) + assert read_state_recovery_audit(destination)["measurements"] == audit["measurements"] + assert "archive checks/copy:" in render_state_recovery_markdown(audit) [goal] = [row for row in audit["components"] if row["owner_kind"] == "coordination_authority"] assert goal["state"] == "readable", goal assert goal["facts"]["todo_count"] == 1 @@ -157,6 +162,11 @@ def test_changed_audit_and_true_execution_flag_are_not_trusted_on_readback(tmp_p destination = tmp_path / "inert" result = verify_state_backup(archive=archive, manifest=manifest, destination=destination, execute=True) path = destination / "recovery-audit.json" + altered_measurement = {**result, "measurements": {**result["measurements"], + "owner_read_ms": result["measurements"]["owner_read_ms"] + 1}} + path.write_text(json.dumps(altered_measurement)) + with pytest.raises(StateRecoveryError, match="audit_digest_mismatch"): + read_state_recovery_audit(destination) changed = {**result, "observed_at": "2020-01-01T00:00:00.000Z"} path.write_text(json.dumps(changed)) with pytest.raises(StateRecoveryError, match="audit_digest_mismatch"): @@ -178,6 +188,7 @@ def test_missing_selected_storage_is_unknown_without_creation_or_fallback(tmp_pa sqlite_files = [path.relative_to(destination / "data").as_posix() for path in selected.glob("*.sqlite")] selected.rename(tmp_path / "unavailable-store") observed = _audit_owners({"data_root": str(destination / "data"), + "measurements": original["measurements"], "verified": {key: original[key] for key in ("ok", "schema_version", "recovery_id", "backup", "file_count", "file_witnesses_verified", "manifest_verified", "capture_profile", "execution_authority_granted")}, "sqlite_members": sqlite_files, "standalone_sqlite_members": sqlite_files}) From cd764786e0c85e836398e1dde27370a5885e952e Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 08:05:38 +0800 Subject: [PATCH 08/19] docs(recovery): define observed cost and historical measurement limits Signed-off-by: song --- .../architecture/rfcs/complete-state-recovery-v0.md | 5 +++-- .../rfcs/complete-state-recovery-v0.zh-CN.md | 4 ++-- docs/reference/configuration-backup.md | 13 +++++++++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.md b/docs/architecture/rfcs/complete-state-recovery-v0.md index 5974767845..00bee22b7f 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.md @@ -33,8 +33,9 @@ File/SQLite Todo/history through existing owners after original sources become unavailable. Every v0 audit remains held for incomplete capture/owner metadata; unknown state is not relabeled recoverable, and execution authority is always false. Limits, rollback and reproduction are in the [backup guide](../../reference/configuration-backup.md#inspect-a-complete-backup-without-activating-it). -M1 operational qualification remains partial: per-audit verification duration -and data-size reporting, and measurements across the full capacity profile, +Reports retain measured archive/regular-file bytes and archive-processing and +owner-read durations; historical readback does not retime them. M1 operational +qualification remains partial: measurements across the full capacity profile still belong to the recovery owner under Section 10. This checkpoint does not qualify M2/M3, live adoption, remote providers or the G1 team journey. No original identity or receipt is rewritten. diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md index e5d95351ed..2f2e9d9af5 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md @@ -26,8 +26,8 @@ M1 的有界路径已包含增量 `backup-state verify`、loopback HTTP 上传 以及打包设置/能力中心交互。真实生产归档在原来源不可达后,通过既有配置与选定的 File/SQLite Todo/history owner 检查。v0 缺少完整捕获/owner 元数据,因此所有审计仍为 held,未知状态不改称可恢复,执行权限始终为 false。限制与回退见[备份指南](../../reference/configuration-backup.md#inspect-a-complete-backup-without-activating-it)。 -M1 的运行资格仍不完整:逐份审计的验证耗时和数据大小报告、完整容量范围测量, -继续由第 10 节的 recovery owner 承接。 +报告保留实际归档/普通文件字节数及归档处理、owner 读取耗时;历史读回不重新计时。 +M1 的运行资格仍不完整:完整容量范围测量继续由第 10 节的 recovery owner 承接。 本检查点不证明 M2/M3、live adoption、远端 provider 或 G1 团队验收,不重写原身份或回执。 diff --git a/docs/reference/configuration-backup.md b/docs/reference/configuration-backup.md index 4f8fc0b46b..72675ec0a6 100644 --- a/docs/reference/configuration-backup.md +++ b/docs/reference/configuration-backup.md @@ -172,6 +172,15 @@ view and leaves existing checkpoints intact. If a response was lost, **Check for an existing result** reads the original checkpoint; a repeated write to that occupied target is refused. +**Verification details** retain the archive byte size, copied regular-file +bytes (including the embedded manifest), and two measured phases. Archive +processing covers input checks, both scans and copying; owner reads cover the +isolated configuration/storage audit. Durations use monotonic clocks and exclude +HTTP upload, verifier process startup, the final source rehash/report publication, +and historical readback. They describe processing cost, not end-to-end recovery +time or a deadline guarantee. Earlier reports without measurements show that +absence instead of zero. Reading history preserves the original measurements. + The additive CLI route preserves existing backup creation syntax: ```bash @@ -213,8 +222,8 @@ unsupported/service-owned providers remain unknown and need their existing owner; PostgreSQL is never contacted. The normal 2 MiB RPC wire is unchanged; only the two complete-manifest checks opt into the existing 64 MiB private-file transport. These bounds define this inspection profile; full-capacity -qualification and per-audit duration/data-size reporting remain open under the -recovery RFC's operational contract. No production SLO is claimed. +qualification remains open under the recovery RFC's operational contract. +No production SLO is claimed. 中文:在设置的能力中心选择设备范围,打开既有备份入口并切到“完整归档检查”, 选择同一份备份的归档与清单,一次明确操作建立隔离副本。可读、仅历史和未知分别 From 1f45a9819ab41d05eaff9a375760c1526393a2e1 Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 08:06:10 +0800 Subject: [PATCH 09/19] test(recovery): capture rendered verification measurements Signed-off-by: song --- examples/personal-workspace-browser/configuration-backup.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/personal-workspace-browser/configuration-backup.mjs b/examples/personal-workspace-browser/configuration-backup.mjs index a40e8b8014..3db34ccc3b 100644 --- a/examples/personal-workspace-browser/configuration-backup.mjs +++ b/examples/personal-workspace-browser/configuration-backup.mjs @@ -126,6 +126,8 @@ export const configurationBackupScenario = { await verificationDetails.locator("summary").click(); await verificationDetails.getByText("归档字节:", {exact: false}).waitFor(); const recordedMeasurements = await verificationDetails.locator("p").innerText(); + await verificationDetails.scrollIntoViewIfNeeded(); + await page.screenshot({path: resolve(outputDir, "state-recovery-measurements.png"), animations: "disabled"}); await verificationDetails.locator("summary").click(); assert.equal(backend.original_sources_absent, true); const registryAfter = JSON.parse(await readFile(join(root, "registry.json"), "utf8")); From 884eae425c77ae6db9d8042bd98ee19663e7bdd4 Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 08:18:00 +0800 Subject: [PATCH 10/19] fix(recovery): abort publication when authority history exceeds capacity Signed-off-by: song --- .../state-recovery-settings.tsx | 1 + loopx/control_plane/state_recovery.ts | 6 +++- tests/test_state_recovery.py | 34 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx index a14b2dc852..38c332bed7 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx @@ -26,6 +26,7 @@ const errors: Record = { recovery_capacity_exceeded: ["备份超过当前检查容量,请使用较小的备份;未建立新副本。", "This backup exceeds the inspection limits. Use a smaller backup; no new copy was published."], recovery_input_unavailable: ["上传文件已不可用,请重新选择归档与清单。", "The uploaded files are unavailable. Select the archive and manifest again."], recovery_owner_timeout: ["离线检查超时,未发布副本;可使用较小的备份重试。", "Offline verification timed out without publishing a copy. Retry with a smaller backup."], + authority_history_capacity: ["单个 Goal 的历史超过 20,000 次提交,未发布副本。请保留完整备份,通过存储 owner 的恢复流程检查。", "One Goal exceeds the 20,000-commit inspection limit; no copy was published. Keep the complete backup and use the storage owner's recovery procedure."], member_witness_mismatch: ["保存的文件与清单记录不符,已停止检查。", "Saved files differ from their manifest witnesses. Inspection stopped."], }; diff --git a/loopx/control_plane/state_recovery.ts b/loopx/control_plane/state_recovery.ts index b089d5db80..89f24d401e 100644 --- a/loopx/control_plane/state_recovery.ts +++ b/loopx/control_plane/state_recovery.ts @@ -4,7 +4,7 @@ import {lstat, readFile} from "node:fs/promises"; import {join, relative} from "node:path"; import type {JsonObject} from "./effect_program.ts"; import {requireJsonObject} from "./runtime_decode.ts"; -import {canonicalAuthoritySha256, AuthorityStoreProtocolError} from "./coordination/authority_store_codec.ts"; +import {canonicalAuthoritySha256, AuthorityStoreProtocolError, parseAuthorityCursor} from "./coordination/authority_store_codec.ts"; import {EffectRuntimeRequestError} from "./effect_runtime_errors.ts"; import {verifyConfigurationBackup} from "./configuration_backup.ts"; import {decodeLocalAuthoritySelection, localAuthorityProviderPaths, @@ -145,6 +145,7 @@ async function inspectGoal(root: string, goalId: string, standaloneSqlite: Set Date: Mon, 12 Oct 2026 08:26:58 +0800 Subject: [PATCH 11/19] test(recovery): exercise native history overflow in the packaged journey Signed-off-by: song --- .../configuration-backup.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/personal-workspace-browser/configuration-backup.mjs b/examples/personal-workspace-browser/configuration-backup.mjs index 3db34ccc3b..01c5d74ad0 100644 --- a/examples/personal-workspace-browser/configuration-backup.mjs +++ b/examples/personal-workspace-browser/configuration-backup.mjs @@ -138,6 +138,17 @@ export const configurationBackupScenario = { await recovery.getByRole("button", {name: "验证并建立隔离副本", exact: true}).click(); await recovery.getByRole("alert").waitFor(); assert.equal(await recovery.getByText("保存的任务: 1", {exact: false}).count(), 0, "a changed input must clear the old success view"); + // Optional real capacity fixture, produced through the normal backup + // command. The ordinary browser smoke does not seed 20,001 transactions. + if (process.env.LOOPX_RECOVERY_HISTORY_OVERFLOW_ARCHIVE) { + assert.ok(process.env.LOOPX_RECOVERY_HISTORY_OVERFLOW_MANIFEST); + await recovery.getByLabel("完整归档", {exact: true}).setInputFiles(process.env.LOOPX_RECOVERY_HISTORY_OVERFLOW_ARCHIVE); + await recovery.getByLabel("配套清单", {exact: true}).setInputFiles(process.env.LOOPX_RECOVERY_HISTORY_OVERFLOW_MANIFEST); + await recovery.getByRole("button", {name: "验证并建立隔离副本", exact: true}).click(); + await recovery.getByRole("alert").filter({hasText: "20,000"}).waitFor(); + assert.equal(await recovery.getByText("保存的任务: 1", {exact: false}).count(), 0); + await page.screenshot({path: resolve(outputDir, "state-recovery-history-capacity.png"), animations: "disabled"}); + } await recovery.getByRole("button", {name: "历史检查结果", exact: true}).click(); const history = recovery.getByLabel("历史备份检查", {exact: true}); await history.getByRole("button").click(); From 729287c1a08de687febb15058b24249b7ed37349 Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 08:26:58 +0800 Subject: [PATCH 12/19] docs(recovery): record measured local capacity and qualification boundaries Signed-off-by: song --- .../rfcs/complete-state-recovery-v0.md | 8 +-- .../rfcs/complete-state-recovery-v0.zh-CN.md | 4 +- docs/reference/configuration-backup.md | 53 +++++++++++++++++-- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.md b/docs/architecture/rfcs/complete-state-recovery-v0.md index 00bee22b7f..ba3fe4bf69 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.md @@ -34,9 +34,11 @@ unavailable. Every v0 audit remains held for incomplete capture/owner metadata; unknown state is not relabeled recoverable, and execution authority is always false. Limits, rollback and reproduction are in the [backup guide](../../reference/configuration-backup.md#inspect-a-complete-backup-without-activating-it). Reports retain measured archive/regular-file bytes and archive-processing and -owner-read durations; historical readback does not retime them. M1 operational -qualification remains partial: measurements across the full capacity profile -still belong to the recovery owner under Section 10. +owner-read durations; historical readback does not retime them. The backup guide +records individual native capacity cases, including exact member/history +boundaries and 128 File/SQLite Goals. History overflow refuses publication. +M1 operational qualification remains partial: the Section 12 maintainer freeze, +compound worst cases and broader platform/provider profiles remain open. This checkpoint does not qualify M2/M3, live adoption, remote providers or the G1 team journey. No original identity or receipt is rewritten. diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md index 2f2e9d9af5..b5f6cfe9cd 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md @@ -27,7 +27,9 @@ M1 的有界路径已包含增量 `backup-state verify`、loopback HTTP 上传 File/SQLite Todo/history owner 检查。v0 缺少完整捕获/owner 元数据,因此所有审计仍为 held,未知状态不改称可恢复,执行权限始终为 false。限制与回退见[备份指南](../../reference/configuration-backup.md#inspect-a-complete-backup-without-activating-it)。 报告保留实际归档/普通文件字节数及归档处理、owner 读取耗时;历史读回不重新计时。 -M1 的运行资格仍不完整:完整容量范围测量继续由第 10 节的 recovery owner 承接。 +备份指南记录了独立的真实容量案例,包括精确成员/历史边界及 128 个 File/SQLite Goal; +历史超限在发布前拒绝。M1 的运行资格仍不完整:第 12 节维护者冻结、多个上限组合的 +最坏情形及更广的平台/provider 资格继续开放。 本检查点不证明 M2/M3、live adoption、远端 provider 或 G1 团队验收,不重写原身份或回执。 diff --git a/docs/reference/configuration-backup.md b/docs/reference/configuration-backup.md index 72675ec0a6..7137b5c094 100644 --- a/docs/reference/configuration-backup.md +++ b/docs/reference/configuration-backup.md @@ -221,9 +221,56 @@ and virtual tables cannot masquerade as authority tables. WAL-mode copies and unsupported/service-owned providers remain unknown and need their existing owner; PostgreSQL is never contacted. The normal 2 MiB RPC wire is unchanged; only the two complete-manifest checks opt into the existing 64 MiB private-file -transport. These bounds define this inspection profile; full-capacity -qualification remains open under the recovery RFC's operational contract. -No production SLO is claimed. +transport. These candidate bounds define this inspection profile. The RFC's +Section 12 maintainer freeze, compound worst-case qualification and broader +platform/provider qualification remain open. No production SLO is claimed. + +### Measured local capacity cases + +At source `884eae425c77ae6db9d8042bd98ee19663e7bdd4`, disposable native backups +were inspected through the real `backup-state verify --execute` CLI after hiding +their original project/runtime/registry. Measurements used macOS arm64, Python +3.11.7 and Node 22.22.3. Times below include the verifier command, not backup +creation; peak RSS is the macOS `time -l` command metric, not a simultaneous +process-tree total. These are individual synthetic observations under local +development load, not latency targets or proof that all maxima combine. + +| Native input / boundary | Result | Wall seconds | Peak RSS MiB | +| --- | --- | ---: | ---: | +| 511 MiB zero-filled regular file | Held audit; captured configuration readable | 4.941 | 97.8 | +| 512 MiB regular file plus archive metadata | Refused: expanded capacity | 1.931 | 88.2 | +| 127 MiB seeded incompressible file; 133,212,049 archive bytes | Held audit | 1.284 | 100.6 | +| Archive input 134,217,729 bytes | Refused before decoding: archive capacity | 0.521 | 81.5 | +| External manifest padded with JSON whitespace to 16 MiB | Held audit; bounds input bytes, not maximum structured metadata | 0.945 | 113.8 | +| External manifest 16 MiB + 1 byte | Refused: manifest capacity | 0.520 | 81.4 | +| Exactly 20,000 archive members | Complete inventory and held audit | 8.698 | 225.8 | +| Exactly 20,001 archive members | Refused: member capacity | 1.888 | 143.8 | +| 128 native File Goals, one retained task/commit each | All 128 Goal records readable | 0.843 | 103.5 | +| 128 native SQLite Goals/databases, one retained task/commit each | All 128 Goal records readable; database bytes historical-only | 1.041 | 107.1 | +| 129 File Goals | Refused: Goal inventory capacity | 0.738 | 96.8 | +| 129 SQLite databases | Refused: SQLite inventory capacity | 0.534 | 83.7 | + +All refusal cases left no destination or staging directory. The real SQLite +history regression additionally proves that **20,000 committed transactions +are fully read**, while **20,001 refuse publication**. An earlier implementation +incorrectly turned that overflow into an unknown component and published the +copy. The owner now checks the canonical cursor before scanning and propagates +the capacity error instead of treating it as unreadable source state: + +```bash +uv run --extra test python -m pytest tests/test_state_recovery.py \ + -k real_sqlite_history_limit -q +``` + +Keep an over-limit backup complete and use the owning storage recovery process; +do not trim historical transactions to force inspection through this profile. +The optional browser capacity case accepts a native over-limit archive pair via +`LOOPX_RECOVERY_HISTORY_OVERFLOW_ARCHIVE` and +`LOOPX_RECOVERY_HISTORY_OVERFLOW_MANIFEST`, alongside the existing +`configuration-backup` scenario. It must show refusal and retain the earlier +historical report. Installed-wheel checks on Python 3.12.13 and 3.13.12 also +passed normal source-loss inspection and early GNU/PAX sparse/metadata refusal; +they do not expand the capacity observations or qualify other platforms. 中文:在设置的能力中心选择设备范围,打开既有备份入口并切到“完整归档检查”, 选择同一份备份的归档与清单,一次明确操作建立隔离副本。可读、仅历史和未知分别 From 13dd7eca02c1cd240fa9a68bf51e6e917e0f6aed Mon Sep 17 00:00:00 2001 From: song Date: Mon, 12 Oct 2026 08:29:56 +0800 Subject: [PATCH 13/19] fix(recovery): distinguish capacity refusal from a lost inspection response Signed-off-by: song --- .../src/features/personal-workspace/state-recovery-settings.tsx | 1 + examples/personal-workspace-browser/configuration-backup.mjs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx index 38c332bed7..6e3b0c6412 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/state-recovery-settings.tsx @@ -52,6 +52,7 @@ export function StateRecoverySettings() { setBusy(true); setError(""); try { await operation(); } catch (failure) { const code = failure instanceof ChatApiError ? String(failure.payload.error_code ?? "") : ""; + if (code === "authority_history_capacity") setPending(null); setError(errors[code]?.[index] ?? (zh ? "检查未完成,请核对文件或稍后重试。" : "Inspection did not finish. Check the files or retry later.")); } finally { setBusy(false); } diff --git a/examples/personal-workspace-browser/configuration-backup.mjs b/examples/personal-workspace-browser/configuration-backup.mjs index 01c5d74ad0..cd6753ff0e 100644 --- a/examples/personal-workspace-browser/configuration-backup.mjs +++ b/examples/personal-workspace-browser/configuration-backup.mjs @@ -146,6 +146,8 @@ export const configurationBackupScenario = { await recovery.getByLabel("配套清单", {exact: true}).setInputFiles(process.env.LOOPX_RECOVERY_HISTORY_OVERFLOW_MANIFEST); await recovery.getByRole("button", {name: "验证并建立隔离副本", exact: true}).click(); await recovery.getByRole("alert").filter({hasText: "20,000"}).waitFor(); + assert.equal(await recovery.getByRole("button", {name: "检查是否已有结果", exact: true}).count(), 0, + "a definitive pre-publication refusal must not suggest a lost response"); assert.equal(await recovery.getByText("保存的任务: 1", {exact: false}).count(), 0); await page.screenshot({path: resolve(outputDir, "state-recovery-history-capacity.png"), animations: "disabled"}); } From 41d6b11bf99558ea037c3cbda7af2302a353404e Mon Sep 17 00:00:00 2001 From: song Date: Wed, 14 Oct 2026 12:37:23 +0800 Subject: [PATCH 14/19] docs(recovery): prepare pending M1 capacity-default decision Signed-off-by: song --- .../rfcs/complete-state-recovery-v0.md | 47 ++++++++++++++++++- .../rfcs/complete-state-recovery-v0.zh-CN.md | 38 +++++++++++++++ docs/reference/configuration-backup.md | 3 ++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.md b/docs/architecture/rfcs/complete-state-recovery-v0.md index 932fc0c984..d18df32cf3 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.md @@ -683,7 +683,9 @@ The RFC pull request implements none of these runtime milestones. 1. **M1 capacity defaults.** The backup/reliability maintainers must freeze archive, expansion, entry, graph, and verification-time defaults before M1 merge. Recommendation: derive them from current backup-size telemetry and - fail closed with explicit overrides. This does not block the RFC. + fail closed with explicit overrides. This does not block the RFC. The + [pending decision below](#pending-m1-capacity-default-decision) makes the + current inspection profile reviewable; it does not record approval. 2. **Exact replacement continuity.** The Goal lifecycle and provider owners must decide before M3 whether any local disaster-recovery profile may retain a source `goal_instance_id`. Recommendation: mint a new lifetime unless the old @@ -698,6 +700,49 @@ The RFC pull request implements none of these runtime milestones. claims support. M1 may report the platform as unqualified and stop before extraction. +### Pending M1 capacity-default decision + +**Status: pending backup/reliability maintainer decision.** The proposal is to +freeze the existing limits below for the v0 **local POSIX File/SQLite inert +inspection** profile, without increasing a limit or adding an override. This +would satisfy only the capacity-default decision in Section 12 item 1; the +Section 11 M1 merge gate remains open until the owner records that decision. +It would not approve compound maximum loads, a production SLO, another platform +or provider, capture consistency, identity adoption, or M2/M3 reactivation. + +The archive adapter, typed audit owner and disposable worker enforce the +current bounds; the [backup guide](../../reference/configuration-backup.md#measured-local-capacity-cases) +retains the original source, environment, measurements and reproduction command. +Those measurements are historical evidence, not measurements of a later merge. + +| Existing value proposed for this profile | What it protects | Available evidence and limit of that evidence | +| --- | --- | --- | +| Archive 128 MiB; expanded tar stream and regular-file total 512 MiB; expansion ratio 2,000 | Bound compressed input, copying and decompression amplification before publication | Guide: near-limit native inputs and over-limit refusals. Ratio is source-enforced; an independent exact-ratio boundary measurement is not recorded. | +| External and embedded manifest 16 MiB each; extended tar header 128 KiB each and 16 MiB cumulative; JSON depth 64 | Bound metadata decoding and traversal | Guide: external 16 MiB / one-byte-over cases; recovery tests reject oversized PAX variants and sparse records before decoding. Maximum structured metadata, cumulative-header and depth combinations remain unqualified. | +| 20,000 members; paths at most 64 components / 1,024 characters / 255 UTF-8 bytes per component; link resolution at most 40 hops | Bound inventory/path work and reject ambiguous or escaping extraction | Guide: exact 20,000 / 20,001 members; recovery tests cover path, link, collision and occupied-target refusals. Exact limits for every path shape and maximum link chains are not measured. | +| 128 Goals; 128 SQLite files; 20,000 committed transactions per Goal | Bound owner inventory and complete historical reads, without truncating history | Guide: native 128 / 129 inventories. `test_real_sqlite_history_limit_is_complete_and_overflow_refuses_publication` covers 20,000 / 20,001. This is not a guarantee that 128 maximum-history Goals fit the other bounds or deadline. | +| At most 258 emitted components and 128 dependency links in the current audit shape | Bound this profile's graph through the above inventories | Derived from one configuration component, up to 128 database components, up to 128 Goal components each linked to configuration, and one unknown remainder. This is not a general graph limit or a complete mutable-owner inventory; generic graph profiles remain outside this proposal. | +| Disposable owner verifier: 90 seconds; V8 old-space 512 MiB; SQLite process-wide heap at most 64 MiB | Contain database/owner inspection outside the shared daemon | Timeout cleanup and SQLite-view refusal tests exercise containment. Heap values are source-enforced; process-tree peak memory and worst-case timing are not qualified. The timeout is not an end-to-end archive-processing deadline or RTO. | +| Existing transport limits: 64 MiB private snapshot only for the two manifest checks; normal RPC and worker input 2 MiB; owner stdout 2 Mi characters; saved-audit input 16 MiB | Bound exchanges and historical report decoding without widening ordinary transport | Large-manifest transport and audit-integrity tests cover the path. These independent limits are not additive capacity or proof that every maximum-size audit is publishable. | +| Free destination space at least copied regular-file bytes plus 16 MiB before copying | Refuse a known-insufficient destination before publication | Source check plus failure-cleanup tests; concurrent disk consumption and a successful worst-case space reservation are not guaranteed. | + +Source owners: [`state_recovery.py`](../../../loopx/state_recovery.py), +[`state_recovery.ts`](../../../loopx/control_plane/state_recovery.ts), and +[`state_recovery_worker.ts`](../../../loopx/control_plane/state_recovery_worker.ts). +Existing [Python recovery cases](../../../tests/test_state_recovery.py) and +[typed audit cases](../../../tests/control_plane_ts/state_recovery.test.ts) +separate readability, rejection, cleanup and historical readback. No test count +substitutes for the maintainer decision. Actual production backup-size telemetry +and compound maximum-load qualification are not supplied by the synthetic guide. + +The requested decision is **approve these unchanged capacity defaults for this +inert profile, or identify the specific value requiring further evidence or +revision**. Record the owner, decision, reviewed implementation revision and +profile here or in the exact-head PR review. Until then, keep this proposal and +the current checkpoint pending. Readable copied state remains historical data: +the decision cannot activate a Goal, confer a current Goal-basis writer, restore +a lease, or authorize replay of a pending effect. + --- ## Appendix A: Execution ledger (non-normative) diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md index 3668992695..dfd2d77079 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md @@ -593,6 +593,7 @@ retention 与运行资格。后二者都不会推迟基本的 M1/M3 产品旅程 1. **M1 capacity 默认值。** Backup/reliability maintainer 必须在 M1 合并前冻结 archive、expansion、entry、graph 与 verification-time 默认值。建议从当前 backup-size telemetry 推导,并以显式 override fail closed。这不阻塞 RFC。 + [下述待决提案](#待决m1-容量默认值)列出当前检查 profile 的可审值,不代表批准。 2. **精确 replacement continuity。** Goal lifecycle 与 provider owner 必须在 M3 前决定是否允许任一本地 disaster-recovery profile 保留来源 `goal_instance_id`。建议铸造新 lifetime,除非旧 authority 可证明已不可逆 fence, @@ -605,6 +606,43 @@ retention 与运行资格。后二者都不会推迟基本的 M1/M3 产品旅程 验证 ACL、link、case、Unicode、sparse-file 和 atomic-publication 行为。M1 可以 报告 platform unqualified,并在提取前停止。 +### 待决:M1 容量默认值 + +**状态:等待 backup/reliability maintainer 决定。** 提案仅冻结下表既有默认值, +范围为 v0 **本地 POSIX File/SQLite 惰性检查**,不提高上限、不新增 override。 +该决定只满足第 12 节第 1 项容量默认值要求;owner 记录决定之前,第 11 节 M1 +合并门保持开放。它不批准多个上限组合的最坏负载、生产 SLO、其它平台/provider、 +捕获一致性、身份接纳或 M2/M3 重新启用。 + +当前上限由归档 adapter、typed audit owner 与隔离 worker 实施; +[备份指南](../../reference/configuration-backup.md#measured-local-capacity-cases) +保留原始源码版本、环境、测量值和复现命令。这些是历史证据,不是后续合并版本的实测。 + +| 拟冻结的当前 profile 值 | 保护目标 | 已有证据及边界 | +| --- | --- | --- | +| 归档 128 MiB;展开 tar 流和普通文件总量 512 MiB;展开比例 2,000 | 发布前限制压缩输入、复制和解压放大 | 指南有接近上限的原生输入及超限拒绝。比例有源码限制,但未记录独立的精确比例边界测量。 | +| 外部及内嵌清单各 16 MiB;扩展 tar header 每项 128 KiB、累计 16 MiB;JSON 深度 64 | 限制 metadata 解码与遍历 | 指南有外部清单 16 MiB/超一字节;恢复测试在解码前拒绝超大 PAX 变体及 sparse。最大结构化 metadata、累计 header 与深度组合未资格化。 | +| 20,000 个 member;路径最多 64 段/1,024 字符/每段 255 UTF-8 字节;链接解析最多 40 跳 | 限制清单与路径处理,拒绝歧义或越界提取 | 指南有精确 20,000/20,001;测试覆盖路径、链接、碰撞与占用目标拒绝。并未测量所有路径形状的精确上限和最长链接链。 | +| 128 个 Goal;128 个 SQLite 文件;每 Goal 20,000 个已提交事务 | 限制 owner 清单与完整历史读取,不截断历史 | 指南有原生 128/129 清单;`test_real_sqlite_history_limit_is_complete_and_overflow_refuses_publication` 覆盖 20,000/20,001。不保证 128 个满历史 Goal 同时符合其它上限与时限。 | +| 当前 audit 结构最多 258 个 component、128 个 dependency link | 通过上述清单上限约束本 profile 的图 | 来自 1 个配置、最多 128 个数据库、最多 128 个各自依赖配置的 Goal,以及 1 个 unknown 余项。这不是通用 graph 上限或完整 mutable-owner 清单,通用 graph profile 不在本提案内。 | +| 隔离 owner verifier:90 秒;V8 old-space 512 MiB;SQLite 进程级 heap 至多 64 MiB | 将数据库/owner 检查限制在共享 daemon 之外 | Timeout 清理和 SQLite view 拒绝测试覆盖隔离。Heap 有源码限制;整个进程树峰值内存、最坏耗时未资格化。该超时不是归档全流程 deadline 或 RTO。 | +| 既有 transport:仅两项清单检查用 64 MiB private snapshot;普通 RPC 和 worker 输入 2 MiB;owner stdout 2 Mi 字符;历史 audit 输入 16 MiB | 限制传输和历史报告解码,不扩大普通 transport | 大清单 transport 与 audit 完整性测试覆盖路径。独立上限不能相加,也不保证任意最大 audit 均可发布。 | +| 复制前目标剩余空间至少为普通文件复制字节数加 16 MiB | 发布前拒绝已知空间不足 | 有源码检查和失败清理测试;不保证抵御并发磁盘消耗,也不证明最坏空间预留成功。 | + +源码 owner 为 [`state_recovery.py`](../../../loopx/state_recovery.py)、 +[`state_recovery.ts`](../../../loopx/control_plane/state_recovery.ts) 和 +[`state_recovery_worker.ts`](../../../loopx/control_plane/state_recovery_worker.ts)。 +既有 [Python 恢复用例](../../../tests/test_state_recovery.py)及 +[typed audit 用例](../../../tests/control_plane_ts/state_recovery.test.ts) +分别验证可读性、拒绝、清理和历史回读。测试数量不能代替维护者决定;合成指南没有 +提供实际生产 backup-size telemetry 或多个最大负载组合的资格证据。 + +请求的决定仅为:**批准这些不变的容量默认值用于本惰性检查 profile,或指出需要 +更多证据/调整的具体值**。在本节或精确 head 的 PR review 记录 owner、决定、 +已审实现版本及 profile;此前提案和当前 checkpoint 均保持 pending。可读副本仍是 +历史数据:该决定不启用 Goal、不授予当前 Goal 依据的 writer、不恢复 lease, +也不授权重放未决 effect。 + --- ## 附录 A:执行台账(非规范性) diff --git a/docs/reference/configuration-backup.md b/docs/reference/configuration-backup.md index bd546a55ca..143ba9621c 100644 --- a/docs/reference/configuration-backup.md +++ b/docs/reference/configuration-backup.md @@ -282,6 +282,9 @@ only the two complete-manifest checks opt into the existing 64 MiB private-file transport. These candidate bounds define this inspection profile. The RFC's Section 12 maintainer freeze, compound worst-case qualification and broader platform/provider qualification remain open. No production SLO is claimed. +The RFC's [pending capacity-default decision](../architecture/rfcs/complete-state-recovery-v0.md#pending-m1-capacity-default-decision) +lists the unchanged values, protection goals and evidence gaps for the maintainer; +it is not an approval or an additional activation step for the operator. ### Measured local capacity cases From f4bfab4a391e8754cb961471237bc57f4c651eed Mon Sep 17 00:00:00 2001 From: song Date: Wed, 14 Oct 2026 12:43:58 +0800 Subject: [PATCH 15/19] refactor(recovery): group HTTP routes with backup operations Signed-off-by: song --- loopx/presentation/configuration_api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/loopx/presentation/configuration_api.py b/loopx/presentation/configuration_api.py index 84b96d46ba..4c5dd63b06 100644 --- a/loopx/presentation/configuration_api.py +++ b/loopx/presentation/configuration_api.py @@ -30,10 +30,10 @@ class ChatConfigurationRequestMixin( def _configuration_get_routes(self) -> dict[str, Callable[[], None]]: return { - recovery_api.STATE_RECOVERY_PATH: self._state_recovery_list, - f"{recovery_api.STATE_RECOVERY_PATH}/profile": self._state_recovery_profile, amendment_api.CHAT_GOAL_AMENDMENT_PATH: self._amendment_inspect, storage_api.CHAT_GOAL_STORAGE_PATH: self._storage_inspect, + recovery_api.STATE_RECOVERY_PATH: self._state_recovery_list, + f"{recovery_api.STATE_RECOVERY_PATH}/profile": self._state_recovery_profile, ownership_api.CHAT_GOAL_OWNERSHIP_PATH: self._ownership_inspect, cadence_api.CHAT_AUTOMATION_CADENCE_PATH: self._cadence_read, goal_api.CHAT_GOAL_CONFIGURATION_PATH: self._goal_configuration_inspect, @@ -44,10 +44,6 @@ def _configuration_get_routes(self) -> dict[str, Callable[[], None]]: def _configuration_post_routes(self) -> dict[str, Callable[[], None]]: return { - f"{recovery_api.STATE_RECOVERY_PATH}/archive": lambda: self._state_recovery_upload("archive"), - f"{recovery_api.STATE_RECOVERY_PATH}/manifest": lambda: self._state_recovery_upload("manifest"), - f"{recovery_api.STATE_RECOVERY_PATH}/verify": self._state_recovery_verify, - f"{recovery_api.STATE_RECOVERY_PATH}/read": self._state_recovery_read, amendment_api.CHAT_GOAL_AMENDMENT_PATH: self._amendment_configure, f"{storage_api.CHAT_GOAL_STORAGE_PATH}/preview": lambda: self._storage_update(action="plan-migration"), f"{storage_api.CHAT_GOAL_STORAGE_PATH}/apply": lambda: self._storage_update(action="migrate"), @@ -57,6 +53,10 @@ def _configuration_post_routes(self) -> dict[str, Callable[[], None]]: f"{storage_api.CHAT_GOAL_STORAGE_PATH}/import/recover": lambda: self._storage_import(action="readback"), f"{backup_api.CONFIGURATION_BACKUP_PATH}/export": self._configuration_backup_export, f"{backup_api.CONFIGURATION_BACKUP_PATH}/restore": self._configuration_backup_restore, + f"{recovery_api.STATE_RECOVERY_PATH}/archive": lambda: self._state_recovery_upload("archive"), + f"{recovery_api.STATE_RECOVERY_PATH}/manifest": lambda: self._state_recovery_upload("manifest"), + f"{recovery_api.STATE_RECOVERY_PATH}/verify": self._state_recovery_verify, + f"{recovery_api.STATE_RECOVERY_PATH}/read": self._state_recovery_read, f"{ownership_api.CHAT_GOAL_OWNERSHIP_PATH}/preview": lambda: self._ownership_update(execute=False), f"{ownership_api.CHAT_GOAL_OWNERSHIP_PATH}/apply": lambda: self._ownership_update(execute=True), cadence_api.CHAT_AUTOMATION_CADENCE_PREVIEW_PATH: lambda: self._cadence_update(execute=False), From 5751b050f616f156fa591aac5aa82627d8396fe0 Mon Sep 17 00:00:00 2001 From: song Date: Thu, 15 Oct 2026 09:31:49 +0800 Subject: [PATCH 16/19] test(recovery): preserve origin when forwarding backup requests Use the existing workspace backend forwarder for state-recovery routes so same-origin archive uploads reach the disposable real server. Assert the upload HTTP status before waiting for the success view, making rejected requests fail directly. Reproduced the packaged scenario timeout on the parent, then HTTP 403 with the added assertion. Verified the complete scenario with SQLite, file authority, and an isolated installed wheel; origin guards and focused recovery/history controls pass. No production origin policy or capacity defaults change. Signed-off-by: song --- .../personal-workspace-browser/configuration-backup.mjs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/personal-workspace-browser/configuration-backup.mjs b/examples/personal-workspace-browser/configuration-backup.mjs index 94483c32fa..a720bad937 100644 --- a/examples/personal-workspace-browser/configuration-backup.mjs +++ b/examples/personal-workspace-browser/configuration-backup.mjs @@ -84,8 +84,7 @@ export const configurationBackupScenario = { await route.fulfill({response}); }); await page.route("**/api/chat/state-recovery**", async route => { - const path = new URL(route.request().url()).pathname; - const response = await route.fetch({url: `http://127.0.0.1:${port}${path}`}); + const response = await fetchWorkspaceBackend(route, `http://127.0.0.1:${port}`); await route.fulfill({response}); }); }}); @@ -117,7 +116,12 @@ export const configurationBackupScenario = { await recovery.getByLabel("配套清单", {exact: true}).focus(); await page.keyboard.press("Tab"); assert.equal(await page.evaluate(() => document.activeElement.textContent), "验证并建立隔离副本"); + const uploadedArchive = page.waitForResponse(response => + new URL(response.url()).pathname === "/api/chat/state-recovery/archive" + && response.request().method() === "POST"); await page.keyboard.press("Enter"); + const uploadResponse = await uploadedArchive; + assert.ok(uploadResponse.ok(), `Archive upload failed (HTTP ${uploadResponse.status()})`); await recovery.getByText("隔离副本已核对;Goal 尚未启用。", {exact: true}).waitFor(); await recovery.getByText("recovery-fixture", {exact: false}).first().waitFor(); await recovery.getByText("保存的任务: 1", {exact: false}).waitFor(); From 7f62b602019429c05a03254ab2f30a3fafa32c6e Mon Sep 17 00:00:00 2001 From: song Date: Thu, 15 Oct 2026 13:55:10 +0800 Subject: [PATCH 17/19] test(recovery): isolate the managed runtime used by backup checks Signed-off-by: song --- tests/conftest.py | 14 ++++++++++++++ tests/test_state_backup.py | 12 +----------- tests/test_state_recovery.py | 3 +++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5a7cd9f8aa..5c6ff5d628 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import importlib import os import sys +import tempfile from pathlib import Path @@ -36,6 +37,19 @@ _REAL_DEFAULT_ROUTES = (paths.DEFAULT_RUNTIME_ROOT, paths.LEGACY_RUNTIME_ROOT) +@pytest.fixture +def isolated_effect_runtime(tmp_path, monkeypatch): + temporary = tmp_path / "temporary" + temporary.mkdir() + monkeypatch.setenv("TMPDIR", str(temporary)) + monkeypatch.setattr(tempfile, "tempdir", str(temporary)) + from loopx.control_plane import effect_runtime + + yield + # Stop only the real runtime launched in this test's private directory. + effect_runtime.restart_effect_runtime() + + @pytest.fixture(autouse=True) def _isolated_default_runtime_routes(tmp_path_factory, monkeypatch): home = tmp_path_factory.mktemp("home") diff --git a/tests/test_state_backup.py b/tests/test_state_backup.py index b43eabb6b2..a51e535724 100644 --- a/tests/test_state_backup.py +++ b/tests/test_state_backup.py @@ -8,25 +8,15 @@ import subprocess import sys import tarfile -import tempfile from pathlib import Path import pytest import loopx.state_backup as state_backup -from loopx.control_plane import effect_runtime from loopx.state_backup import build_state_backup_plan, execute_state_backup_plan -@pytest.fixture(autouse=True) -def isolated_runtime(tmp_path, monkeypatch): - temporary = tmp_path / "temporary" - temporary.mkdir() - monkeypatch.setenv("TMPDIR", str(temporary)) - monkeypatch.setattr(tempfile, "tempdir", str(temporary)) - yield - # Stop only the real runtime launched in this test's private directory. - effect_runtime.restart_effect_runtime() +pytestmark = pytest.mark.usefixtures("isolated_effect_runtime") def backup_plan(root: Path, backup_id: str = "fixture"): diff --git a/tests/test_state_recovery.py b/tests/test_state_recovery.py index ea9fceeb54..e1a0e4d3dd 100644 --- a/tests/test_state_recovery.py +++ b/tests/test_state_recovery.py @@ -20,6 +20,9 @@ from tests.control_plane.canonical_authority_fixture import promoted_create_fixture +pytestmark = pytest.mark.usefixtures("isolated_effect_runtime") + + def _cli(*args: object) -> tuple[int, dict]: result = subprocess.run([sys.executable, "-m", "loopx.cli", "--format", "json", *map(str, args)], capture_output=True, text=True, env={**os.environ, "LOOPX_USAGE_PING": "0"}, timeout=90) From d0146600005be2157fafda905b4df358a0bbf298 Mon Sep 17 00:00:00 2001 From: song Date: Thu, 15 Oct 2026 15:39:14 +0800 Subject: [PATCH 18/19] docs(recovery): record capacity consent and checkpoint reuse boundaries Signed-off-by: song --- .../rfcs/complete-state-recovery-v0.md | 93 ++++++++++++++----- .../rfcs/complete-state-recovery-v0.zh-CN.md | 68 ++++++++++---- docs/reference/configuration-backup.md | 33 +++++-- 3 files changed, 150 insertions(+), 44 deletions(-) diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.md b/docs/architecture/rfcs/complete-state-recovery-v0.md index d18df32cf3..0cb9cab292 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.md @@ -37,8 +37,8 @@ Reports retain measured archive/regular-file bytes and archive-processing and owner-read durations; historical readback does not retime them. The backup guide records individual native capacity cases, including exact member/history boundaries and 128 File/SQLite Goals. History overflow refuses publication. -M1 operational qualification remains partial: the Section 12 maintainer freeze, -compound worst cases and broader platform/provider profiles remain open. +The Section 12 capacity-default decision is recorded; operational qualification +remains partial for compound worst cases and broader platform/provider profiles. This checkpoint does not qualify M2/M3, live adoption, remote providers or the G1 team journey. No original identity or receipt is rewritten. @@ -199,6 +199,55 @@ This RFC composes existing owners; it does not replace them: | [Effect Interpreter](agent-loop-effect-interpreter-v0.md) | Owns effect identity, settlement, unknown-outcome readback, and replay decisions. Recovery only routes original evidence to it. | | [Configuration backup](../../reference/configuration-backup.md) | Keeps configuration in an isolated checkpoint. Adoption continues through machine and Goal configuration owners. | +### Backup inspection and research checkpoint reuse + +The merged [research checkpoint contract](../../reference/research-checkpoint.md) +serves a distinct research branch with an independently admitted Goal/session. +Complete-backup inspection serves an operator whose source may be unavailable. +Neither format is an input to the other's verifier today. Both retain original +owner authority; research branching may retain a live source, while disaster +replacement must exclude old writers before admitting new work. + +| Boundary | Existing owner and reuse | Deliberate separation | +| --- | --- | --- | +| State inventory | `state_backup.build_state_backup_plan`; checkpoint `_loopx_state_components` already calls it for a scoped single Goal | Full archives can contain multiple Goals; checkpoint rejects incomplete or moving scoped inventory | +| SQLite capture | Both creation paths use the existing `snapshotSqliteDatabase` owner | M1 reads captured databases offline; it must not snapshot an untrusted restored database into current authority | +| Digests | Both typed owners reuse `canonicalAuthoritySha256` for structured identity; byte digests bind files | Archive/external/embedded manifests and checkpoint component witnesses have different schemas; digest equality is not cross-format compatibility | +| Isolation and publication | Existing archive adapter plus typed recovery owner; checkpoint `checkpoint_artifacts.ts` owns its private staging and manifest verification | Untrusted tar links, headers and expansion require archive-specific checks; checkpoint directory capture rejects symlinks and checks source movement. Similar filesystem operations do not establish duplicate decision rules | +| Historical readback | M1 uses canonical configuration/File/SQLite/Todo/history readers and immutable saved audits | Checkpoint uses revision-bound Todo context, Explore event codec and native session history; an M1 readability report is not an adoptable session | +| Session, knowledge and work adoption | Existing Goal lifetime, Codex session, Todo continuation, Explore and effect owners | Checkpoint admission/adoption is explicit; M1 invokes none of it. Leases, quota, timers, credentials and effects remain historical | + +The bounded refactor assessment finds the shared inventory, snapshot and digest +rules already reused. No new common restore service or format converter is +needed for M1; merging the two extraction paths would obscure their different +input checks. Python remains the archive/Host adapter; typed owners retain +shared decisions. This assessment records source contracts, not a newly executed +joint-recovery result. + +**M2 checkpoint:** extend the existing component/profile acceptance with explicit +format/version, complete component inventory, captured owner revisions and +capture boundary, and provider qualification. A future bridge must first verify +these facts in isolation. Missing v0 facts stay unknown; never infer them from +file timestamps or successful SQLite reads. Keep existing v0 historical readers. + +**M3 checkpoint:** only a qualified profile can proceed from isolated verification +to current Goal/session admission, knowledge adoption and current work admission. +Reuse the existing owners above; retain source digest, destination identity and +operation identity across retries. A partial interruption requires original-owner +readback/compensation, and an unknown external result requires reconciliation +under its original identity. Branching does not certify exclusive replacement. +No format bridge or automatic activation is shipped by M1. + +The existing M3 drill remains the joint acceptance: an ordinary Goal completes +some work and leaves a Todo unfinished; after its source becomes unavailable, +the packaged journey previews recovered/historical/unknown work, obtains owner +admission, continues the unfinished work and independently checks its result. +Include missing/corrupt components, version/provider refusal, capture movement, +stale identity, retry and interrupted adoption; prove no old authority revival +or private-data disclosure. This is M2/M3 evidence, not a new full-recovery M1 +prerequisite. M1's existing Settings entry reports readable tasks/history and +holds; it cannot offer a working continuation action before those stages qualify. + ## 4. Current-system contract At the named baseline, `loopx/state_backup.py` discovers the runtime root, @@ -684,8 +733,8 @@ The RFC pull request implements none of these runtime milestones. archive, expansion, entry, graph, and verification-time defaults before M1 merge. Recommendation: derive them from current backup-size telemetry and fail closed with explicit overrides. This does not block the RFC. The - [pending decision below](#pending-m1-capacity-default-decision) makes the - current inspection profile reviewable; it does not record approval. + [recorded decision below](#m1-capacity-default-decision) freezes only the + existing local inert-inspection profile; broader qualification remains open. 2. **Exact replacement continuity.** The Goal lifecycle and provider owners must decide before M3 whether any local disaster-recovery profile may retain a source `goal_instance_id`. Recommendation: mint a new lifetime unless the old @@ -700,22 +749,25 @@ The RFC pull request implements none of these runtime milestones. claims support. M1 may report the platform as unqualified and stop before extraction. -### Pending M1 capacity-default decision +
+### M1 capacity-default decision -**Status: pending backup/reliability maintainer decision.** The proposal is to -freeze the existing limits below for the v0 **local POSIX File/SQLite inert -inspection** profile, without increasing a limit or adding an override. This -would satisfy only the capacity-default decision in Section 12 item 1; the -Section 11 M1 merge gate remains open until the owner records that decision. -It would not approve compound maximum loads, a production SLO, another platform -or provider, capture consistency, identity adoption, or M2/M3 reactivation. +**Status: approved for the existing local POSIX File/SQLite inert-inspection +profile.** On 2026-10-15, maintainer @huangruiteng [approved the capacity +proposal](https://github.com/loopx-project/loopx/pull/6279#issuecomment-6186241464) presented for revision +`5751b050f616f156fa591aac5aa82627d8396fe0`. The limits below remain unchanged at +`7f62b602019429c05a03254ab2f30a3fafa32c6e`. This closes only Section 12 item 1; +it does not grant merge authority or approve compound maximum loads, a production +SLO, another platform/provider, capture consistency, identity adoption or M2/M3 +reactivation. The same comment requests checkpoint reuse assessment, recorded +in Section 3 below; that requirement is independent of the capacity consent. The archive adapter, typed audit owner and disposable worker enforce the current bounds; the [backup guide](../../reference/configuration-backup.md#measured-local-capacity-cases) retains the original source, environment, measurements and reproduction command. Those measurements are historical evidence, not measurements of a later merge. -| Existing value proposed for this profile | What it protects | Available evidence and limit of that evidence | +| Approved existing value for this profile | What it protects | Available evidence and limit of that evidence | | --- | --- | --- | | Archive 128 MiB; expanded tar stream and regular-file total 512 MiB; expansion ratio 2,000 | Bound compressed input, copying and decompression amplification before publication | Guide: near-limit native inputs and over-limit refusals. Ratio is source-enforced; an independent exact-ratio boundary measurement is not recorded. | | External and embedded manifest 16 MiB each; extended tar header 128 KiB each and 16 MiB cumulative; JSON depth 64 | Bound metadata decoding and traversal | Guide: external 16 MiB / one-byte-over cases; recovery tests reject oversized PAX variants and sparse records before decoding. Maximum structured metadata, cumulative-header and depth combinations remain unqualified. | @@ -735,13 +787,9 @@ separate readability, rejection, cleanup and historical readback. No test count substitutes for the maintainer decision. Actual production backup-size telemetry and compound maximum-load qualification are not supplied by the synthetic guide. -The requested decision is **approve these unchanged capacity defaults for this -inert profile, or identify the specific value requiring further evidence or -revision**. Record the owner, decision, reviewed implementation revision and -profile here or in the exact-head PR review. Until then, keep this proposal and -the current checkpoint pending. Readable copied state remains historical data: -the decision cannot activate a Goal, confer a current Goal-basis writer, restore -a lease, or authorize replay of a pending effect. +The approved values retain the evidence limits above. Readable copied state +remains historical data: capacity consent cannot activate a Goal, confer a +current Goal-basis writer, restore a lease or authorize pending-effect replay. --- @@ -756,9 +804,10 @@ entries under `ledger/complete-state-recovery-v0/` following the | Date | Decision | Owner / approval | Alternatives | Normative sections changed | | --- | --- | --- | --- | --- | | 2026-10-08 | Propose inert-first, owner-composed complete state recovery | RFC review through issue #5940 | Raw extraction, monolithic transaction, manual-only runbook | Initial document | +| 2026-10-15 | Freeze unchanged M1 local POSIX File/SQLite inert-inspection capacities; reviewed proposal `5751b050f`, unchanged at `7f62b6020` | [@huangruiteng capacity consent](https://github.com/loopx-project/loopx/pull/6279#issuecomment-6186241464) | Raise limits, add overrides, or defer the local profile | §12.1; broader qualification and merge authority excluded | -Design acceptance occurs only when the RFC pull request is merged. The row above -records proposal history, not prior maintainer approval. +The initial row records proposal history. The scoped capacity decision above +does not qualify the remaining recovery milestones. ## Appendix C: Evidence registry diff --git a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md index dfd2d77079..7e2c5382fa 100644 --- a/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md +++ b/docs/architecture/rfcs/complete-state-recovery-v0.zh-CN.md @@ -28,7 +28,7 @@ File/SQLite Todo/history owner 检查。v0 缺少完整捕获/owner 元数据, held,未知状态不改称可恢复,执行权限始终为 false。限制与回退见[备份指南](../../reference/configuration-backup.md#inspect-a-complete-backup-without-activating-it)。 报告保留实际归档/普通文件字节数及归档处理、owner 读取耗时;历史读回不重新计时。 备份指南记录了独立的真实容量案例,包括精确成员/历史边界及 128 个 File/SQLite Goal; -历史超限在发布前拒绝。M1 的运行资格仍不完整:第 12 节维护者冻结、多个上限组合的 +历史超限在发布前拒绝。M1 的运行资格仍不完整:第 12 节容量决定已记录,多个上限组合的 最坏情形及更广的平台/provider 资格继续开放。 本检查点不证明 M2/M3、live adoption、远端 provider 或 G1 团队验收,不重写原身份或回执。 @@ -165,6 +165,42 @@ reconciliation 和 readback。任何单独 owner 都不能安全推断其他 own | [Effect Interpreter](agent-loop-effect-interpreter-v0.zh-CN.md) | 拥有 effect identity、settlement、未知结果 readback 和 replay 决策。恢复流程只向其路由原始证据。 | | [配置备份](../../reference/configuration-backup.md) | 保持配置处于隔离 checkpoint。Adoption 继续通过 machine 与 Goal configuration owner 完成。 | +### 完整备份检查与研究 checkpoint 的复用 + +已合入的[研究 checkpoint 契约](../../reference/research-checkpoint.md)面向独立 +准入的 Goal/会话研究分支;完整备份检查面向原来源可能已不可用的操作者。 +当前两种格式不能互相交给对方 verifier。两者均保留原 owner 权威;研究分支允许 +来源继续存在,灾难替换则必须在准入新工作前排除旧 writer。 + +| 边界 | 既有 owner 与复用 | 保留的分工 | +| --- | --- | --- | +| 状态清单 | `state_backup.build_state_backup_plan`;checkpoint `_loopx_state_components` 已为单 Goal 调用它 | 完整归档可含多 Goal;checkpoint 拒绝不完整或捕获期间移动的 scoped 清单 | +| SQLite 捕获 | 两种创建路径共用既有 `snapshotSqliteDatabase` owner | M1 离线读已捕获库,不能将不可信恢复库快照为当前 authority | +| 摘要 | 两个 typed owner 共用 `canonicalAuthoritySha256` 表达结构身份,文件以字节摘要绑定 | 归档/外部/内嵌清单与 checkpoint witness schema 不同;摘要相等不代表格式互通 | +| 隔离与发布 | 归档 adapter 加 typed recovery owner;checkpoint `checkpoint_artifacts.ts` 管私有 staging 和清单验证 | 不可信 tar 的链接、header、展开需专用检查;目录捕获拒绝符号链接并检查来源移动。相似文件操作不等于重复决策 | +| 历史回读 | M1 共用 canonical configuration/File/SQLite/Todo/history reader 和不可变 audit | Checkpoint 使用版本绑定的 Todo context、Explore event codec、native 会话历史;M1 可读报告不是可采用会话 | +| 会话、知识与工作采用 | 既有 Goal lifetime、Codex session、Todo continuation、Explore、effect owner | Checkpoint 显式准入/采用;M1 不调用它们。Lease、quota、timer、credential、effect 保持历史状态 | + +有界重构审视结论:共用的清单、快照及摘要规则已经复用,M1 无需新增统一恢复服务 +或格式转换器;强并提取路径会掩盖输入检查差异。Python 保留 archive/Host adapter +职责,共享决策留在 typed owner。这是源码契约审视,不是新执行的联合恢复验收。 + +**M2 检查点:**在原 component/profile 验收内明确格式/版本、完整组件清单、已捕获 +owner revision 与捕获边界、provider 资格。未来衔接先隔离核验这些事实;v0 缺失事实 +仍为 unknown,不能从文件时间或 SQLite 可读性推断。保留 v0 历史 reader。 + +**M3 检查点:**仅合格 profile 能从隔离核验进入当前 Goal/会话准入、知识采用及 +当前工作准入。复用上述 owner,重试保留来源摘要、目标身份与 operation 身份。 +中断采用需原 owner 回读/补偿;外部结果未知须按原身份对账。研究分支不证明排他 +替换。M1 不交付格式桥接或自动激活。 + +原 M3 演练即联合验收:普通 Goal 先完成部分工作并留下未完成 Todo,来源不可用后, +经 packaged journey 预览找回/历史/未知工作、取得 owner 准入、继续未完成工作, +并独立检查结果。覆盖缺失/损坏组件、版本/provider 拒绝、捕获移动、过期身份、 +重试及采用中断,证明不复活旧权限、不泄露私有数据。这属于 M2/M3 证据,不新增为 +M1 全恢复前置条件。M1 原 Settings 入口展示可读任务/历史及 hold;后续阶段合格前 +不能提供看似可用的继续执行动作。 + ## 4. 当前系统契约 在指定基线上,`loopx/state_backup.py` 会发现 runtime root、项目 `.loopx` 状态、 @@ -593,7 +629,7 @@ retention 与运行资格。后二者都不会推迟基本的 M1/M3 产品旅程 1. **M1 capacity 默认值。** Backup/reliability maintainer 必须在 M1 合并前冻结 archive、expansion、entry、graph 与 verification-time 默认值。建议从当前 backup-size telemetry 推导,并以显式 override fail closed。这不阻塞 RFC。 - [下述待决提案](#待决m1-容量默认值)列出当前检查 profile 的可审值,不代表批准。 + [下述决定](#m1-容量默认值决定)仅冻结现有本地惰性检查 profile;更广资格仍未完成。 2. **精确 replacement continuity。** Goal lifecycle 与 provider owner 必须在 M3 前决定是否允许任一本地 disaster-recovery profile 保留来源 `goal_instance_id`。建议铸造新 lifetime,除非旧 authority 可证明已不可逆 fence, @@ -606,19 +642,22 @@ retention 与运行资格。后二者都不会推迟基本的 M1/M3 产品旅程 验证 ACL、link、case、Unicode、sparse-file 和 atomic-publication 行为。M1 可以 报告 platform unqualified,并在提取前停止。 -### 待决:M1 容量默认值 + +### M1 容量默认值决定 -**状态:等待 backup/reliability maintainer 决定。** 提案仅冻结下表既有默认值, -范围为 v0 **本地 POSIX File/SQLite 惰性检查**,不提高上限、不新增 override。 -该决定只满足第 12 节第 1 项容量默认值要求;owner 记录决定之前,第 11 节 M1 -合并门保持开放。它不批准多个上限组合的最坏负载、生产 SLO、其它平台/provider、 -捕获一致性、身份接纳或 M2/M3 重新启用。 +**状态:已批准现有本地 POSIX File/SQLite 惰性检查 profile。** 2026-10-15, +维护者 @huangruiteng [同意容量提案](https://github.com/loopx-project/loopx/pull/6279#issuecomment-6186241464), +所审提案版本为 `5751b050f616f156fa591aac5aa82627d8396fe0`;下表默认值在 +`7f62b602019429c05a03254ab2f30a3fafa32c6e` 保持不变。该决定只关闭第 12 节 +第 1 项,不授予合并权限,不批准多个上限组合的最坏负载、生产 SLO、其它平台/ +provider、捕获一致性、身份接纳或 M2/M3 重新启用。同条评论要求的 checkpoint +复用审视记录于第 3 节,与容量同意是独立事项。 当前上限由归档 adapter、typed audit owner 与隔离 worker 实施; [备份指南](../../reference/configuration-backup.md#measured-local-capacity-cases) 保留原始源码版本、环境、测量值和复现命令。这些是历史证据,不是后续合并版本的实测。 -| 拟冻结的当前 profile 值 | 保护目标 | 已有证据及边界 | +| 已批准的当前 profile 值 | 保护目标 | 已有证据及边界 | | --- | --- | --- | | 归档 128 MiB;展开 tar 流和普通文件总量 512 MiB;展开比例 2,000 | 发布前限制压缩输入、复制和解压放大 | 指南有接近上限的原生输入及超限拒绝。比例有源码限制,但未记录独立的精确比例边界测量。 | | 外部及内嵌清单各 16 MiB;扩展 tar header 每项 128 KiB、累计 16 MiB;JSON 深度 64 | 限制 metadata 解码与遍历 | 指南有外部清单 16 MiB/超一字节;恢复测试在解码前拒绝超大 PAX 变体及 sparse。最大结构化 metadata、累计 header 与深度组合未资格化。 | @@ -637,11 +676,8 @@ retention 与运行资格。后二者都不会推迟基本的 M1/M3 产品旅程 分别验证可读性、拒绝、清理和历史回读。测试数量不能代替维护者决定;合成指南没有 提供实际生产 backup-size telemetry 或多个最大负载组合的资格证据。 -请求的决定仅为:**批准这些不变的容量默认值用于本惰性检查 profile,或指出需要 -更多证据/调整的具体值**。在本节或精确 head 的 PR review 记录 owner、决定、 -已审实现版本及 profile;此前提案和当前 checkpoint 均保持 pending。可读副本仍是 -历史数据:该决定不启用 Goal、不授予当前 Goal 依据的 writer、不恢复 lease, -也不授权重放未决 effect。 +批准的值仍保留上述证据限制。可读副本仍是历史数据:容量同意不启用 Goal、 +不授予当前 Goal 依据的 writer、不恢复 lease,也不授权重放未决 effect。 --- @@ -656,9 +692,9 @@ retention 与运行资格。后二者都不会推迟基本的 M1/M3 产品旅程 | 日期 | 决策 | Owner/批准 | 替代方案 | 变更的规范章节 | | --- | --- | --- | --- | --- | | 2026-10-08 | 提议以惰性优先、由既有 owner 组合的完整状态恢复 | 通过 issue #5940 评审 RFC | 原始提取、单体事务、仅人工 runbook | 初始文档 | +| 2026-10-15 | 冻结不变的 M1 本地 POSIX File/SQLite 惰性检查容量;提案 `5751b050f`,`7f62b6020` 未改值 | [@huangruiteng 容量同意](https://github.com/loopx-project/loopx/pull/6279#issuecomment-6186241464) | 提高上限、新增 override 或推迟本地 profile | §12.1;不含更广资格或合并授权 | -只有 RFC 拉取请求合并后,设计才被接受。上表记录提案历史,不表示先前已有 -maintainer approval。 +初始行记录提案历史。下列容量决定仅覆盖指定范围,不证明其它恢复阶段完成。 ## 附录 C:证据注册表 diff --git a/docs/reference/configuration-backup.md b/docs/reference/configuration-backup.md index 143ba9621c..6de9c45516 100644 --- a/docs/reference/configuration-backup.md +++ b/docs/reference/configuration-backup.md @@ -279,12 +279,33 @@ and virtual tables cannot masquerade as authority tables. WAL-mode copies and unsupported/service-owned providers remain unknown and need their existing owner; PostgreSQL is never contacted. The normal 2 MiB RPC wire is unchanged; only the two complete-manifest checks opt into the existing 64 MiB private-file -transport. These candidate bounds define this inspection profile. The RFC's -Section 12 maintainer freeze, compound worst-case qualification and broader -platform/provider qualification remain open. No production SLO is claimed. -The RFC's [pending capacity-default decision](../architecture/rfcs/complete-state-recovery-v0.md#pending-m1-capacity-default-decision) -lists the unchanged values, protection goals and evidence gaps for the maintainer; -it is not an approval or an additional activation step for the operator. +transport. These unchanged bounds are approved for local POSIX File/SQLite +inert inspection by the [recorded maintainer decision](../architecture/rfcs/complete-state-recovery-v0.md#m1-capacity-default-decision). +Compound worst cases and broader platforms/providers remain unqualified; no +production SLO or merge authority follows from capacity consent. + +### Choosing backup inspection or a research checkpoint + +Use this Settings entry or `backup-state verify` to read a complete archive when +the original source is unavailable. Its tasks/history are historical evidence; +unknown components remain unknown, and the report does not resume work. +[Research checkpoints](research-checkpoint.md) instead capture a settled research +boundary and explicitly admit a distinct Goal/session before adopting context. +They already reuse backup inventory and SQLite snapshot owners, but their +component format is not accepted by `backup-state verify`, and a full archive +is not a research checkpoint input. + +The [shared-owner and staged recovery contract](../architecture/rfcs/complete-state-recovery-v0.md#backup-inspection-and-research-checkpoint-reuse) +keeps format/capture qualification in M2 and guided continuation in M3. No manual +format conversion can establish missing authority or consistency. Keep the +original archive and read the existing report's component states and holds; +automatic activation and the joint recovery journey are not available in M1. + +中文:完整备份检查用于原来源不可用时找回可读任务和历史;报告不恢复执行。 +研究 checkpoint 用于已稳定的研究边界,在独立 Goal/会话准入后显式采用上下文。 +两者已共用备份清单和 SQLite 快照,但格式不互换。缺失组件或捕获事实仍为未知, +不能手工改格式来取得权限;M2 负责资格,M3 才交付引导式继续工作,M1 保留原报告 +的状态和 hold 供读取。 ### Measured local capacity cases From aad964cd390307c3639f204342a54ecf504609c9 Mon Sep 17 00:00:00 2001 From: song Date: Thu, 15 Oct 2026 15:45:35 +0800 Subject: [PATCH 19/19] test(chat): preserve task back-navigation after closing details Signed-off-by: song --- examples/personal-workspace-browser/goal-workspace-m2.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/personal-workspace-browser/goal-workspace-m2.mjs b/examples/personal-workspace-browser/goal-workspace-m2.mjs index f7c782231b..a375d6fa52 100644 --- a/examples/personal-workspace-browser/goal-workspace-m2.mjs +++ b/examples/personal-workspace-browser/goal-workspace-m2.mjs @@ -77,9 +77,14 @@ export const goalWorkspaceM2Scenario = { await now.getByRole('button',{name:'按负责人',exact:true}).click(); await now.locator('[data-todo-id="showcase-reconcile"]').click(); await page.locator('.personal-drawer-close').click(); + await page.waitForFunction(() => !new URL(location.href).searchParams.has("todo")); + assert.equal(await page.locator('.personal-context-drawer').count(), 0); + // Closing details records the cleared selection. Back restores the task + // just closed, not an older selection from before the view switches. await page.goBack({waitUntil:"domcontentloaded"}); await page.locator('.personal-context-drawer').waitFor(); - assert.equal(new URL(page.url()).searchParams.get("todo"), "showcase-map-columns"); + assert.equal(new URL(page.url()).searchParams.get("todo"), "showcase-reconcile"); + assert.equal(await now.locator('[data-todo-id="showcase-reconcile"]').getAttribute('aria-pressed'), 'true'); await page.locator('.personal-drawer-close').click(); assert.equal(writes.length,0,`Read-only navigation made an effect request: ${writes}`); await page.screenshot({path:resolve(outputDir,'goal-workspace-m2-desktop.png'),animations:'disabled'});