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
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Read / Glob / Grep / Write / Edit / ManagedNodeTest
```

- Read/Glob/Grep:`replay_safe`,读取 accepted tree;
- Write/Edit:`reconcile + managed_mutation_v1`;
- Write/Edit:`reconcile + managed_mutation_v2`;
- ManagedNodeTest:`replay_safe + managed_observation_v2`;dependency 输入只能是显式 `none` 或 owner-bound
immutable snapshot lease,禁止从 checkout `node_modules` 回退;
- Bash、npm、package script、PATH executable 与 attached checkout 均不在 profile 内。
Expand Down
80 changes: 80 additions & 0 deletions docs/architecture/managed-node-transform-kernel-v1.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Managed Node workspace transform kernel v1

status: enabling infrastructure

milestone: M5 workspace transform

## 1. 主要不变量

一次 `ManagedNodeTransform` 必须是下面这条纯转换链:

```text
exact accepted Git tree + exact entry + exact argv
-> sandboxed Node process
-> one owner-selected bounded UTF-8 output
-> immutable Gitoxide candidate
-> atomic SQLite successor acceptance
-> accepted ref projection
```

transform 不能直接写 managed worktree、accepted ref、用户 checkout 或任意调用者路径。模型只能选择 accepted
tree 中的显式 Node entry、一个 canonical output path 和有界 argv;物理 output path、cwd、environment、toolchain、
sandbox、timeout 与 candidate publication 都由 owner 决定。

本切片只建立 kernel。packaged manifest/profile v4、Desktop pre-Session negotiation 与真实 Host kill/reopen 属于
独立产品组合切片;在该切片落地前,生产 Host 不宣告此工具。

## 2. Owner 与权限

- accepted input owner:Gitoxide managed session;
- execution-root owner:短生命周期 accepted-tree materialization 与 scratch;
- toolchain/process owner:opaque toolchain capability 与 `ManagedCommandSandboxOwnerInternal`;
- transform admission owner:`ManagedNodeTransformOwnerInternal`;
- candidate/projection owner:Gitoxide mutation candidate authority;
- accepted truth owner:SQLite RuntimeEvents 与 workspace successor transaction;
- provider result owner:Runtime 的有界 immutable snapshot。

`managed_mutation_v3` 在 T1 前绑定 exact accepted head、workspace epoch、output path、entry bytes/hash、argv、
toolchain identity 与 execution profile。`managed_mutation_v2` 继续表达 Write/Edit 的参数形状;两种 RuntimeEvent
payload 都只允许同一个 canonical mutation execution profile v2 digest。payload protocol 用于区分 operation
proof 的字段,不再形成两套 execution authority,也没有 v1 digest 兼容路径。

## 3. 原子性边界

T1 前:

1. 验证当前 durable epoch/head;
2. materialize exact accepted tree 到 owner root;
3. 观察 entry identity并验证 `workspace_transform_v1` toolchain capability;
4. 冻结 v3 dispatch 与唯一 operation capability;
5. SQLite 在同一 T1 transaction 写 call、dispatch 与 exclusive mutation reservation。

T1 后:

1. Node 只读取 accepted materialization,只能写 scratch 中 owner 指定的 `MAKA_OUTPUT_PATH`;
2. `PATH=''`,child process 禁止,network restricted;
3. root process 完全退出后读取一个普通、非 symlink、最多 1 MiB 的 strict UTF-8 文件;
4. entry 前后 identity、output bytes 与 SHA-256 必须一致;
5. Gitoxide 把 exact output content 固化为 immutable candidate;
6. SQLite 原子提交 T2、successor fact、canonical head,并释放 reservation;
7. accepted ref 是可重建 projection,promotion 失败不得回滚已经接受的 Runtime truth。

## 4. 失败状态与收敛

- T1 前失败:删除 execution roots,不写 durable mutation;
- T1 后、process dispatch 前后失败:没有充分 terminal evidence 时保持 reservation 并 park;禁止 generic T2;
- transform 返回 no-change:提交 `no_workspace_change` terminal 并释放 reservation;
- candidate 已发布、SQLite 未接受:retry/recovery 重验 exact operation、profile、path 与 candidate receipt;
- SQLite 已接受、ref 未 promotion:只重放 projection,不重跑 Node transform;
- cleanup 失败:execution root 是 disposable artifact,不改变 accepted truth,由 maintenance 回收。

## 5. 平台矩阵

| 平台 | kernel 合同 | 产品 gate |
| --- | --- | --- |
| Linux | Node permission + enforcing OS sandbox;单一输出;Gitoxide/SQLite 收敛 | v4 real Host kill/reopen |
| macOS | 与 Linux 相同;路径先 realpath/canonicalize | v4 real Host kill/reopen |
| Windows | Node permission 限制 input/child;network 仍依赖外层 enforcing sandbox | 未有完整证据前 profile unavailable |

v1 只承诺 process-crash convergence,不承诺硬件断电持久性。任何平台无法证明 sandbox profile 时必须在 T1 前
报告 unavailable,不能回落到普通 Node、Bash 或用户 checkout。
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,8 @@ Host 后,新的 Run 只采用 durable outcome/candidate/evidence;已完成

每个 PR 必须列出 owner、原子性边界、失败状态、回滚/收敛方式和平台矩阵。CI 全绿只表示已布置用例通过;
并发、崩溃与数据安全仍需单独论证。

当前 workspace-transform kernel 已采用 `managed_mutation_v3`:受限 Node 进程只产生一个 owner-selected UTF-8
输出,Gitoxide candidate 与 SQLite successor 才能把它纳入 accepted history。它不直接写 worktree,也不让
caller 选择 executable/environment。下一切片才加入 packaged profile v4、Desktop negotiation 与 Host crash
gate;之后再推进 external-effect fencing。详见 `managed-node-transform-kernel-v1.zh-CN.md`。
101 changes: 77 additions & 24 deletions packages/core/src/__tests__/runtime-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ import {
MANAGED_OBSERVATION_EXECUTION_PROFILE_V2_SPEC,
MANAGED_OBSERVATION_EXECUTION_PROFILE_V3_DIGEST,
MANAGED_OBSERVATION_EXECUTION_PROFILE_V3_SPEC,
MANAGED_MUTATION_EXECUTION_PROFILE_V1_DIGEST,
MANAGED_MUTATION_EXECUTION_PROFILE_V1_SPEC,
MANAGED_MUTATION_EXECUTION_PROFILE_V2_DIGEST,
MANAGED_MUTATION_EXECUTION_PROFILE_V2_SPEC,
runtimeEventHasModelVisibleContent,
type RuntimeEvent,
type RuntimeEventActions,
Expand Down Expand Up @@ -750,30 +750,17 @@ describe('RuntimeEvent actions', () => {
);
});

test('binds the managed mutation digest to its canonical execution semantics', () => {
const canonicalProfile = JSON.stringify({
protocol: 'managed_mutation_execution_profile_v1',
toolNames: ['Write', 'Edit'],
transform: 'pure_frozen_args_only_v1',
objectFormat: 'sha1',
pathPolicyVersion: 3,
resultSnapshot: {
maxBytes: 1_048_576,
maxDepth: 64,
maxNodes: 65_536,
maxProperties: 65_536,
maxArrayLength: 65_536,
format: 'strict_json_v1',
},
terminalAuthority: 'owner_committed_exact_outcome_v1',
genericFallback: 'forbidden',
});

test('binds every managed mutation to one canonical v2 execution profile', () => {
const canonicalProfile = JSON.stringify(MANAGED_MUTATION_EXECUTION_PROFILE_V2_SPEC);
assert.equal(
MANAGED_MUTATION_EXECUTION_PROFILE_V1_DIGEST,
MANAGED_MUTATION_EXECUTION_PROFILE_V2_DIGEST,
`sha256:${createHash('sha256').update(canonicalProfile).digest('hex')}`,
);
assert.equal(JSON.stringify(MANAGED_MUTATION_EXECUTION_PROFILE_V1_SPEC), canonicalProfile);
assert.deepEqual(MANAGED_MUTATION_EXECUTION_PROFILE_V2_SPEC.toolNames, [
'Write',
'Edit',
'ManagedNodeTransform',
]);
});

test('decodes only a platform-independent T1-frozen managed mutation identity', () => {
Expand All @@ -791,7 +778,7 @@ describe('RuntimeEvent actions', () => {
baseTreeOid: '2'.repeat(40),
expectedPath: 'src/a.ts',
pathPolicyVersion: 3,
executionProfileDigest: MANAGED_MUTATION_EXECUTION_PROFILE_V1_DIGEST,
executionProfileDigest: MANAGED_MUTATION_EXECUTION_PROFILE_V2_DIGEST,
} as const;
const toolDispatch = {
protocol: 't1_after_preflight_v1',
Expand Down Expand Up @@ -832,6 +819,72 @@ describe('RuntimeEvent actions', () => {
}
});

test('binds one sandboxed Node transform and its output path before T1', () => {
const canonicalProfile = JSON.stringify(MANAGED_MUTATION_EXECUTION_PROFILE_V2_SPEC);
assert.equal(
MANAGED_MUTATION_EXECUTION_PROFILE_V2_DIGEST,
`sha256:${createHash('sha256').update(canonicalProfile).digest('hex')}`,
);
const managedMutation = {
protocol: 'managed_mutation_v3',
repositoryId: 'repository_11111111111111111111111111111111',
workspaceId: 'workspace_22222222222222222222222222222222',
workspaceEpochId: 'epoch_33333333333333333333333333333333',
workspaceInstanceId: 'instance_44444444444444444444444444444444',
objectFormat: 'sha1',
baseWorkspaceVersionId: 'version_55555555555555555555555555555555',
baseAcceptedEventId: 'baseline-event-1',
baseHeadRevision: 1,
baseCommitOid: '1'.repeat(40),
baseTreeOid: '2'.repeat(40),
expectedPath: 'generated/output.json',
pathPolicyVersion: 3,
operationKind: 'node_transform_v1',
executionProfileDigest: MANAGED_MUTATION_EXECUTION_PROFILE_V2_DIGEST,
toolchainIdentityDigest: `sha256:${'a'.repeat(64)}`,
entry: {
relativePath: 'scripts/generate.mjs',
bytes: 42,
sha256: `sha256:${'b'.repeat(64)}`,
},
args: ['--format', 'json'],
} as const;
const toolDispatch = {
protocol: 't1_after_preflight_v1',
operationId: 'operation-transform-1',
providerToolCallId: 'call-transform-1',
toolName: 'ManagedNodeTransform',
canonicalArgsHash: `sha256:${'c'.repeat(64)}`,
recoveryMode: 'reconcile',
managedMutation,
} as const;
assert.deepEqual(
decodeRuntimeEvent(baseEvent({ role: 'system', author: 'system', actions: { toolDispatch } }))
.actions?.toolDispatch?.managedMutation,
managedMutation,
);
for (const invalid of [
{ ...managedMutation, operationKind: 'node_command_v3' },
{
...managedMutation,
executionProfileDigest: `sha256:${'d'.repeat(64)}`,
},
{ ...managedMutation, entry: { ...managedMutation.entry, relativePath: 'README.md' } },
{ ...managedMutation, args: ['x'.repeat(4097)] },
{ ...managedMutation, extra: true },
]) {
assert.throws(() =>
decodeRuntimeEvent(
baseEvent({
role: 'system',
author: 'system',
actions: { toolDispatch: { ...toolDispatch, managedMutation: invalid } as never },
}),
),
);
}
});

test('permission and user-question interactions are first-class actions', () => {
const actions: RuntimeEventActions = {
permissionRequest: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('workspace version authority contract', () => {
changedFileCount: 1,
deletedFileCount: 0,
executionProfileDigest:
'sha256:ffdfdda9cf38f382e0c4db81dac7319cd33586a6c65051a97a15e6c41b88f825' as const,
'sha256:7ff4eb75e8833f7bf97eaa252f47316f609093d89aa32acdeae7fc6caaa11a92' as const,
},
},
},
Expand Down Expand Up @@ -440,7 +440,7 @@ function historySuccessorInput(): WorkspaceHistorySuccessorAuthorityInput {
changedFileCount: 1,
deletedFileCount: 0,
executionProfileDigest:
'sha256:ffdfdda9cf38f382e0c4db81dac7319cd33586a6c65051a97a15e6c41b88f825',
'sha256:7ff4eb75e8833f7bf97eaa252f47316f609093d89aa32acdeae7fc6caaa11a92',
},
origin: {
restoreId: 'restore_77777777777777777777777777777777',
Expand Down Expand Up @@ -496,7 +496,7 @@ function successorInput(): WorkspaceSuccessorAuthorityInput {
changedFileCount: 1,
deletedFileCount: 0,
executionProfileDigest:
'sha256:ffdfdda9cf38f382e0c4db81dac7319cd33586a6c65051a97a15e6c41b88f825' as const,
'sha256:7ff4eb75e8833f7bf97eaa252f47316f609093d89aa32acdeae7fc6caaa11a92' as const,
},
origin: {
operationId: 'operation-successor-1',
Expand Down
Loading
Loading