From effca546b691802ac65452b69a07563b19bb45d8 Mon Sep 17 00:00:00 2001 From: whitelonng Date: Fri, 14 Aug 2026 01:18:44 +0800 Subject: [PATCH 1/2] fix: name the offending symlink path in MANCODE_ARTIFACT_PATH_UNSAFE errors The V3 adapter path-safety assertion rejected symlinked fixed targets (e.g. CLAUDE.md -> AGENTS.md, the openai/codex convention) with a bare error code, leaving failures unexplained. The error now names the relative path, the resolved link target, and the remediation while keeping the MANCODE_ARTIFACT_PATH_UNSAFE prefix intact. --- src/installers/v3-adapter.ts | 35 ++++++++++++++++++++++++------ tests/v3-adapter-contracts.test.ts | 21 ++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/installers/v3-adapter.ts b/src/installers/v3-adapter.ts index ab00578..a00b411 100644 --- a/src/installers/v3-adapter.ts +++ b/src/installers/v3-adapter.ts @@ -3,6 +3,7 @@ import { lstat, mkdir, readFile, + realpath, rename, rm, rmdir, @@ -2376,11 +2377,15 @@ async function assertAdapterPathSafe( ): Promise { const relative = path.relative(root, target); if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) { - throw new Error('MANCODE_ARTIFACT_PATH_UNSAFE'); + throw new Error( + `MANCODE_ARTIFACT_PATH_UNSAFE: adapter target must stay inside the project root: ${target}`, + ); } const rootEntry = await lstat(root); if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) { - throw new Error('MANCODE_ARTIFACT_PATH_UNSAFE'); + throw new Error( + `MANCODE_ARTIFACT_PATH_UNSAFE: project root must be a real directory, not a symbolic link: ${root}`, + ); } const segments = relative.split(path.sep); let current = root; @@ -2388,11 +2393,18 @@ async function assertAdapterPathSafe( current = path.join(current, segments[index] ?? ''); try { const entry = await lstat(current); - if ( - entry.isSymbolicLink() || - (index < segments.length - 1 && !entry.isDirectory()) - ) { - throw new Error('MANCODE_ARTIFACT_PATH_UNSAFE'); + if (entry.isSymbolicLink()) { + const detail = await describeAdapterSymlink(current); + const replacement = + index === segments.length - 1 ? 'a regular file' : 'a real directory'; + throw new Error( + `MANCODE_ARTIFACT_PATH_UNSAFE: ${relative} is a symbolic link${detail}; mancode never writes through links. Replace it with ${replacement} before initializing the adapter.`, + ); + } + if (index < segments.length - 1 && !entry.isDirectory()) { + throw new Error( + `MANCODE_ARTIFACT_PATH_UNSAFE: ${relative} cannot be used because ${segments[index]} is not a directory`, + ); } } catch (error) { if (isNodeError(error) && error.code === 'ENOENT') return; @@ -2401,6 +2413,15 @@ async function assertAdapterPathSafe( } } +async function describeAdapterSymlink(linkPath: string): Promise { + try { + const resolved = await realpath(linkPath); + return ` (resolves to ${resolved})`; + } catch { + return ' (broken link)'; + } +} + async function removeManagedV3Block( filePath: string, startMarker: string, diff --git a/tests/v3-adapter-contracts.test.ts b/tests/v3-adapter-contracts.test.ts index aa32af4..b8d8b68 100644 --- a/tests/v3-adapter-contracts.test.ts +++ b/tests/v3-adapter-contracts.test.ts @@ -644,6 +644,27 @@ describe('V3 adapter bootstrap integration', () => { }, ); + it.skipIf(process.platform === 'win32')( + 'names an in-repo symlinked fixed target instead of a bare path error', + async () => { + await init(root, { v3: true, platform: 'codex' }); + // Repo convention (e.g. openai/codex): CLAUDE.md mirrors AGENTS.md. + await writeFile( + path.join(root, 'AGENTS.md'), + '# shared agent instructions\n', + ); + await symlink('AGENTS.md', path.join(root, 'CLAUDE.md')); + + await expect(installV3Adapter(root, 'claude-code')).rejects.toThrow( + /MANCODE_ARTIFACT_PATH_UNSAFE: CLAUDE\.md is a symbolic link \(resolves to .*AGENTS\.md\)/, + ); + // The link itself is left untouched: mancode never writes through it. + await expect( + readFile(path.join(root, 'CLAUDE.md'), 'utf8'), + ).resolves.toBe('# shared agent instructions\n'); + }, + ); + it('retires legacy managed entrypoints when repairing an active V3 adapter', async () => { await init(root, { v3: true }); const legacyCodexAlias = path.join( From bc1e296ba3a3cbddc6b34c930886c09a4e2a6745 Mon Sep 17 00:00:00 2001 From: whitelonng Date: Fri, 14 Aug 2026 01:22:40 +0800 Subject: [PATCH 2/2] release: mancode 0.6.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4a0721..0794453 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mancode", - "version": "0.6.1", + "version": "0.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mancode", - "version": "0.6.1", + "version": "0.6.2", "license": "AGPL-3.0-only", "dependencies": { "commander": "^12.1.0", diff --git a/package.json b/package.json index deff09c..f6c8de2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mancode", - "version": "0.6.1", + "version": "0.6.2", "description": "AI coding agent workflow harness with mancode Continuity for cross-conversation tasks, decisions, verification, and team coordination.", "type": "module", "license": "AGPL-3.0-only",