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
24 changes: 24 additions & 0 deletions apps/desktop/bundled-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,29 @@
"hardenedRuntime": false,
"notarization": "missing",
"distributionReady": false
},
"windowsCu": {
"repo": "sunheyi6/maka-cu",
"source": "maka-cu/apps/OpenComputerUseWindows/native",
"expectedProtocolVersion": "maka.cu/2",
"binaryName": "maka-cu-windows.exe",
"binarySizeBytes": 710656,
"binarySha256": "9d62d9043443b82e8586dbf784792b0ff502fe55020850fff001baeb9ea815e9",
"files": [
{
"name": "maka-cu-windows.exe",
"sizeBytes": 710656,
"sha256": "9d62d9043443b82e8586dbf784792b0ff502fe55020850fff001baeb9ea815e9"
}
],
"publishContract": {
"executor": "rust-native-windows",
"protocol": "maka.cu/2",
"runtimeIdentifier": "win-x64",
"cargoProfile": "release",
"lto": true,
"staticNativeDependencies": true
},
"distributionReady": false
}
}
8 changes: 7 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 Down Expand Up @@ -138,6 +138,12 @@ const baseDesktopBuilderConfig = {
},
...(process.platform === 'win32'
? [
...(existsSync('resources/bin/maka-cu-windows/maka-cu-windows.exe')
? [{
from: 'resources/bin/maka-cu-windows',
to: 'bin/maka-cu-windows',
}]
: []),
{
from: 'resources/windows-sandbox/maka-windows-sandbox.exe',
to: 'windows-sandbox/maka-windows-sandbox.exe',
Expand Down
50 changes: 48 additions & 2 deletions apps/desktop/src/main/__tests__/computer-use-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@

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

it('resolves the Windows native executor entry with the same host hash gate', async () => {
const directory = await mkdtemp(join(tmpdir(), 'maka-windows-host-'));
try {
const binaryPath = join(directory, 'maka-cu-windows.exe');
await copyFile(process.execPath, binaryPath);
const binaryBytes = await readFile(binaryPath);
const sidecarPath = join(directory, 'PresentationNative_cor3.dll');
const sidecarBytes = Buffer.from('native-sidecar');
await writeFile(sidecarPath, sidecarBytes);
const hash = createHash('sha256').update(binaryBytes).digest('hex');
const files = [
{ name: basename(binaryPath), sizeBytes: binaryBytes.byteLength, sha256: hash },
{
name: basename(sidecarPath),
sizeBytes: sidecarBytes.byteLength,
sha256: createHash('sha256').update(sidecarBytes).digest('hex'),
},
];
const manifestPath = join(directory, 'bundled-tools.json');
const writeManifest = (distributionReady: boolean) =>
writeFile(
manifestPath,
JSON.stringify({ windowsCu: { binarySha256: hash, files, distributionReady } }),
);
await writeManifest(false);
const development = createComputerUseHost({ isPackaged: false, resourcesPath: directory, manifestPath, binaryPath, platform: 'win32', physicalInputRecentlyActive: () => false });
assert.equal(development.selected.backendId, 'windows-native');
await writeManifest(true);
const packaged = createComputerUseHost({ isPackaged: true, resourcesPath: directory, manifestPath, binaryPath, platform: 'win32', physicalInputRecentlyActive: () => false });
assert.equal(packaged.selected.backendId, 'windows-native');
await rm(sidecarPath);
const missing = createComputerUseHost({ isPackaged: false, resourcesPath: directory, manifestPath, binaryPath, platform: 'win32', physicalInputRecentlyActive: () => false });
assert.equal(missing.selected.backendId, 'none');
await writeFile(sidecarPath, sidecarBytes);
await writeFile(join(directory, 'unexpected.dll'), 'unmanaged');
const extra = createComputerUseHost({ isPackaged: false, resourcesPath: directory, manifestPath, binaryPath, platform: 'win32', physicalInputRecentlyActive: () => false });
assert.equal(extra.selected.backendId, 'none');
await rm(join(directory, 'unexpected.dll'));
await writeFile(sidecarPath, 'tampered');
const tampered = createComputerUseHost({ isPackaged: false, resourcesPath: directory, manifestPath, binaryPath, platform: 'win32', physicalInputRecentlyActive: () => false });
assert.equal(tampered.selected.backendId, 'none');
} finally {
await rm(directory, { recursive: true, force: true });
}
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ test('remote providers do not request Host paths and use a Client-owned cwd', as
);
});

test('publishes the real Computer Use schema through the Client Capability protocol', () => {
test('publishes the semantic-only Computer Use schema through the Client Capability protocol', () => {
const computerUseTools = buildComputerUseTools({ backend: computerBackend() });
const provider = createDesktopNativeCapabilityProvider({
browserTools: [],
Expand All @@ -127,10 +127,12 @@ test('publishes the real Computer Use schema through the Client Capability proto
offers: provider.offers(),
}),
);
const coordinateSchema = provider.offers()[0]?.tools[0]?.inputSchema.properties as
| Record<string, { items?: unknown }>
const properties = provider.offers()[0]?.tools[0]?.inputSchema.properties as
| Record<string, unknown>
| undefined;
assert.equal(Array.isArray(coordinateSchema?.coordinate?.items), true);
assert.equal(properties?.coordinate, undefined);
assert.notEqual(properties?.position, undefined);
assert.notEqual(properties?.size, undefined);
});

test('publishes every production Desktop-owned tool schema through the protocol', () => {
Expand Down
113 changes: 84 additions & 29 deletions apps/desktop/src/main/computer-use-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import {
fstatSync,
openSync,
readFileSync,
readdirSync,
} from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { MakaCuBackendOptions } from '@maka/computer-use';
import type { MakaCuServiceSnapshot } from '@maka/computer-use';
import type { WindowsCuServiceState } from '@maka/computer-use';
import {
selectComputerUseBackend,
type SelectedComputerUseBackend,
Expand All @@ -42,6 +44,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 +65,39 @@ function readRegularFile(path: string): Buffer {
}
}

function hasPinnedWindowsHelperFiles(
binaryPath: string,
files: NonNullable<BundledToolManifest['windowsCu']>['files'],
): boolean {
if (!files || files.length === 0) return false;
const expected = new Map<string, { sizeBytes: number; sha256: string }>();
for (const file of files) {
const sizeBytes = file.sizeBytes;
const sha256 = file.sha256;
if (
!file.name || file.name !== file.name.split(/[\\/]/).pop() ||
typeof sizeBytes !== 'number' || !Number.isSafeInteger(sizeBytes) || sizeBytes < 0 ||
typeof sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(sha256) || expected.has(file.name)
) return false;
expected.set(file.name, { sizeBytes, sha256 });
}
const directory = dirname(binaryPath);
const actual = readdirSync(directory, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name !== 'bundled-tools.json')
.map((entry) => entry.name);
if (actual.length !== expected.size || actual.some((name) => !expected.has(name))) return false;
for (const [name, pin] of expected) {
try {
const bytes = readRegularFile(join(directory, 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 +112,7 @@ export function createComputerUseHost(input: {
screenLocked?: (context: { sessionId: string }) => boolean | Promise<boolean>;
onTrace?: MakaCuBackendOptions['onTrace'];
overlay?: CuOverlayHook;
platform?: NodeJS.Platform;
}): ComputerUseHostState {
const manifestPath = input.manifestPath ?? (input.isPackaged
? join(input.resourcesPath, 'bundled-tools.json')
Expand All @@ -77,36 +122,42 @@ 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 +171,13 @@ export function createComputerUseHost(input: {
...(input.screenLocked ? { screenLocked: input.screenLocked } : {}),
...(input.onTrace ? { onTrace: input.onTrace } : {}),
...(input.overlay ? { overlay: input.overlay } : {}),
...(input.platform ? { platform: input.platform } : {}),
}),
binaryPath,
expectedBinarySha256,
};
} catch {
return { selected: selectComputerUseBackend() };
return { selected: selectComputerUseBackend({ platform }) };
}
}

Expand All @@ -144,28 +196,31 @@ export function createDesktopPhysicalInputGuard(
*/
export function computerUseServiceHealth(
backendId: SelectedComputerUseBackend['backendId'],
state: MakaCuServiceSnapshot | undefined,
state: MakaCuServiceSnapshot | { state: WindowsCuServiceState; generation: number } | undefined,
): {
state: 'not_available' | 'not_run' | 'healthy' | 'degraded';
reason: string;
} {
const label = backendId === 'windows-native' ? 'Windows native helper' : 'maka-cu executor';
if (backendId === 'none' || !state) {
return {
state: 'not_available',
reason: '未找到通过完整性检查且可分发的 maka-cu executor。',
reason: backendId === 'windows-native'
? '未找到通过完整性检查且可分发的 Windows native helper。'
: '未找到通过完整性检查且可分发的 maka-cu executor。',
};
}
switch (state.state) {
case 'disposed':
return { state: 'not_available', reason: 'maka-cu executor 已停止。' };
return { state: 'not_available', reason: `${label} 已停止。` };
case 'unavailable':
return { state: 'not_available', reason: 'maka-cu executor 启动失败或已退出。' };
return { state: 'not_available', reason: `${label} 启动失败或已退出。` };
case 'starting':
case 'backing_off':
return { state: 'degraded', reason: 'maka-cu executor 正在启动或恢复。' };
return { state: 'degraded', reason: `${label} 正在启动或恢复。` };
case 'ready':
return { state: 'healthy', reason: 'maka-cu executor 已就绪。' };
return { state: 'healthy', reason: `${label} 已就绪。` };
default:
return { state: 'not_run', reason: 'maka-cu 已可用,将在首次调用时启动。' };
return { state: 'not_run', reason: `${backendId === 'windows-native' ? 'Windows helper' : 'maka-cu'} 已可用,将在首次调用时启动。` };
}
}
1 change: 1 addition & 0 deletions apps/desktop/src/main/main-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,7 @@ function emitRealWindowSmokeDiagnostic(stage: string): void {
windowExists: true,
title: target.getTitle(),
bounds: target.getBounds(),
contentBounds: target.getContentBounds(),
normalBounds: target.getNormalBounds(),
isVisible: target.isVisible(),
isFocused: target.isFocused(),
Expand Down
6 changes: 5 additions & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@
// executor preparation scripts, so reformatting breaks provenance
// verification; bundled-tools.json is their generated sibling.
"!apps/desktop/resources/licenses/**",
"!apps/desktop/bundled-tools.json"
"!apps/desktop/bundled-tools.json",
// Computer Use harness outputs are raw evidence, not authored source.
// Keep their exact captured formatting and avoid Biome's 1 MiB input
// ceiling for repeated Chromium runs.
"!experiments/maka-cu-windows/**/*-results*.json"
]
},
"javascript": {
Expand Down
29 changes: 28 additions & 1 deletion docs/windows-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@

# Windows support baseline

## Native Computer Use helper

Windows Computer Use uses a native Windows adapter implementing the shared
`maka.cu/2` executor protocol. The desktop host starts it as a direct stdio
child, verifies the SHA-256 and size pins for the executable and every native
companion recorded in `apps/desktop/bundled-tools.json`, and invalidates
observations whenever the shared helper generation exits or restarts. Windows
must not introduce a private wire schema or a second lifecycle supervisor.

The surface is deliberately closed: enumerate and explicitly select one
running top level window, read its UI Automation tree, capture that window with
Windows Graphics Capture, and invoke semantic actions using a single-use
snapshot token. Ambiguous app/window matches fail closed. Global pointer input,
screen rectangle capture, post-message, and unsupported actions are not
fallbacks. A packaged build is enabled only when the Windows manifest entry
sets `distributionReady: true`; development artifacts are prepared with
`MAKA_CU_WINDOWS_SOURCE=<path-to-maka-cu> node scripts/prepare-windows-cu-helper.mjs`.
The script follows the native publish contract (`win-x64`, self-contained,
single-file, uncompressed, untrimmed, embedded debug), copies the executable
and required Windows Desktop native companion files together, rejects the
small framework-dependent apphost, and remains distribution-ineligible by
default.

Windows is an active enablement target, not a fully supported Maka platform yet. The CLI and Electron desktop application can run from source, and release workflows produce a verified unsigned Windows x64 preview. The x64 package includes an AppContainer sandbox for the managed filesystem-worker surface, with packaged lifecycle and adversarial evidence, and automatic updates are verified end to end in CI on the unsigned preview channel. Signing, the wider general-command sandbox tier, direct Credential Manager/DPAPI probes, independent security review, and computer-use guarantees remain incomplete. Progress is tracked in [GitHub issue #2142](https://github.com/apache/maka/issues/2142).

## Install the Windows x64 preview
Expand Down Expand Up @@ -231,7 +254,11 @@ The root test timeout is tracked separately from individual test failures. Phase
host named pipes, ambient environment, host registry values, parent tokens, and descendant
denial or AppContainer/Job inheritance. It does not claim UDP/DNS/SMB enforcement, local inbound-listener enforcement, the deferred
no-Win32k/window-station tier, or direct Credential Manager/DPAPI isolation.
- Computer-use has no Windows backend.
- Computer Use has a Windows native backend when a pinned helper digest is
present. Packaged builds also require `distributionReady: true`; source
development artifacts remain opt-in and do not establish full Windows
support. The product integration is tracked in [issue #4318](https://github.com/apache/maka/issues/4318),
with related executor hardening in [issue #3785](https://github.com/apache/maka/issues/3785).
- The Windows x64 NSIS installer is unsigned. The in-app automatic-update path (electron-updater →
NSIS handoff → relaunch) is verified end to end in CI against a loopback feed; the production
GitHub feed configuration is pinned by unit tests. Updates are not signature-verified until an
Expand Down
Loading