Skip to content
Open
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: 0 additions & 2 deletions modules/jarvos-agent-context/scripts/jarvos-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ const TOOLS = [
issueIdentifier: { type: 'string', description: 'Issue identifier such as SUP-2219.' },
artifact: { type: 'string', description: 'Artifact pointer such as an issue, branch, note, URL, or file path.' },
project: { type: 'string', description: 'Project tag used when no explicit thread id is provided.' },
title: { type: 'string', description: 'Explicit note title to read.' },
routeCapability: { type: 'string', description: 'Opaque short-lived route binding issued by the trusted native adapter.' },
maxChars: { type: 'number', description: 'Maximum characters of thread content to return.' },
},
Expand All @@ -196,7 +195,6 @@ const TOOLS = [
summary: { type: 'string', description: 'What changed or what the next AI needs to know.' },
decision: { type: 'string', description: 'Latest decision to preserve.' },
nextStep: { type: 'string', description: 'Concrete next action for the next host.' },
title: { type: 'string', description: 'Explicit note title to write.' },
routeCapability: { type: 'string', description: 'Opaque short-lived route binding issued by the trusted native adapter.' },
},
},
Expand Down
18 changes: 15 additions & 3 deletions modules/jarvos-agent-context/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -860,11 +860,21 @@ function normalizeThreadKey(input = {}) {
}

function sessionThreadTitle(input = {}) {
const explicit = firstString(input.title, input.noteTitle);
if (explicit) return sanitizeTitle(explicit);
return sanitizeTitle(`${DEFAULT_SESSION_THREAD_PREFIX} - ${normalizeThreadKey(input)}`);
}

function hasSessionThreadFrontmatter(markdown) {
const frontmatter = String(markdown || '').match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
if (!frontmatter) return false;
return /^(?:type|subtype):\s*["']?session-thread["']?\s*$/m.test(frontmatter);
}

function assertSessionThreadNote(markdown, title) {
if (!hasSessionThreadFrontmatter(markdown)) {
throw new Error(`Refusing to access non-session-thread note: ${title}`);
}
}

function stripFrontmatter(markdown) {
return String(markdown || '').replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
}
Expand Down Expand Up @@ -943,6 +953,7 @@ function renderSessionThreadRead(result) {
function readSessionThread(input = {}) {
const thread = resolveSessionThread(input);
const raw = readIfExists(thread.notePath);
if (raw !== null) assertSessionThreadNote(raw, thread.title);
const content = raw === null ? '' : boundedMarkdown(stripFrontmatter(raw), Number(input.maxChars || 4000));
const result = {
ok: true,
Expand All @@ -967,6 +978,7 @@ function writeSessionThread(input = {}) {
let readBack;
try {
const existing = readIfExists(thread.notePath);
if (existing !== null) assertSessionThreadNote(existing, thread.title);
const existingBody = existing ? stripFrontmatter(existing) : '';
const timestamp = firstString(input.timestamp) || new Date().toISOString();
const entry = formatThreadEntry(input, timestamp);
Expand All @@ -991,7 +1003,7 @@ function writeSessionThread(input = {}) {
createJournalIfMissing: input.createJournalIfMissing !== false,
mutationService,
});
if (noteResult.written) readBack = readSessionThread({ ...input, title: thread.title, maxChars: input.maxChars });
if (noteResult.written) readBack = readSessionThread({ ...input, maxChars: input.maxChars });
} finally {
releaseLock();
}
Expand Down
37 changes: 37 additions & 0 deletions modules/jarvos-agent-context/test/agent-context.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,43 @@ test('session thread writes a note, links today journal, and reads across hosts'
});
});

test('session thread access cannot be redirected to arbitrary vault notes', () => {
withTempVault(({ notes, mutationService }) => {
const privateNote = path.join(notes, 'Private Vault Note.md');
fs.writeFileSync(privateNote, '---\ntype: private-note\n---\nPrivate medical plan.', 'utf8');

const read = readSessionThread({ title: 'Private Vault Note', threadId: 'safe-thread' });
assert.equal(read.found, false);
assert.doesNotMatch(read.markdown, /Private medical plan/);

writeSessionThread({
title: 'Private Vault Note',
threadId: 'safe-thread',
summary: 'A safe checkpoint.',
mutationService,
});
assert.equal(fs.readFileSync(privateNote, 'utf8').includes('A safe checkpoint.'), false);
});
});

test('session thread access rejects namespace collisions without session-thread frontmatter', () => {
withTempVault(({ notes, mutationService }) => {
const privateNote = path.join(notes, 'JarvOS Session Thread - collision.md');
const original = '---\ntype: private-note\n---\nPrivate collision content.';
fs.writeFileSync(privateNote, original, 'utf8');

assert.throws(
() => readSessionThread({ threadId: 'collision' }),
/Refusing to access non-session-thread note/,
);
assert.throws(
() => writeSessionThread({ threadId: 'collision', summary: 'Injected checkpoint.', mutationService }),
/Refusing to access non-session-thread note/,
);
assert.equal(fs.readFileSync(privateNote, 'utf8'), original);
});
});

test('session thread appends checkpoints through latest-content transforms', () => {
withTempVault(({ mutationService }) => {
for (let index = 0; index < 6; index += 1) {
Expand Down
Loading