Skip to content

Commit 8321021

Browse files
authored
fix(memory): classify directory traversal failures
1 parent 90ef62c commit 8321021

2 files changed

Lines changed: 58 additions & 4 deletions

File tree

scripts/lib/memory-vault.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,12 @@ function publicMemoryFileError(error) {
443443
};
444444
}
445445

446+
function incompleteMemoryLookupError() {
447+
const error = new Error('Memory lookup is incomplete. Inspect the authorized vault before retrying.');
448+
error.code = 'ECC_MEMORY_INCOMPLETE';
449+
return error;
450+
}
451+
446452
function readMemoryFiles(options = {}) {
447453
const roots = options.roots || resolveVaultRoots(options);
448454
const scopes = normalizeScopes(options.scopes || DEFAULT_RECALL_SCOPES);
@@ -461,7 +467,13 @@ function readMemoryFiles(options = {}) {
461467
break;
462468
}
463469
const root = assertMemoryRootSafe(roots, scope);
464-
const walked = walkMemoryRoot(root, MAX_FILES - visitedCount);
470+
let walked;
471+
try {
472+
walked = walkMemoryRoot(root, MAX_FILES - visitedCount);
473+
} catch {
474+
// Directory open/read/close failures cannot establish a complete lookup.
475+
throw incompleteMemoryLookupError();
476+
}
465477
visitedCount += walked.visitedCount;
466478
truncated = truncated || walked.truncated;
467479
skippedSymlinkCount += walked.skippedSymlinkCount;
@@ -660,9 +672,7 @@ function readMemoryById(id, options = {}) {
660672
: null;
661673
const loaded = readMemoryFiles(options);
662674
if (loaded.truncated || loaded.invalidFileCount > 0) {
663-
const error = new Error('Memory lookup is incomplete. Inspect the authorized vault before retrying.');
664-
error.code = 'ECC_MEMORY_INCOMPLETE';
665-
throw error;
675+
throw incompleteMemoryLookupError();
666676
}
667677
const matches = loaded.entries
668678
.filter(entry => entry.memory.id === memoryId)

tests/lib/memory-read-completeness.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,50 @@ async function main() {
7575
fs.openSync = open;
7676
}
7777
});
78+
function failTraversal(roots, phase, fn) {
79+
const open = fs.opendirSync;
80+
let closed = false;
81+
fs.opendirSync = (directory, ...args) => {
82+
if (directory !== roots.project) return open(directory, ...args);
83+
const fail = () => {
84+
const error = new Error('Synthetic private directory detail.');
85+
error.code = 'EACCES';
86+
throw error;
87+
};
88+
if (phase === 'open') return fail();
89+
const handle = open(directory, ...args);
90+
return {
91+
readSync: () => phase === 'read' ? fail() : handle.readSync(),
92+
closeSync: () => {
93+
handle.closeSync(); closed = true;
94+
if (phase === 'close') fail();
95+
},
96+
};
97+
};
98+
try { fn(); } finally {
99+
fs.opendirSync = open;
100+
if (phase !== 'open') assert.equal(closed, true);
101+
}
102+
}
103+
for (const phase of ['open', 'read', 'close']) {
104+
check(`direct read classifies directory ${phase} failure`, ({ roots, read }) => {
105+
failTraversal(roots, phase, () => {
106+
assert.throws(read, error => error.code === 'ECC_MEMORY_INCOMPLETE'
107+
&& !error.message.includes('Synthetic private directory detail.'));
108+
});
109+
});
110+
check(`MCP read classifies directory ${phase} failure`, ({ roots, mcp }) => {
111+
failTraversal(roots, phase, () => {
112+
const result = mcp(); assert.equal(result.isError, true);
113+
const error = JSON.parse(result.content[0].text).error;
114+
assert.equal(error.code, 'MEMORY_READ_INCOMPLETE');
115+
assert.equal(error.message.includes('Synthetic private directory detail.'), false);
116+
});
117+
});
118+
}
119+
check('unreadable traversal cannot establish missing memory', ({ roots, read }) => {
120+
failTraversal(roots, 'open', () => incomplete(() => read('mem_synthetic_missing')));
121+
});
78122
check('MCP incomplete lookup has a distinct bounded error', ({ mcp, truncate }) => {
79123
truncate(); const result = mcp(); assert.equal(result.isError, true);
80124
const error = JSON.parse(result.content[0].text).error;

0 commit comments

Comments
 (0)