From b9494dfc9e8bc491b2a9471e8caa023b5c05c17a Mon Sep 17 00:00:00 2001 From: zhiiw Date: Mon, 31 Aug 2026 04:55:03 +0800 Subject: [PATCH] feat(runtime-host): compose managed coding v2 --- .../runtime-host-workspace-ipc-main.test.ts | 29 ++++- .../main/runtime-host-workspace-ipc-main.ts | 9 +- apps/desktop/src/renderer/app-shell.tsx | 9 +- ...ged-coding-v2-product-composition.zh-CN.md | 60 +++++++++++ ...time-durable-coding-m3-m5-roadmap.zh-CN.md | 4 +- packages/core/src/session.ts | 11 ++ .../execution-model-composition.test.ts | 29 +++++ .../hosted-execution-tool-profile.test.ts | 30 ++++++ .../src/server/execution-composition.ts | 84 ++++++++++++--- .../server/hosted-execution-tool-profile.ts | 22 +++- .../managed-workspace-review-coordinator.ts | 19 ++-- .../src/server/root-turn-coordinator.ts | 10 +- .../src/__tests__/session-manager.test.ts | 102 +++++++++--------- packages/runtime/src/runtime-kernel.ts | 3 +- packages/runtime/src/session-manager.ts | 10 +- 15 files changed, 338 insertions(+), 93 deletions(-) create mode 100644 docs/architecture/managed-coding-v2-product-composition.zh-CN.md diff --git a/apps/desktop/src/main/__tests__/runtime-host-workspace-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-workspace-ipc-main.test.ts index 6d0ff18432..0c8c9a59c1 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-workspace-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-workspace-ipc-main.test.ts @@ -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 })); @@ -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 { diff --git a/apps/desktop/src/main/runtime-host-workspace-ipc-main.ts b/apps/desktop/src/main/runtime-host-workspace-ipc-main.ts index 96671f9d5b..e260c71daf 100644 --- a/apps/desktop/src/main/runtime-host-workspace-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-workspace-ipc-main.ts @@ -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 { @@ -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'); } @@ -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); @@ -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); @@ -206,7 +207,7 @@ function historicalRestoreRequest(value: unknown): { async function requireManagedSession(client: WorkspaceClient, sessionId: string): Promise { 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'); } } diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index d02d2d4f7a..72862fab6f 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -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'; @@ -812,7 +815,7 @@ function AppShellContent({ resumeInterruptedSession, } = useShellResume({ activeId: ownerActiveId, - managed: activeCatalogSession?.toolProfile === 'managed-coding-v1', + managed: isManagedCodingSessionToolProfile(activeCatalogSession?.toolProfile), toastApi, shellCopy, uiLocale, @@ -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 diff --git a/docs/architecture/managed-coding-v2-product-composition.zh-CN.md b/docs/architecture/managed-coding-v2-product-composition.zh-CN.md new file mode 100644 index 0000000000..f864298ab5 --- /dev/null +++ b/docs/architecture/managed-coding-v2-product-composition.zh-CN.md @@ -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 | diff --git a/docs/architecture/runtime-durable-coding-m3-m5-roadmap.zh-CN.md b/docs/architecture/runtime-durable-coding-m3-m5-roadmap.zh-CN.md index 0b1374e7c7..3d29ed0a6e 100644 --- a/docs/architecture/runtime-durable-coding-m3-m5-roadmap.zh-CN.md +++ b/docs/architecture/runtime-durable-coding-m3-m5-roadmap.zh-CN.md @@ -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 diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index dc6dd0a2f2..a63c35a580 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -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; diff --git a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts index afeeb3a748..c3770a4b39 100644 --- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts +++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts @@ -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(); diff --git a/packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts b/packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts index e6b8c48030..8315e113b5 100644 --- a/packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts +++ b/packages/runtime-host/src/__tests__/hosted-execution-tool-profile.test.ts @@ -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({ @@ -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'); +}); diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts index 9cb16a7324..2131d5163c 100644 --- a/packages/runtime-host/src/server/execution-composition.ts +++ b/packages/runtime-host/src/server/execution-composition.ts @@ -31,6 +31,7 @@ import { emptyPlanSessionState } from '@maka/core/plan'; import type { PermissionMode } from '@maka/core/permission'; import { isDeepResearchSession, + isManagedCodingSessionToolProfile, type SessionHeader, WORKHUB_COORDINATION_SESSION_ID, } from '@maka/core/session'; @@ -163,6 +164,15 @@ import { PackagedGitoxideHelperError, resolvePackagedGitoxideHelperInternal, } from './packaged-gitoxide-helper-internal.js'; +import { + CurrentProcessManagedToolchainError, + resolveCurrentProcessManagedToolchainInternal, +} from './current-process-managed-toolchain-internal.js'; +import { createManagedCommandSandboxOwnerInternal } from './managed-command-sandbox-owner-internal.js'; +import { + createManagedNodeTestAdmissionOwnerInternal, + createManagedNodeTestExecutionRootOwnerInternal, +} from './managed-node-test-admission-owner-internal.js'; import { HostProjectDirectoryAuthority, type PublishedProjectDirectoryRoot, @@ -298,6 +308,18 @@ export async function createExecutionRuntimeHostComposition( } throw error; }); + const managedToolchainInvocationOwnerToken = {}; + const managedToolchainCapability = await resolveCurrentProcessManagedToolchainInternal({ + invocationOwnerToken: managedToolchainInvocationOwnerToken, + }).catch((error: unknown) => { + if ( + error instanceof CurrentProcessManagedToolchainError && + error.code === 'current_process_managed_toolchain_unavailable' + ) { + return undefined; + } + throw error; + }); let graphControlStore: ReturnType | undefined; let graphClient: HostAgentGraphCoordinator | undefined; let sessionEffects: HostSessionEffectCoordinator | undefined; @@ -371,6 +393,17 @@ export async function createExecutionRuntimeHostComposition( }, }); const sandboxManager = createBuiltinSandboxManager(); + const managedCommandOwner = + sandboxManager && managedToolchainCapability + ? createManagedCommandSandboxOwnerInternal({ + invocationOwnerToken: managedToolchainInvocationOwnerToken, + toolchainCapability: managedToolchainCapability, + sandboxManager, + }) + : undefined; + const managedNodeTestExecutionRootOwner = createManagedNodeTestExecutionRootOwnerInternal({ + storageRootLease: context.owner.lease, + }); const filesystemWorkerLaunchSpecProvider = sandboxManager && isBuiltinFilesystemWorkerSandboxAvailable() ? createFilesystemWorkerLaunchSpecProvider({ @@ -713,22 +746,38 @@ export async function createExecutionRuntimeHostComposition( 'ai-sdk', dependencies.primaryBackendFactory ?? (async (backendContext) => { - const managedSession = - backendContext.header.toolProfile === 'managed-coding-v1' - ? await (async () => { - if (!gitoxideHelperCapability) { + const managedSession = isManagedCodingSessionToolProfile( + backendContext.header.toolProfile, + ) + ? await (async () => { + if (!gitoxideHelperCapability) { + throw new Error( + 'managed_workspace_profile_unavailable: packaged Gitoxide helper authority is unavailable', + ); + } + return openGitoxideManagedSessionOwnerInternal({ + storageRootLease: context.owner.lease, + stores, + invocationOwnerToken: gitoxideInvocationOwnerToken, + helperCapability: gitoxideHelperCapability, + sourceRoot: backendContext.header.cwd, + sessionId: backendContext.sessionId, + abortSignal: backendContext.abortSignal, + }); + })() + : undefined; + const managedNodeTestAdmission = + backendContext.header.toolProfile === 'managed-coding-v2' + ? (() => { + if (!managedCommandOwner || !managedSession) { throw new Error( - 'managed_workspace_profile_unavailable: packaged Gitoxide helper authority is unavailable', + 'managed_workspace_profile_unavailable: hermetic Node test authority is unavailable', ); } - return openGitoxideManagedSessionOwnerInternal({ - storageRootLease: context.owner.lease, - stores, - invocationOwnerToken: gitoxideInvocationOwnerToken, - helperCapability: gitoxideHelperCapability, - sourceRoot: backendContext.header.cwd, - sessionId: backendContext.sessionId, - abortSignal: backendContext.abortSignal, + return createManagedNodeTestAdmissionOwnerInternal({ + executionRootOwner: managedNodeTestExecutionRootOwner, + sourceOwner: managedSession.nodeTestSource, + commandOwner: managedCommandOwner, }); })() : undefined; @@ -757,7 +806,9 @@ export async function createExecutionRuntimeHostComposition( ), goalTools: requireGoal(goal).tools, builtinTools: runBuiltinTools, - hostTools, + hostTools: managedNodeTestAdmission + ? [...hostTools, managedNodeTestAdmission.tool] + : hostTools, resolveRootTools: (sessionId) => requireGraphCoordinator(graphCoordinator).toolsForSession(sessionId), parentAgentTools: childAgentTools.parentTools, @@ -781,6 +832,9 @@ export async function createExecutionRuntimeHostComposition( ...(managedSession ? { admitManagedMutation: managedSession.writeEdit.admitManagedMutation } : {}), + ...(managedNodeTestAdmission + ? { admitManagedObservation: managedNodeTestAdmission.admit } + : {}), requestDrain: context.requestDrain, }); }), @@ -1058,7 +1112,7 @@ export async function createExecutionRuntimeHostComposition( }, readManagedWorkspaceBoundary: async (sessionId) => { const header = await stores.sessionStore.readHeaderSnapshot(sessionId); - if (header.toolProfile !== 'managed-coding-v1') return undefined; + if (!isManagedCodingSessionToolProfile(header.toolProfile)) return undefined; if (!gitoxideHelperCapability) { throw new Error( 'managed_workspace_profile_unavailable: packaged Gitoxide helper authority is unavailable', diff --git a/packages/runtime-host/src/server/hosted-execution-tool-profile.ts b/packages/runtime-host/src/server/hosted-execution-tool-profile.ts index 7991970e50..fb84be2aa9 100644 --- a/packages/runtime-host/src/server/hosted-execution-tool-profile.ts +++ b/packages/runtime-host/src/server/hosted-execution-tool-profile.ts @@ -32,6 +32,7 @@ const HEADLESS_CODING_V1_TOOL_NAMES = [ ] as const; const MANAGED_CODING_V1_TOOL_NAMES = ['Read', 'Glob', 'Grep', 'Write', 'Edit'] as const; +const MANAGED_CODING_V2_TOOL_NAMES = [...MANAGED_CODING_V1_TOOL_NAMES, 'ManagedNodeTest'] as const; const MANAGED_CODING_V1_SYSTEM_PROMPT = [ 'Inspect the managed Git workspace with Read, Glob, and Grep.', 'Modify it with Write and Edit.', @@ -40,6 +41,11 @@ const MANAGED_CODING_V1_SYSTEM_PROMPT = [ 'No shell, attached-workspace read, or unmanaged filesystem authority is available in this profile.', 'Stop when the requested changes are complete.', ].join('\n'); +const MANAGED_CODING_V2_SYSTEM_PROMPT = [ + MANAGED_CODING_V1_SYSTEM_PROMPT, + 'Run only explicit dependency-free Node tests with ManagedNodeTest.', + 'The test consumes the same immutable accepted Git tree and cannot use npm, package scripts, PATH, network, or the attached checkout.', +].join('\n'); const HEADLESS_CODING_V1_SYSTEM_PROMPT = [ 'Complete the task by acting with the available tools, not by narrating.', @@ -90,6 +96,13 @@ export function hostedExecutionRunProfile( memoryExtraction: false, }; } + if (profile === 'managed-coding-v2') { + return { + toolNames: MANAGED_CODING_V2_TOOL_NAMES, + systemPrompt: MANAGED_CODING_V2_SYSTEM_PROMPT, + memoryExtraction: false, + }; + } if (profile === 'workhub-coordination-v1') { return { toolNames: [], @@ -114,10 +127,17 @@ export function projectHostedExecutionTools( throw new Error(`Hosted tool profile is unavailable: ${missing.join(', ')}`); } return (selected as MakaTool[]).map((tool) => { - if (profile === 'managed-coding-v1') { + if (profile === 'managed-coding-v1' || profile === 'managed-coding-v2') { if (tool.name === 'Read' || tool.name === 'Glob' || tool.name === 'Grep') { return { ...tool, recoveryMode: 'replay_safe' }; } + if (tool.name === 'ManagedNodeTest') { + return { + ...tool, + recoveryMode: 'replay_safe', + durableExecutionProfile: 'managed_observation_v1', + }; + } return { ...tool, recoveryMode: 'reconcile', diff --git a/packages/runtime-host/src/server/managed-workspace-review-coordinator.ts b/packages/runtime-host/src/server/managed-workspace-review-coordinator.ts index b940640b5b..1389172ffd 100644 --- a/packages/runtime-host/src/server/managed-workspace-review-coordinator.ts +++ b/packages/runtime-host/src/server/managed-workspace-review-coordinator.ts @@ -18,6 +18,7 @@ */ import type { InteractiveExecutionStoresWriter } from '@maka/storage/execution-stores'; +import { isManagedCodingSessionToolProfile } from '@maka/core/session'; import { isSessionNotFoundError } from '@maka/storage/execution-stores'; import type { StorageRootLease } from '@maka/storage/root-authority'; import type { @@ -66,7 +67,7 @@ export class HostManagedWorkspaceReviewCoordinator { } try { const header = await this.input.stores.sessionStore.readHeaderSnapshot(input.sessionId); - if (header.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header.toolProfile)) { return failure('invalid_request', 'Session does not own a managed workspace'); } const session = await openGitoxideManagedSessionOwnerInternal({ @@ -101,7 +102,7 @@ export class HostManagedWorkspaceReviewCoordinator { } try { const header = await this.input.stores.sessionStore.readHeaderSnapshot(input.sessionId); - if (header.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header.toolProfile)) { return failure('invalid_request', 'Session does not own a managed workspace'); } const session = await openGitoxideManagedSessionOwnerInternal({ @@ -143,7 +144,7 @@ export class HostManagedWorkspaceReviewCoordinator { } try { const header = await this.input.stores.sessionStore.readHeaderSnapshot(input.sessionId); - if (header.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header.toolProfile)) { return failure('invalid_request', 'Session does not own a managed workspace'); } const session = await openGitoxideManagedSessionOwnerInternal({ @@ -194,7 +195,7 @@ export class HostManagedWorkspaceReviewCoordinator { } try { const header = await this.input.stores.sessionStore.readHeaderSnapshot(input.sessionId); - if (header.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header.toolProfile)) { return failure('invalid_request', 'Session does not own a managed workspace'); } const session = await openGitoxideManagedSessionOwnerInternal({ @@ -234,7 +235,7 @@ export class HostManagedWorkspaceReviewCoordinator { } try { const header = await this.input.stores.sessionStore.readHeaderSnapshot(input.sessionId); - if (header.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header.toolProfile)) { return failure('invalid_request', 'Session does not own a managed workspace'); } const session = await openGitoxideManagedSessionOwnerInternal({ @@ -274,7 +275,7 @@ export class HostManagedWorkspaceReviewCoordinator { } try { const header = await this.input.stores.sessionStore.readHeaderSnapshot(input.sessionId); - if (header.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header.toolProfile)) { return failure('invalid_request', 'Session does not own a managed workspace'); } const session = await openGitoxideManagedSessionOwnerInternal({ @@ -306,7 +307,7 @@ export class HostManagedWorkspaceReviewCoordinator { } try { const header = await this.input.stores.sessionStore.readHeaderSnapshot(input.sessionId); - if (header.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header.toolProfile)) { return failure('invalid_request', 'Session does not own a managed workspace'); } const session = await openGitoxideManagedSessionOwnerInternal({ @@ -350,7 +351,7 @@ export class HostManagedWorkspaceReviewCoordinator { } try { const header = await this.input.stores.sessionStore.readHeaderSnapshot(input.sessionId); - if (header.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header.toolProfile)) { return failure('invalid_request', 'Session does not own a managed workspace'); } const session = await openGitoxideManagedSessionOwnerInternal({ @@ -394,7 +395,7 @@ export class HostManagedWorkspaceReviewCoordinator { } try { const header = await this.input.stores.sessionStore.readHeaderSnapshot(input.sessionId); - if (header.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header.toolProfile)) { return failure('invalid_request', 'Session does not own a managed workspace'); } const session = await openGitoxideManagedSessionOwnerInternal({ diff --git a/packages/runtime-host/src/server/root-turn-coordinator.ts b/packages/runtime-host/src/server/root-turn-coordinator.ts index 405dea6fb0..e5745658ef 100644 --- a/packages/runtime-host/src/server/root-turn-coordinator.ts +++ b/packages/runtime-host/src/server/root-turn-coordinator.ts @@ -30,7 +30,11 @@ import { type MessageContent, type SessionEvent, } from '@maka/core/events'; -import { isWorkHubCoordinationSessionId, type SessionHeader } from '@maka/core/session'; +import { + isManagedCodingSessionToolProfile, + isWorkHubCoordinationSessionId, + type SessionHeader, +} from '@maka/core/session'; import { resolveEffectiveOrchestration } from '@maka/core/orchestration'; import { decodeSkillInvocationResult, @@ -455,7 +459,7 @@ export class RootTurnCoordinator implements HostedExecutionAuthority { */ async resumeManagedContinuationsAfterRecovery(sessions: readonly SessionHeader[]): Promise { for (const session of [...sessions].sort((left, right) => left.id.localeCompare(right.id))) { - if (session.isArchived || session.toolProfile !== 'managed-coding-v1') continue; + if (session.isArchived || !isManagedCodingSessionToolProfile(session.toolProfile)) continue; await this.resumeManagedContinuationAfterRecovery(session.id); } } @@ -471,7 +475,7 @@ export class RootTurnCoordinator implements HostedExecutionAuthority { const header = await this.stores.sessionStore.readHeaderSnapshot(sessionId); if ( header.isArchived || - header.toolProfile !== 'managed-coding-v1' || + !isManagedCodingSessionToolProfile(header.toolProfile) || runtimeHostSafeBoundaryContinuationUnavailableReason(header) || this.#executions.has(sessionId) ) { diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts index e39aa6ee3f..83ca9ffe02 100644 --- a/packages/runtime/src/__tests__/session-manager.test.ts +++ b/packages/runtime/src/__tests__/session-manager.test.ts @@ -4723,63 +4723,65 @@ describe('SessionManager permission mode updates', () => { expect(plan.rejectionReasons).toEqual(['safety_observation_unavailable']); }); - test('never downgrades a managed session when its workspace boundary is unavailable', async () => { - const store = new MemorySessionStore(); - const runStore = new MemoryAgentRunStore(); - const manager = new SessionManager({ - store, - runStore, - runtimeEventStore: runStore, - backends: new BackendRegistry(), - safeBoundaryResumeEnabled: true, - inspectContinuationSafety: async () => ({ - workspaceIdentity: 'workspace-managed', - backgroundOperationsSettled: true, - availableToolNames: ['Write', 'Edit'], - }), - newId: nextId(), - now: nextNow(6_533), - }); - const session = await manager.createSession(makeInput({ toolProfile: 'managed-coding-v1' })); - const header = await store.readHeader(session.id); - expect(header.toolProfile).toBe('managed-coding-v1'); - const sourceRunId = 'source-run-managed-boundary-missing'; - const sourceTurnId = 'source-turn-managed-boundary-missing'; - await seedRuntimeRun( - runStore, - makeRunHeader({ - runId: sourceRunId, - sessionId: session.id, - turnId: sourceTurnId, - status: 'failed', - cwd: header.cwd, - workspaceIdentity: 'workspace-managed', - createdAt: 1, - updatedAt: 2, - completedAt: 2, - failureClass: 'app_restarted', - }), - [ - runtimeEvent({ - id: 'source-terminal-managed-boundary-missing', - invocationId: 'source-invocation-managed-boundary-missing', + for (const toolProfile of ['managed-coding-v1', 'managed-coding-v2'] as const) { + test(`never downgrades a ${toolProfile} session when its workspace boundary is unavailable`, async () => { + const store = new MemorySessionStore(); + const runStore = new MemoryAgentRunStore(); + const manager = new SessionManager({ + store, + runStore, + runtimeEventStore: runStore, + backends: new BackendRegistry(), + safeBoundaryResumeEnabled: true, + inspectContinuationSafety: async () => ({ + workspaceIdentity: 'workspace-managed', + backgroundOperationsSettled: true, + availableToolNames: ['Write', 'Edit'], + }), + newId: nextId(), + now: nextNow(6_533), + }); + const session = await manager.createSession(makeInput({ toolProfile })); + const header = await store.readHeader(session.id); + expect(header.toolProfile).toBe(toolProfile); + const sourceRunId = 'source-run-managed-boundary-missing'; + const sourceTurnId = 'source-turn-managed-boundary-missing'; + await seedRuntimeRun( + runStore, + makeRunHeader({ runId: sourceRunId, sessionId: session.id, turnId: sourceTurnId, - ts: 2, status: 'failed', - actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } }, + cwd: header.cwd, + workspaceIdentity: 'workspace-managed', + createdAt: 1, + updatedAt: 2, + completedAt: 2, + failureClass: 'app_restarted', }), - ], - ); + [ + runtimeEvent({ + id: 'source-terminal-managed-boundary-missing', + invocationId: 'source-invocation-managed-boundary-missing', + runId: sourceRunId, + sessionId: session.id, + turnId: sourceTurnId, + ts: 2, + status: 'failed', + actions: { endInvocation: true, stateDelta: { failureClass: 'app_restarted' } }, + }), + ], + ); - const plan = await manager.planAuthoritativeSafeBoundaryContinuation(session.id, { - sourceRunId, - }); + const plan = await manager.planAuthoritativeSafeBoundaryContinuation(session.id, { + sourceRunId, + }); - expect(plan.disposition).toBe('park'); - expect(plan.rejectionReasons).toEqual(['workspace_boundary_unavailable']); - }); + expect(plan.disposition).toBe('park'); + expect(plan.rejectionReasons).toEqual(['workspace_boundary_unavailable']); + }); + } test('keeps the authoritative continuation entry disabled unless the host enables it', async () => { const store = new MemorySessionStore(); diff --git a/packages/runtime/src/runtime-kernel.ts b/packages/runtime/src/runtime-kernel.ts index f4987689a5..e08e41c902 100644 --- a/packages/runtime/src/runtime-kernel.ts +++ b/packages/runtime/src/runtime-kernel.ts @@ -52,6 +52,7 @@ import type { TurnRecord, TurnStateMessage, } from '@maka/core/session'; +import { isManagedCodingSessionToolProfile } from '@maka/core/session'; import { isDeepStrictEqual } from 'node:util'; import type { UserMessageInput } from '@maka/core/runtime-inputs'; import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary'; @@ -631,7 +632,7 @@ export class RuntimeKernel implements RuntimeKernelLike { await this.enterExecutionClaim(execution); const header = await this.deps.store.readHeader(sessionId); let workspaceIdentity: string | undefined; - const managedCoding = header.toolProfile === 'managed-coding-v1'; + const managedCoding = isManagedCodingSessionToolProfile(header.toolProfile); if (managedCoding && !this.deps.inspectContinuationSafety) { throw new Error('Managed coding workspace boundary authority is unavailable'); } diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts index 95de24bd26..3fad09d62f 100644 --- a/packages/runtime/src/session-manager.ts +++ b/packages/runtime/src/session-manager.ts @@ -59,6 +59,7 @@ import type { SystemNoteMessage, PersistedBackendKind, } from '@maka/core/session'; +import { isManagedCodingSessionToolProfile } from '@maka/core/session'; import type { CreateSessionInput, BranchFromTurnInput, @@ -2065,7 +2066,7 @@ export class SessionManager { let header: SessionHeader | undefined; if (this.deps.safeBoundaryResumeEnabled !== true) { header = await this.deps.store.readHeader(sessionId).catch(() => undefined); - if (header?.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(header?.toolProfile)) { const plan = resumeFeatureDisabledPlan(); this.recordContinuationPlan(sessionId, input.sourceRunId, plan); return plan; @@ -2138,8 +2139,9 @@ export class SessionManager { currentWorkspaceIdentity: observation.workspaceIdentity, backgroundOperationsSettled: observation.backgroundOperationsSettled, availableToolNames: observation.availableToolNames, - workspaceBoundaryRequirement: - header.toolProfile === 'managed-coding-v1' ? 'required' : 'optional', + workspaceBoundaryRequirement: isManagedCodingSessionToolProfile(header.toolProfile) + ? 'required' + : 'optional', ...(input.expectedRuntimeEventHighWater !== undefined ? { expectedRuntimeEventHighWater: input.expectedRuntimeEventHighWater } : {}), @@ -2161,7 +2163,7 @@ export class SessionManager { // discovery; an absent/unreadable/non-managed header stays on the legacy // disabled path and cannot silently gain Resume authority. const managedHeader = await this.deps.store.readHeader(sessionId).catch(() => undefined); - if (managedHeader?.toolProfile !== 'managed-coding-v1') { + if (!isManagedCodingSessionToolProfile(managedHeader?.toolProfile)) { const plan = resumeFeatureDisabledPlan(); this.recordContinuationPlan(sessionId, '', plan); return plan;