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
2 changes: 1 addition & 1 deletion packages/mcode-harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This companion package lets the agent host run MiniMax Code with OpenAgentCore's workspace. MiniMax Code keeps its own ACP Session, model loop and history. The package supplies a trusted MCP server (`bridge.mjs`), which the daemon registers as `oac_workspace` (its MCP server info names it `oac-workspace`). It exposes six native MiniMax Code tools rooted at the Session's workspace: `workspace_read`, `workspace_write`, `workspace_edit`, `workspace_bash`, `workspace_grep` and `workspace_glob`. The bridge and its tools run beside the CLI in the Session's agent-host view, where Bash, `rg` and `git` run in the sandbox; the package adds no inner sandbox. The [maintainer guide](../../docs/maintainers.md#runtime-images-and-helpers) owns the agent-host image build; [Harness qualification](../../contracts/agents-api/harness-onboarding.md#qualify-the-view) owns deployment evidence.

One patch script (`patch-native.mjs`) patches the pinned native CLI source. The SQLite task-admission transaction enforces the daemon's Subagent concurrency limit before child work starts; foreground, background, nested and idle-child append admissions share that transaction, and terminal native tasks release capacity. ACP initialization reports `oac/subagents` metadata: its version, the applied workspace tool policy and the admission limit. The native tool catalog applies the `protected-mcp-v1` tool gate described under [Subagents and cancellation](#subagents-and-cancellation). Under the same policy, the CLI ignores the workspace's project `.mcp.json`, so the Session's MCP comes only from the daemon. No second model or scheduling loop is introduced. Hosted public execution is not qualified by this package alone.
One patch script (`patch-native.mjs`) patches the pinned native CLI source. Native Skill refresh reads at most four independent files per root concurrently, retaining the upstream identity checks, ordered diagnostics, cache keys and precedence. The SQLite task-admission transaction enforces the daemon's Subagent concurrency limit before child work starts; foreground, background, nested and idle-child append admissions share that transaction, and terminal native tasks release capacity. ACP initialization reports `oac/subagents` metadata: its version, the applied workspace tool policy and the admission limit. The native tool catalog applies the `protected-mcp-v1` tool gate described under [Subagents and cancellation](#subagents-and-cancellation). Under the same policy, the CLI ignores the workspace's project `.mcp.json`, so the Session's MCP comes only from the daemon. No second model or scheduling loop is introduced. Hosted public execution is not qualified by this package alone.

## Workspace tools

Expand Down
120 changes: 120 additions & 0 deletions packages/mcode-harness/native-skills.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { promises as fs } from 'node:fs';
import { createRequire } from 'node:module';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { pathToFileURL } from 'node:url';

const root = process.env.MCODE_SOURCE;
test('native Skill refresh bounds independent reads and retains file identity and snapshot order', { skip: !root }, async t => {
const require = createRequire(join(root, 'package.json'));
const { build } = require('esbuild');
const artifacts = join(homedir(), '.oac', 'tests');
await fs.mkdir(artifacts, { recursive: true });
const directory = await fs.mkdtemp(join(artifacts, 'mcode-skills-'));
t.after(() => fs.rm(directory, { recursive: true, force: true }));
const result = await build({
entryPoints: [join(root, 'packages/agent-modules/skills/src/registry.ts')],
bundle: true, write: false, platform: 'node', format: 'esm', target: 'node22',
nodePaths: [join(root, 'node_modules')],
});
const bundle = join(directory, 'skills.mjs');
await fs.writeFile(bundle, result.outputFiles[0].text);
const { SkillRegistry } = await import(pathToFileURL(bundle).href);
const global = join(directory, 'global'), project = join(directory, 'project');
const outside = join(directory, 'outside');
for (const path of [global, project, outside]) await fs.mkdir(path);
const body = name => `---\nname: ${name}\ndescription: Test ${name}.\n---\n# ${name}\nTest body.\n`;
for (let i = 0; i < 9; i++) {
await fs.mkdir(join(global, `skill-${i}`));
await fs.writeFile(join(global, `skill-${i}`, 'SKILL.md'), body(`skill-${i}`));
}
await fs.mkdir(join(project, 'duplicate'));
await fs.writeFile(join(project, 'duplicate/SKILL.md'), body('skill-0'));
await fs.writeFile(join(outside, 'SKILL.md'), body('outside'));
await fs.symlink(outside, join(global, 'linked-directory'));
await fs.mkdir(join(global, 'linked-file'));
await fs.symlink(join(outside, 'SKILL.md'), join(global, 'linked-file/SKILL.md'));
const roots = [
{ id: 'global', kind: 'global', rootPath: global },
{ id: 'project', kind: 'project', rootPath: project },
{ id: 'absent', kind: 'global', rootPath: join(directory, 'absent') },
];
const registry = new SkillRegistry(roots);
const first = await registry.refresh();
assert.equal(first.metrics.filesRead, 11);
assert.equal(first.metrics.winners, 10);
assert.equal(first.winners.find(entry => entry.name === 'skill-0').rootId, 'project');
assert.deepEqual(first.diagnostics.map(value => value.code), ['skill_symlink_rejected', 'root_unreadable']);
const cached = await registry.refresh();
assert.deepEqual(cached.entries, first.entries);
assert.deepEqual(cached.diagnostics, first.diagnostics);
assert.equal(cached.metrics.filesRead, 0);
assert.equal(cached.metrics.filesReused, 11);

await fs.writeFile(join(global, 'skill-3/SKILL.md'), body('skill-3') + 'Changed.\n');
await fs.rm(join(global, 'skill-1'), { recursive: true });
await fs.rm(join(global, 'skill-2/SKILL.md'));
await fs.symlink(join(outside, 'SKILL.md'), join(global, 'skill-2/SKILL.md'));
const changed = await registry.refresh();
assert.equal(changed.metrics.filesRead, 1);
assert.equal(changed.metrics.filesReused, 8);
assert.equal(changed.entries.some(entry => ['skill-1', 'skill-2'].includes(entry.name)), false);
assert.match(changed.entries.find(entry => entry.name === 'skill-3').content, /Changed\./);

// Deliberately reverse completion order; result and diagnostic order must
// remain the native candidate order, with no more than four opens in flight.
const open = fs.open;
let active = 0, maximum = 0;
fs.open = async (path, ...args) => {
active++;
maximum = Math.max(maximum, active);
try {
await new Promise(resolve => setTimeout(resolve, String(path).includes('skill-4') ? 20 : 5));
return await open(path, ...args);
} finally { active--; }
};
try {
const reordered = await new SkillRegistry(roots).refresh();
assert.deepEqual(reordered.entries, changed.entries);
assert.deepEqual(reordered.diagnostics, changed.diagnostics);
assert.ok(maximum > 1 && maximum <= 4, `concurrent opens: ${maximum}`);
} finally { fs.open = open; }

// Candidate inspection failures finish in reverse order within a batch.
// Preserve the original diagnostic order, not completion order.
const completed = [];
fs.open = async (path, ...args) => {
const name = ['skill-6', 'skill-7'].find(name => String(path) === join(global, name, 'SKILL.md'));
if (!name) return open(path, ...args);
await new Promise(resolve => setTimeout(resolve, name === 'skill-6' ? 20 : 1));
completed.push(name);
throw Object.assign(new Error('Fixture permission denied'), { code: 'EACCES' });
};
try {
const failed = await new SkillRegistry(roots).refresh();
assert.deepEqual(completed, ['skill-7', 'skill-6']);
const failures = failed.diagnostics.filter(value => value.code === 'skill_stat_failed');
assert.deepEqual(failures.map(value => value.locationUri),
['skill-6', 'skill-7'].map(name => changed.entries.find(entry => entry.name === name).locationUri));
} finally { fs.open = open; }

// The same open-file identity check must still reject replacement between
// inspection and content read, even when other files finish independently.
let opens = 0;
fs.open = async (path, ...args) => {
const handle = await open(path, ...args);
if (String(path) !== join(global, 'skill-3/SKILL.md') || ++opens !== 2) return handle;
return new Proxy(handle, { get(target, key) {
if (key === 'stat') return async () => { const stat = await target.stat(); stat.ino++; return stat; };
const value = Reflect.get(target, key);
return typeof value === 'function' ? value.bind(target) : value;
} });
};
try {
const replaced = await new SkillRegistry(roots).refresh();
assert.ok(replaced.diagnostics.some(value => value.code === 'skill_changed_during_read'));
assert.equal(replaced.entries.some(entry => entry.name === 'skill-3'), false);
} finally { fs.open = open; }
});
66 changes: 65 additions & 1 deletion packages/mcode-harness/patch-native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ copyFileSync(join(here, 'native-subagent-admission.mjs'), join(root, 'packages/l
function replaceNative(file, before, after) {
const path = join(root, file);
const source = readFileSync(path, 'utf8');
if (source.split(before).length !== 2) throw new Error('Pinned native MCP lifecycle source changed: ' + file);
if (source.split(before).length !== 2) throw new Error('Pinned native source changed: ' + file);
writeFileSync(path, source.replace(before, after));
}
const poolFile = 'packages/agent-modules/mcp/src/runtime/connection-pool.ts';
Expand Down Expand Up @@ -234,6 +234,70 @@ replaceNative('packages/tui/src/acp/agent.ts',
});

app.onNotification(acp.methods.agent.session.cancel, async ({ params }) => {`);
// Read independent Skills concurrently while committing the existing snapshot
// and cache in candidate order. All stable-file checks stay in the native reader.
replaceNative('packages/agent-modules/skills/src/registry.ts',
` for (const candidate of candidates) {
const cacheLocation = \`\${root.id}:\${candidate.entryDir}\`;
seenCacheLocations.add(cacheLocation);
metrics.filesSeen += 1;
const stat = await statSkillFile(root, candidate, diagnostics);
if (!stat) {
this.cache.delete(cacheLocation);
continue;
}
const cacheKey = \`\${candidate.fileLocation}:\${stat.dev}:\${stat.ino}:\${stat.size}:\${stat.mtimeMs}\`;
const cached = this.cache.get(cacheLocation);
let parsed: CachedSkillFile;

if (cached?.key === cacheKey) {
metrics.filesReused += 1;
parsed = cached;
} else {
metrics.filesRead += 1;
parsed = await parseSkillFile(root, candidate, stat, cacheKey);
this.cache.set(cacheLocation, parsed);
}

diagnostics.push(...parsed.diagnostics);
if (parsed.entry) {
entries.push(parsed.entry);
}
}`,
` for (let offset = 0; offset < candidates.length; offset += 4) {
const results = await Promise.allSettled(
candidates.slice(offset, offset + 4).map(async (candidate) => {
const cacheLocation = \`\${root.id}:\${candidate.entryDir}\`;
const diagnostics: SkillDiagnostic[] = [];
const stat = await statSkillFile(root, candidate, diagnostics);
if (!stat) return { cacheLocation, diagnostics };
const cacheKey = \`\${candidate.fileLocation}:\${stat.dev}:\${stat.ino}:\${stat.size}:\${stat.mtimeMs}\`;
const cached = this.cache.get(cacheLocation);
const reused = cached?.key === cacheKey;
const parsed = reused ? cached : await parseSkillFile(root, candidate, stat, cacheKey);
return { cacheLocation, diagnostics, parsed, reused };
}),
);
for (const result of results) {
if (result.status === 'rejected') throw result.reason;
const value = result.value;
seenCacheLocations.add(value.cacheLocation);
metrics.filesSeen += 1;
diagnostics.push(...value.diagnostics);
if (!value.parsed) {
this.cache.delete(value.cacheLocation);
continue;
}
if (value.reused) {
metrics.filesReused += 1;
} else {
metrics.filesRead += 1;
this.cache.set(value.cacheLocation, value.parsed);
}
diagnostics.push(...value.parsed.diagnostics);
if (value.parsed.entry) entries.push(value.parsed.entry);
}
}`);
const digest = name => createHash('sha256').update(readFileSync(join(here, name))).digest('hex');
writeFileSync(join(root, '.oac-native-patch.json'), JSON.stringify({ revision: pin.revision,
files: { 'patch-native.mjs':digest('patch-native.mjs'), 'native-subagent-admission.mjs':digest('native-subagent-admission.mjs') } }, null, 2)+'\n');
2 changes: 1 addition & 1 deletion scripts/build-mcode-harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ test "$(node "$native/cli.js" --version)" = "$version"
)
(
cd "$context/upstream"
MCODE_SOURCE="$context/upstream" node --test "$context/native-prompt.test.mjs" "$context/native-mcp-lifecycle.test.mjs"
MCODE_SOURCE="$context/upstream" node --test "$context/native-prompt.test.mjs" "$context/native-mcp-lifecycle.test.mjs" "$context/native-skills.test.mjs"
node scripts/build.mjs
)
artifact="$context/artifact"
Expand Down
Loading