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
76 changes: 76 additions & 0 deletions docs/architecture/managed-hermetic-node-command-kernel-v1.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Managed hermetic Node command kernel v1

status: enabling infrastructure

milestone: M5 foreground command loop

## 1. 主要不变量

一次 `ManagedNodeRun` 只能把一个 immutable accepted Git tree 中的显式 `.js`、`.mjs` 或 `.cjs`
入口作为受限 Node 进程执行。入口路径、exact argv、accepted head、entry bytes/hash、toolchain identity 与
execution profile 必须在 T1 前由 admission owner 冻结;T1 后禁止切换到普通 Bash、`PATH`、source checkout、
依赖发现或 generic tool boundary。

本切片只建立 kernel 与 admission,不让现有 Host 宣告 `managed-coding-v3`。下一产品组合切片必须同时提供
packaged toolchain claim、Host profile negotiation 与 production-shaped crash test,才允许 Desktop 创建 v3
Session。

## 2. Owner 与权限

- accepted content owner:Gitoxide managed session;
- execution-root owner:Storage root 下的短生命周期 input/scratch lease;
- toolchain owner:opaque managed-toolchain invocation capability;
- process owner:`ManagedCommandSandboxOwnerInternal`;
- durable fact owner:Runtime T1/T2;
- provider result owner:Runtime 捕获的 strict bounded snapshot。

模型只能提交:

```ts
{
entryPath: string;
args?: string[];
}
```

模型不能提交 executable、cwd、environment、Node flags、sandbox preference、dependency root、timeout、网络策略
或输出上限。

## 3. 原子性边界

T1 前:

1. 读取 accepted boundary;
2. materialize exact accepted commit/tree 到 owner root;
3. 观察 entry identity;
4. 验证 Node 24 toolchain capability;
5. 冻结 entry 与 argv;
6. 持久化 `managed_observation_v3`。

T1 后:

1. 只在 read-only accepted input + disposable scratch 上启动一个 root process;
2. `PATH=''`,不授予 child process,外层 sandbox 要求 `network: restricted`;
3. entry 执行前后 identity 必须相同;
4. stdout/stderr 各最多 32 KiB,完整 provider result 最多 64 KiB;
5. Runtime 提交 T2 后才发布 live result。

非零 exit code 是命令 observation 的数据,不是 authority failure。timeout、abort、sandbox unavailable、输出溢出、
entry drift 或 toolchain drift 都是未取得合法 observation 的失败。

## 4. 失败与回滚

- T1 前失败:删除 input/scratch,拒绝 dispatch;
- T1 后、T2 前失败:保留 durable T1,按 replay-safe observation 恢复;命令不能写 accepted content 或外部世界;
- T2 后响应丢失:采用 durable outcome,不重新发布第二个事实;
- cleanup 失败:不改变 durable outcome,后续由 execution-root maintenance 回收 disposable roots。

## 5. 平台矩阵

| 平台 | kernel | product availability |
| --- | --- | --- |
| Linux | Node permission + enforcing OS sandbox | 待 v3 Host crash gate |
| macOS | Node permission + enforcing OS sandbox | 待 v3 Host crash gate |
| Windows | Node permission 可限制输入写入/child;网络仍依赖外层 sandbox | 在完整网络隔离证据前不得宣告 v3 |

没有平台证据时必须报告 profile unavailable,禁止回落到普通 Node/Bash。
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ exit status、test summary 与 artifact digest。缓存是 projection;test out
Windows 当前只获得 v1,禁止执行时降级;
5. 需要外部包的项目在 M5.3 capability 可用前明确 unavailable,禁止静默降级。

当前下一步采用更窄的 `ManagedNodeRun` kernel:只运行 accepted tree 中的显式 Node 入口,exact argv 在 T1
前冻结,写入仅限 disposable scratch。它不等同于 Bash,也不发现 package scripts 或 `PATH` 工具。kernel
完成后,独立产品组合切片才把 `managed-coding-v3` 加入 Host negotiation,并用真实 Host kill/restart 证明
完成结果不重放。详见 `managed-hermetic-node-command-kernel-v1.zh-CN.md`。

### M5.5 External-effect fencing

外部调用必须在 T1 前绑定 operation id/idempotency key、目标 authority 与 reconciliation contract。远端没有
Expand Down
84 changes: 84 additions & 0 deletions packages/core/src/__tests__/runtime-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
MANAGED_OBSERVATION_EXECUTION_PROFILE_V1_SPEC,
MANAGED_OBSERVATION_EXECUTION_PROFILE_V2_DIGEST,
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,
runtimeEventHasModelVisibleContent,
Expand Down Expand Up @@ -666,6 +668,88 @@ describe('RuntimeEvent actions', () => {
);
});

test('binds one exact accepted-world Node command into managed observation v3', () => {
const managedObservation = {
protocol: 'managed_observation_v3',
repositoryId: 'repository_11111111111111111111111111111111',
workspaceId: 'workspace_22222222222222222222222222222222',
workspaceEpochId: 'epoch_33333333333333333333333333333333',
workspaceInstanceId: 'instance_44444444444444444444444444444444',
objectFormat: 'sha1',
acceptedWorkspaceVersionId: 'version_55555555555555555555555555555555',
acceptedEventId: 'accepted-event-1',
acceptedHeadRevision: 2,
acceptedCommitOid: '1'.repeat(40),
acceptedTreeOid: '2'.repeat(40),
operationKind: 'node_command_v3',
effectClass: 'hermetic_observation_v3',
executionProfileDigest: MANAGED_OBSERVATION_EXECUTION_PROFILE_V3_DIGEST,
toolchainIdentityDigest: `sha256:${'3'.repeat(64)}`,
entry: {
relativePath: 'scripts/check.mjs',
bytes: 123,
sha256: `sha256:${'4'.repeat(64)}`,
},
args: Object.freeze(['--check', 'src/index.js']),
} as const;
const toolDispatch = {
protocol: 't1_after_preflight_v1',
operationId: 'operation-1',
providerToolCallId: 'call-1',
toolName: 'ManagedNodeRun',
canonicalArgsHash: `sha256:${'b'.repeat(64)}`,
recoveryMode: 'replay_safe',
managedObservation,
} as const;

assert.deepEqual(
decodeRuntimeEvent(baseEvent({ role: 'system', author: 'system', actions: { toolDispatch } }))
.actions?.toolDispatch?.managedObservation,
managedObservation,
);
for (const invalid of [
{ ...managedObservation, operationKind: 'node_test_v2' },
{ ...managedObservation, effectClass: 'hermetic_observation_v2' },
{ ...managedObservation, executionProfileDigest: `sha256:${'0'.repeat(64)}` },
{
...managedObservation,
entry: { ...managedObservation.entry, relativePath: '../check.mjs' },
},
{ ...managedObservation, args: [1] },
{ ...managedObservation, args: ['x'.repeat(4097)] },
{ ...managedObservation, extra: true },
]) {
assert.throws(() =>
decodeRuntimeEvent(
baseEvent({
role: 'system',
author: 'system',
actions: { toolDispatch: { ...toolDispatch, managedObservation: invalid } as never },
}),
),
);
}
assert.throws(() =>
decodeRuntimeEvent(
baseEvent({
role: 'system',
author: 'system',
actions: {
toolDispatch: { ...toolDispatch, toolName: 'ManagedNodeTest' } as never,
},
}),
),
);
});

test('binds managed observation v3 to its canonical command semantics', () => {
const canonical = JSON.stringify(MANAGED_OBSERVATION_EXECUTION_PROFILE_V3_SPEC);
assert.equal(
MANAGED_OBSERVATION_EXECUTION_PROFILE_V3_DIGEST,
`sha256:${createHash('sha256').update(canonical).digest('hex')}`,
);
});

test('binds the managed mutation digest to its canonical execution semantics', () => {
const canonicalProfile = JSON.stringify({
protocol: 'managed_mutation_execution_profile_v1',
Expand Down
140 changes: 138 additions & 2 deletions packages/core/src/runtime-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,42 @@ export const MANAGED_OBSERVATION_EXECUTION_PROFILE_V2_SPEC = Object.freeze({
export const MANAGED_OBSERVATION_EXECUTION_PROFILE_V2_DIGEST =
'sha256:be3ca7af72a0d35cda471a6de71eed7dd260890624f11c8b5d71cccb2067c333' as const;

export const MANAGED_OBSERVATION_EXECUTION_PROFILE_V3_SPEC = Object.freeze({
protocol: 'managed_observation_execution_profile_v3',
operationKind: 'node_command_v3',
effectClass: 'hermetic_observation_v3',
objectFormat: 'sha1',
acceptedInput: 'read_only_accepted_tree_v1',
dependencyInput: 'none',
command: Object.freeze({
runtime: 'node_24',
api: 'node_entrypoint_v1',
extensions: Object.freeze(['.cjs', '.js', '.mjs'] as const),
maxArgs: 64,
maxArgBytes: 4096,
maxTotalArgBytes: 32_768,
}),
sandbox: Object.freeze({
required: true,
network: 'restricted',
acceptedTree: 'read_only',
scratch: 'disposable_write_v1',
childProcess: 'forbidden',
path: 'empty',
}),
result: Object.freeze({
format: 'strict_json_v1',
maxBytes: 65_536,
stdoutBytes: 32_768,
stderrBytes: 32_768,
}),
replay: 'same_accepted_tree_replay_safe_v1',
executionFallback: 'forbidden',
} as const);

export const MANAGED_OBSERVATION_EXECUTION_PROFILE_V3_DIGEST =
'sha256:8cde26b9e1b475fac75f0980baf04d09baed94184757bf02c3cc12fc5df2b50e' as const;

export interface RuntimeEventManagedObservationFileV1 {
readonly relativePath: string;
readonly bytes: number;
Expand Down Expand Up @@ -389,9 +425,30 @@ export interface RuntimeEventManagedWorkspaceObservationV2 {
readonly files: readonly RuntimeEventManagedObservationFileV1[];
}

export interface RuntimeEventManagedWorkspaceObservationV3 {
readonly protocol: 'managed_observation_v3';
readonly repositoryId: string;
readonly workspaceId: string;
readonly workspaceEpochId: string;
readonly workspaceInstanceId: string;
readonly objectFormat: 'sha1';
readonly acceptedWorkspaceVersionId: string;
readonly acceptedEventId: string;
readonly acceptedHeadRevision: number;
readonly acceptedCommitOid: string;
readonly acceptedTreeOid: string;
readonly operationKind: 'node_command_v3';
readonly effectClass: 'hermetic_observation_v3';
readonly executionProfileDigest: typeof MANAGED_OBSERVATION_EXECUTION_PROFILE_V3_DIGEST;
readonly toolchainIdentityDigest: `sha256:${string}`;
readonly entry: RuntimeEventManagedObservationFileV1;
readonly args: readonly string[];
}

export type RuntimeEventManagedWorkspaceObservation =
| RuntimeEventManagedWorkspaceObservationV1
| RuntimeEventManagedWorkspaceObservationV2;
| RuntimeEventManagedWorkspaceObservationV2
| RuntimeEventManagedWorkspaceObservationV3;

/**
* Canonical semantics bound by the managed mutation execution-profile digest.
Expand Down Expand Up @@ -783,6 +840,29 @@ const RUNTIME_MANAGED_WORKSPACE_OBSERVATION_V2_SHAPE =
],
[],
);
const RUNTIME_MANAGED_WORKSPACE_OBSERVATION_V3_SHAPE =
defineObjectShape<RuntimeEventManagedWorkspaceObservationV3>()(
[
'protocol',
'repositoryId',
'workspaceId',
'workspaceEpochId',
'workspaceInstanceId',
'objectFormat',
'acceptedWorkspaceVersionId',
'acceptedEventId',
'acceptedHeadRevision',
'acceptedCommitOid',
'acceptedTreeOid',
'operationKind',
'effectClass',
'executionProfileDigest',
'toolchainIdentityDigest',
'entry',
'args',
],
[],
);
const RUNTIME_MANAGED_DEPENDENCY_NONE_SHAPE = defineObjectShape<{ kind: 'none' }>()(['kind'], []);
const RUNTIME_MANAGED_DEPENDENCY_SNAPSHOT_SHAPE = defineObjectShape<
Extract<RuntimeEventManagedDependencyObservationV1, { kind: 'managed_dependency_snapshot_v1' }>
Expand Down Expand Up @@ -1104,7 +1184,12 @@ function isRuntimeToolDispatch(value: unknown): value is RuntimeEventToolDispatc
isRuntimeManagedWorkspaceObservation(value.managedObservation)) &&
!(value.managedMutation !== undefined && value.managedObservation !== undefined) &&
(value.managedObservation === undefined ||
(value.toolName === 'ManagedNodeTest' && value.recoveryMode === 'replay_safe'))
(value.recoveryMode === 'replay_safe' &&
((value.toolName === 'ManagedNodeTest' &&
(value.managedObservation.protocol === 'managed_observation_v1' ||
value.managedObservation.protocol === 'managed_observation_v2')) ||
(value.toolName === 'ManagedNodeRun' &&
value.managedObservation.protocol === 'managed_observation_v3'))))
);
}

Expand All @@ -1114,6 +1199,9 @@ function isRuntimeManagedWorkspaceObservation(
if (isRecord(value) && value.protocol === 'managed_observation_v2') {
return isRuntimeManagedWorkspaceObservationV2(value);
}
if (isRecord(value) && value.protocol === 'managed_observation_v3') {
return isRuntimeManagedWorkspaceObservationV3(value);
}
if (
!isRecord(value) ||
!hasExactShape(value, RUNTIME_MANAGED_WORKSPACE_OBSERVATION_SHAPE) ||
Expand Down Expand Up @@ -1188,6 +1276,54 @@ function isRuntimeManagedWorkspaceObservationV2(
);
}

function isRuntimeManagedWorkspaceObservationV3(
value: unknown,
): value is RuntimeEventManagedWorkspaceObservationV3 {
if (!isRecord(value)) return false;
return (
hasExactShape(value, RUNTIME_MANAGED_WORKSPACE_OBSERVATION_V3_SHAPE) &&
value.protocol === 'managed_observation_v3' &&
typeof value.repositoryId === 'string' &&
/^repository_[0-9a-f]{32}$/u.test(value.repositoryId) &&
typeof value.workspaceId === 'string' &&
/^workspace_[0-9a-f]{32}$/u.test(value.workspaceId) &&
typeof value.workspaceEpochId === 'string' &&
/^epoch_[0-9a-f]{32}$/u.test(value.workspaceEpochId) &&
typeof value.workspaceInstanceId === 'string' &&
/^instance_[0-9a-f]{32}$/u.test(value.workspaceInstanceId) &&
value.objectFormat === 'sha1' &&
typeof value.acceptedWorkspaceVersionId === 'string' &&
/^version_[0-9a-f]{32}$/u.test(value.acceptedWorkspaceVersionId) &&
typeof value.acceptedEventId === 'string' &&
/^[A-Za-z0-9_-]{1,128}$/u.test(value.acceptedEventId) &&
Number.isSafeInteger(value.acceptedHeadRevision) &&
(value.acceptedHeadRevision as number) >= 1 &&
typeof value.acceptedCommitOid === 'string' &&
/^[0-9a-f]{40}$/u.test(value.acceptedCommitOid) &&
typeof value.acceptedTreeOid === 'string' &&
/^[0-9a-f]{40}$/u.test(value.acceptedTreeOid) &&
value.operationKind === 'node_command_v3' &&
value.effectClass === 'hermetic_observation_v3' &&
value.executionProfileDigest === MANAGED_OBSERVATION_EXECUTION_PROFILE_V3_DIGEST &&
isSha256Digest(value.toolchainIdentityDigest) &&
areRuntimeManagedObservationFiles([value.entry]) &&
isRuntimeManagedNodeCommandArgs(value.args)
);
}

function isRuntimeManagedNodeCommandArgs(value: unknown): value is readonly string[] {
if (!Array.isArray(value) || value.length > 64) return false;
let totalBytes = 0;
for (const item of value) {
if (typeof item !== 'string') return false;
const bytes = UTF8.encode(item).byteLength;
if (bytes > 4096) return false;
totalBytes += bytes;
if (totalBytes > 32_768) return false;
}
return true;
}

function isRuntimeManagedDependencyObservation(
value: unknown,
): value is RuntimeEventManagedDependencyObservationV1 {
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,13 @@ export const SESSION_TOOL_PROFILES = [
'headless-coding-v1',
'managed-coding-v1',
'managed-coding-v2',
'managed-coding-v3',
'workhub-coordination-v1',
] as const;
export type SessionToolProfile = (typeof SESSION_TOOL_PROFILES)[number];
export type ManagedCodingSessionToolProfile = Extract<
SessionToolProfile,
'managed-coding-v1' | 'managed-coding-v2'
'managed-coding-v1' | 'managed-coding-v2' | 'managed-coding-v3'
>;

export function isSessionToolProfile(value: unknown): value is SessionToolProfile {
Expand All @@ -224,7 +225,9 @@ export function isSessionToolProfile(value: unknown): value is SessionToolProfil
export function isManagedCodingSessionToolProfile(
value: unknown,
): value is ManagedCodingSessionToolProfile {
return value === 'managed-coding-v1' || value === 'managed-coding-v2';
return (
value === 'managed-coding-v1' || value === 'managed-coding-v2' || value === 'managed-coding-v3'
);
}

export interface SessionExternalOrigin {
Expand Down
Loading