Skip to content
Closed
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
896e333
feat(runtime): add GPT-compatible ApplyPatch editing projection
sunheyi6 Jul 28, 2026
bf9bad1
fix(ci): format ApplyPatch sources and exhaust worker mocks
sunheyi6 Jul 28, 2026
0f861f6
fix(runtime): address ApplyPatch review — shared engine, permissions,…
sunheyi6 Jul 29, 2026
ff84a98
fix(ci): format ApplyPatch sources and update surface identity tests
sunheyi6 Jul 29, 2026
4273188
fix(ci): expect editingProtocol in headless product-tool identity sna…
sunheyi6 Jul 29, 2026
1879417
fix(runtime): harden ApplyPatch against review P1 regressions
sunheyi6 Jul 30, 2026
9f27a6c
fix(ci): format ApplyPatch artifact test for biome
sunheyi6 Jul 30, 2026
2bf388a
fix ApplyPatch product wiring and filesystem edges
sunheyi6 Jul 30, 2026
1aefe98
fix latest ApplyPatch review findings
sunheyi6 Jul 30, 2026
766b680
fix Desktop tool policy CI assertion
sunheyi6 Jul 30, 2026
796e793
refactor(runtime): centralize apply patch transaction planning
sunheyi6 Jul 30, 2026
6da3471
fix(desktop): prevent startup white screen
sunheyi6 Aug 1, 2026
1dcff3f
fix(ui): redesign keyboard shortcuts dialog
sunheyi6 Aug 1, 2026
1174e7d
Merge PR #1734: fix(desktop) prevent startup white screen
sunheyi6 Aug 1, 2026
9db9b70
Merge PR #1735: fix(ui) redesign keyboard shortcuts dialog
sunheyi6 Aug 1, 2026
85623f5
Merge branch 'main' of https://github.com/sunheyi6/maka-agent
sunheyi6 Aug 1, 2026
4c86288
Merge branch 'main' of https://github.com/sunheyi6/maka-agent
sunheyi6 Aug 2, 2026
b36c316
Merge remote-tracking branch 'origin/main' into pr-1556-work
sunheyi6 Aug 4, 2026
95e1455
fix(runtime): address ApplyPatch review findings and resolve main merge
sunheyi6 Aug 4, 2026
81f5a7e
merge upstream main: resolve workspace-containment refactor (#2059) c…
sunheyi6 Aug 4, 2026
fee4682
adapt worker tests to #2059 follow-final semantics; restore editingPr…
sunheyi6 Aug 4, 2026
e29ce4d
fix CI: worker pin gating, replace-mode follow, symlink targetType, s…
sunheyi6 Aug 4, 2026
c6c5583
chore: biome format touched worker sources
sunheyi6 Aug 4, 2026
03338c8
Merge remote-tracking branch 'upstream/main' into pr-1556-work
sunheyi6 Aug 4, 2026
9c8359e
chore: biome format headless tools test, subagent-tools test, agent-c…
sunheyi6 Aug 4, 2026
d3ace75
fix(runtime): replace-mode writes resolve through the followed target…
sunheyi6 Aug 4, 2026
776e651
merge upstream main: execution-boundary file-path authority (#2087)
sunheyi6 Aug 4, 2026
92aa5f7
fix(runtime): worker revalidation follows replace-mode like the client
sunheyi6 Aug 4, 2026
8ed7bd4
refactor(runtime): converge ApplyPatch on one filesystem authority pe…
sunheyi6 Aug 4, 2026
09615bb
merge upstream main: resolve e2e conflict (main-side version)
sunheyi6 Aug 4, 2026
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
29 changes: 28 additions & 1 deletion apps/desktop/src/main/__tests__/create-session-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { describe, it } from 'node:test';
import type { AppSettings, ChatDefaultPermissionMode } from '@maka/core';
import { DEEP_RESEARCH_SESSION_LABEL, DEFAULT_SESSION_NAME } from '@maka/core';

import { type CreateSessionRequest, resolveCreateSessionInput } from '../create-session-input.js';
import {
type CreateSessionRequest,
resolveCreateSessionInput,
resolveEditingProtocolEnv,
} from '../create-session-input.js';

function settings(permissionMode: ChatDefaultPermissionMode) {
return async () => ({ chatDefaults: { permissionMode } }) as AppSettings;
Expand Down Expand Up @@ -126,4 +130,27 @@ describe('resolveCreateSessionInput', () => {
assert.equal(resolved.collaborationMode, 'plan');
assert.equal(resolved.orchestrationMode, 'swarm');
});

it('normalizes the editing protocol per session request', async () => {
assert.equal((await resolve({ editingProtocol: 'apply_patch' })).editingProtocol, 'apply_patch');
assert.equal((await resolve({})).editingProtocol, 'edit_write');
await assert.rejects(() => resolve({ editingProtocol: 'all' }), TypeError);
});

it('uses the process setting only as the default for an individual session', async () => {
const readSettings = settings('ask');
const configured = await resolveCreateSessionInput(undefined, {
readSettings,
defaultEditingProtocol: 'apply_patch',
});
const overridden = await resolveCreateSessionInput(
{ editingProtocol: 'edit_write' },
{ readSettings, defaultEditingProtocol: 'apply_patch' },
);

assert.equal(configured.editingProtocol, 'apply_patch');
assert.equal(overridden.editingProtocol, 'edit_write');
assert.equal(resolveEditingProtocolEnv('apply_patch'), 'apply_patch');
assert.throws(() => resolveEditingProtocolEnv('all'));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {

const readTool = tool('Read', 'read');
const writeTool = tool('Write', 'file_write');
const applyPatchTool = tool('ApplyPatch', 'file_write');
const computerTool = tool('maka_computer', 'computer_use');
const availability: ToolAvailabilityConfig = {
economy: true,
Expand Down Expand Up @@ -135,6 +136,25 @@ describe('Desktop backend tool surface', () => {
assert.equal(surface.skillHost.toolNames.has('Write'), true);
});

it('projects ApplyPatch through the standard Desktop backend policy', async () => {
const surface = await resolveDesktopBackendToolSurface(
makeDeps({
builtinTools: [readTool, writeTool, applyPatchTool],
}),
{
...inputFor('claude-sonnet-4-5-20250929'),
header: {
...inputFor('claude-sonnet-4-5-20250929').header,
editingProtocol: 'apply_patch',
},
},
);

assert.equal(surface.skillHost.toolNames.has('ApplyPatch'), true);
assert.equal(surface.skillHost.toolNames.has('Write'), false);
assert.equal(surface.selectedTools.some((tool) => tool.name === 'Edit'), false);
});

it('keeps scoped child tools ahead of root-only computer-use and Plan controls', async () => {
const deps = makeDeps({ isComputerUseRealModelE2e: true });
const input = inputFor('claude-sonnet-4-5-20250929', 'plan');
Expand Down
15 changes: 14 additions & 1 deletion apps/desktop/src/main/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
SessionActivityRegistry,
listInvocableSkills,
prepareSkillInvocationMessage,
projectEffectiveProductToolSurface,
resolveSkillDiscoveryPaths,
} from '@maka/runtime';
import type {
Expand Down Expand Up @@ -85,6 +86,7 @@ import {
requireReadyConnection,
} from './chat-readiness.js';
import { assertDesktopExecutionBoundary } from './desktop-execution-admission.js';
import { resolveEditingProtocolEnv } from './create-session-input.js';
import { createFileCredentialStore } from './credential-store.js';
import { bindOnboardingDeps, createOnboardingService } from './onboarding-service.js';
import { createDailyReviewArchiveStore } from './daily-review-archive-store.js';
Expand Down Expand Up @@ -886,7 +888,17 @@ const runtime = new SessionManager({
inspectContinuationSafety: createLocalContinuationSafetyInspector({
readSessionCwd: async (sessionId) => (await store.readHeader(sessionId)).cwd,
resolveWorkspaceIdentity: async (cwd) => resolveWorkspaceIdentity({ path: cwd }),
listAvailableToolNames: async () => builtinTools.map((tool) => tool.name),
listAvailableToolNames: async (sessionId) => {
const header = await store.readHeader(sessionId);
return projectEffectiveProductToolSurface({
host: 'desktop',
tools: builtinTools,
policy: {
...desktopProductToolSurface.identity.policy,
editingProtocol: header.editingProtocol ?? 'edit_write',
},
}).tools.map((tool) => tool.name);
},
hasPendingBackgroundOperations: async (sessionId) => {
const [shellUpdates, runs] = await Promise.all([
shellRuns.listSessionUpdates(sessionId),
Expand Down Expand Up @@ -1127,6 +1139,7 @@ function registerIpc(): void {
streamEvents,
getWorkspacePrivacyContext,
canCreateFakeSession: canCreateFakeSessionFromRenderer,
defaultEditingProtocol: resolveEditingProtocolEnv(process.env.MAKA_EDITING_PROTOCOL),
consumeNativeAudioOperation: (input) =>
voiceIpcService.consumeNativeAudioOperation(input),
});
Expand Down
21 changes: 20 additions & 1 deletion apps/desktop/src/main/create-session-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import type {
AppSettings,
CollaborationMode,
EditingProtocol,
OrchestrationMode,
PermissionMode,
SessionStartMode,
Expand All @@ -28,7 +29,9 @@ import {
DEFAULT_SESSION_NAME,
isChatDefaultPermissionMode,
isCollaborationMode,
isEditingProtocol,
isOrchestrationMode,
resolveEditingProtocolEnv as resolveEditingProtocolEnvValue,
} from '@maka/core';

import { resolveDefaultPermissionMode } from './permission-mode-default.js';
Expand Down Expand Up @@ -61,6 +64,12 @@ const SESSION_MODE_SEEDS = {
},
} satisfies Record<SessionStartMode, SessionModeSeed>;

export function resolveEditingProtocolEnv(value: string | undefined): EditingProtocol {
// Desktop sessions default to Edit/Write; an explicit env override is the
// only way to reach apply_patch.
return resolveEditingProtocolEnvValue(value) ?? 'edit_write';
}

/**
* `unknown`, because this is an IPC boundary and the renderer's type is a
* promise, not a guarantee. An unrecognized value confers nothing — it is not
Expand All @@ -82,6 +91,7 @@ export interface CreateSessionRequest {
permissionMode?: PermissionMode;
collaborationMode?: CollaborationMode;
orchestrationMode?: OrchestrationMode;
editingProtocol?: EditingProtocol;
name?: string;
labels?: string[];
}
Expand All @@ -90,13 +100,17 @@ export interface ResolvedCreateSessionInput {
permissionMode: PermissionMode;
collaborationMode: CollaborationMode;
orchestrationMode: OrchestrationMode;
editingProtocol: EditingProtocol;
name: string;
labels: string[] | undefined;
}

export async function resolveCreateSessionInput(
input: CreateSessionRequest | undefined,
deps: { readSettings: () => Promise<AppSettings> },
deps: {
readSettings: () => Promise<AppSettings>;
defaultEditingProtocol?: EditingProtocol;
},
): Promise<ResolvedCreateSessionInput> {
const modeSeed = sessionModeSeed(input?.mode);

Expand All @@ -108,6 +122,10 @@ export async function resolveCreateSessionInput(
if (!isOrchestrationMode(orchestrationMode)) {
throw new TypeError('Invalid orchestration mode.');
}
const editingProtocol = input?.editingProtocol ?? deps.defaultEditingProtocol ?? 'edit_write';
if (!isEditingProtocol(editingProtocol)) {
throw new TypeError('Invalid editing protocol.');
}
// `explore` is a boundary a mode confers, never one a caller may open a
// session at — core already spells that out as `ChatDefaultPermissionMode`
// (the modes a user can pick). Refusing it here is what makes the seed the
Expand All @@ -124,6 +142,7 @@ export async function resolveCreateSessionInput(
(await resolveDefaultPermissionMode(deps.readSettings)),
collaborationMode,
orchestrationMode,
editingProtocol,
name: modeSeed?.name ?? input?.name ?? DEFAULT_SESSION_NAME,
// Merged, not replaced: a mode adds a label, it does not own the set. No
// caller sends both today, and silently dropping the caller's would be the
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/main/desktop-backend-tool-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
selectCollaborationTools,
} from '@maka/runtime';
import type {
EditingProtocol,
HostCapabilities,
MakaTool,
ToolAvailabilityConfig,
Expand Down Expand Up @@ -79,6 +80,7 @@ export interface DesktopBackendToolSurface {

export interface DesktopNewSessionSkillContext {
collaborationMode?: CollaborationMode;
editingProtocol?: EditingProtocol;
}

/**
Expand Down Expand Up @@ -141,6 +143,7 @@ export async function resolveDesktopNewSessionSkillHost(
connectionLocked: false,
model: input.readyConnection.model,
permissionMode: 'ask',
editingProtocol: input.context?.editingProtocol ?? 'edit_write',
collaborationMode: input.context?.collaborationMode ?? 'agent',
orchestrationMode: 'default',
schemaVersion: 1,
Expand Down Expand Up @@ -230,7 +233,10 @@ export async function resolveDesktopBackendToolSurface(
const productToolSurface = projectEffectiveProductToolSurface({
host: 'desktop',
tools: selectedTools,
policy: { economy: toolEconomy },
policy: {
economy: toolEconomy,
editingProtocol: input.header.editingProtocol ?? 'edit_write',
},
});

return {
Expand Down
19 changes: 17 additions & 2 deletions apps/desktop/src/main/sessions-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
SessionListFilter,
StoredMessage,
ThinkingLevel,
EditingProtocol,
EphemeralVoiceAudio,
} from '@maka/core';
import type { ProviderType } from '@maka/core/llm-connections';
Expand Down Expand Up @@ -140,6 +141,8 @@ export interface SessionsIpcDeps {
) => Promise<{ turnId: string; ok: boolean; error?: string }>;
getWorkspacePrivacyContext: () => Promise<WorkspacePrivacyContext>;
canCreateFakeSession: () => boolean;
/** Default captured for new sessions; an IPC request may override it per session. */
defaultEditingProtocol?: EditingProtocol;
consumeNativeAudioOperation?: (input: {
operationId: string;
connectionSlug: string;
Expand Down Expand Up @@ -242,6 +245,7 @@ export function registerSessionsIpc(
streamEvents,
getWorkspacePrivacyContext,
canCreateFakeSession,
defaultEditingProtocol,
consumeNativeAudioOperation,
} = deps;
registerSessionExecutionIpc({
Expand Down Expand Up @@ -295,8 +299,17 @@ export function registerSessionsIpc(
// what the renderer may ask for directly, and what the configured default
// fills in are all resolved in one pure place (create-session-input.ts),
// which is also the only place any of it can be tested.
const { permissionMode, collaborationMode, orchestrationMode, name, labels } =
await resolveCreateSessionInput(input, { readSettings: () => settingsStore.get() });
const {
permissionMode,
collaborationMode,
orchestrationMode,
editingProtocol,
name,
labels,
} = await resolveCreateSessionInput(input, {
readSettings: () => settingsStore.get(),
defaultEditingProtocol,
});
if (input?.backend === 'fake') {
if (!canCreateFakeSession()) {
throw new Error('FakeBackend sessions are only available in development.');
Expand All @@ -308,6 +321,7 @@ export function registerSessionsIpc(
llmConnectionSlug: input.llmConnectionSlug ?? 'fake',
model: input.model ?? 'fake-model',
permissionMode,
editingProtocol,
collaborationMode,
orchestrationMode,
name,
Expand All @@ -329,6 +343,7 @@ export function registerSessionsIpc(
model,
...(thinkingLevel !== undefined ? { thinkingLevel } : {}),
permissionMode,
editingProtocol,
collaborationMode,
orchestrationMode,
name,
Expand Down
6 changes: 4 additions & 2 deletions apps/desktop/src/main/tool-assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ export function assembleDesktopTools(deps: DesktopToolAssemblyDeps) {
const desktopProductToolSurface = projectEffectiveProductToolSurface({
host: 'desktop',
tools: builtinTools,
policy: { economy: economyEnabled },
policy: { economy: economyEnabled, editingProtocol: 'edit_write' },
});
// Build the union needed by catalog child profiles. SessionManager applies
// each profile's narrower allowlist; parent-facing runtime refs are omitted.
Expand All @@ -415,7 +415,9 @@ export function assembleDesktopTools(deps: DesktopToolAssemblyDeps) {
computerUseScreenLock,
computerUseTools,
desktopProductToolSurface,
builtinTools: [...desktopProductToolSurface.tools],
// Keep the protocol union bound at process scope. The backend projector
// selects exactly one surface from each persisted session header.
builtinTools,
childAgentTools,
sandboxDiagnosticsProvider,
};
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/main/workspace-resources-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface NewSessionSkillContext {
llmConnectionSlug?: string;
model?: string;
collaborationMode?: CollaborationMode;
editingProtocol?: import('@maka/core').EditingProtocol;
}

interface WorkspaceResourcesIpcDeps {
Expand Down
15 changes: 14 additions & 1 deletion docs/architecture/runtime-core-architecture-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ counterpart: ./runtime-core-architecture-draft.zh-CN.md
implementation_status: current
document_status: draft
translation_status: synced
last_verified: 2026-07-12
last_verified: 2026-07-30
owners:
- maka-backend
---
Expand Down Expand Up @@ -367,6 +367,19 @@ Startup recovery does not re-execute model requests or tool side effects. It sca

This is state repair, not checkpoint resume. The current Runtime can retain partial output, recover a consistent terminal state, and provide the facts needed for future mid-run recovery. It does not automatically continue from the line after an interrupted tool call when the process restarts.

## Editing policy and the ApplyPatch transaction boundary

Editing-tool selection is part of the normalized per-run product-tool policy, not a host builder switch. Desktop, CLI, and Headless bind the union of editing implementations. `projectEffectiveProductToolSurface()` selects either `Write` plus `Edit`, or `ApplyPatch`, exactly once. Child-agent tool surfaces are then projected inside that effective parent surface, so a child can narrow the parent's capabilities but cannot recover a hidden editing protocol.

`ApplyPatch` also has one semantic implementation:

1. Core parses and canonicalizes hunks, then produces a pure mutation plan from an immutable no-follow filesystem snapshot.
2. Runtime acquires stable locks for every referenced path, reads the snapshot, plans every mutation, and preflights every permission before the first side effect.
3. Runtime applies the plan through minimal primitives: no-follow `lstat`, text read, no-clobber create, regular-file replace, recursive parent creation, and directory-entry delete.
4. Desktop's filesystem worker and Headless isolation provide only those primitives. They do not reimplement patch planning.

Create operations must not overwrite an existing destination. Replace and Move sources must be regular files. Delete operates on the named directory entry, while Move deletes that named regular-file source only after creating its destination; neither operation substitutes or deletes a symlink target. If an apply step fails after earlier mutations completed, Runtime returns an explicit partial result with the completed paths; it does not claim rollback that the underlying filesystem cannot guarantee.

## What this design buys—and what it costs

### Capabilities gained
Expand Down
15 changes: 14 additions & 1 deletion docs/architecture/runtime-core-architecture-draft.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ counterpart: ./runtime-core-architecture-draft.md
implementation_status: current
document_status: draft
translation_status: synced
last_verified: 2026-07-12
last_verified: 2026-07-30
owners:
- maka-backend
---
Expand Down Expand Up @@ -367,6 +367,19 @@ Maka 当前保护的核心不变量是:

这是“状态修复”,不是 checkpoint resume。当前 Runtime 可以保留部分输出、恢复一致的最终状态,并为以后真正的中点恢复提供事实基础;它不会在进程重启后自动从某个工具调用的下一行继续执行。

## 编辑策略与 ApplyPatch 事务边界

编辑工具的选择属于规范化后的每次运行产品工具策略,而不是 Host builder 的第二个开关。Desktop、CLI 和 Headless 都先绑定全部编辑实现,再由 `projectEffectiveProductToolSurface()` 唯一一次选择 `Write` 加 `Edit`,或 `ApplyPatch`。子 Agent 的工具面随后只能在父运行的有效工具面内继续投影,因此可以进一步收窄能力,但不能重新获得父运行已隐藏的编辑协议。

`ApplyPatch` 也只有一份语义实现:

1. Core 解析并规范化 hunk,再根据一份不可变、且不跟随最终符号链接的文件系统快照生成纯 mutation plan。
2. Runtime 为所有相关路径获取稳定锁,读取快照,规划全部 mutation,并在第一个副作用发生前完成全部权限预检。
3. Runtime 只通过最小原语执行计划:不跟随链接的 `lstat`、文本读取、禁止覆盖的创建、仅限普通文件的替换、递归创建父目录,以及删除指定目录项。
4. Desktop 文件系统 worker 和 Headless 隔离层只提供这些原语,不再各自实现 patch 规划。

创建操作不得覆盖已有目标;替换操作和 Move 源都必须是普通文件。Delete 操作用户指定的目录项;Move 也只会在创建目标后删除指定的普通文件源;两者都不会把符号链接替换成其目标或删除链接目标。如果某一步执行失败而此前 mutation 已经完成,Runtime 会返回明确的 partial 结果及已完成路径;底层文件系统不能保证回滚时,Runtime 不会声称存在回滚。

## 这套设计换来了什么,又付出了什么

### 得到的能力
Expand Down
Loading
Loading