Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/gitoxide-helper-admission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ on:
- 'packages/runtime-host/src/__tests__/gitoxide-helper-*.test.ts'
- 'packages/runtime-host/src/server/gitoxide-repository-admission-authority-internal.ts'
- 'packages/runtime-host/src/server/gitoxide-mutation-candidate-receipt-authority-internal.ts'
- 'packages/runtime-host/src/server/gitoxide-managed-write-edit-owner-internal.ts'
- 'packages/runtime-host/src/__tests__/gitoxide-repository-admission-authority-internal.test.ts'
- 'packages/runtime-host/src/__tests__/gitoxide-managed-write-edit-owner-internal.test.ts'
- 'packages/runtime-host/src/__tests__/fixtures/gitoxide-candidate-receipt-crash-child.ts'
- 'packages/runtime-host/src/__tests__/fixtures/gitoxide-managed-write-edit-owner-crash-child.ts'
- 'packages/storage/src/execution-stores-workspace-authority-internal.ts'
- 'packages/storage/src/workspace-version-authority-internal.ts'
- 'packages/storage/src/sqlite-runtime-store.ts'
- 'packages/runtime/package.json'
- 'docs/architecture/gitoxide-*.md'
push:
Expand All @@ -40,8 +46,14 @@ on:
- 'packages/runtime-host/src/__tests__/gitoxide-helper-*.test.ts'
- 'packages/runtime-host/src/server/gitoxide-repository-admission-authority-internal.ts'
- 'packages/runtime-host/src/server/gitoxide-mutation-candidate-receipt-authority-internal.ts'
- 'packages/runtime-host/src/server/gitoxide-managed-write-edit-owner-internal.ts'
- 'packages/runtime-host/src/__tests__/gitoxide-repository-admission-authority-internal.test.ts'
- 'packages/runtime-host/src/__tests__/gitoxide-managed-write-edit-owner-internal.test.ts'
- 'packages/runtime-host/src/__tests__/fixtures/gitoxide-candidate-receipt-crash-child.ts'
- 'packages/runtime-host/src/__tests__/fixtures/gitoxide-managed-write-edit-owner-crash-child.ts'
- 'packages/storage/src/execution-stores-workspace-authority-internal.ts'
- 'packages/storage/src/workspace-version-authority-internal.ts'
- 'packages/storage/src/sqlite-runtime-store.ts'
- 'packages/runtime/package.json'
- 'docs/architecture/gitoxide-*.md'

Expand Down Expand Up @@ -93,3 +105,4 @@ jobs:
packages/runtime-host/dist/__tests__/gitoxide-helper-artifact-authority-internal.test.js
packages/runtime-host/dist/__tests__/gitoxide-helper-invocation-internal.test.js
packages/runtime-host/dist/__tests__/gitoxide-repository-admission-authority-internal.test.js
packages/runtime-host/dist/__tests__/gitoxide-managed-write-edit-owner-internal.test.js
72 changes: 72 additions & 0 deletions docs/architecture/gitoxide-managed-write-edit-owner-v1.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Gitoxide managed Write/Edit owner v1

## 主要不变量

一次 managed Write/Edit 在 T1 前绑定 durable workspace epoch、canonical head、单一路径和固定执行
profile。T1 后只能收敛到以下三类 durable 结果之一:

- 无变化或已证明无副作用的失败,由 SQLite 原子提交 terminal T2 并释放 reservation;
- exact result 被固化为 operation-bound Gitoxide candidate,由 SQLite 原子提交 T2、successor 和
canonical head,再将 `refs/maka/accepted` CAS 到该 candidate;
- 证据不完整或状态无法确定时保持 unsettled,禁止 generic T2、fallback 和工具副作用重放。

## Owner 与权限

- Runtime 独占原始 Write/Edit 参数和 provider result。Host 只能读取 Runtime-issued immutable
operation proof,不能返回或替换 provider result。
- Execution Stores authority 从 durable epoch 读取 `workspaceInstanceId`。调用者不能自报 reservation
identity。
- Gitoxide repository authority 只从 exact accepted commit/tree 读取 base content;Write/Edit 是
`F(immutable base, Runtime-owned args)` 的纯变换,不读写 live checkout。
- Candidate receipt authority 拥有 candidate ref、receipt 和 exact retry。
- SQLite workspace authority 是 accepted truth owner。只有成功的 successor transaction 才签发
owner-bound projection capability。
- Gitoxide helper 只消费该 capability 做 accepted-ref CAS;裸路径、OID、receipt 或 caller object 都不能
推进 accepted ref。

## 原子性与恢复边界

Git ref 与 SQLite 无法组成一个物理事务,因此 v1 采用两个有序线性化点:

1. SQLite transaction 提交 tool outcome、workspace successor、head CAS,并释放 T1 reservation;
2. Gitoxide helper 将 accepted ref 从 exact base CAS 到 exact candidate。

进程若死在两者之间,新 owner 按 operation ID 从 `tool_operations` 主键和 RuntimeEvent event ID 读取
exact call、dispatch、outcome。该读取是有界主键查询,不需要 schema 15 表达式索引,也不扫描完整账本。
恢复随后:

1. 以 durable parent head 重开 accepted repository;
2. 从 immutable base 和 durable call args 重新计算 pure transform;
3. exact-replay candidate receipt/ref;
4. exact-replay 已提交的 SQLite successor,以重新签发 process-local projection capability;
5. 重放 accepted-ref CAS。

恢复不会调用普通工具实现,也不会产生第二次文件系统副作用。

## 失败状态与回滚

| 状态 | 行为 |
| --- | --- |
| T1 前路径、epoch、head、version 或 helper admission 不匹配 | 拒绝进入 T1 |
| T1 后 Runtime 证明 no-change/no-effect failure | 原子 terminal T2,释放 reservation,不推进 head |
| candidate/receipt 与 base、path、content 或 profile 不匹配 | unsettled;保留 reservation 或 accepted truth,禁止覆盖 |
| SQLite successor 未提交 | 不签发 projection capability,candidate 只是未接受 artifact |
| SQLite successor 已提交、accepted ref 仍为 base | 重建 proof 并重放 CAS,不重跑工具 |
| accepted ref 已为 candidate | exact replay success |
| accepted ref 为第三值或 durable evidence 不一致 | fail closed,不 reset、不自动覆盖 |

## 平台能力矩阵

| 平台 | v1 承诺 |
| --- | --- |
| Linux | short-lived Gitoxide helper、SQLite crash convergence、exact candidate/ref replay |
| macOS | 与 Linux 相同的进程崩溃合同;不承诺 `fsync` 之外的断电语义 |
| Windows | 相同的 SQLite/ref 协议;successor-commit 后的子进程 exit/reopen 已进入三平台 Gitoxide helper workflow |

三平台均不依赖 system Git、bundled Git CLI、linked worktree rotation 或 live checkout 写权限。

## 当前交付边界

本切片提供 owner 与真实 helper/SQLite 合同测试,但尚不自行创建 Desktop/CLI managed coding session。
产品组合与 continuation 分别属于后续交付;它们只能消费这里签发的窄 capability,不能重新开放裸路径或
caller-provided execution profile。
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,14 @@ Gitoxide helper 与 npm producer 分别保留自己的 trust root。packaged Hos
- 延期 projection quarantine GC 到 projection owner 自己的 lifecycle 交付;
- 延期 dependency environment 与 Write/Edit 恢复的耦合;纯文件 transform 不应被 npm producer 阻塞;
- 禁止为使旧 PR 可编译而恢复 v1/v2 fallback。

## 7. 当前重建状态

- R0:完成。Runtime 不再接受 Host 提交的 `providerResult`;运行时夹带字段也会被忽略。
- R1:完成。candidate receipt/ref 支持 exact replay 与 source import retry。
- R2:owner 主链已完成:durable epoch/head、pure transform、candidate、SQLite successor 与 accepted-ref
projection 已串联;真实子进程会在 SQLite successor 提交后直接退出,新 owner 只从 durable evidence
重建 candidate 并推进 accepted ref。该用例已进入三平台 Gitoxide helper workflow。
- R3:accepted-ref projection 已实现为 accepted truth 的派生 CAS;filesystem checkout projection 仍保持
独立延期,不参与 canonical Write/Edit read/write。
- R4、R5:尚未在新基线上重建。
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { createHash } from 'node:crypto';
import { readFile, realpath, stat } from 'node:fs/promises';
import { canonicalToolArgsHash } from '@maka/core/tool-args-identity';
import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores';
import {
discoverMarkedStorageRoot,
tryAcquireInteractiveRootOwner,
} from '@maka/storage/root-authority';
import {
admitGitoxideHelperArtifactInternal,
issueGitoxideHelperReleaseArtifactClaimInternal,
} from '../../server/gitoxide-helper-artifact-authority-internal.js';
import { createGitoxideManagedWriteEditOwnerInternal } from '../../server/gitoxide-managed-write-edit-owner-internal.js';

interface Fixture {
readonly storageRoot: string;
readonly repositoryPath: string;
readonly helperPath: string;
readonly workspaceId: string;
readonly workspaceEpochId: string;
readonly operationId: string;
readonly toolCallId: string;
readonly args: {
readonly path: string;
readonly content: string;
};
}

const fixturePath = process.argv[2];
if (!fixturePath) throw new Error('Missing managed Write crash fixture path');
const fixture = JSON.parse(await readFile(fixturePath, 'utf8')) as Fixture;
const rootCapability = await discoverMarkedStorageRoot({ path: fixture.storageRoot });
if (rootCapability.kind !== 'interactive') throw new Error('Crash fixture root kind is invalid');
const rootOwner = await tryAcquireInteractiveRootOwner(rootCapability);
if (!rootOwner) throw new Error('Crash fixture could not acquire the storage root');
const stores = await openInteractiveExecutionStoresForWrite(rootOwner.lease);

const helperPath = await realpath(fixture.helperPath);
const helperBytes = await readFile(helperPath);
const helperInfo = await stat(helperPath);
const releaseOwnerToken = {};
const invocationOwnerToken = {};
const claim = issueGitoxideHelperReleaseArtifactClaimInternal(releaseOwnerToken, {
executablePath: helperPath,
expectedSha256: `sha256:${createHash('sha256').update(helperBytes).digest('hex')}`,
expectedBytes: helperInfo.size,
platform: process.platform,
arch: process.arch,
protocolVersion: 1,
supportedOperations: [
'inspect_repository',
'import_source_head',
'create_candidate',
'promote_candidate',
'observe_accepted_ref',
'read_tree_file',
],
});
const helperCapability = await admitGitoxideHelperArtifactInternal({
releaseOwnerToken,
invocationOwnerToken,
claim,
});
const owner = createGitoxideManagedWriteEditOwnerInternal({
storageRootLease: rootOwner.lease,
stores,
invocationOwnerToken,
helperCapability,
repositoryPath: fixture.repositoryPath,
workspaceId: fixture.workspaceId,
workspaceEpochId: fixture.workspaceEpochId,
failpoint(point) {
if (point === 'after_workspace_successor_commit') process.exit(73);
},
});
const admission = await owner.admitManagedMutation({
operationId: fixture.operationId,
toolName: 'Write',
persistedArgs: fixture.args,
abortSignal: new AbortController().signal,
});
if (admission.immutableBase?.content !== 'before\n') {
throw new Error('Crash fixture opened a different immutable base');
}
const identity = {
sessionId: 'session-real-write',
invocationId: 'invocation-real-write',
runId: 'run-real-write',
turnId: 'turn-real-write',
};
const canonicalArgsHash = canonicalToolArgsHash('Write', fixture.args);
await stores.runtimeEventStore.commitToolPrepared({
operationId: fixture.operationId,
journalEventId: `${fixture.operationId}_prepared`,
runtimeEvent: {
id: 'call-event-real-write',
...identity,
ts: 2,
partial: false,
role: 'model',
author: 'agent',
content: {
kind: 'function_call',
id: fixture.toolCallId,
name: 'Write',
args: fixture.args,
},
refs: { operationId: fixture.operationId, toolCallId: fixture.toolCallId },
},
dispatchRuntimeEvent: {
id: 'dispatch-real-write',
...identity,
ts: 2,
partial: false,
role: 'system',
author: 'system',
actions: {
toolDispatch: {
protocol: 't1_after_preflight_v1',
operationId: fixture.operationId,
providerToolCallId: fixture.toolCallId,
toolName: 'Write',
canonicalArgsHash,
recoveryMode: 'reconcile',
managedMutation: admission.durableDispatch,
},
},
refs: { operationId: fixture.operationId, toolCallId: fixture.toolCallId },
},
providerToolCallId: fixture.toolCallId,
toolName: 'Write',
canonicalArgsHash,
recoveryMode: 'reconcile',
committedAt: 2,
});
const durableOutcome = {
id: 'outcome-event-real-write',
...identity,
ts: 3,
partial: false,
role: 'tool' as const,
author: 'tool' as const,
content: {
kind: 'function_response' as const,
id: fixture.toolCallId,
name: 'Write',
result: {
kind: 'json' as const,
value: { kind: 'file_write', path: fixture.args.path },
},
},
refs: { operationId: fixture.operationId, toolCallId: fixture.toolCallId },
};
await admission.execute(async () => ({
content: durableOutcome.content.result,
isError: false,
durationMs: 1,
mutationResult: {
path: fixture.args.path,
content: fixture.args.content,
changed: true,
},
durableOutcome,
}));
throw new Error('Crash fixture did not stop after the durable successor commit');
Loading
Loading