diff --git a/docs/architecture/managed-dependency-production-composition-v1.zh-CN.md b/docs/architecture/managed-dependency-production-composition-v1.zh-CN.md new file mode 100644 index 00000000000..c98b596a58d --- /dev/null +++ b/docs/architecture/managed-dependency-production-composition-v1.zh-CN.md @@ -0,0 +1,168 @@ +--- +document_status: implementation-contract +status: implemented-stacked-pr4 +date: 2026-08-09 +milestone: M1.3 +stack_base: bundled-npm-runtime-attestation-v1 +--- + +# Managed Dependency Production Composition v1 + +## 1. 本切片只证明一个主要不变量 + +> Runtime Host 请求 `dependency_environment_v1` 时,只能消费由当前 storage-root owner 创建、由 attested bundled npm producer 物化、由 durable receipt 证明且仍持有 active lease 的 Maka-owned dependency artifact;任一证据缺失时必须在 worker dispatch 前 fail closed,不得退回 attached checkout 的 `node_modules`、用户 PATH 上的 npm 或未经验证的目录。 + +本切片把前三个独立基础切片组合成首个生产组合链路: + +1. storage authority:拥有 identity、staging publication、receipt、lease、GC 与 crash convergence; +2. producer boundary:拥有固定 npm argv、Permission Model、hermetic environment、process-tree 回收与 bounded observation; +3. bundled npm attestation:拥有 release manifest、完整 runtime inventory、Node executable/runtime identity 与重验能力; +4. 本切片:由 Runtime Host candidate、execution composition、`ManagedWorkspaceOwner` 和 filesystem worker bridge 连接以上能力。 + +本切片不启用 Desktop UI 的默认 managed mode,不改变普通 attached checkout,不提供 Shell/Build、Write/Edit、secret projection 或 scratch capability,也不把依赖环境当作 workspace checkpoint。 + +首个生产消费者是模型可见的 `ManagedWorkspaceInspect` 专用只读任务工具。它不是 session execution mode,也不会替换普通 Read/Glob/Grep:只有模型显式调用该工具时,当前 session cwd 才会作为 source 进入 managed admission。工具参数只能表达一个有界的 Read、Glob 或 Grep 操作,不能提供 source root、workspace identity、raw cwd 或 execution profile。 + +## 2. Owner 与能力边界 + +### 2.1 生产 owner 链 + +```text +packaged Electron process identity + -> fixed process.resourcesPath + -> bundled Git + bundled npm attestation + -> interactive root owner / OS writer lock + -> Runtime Host execution composition + -> ManagedWorkspaceOwner + -> ManagedDependencyEnvironmentAuthority + -> owner-token execution scope + -> sandboxed filesystem worker +``` + +只有 packaged Electron(存在 Electron identity 且 `defaultApp !== true`)可以把 `process.resourcesPath` 解释为 release authority。Node/CLI 与开发态 Electron 不从 ambient directory 自动发现 bundled runtime。测试可以显式注入 verified Git input,但生产 npm producer只能从 attested bundled resources 创建。 + +`ManagedWorkspaceOwner` 是跨 Git baseline、dependency lease 与 worker scope 的组合 owner。它负责: + +- 从 accepted baseline commit 读取 tracked `package.json` 与 `package-lock.json`; +- 拒绝 canonical tree 中已 tracked 的 `node_modules`; +- 要求 `packageManager` 精确等于 `npm@12.0.2`; +- 用 manifest/lockfile bytes、Node ABI、platform/arch、producer runtime/policy digest 计算 environment identity; +- 在可能耗时的 provision 完成后重新验证 root identity、Git artifact 与 immutable workspace head; +- 仅在上述重验通过后签发带 dependency root 的 owner-token scope; +- scope 结束时先 revoke,再 release dependency lease;owner drain 后关闭 dependency authority,最后才允许 root owner 关闭。 + +Runtime Host 只接收公开的 execution-stores facade。raw workspace baseline writer 通过 module-private WeakMap seam 绑定到该 facade,不作为公共 API 暴露,调用方不能自己拼接未认证 store。 + +## 3. 原子性边界与 durable root identity + +artifact publication 与 receipt 的原子性仍由 storage authority 切片负责。本切片不创建第二套事实源。生产组合只消费: + +```text +verified baseline commit + + canonical environment identity + + verified artifact/receipt pair + + active lease + + current workspace head +``` + +workspace baseline authority 首次使用前必须绑定 `.maka-storage-root.json` 的 durable `root_id`。新建 runtime database 在其他 operational authority 写入事实前完成绑定。旧的 unbound database 如果已有 session、RuntimeEvent 或非空 authority state,必须走显式 whole-root adoption;不能由启动代码静默认领。 + +允许在 binding 前存在的只有精确 bootstrap 状态:schema/capability rows、revision 为 0 的 automation/usage authority row,以及 generation/pending_writes 均为 0 的 session catalog row。控制行一旦变化即属于 logical state,自动绑定必须 fail closed。 + +## 4. Logical `node_modules` binding + +managed worktree 内不创建 symlink、junction,也不复制 dependency tree。owner scope 内部持有真实 dependency root;worker bridge 把以下逻辑路径映射到该 root: + +```text +node_modules/** +/node_modules/** +``` + +worker permission profile只增加 exact dependency root 的 read-only subtree,不扩大到 storage root。返回的 Glob/Grep 路径重新映射成 `node_modules/**`,真实 artifact path 不进入模型可见结果。 + +路径 admission 在 worker dispatch 前拒绝: + +- `..`、`.`、空 segment; +- NUL; +- `:`(Windows ADS 与 drive-like 非 canonical 形状); +- absolute escape 或 dependency root 外路径; +- provisioning 与 dependency root 不成对的 forged scope。 + +路由判断不直接消费原始首段。完整输入必须先按平台路径语义验证;任何包含 `.`、`..`、空 segment 或其他非 canonical 形状的路径都直接拒绝。Windows 的 `node_modules` 身份比较大小写不敏感,因此 `NODE_MODULES/**` 也必须进入 leased dependency root,不能落回 managed worktree。 + +如果 scope 请求 `dependency_environment_v1` 但没有 exact lease root,必须拒绝整个 operation,不能把相同逻辑路径交给 attached cwd。 + +## 5. Producer 与运行时协议 + +生产 producer只接受 PR3 发出的不可伪造 `ManagedNpmRuntimeCapability`。每次 provision 都重新验证 Node executable 与 bundled npm完整 inventory,然后运行: + +```text +node --permission + --allow-fs-read= + --allow-fs-read= + --allow-fs-write= + npm-cli.js ci + --ignore-scripts --no-audit --no-fund --package-lock=true +``` + +环境不继承 host secrets、HOME、npm config 或 PATH package manager。HOME、cache 与 TEMP 都位于本次 staging。Node compile cache 显式禁用:producer 生命周期很短,没有可衡量收益;Node 26/Windows 在 Permission Model 与 `NODE_COMPILE_CACHE` 同时启用时可能卡在启动阶段,因此该组合不属于 v1 profile。 + +producer failure、timeout、abort、process-tree drain failure、runtime revalidation failure 或 observed limit failure都回滚 staging,不发布 receipt,也不 fallback。 + +调用者取消从 Runtime Host composition 贯穿 baseline admission、Git invocation、`ManagedWorkspaceOwner`、dependency authority 到 producer。可取消的 artifact writer admission 使用有界轮询获取 OS lock,取消后不会留下仍在等待锁的后台 owner。等价 environment 的并发 acquisition 共用一次 publication,但共享 producer 不受任意单个 waiter 支配:只有最后一个 waiter 放弃且 publication 尚未结束时,authority 才终止 producer。若新 caller 在被撤销 publication 的清理窗口进入,它必须等待旧 owner 收敛后重新观察 canonical slot,不能继承旧 caller 的 abort。Host drain 等待 admission、acquisition 和 producer 清理结束后才能关闭 receipt authority。 + +`ManagedWorkspaceInspect` 从 ToolRuntime context 取得 canonical session cwd 与 AbortSignal。Host 以 canonical source root 和 session identity 域分离地产生稳定的 repository/workspace/epoch/instance identity;模型无法自认证这些 ID。同一 session/source 的多次只读任务复用同一 accepted baseline,source 后续漂移时 fail closed,不在工具内部创建隐式 rebaseline。工具使用 `replay_safe`:它只消费同一 managed baseline 的只读结果,崩溃后重复 admission 不产生 workspace mutation;provider-facing 结果另有 64 KiB 上限,超限要求缩小读取或搜索范围。 + +## 6. 稳定失败与回滚 + +主要稳定失败包括: + +```text +managed_workspace_profile_unavailable +managed_workspace_execution_options_invalid +managed_dependency_producer_unavailable +managed_dependency_manifest_unsupported +managed_dependency_provision_failed +managed_workspace_operation_denied +``` + +回滚/收敛规则: + +- bundled runtime 不可验证:candidate 不获得 managed owner;attached execution 独立存在; +- baseline manifest/lockfile 不支持:scope 不签发,worktree 与 source 不修改; +- provision 中断:producer tree被回收,staging由 authority 清理; +- artifact 已发布但 receipt 未提交:按 storage authority 协议删除 orphan 并重建; +- receipt 已提交但 scope 未签发:下次 acquire 重验并复用,无 durable half-scope; +- provision 后 workspace head/drift 变化:释放 lease并拒绝 scope; +- operation 完成/失败:先 revoke scope,再释放 lease; +- host drain:等待 active operation,关闭 workspace composition/managed owner/dependency authority,最后释放 root owner。 +- caller abort:baseline Git admission、尚未签发 scope 的 acquisition 与 filesystem worker 均消费同一 signal;若已没有其他 waiter,则回收 producer tree 与 staging;不得继续签发 scope。 + +## 7. 平台能力矩阵 + +| 平台 | dependency authority | producer profile | logical worker binding | v1 承诺 | +|---|---|---|---|---| +| Linux | 支持 | Node Permission Model + process group | exact read-only root | process-crash convergence;不承诺断电级 storage durability | +| macOS | 支持 | 同 Linux;canonical path alias | exact read-only root | process-crash convergence;普通 `fsync` 不提升为 `F_FULLFSYNC` 承诺 | +| Windows | 支持 | Node Permission Model + process-tree owner;compile cache关闭 | 依赖 M1.2 Windows sandbox能力,缺失则 fail closed | process-crash convergence;不承诺 power-loss convergence | + +observed bytes/entries 是 soft postcondition,不是 OS quota;不能宣称它能阻止瞬时磁盘超写。项目 dependency artifact 存在 storage cache,不打进 Maka release,因此用户项目依赖不会继续膨胀安装包。 + +## 8. Production-shaped verification + +本切片必须至少证明: + +1. 真实 Git source baseline,且 source 中存在 ignored attached `node_modules`; +2. 真实 execution stores/root owner 与 durable root binding; +3. attested fixture npm runtime经真实 producer child process运行; +4. storage authority发布并租用 artifact; +5. Runtime Host workspace composition打开 managed profile; +6. 生产模型工具注册 `ManagedWorkspaceInspect`,并由该工具打开 owner-bound profile; +7. filesystem worker读取逻辑 `node_modules/**` 时得到 Maka-owned 内容,而不是 attached 内容; +8. drain/close 能在 lease释放后完成; +9. producer missing、manifest mismatch、path traversal、forged scope、unbound logical state均 fail closed。 +10. 真实 Host 子进程在 dependency receipt durable 后退出;同一 source/session/task 重开后只存在一份 canonical baseline、dependency artifact 与 receipt,staging 为空,并返回相同只读结果。 + +当前切片通过 `ManagedWorkspaceInspect` 形成 M1.3 的生产闭环:普通 session 保持 attached;显式工具调用才进入 managed baseline、dependency lease 与 read-only worker,工具完成后 scope 与 lease 均释放。production-shaped 测试使用真实 Git source、execution stores、root owner、attested npm child process 和 managed owner,并通过该生产工具读取 Maka-owned `node_modules`,不再直接把内部 composition API 当作消费者。 + +这不等于 session-level managed mode 已完成。将来若让整个 session 的普通 Read/Write/Bash 都进入 managed workspace,仍必须显式定义 durable execution profile、M2 mutation acceptance 与 M3 workspace-bound continuation;不能把本工具的 session/source identity 偷换成全 session 模式。 diff --git a/packages/runtime-host/src/__tests__/bundled-npm-runtime.test.ts b/packages/runtime-host/src/__tests__/bundled-npm-runtime.test.ts index c1c5d5d8210..61ec30d823e 100644 --- a/packages/runtime-host/src/__tests__/bundled-npm-runtime.test.ts +++ b/packages/runtime-host/src/__tests__/bundled-npm-runtime.test.ts @@ -6,6 +6,7 @@ import test from 'node:test'; import { resolveBundledNpmRuntime } from '../server/bundled-npm-runtime.js'; import { runManagedNpmDependencyProvision } from '../server/managed-dependency-producer-process.js'; +import { createManagedNpmDependencyEnvironmentProducer } from '../server/managed-npm-dependency-producer.js'; test('attests a strict packaged npm tree against every declared file', async (t) => { const fixture = await createFixture(); @@ -28,6 +29,22 @@ test('attests a strict packaged npm tree against every declared file', async (t) assert.match(capability.runtimeIdentitySha256, /^sha256:[a-f0-9]{64}$/u); }); +test('binds the storage producer identity to the attested npm capability', async (t) => { + const fixture = await createFixture(); + t.after(fixture.remove); + const runtime = await resolveBundledNpmRuntime({ resourcesRoot: fixture.root }); + + const producer = createManagedNpmDependencyEnvironmentProducer(runtime); + + assert.equal(producer.packageManagerName, 'npm'); + assert.equal(producer.packageManagerVersion, runtime.npmVersion); + assert.equal(producer.nodeRuntime.version, runtime.nodeVersion); + assert.equal(producer.nodeRuntime.abi, runtime.nodeAbi); + assert.equal(producer.capability.runtimeIdentitySha256, runtime.runtimeIdentitySha256); + assert.equal(producer.capability.kind, 'hermetic_dependency_builder_v1'); + assert.equal(producer.capability.lifecycleScripts, 'disabled'); +}); + test('rejects a bundled npm file changed before attestation', async (t) => { const fixture = await createFixture(); t.after(fixture.remove); diff --git a/packages/runtime-host/src/__tests__/execution-bundled-resources.test.ts b/packages/runtime-host/src/__tests__/execution-bundled-resources.test.ts new file mode 100644 index 00000000000..cab8c3baa87 --- /dev/null +++ b/packages/runtime-host/src/__tests__/execution-bundled-resources.test.ts @@ -0,0 +1,56 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { resolveExecutionBundledResourcesRoot } from '../server/execution-bundled-resources.js'; +import { startExecutionRuntimeHostCandidate } from '../server/execution-candidate.js'; + +test('admits bundled resources only from packaged Electron identity', () => { + assert.equal( + resolveExecutionBundledResourcesRoot({ + electronVersion: '43.2.0', + defaultApp: false, + resourcesPath: '/Applications/Maka.app/Contents/Resources', + }), + '/Applications/Maka.app/Contents/Resources', + ); + assert.equal( + resolveExecutionBundledResourcesRoot({ + electronVersion: '43.2.0', + defaultApp: true, + resourcesPath: '/electron/Resources', + }), + undefined, + ); + assert.equal( + resolveExecutionBundledResourcesRoot({ resourcesPath: '/ambient/resources' }), + undefined, + ); +}); + +test('candidate refuses bundled npm without the paired managed Git authority', async () => { + await assert.rejects( + startExecutionRuntimeHostCandidate({ + rootPath: '/unused', + expectedRootId: 'a'.repeat(64), + bundledNpmResourcesRoot: '/untrusted/npm', + }), + /requires managed workspace Git authority/u, + ); +}); + +test('candidate attests bundled npm before acquiring the operational root', async () => { + await assert.rejects( + startExecutionRuntimeHostCandidate({ + rootPath: '/must-not-be-opened', + expectedRootId: 'a'.repeat(64), + managedWorkspaceGitRuntime: { + executablePath: process.execPath, + expectedSha256: `sha256:${'0'.repeat(64)}`, + }, + bundledNpmResourcesRoot: '/missing/bundled/resources', + }), + (error) => + error instanceof Error && + error.name === 'BundledNpmRuntimeError' && + error.message === 'Bundled npm runtime is unavailable', + ); +}); diff --git a/packages/runtime-host/src/__tests__/fixtures/managed-workspace-inspection-crash-child.ts b/packages/runtime-host/src/__tests__/fixtures/managed-workspace-inspection-crash-child.ts new file mode 100644 index 00000000000..d6c7ba583fb --- /dev/null +++ b/packages/runtime-host/src/__tests__/fixtures/managed-workspace-inspection-crash-child.ts @@ -0,0 +1,80 @@ +import { execFile } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import { readFile, realpath } from 'node:fs/promises'; +import { promisify } from 'node:util'; +import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores'; +import { openManagedWorkspaceOwner } from '@maka/storage/managed-workspace-owner'; +import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority'; +import { resolveBundledNpmRuntime } from '../../server/bundled-npm-runtime.js'; +import { createManagedNpmDependencyEnvironmentProducer } from '../../server/managed-npm-dependency-producer.js'; +import { createManagedWorkspaceInspectionTool } from '../../server/managed-workspace-inspection-tool.js'; +import { createRuntimeHostWorkspaceExecutionComposition } from '../../server/workspace-execution-composition.js'; + +const execFileAsync = promisify(execFile); +const storageRoot = process.env.MAKA_MANAGED_INSPECTION_CRASH_STORAGE_ROOT; +const sourceRoot = process.env.MAKA_MANAGED_INSPECTION_CRASH_SOURCE_ROOT; +const resourcesRoot = process.env.MAKA_MANAGED_INSPECTION_CRASH_RESOURCES_ROOT; +if (!storageRoot || !sourceRoot || !resourcesRoot) { + throw new Error('Missing managed inspection crash fixture input'); +} + +const capability = await resolveStorageRoot({ path: storageRoot, kind: 'interactive' }); +const rootOwner = await tryAcquireInteractiveRootOwner(capability); +if (!rootOwner) throw new Error('Managed inspection crash fixture did not acquire root ownership'); +const stores = await openInteractiveExecutionStoresForWrite(rootOwner.lease); +const gitExecutable = await findGitExecutable(); +const owner = await openManagedWorkspaceOwner({ + rootOwner, + gitRuntime: { + executablePath: gitExecutable, + expectedSha256: await sha256File(gitExecutable), + }, + dependencyEnvironmentProducer: createManagedNpmDependencyEnvironmentProducer( + await resolveBundledNpmRuntime({ resourcesRoot }), + ), + filesystemWorker: { + async execute(input) { + if (input.operation.kind !== 'read') throw new Error('Crash fixture expected Read'); + return { kind: 'read', content: await readFile(input.operation.path, 'utf8') }; + }, + }, + failpoint(point) { + if (String(point) === 'after_environment_receipt_durable') process.exit(73); + }, +}); +const composition = createRuntimeHostWorkspaceExecutionComposition({ + managedOwner: owner, + executionStores: stores, +}); +const tool = createManagedWorkspaceInspectionTool(composition); +await tool.impl( + { kind: 'read', path: 'node_modules/fixture-package/index.js' }, + { + sessionId: 'session_11111111111111111111111111111111', + turnId: 'turn_22222222222222222222222222222222', + toolCallId: 'call_33333333333333333333333333333333', + cwd: sourceRoot, + abortSignal: new AbortController().signal, + emitOutput() {}, + }, +); +throw new Error('Managed inspection crash fixture missed its failpoint'); + +async function findGitExecutable(): Promise { + const { stdout } = await execFileAsync(process.platform === 'win32' ? 'where.exe' : 'which', [ + 'git', + ]); + const first = stdout + .split(/\r?\n/u) + .find((line) => line.trim()) + ?.trim(); + if (!first) throw new Error('Git executable is unavailable'); + return await realpath(first); +} + +async function sha256File(path: string): Promise<`sha256:${string}`> { + const hash = createHash('sha256'); + for await (const chunk of createReadStream(path)) hash.update(chunk); + return `sha256:${hash.digest('hex')}`; +} diff --git a/packages/runtime-host/src/__tests__/managed-dependency-producer-process.test.ts b/packages/runtime-host/src/__tests__/managed-dependency-producer-process.test.ts index f3e4a38e7ee..30f35f9d77e 100644 --- a/packages/runtime-host/src/__tests__/managed-dependency-producer-process.test.ts +++ b/packages/runtime-host/src/__tests__/managed-dependency-producer-process.test.ts @@ -116,6 +116,8 @@ test('runs the fixed npm install protocol with a hermetic environment', { ]); assert.equal(invocation.env.npm_config_registry, 'https://registry.npmjs.org/'); assert.equal(invocation.env.npm_config_ignore_scripts, 'true'); + assert.equal(invocation.env.NODE_DISABLE_COMPILE_CACHE, '1'); + assert.equal(invocation.env.NODE_COMPILE_CACHE, undefined); assert.equal(invocation.env.MAKA_DEPENDENCY_SECRET_FOR_TEST, undefined); if (process.platform === 'win32') { const relativeHome = join('.maka-runtime', 'home'); diff --git a/packages/runtime-host/src/__tests__/managed-dependency-production-composition.test.ts b/packages/runtime-host/src/__tests__/managed-dependency-production-composition.test.ts new file mode 100644 index 00000000000..224095d0ec7 --- /dev/null +++ b/packages/runtime-host/src/__tests__/managed-dependency-production-composition.test.ts @@ -0,0 +1,305 @@ +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; +import test from 'node:test'; +import { openInteractiveExecutionStoresForWrite } from '@maka/storage/execution-stores'; +import { openManagedWorkspaceOwner } from '@maka/storage/managed-workspace-owner'; +import { resolveStorageRoot, tryAcquireInteractiveRootOwner } from '@maka/storage/root-authority'; +import { resolveBundledNpmRuntime } from '../server/bundled-npm-runtime.js'; +import { createManagedNpmDependencyEnvironmentProducer } from '../server/managed-npm-dependency-producer.js'; +import { createManagedWorkspaceInspectionTool } from '../server/managed-workspace-inspection-tool.js'; +import { createRuntimeHostWorkspaceExecutionComposition } from '../server/workspace-execution-composition.js'; + +const execFileAsync = promisify(execFile); +const crashChildEntrypoint = fileURLToPath( + new URL('./fixtures/managed-workspace-inspection-crash-child.js', import.meta.url), +); + +test('production composition acquires, reads, and drains an attested dependency environment', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-managed-dependency-composition-')); + const storageRoot = join(root, 'storage'); + const sourceRoot = await createSource(join(root, 'source')); + const resourcesRoot = await createNpmFixture(join(root, 'resources')); + const capability = await resolveStorageRoot({ path: storageRoot, kind: 'interactive' }); + const rootOwner = await tryAcquireInteractiveRootOwner(capability); + assert.ok(rootOwner); + const stores = await openInteractiveExecutionStoresForWrite(rootOwner.lease); + let composition: ReturnType | undefined; + try { + const gitExecutable = await findGitExecutable(); + const owner = await openManagedWorkspaceOwner({ + rootOwner, + gitRuntime: { + executablePath: gitExecutable, + expectedSha256: await sha256File(gitExecutable), + }, + dependencyEnvironmentProducer: createManagedNpmDependencyEnvironmentProducer( + await resolveBundledNpmRuntime({ resourcesRoot }), + ), + filesystemWorker: { + async execute(input) { + assert.equal(input.operation.kind, 'read'); + assert.equal(input.operation.path.startsWith(sourceRoot), false); + return { kind: 'read', content: await readFile(input.operation.path, 'utf8') }; + }, + }, + }); + composition = createRuntimeHostWorkspaceExecutionComposition({ + managedOwner: owner, + executionStores: stores, + }); + const tool = createManagedWorkspaceInspectionTool(composition); + + assert.deepEqual( + await tool.impl( + { kind: 'read', path: 'node_modules/fixture-package/index.js' }, + { + sessionId: 'session_11111111111111111111111111111111', + turnId: 'turn_22222222222222222222222222222222', + toolCallId: 'call_33333333333333333333333333333333', + cwd: sourceRoot, + abortSignal: new AbortController().signal, + emitOutput() {}, + }, + ), + { + kind: 'managed_workspace_inspection_v1', + result: { kind: 'read', content: 'module.exports = "maka-owned";\n' }, + }, + ); + composition.beginDrain(); + await composition.close(); + assert.equal(composition.state, 'closed'); + } finally { + await composition?.close().catch(() => undefined); + await stores.sessionStore.close?.(); + await rootOwner.close(); + await rm(root, { recursive: true, force: true }); + } +}); + +test('replays one managed inspection after a real Host crash at durable dependency receipt', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-managed-inspection-crash-')); + const storageRoot = join(root, 'storage'); + const sourceRoot = await createSource(join(root, 'source')); + const resourcesRoot = await createNpmFixture(join(root, 'resources')); + let rootOwner: Awaited>; + let stores: Awaited> | undefined; + let composition: ReturnType | undefined; + try { + await assert.rejects( + execFileAsync(process.execPath, [crashChildEntrypoint], { + env: { + ...process.env, + MAKA_MANAGED_INSPECTION_CRASH_STORAGE_ROOT: storageRoot, + MAKA_MANAGED_INSPECTION_CRASH_SOURCE_ROOT: sourceRoot, + MAKA_MANAGED_INSPECTION_CRASH_RESOURCES_ROOT: resourcesRoot, + }, + windowsHide: true, + }), + (error: unknown) => error instanceof Error && 'code' in error && Number(error.code) === 73, + ); + + const capability = await resolveStorageRoot({ path: storageRoot, kind: 'interactive' }); + rootOwner = await tryAcquireInteractiveRootOwner(capability); + assert.ok(rootOwner); + stores = await openInteractiveExecutionStoresForWrite(rootOwner.lease); + const gitExecutable = await findGitExecutable(); + const owner = await openManagedWorkspaceOwner({ + rootOwner, + gitRuntime: { + executablePath: gitExecutable, + expectedSha256: await sha256File(gitExecutable), + }, + dependencyEnvironmentProducer: createManagedNpmDependencyEnvironmentProducer( + await resolveBundledNpmRuntime({ resourcesRoot }), + ), + filesystemWorker: { + async execute(input) { + assert.equal(input.operation.kind, 'read'); + return { kind: 'read', content: await readFile(input.operation.path, 'utf8') }; + }, + }, + }); + composition = createRuntimeHostWorkspaceExecutionComposition({ + managedOwner: owner, + executionStores: stores, + }); + const result = await createManagedWorkspaceInspectionTool(composition).impl( + { kind: 'read', path: 'node_modules/fixture-package/index.js' }, + { + sessionId: 'session_11111111111111111111111111111111', + turnId: 'turn_22222222222222222222222222222222', + toolCallId: 'call_33333333333333333333333333333333', + cwd: sourceRoot, + abortSignal: new AbortController().signal, + emitOutput() {}, + }, + ); + + assert.deepEqual(result, { + kind: 'managed_workspace_inspection_v1', + result: { kind: 'read', content: 'module.exports = "maka-owned";\n' }, + }); + const dependencyRoot = join(storageRoot, 'managed-workspaces', 'dependency-environments'); + assert.deepEqual(await readdir(join(dependencyRoot, '.staging')), []); + assert.equal( + (await readdir(dependencyRoot, { withFileTypes: true })).filter( + (entry) => entry.isDirectory() && entry.name !== '.staging', + ).length, + 1, + ); + assert.equal(await countFilesNamed(join(storageRoot, 'managed-workspaces'), 'binding.json'), 1); + assert.equal( + await countFilesNamed(join(storageRoot, 'managed-workspaces'), 'baseline-receipt.json'), + 1, + ); + } finally { + composition?.beginDrain(); + await composition?.close().catch(() => undefined); + await stores?.sessionStore.close?.(); + await rootOwner?.close(); + await rm(root, { recursive: true, force: true }); + } +}); + +async function countFilesNamed(root: string, expectedName: string): Promise { + let count = 0; + for (const entry of await readdir(root, { withFileTypes: true })) { + const path = join(root, entry.name); + if (entry.isDirectory()) count += await countFilesNamed(path, expectedName); + else if (entry.isFile() && entry.name === expectedName) count += 1; + } + return count; +} + +async function createSource(sourceRoot: string): Promise { + await mkdir(sourceRoot, { recursive: true }); + await git(sourceRoot, 'init', '--quiet'); + await Promise.all([ + writeFile( + join(sourceRoot, 'package.json'), + '{"name":"fixture","version":"1.0.0","packageManager":"npm@12.0.2"}\n', + ), + writeFile( + join(sourceRoot, 'package-lock.json'), + '{"name":"fixture","version":"1.0.0","lockfileVersion":3,"packages":{"":{"name":"fixture","version":"1.0.0"}}}\n', + ), + writeFile(join(sourceRoot, '.gitignore'), 'node_modules/\n.maka-workspace.json\n'), + ]); + await git(sourceRoot, 'add', 'package.json', 'package-lock.json', '.gitignore'); + await git( + sourceRoot, + '-c', + 'user.name=Maka Test', + '-c', + 'user.email=test@maka.invalid', + 'commit', + '--quiet', + '-m', + 'dependency baseline', + ); + const attached = join(sourceRoot, 'node_modules', 'fixture-package'); + await mkdir(attached, { recursive: true }); + await writeFile(join(attached, 'index.js'), 'module.exports = "attached";\n'); + return await realpath(sourceRoot); +} + +async function createNpmFixture(resourcesRoot: string): Promise { + const npmRoot = join(resourcesRoot, 'npm'); + const cli = Buffer.from( + "const fs=require('node:fs');fs.mkdirSync('node_modules/fixture-package',{recursive:true});fs.writeFileSync('node_modules/fixture-package/index.js','module.exports = \\\"maka-owned\\\";\\n');\n", + ); + const packageJson = Buffer.from('{"name":"npm","version":"12.0.2","license":"Artistic-2.0"}\n'); + const license = Buffer.from('Artistic License fixture\n'); + await mkdir(join(npmRoot, 'bin'), { recursive: true }); + await Promise.all([ + writeFile(join(npmRoot, 'bin', 'npm-cli.js'), cli), + writeFile(join(npmRoot, 'package.json'), packageJson), + writeFile(join(npmRoot, 'LICENSE'), license), + ]); + await writeFile( + join(resourcesRoot, 'bundled-npm.json'), + `${JSON.stringify({ + schemaVersion: 1, + protocol: 'maka_bundled_npm_runtime_v1', + provider: 'desktop/npm-cli', + npmVersion: '12.0.2', + platform: process.platform, + arch: process.arch, + runtimeRootRelativePath: 'npm', + cliRelativePath: 'npm/bin/npm-cli.js', + securityPatches: approvedSecurityPatches, + files: [ + manifestFile('LICENSE', license), + manifestFile('bin/npm-cli.js', cli), + manifestFile('package.json', packageJson), + ], + distributionReady: true, + })}\n`, + ); + return resourcesRoot; +} + +function manifestFile(path: string, bytes: Buffer) { + return { path, bytes: bytes.byteLength, sha256: sha256(bytes) }; +} + +async function findGitExecutable(): Promise { + const { stdout } = await execFileAsync(process.platform === 'win32' ? 'where.exe' : 'which', [ + 'git', + ]); + const first = stdout + .split(/\r?\n/u) + .find((line) => line.trim()) + ?.trim(); + if (!first) throw new Error('Git executable is unavailable'); + return await realpath(first); +} + +async function sha256File(path: string): Promise<`sha256:${string}`> { + const hash = createHash('sha256'); + for await (const chunk of createReadStream(path)) hash.update(chunk); + return `sha256:${hash.digest('hex')}`; +} + +function sha256(value: Uint8Array): `sha256:${string}` { + return `sha256:${createHash('sha256').update(value).digest('hex')}`; +} + +async function git(cwd: string, ...args: string[]): Promise { + await execFileAsync('git', args, { cwd, maxBuffer: 8 * 1024 * 1024 }); +} + +const approvedSecurityPatches = [ + { + packageName: 'tar', + fromVersion: '7.5.19', + toVersion: '7.5.22', + advisories: ['GHSA-r292-9mhp-454m'], + }, + { + packageName: 'brace-expansion', + fromVersion: '5.0.7', + toVersion: '5.0.9', + advisories: ['GHSA-mh99-v99m-4gvg', 'GHSA-rgw5-rvv9-x895'], + }, + { + packageName: 'ip-address', + fromVersion: '10.2.0', + toVersion: '10.4.0', + advisories: ['GHSA-mwp4-54f8-5fhr', 'GHSA-4xrf-jv44-h6hh', 'GHSA-22jq-vg5j-6vgg'], + }, + { + packageName: 'undici', + fromVersion: '6.27.0', + toVersion: '6.28.0', + advisories: ['GHSA-8xcm-r25x-g524', 'GHSA-m8rv-5g2x-5cg5', 'GHSA-v3r7-h72x-cjcm'], + }, +] as const; diff --git a/packages/runtime-host/src/__tests__/managed-workspace-inspection-tool.test.ts b/packages/runtime-host/src/__tests__/managed-workspace-inspection-tool.test.ts new file mode 100644 index 00000000000..e94b3f83816 --- /dev/null +++ b/packages/runtime-host/src/__tests__/managed-workspace-inspection-tool.test.ts @@ -0,0 +1,172 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import type { MakaToolContext } from '@maka/runtime'; +import { + createManagedWorkspaceInspectionTool, + type ManagedWorkspaceInspectionToolResult, +} from '../server/managed-workspace-inspection-tool.js'; +import type { + RuntimeHostWorkspaceExecutionComposition, + RuntimeHostWorkspaceExecutionProfile, +} from '../server/workspace-execution-composition.js'; + +test('opens an owner-bound dependency profile from the session cwd before inspecting', async () => { + const opens: unknown[] = []; + const executions: unknown[] = []; + const profile = Object.freeze({ + kind: 'managed_worktree_v1', + executionHandle: Object.freeze({ kind: 'managed_workspace_execution_handle_v1' as const }), + provisioning: 'dependency_environment_v1' as const, + }); + const composition: RuntimeHostWorkspaceExecutionComposition = { + state: 'ready', + async openManagedWorkspace(input, options) { + opens.push({ input, options }); + return profile; + }, + async executeReadOnly(actualProfile, operation, abortSignal) { + executions.push({ profile: actualProfile, operation, abortSignal }); + return { kind: 'read', content: 'isolated' }; + }, + beginDrain() {}, + async close() {}, + }; + const tool = createManagedWorkspaceInspectionTool(composition, { + canonicalizeSourceRoot: async () => '/canonical/source', + }); + const abort = new AbortController(); + const context = toolContext(abort.signal); + + const result = (await tool.impl( + { kind: 'read', path: 'node_modules/fixture/index.js' }, + context, + )) as ManagedWorkspaceInspectionToolResult; + + assert.deepEqual(result, { + kind: 'managed_workspace_inspection_v1', + result: { kind: 'read', content: 'isolated' }, + }); + assert.equal(opens.length, 1); + const opened = opens[0] as { + input: { + repositoryId: string; + workspaceId: string; + workspaceEpochId: string; + workspaceInstanceId: string; + sourceRoot: string; + }; + options: unknown; + }; + assert.match(opened.input.repositoryId, /^repository_[a-f0-9]{32}$/u); + assert.match(opened.input.workspaceId, /^workspace_[a-f0-9]{32}$/u); + assert.match(opened.input.workspaceEpochId, /^epoch_[a-f0-9]{32}$/u); + assert.match(opened.input.workspaceInstanceId, /^instance_[a-f0-9]{32}$/u); + assert.equal(opened.input.sourceRoot, '/canonical/source'); + assert.deepEqual(opened.options, { + provisioning: 'dependency_environment_v1', + abortSignal: abort.signal, + }); + assert.deepEqual(executions, [ + { + profile, + operation: { kind: 'read', path: 'node_modules/fixture/index.js' }, + abortSignal: abort.signal, + }, + ]); +}); + +test('keeps one stable managed workspace identity for the same session and source', async () => { + const identities: unknown[] = []; + const profile = Object.freeze({ + kind: 'managed_worktree_v1', + executionHandle: Object.freeze({ kind: 'managed_workspace_execution_handle_v1' as const }), + provisioning: 'dependency_environment_v1' as const, + }) satisfies RuntimeHostWorkspaceExecutionProfile; + const composition: RuntimeHostWorkspaceExecutionComposition = { + state: 'ready', + async openManagedWorkspace(input) { + identities.push(input); + return profile; + }, + async executeReadOnly() { + return { kind: 'glob', files: ['src/index.ts'] }; + }, + beginDrain() {}, + async close() {}, + }; + const tool = createManagedWorkspaceInspectionTool(composition, { + canonicalizeSourceRoot: async () => '/canonical/source', + }); + const context = toolContext(new AbortController().signal); + + await tool.impl({ kind: 'glob', path: '.', pattern: '**/*.ts', limit: 10 }, context); + await tool.impl({ kind: 'grep', path: '.', pattern: 'needle', limit: 10 }, context); + + assert.deepEqual(identities[0], identities[1]); +}); + +test('schema rejects mutation and caller-owned workspace authority fields', () => { + const tool = createManagedWorkspaceInspectionTool({} as never); + const schema = tool.parameters as { + safeParse(value: unknown): { success: boolean }; + }; + + assert.equal( + schema.safeParse({ kind: 'write', path: 'src/a.ts', content: 'unsafe' }).success, + false, + ); + assert.equal( + schema.safeParse({ + kind: 'read', + path: 'README.md', + sourceRoot: '/forged', + workspaceId: 'workspace_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + }).success, + false, + ); +}); + +test('pre-aborted task never opens a managed workspace', async () => { + let canonicalizeCalls = 0; + let openCalls = 0; + const tool = createManagedWorkspaceInspectionTool( + { + state: 'ready', + async openManagedWorkspace() { + openCalls += 1; + throw new Error('must not open'); + }, + async executeReadOnly() { + throw new Error('must not execute'); + }, + beginDrain() {}, + async close() {}, + }, + { + async canonicalizeSourceRoot() { + canonicalizeCalls += 1; + return '/canonical/source'; + }, + }, + ); + const abort = new AbortController(); + abort.abort(new DOMException('Task cancelled', 'AbortError')); + + await assert.rejects( + async () => await tool.impl({ kind: 'read', path: 'README.md' }, toolContext(abort.signal)), + { name: 'AbortError' }, + ); + assert.equal(canonicalizeCalls, 0); + assert.equal(openCalls, 0); +}); + +function toolContext(abortSignal: AbortSignal): MakaToolContext { + return { + sessionId: 'session_11111111111111111111111111111111', + turnId: 'turn_22222222222222222222222222222222', + toolCallId: 'call_33333333333333333333333333333333', + cwd: '/attached/source', + abortSignal, + emitOutput() {}, + }; +} diff --git a/packages/runtime-host/src/__tests__/workspace-execution-composition.test.ts b/packages/runtime-host/src/__tests__/workspace-execution-composition.test.ts index 28e0dfb2ab5..7fbc3ae495b 100644 --- a/packages/runtime-host/src/__tests__/workspace-execution-composition.test.ts +++ b/packages/runtime-host/src/__tests__/workspace-execution-composition.test.ts @@ -21,6 +21,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import type { ManagedWorkspaceExecutionHandle, + ManagedWorkspaceExecutionOptions, ManagedWorkspaceExecutionScope, ManagedWorkspaceOwner, } from '@maka/storage/managed-workspace-owner'; @@ -86,6 +87,64 @@ test('never falls back a managed profile to attached execution', async () => { await composition.close(); }); +test('preserves explicit dependency provisioning through managed admission', async () => { + const calls: string[] = []; + const admissionOptions: unknown[] = []; + const handle = Object.freeze({ kind: 'managed_workspace_execution_handle_v1' as const }); + const scope = Object.freeze({ kind: 'managed_workspace_execution_scope_v1' as const }); + const managedOwner = fakeManagedOwner({ handle, scope, calls, admissionOptions }); + const composition = createRuntimeHostWorkspaceExecutionComposition({ managedOwner }); + const profile = createManagedWorkspaceExecutionProfile(handle, { + provisioning: 'dependency_environment_v1', + }); + const abort = new AbortController(); + + await composition.executeReadOnly( + profile, + { + kind: 'read', + path: 'node_modules/fixture/index.js', + }, + abort.signal, + ); + + assert.deepEqual(admissionOptions, [ + { provisioning: 'dependency_environment_v1', abortSignal: abort.signal }, + ]); + await composition.close(); +}); + +test('opens a managed profile through the authenticated production stores', async () => { + const calls: string[] = []; + const handle = Object.freeze({ kind: 'managed_workspace_execution_handle_v1' as const }); + const scope = Object.freeze({ kind: 'managed_workspace_execution_scope_v1' as const }); + const managedOwner = fakeManagedOwner({ handle, scope, calls }); + const executionStores = Object.freeze({}) as never; + const composition = createRuntimeHostWorkspaceExecutionComposition({ + managedOwner, + executionStores, + }); + + const profile = await composition.openManagedWorkspace( + { + repositoryId: 'repository_11111111111111111111111111111111', + workspaceId: 'workspace_22222222222222222222222222222222', + workspaceEpochId: 'epoch_33333333333333333333333333333333', + workspaceInstanceId: 'instance_44444444444444444444444444444444', + sourceRoot: '/source', + }, + { provisioning: 'dependency_environment_v1' }, + ); + + assert.deepEqual(profile, { + kind: 'managed_worktree_v1', + executionHandle: handle, + provisioning: 'dependency_environment_v1', + }); + assert.deepEqual(calls, ['managed:open']); + await composition.close(); +}); + test('rejects forged or malformed profiles before worker dispatch', async () => { let workerCalls = 0; const composition = createRuntimeHostWorkspaceExecutionComposition({ @@ -145,11 +204,129 @@ test('drains tool operations before closing the managed owner', async () => { assert.deepEqual(calls, ['managed:admit', 'managed:read', 'managed:close']); }); +test('caller cancellation releases a provisioning operation so drain can close', async () => { + const handle = Object.freeze({ kind: 'managed_workspace_execution_handle_v1' as const }); + let acknowledgeProvision!: () => void; + const provisionStarted = new Promise((resolve) => { + acknowledgeProvision = resolve; + }); + let closed = false; + const managedOwner: ManagedWorkspaceOwner = { + state: 'ready', + async openManagedWorkspaceBaseline() { + throw new Error('not used'); + }, + async openManagedWorkspaceBaselineFromExecutionStores() { + throw new Error('not used'); + }, + async withManagedWorkspaceExecution( + _handle: ManagedWorkspaceExecutionHandle, + _operation: (scope: ManagedWorkspaceExecutionScope) => Promise, + options?: ManagedWorkspaceExecutionOptions, + ): Promise { + acknowledgeProvision(); + return await new Promise((_resolve, reject) => { + const abort = () => + reject( + options?.abortSignal?.reason ?? + new DOMException('Managed execution cancelled', 'AbortError'), + ); + if (options?.abortSignal?.aborted) abort(); + else options?.abortSignal?.addEventListener('abort', abort, { once: true }); + }); + }, + async executeReadOnlyFilesystemOperation() { + throw new Error('worker must not run while provisioning is pending'); + }, + async close() { + closed = true; + }, + }; + const composition = createRuntimeHostWorkspaceExecutionComposition({ managedOwner }); + const profile = createManagedWorkspaceExecutionProfile(handle, { + provisioning: 'dependency_environment_v1', + }); + const abort = new AbortController(); + const execution = composition.executeReadOnly( + profile, + { kind: 'read', path: 'node_modules/fixture/index.js' }, + abort.signal, + ); + await provisionStarted; + + composition.beginDrain(); + const closing = composition.close(); + abort.abort(new DOMException('User cancelled managed execution', 'AbortError')); + + await assert.rejects(execution, { name: 'AbortError' }); + await closing; + assert.equal(closed, true); +}); + +test('caller cancellation releases baseline admission so drain can close', async () => { + let acknowledgeAdmission!: () => void; + const admissionStarted = new Promise((resolve) => { + acknowledgeAdmission = resolve; + }); + let closed = false; + const managedOwner: ManagedWorkspaceOwner = { + state: 'ready', + async openManagedWorkspaceBaseline() { + throw new Error('not used'); + }, + async openManagedWorkspaceBaselineFromExecutionStores(_stores, _input, options) { + acknowledgeAdmission(); + return await new Promise((_resolve, reject) => { + const abort = () => + reject( + options?.abortSignal?.reason ?? + new DOMException('Managed baseline admission cancelled', 'AbortError'), + ); + if (options?.abortSignal?.aborted) abort(); + else options?.abortSignal?.addEventListener('abort', abort, { once: true }); + }); + }, + async withManagedWorkspaceExecution() { + throw new Error('not used'); + }, + async executeReadOnlyFilesystemOperation() { + throw new Error('not used'); + }, + async close() { + closed = true; + }, + }; + const composition = createRuntimeHostWorkspaceExecutionComposition({ + managedOwner, + executionStores: Object.freeze({}) as never, + }); + const abort = new AbortController(); + const admission = composition.openManagedWorkspace( + { + repositoryId: 'repository_11111111111111111111111111111111', + workspaceId: 'workspace_22222222222222222222222222222222', + workspaceEpochId: 'epoch_33333333333333333333333333333333', + workspaceInstanceId: 'instance_44444444444444444444444444444444', + sourceRoot: '/source', + }, + { provisioning: 'dependency_environment_v1', abortSignal: abort.signal }, + ); + await admissionStarted; + + const closing = composition.close(); + abort.abort(new DOMException('User cancelled baseline admission', 'AbortError')); + + await assert.rejects(admission, { name: 'AbortError' }); + await closing; + assert.equal(closed, true); +}); + function fakeManagedOwner(input: { handle: ManagedWorkspaceExecutionHandle; scope: ManagedWorkspaceExecutionScope; calls: string[]; workerBlocked?: Promise; + admissionOptions?: unknown[]; }): ManagedWorkspaceOwner { return { state: 'ready', @@ -157,10 +334,16 @@ function fakeManagedOwner(input: { throw new Error('not used'); }, async openManagedWorkspaceBaselineFromExecutionStores() { - throw new Error('not used'); + input.calls.push('managed:open'); + return { + created: true, + head: Object.freeze({}) as never, + executionHandle: input.handle, + }; }, - async withManagedWorkspaceExecution(handle, operation) { + async withManagedWorkspaceExecution(handle, operation, options) { assert.equal(handle, input.handle); + input.admissionOptions?.push(options); input.calls.push('managed:admit'); return await operation(input.scope); }, diff --git a/packages/runtime-host/src/server/execution-bundled-resources.ts b/packages/runtime-host/src/server/execution-bundled-resources.ts new file mode 100644 index 00000000000..9f981048370 --- /dev/null +++ b/packages/runtime-host/src/server/execution-bundled-resources.ts @@ -0,0 +1,19 @@ +export interface ExecutionBundledResourceProcessIdentity { + readonly electronVersion?: string; + readonly defaultApp?: boolean; + readonly resourcesPath?: string; +} + +/** + * Only a packaged Electron executable owns the release resource directory. + * Node/CLI and development Electron candidates must not reinterpret ambient + * directories as signed bundled-runtime authority. + */ +export function resolveExecutionBundledResourcesRoot( + identity: ExecutionBundledResourceProcessIdentity, +): string | undefined { + if (!identity.electronVersion || identity.defaultApp === true) return undefined; + return typeof identity.resourcesPath === 'string' && identity.resourcesPath.length > 0 + ? identity.resourcesPath + : undefined; +} diff --git a/packages/runtime-host/src/server/managed-dependency-producer-process.ts b/packages/runtime-host/src/server/managed-dependency-producer-process.ts index 4dc76c9b57a..88575cc0ada 100644 --- a/packages/runtime-host/src/server/managed-dependency-producer-process.ts +++ b/packages/runtime-host/src/server/managed-dependency-producer-process.ts @@ -84,11 +84,10 @@ export async function runManagedNpmDependencyProvision( if (!isPathWithin(npmCliPath, npmRuntimeRoot)) { throw new Error('Managed npm CLI escapes its verified runtime root'); } - const [homeRoot, npmCache, temporaryRoot, compileCacheRoot] = await Promise.all([ + const [homeRoot, npmCache, temporaryRoot] = await Promise.all([ createOwnedScratchDirectory(scratchRoot, 'home'), createOwnedScratchDirectory(scratchRoot, 'cache'), createOwnedScratchDirectory(scratchRoot, 'temp'), - createOwnedScratchDirectory(scratchRoot, 'node-compile-cache'), ]); const userConfig = join(homeRoot, 'npmrc'); const globalConfig = join(homeRoot, 'global-npmrc'); @@ -122,13 +121,7 @@ export async function runManagedNpmDependencyProvision( `--globalconfig=${globalConfig}`, ], cwd: projectRoot, - env: hermeticNpmEnvironment( - homeRoot, - userConfig, - globalConfig, - temporaryRoot, - compileCacheRoot, - ), + env: hermeticNpmEnvironment(homeRoot, userConfig, globalConfig, temporaryRoot), monitorRoot: projectRoot, ...(input.producerInput.abortSignal ? { abortSignal: input.producerInput.abortSignal } : {}), timeoutMs: DEFAULT_PRODUCER_TIMEOUT_MS, @@ -264,7 +257,6 @@ function hermeticNpmEnvironment( userConfig: string, globalConfig: string, temporaryRoot: string, - compileCacheRoot: string, ): NodeJS.ProcessEnv { // libuv's Windows home lookup rejects USERPROFILE values at MAX_PATH even // though Node's filesystem APIs can access the owned long path. The cwd is @@ -284,7 +276,11 @@ function hermeticNpmEnvironment( TEMP: temporaryRoot, TMP: temporaryRoot, TMPDIR: temporaryRoot, - NODE_COMPILE_CACHE: compileCacheRoot, + // This producer is short lived, so a compile cache has no meaningful + // performance value. More importantly, Node 26 on Windows can hang while + // initializing NODE_COMPILE_CACHE under the Permission Model. Keep the + // execution profile explicit and deterministic across supported hosts. + NODE_DISABLE_COMPILE_CACHE: '1', ...(process.platform === 'win32' ? { APPDATA: join(effectiveHomeRoot, 'AppData', 'Roaming'), diff --git a/packages/runtime-host/src/server/managed-npm-dependency-producer.ts b/packages/runtime-host/src/server/managed-npm-dependency-producer.ts new file mode 100644 index 00000000000..6008297e1ec --- /dev/null +++ b/packages/runtime-host/src/server/managed-npm-dependency-producer.ts @@ -0,0 +1,33 @@ +import { + createManagedDependencyEnvironmentProducerCapability, + type ManagedDependencyEnvironmentProducer, + type ManagedDependencyEnvironmentProducerInput, +} from '@maka/storage/managed-dependency-environment'; +import { + runManagedNpmDependencyProvision, + type ManagedNpmRuntimeCapability, +} from './managed-dependency-producer-process.js'; + +/** + * Internal composition seam from an attested bundled npm runtime to the + * storage-owned dependency artifact publisher. The returned producer cannot + * weaken the runtime capability: every invocation re-enters the PR3 + * attestation owner before starting npm. + */ +export function createManagedNpmDependencyEnvironmentProducer( + runtime: ManagedNpmRuntimeCapability, +): ManagedDependencyEnvironmentProducer { + return Object.freeze({ + capability: createManagedDependencyEnvironmentProducerCapability(runtime.runtimeIdentitySha256), + packageManagerName: 'npm' as const, + packageManagerVersion: runtime.npmVersion, + nodeRuntime: Object.freeze({ + version: runtime.nodeVersion, + abi: runtime.nodeAbi, + platform: runtime.platform, + arch: runtime.arch, + }), + provision: (producerInput: ManagedDependencyEnvironmentProducerInput) => + runManagedNpmDependencyProvision({ runtime, producerInput }), + }); +} diff --git a/packages/runtime-host/src/server/managed-workspace-inspection-tool.ts b/packages/runtime-host/src/server/managed-workspace-inspection-tool.ts new file mode 100644 index 00000000000..e8217a1356d --- /dev/null +++ b/packages/runtime-host/src/server/managed-workspace-inspection-tool.ts @@ -0,0 +1,150 @@ +import { createHash } from 'node:crypto'; +import { realpath } from 'node:fs/promises'; +import { z } from 'zod'; +import type { MakaTool } from '@maka/runtime'; +import type { + ManagedWorkspaceReadOnlyOperation, + ManagedWorkspaceReadOnlyResult, +} from '@maka/storage/managed-workspace-owner'; +import type { RuntimeHostWorkspaceExecutionComposition } from './workspace-execution-composition.js'; + +const INSPECTION_PATH_MAX_CHARS = 4_096; +const INSPECTION_PATTERN_MAX_CHARS = 4_096; +const INSPECTION_RESULT_MAX_BYTES = 64 * 1024; +const INSPECTION_LIMIT_MAX = 256; +const GREP_TIMEOUT_MAX_MS = 120_000; + +const boundedPath = z.string().min(1).max(INSPECTION_PATH_MAX_CHARS); +const boundedPattern = z.string().min(1).max(INSPECTION_PATTERN_MAX_CHARS); +const boundedLimit = z.number().int().positive().max(INSPECTION_LIMIT_MAX).optional(); + +const managedWorkspaceInspectionInputSchema = z.discriminatedUnion('kind', [ + z + .object({ + kind: z.literal('read'), + path: boundedPath, + offset: z.number().int().nonnegative().optional(), + limit: boundedLimit, + }) + .strict(), + z + .object({ + kind: z.literal('glob'), + path: boundedPath, + pattern: boundedPattern, + limit: boundedLimit, + }) + .strict(), + z + .object({ + kind: z.literal('grep'), + path: boundedPath, + pattern: boundedPattern, + glob: z.string().max(INSPECTION_PATTERN_MAX_CHARS).optional(), + maxCountPerFile: z.number().int().positive().max(INSPECTION_LIMIT_MAX).optional(), + limit: boundedLimit, + timeoutMs: z.number().int().positive().max(GREP_TIMEOUT_MAX_MS).optional(), + }) + .strict(), +]); + +export type ManagedWorkspaceInspectionToolInput = z.infer< + typeof managedWorkspaceInspectionInputSchema +>; + +export interface ManagedWorkspaceInspectionToolResult { + readonly kind: 'managed_workspace_inspection_v1'; + readonly result: ManagedWorkspaceReadOnlyResult; +} + +export interface ManagedWorkspaceInspectionToolDependencies { + readonly canonicalizeSourceRoot?: (cwd: string) => Promise; +} + +/** + * First production consumer of the M1 managed-workspace read profile. + * + * The model supplies only a bounded read operation. Session cwd, managed + * identity, dependency provisioning, and the owner-bound execution profile + * remain Host-owned and cannot be replaced with an attached checkout path. + */ +export function createManagedWorkspaceInspectionTool( + workspaceExecution: RuntimeHostWorkspaceExecutionComposition, + dependencies: ManagedWorkspaceInspectionToolDependencies = {}, +): MakaTool { + const canonicalizeSourceRoot = dependencies.canonicalizeSourceRoot ?? realpath; + return { + name: 'ManagedWorkspaceInspect', + displayName: 'Inspect isolated workspace', + description: + 'Read or search the project in a Maka-owned isolated Git workspace with its verified dependency environment. ' + + 'Use this for dependency-aware inspection that must not read from or modify the current checkout. ' + + 'This tool is read-only and supports only Read, Glob, and Grep operations.', + parameters: managedWorkspaceInspectionInputSchema, + categoryHint: 'read', + recoveryMode: 'replay_safe', + executionSemantics: 'exclusive_step', + impl: async (input, context) => { + context.abortSignal.throwIfAborted(); + const sourceRoot = await canonicalizeSourceRoot(context.cwd); + context.abortSignal.throwIfAborted(); + const identity = managedInspectionIdentity(sourceRoot, context.sessionId); + const profile = await workspaceExecution.openManagedWorkspace( + { ...identity, sourceRoot }, + { + provisioning: 'dependency_environment_v1', + abortSignal: context.abortSignal, + }, + ); + context.abortSignal.throwIfAborted(); + const result = await workspaceExecution.executeReadOnly( + profile, + toReadOnlyOperation(input), + context.abortSignal, + ); + assertBoundedInspectionResult(result); + return Object.freeze({ kind: 'managed_workspace_inspection_v1', result }); + }, + }; +} + +function toReadOnlyOperation( + input: ManagedWorkspaceInspectionToolInput, +): ManagedWorkspaceReadOnlyOperation { + if (input.kind !== 'grep') return Object.freeze({ ...input }); + return Object.freeze({ + ...input, + maxCountPerFile: input.maxCountPerFile ?? 32, + limit: input.limit ?? 64, + timeoutMs: input.timeoutMs ?? 120_000, + }); +} + +function managedInspectionIdentity(sourceRoot: string, sessionId: string) { + const sourceIdentity = domainDigest('source', sourceRoot); + const sessionIdentity = domainDigest('session', sourceRoot, sessionId); + return Object.freeze({ + repositoryId: `repository_${sourceIdentity}`, + workspaceId: `workspace_${sessionIdentity}`, + workspaceEpochId: `epoch_${domainDigest('epoch', sourceRoot, sessionId)}`, + workspaceInstanceId: `instance_${domainDigest('instance', sourceRoot, sessionId)}`, + }); +} + +function domainDigest(domain: string, ...values: readonly string[]): string { + const hash = createHash('sha256'); + hash.update(`maka-managed-inspection-${domain}-v1\0`, 'utf8'); + for (const value of values) { + hash.update(value, 'utf8'); + hash.update('\0', 'utf8'); + } + return hash.digest('hex').slice(0, 32); +} + +function assertBoundedInspectionResult(result: ManagedWorkspaceReadOnlyResult): void { + if (Buffer.byteLength(JSON.stringify(result), 'utf8') > INSPECTION_RESULT_MAX_BYTES) { + throw new Error( + 'Managed workspace inspection result exceeds the bounded tool response; narrow the read or search.', + ); + } +} diff --git a/packages/runtime-host/src/server/workspace-execution-composition.ts b/packages/runtime-host/src/server/workspace-execution-composition.ts index 570532efbb9..7132665443d 100644 --- a/packages/runtime-host/src/server/workspace-execution-composition.ts +++ b/packages/runtime-host/src/server/workspace-execution-composition.ts @@ -20,12 +20,15 @@ import { createReadOnlyPermissionProfile } from '@maka/core/permission-profile'; import { createManagedExecutionBoundary } from '@maka/core/sandbox-boundary'; import type { + OpenManagedWorkspaceBaselineInput, ManagedWorkspaceExecutionHandle, + ManagedWorkspaceExecutionOptions, ManagedWorkspaceFilesystemWorker, ManagedWorkspaceOwner, ManagedWorkspaceReadOnlyOperation, ManagedWorkspaceReadOnlyResult, } from '@maka/storage/managed-workspace-owner'; +import type { InteractiveExecutionStoresWriter } from '@maka/storage/execution-stores'; export type RuntimeHostWorkspaceExecutionProfile = | { @@ -35,6 +38,7 @@ export type RuntimeHostWorkspaceExecutionProfile = | { readonly kind: 'managed_worktree_v1'; readonly executionHandle: ManagedWorkspaceExecutionHandle; + readonly provisioning: NonNullable; }; export type RuntimeHostWorkspaceExecutionErrorCode = @@ -60,6 +64,10 @@ export interface RuntimeHostWorkspaceExecutionComposition { operation: ManagedWorkspaceReadOnlyOperation, abortSignal?: AbortSignal, ): Promise; + openManagedWorkspace( + input: OpenManagedWorkspaceBaselineInput, + options?: ManagedWorkspaceExecutionOptions, + ): Promise; beginDrain(): void; close(): Promise; } @@ -67,6 +75,7 @@ export interface RuntimeHostWorkspaceExecutionComposition { export interface CreateRuntimeHostWorkspaceExecutionCompositionInput { readonly filesystemWorker?: ManagedWorkspaceFilesystemWorker; readonly managedOwner?: ManagedWorkspaceOwner; + readonly executionStores?: InteractiveExecutionStoresWriter; } export function createAttachedWorkspaceExecutionProfile( @@ -83,8 +92,13 @@ export function createAttachedWorkspaceExecutionProfile( export function createManagedWorkspaceExecutionProfile( executionHandle: ManagedWorkspaceExecutionHandle, + options: ManagedWorkspaceExecutionOptions = {}, ): RuntimeHostWorkspaceExecutionProfile { - return Object.freeze({ kind: 'managed_worktree_v1', executionHandle }); + return Object.freeze({ + kind: 'managed_worktree_v1', + executionHandle, + provisioning: normalizeManagedProvisioning(options.provisioning), + }); } export function createRuntimeHostWorkspaceExecutionComposition( @@ -113,6 +127,32 @@ export function createRuntimeHostWorkspaceExecutionComposition( get state() { return state; }, + async openManagedWorkspace(baselineInput, options = {}) { + if (state !== 'ready') { + throw new RuntimeHostWorkspaceExecutionError( + 'workspace_execution_draining', + 'Runtime Host workspace execution is draining', + ); + } + if (!input.managedOwner || !input.executionStores) { + throw new RuntimeHostWorkspaceExecutionError( + 'managed_workspace_profile_unavailable', + 'Managed workspace execution is not composed for this Runtime Host', + ); + } + const provisioning = normalizeManagedProvisioning(options.provisioning); + activeOperations += 1; + try { + const opened = await input.managedOwner.openManagedWorkspaceBaselineFromExecutionStores( + input.executionStores, + baselineInput, + options.abortSignal ? { abortSignal: options.abortSignal } : undefined, + ); + return createManagedWorkspaceExecutionProfile(opened.executionHandle, { provisioning }); + } finally { + finishOperation(); + } + }, async executeReadOnly(profile, operation, abortSignal) { if (state !== 'ready') { throw new RuntimeHostWorkspaceExecutionError( @@ -145,6 +185,10 @@ export function createRuntimeHostWorkspaceExecutionComposition( profile.executionHandle, (scope) => input.managedOwner!.executeReadOnlyFilesystemOperation(scope, operation, abortSignal), + { + provisioning: profile.provisioning, + ...(abortSignal ? { abortSignal } : {}), + }, ); } if (!input.filesystemWorker) { @@ -176,6 +220,19 @@ export function createRuntimeHostWorkspaceExecutionComposition( }; } +function normalizeManagedProvisioning( + provisioning: ManagedWorkspaceExecutionOptions['provisioning'], +): NonNullable { + const value = provisioning ?? 'canonical_tree_only_v1'; + if (value !== 'canonical_tree_only_v1' && value !== 'dependency_environment_v1') { + throw new RuntimeHostWorkspaceExecutionError( + 'workspace_operation_denied', + 'Managed workspace provisioning profile is invalid', + ); + } + return value; +} + function isReadOnlyOperation(input: unknown): input is ManagedWorkspaceReadOnlyOperation { if (!input || typeof input !== 'object') return false; const kind = (input as { kind?: unknown }).kind; @@ -190,12 +247,15 @@ function isWorkspaceExecutionProfile( kind?: unknown; cwd?: unknown; executionHandle?: { kind?: unknown }; + provisioning?: unknown; }; if (candidate.kind === 'attached_checkout_v1') { return typeof candidate.cwd === 'string' && candidate.cwd.length > 0; } return ( candidate.kind === 'managed_worktree_v1' && - candidate.executionHandle?.kind === 'managed_workspace_execution_handle_v1' + candidate.executionHandle?.kind === 'managed_workspace_execution_handle_v1' && + (candidate.provisioning === 'canonical_tree_only_v1' || + candidate.provisioning === 'dependency_environment_v1') ); }