mcp-tool-acl.mjs
145 lines 4.0 KB
Raw
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