From 626693bb69fd2d7b68c135ac0b6b4838fd3648a6 Mon Sep 17 00:00:00 2001 From: Serge Alex Date: Tue, 3 Nov 2026 03:12:41 +0300 Subject: [PATCH] enforce-always-and-directives-limits-workspace-key-filter --- bridge/memory.js | 17 +++++++++++++++-- bridge/server.js | 3 ++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/bridge/memory.js b/bridge/memory.js index 3cfba08..3ee4b18 100644 --- a/bridge/memory.js +++ b/bridge/memory.js @@ -156,6 +156,8 @@ function opBudgets(state) { return { groups: out, alwaysCount: alwaysCount, alwaysLimit: ALWAYS_LIMIT, totalEntries: state.entries.length }; } +function normWorkspace(w) { return String(w || '').replace(/\\/g, '/').toLowerCase(); } + function validateEntry(e) { if (!e.track || (e.track !== 'user' && e.track !== 'agent')) throw new Error('track user or agent'); if (!e.scope || (e.scope !== 'user-global' && e.scope !== 'workspace')) throw new Error('scope user-global or workspace'); @@ -176,6 +178,10 @@ function opWrite(state, args) { validateEntry(e); const alwaysCount = state.entries.filter(function (x) { return x.importance === 'always'; }).length; if (e.importance === 'always' && alwaysCount >= ALWAYS_LIMIT) throw new Error('E_TOO_MANY_ALWAYS: limit ' + ALWAYS_LIMIT); + if (String(e.key || '').indexOf('directive_') === 0) { + const dirCount = state.entries.filter(function (x) { return String(x.key).indexOf('directive_') === 0 && x.state === 'active' && (x.chatId || '') === (e.chatId || ''); }).length; + if (dirCount >= DIRECTIVES_PER_CHAT) throw new Error('E_TOO_MANY_DIRECTIVES: limit ' + DIRECTIVES_PER_CHAT + ' per chat'); + } const entry = { id: newId(), track: e.track, scope: e.scope, workspaceKey: String(e.workspaceKey || ''), key: e.key, text: e.text, importance: e.importance, tags: Array.isArray(e.tags) ? e.tags.slice(0, TAGS_LIMIT) : [], @@ -192,7 +198,13 @@ function opWrite(state, args) { if (!e) throw new Error('ENTRY_NOT_FOUND'); if (typeof args.text === 'string' && args.text) { e.text = args.text; e.bytes = byteLen(args.text); } if (Array.isArray(args.tags)) e.tags = args.tags.slice(0, TAGS_LIMIT); - if (args.importance === 'always' || args.importance === 'called') e.importance = args.importance; + if (args.importance === 'always' || args.importance === 'called') { + if (args.importance === 'always' && e.importance !== 'always') { + const alwaysCount = state.entries.filter(function (x) { return x.importance === 'always'; }).length; + if (alwaysCount >= ALWAYS_LIMIT) throw new Error('E_TOO_MANY_ALWAYS: limit ' + ALWAYS_LIMIT); + } + e.importance = args.importance; + } if (args.state === 'active' || args.state === 'done' || args.state === 'archive') e.state = args.state; if (typeof args.chatId === 'string') e.chatId = args.chatId; e.version = (e.version || 1) + 1; @@ -239,10 +251,11 @@ function query(state, args) { throw new Error('unknown op: ' + op); } -function getInjection(state, chatId) { +function getInjection(state, chatId, workspaceKey) { const entries = []; for (const e of state.entries) { if (e.key && e.key.indexOf('directive_') === 0 && e.scope === 'workspace' && e.state === 'active') { + var ew = normWorkspace(e.workspaceKey); var cw = normWorkspace(workspaceKey); if (ew && cw && ew !== cw) continue; if (!chatId || !e.chatId || e.chatId === chatId) { entries.push(e); } continue; } diff --git a/bridge/server.js b/bridge/server.js index 92d2634..ac8badd 100644 --- a/bridge/server.js +++ b/bridge/server.js @@ -228,7 +228,8 @@ async function handle(req, res, pathname, body) { const memDir = resolveMemoryDir(config, BRIDGE_DIR); const st = memoryModule.loadState(memDir); const chatId = body && body.chatId ? String(body.chatId) : ''; - const inj = memoryModule.getInjection(st, chatId); + const wsKey = body && body.workspaceKey ? String(body.workspaceKey) : String(config.allowedRoots[0] || ''); + const inj = memoryModule.getInjection(st, chatId, wsKey); memoryModule.saveState(memDir, st); return { ok: true, data: inj }; }