fix(memory): classify directory traversal failures

This commit is contained in:
Affaan Mustafa authored and GitHub committed 2026-09-12 07:45:40 -04:00
1 parent 90ef62cb1d
commit 8321021c54
2 files changed
+58 -4

No files matched your search

+14 -4
View File
@@ -443,6 +443,12 @@ function publicMemoryFileError(error) {
}; };
} }
function incompleteMemoryLookupError() {
const error = new Error('Memory lookup is incomplete. Inspect the authorized vault before retrying.');
error.code = 'ECC_MEMORY_INCOMPLETE';
return error;
}
function readMemoryFiles(options = {}) { function readMemoryFiles(options = {}) {
const roots = options.roots || resolveVaultRoots(options); const roots = options.roots || resolveVaultRoots(options);
const scopes = normalizeScopes(options.scopes || DEFAULT_RECALL_SCOPES); const scopes = normalizeScopes(options.scopes || DEFAULT_RECALL_SCOPES);
@@ -461,7 +467,13 @@ function readMemoryFiles(options = {}) {
break; break;
} }
const root = assertMemoryRootSafe(roots, scope); const root = assertMemoryRootSafe(roots, scope);
const walked = walkMemoryRoot(root, MAX_FILES - visitedCount); let walked;
try {
walked = walkMemoryRoot(root, MAX_FILES - visitedCount);
} catch {
// Directory open/read/close failures cannot establish a complete lookup.
throw incompleteMemoryLookupError();
}
visitedCount += walked.visitedCount; visitedCount += walked.visitedCount;
truncated = truncated || walked.truncated; truncated = truncated || walked.truncated;
skippedSymlinkCount += walked.skippedSymlinkCount; skippedSymlinkCount += walked.skippedSymlinkCount;
@@ -660,9 +672,7 @@ function readMemoryById(id, options = {}) {
: null; : null;
const loaded = readMemoryFiles(options); const loaded = readMemoryFiles(options);
if (loaded.truncated || loaded.invalidFileCount > 0) { if (loaded.truncated || loaded.invalidFileCount > 0) {
const error = new Error('Memory lookup is incomplete. Inspect the authorized vault before retrying.'); throw incompleteMemoryLookupError();
error.code = 'ECC_MEMORY_INCOMPLETE';
throw error;
} }
const matches = loaded.entries const matches = loaded.entries
.filter(entry => entry.memory.id === memoryId) .filter(entry => entry.memory.id === memoryId)
@@ -75,6 +75,50 @@ async function main() {
fs.openSync = open; fs.openSync = open;
} }
}); });
function failTraversal(roots, phase, fn) {
const open = fs.opendirSync;
let closed = false;
fs.opendirSync = (directory, ...args) => {
if (directory !== roots.project) return open(directory, ...args);
const fail = () => {
const error = new Error('Synthetic private directory detail.');
error.code = 'EACCES';
throw error;
};
if (phase === 'open') return fail();
const handle = open(directory, ...args);
return {
readSync: () => phase === 'read' ? fail() : handle.readSync(),
closeSync: () => {
handle.closeSync(); closed = true;
if (phase === 'close') fail();
},
};
};
try { fn(); } finally {
fs.opendirSync = open;
if (phase !== 'open') assert.equal(closed, true);
}
}
for (const phase of ['open', 'read', 'close']) {
check(`direct read classifies directory ${phase} failure`, ({ roots, read }) => {
failTraversal(roots, phase, () => {
assert.throws(read, error => error.code === 'ECC_MEMORY_INCOMPLETE'
&& !error.message.includes('Synthetic private directory detail.'));
});
});
check(`MCP read classifies directory ${phase} failure`, ({ roots, mcp }) => {
failTraversal(roots, phase, () => {
const result = mcp(); assert.equal(result.isError, true);
const error = JSON.parse(result.content[0].text).error;
assert.equal(error.code, 'MEMORY_READ_INCOMPLETE');
assert.equal(error.message.includes('Synthetic private directory detail.'), false);
});
});
}
check('unreadable traversal cannot establish missing memory', ({ roots, read }) => {
failTraversal(roots, 'open', () => incomplete(() => read('mem_synthetic_missing')));
});
check('MCP incomplete lookup has a distinct bounded error', ({ mcp, truncate }) => { check('MCP incomplete lookup has a distinct bounded error', ({ mcp, truncate }) => {
truncate(); const result = mcp(); assert.equal(result.isError, true); truncate(); const result = mcp(); assert.equal(result.isError, true);
const error = JSON.parse(result.content[0].text).error; const error = JSON.parse(result.content[0].text).error;