Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/mcode-harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ One patch script (`patch-native.mjs`) patches the pinned native CLI source. The

The native process, its ACP Session and the workspace tools share the Session's workspace as their working directory; native configuration, Skills and history stay in the private Session data directory. Builtin file tools are disabled, so project files are read and written through the bridge's tools. Native Session creation and loading connect the required workspace bridge and validate its tool inventory before preparation succeeds, using the existing Session-scoped connection pool and its normal request timeout. The adapter’s ACP request and Core preparation deadlines continue to bound startup. Turn discovery retains its existing shorter timeout. An unavailable or incomplete bridge fails preparation; optional MCP servers retain native lazy discovery. The expected inventory is generated from the worker's `--describe` output. Only the adapter registers this bridge; callers cannot supply its command, profile, working directory or environment. Native diff/undo capture is not provided by this path. Common Files and Artifacts use the same bound workspace.

For each tool call, the bridge starts `launch.mjs` with the Session's private profile (`workspace-profile.json`, written by the daemon). The launcher checks that the profile's `workspace` is a canonical absolute path, creates the `scratch` directory, and runs the worker in the workspace with the bridge's environment. An invalid profile rejects the call.
The bridge reads the Session's private profile (`workspace-profile.json`, written by the daemon) during initialization. Its tool executor checks that `workspace` is a canonical absolute path and creates the `scratch` directory before accepting calls. Each call starts the worker directly in that workspace with the bridge's environment. An invalid profile rejects bridge initialization.

## Build

Expand All @@ -24,7 +24,7 @@ This standalone companion uses its own npm lock and is excluded from the root pn

`subagent-snapshot.mjs` is a daemon-only reader of the private native SQLite history. It opens a read-only transaction, scopes recursive descendants to the bound root, and fails explicitly if a complete snapshot exceeds its bounds. It never executes a model or exposes native history to workspace tools. The adapter freezes root output and keeps the same ACP owner for bounded child settlement. A completed or idle task remains an active public Subagent; native abort is a Turn cancellation and never implies a closed Subagent.

The bridge owns each launcher until exit. MCP cancellation and transport shutdown stop all owned workers before releasing the bridge. The outer Runtime owns the native process group. Both boundaries require real Docker cancellation tests.
The bridge owns each worker until exit. MCP cancellation and transport shutdown stop all owned workers before releasing the bridge. The outer Runtime owns the native process group. Both boundaries require real Docker cancellation tests.

For declared stdio MCP calls, the adapter captures the affected server identities before sending cancellation and retains identities from callbacks received while cancellation drains. A local failure remains unconfirmed within its Turn even if native reports the tool as finished. The native tool wrapper sets the private `details.oac_response_received` field only for a result returned by the MCP SDK, including a server's `isError` reply; the adapter validates the result identity before releasing that call's owner. It waits for the Runtime to close every selected server's process scope, including background descendants, then calls the private `oac/session/mcp/disconnect` ACP extension with the Session ID and those server names. The native owner disconnects only the selected Session connections and waits for their old transports' actual close events. Configurations remain installed for lazy reconnection on a later call; other MCP servers and the workspace bridge keep their connections. HTTP servers, unknown names and `oac_workspace` are rejected by this control operation. ACP initialization advertises `oac/mcp-lifecycle` version 3, which the adapter requires for a workspace or declared stdio MCP. The request uses the existing native Session, MCP service and connection pool; it adds no model or tool execution loop.

Expand Down
7 changes: 4 additions & 3 deletions packages/mcode-harness/bridge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { readFileSync } from 'node:fs';
import { isAbsolute } from 'node:path';
import { ToolExecutor } from './tool-executor.mjs';

const profile = process.argv[2];
if (!profile || !isAbsolute(profile)) throw new Error('Private workspace profile is required');
const workspace = JSON.parse(readFileSync(profile, 'utf8')).workspace;
const profilePath = process.argv[2];
if (!profilePath || !isAbsolute(profilePath)) throw new Error('Private workspace profile is required');
const profile = JSON.parse(readFileSync(profilePath, 'utf8'));
const workspace = profile.workspace;
const definitions = JSON.parse(readFileSync(new URL('./dist/tools.json', import.meta.url), 'utf8'));
const tools = new Map(definitions.map(tool => ['workspace_' + tool.name, tool]));
const executor = new ToolExecutor(profile);
Expand Down
4 changes: 1 addition & 3 deletions packages/mcode-harness/cancellation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@ for (const operation of ['cancel', 'close']) {
if(name==='parent') spawn(process.execPath,['ticks.mjs','child'],{stdio:'inherit'});
setInterval(()=>appendFileSync(name+'.ticks','tick\\n'),20);
`);
const profile = join(root, 'profile.json');
await writeFile(profile, JSON.stringify({ workspace: root, scratch }));
const { ToolExecutor } = await import(pathToFileURL(join(artifact, 'tool-executor.mjs')));
const executor = new ToolExecutor(profile);
const executor = new ToolExecutor({ workspace: root, scratch });
t.after(() => executor.close());
const abort = new AbortController();
const finished = executor.execute('bash', { command: `${quote(process.execPath)} ticks.mjs parent` }, abort.signal);
Expand Down
2 changes: 1 addition & 1 deletion packages/mcode-harness/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';

if (!['linux', 'darwin'].includes(process.platform)) throw new Error('MiniMax native execution requires Linux or macOS');
for (const file of ['bridge.mjs', 'launch.mjs', 'tool-executor.mjs', 'dist/worker.mjs',
for (const file of ['bridge.mjs', 'tool-executor.mjs', 'dist/worker.mjs',
'subagent-snapshot.mjs',
'native/cli.js', 'native-patch.json']) accessSync(new URL(file, import.meta.url));
for (const command of ['bash', 'rg']) execFileSync('which', [command], { stdio: 'ignore' });
Expand Down
20 changes: 0 additions & 20 deletions packages/mcode-harness/launch.mjs

This file was deleted.

28 changes: 0 additions & 28 deletions packages/mcode-harness/launch.test.mjs

This file was deleted.

2 changes: 1 addition & 1 deletion packages/mcode-harness/native.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ test('packaged native tools use writable scratch and retain large output', {
assert.ok(isAbsolute(profile) && isAbsolute(artifact));
const config = JSON.parse(await readFile(profile, 'utf8'));
const { ToolExecutor } = await import(pathToFileURL(join(artifact, 'tool-executor.mjs')));
const executor = new ToolExecutor(profile);
const executor = new ToolExecutor(config);
t.after(() => executor.close());
const temporary = await executor.execute('bash', {
command: 'node -p "require(\'os\').tmpdir()"; f=$(mktemp) && printf TEMP_OK > "$f" && cat "$f" && rm "$f"',
Expand Down
16 changes: 11 additions & 5 deletions packages/mcode-harness/tool-executor.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { spawn } from 'node:child_process';
import { mkdirSync } from 'node:fs';
import { isAbsolute, normalize } from 'node:path';
import { fileURLToPath } from 'node:url';

const limit = 16 * 1024 * 1024;
const launcher = fileURLToPath(new URL('./launch.mjs', import.meta.url));
const worker = fileURLToPath(new URL('./dist/worker.mjs', import.meta.url));

// One bridge owns every launcher until its stdio and native tools have settled.
// One bridge owns every worker until its stdio and native tools have settled.
export class ToolExecutor {
#calls = new Set();
#closed = false;

constructor(profile, entrypoint = launcher) {
this.profile = profile;
constructor({ workspace, scratch }, entrypoint = worker) {
if (!isAbsolute(workspace) || normalize(workspace) !== workspace)
throw new Error('Invalid workspace profile');
mkdirSync(scratch, { recursive: true });
this.workspace = workspace;
this.entrypoint = entrypoint;
}

Expand All @@ -19,7 +24,8 @@ export class ToolExecutor {
signal?.throwIfAborted();
const request = JSON.stringify({ tool, input });
if (Buffer.byteLength(request) > limit) throw new Error('Workspace tool input exceeds limit');
const child = spawn(process.execPath, [this.entrypoint, this.profile], {
const child = spawn(process.execPath, [this.entrypoint, this.workspace], {
cwd: this.workspace,
env: process.env,
stdio: ['pipe', 'pipe', 'pipe'],
});
Expand Down
25 changes: 23 additions & 2 deletions packages/mcode-harness/tool-executor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ async function fixture(t, body) {
await mkdir(root, { recursive: true });
const dir = await mkdtemp(join(root, 'mcode-worker-'));
t.after(() => rm(dir, { recursive: true, force: true }));
const script = join(dir, 'launcher.mjs');
const script = join(dir, 'worker.mjs');
await writeFile(script, body);
const executor = new ToolExecutor(dir, script);
const executor = new ToolExecutor({ workspace: dir, scratch: join(dir, 'scratch') }, script);
t.after(() => executor.close());
return { dir, executor };
}
Expand Down Expand Up @@ -62,3 +62,24 @@ for (const shutdown of ['cancel', 'transport close']) {
await assert.rejects(executor.execute('read', {}), /transport is closed/);
});
}

test('input limit and prior cancellation reject before worker start', async t => {
const { dir, executor } = await fixture(t, `
import { writeFileSync } from 'node:fs';
writeFileSync(process.argv[2]+'/started','1');`);
await assert.rejects(executor.execute('read', { path: 'x'.repeat(16 * 1024 * 1024) }), /input exceeds limit/);
const abort = new AbortController();
abort.abort();
await assert.rejects(executor.execute('read', {}, abort.signal), { name: 'AbortError' });
await assert.rejects(access(join(dir, 'started')), { code: 'ENOENT' });
});

test('output limit stops and joins the worker', async t => {
const { executor } = await fixture(t, `
process.stdin.resume();
process.stdin.on('end', () => {
process.stdout.write('x'.repeat(16 * 1024 * 1024 + 1));
setInterval(() => {}, 1000);
});`);
await assert.rejects(executor.execute('read', {}), /output exceeds limit/);
});
35 changes: 35 additions & 0 deletions packages/mcode-harness/workspace.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { mkdtemp, mkdir, writeFile, rm, realpath, stat } from 'node:fs/promises';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { ToolExecutor } from './tool-executor.mjs';

test('worker runs with the initialized workspace and bridge environment', async t => {
const parent = join(homedir(), '.oac', 'tests');
await mkdir(parent, { recursive: true });
const root = await realpath(await mkdtemp(join(parent, 'mcode-workspace-')));
t.after(() => rm(root, { recursive: true, force: true }));
const workspace = join(root, 'workspace');
await mkdir(workspace);
const worker = join(root, 'worker.mjs');
await writeFile(worker, `
import { readFileSync } from 'node:fs';
const request = JSON.parse(readFileSync(0, 'utf8'));
console.log(JSON.stringify({tool_name:request.tool,
text:JSON.stringify({cwd:process.cwd(),root:process.argv[2],value:process.env.OAC_TEST_BRIDGE_ENV}),content:[]}));
`);
const scratch = join(root, 'scratch');
process.env.OAC_TEST_BRIDGE_ENV = 'ordinary';
t.after(() => { delete process.env.OAC_TEST_BRIDGE_ENV; });
const profile = { workspace, scratch };
const executor = new ToolExecutor(profile, worker);
t.after(() => executor.close());
assert.ok((await stat(scratch)).isDirectory());
profile.workspace = root;
const result = await executor.execute('bash', {});
assert.deepEqual(JSON.parse(result.text), { cwd: workspace, root: workspace, value: 'ordinary' });
for (const invalid of ['workspace', workspace + '/../workspace']) {
assert.throws(() => new ToolExecutor({ workspace: invalid, scratch }, worker), /Invalid workspace profile/);
}
});
2 changes: 1 addition & 1 deletion scripts/build-mcode-harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ test "$(node "$native/cli.js" --version)" = "$version"
artifact="$context/artifact"
mkdir "$artifact"
cp -R "$context/dist" "$context/node_modules" "$artifact/"
cp "$package/launch.mjs" "$package/bridge.mjs" "$package/check.mjs" "$package/tool-executor.mjs" "$package/source.json" "$artifact/"
cp "$package/bridge.mjs" "$package/check.mjs" "$package/tool-executor.mjs" "$package/source.json" "$artifact/"
cp "$package/subagent-snapshot.mjs" "$artifact/"
mkdir "$artifact/native"
cp -R "$context/upstream/dist/." "$artifact/native/"
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-mcode-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ for directory in "$runtime_root" "$output" "$native" "$companion"; do
done
test -f "$companion/provenance.json"
test -f "$companion/native-patch.json"
for file in launch.mjs bridge.mjs check.mjs tool-executor.mjs subagent-snapshot.mjs source.json; do
for file in bridge.mjs check.mjs tool-executor.mjs subagent-snapshot.mjs source.json; do
if ! cmp -s "$companion/$file" "$repo_root/packages/mcode-harness/$file"; then
printf 'MiniMax Code companion does not match the current source: %s\n' "$file" >&2
exit 1
Expand Down
2 changes: 1 addition & 1 deletion scripts/core-distribution-manifest.test.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_mcode_payload_rejects_stale_companion_at_the_same_version(self):
(companion / "native/cli.js").write_text('console.log("0.4.12");\n')
for receipt in ("provenance.json", "native-patch.json"):
(companion / receipt).write_text("{}")
files = ("launch.mjs", "bridge.mjs", "check.mjs", "tool-executor.mjs", "subagent-snapshot.mjs", "source.json")
files = ("bridge.mjs", "check.mjs", "tool-executor.mjs", "subagent-snapshot.mjs", "source.json")
for name in files:
(companion / name).write_bytes((repository / "packages/mcode-harness" / name).read_bytes())
output = self.stage / "payload"
Expand Down
Loading