Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .github/workflows/gitoxide-helper-admission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ on:
- '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__/gitoxide-managed-session-owner-internal.test.ts'
- 'packages/runtime-host/src/__tests__/gitoxide-managed-continuation-crash.test.ts'
- 'packages/runtime-host/src/__tests__/fixtures/execution-host*.ts'
- 'packages/runtime-host/src/__tests__/packaged-gitoxide-helper.test.ts'
- 'packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts'
- 'scripts/prepare-gitoxide-helper*'
Expand Down Expand Up @@ -63,6 +65,8 @@ on:
- '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__/gitoxide-managed-session-owner-internal.test.ts'
- 'packages/runtime-host/src/__tests__/gitoxide-managed-continuation-crash.test.ts'
- 'packages/runtime-host/src/__tests__/fixtures/execution-host*.ts'
- 'packages/runtime-host/src/__tests__/packaged-gitoxide-helper.test.ts'
- 'packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts'
- 'scripts/prepare-gitoxide-helper*'
Expand Down Expand Up @@ -126,6 +130,7 @@ jobs:
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
packages/runtime-host/dist/__tests__/gitoxide-managed-session-owner-internal.test.js
packages/runtime-host/dist/__tests__/gitoxide-managed-continuation-crash.test.js
packages/runtime-host/dist/__tests__/packaged-gitoxide-helper.test.js
packages/runtime-host/dist/__tests__/hosted-execution-tool-profile.test.js
- name: Test packaged helper preparation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Gitoxide workspace-bound continuation v1

## 目标

本切片只证明一个主要不变量:

> managed-coding-v1 的 continuation 必须同时绑定不可变 RuntimeEvent 前缀与同一 SQLite 事务读取到的 accepted Git workspace head;任一侧漂移都不得继续 provider dispatch。

普通 continuation 仍使用 `continuation_claim_v1`。Managed continuation 使用
`continuation_claim_v2` 和 `continuation_source_v3`,不存在从 v2 静默降级为 v1 的路径。

## Owner 与权限

- Runtime owns:RuntimeEvent high-water、provider replay manifest、target Run identity。
- SQLite owns:workspace epoch、accepted version/head、claim/start 的原子写入与重验。
- Gitoxide helper owns:source HEAD 与 `refs/maka/accepted` 的只读复核。
- Runtime Host owns:把 SQLite observation 与 Gitoxide observation 组合成 safety observation。

Host 只能通过 `execution_stores_workspace_continuation_authority_v1` 读取 continuation
boundary。该 opaque capability 不暴露 candidate、successor、T2 或 head 推进权限。

## 绑定内容

`managed_workspace_continuation_boundary_v1` 包含:

- storage root、repository、workspace、epoch、instance identity;
- source commit/tree;
- accepted workspace version、accepted event、revision;
- accepted commit/tree;
- materialization、policy、execution profile digest。

最终 `boundaryDigest` 对 Runtime boundary 与上述 workspace boundary 一起做 canonical
SHA-256 commitment。`replayManifestDigest` 仍只表示 RuntimeEvent lineage,不能与 composite
boundary digest 混用。

## 原子性边界

1. continuation plan 从 immutable RuntimeEvents 建立 Runtime boundary;
2. SQLite 在单个 read transaction 中读取并校验 epoch/head/version/projection;
3. Host 用短生命周期 Gitoxide helper 重验 source HEAD 与 accepted ref;
4. claim transaction 再次重验 workspace boundary 后写入 claim;
5. continuation-start transaction 再次重验 workspace boundary后写入首个 RuntimeEvent;
6. 只有以上步骤完成后允许 provider dispatch。

Git 与 SQLite 不组成跨系统事务。Git observation 只作为 admission proof;accepted truth 仍是
RuntimeEvents + 可重建 workspace authority projection。

## 失败状态与恢复

- boundary 缺失、helper 缺失、source/accepted-ref 漂移:park/fail closed;
- claim 已提交但 start 未提交:使用专用 repair writer,不调用 provider;
- start 已提交而 Host 退出:标记 `continuation_started_indeterminate`,禁止 provider replay;
- provider 已被调用但响应未知:保持 indeterminate,后续显式重试也不得再次调用 provider;
- malformed claim、row/payload mismatch、projection corruption:拒绝 session continuation authority。

回滚仅删除尚未提交的事务内状态。已经持久化的 claim/start 不回滚,通过重验与专用 repair
路径收敛。

## 平台矩阵

| 平台 | v1 承诺 |
| --- | --- |
| Linux | SQLite claim/start crash convergence;Gitoxide source/ref 重验;Host kill 后 provider-at-most-once |
| macOS | 与 Linux 相同;路径先 realpath canonicalize |
| Windows | 与 Linux 相同;进程终止证据由专用 Gitoxide workflow 执行 |

三平台共用 `.github/workflows/gitoxide-helper-admission.yml` 中的真实 helper/Host crash test。
没有系统 Git 或 PATH fallback。

## 产品范围

该能力只对显式 `managed-coding-v1` 生效。普通会话行为不变。当前 UI 是否开放该 profile 是独立
产品决策;协议与 Host consumer 已真实接通,不依赖 Desktop 控件存在。
13 changes: 13 additions & 0 deletions packages/core/src/__tests__/agent-run-continuation-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ describe('AgentRun continuation source decoding', () => {
/Invalid AgentRun header schema/,
);
});

it('accepts a V3 workspace-bound source with a distinct replay manifest digest', () => {
const source = {
...validV2ContinuationSource(),
protocol: 'continuation_source_v3' as const,
replayManifestDigest: `sha256:${'c'.repeat(64)}` as const,
};

assert.deepEqual(
decodeAgentRunHeader(headerWithContinuation(source)).continuationSource,
source,
);
});
});

function headerWithContinuation(
Expand Down
78 changes: 78 additions & 0 deletions packages/core/src/__tests__/runtime-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
buildImmutableRuntimePrefix,
createRuntimeBoundaryCursor,
decodeContinuationClaim,
digestWorkspaceBoundContinuationBoundary,
runtimePrefixSegment,
type RuntimeBoundaryCursorV1,
type RuntimePrefixIdentityV1,
Expand Down Expand Up @@ -298,8 +299,85 @@ describe('immutable RuntimeEvent boundary', () => {
/target turnId reuses source identity/,
);
});

it('binds a v2 continuation claim to one exact accepted workspace head', () => {
const boundary = boundaryForRuns('run-source');
const workspaceBoundary = workspaceBoundaryV1();
const claim = claimForBoundary(boundary);
const boundaryDigest = digestWorkspaceBoundContinuationBoundary(boundary, workspaceBoundary);

const decoded = decodeContinuationClaim({
...claim,
protocol: 'continuation_claim_v2',
boundaryDigest,
workspaceBoundary,
targetRunHeader: {
...claim.targetRunHeader,
continuationSource: {
...claim.targetRunHeader.continuationSource,
protocol: 'continuation_source_v3',
boundaryDigest,
},
},
});

assert.equal(decoded.protocol, 'continuation_claim_v2');
if (decoded.protocol !== 'continuation_claim_v2') return;
assert.deepEqual(decoded.workspaceBoundary, workspaceBoundary);
});

it('rejects a v2 claim when the accepted workspace version changes', () => {
const boundary = boundaryForRuns('run-source');
const workspaceBoundary = workspaceBoundaryV1();
const claim = claimForBoundary(boundary);
const boundaryDigest = digestWorkspaceBoundContinuationBoundary(boundary, workspaceBoundary);

assert.throws(
() =>
decodeContinuationClaim({
...claim,
protocol: 'continuation_claim_v2',
boundaryDigest,
workspaceBoundary: {
...workspaceBoundary,
workspaceVersionId: `version_${'9'.repeat(32)}`,
},
targetRunHeader: {
...claim.targetRunHeader,
continuationSource: {
...claim.targetRunHeader.continuationSource,
protocol: 'continuation_source_v3',
boundaryDigest,
},
},
}),
/workspace boundary digest mismatch/,
);
});
});

function workspaceBoundaryV1() {
return {
protocol: 'managed_workspace_continuation_boundary_v1' as const,
storageRootId: '1'.repeat(64),
repositoryId: `repository_${'2'.repeat(32)}`,
workspaceId: `workspace_${'3'.repeat(32)}`,
workspaceEpochId: `epoch_${'4'.repeat(32)}`,
workspaceInstanceId: `instance_${'5'.repeat(32)}`,
workspaceVersionId: `version_${'6'.repeat(32)}`,
acceptedEventId: 'workspace-accepted-event-1',
revision: 2,
objectFormat: 'sha1' as const,
sourceCommitOid: '5'.repeat(40),
sourceTreeOid: '6'.repeat(40),
commitOid: '7'.repeat(40),
treeOid: '8'.repeat(40),
materializationProfileDigest: `sha256:${'9'.repeat(64)}` as const,
policyHash: `sha256:${'a'.repeat(64)}` as const,
executionProfileDigest: `sha256:${'b'.repeat(64)}` as const,
};
}

function runtimeIdentity(runId: string): RuntimePrefixIdentityV1 {
return {
sessionId: 'session-1',
Expand Down
45 changes: 39 additions & 6 deletions packages/core/src/agent-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,20 @@ export interface AgentRunContinuationSourceV2 extends AgentRunContinuationSource
replayManifestDigest: `sha256:${string}`;
}

export interface AgentRunContinuationSourceV3 extends AgentRunContinuationSourceV1 {
protocol: 'continuation_source_v3';
claimId: string;
/** Composite RuntimeEvent + accepted workspace boundary identity. */
boundaryDigest: `sha256:${string}`;
sourcePrefixDigest: `sha256:${string}`;
/** RuntimeEvent-only replay lineage identity. */
replayManifestDigest: `sha256:${string}`;
}

export type AgentRunContinuationSource =
| AgentRunContinuationSourceV1
| AgentRunContinuationSourceV2;
| AgentRunContinuationSourceV2
| AgentRunContinuationSourceV3;

export type RootExecutionDescriptor =
| {
Expand Down Expand Up @@ -104,6 +115,7 @@ export type RootExecutionDescriptor =
sourceRuntimeEventHighWater: number;
claimId: string;
boundaryDigest: `sha256:${string}`;
replayManifestDigest?: `sha256:${string}`;
providerReplayDigest: `sha256:${string}`;
safetyDigest: `sha256:${string}`;
targetInvocationId: string;
Expand Down Expand Up @@ -150,6 +162,20 @@ const AGENT_RUN_CONTINUATION_SOURCE_V2_SHAPE = defineObjectShape<AgentRunContinu
],
[],
);
const AGENT_RUN_CONTINUATION_SOURCE_V3_SHAPE = defineObjectShape<AgentRunContinuationSourceV3>()(
[
'protocol',
'claimId',
'boundaryDigest',
'sourceInvocationId',
'sourceRunId',
'sourceTurnId',
'sourceRuntimeEventHighWater',
'sourcePrefixDigest',
'replayManifestDigest',
],
[],
);

export interface AgentRunHeader {
runId: string;
Expand Down Expand Up @@ -282,14 +308,16 @@ export function agentRunMatchesHostedRootExecution(
run.parentTurnId === execution.sourceTurnId &&
source !== undefined &&
'protocol' in source &&
source.protocol === 'continuation_source_v2' &&
(source.protocol === 'continuation_source_v2' ||
source.protocol === 'continuation_source_v3') &&
source.sourceInvocationId === execution.sourceInvocationId &&
source.sourceRunId === execution.sourceRunId &&
source.sourceTurnId === execution.sourceTurnId &&
source.sourceRuntimeEventHighWater === execution.sourceRuntimeEventHighWater &&
source.claimId === execution.claimId &&
source.boundaryDigest === execution.boundaryDigest &&
source.replayManifestDigest === execution.boundaryDigest &&
source.replayManifestDigest ===
(execution.replayManifestDigest ?? execution.boundaryDigest) &&
run.resumedFromRunId === undefined &&
run.retriedFromRunId === undefined &&
run.agentId === undefined &&
Expand Down Expand Up @@ -718,9 +746,14 @@ function isAgentRunContinuationSource(value: unknown): value is AgentRunContinua
value.sourceRuntimeEventHighWater >= 0;
if (!common) return false;
if (hasExactShape(value, AGENT_RUN_CONTINUATION_SOURCE_V1_SHAPE)) return true;
return (
const isV2 =
hasExactShape(value, AGENT_RUN_CONTINUATION_SOURCE_V2_SHAPE) &&
value.protocol === 'continuation_source_v2' &&
value.protocol === 'continuation_source_v2';
const isV3 =
hasExactShape(value, AGENT_RUN_CONTINUATION_SOURCE_V3_SHAPE) &&
value.protocol === 'continuation_source_v3';
return (
(isV2 || isV3) &&
typeof value.claimId === 'string' &&
value.claimId.length > 0 &&
typeof value.sourceInvocationId === 'string' &&
Expand All @@ -734,7 +767,7 @@ function isAgentRunContinuationSource(value: unknown): value is AgentRunContinua
isSha256Digest(value.boundaryDigest) &&
isSha256Digest(value.sourcePrefixDigest) &&
isSha256Digest(value.replayManifestDigest) &&
value.replayManifestDigest === value.boundaryDigest
(!isV2 || value.replayManifestDigest === value.boundaryDigest)
);
}

Expand Down
Loading
Loading