mcp-tool-acl.mjs
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠ breaking
10 days ago
| 1 | /** |
| 2 | * Issue #1 Phase D2 — role-based tool access control for hosted MCP. |
| 3 | * Filters available tools based on user role (viewer, editor, admin, evaluator). |
| 4 | * Hosted prompts (Track B1–B3): default **viewer**; **`write-from-capture`** is **editor** (implies persisting notes). |
| 5 | */ |
| 6 | |
| 7 | const READ_TOOLS = new Set([ |
| 8 | 'search', |
| 9 | 'get_note', |
| 10 | 'get_note_outline', |
| 11 | 'get_document_tree', |
| 12 | 'get_metadata_facets', |
| 13 | 'get_section_source', |
| 14 | 'list_notes', |
| 15 | 'relate', |
| 16 | 'backlinks', |
| 17 | 'extract_tasks', |
| 18 | 'cluster', |
| 19 | 'tag_suggest', |
| 20 | 'summarize', |
| 21 | 'enrich', |
| 22 | 'attachment_list', |
| 23 | 'attachment_get', |
| 24 | 'media_import_consent_list', |
| 25 | ]); |
| 26 | |
| 27 | const WRITE_TOOLS = new Set([ |
| 28 | ...READ_TOOLS, |
| 29 | 'write', |
| 30 | 'hub_create_proposal', |
| 31 | 'capture', |
| 32 | 'transcribe', |
| 33 | 'vault_sync', |
| 34 | 'media_external_link_propose', |
| 35 | 'media_attach_propose', |
| 36 | ]); |
| 37 | |
| 38 | const ADMIN_TOOLS = new Set([ |
| 39 | ...WRITE_TOOLS, |
| 40 | 'index', |
| 41 | 'export', |
| 42 | 'import', |
| 43 | 'import_url', |
| 44 | ]); |
| 45 | |
| 46 | const ROLE_TOOL_MAP = { |
| 47 | viewer: READ_TOOLS, |
| 48 | editor: WRITE_TOOLS, |
| 49 | /** Same tool surface as editor (incl. hub_create_proposal); bridge hosted-context may report role evaluator. */ |
| 50 | evaluator: WRITE_TOOLS, |
| 51 | admin: ADMIN_TOOLS, |
| 52 | }; |
| 53 | |
| 54 | /** Hosted MCP prompt IDs (Track B1 + B2 + B3 memory trio); each maps to canister / bridge routes like tools — no local vault files. */ |
| 55 | const HOSTED_PROMPT_IDS = new Set([ |
| 56 | 'daily-brief', |
| 57 | 'search-and-synthesize', |
| 58 | 'project-summary', |
| 59 | 'temporal-summary', |
| 60 | 'content-plan', |
| 61 | 'meeting-notes', |
| 62 | 'knowledge-gap', |
| 63 | 'causal-chain', |
| 64 | 'extract-entities', |
| 65 | 'write-from-capture', |
| 66 | 'memory-context', |
| 67 | 'memory-informed-search', |
| 68 | 'resume-session', |
| 69 | ]); |
| 70 | |
| 71 | /** Minimum role per prompt (`write-from-capture` implies vault write → editor). */ |
| 72 | const PROMPT_MIN_ROLE = /** @type {Record<string, 'viewer' | 'editor' | 'admin' | 'evaluator'>} */ ({ |
| 73 | 'daily-brief': 'viewer', |
| 74 | 'search-and-synthesize': 'viewer', |
| 75 | 'project-summary': 'viewer', |
| 76 | 'temporal-summary': 'viewer', |
| 77 | 'content-plan': 'viewer', |
| 78 | 'meeting-notes': 'viewer', |
| 79 | 'knowledge-gap': 'viewer', |
| 80 | 'causal-chain': 'viewer', |
| 81 | 'extract-entities': 'viewer', |
| 82 | 'write-from-capture': 'editor', |
| 83 | 'memory-context': 'viewer', |
| 84 | 'memory-informed-search': 'viewer', |
| 85 | 'resume-session': 'viewer', |
| 86 | }); |
| 87 | |
| 88 | /** evaluator ≥ editor for prompts; admin remains highest for future admin-only prompts. */ |
| 89 | const ROLE_RANK = { viewer: 0, editor: 1, evaluator: 2, admin: 3 }; |
| 90 | |
| 91 | /** |
| 92 | * Get the set of allowed tool names for a given role. |
| 93 | * @param {'viewer' | 'editor' | 'admin' | 'evaluator'} role |
| 94 | * @returns {Set<string>} |
| 95 | */ |
| 96 | export function allowedToolsForRole(role) { |
| 97 | return ROLE_TOOL_MAP[role] || READ_TOOLS; |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Check whether a specific tool is allowed for the given role. |
| 102 | * @param {string} toolName |
| 103 | * @param {'viewer' | 'editor' | 'admin' | 'evaluator'} role |
| 104 | * @returns {boolean} |
| 105 | */ |
| 106 | export function isToolAllowed(toolName, role) { |
| 107 | const allowed = allowedToolsForRole(role); |
| 108 | return allowed.has(toolName); |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Filter a list of tool definitions to only those allowed for the role. |
| 113 | * @param {{ name: string }[]} tools |
| 114 | * @param {'viewer' | 'editor' | 'admin' | 'evaluator'} role |
| 115 | * @returns {{ name: string }[]} |
| 116 | */ |
| 117 | export function filterToolsByRole(tools, role) { |
| 118 | const allowed = allowedToolsForRole(role); |
| 119 | return tools.filter((t) => allowed.has(t.name)); |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * Prompt names exposed for this role (subset of {@link HOSTED_PROMPT_IDS} when min role not met). |
| 124 | * @param {'viewer' | 'editor' | 'admin' | 'evaluator'} role |
| 125 | * @returns {Set<string>} |
| 126 | */ |
| 127 | export function allowedPromptsForRole(role) { |
| 128 | const rank = ROLE_RANK[role] ?? 0; |
| 129 | const out = new Set(); |
| 130 | for (const name of HOSTED_PROMPT_IDS) { |
| 131 | const min = PROMPT_MIN_ROLE[name] ?? 'viewer'; |
| 132 | if (rank >= ROLE_RANK[min]) out.add(name); |
| 133 | } |
| 134 | return out; |
| 135 | } |
| 136 | |
| 137 | /** |
| 138 | * @param {string} promptName |
| 139 | * @param {'viewer' | 'editor' | 'admin' | 'evaluator'} role |
| 140 | */ |
| 141 | export function isPromptAllowed(promptName, role) { |
| 142 | if (!HOSTED_PROMPT_IDS.has(promptName)) return false; |
| 143 | const min = PROMPT_MIN_ROLE[promptName] ?? 'viewer'; |
| 144 | return (ROLE_RANK[role] ?? 0) >= ROLE_RANK[min]; |
| 145 | } |
File History
4 commits
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf
mirror: GitHub Phase A durable MCP OAuth (#270)
Human
minor
⚠
10 days ago
sha256:d8c648b20a4d53b2673c5c082ee7edfa7b2fc9b11080832da1f38807b6bf940b
fix(7C-L1b): route hosted delegation proposals through cani…
Human
minor
⚠
30 days ago
sha256:2827ba9e7632a4b141c50caf1e8f7d77abbc3515be20e7465f2bccb0ac4edf91
fix: repair endpoint now sets has_active_subscription when …
Human
minor
⚠
49 days ago
sha256:6a102aafafdfe7e70a24f4e59740200f0ee713ce7915f1b53e9d4ba5ee8b4410
Initial Muse snapshot
Human
82 days ago