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 @@ -77,6 +77,33 @@ test('managed Review reads the accepted tree from Runtime Host', async () => {
assert.equal(managedReads, 1);
});

test('managed coding v2 keeps Desktop Review on the accepted tree', async () => {
const ipc = ipcHarness();
let managedReads = 0;
registerRuntimeHostWorkspaceIpc({
ipcMain: ipc as never,
allowLocalWorkspace: false,
client: {
async getSession() {
return sessionProjection('managed-coding-v2');
},
async readManagedWorkspaceReview() {
managedReads += 1;
return { ok: false, reason: 'not_a_repository' };
},
} as never,
});

assert.deepEqual(
await ipc.invoke('git-review:read', {
sessionId: 'session-managed',
source: 'branch',
}),
{ ok: false, reason: 'not_a_repository' },
);
assert.equal(managedReads, 1);
});

test('ordinary Review keeps reading the attached checkout', async (t) => {
const workspace = await mkdtemp(join(tmpdir(), 'maka-review-ordinary-'));
t.after(() => rm(workspace, { recursive: true, force: true }));
Expand Down Expand Up @@ -562,7 +589,7 @@ test('managed workspace lifecycle commands stay bound to the same session', asyn
});

function sessionProjection(
toolProfile?: 'managed-coding-v1',
toolProfile?: 'managed-coding-v1' | 'managed-coding-v2',
hostCwd = process.cwd(),
): SessionCatalogProjection {
return {
Expand Down
9 changes: 5 additions & 4 deletions apps/desktop/src/main/runtime-host-workspace-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { stat } from 'node:fs/promises';
import type { GitReviewSource } from '@maka/core/git-review';
import { isManagedCodingSessionToolProfile } from '@maka/core/session';
import type { DesktopRuntimeHostClient } from './runtime-host-client.js';
import { readGitReview } from './git-review-main.js';
import {
Expand Down Expand Up @@ -51,7 +52,7 @@ export function registerRuntimeHostWorkspaceIpc(
const request = readRequest(raw);
const session = await input.client.getSession(request.sessionId);
if (!session) throw new Error(`No such Session: ${request.sessionId}`);
if (session.toolProfile === 'managed-coding-v1') {
if (isManagedCodingSessionToolProfile(session.toolProfile)) {
if (request.source !== 'branch' || request.baseBranch !== undefined) {
throw new Error('Managed workspace Review only supports its accepted history');
}
Expand All @@ -69,7 +70,7 @@ export function registerRuntimeHostWorkspaceIpc(
const request = publishRequest(raw);
const session = await input.client.getSession(request.sessionId);
if (!session) throw new Error(`No such Session: ${request.sessionId}`);
if (session.toolProfile !== 'managed-coding-v1') {
if (!isManagedCodingSessionToolProfile(session.toolProfile)) {
throw new Error('Session does not own a managed workspace');
}
return input.client.publishManagedWorkspaceSnapshot(request.sessionId, request.publishId);
Expand All @@ -85,7 +86,7 @@ export function registerRuntimeHostWorkspaceIpc(
const request = restoreRequest(raw);
const session = await input.client.getSession(request.sessionId);
if (!session) throw new Error(`No such Session: ${request.sessionId}`);
if (session.toolProfile !== 'managed-coding-v1') {
if (!isManagedCodingSessionToolProfile(session.toolProfile)) {
throw new Error('Session does not own a managed workspace');
}
return input.client.restoreManagedWorkspaceSnapshot(request.sessionId, request.restoreId);
Expand Down Expand Up @@ -206,7 +207,7 @@ function historicalRestoreRequest(value: unknown): {
async function requireManagedSession(client: WorkspaceClient, sessionId: string): Promise<void> {
const session = await client.getSession(sessionId);
if (!session) throw new Error(`No such Session: ${sessionId}`);
if (session.toolProfile !== 'managed-coding-v1') {
if (!isManagedCodingSessionToolProfile(session.toolProfile)) {
throw new Error('Session does not own a managed workspace');
}
}
Expand Down
9 changes: 6 additions & 3 deletions apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ import type { ChatDefaultPermissionMode } from '@maka/core/settings';
import type { SlashCommandIdForSurface } from '@maka/core/slash-command-catalog';
import type { UiLocale, UiLocalePreference } from '@maka/core/ui-locale';
import { collapseSessionRevisions } from '@maka/core/session-revisions';
import { isLinkedSubagentSession } from '@maka/core/session';
import {
isLinkedSubagentSession,
isManagedCodingSessionToolProfile,
} from '@maka/core/session';
import { resolveUiLocale } from '@maka/core/ui-locale';
import { slashCommandsForSurface } from '@maka/core/slash-command-catalog';
import { hasSettledInitialOnboarding } from '@maka/core/onboarding-milestone';
Expand Down Expand Up @@ -812,7 +815,7 @@ function AppShellContent({
resumeInterruptedSession,
} = useShellResume({
activeId: ownerActiveId,
managed: activeCatalogSession?.toolProfile === 'managed-coding-v1',
managed: isManagedCodingSessionToolProfile(activeCatalogSession?.toolProfile),
toastApi,
shellCopy,
uiLocale,
Expand Down Expand Up @@ -3071,7 +3074,7 @@ function AppShellContent({
planModeActive={activePlanMode}
managedTaskActive={
activeId
? activeSessionForView?.toolProfile === 'managed-coding-v1'
? isManagedCodingSessionToolProfile(activeSessionForView?.toolProfile)
: false
}
// No pending-keyed disable while a toggle commits: the
Expand Down
60 changes: 60 additions & 0 deletions docs/architecture/managed-coding-v2-product-composition.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Managed Coding v2 Product Composition

## 1. 为什么是 v2

`managed-coding-v1` 已经是持久化 Session 合同:它只有 accepted-world `Read/Glob/Grep/Write/Edit`。直接把
新工具塞进 v1,会让同一 durable profile 在不同版本拥有不同权限,也会让旧 Session 因新 toolchain/sandbox 缺失而
突然无法打开。

因此 v1 保持冻结,v2 只增加一个能力:

```text
ManagedNodeTest(explicit sorted .js/.mjs/.cjs files)
```

它不是 Bash、npm script 或任意 command;它只能观察同一个 accepted Git tree。

## 2. 主要不变量

> `managed-coding-v2` 只有在一个 Runtime Host 同时拥有 accepted Gitoxide session、current-process Node
> toolchain、enforcing sandbox 与 storage-root execution capability 时才可组合;缺一项必须在 T1 前明确不可用。

v2 工具集合固定为:

```text
Read / Glob / Grep / Write / Edit / ManagedNodeTest
```

- Read/Glob/Grep:`replay_safe`,读取 accepted tree;
- Write/Edit:`reconcile + managed_mutation_v1`;
- ManagedNodeTest:`replay_safe + managed_observation_v1`;
- Bash、npm、package script、PATH executable 与 attached checkout 均不在 profile 内。

## 3. Owner 与组合顺序

1. Host boot 尝试 admission packaged Gitoxide helper 与 current-process managed toolchain;缺失只让对应 profile
unavailable,不让普通 Session 获得 fallback;manifest 损坏仍 fail Host boot。
2. Session run 开始时,Gitoxide owner读取 durable epoch/head/version。
3. v2 additionally 组合 command sandbox owner、execution-root owner 与 Node-test admission owner。
4. Run composer 将 exact profile 工具投影给模型,同时把 mutation/observation admission 分别交给 Runtime。
5. Runtime 在 T1 前冻结 mode;T1 后不允许换回 v1、普通 test runner 或 generic T2。

## 4. 失败与兼容

- 旧 `managed-coding-v1` Session 永远不要求 Node toolchain;
- v2 缺 Gitoxide/toolchain/sandbox:run 在 provider 请求前以
`managed_workspace_profile_unavailable` 失败;
- v2 test admission 失败:没有 T1;
- T1 后 helper/Host 失败:按 `managed_observation_v1` exact-boundary recovery 收敛;
- profile 是 Session immutable identity,不允许运行中从 v2 降级 v1。

本切片建立 Host 产品 composition,但不立即把 Desktop 默认创建策略从 v1 切到 v2。默认切换必须与 packaged
Host/helper kill-reopen 和三平台 enforcing sandbox gate 同一交付完成,避免用户拿到未经证明的默认能力。

## 5. 平台矩阵

| 平台 | composition 语义 | 默认启用前 gate |
| --- | --- | --- |
| Windows | v2 profile 与 owner graph 可组合 | packaged Electron + AppContainer/Job + kill/reopen |
| macOS | 相同 durable profile | signed app + Seatbelt + kill/reopen |
| Linux | 相同 protocol/build | signed distribution authority + Bubblewrap + kill/reopen |
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ exit status、test summary 与 artifact digest。缓存是 projection;test out
T2;
3. Host admission owner 已只从 Gitoxide accepted-world 与 toolchain opaque capability 签发 envelope,并用一次性
input/scratch roots 执行显式 Node tests;它尚未改变现有 `managed-coding-v1` 产品 profile;
4. 下一步定义版本化 Desktop product profile,并在暴露工具以前补真实 Host/helper kill/reopen 与三平台 enforcing
sandbox smoke;
4. `managed-coding-v2` Host composition 已定义版本化工具集合,并保持 v1 不变;Desktop 默认仍停在 v1,直到
真实 Host/helper kill/reopen 与三平台 enforcing sandbox smoke 通过
5. 需要外部包的项目在 M5.3 capability 可用前明确 unavailable,禁止静默降级。

### M5.5 External-effect fencing
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,25 @@ export function isTurnStatus(value: unknown): value is TurnStatus {
export const SESSION_TOOL_PROFILES = [
'headless-coding-v1',
'managed-coding-v1',
'managed-coding-v2',
'workhub-coordination-v1',
] as const;
export type SessionToolProfile = (typeof SESSION_TOOL_PROFILES)[number];
export type ManagedCodingSessionToolProfile = Extract<
SessionToolProfile,
'managed-coding-v1' | 'managed-coding-v2'
>;

export function isSessionToolProfile(value: unknown): value is SessionToolProfile {
return typeof value === 'string' && (SESSION_TOOL_PROFILES as readonly string[]).includes(value);
}

export function isManagedCodingSessionToolProfile(
value: unknown,
): value is ManagedCodingSessionToolProfile {
return value === 'managed-coding-v1' || value === 'managed-coding-v2';
}

export interface SessionExternalOrigin {
readonly adapterId: string;
readonly sourceSessionId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,35 @@ test('hosted execution freezes the headless coding provider wire contract', asyn
/managed_workspace_profile_unavailable/u,
);
assert.equal(provider.requests.length, requestCountBeforeManagedAdmission);

const managedV2Outcome = await composition.handlers['hosted.execution.start'](
{
executionId: '00000000-0000-4000-8000-000000000780',
session: {
workspace: { kind: 'host_path', path: root },
modelTarget: {
kind: 'explicit',
connectionId: connection.connectionId,
connectionSlug: 'profile-deepseek',
model: 'deepseek-v4-flash',
},
permissionMode: 'bypass',
collaborationMode: 'agent',
orchestrationMode: 'default',
toolProfile: 'managed-coding-v2',
},
content: { text: 'Run an accepted-world test.' },
},
context,
);
assert.equal(managedV2Outcome.ok, true);
if (!managedV2Outcome.ok || managedV2Outcome.result.kind !== 'settled') return;
assert.equal(managedV2Outcome.result.status, 'failed');
assert.match(
managedV2Outcome.result.failureReason ?? '',
/managed_workspace_profile_unavailable/u,
);
assert.equal(provider.requests.length, requestCountBeforeManagedAdmission);
} finally {
try {
await composition?.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ test('hosted execution tool profiles are durable Session creation inputs', () =>
}).session.toolProfile,
'managed-coding-v1',
);
assert.equal(
decodeHostedExecutionStartInput({
...decoded,
session: { ...decoded.session, toolProfile: 'managed-coding-v2' },
}).session.toolProfile,
'managed-coding-v2',
);
assert.throws(
() =>
decodeHostedExecutionStartInput({
Expand Down Expand Up @@ -177,3 +184,26 @@ test('the managed coding profile reads and mutates only the accepted Git tree',
assert.equal(tool.durableExecutionProfile, 'managed_mutation_v1');
}
});

test('managed coding v2 adds only the durable accepted-world Node test', () => {
const profile = hostedExecutionRunProfile('managed-coding-v2');
assert.ok(profile);
assert.deepEqual(profile.toolNames, ['Read', 'Glob', 'Grep', 'Write', 'Edit', 'ManagedNodeTest']);
assert.match(profile.systemPrompt, /explicit dependency-free Node tests/u);

const tools = ['Read', 'Glob', 'Grep', 'Write', 'Edit', 'ManagedNodeTest', 'Bash'].map(
(name): MakaTool => ({
name,
description: name,
parameters: z.object({}),
impl: async () => 'not used',
}),
);
const selected = projectHostedExecutionTools(tools, 'managed-coding-v2');
assert.deepEqual(
selected.map(({ name }) => name),
['Read', 'Glob', 'Grep', 'Write', 'Edit', 'ManagedNodeTest'],
);
assert.equal(selected.at(-1)?.recoveryMode, 'replay_safe');
assert.equal(selected.at(-1)?.durableExecutionProfile, 'managed_observation_v1');
});
Loading
Loading