Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/desktop/bundled-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,21 @@
"hardenedRuntime": false,
"notarization": "missing",
"distributionReady": false
},
"windowsCu": {
"repo": "maka-agent/maka-cu",
"source": "apps/OpenComputerUseWindows/native",
"expectedProtocolVersion": "maka.cu/2",
"binaryName": "maka-cu-windows.exe",
"publishContract": {
"executor": "rust-native-windows",
"protocol": "maka.cu/2",
"runtimeIdentifier": "win-x64",
"rustTarget": "x86_64-pc-windows-msvc",
"cargoProfile": "release",
"lto": true,
"staticNativeDependencies": true
},
"distributionReady": false
}
}
20 changes: 19 additions & 1 deletion apps/desktop/electron-builder.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import {
Expand All @@ -31,6 +31,23 @@ function readManifest(relativePath) {
return JSON.parse(readFileSync(new URL(relativePath, import.meta.url), 'utf8'));
}

export function windowsCuExtraResources({
platform = process.platform,
manifest = readManifest('./bundled-tools.json'),
helperExists = existsSync('resources/bin/maka-cu-windows/maka-cu-windows.exe'),
} = {}) {
if (platform !== 'win32' || manifest.windowsCu?.distributionReady !== true) return [];
if (!helperExists) {
throw new Error(
'windowsCu is distribution-ready but resources/bin/maka-cu-windows/maka-cu-windows.exe is missing',
);
}
return [{
from: 'resources/bin/maka-cu-windows',
to: 'bin/maka-cu-windows',
}];
}

// Some license files below ship inside third-party packages that apps/desktop
// depends on (electron, @fontsource-variable/geist*). Locate each package by
// resolving its manifest rather than assuming its node_modules location:
Expand Down Expand Up @@ -138,6 +155,7 @@ const baseDesktopBuilderConfig = {
},
...(process.platform === 'win32'
? [
...windowsCuExtraResources(),
{
from: 'resources/windows-sandbox/maka-windows-sandbox.exe',
to: 'windows-sandbox/maka-windows-sandbox.exe',
Expand Down
44 changes: 43 additions & 1 deletion apps/desktop/src/main/__tests__/computer-use-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { chmod, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it } from 'node:test';
Expand Down Expand Up @@ -141,4 +141,46 @@ describe('Computer Use host health', () => {
}
});

it('selects the shared maka.cu/2 backend for a pinned Windows helper', async () => {
const directory = await mkdtemp(join(tmpdir(), 'maka-cu-host-windows-'));
try {
const binaryPath = join(directory, 'maka-cu-windows.exe');
const manifestPath = join(directory, 'bundled-tools.json');
const bytes = Buffer.from('windows-native-release-artifact');
await writeFile(binaryPath, bytes);
await chmod(binaryPath, 0o755);
const hash = createHash('sha256').update(bytes).digest('hex');
await writeFile(manifestPath, JSON.stringify({
windowsCu: {
binarySha256: hash,
files: [{ name: 'maka-cu-windows.exe', sizeBytes: bytes.length, sha256: hash }],
distributionReady: false,
},
}));

const selected = createComputerUseHost({
isPackaged: false,
resourcesPath: directory,
manifestPath,
binaryPath,
platform: 'win32',
physicalInputRecentlyActive: () => false,
});
assert.equal(selected.selected.backendId, 'maka-cu');

await mkdir(join(directory, 'unexpected-directory'));
const withUnexpectedDirectory = createComputerUseHost({
isPackaged: false,
resourcesPath: directory,
manifestPath,
binaryPath,
platform: 'win32',
physicalInputRecentlyActive: () => false,
});
assert.equal(withUnexpectedDirectory.selected.backendId, 'none');
} finally {
await rm(directory, { recursive: true, force: true });
}
});

});
111 changes: 89 additions & 22 deletions apps/desktop/src/main/computer-use-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
fstatSync,
openSync,
readFileSync,
readdirSync,
} from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
Expand All @@ -42,6 +43,15 @@ export interface ComputerUseHostState {
expectedBinarySha256?: string;
}

type BundledToolManifest = {
makaCu?: { binarySha256?: string; distributionReady?: boolean };
windowsCu?: {
binarySha256?: string;
distributionReady?: boolean;
files?: Array<{ name?: string; sizeBytes?: number; sha256?: string }>;
};
};

function readRegularFile(path: string): Buffer {
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
Expand All @@ -54,6 +64,47 @@ function readRegularFile(path: string): Buffer {
}
}

function hasPinnedWindowsHelperFiles(
binaryPath: string,
files: NonNullable<BundledToolManifest['windowsCu']>['files'],
): boolean {
if (!Array.isArray(files) || files.length === 0) return false;
const expected = new Map<string, { sizeBytes: number; sha256: string }>();
for (const file of files) {
if (
typeof file?.name !== 'string' ||
file.name.length === 0 ||
file.name !== file.name.split(/[\\/]/).pop() ||
typeof file.sizeBytes !== 'number' ||
!Number.isSafeInteger(file.sizeBytes) ||
file.sizeBytes < 0 ||
typeof file.sha256 !== 'string' ||
!/^[a-f0-9]{64}$/.test(file.sha256) ||
expected.has(file.name)
) return false;
expected.set(file.name, { sizeBytes: file.sizeBytes, sha256: file.sha256 });
}
let actual: string[];
try {
const entries = readdirSync(dirname(binaryPath), { withFileTypes: true });
if (entries.some((entry) => !entry.isFile())) return false;
actual = entries.map((entry) => entry.name);
} catch {
return false;
}
if (actual.length !== expected.size || actual.some((name) => !expected.has(name))) return false;
for (const [name, pin] of expected) {
try {
const bytes = readRegularFile(join(dirname(binaryPath), name));
if (bytes.byteLength !== pin.sizeBytes) return false;
if (createHash('sha256').update(bytes).digest('hex') !== pin.sha256) return false;
} catch {
return false;
}
}
return expected.has(binaryPath.split(/[\\/]/).pop() ?? '');
}

export function createComputerUseHost(input: {
isPackaged: boolean;
resourcesPath: string;
Expand All @@ -68,6 +119,8 @@ export function createComputerUseHost(input: {
screenLocked?: (context: { sessionId: string }) => boolean | Promise<boolean>;
onTrace?: MakaCuBackendOptions['onTrace'];
overlay?: CuOverlayHook;
/** Test seam for Windows manifest selection. */
platform?: NodeJS.Platform;
}): ComputerUseHostState {
const manifestPath = input.manifestPath ?? (input.isPackaged
? join(input.resourcesPath, 'bundled-tools.json')
Expand All @@ -77,36 +130,49 @@ export function createComputerUseHost(input: {
'..',
'bundled-tools.json',
));
const binaryPath = input.binaryPath ?? (input.isPackaged
? join(input.resourcesPath, 'bin', 'maka-cu')
: resolve(
dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'resources',
'bin',
'maka-cu',
));
const platform = input.platform ?? process.platform;
const windows = platform === 'win32';
const binaryPath = input.binaryPath ?? (windows
? (process.env.MAKA_WINDOWS_CU_HELPER_PATH ?? (input.isPackaged
? join(input.resourcesPath, 'bin', 'maka-cu-windows', 'maka-cu-windows.exe')
: resolve(
dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'resources',
'bin',
'maka-cu-windows',
'maka-cu-windows.exe',
)))
: (input.isPackaged
? join(input.resourcesPath, 'bin', 'maka-cu')
: resolve(
dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'resources',
'bin',
'maka-cu',
)));
try {
const manifest = JSON.parse(readRegularFile(manifestPath).toString('utf8')) as {
makaCu?: {
binarySha256?: string;
distributionReady?: boolean;
};
};
const expectedBinarySha256 = manifest.makaCu?.binarySha256;
if (input.isPackaged && manifest.makaCu?.distributionReady !== true) {
return { selected: selectComputerUseBackend() };
const manifest = JSON.parse(readRegularFile(manifestPath).toString('utf8')) as BundledToolManifest;
const entry = windows ? manifest.windowsCu : manifest.makaCu;
const expectedBinarySha256 = entry?.binarySha256;
if (input.isPackaged && entry?.distributionReady !== true) {
return { selected: selectComputerUseBackend({ platform }) };
}
if (!expectedBinarySha256 || !/^[a-f0-9]{64}$/.test(expectedBinarySha256)) {
return { selected: selectComputerUseBackend() };
return { selected: selectComputerUseBackend({ platform }) };
}
if (windows && !hasPinnedWindowsHelperFiles(binaryPath, manifest.windowsCu?.files)) {
return { selected: selectComputerUseBackend({ platform }) };
}
accessSync(binaryPath, constants.R_OK | constants.X_OK);
const actual = createHash('sha256')
.update(readRegularFile(binaryPath))
.digest('hex');
if (actual !== expectedBinarySha256) {
return { selected: selectComputerUseBackend() };
return { selected: selectComputerUseBackend({ platform }) };
}
return {
// No `backendId`: the host takes whatever `DEFAULT_CU_BACKEND_ID` names,
Expand All @@ -120,12 +186,13 @@ export function createComputerUseHost(input: {
...(input.screenLocked ? { screenLocked: input.screenLocked } : {}),
...(input.onTrace ? { onTrace: input.onTrace } : {}),
...(input.overlay ? { overlay: input.overlay } : {}),
platform,
}),
binaryPath,
expectedBinarySha256,
};
} catch {
return { selected: selectComputerUseBackend() };
return { selected: selectComputerUseBackend({ platform }) };
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# Windows `maka.cu/2` integration replacement

## Objective

Rebuild the Windows Computer Use integration on the current `apache/main`
baseline, consuming only a pinned, validated Rust helper artifact and the
existing shared `maka.cu/2` host service.

## Scope

- Add Windows platform selection to the existing protocol backend.
- Select the `windowsCu` manifest entry and verify every packaged helper file.
- Package the helper directory only when `distributionReady` is true; fail the
build when readiness is true but the exact helper is missing.
- Keep local preparation permanently at `distributionReady: false`; a future
release qualification verifier must establish attestation, Authenticode,
clean-machine, and packaged-conversation evidence mechanically.
- Do not copy the old PR's generated browser JSON, raw outputs, experiments,
duplicate service, or compatibility input subsystem.

## Evidence boundary

The companion executor fix is pinned separately in `maka-cu#8`. This worktree
does not claim clean-machine validation, packaged conversation E2E, signing,
or distribution readiness. Those fields must be supplied by a release
qualification pipeline and must match the exact binary digest.

## Progress

- [x] Start from the current `apache/main` after #4497.
- [x] Reuse the existing `MakaCuService` and `maka.cu/2` backend.
- [x] Add Windows manifest, exact digest-set validation, and readiness-gated packaging.
- [x] Make readiness fail closed instead of trusting caller-authored provenance booleans.
- [x] Require the packaged helper's exact file set/size/digests and a valid Authenticode status.
- [x] Use a locked explicit `x86_64-pc-windows-msvc` source build contract.
- [ ] Run a real packaged Windows conversation E2E on the exact artifact.

## Validation

- `npm run build --workspace @maka/computer-use` — pass with shared checkout dependencies.
- `npm run typecheck --workspace @maka/desktop` — baseline failure unrelated to
this change; no diagnostic references the changed host or selector files.
- `node --test scripts/prepare-windows-cu-helper.test.mjs` — 4 passed.
- Focused release/verifier assertions pass; unrelated full script tests still
require generated workspace build output and a working Bash/WSL path.
- Windows packaged/clean-machine validation — not run in this environment.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

## [2026-09-03] | Task: 重建 Windows `maka.cu/2` 集成

### Changes

- 在最新 `apache/main` 上让现有 `MakaCuService`/`maka.cu/2` 后端复用到
Windows;没有增加第二套 service 或 model-facing 协议。
- Desktop 按 `windowsCu` manifest 选择 helper,并校验目录内文件集合、大小和
SHA-256;electron-builder 只在 `distributionReady=true` 时打包它,并在此时
helper 缺失则直接失败。
- 增加 `prepare-windows` artifact 准备命令。该本地命令固定保持
`distributionReady=false`,不再信任调用者写入 provenance JSON 的布尔字段。
未来只能由机械验证 exact digest/attestation、Authenticode、clean-machine 和
packaged conversation 的发布流水线开启。
- source build 使用 `--locked --target x86_64-pc-windows-msvc`;安装包验证器在
ready 时校验完整文件集合/大小/hash,并要求 Authenticode 状态为 `Valid`。
- 未带回旧 PR 的 generated JSON、raw outputs、experiments 或兼容输入代码。

### Verification

- `npm run build --workspace @maka/computer-use`:通过。
- `node --test scripts/prepare-windows-cu-helper.test.mjs`:4 passed。
- Desktop main typecheck 的本次文件无诊断;全量 typecheck 被主线既有的无关
类型错误阻断。
- 未执行真实 Windows clean-machine/packaged conversation E2E,未提交或推送。
Loading
Loading