@samirosamsam/aim 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/.codex-plugin/plugin.json +24 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/bin/aim.js +392 -0
- package/hooks/hooks.json +17 -0
- package/package.json +40 -0
- package/skills/aim/SKILL.md +88 -0
- package/skills/aim/agents/openai.yaml +4 -0
- package/src/engine.js +685 -0
package/src/engine.js
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
const PROVIDER_ALIASES = {
|
|
2
|
+
notion: ["notion"],
|
|
3
|
+
github: ["github", "git hub"],
|
|
4
|
+
slack: ["slack"],
|
|
5
|
+
linear: ["linear"],
|
|
6
|
+
drive: ["google drive", "gdrive", "drive"],
|
|
7
|
+
gmail: ["gmail", "mail", "email"],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const ACTION_ALIASES = {
|
|
11
|
+
read: ["read", "lire", "voir", "consulter", "search", "chercher", "list", "lister"],
|
|
12
|
+
write: ["write", "ecrire", "edit", "modifier", "update", "create", "creer", "publish", "publier"],
|
|
13
|
+
delete: ["delete", "remove", "supprimer", "effacer"],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function createStore(seed = {}) {
|
|
17
|
+
return {
|
|
18
|
+
users: clone(seed.users ?? []),
|
|
19
|
+
agents: clone(seed.agents ?? []),
|
|
20
|
+
connections: clone(seed.connections ?? []),
|
|
21
|
+
grants: clone(seed.grants ?? []),
|
|
22
|
+
decisions: clone(seed.decisions ?? []),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createPolicy(seed = {}) {
|
|
27
|
+
return {
|
|
28
|
+
version: seed.version ?? 1,
|
|
29
|
+
grants: clone(seed.grants ?? []),
|
|
30
|
+
decisions: clone(seed.decisions ?? []),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function registerAgent(store, { ownerUserId, name, type = "custom" }) {
|
|
35
|
+
const owner = store.users.find((user) => user.id === ownerUserId);
|
|
36
|
+
if (!owner) throw new Error(`Unknown owner: ${ownerUserId}`);
|
|
37
|
+
const uid = `spiffe://local/agent/${slug(owner.email.split("@")[0])}/${slug(name)}`;
|
|
38
|
+
const agent = {
|
|
39
|
+
uid,
|
|
40
|
+
ownerUserId,
|
|
41
|
+
displayName: name,
|
|
42
|
+
agentType: type,
|
|
43
|
+
status: "active",
|
|
44
|
+
createdAt: now(),
|
|
45
|
+
lastSeenAt: null,
|
|
46
|
+
};
|
|
47
|
+
store.agents.push(agent);
|
|
48
|
+
return agent;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function connectProvider(store, { userId, provider, scopes = [], upstreamMcpUrl = "" }) {
|
|
52
|
+
const connection = {
|
|
53
|
+
id: `conn_${slug(provider)}_${store.connections.length + 1}`,
|
|
54
|
+
userId,
|
|
55
|
+
provider: slug(provider),
|
|
56
|
+
upstreamMcpUrl,
|
|
57
|
+
vaultRef: `vault://${slug(provider)}/${store.connections.length + 1}`,
|
|
58
|
+
scopesGranted: scopes,
|
|
59
|
+
createdAt: now(),
|
|
60
|
+
};
|
|
61
|
+
store.connections.push(connection);
|
|
62
|
+
return connection;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function compileGrantPrompt(prompt, context = {}) {
|
|
66
|
+
const normalized = normalize(prompt);
|
|
67
|
+
const providers = detectProviders(normalized);
|
|
68
|
+
const actions = detectActions(normalized);
|
|
69
|
+
const effect = detectEffect(normalized);
|
|
70
|
+
const resourcePattern = detectResource(prompt, normalized);
|
|
71
|
+
const toolPattern = detectToolPattern(normalized, actions, providers, resourcePattern);
|
|
72
|
+
const questions = [];
|
|
73
|
+
|
|
74
|
+
if (providers.includes("*")) questions.push("Choose which SaaS connection this rule applies to.");
|
|
75
|
+
if (resourcePattern === "*") questions.push("Specify the exact resource pattern before enforcing.");
|
|
76
|
+
|
|
77
|
+
const drafts = providers.flatMap((provider) => {
|
|
78
|
+
const matchingConnections = provider === "*"
|
|
79
|
+
? context.connections ?? []
|
|
80
|
+
: (context.connections ?? []).filter((connection) => connection.provider === provider);
|
|
81
|
+
const connectionIds = matchingConnections.length > 0 ? matchingConnections.map((c) => c.id) : [`${provider}:*`];
|
|
82
|
+
|
|
83
|
+
return connectionIds.flatMap((connectionId) =>
|
|
84
|
+
actions.map((access) => ({
|
|
85
|
+
id: `draft_${hash(`${prompt}:${connectionId}:${access}`)}`,
|
|
86
|
+
agentUid: context.agentUid ?? "",
|
|
87
|
+
connectionId,
|
|
88
|
+
provider,
|
|
89
|
+
toolPattern,
|
|
90
|
+
resourcePattern,
|
|
91
|
+
access: access === "delete" ? "write" : access,
|
|
92
|
+
effect,
|
|
93
|
+
expiresAt: null,
|
|
94
|
+
reason: reasonFor(effect, access, resourcePattern),
|
|
95
|
+
sourcePrompt: prompt,
|
|
96
|
+
confidence: confidenceFor(provider, resourcePattern),
|
|
97
|
+
})),
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return { drafts, questions };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function commitGrantDraft(store, draft, { agentUid, grantedBy }) {
|
|
105
|
+
const grant = materializeGrantDraft(draft, { agentUid, grantedBy });
|
|
106
|
+
store.grants = store.grants.filter((existing) => existing.id !== grant.id);
|
|
107
|
+
store.grants.push(grant);
|
|
108
|
+
return grant;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function addGrantDraft(policy, draft, { agentUid, grantedBy = "codex", connectionId } = {}) {
|
|
112
|
+
const grant = materializeGrantDraft(draft, { agentUid, grantedBy, connectionId });
|
|
113
|
+
policy.grants = (policy.grants ?? []).filter((existing) => existing.id !== grant.id);
|
|
114
|
+
policy.grants.push(grant);
|
|
115
|
+
return grant;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function deriveMcpCall({ agentUid = "", connectionId = "", provider = "", tool, args = {}, access, resource }) {
|
|
119
|
+
const normalizedProvider = slug(provider || providerFromConnectionId(connectionId));
|
|
120
|
+
const normalizedTool = normalizeToolName(tool);
|
|
121
|
+
const inferred = inferMcpCall(normalizedProvider, normalizedTool, args);
|
|
122
|
+
return {
|
|
123
|
+
agentUid,
|
|
124
|
+
connectionId: connectionId || `${normalizedProvider}:*`,
|
|
125
|
+
provider: normalizedProvider,
|
|
126
|
+
tool: normalizedTool,
|
|
127
|
+
resource: resource ?? inferred.resource,
|
|
128
|
+
access: access ?? inferred.access,
|
|
129
|
+
params: args,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function draftGrantFromCall(call, { effect = "allow", strictTool = false, reason } = {}) {
|
|
134
|
+
const access = call.access === "delete" ? "write" : call.access;
|
|
135
|
+
const resourcePattern = call.resource ?? "*";
|
|
136
|
+
return {
|
|
137
|
+
id: `draft_${hash(`${call.agentUid}:${call.connectionId}:${call.tool}:${resourcePattern}:${access}:${effect}`)}`,
|
|
138
|
+
agentUid: call.agentUid,
|
|
139
|
+
connectionId: call.connectionId,
|
|
140
|
+
provider: call.provider,
|
|
141
|
+
toolPattern: strictTool ? call.tool : "*",
|
|
142
|
+
resourcePattern,
|
|
143
|
+
access,
|
|
144
|
+
effect,
|
|
145
|
+
expiresAt: null,
|
|
146
|
+
reason: reason ?? reasonFor(effect, access, resourcePattern),
|
|
147
|
+
sourcePrompt: `mcp:${call.provider}:${call.tool}`,
|
|
148
|
+
confidence: confidenceFor(call.provider, resourcePattern),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function compileCodexRulePrompt(prompt, context = {}) {
|
|
153
|
+
const normalized = normalize(prompt);
|
|
154
|
+
const requestedEffect = detectEffect(normalized);
|
|
155
|
+
const effect = requestedEffect === "allow" ? "deny" : requestedEffect;
|
|
156
|
+
const agentUid = context.agentUid ?? "codex";
|
|
157
|
+
const drafts = [];
|
|
158
|
+
const questions = [];
|
|
159
|
+
|
|
160
|
+
for (const command of detectCommands(prompt, normalized)) {
|
|
161
|
+
drafts.push({
|
|
162
|
+
id: `draft_${hash(`${agentUid}:codex:local:Bash:bash/${command}:write:${effect}`)}`,
|
|
163
|
+
agentUid,
|
|
164
|
+
connectionId: "codex:local",
|
|
165
|
+
provider: "codex",
|
|
166
|
+
toolPattern: "Bash",
|
|
167
|
+
resourcePattern: `bash/${command}`,
|
|
168
|
+
access: inferAccess(command),
|
|
169
|
+
effect,
|
|
170
|
+
expiresAt: null,
|
|
171
|
+
reason: reasonFor(effect, inferAccess(command), `bash/${command}`),
|
|
172
|
+
sourcePrompt: prompt,
|
|
173
|
+
confidence: 0.9,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const file of detectFiles(prompt, normalized)) {
|
|
178
|
+
for (const [toolPattern, resourcePattern] of [
|
|
179
|
+
["apply_patch", `file/${file}`],
|
|
180
|
+
["Bash", `bash/*${file}*`],
|
|
181
|
+
]) {
|
|
182
|
+
drafts.push({
|
|
183
|
+
id: `draft_${hash(`${agentUid}:codex:local:${toolPattern}:${resourcePattern}:write:${effect}`)}`,
|
|
184
|
+
agentUid,
|
|
185
|
+
connectionId: "codex:local",
|
|
186
|
+
provider: "codex",
|
|
187
|
+
toolPattern,
|
|
188
|
+
resourcePattern,
|
|
189
|
+
access: "write",
|
|
190
|
+
effect,
|
|
191
|
+
expiresAt: null,
|
|
192
|
+
reason: reasonFor(effect, "write", resourcePattern),
|
|
193
|
+
sourcePrompt: prompt,
|
|
194
|
+
confidence: 0.85,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (normalized.includes("github") && normalized.includes("collaborator")) {
|
|
200
|
+
drafts.push({
|
|
201
|
+
id: `draft_${hash(`${agentUid}:github:*:*collaborator*:repo/*/collaborators/*:write:${effect}`)}`,
|
|
202
|
+
agentUid,
|
|
203
|
+
connectionId: "github:*",
|
|
204
|
+
provider: "github",
|
|
205
|
+
toolPattern: "*collaborator*",
|
|
206
|
+
resourcePattern: "repo/*/collaborators/*",
|
|
207
|
+
access: "write",
|
|
208
|
+
effect,
|
|
209
|
+
expiresAt: null,
|
|
210
|
+
reason: reasonFor(effect, "write", "repo/*/collaborators/*"),
|
|
211
|
+
sourcePrompt: prompt,
|
|
212
|
+
confidence: 0.8,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (drafts.length === 0) {
|
|
217
|
+
questions.push("Specify a Bash command, file path, or MCP provider/tool/resource to block.");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { drafts: uniqueDrafts(drafts), questions };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function deriveCodexHookCalls(event, { agentUid = "codex" } = {}) {
|
|
224
|
+
const toolName = event.tool_name ?? event.toolName ?? "";
|
|
225
|
+
const input = event.tool_input ?? event.toolInput ?? {};
|
|
226
|
+
|
|
227
|
+
if (toolName === "Bash") {
|
|
228
|
+
const command = cleanCommand(input.command ?? "");
|
|
229
|
+
return [{
|
|
230
|
+
agentUid,
|
|
231
|
+
connectionId: "codex:local",
|
|
232
|
+
provider: "codex",
|
|
233
|
+
tool: "Bash",
|
|
234
|
+
resource: `bash/${command}`,
|
|
235
|
+
access: inferAccess(command),
|
|
236
|
+
params: input,
|
|
237
|
+
}];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (toolName === "apply_patch") {
|
|
241
|
+
return touchedFiles(input.command ?? input.patch ?? "").map((file) => ({
|
|
242
|
+
agentUid,
|
|
243
|
+
connectionId: "codex:local",
|
|
244
|
+
provider: "codex",
|
|
245
|
+
tool: "apply_patch",
|
|
246
|
+
resource: `file/${file.path}`,
|
|
247
|
+
access: file.deleted ? "delete" : "write",
|
|
248
|
+
params: input,
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (toolName.startsWith("mcp__")) {
|
|
253
|
+
const provider = providerFromMcpTool(toolName);
|
|
254
|
+
return [deriveMcpCall({
|
|
255
|
+
agentUid,
|
|
256
|
+
connectionId: `${provider}:codex`,
|
|
257
|
+
provider,
|
|
258
|
+
tool: toolName,
|
|
259
|
+
args: input,
|
|
260
|
+
})];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return [{
|
|
264
|
+
agentUid,
|
|
265
|
+
connectionId: "codex:local",
|
|
266
|
+
provider: "codex",
|
|
267
|
+
tool: toolName,
|
|
268
|
+
resource: `tool/${normalizeToolName(toolName)}`,
|
|
269
|
+
access: inferAccess(toolName),
|
|
270
|
+
params: input,
|
|
271
|
+
}];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function evaluateCodexHook(policy, event, options = {}) {
|
|
275
|
+
const calls = deriveCodexHookCalls(event, options);
|
|
276
|
+
for (const call of calls) {
|
|
277
|
+
const decision = evaluateGuardCall(policy, call);
|
|
278
|
+
if (decision.decision !== "allow") return { ...decision, call };
|
|
279
|
+
}
|
|
280
|
+
return { decision: "allow", reason: "no-matching-block", calls };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function evaluateGuardCall(policy, call) {
|
|
284
|
+
const started = Date.now();
|
|
285
|
+
const matches = activeGrants(policy.grants ?? []).filter((grant) => grantMatches(grant, call));
|
|
286
|
+
const deny = matches.find((grant) => grant.effect === "deny");
|
|
287
|
+
const approval = matches.find((grant) => grant.effect === "approval");
|
|
288
|
+
const match = deny ?? approval;
|
|
289
|
+
|
|
290
|
+
if (!match) {
|
|
291
|
+
return {
|
|
292
|
+
decision: "allow",
|
|
293
|
+
reason: "no-matching-block",
|
|
294
|
+
matchedGrantId: null,
|
|
295
|
+
latencyMs: Date.now() - started,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
decision: "deny",
|
|
301
|
+
reason: match.effect === "approval" ? `Approval required: ${match.reason}` : match.reason,
|
|
302
|
+
matchedGrantId: match.id,
|
|
303
|
+
latencyMs: Date.now() - started,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function evaluateToolCall(store, call) {
|
|
308
|
+
const started = Date.now();
|
|
309
|
+
const agent = store.agents.find((candidate) => candidate.uid === call.agentUid);
|
|
310
|
+
const connection = store.connections.find((candidate) => candidate.id === call.connectionId);
|
|
311
|
+
let decision = "deny";
|
|
312
|
+
let reason = "default-deny";
|
|
313
|
+
let matchedGrantId = null;
|
|
314
|
+
|
|
315
|
+
if (!agent) {
|
|
316
|
+
reason = "unknown-agent";
|
|
317
|
+
} else if (agent.status !== "active") {
|
|
318
|
+
reason = `agent-${agent.status}`;
|
|
319
|
+
} else if (!connection) {
|
|
320
|
+
reason = "unknown-connection";
|
|
321
|
+
} else {
|
|
322
|
+
agent.lastSeenAt = now();
|
|
323
|
+
const matches = activeGrants(store.grants).filter((grant) => grantMatches(grant, call));
|
|
324
|
+
const deny = matches.find((grant) => grant.effect === "deny");
|
|
325
|
+
const approval = matches.find((grant) => grant.effect === "approval");
|
|
326
|
+
const allow = matches.find((grant) => grant.effect === "allow");
|
|
327
|
+
|
|
328
|
+
if (deny) {
|
|
329
|
+
matchedGrantId = deny.id;
|
|
330
|
+
reason = deny.reason;
|
|
331
|
+
} else if (approval) {
|
|
332
|
+
decision = "approval_required";
|
|
333
|
+
matchedGrantId = approval.id;
|
|
334
|
+
reason = approval.reason;
|
|
335
|
+
} else if (allow) {
|
|
336
|
+
decision = "allow";
|
|
337
|
+
matchedGrantId = allow.id;
|
|
338
|
+
reason = allow.reason;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const row = {
|
|
343
|
+
id: `decision_${store.decisions.length + 1}`,
|
|
344
|
+
ts: now(),
|
|
345
|
+
agentUid: call.agentUid,
|
|
346
|
+
ownerUserId: agent?.ownerUserId ?? null,
|
|
347
|
+
connectionId: call.connectionId,
|
|
348
|
+
toolCalled: call.tool,
|
|
349
|
+
resource: call.resource,
|
|
350
|
+
access: call.access,
|
|
351
|
+
paramsDigest: hash(JSON.stringify(call.params ?? {})),
|
|
352
|
+
decision,
|
|
353
|
+
reason,
|
|
354
|
+
matchedGrantId,
|
|
355
|
+
latencyMs: Date.now() - started,
|
|
356
|
+
};
|
|
357
|
+
store.decisions.unshift(row);
|
|
358
|
+
return row;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function evaluatePolicyCall(policy, call) {
|
|
362
|
+
const started = Date.now();
|
|
363
|
+
let decision = "deny";
|
|
364
|
+
let reason = "default-deny";
|
|
365
|
+
let matchedGrantId = null;
|
|
366
|
+
const matches = activeGrants(policy.grants ?? []).filter((grant) => grantMatches(grant, call));
|
|
367
|
+
const deny = matches.find((grant) => grant.effect === "deny");
|
|
368
|
+
const approval = matches.find((grant) => grant.effect === "approval");
|
|
369
|
+
const allow = matches.find((grant) => grant.effect === "allow");
|
|
370
|
+
|
|
371
|
+
if (deny) {
|
|
372
|
+
matchedGrantId = deny.id;
|
|
373
|
+
reason = deny.reason;
|
|
374
|
+
} else if (approval) {
|
|
375
|
+
decision = "approval_required";
|
|
376
|
+
matchedGrantId = approval.id;
|
|
377
|
+
reason = approval.reason;
|
|
378
|
+
} else if (allow) {
|
|
379
|
+
decision = "allow";
|
|
380
|
+
matchedGrantId = allow.id;
|
|
381
|
+
reason = allow.reason;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const row = {
|
|
385
|
+
id: `decision_${(policy.decisions ?? []).length + 1}`,
|
|
386
|
+
ts: now(),
|
|
387
|
+
agentUid: call.agentUid,
|
|
388
|
+
ownerUserId: null,
|
|
389
|
+
connectionId: call.connectionId,
|
|
390
|
+
toolCalled: call.tool,
|
|
391
|
+
resource: call.resource,
|
|
392
|
+
access: call.access,
|
|
393
|
+
paramsDigest: hash(JSON.stringify(call.params ?? {})),
|
|
394
|
+
decision,
|
|
395
|
+
reason,
|
|
396
|
+
matchedGrantId,
|
|
397
|
+
latencyMs: Date.now() - started,
|
|
398
|
+
};
|
|
399
|
+
policy.decisions ??= [];
|
|
400
|
+
policy.decisions.unshift(row);
|
|
401
|
+
return row;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function summarizeAgentAccess(store, agentUid) {
|
|
405
|
+
return store.grants
|
|
406
|
+
.filter((grant) => grant.agentUid === agentUid)
|
|
407
|
+
.map((grant) => {
|
|
408
|
+
const connection = store.connections.find((candidate) => candidate.id === grant.connectionId);
|
|
409
|
+
return {
|
|
410
|
+
...grant,
|
|
411
|
+
provider: connection?.provider ?? "unknown",
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function activeGrants(grants) {
|
|
417
|
+
const current = Date.now();
|
|
418
|
+
return grants.filter((grant) => !grant.expiresAt || Date.parse(grant.expiresAt) > current);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function grantMatches(grant, call) {
|
|
422
|
+
return (
|
|
423
|
+
grant.agentUid === call.agentUid &&
|
|
424
|
+
wildcard(grant.connectionId, call.connectionId) &&
|
|
425
|
+
wildcard(grant.toolPattern, call.tool) &&
|
|
426
|
+
wildcard(grant.resourcePattern, call.resource) &&
|
|
427
|
+
(grant.access === call.access || grant.access === "write" && call.access === "delete")
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function materializeGrantDraft(draft, { agentUid, grantedBy, connectionId }) {
|
|
432
|
+
const resolvedAgentUid = agentUid ?? draft.agentUid;
|
|
433
|
+
const resolvedConnectionId = connectionId ?? draft.connectionId;
|
|
434
|
+
if (!resolvedAgentUid) throw new Error("Grant requires an agent UID.");
|
|
435
|
+
if (!resolvedConnectionId) throw new Error("Grant requires a connection ID or pattern.");
|
|
436
|
+
return {
|
|
437
|
+
id: `grant_${hash(`${resolvedAgentUid}:${resolvedConnectionId}:${draft.toolPattern}:${draft.resourcePattern}:${draft.access}:${draft.effect}`)}`,
|
|
438
|
+
agentUid: resolvedAgentUid,
|
|
439
|
+
connectionId: resolvedConnectionId,
|
|
440
|
+
provider: draft.provider ?? providerFromConnectionId(resolvedConnectionId),
|
|
441
|
+
toolPattern: draft.toolPattern,
|
|
442
|
+
resourcePattern: draft.resourcePattern,
|
|
443
|
+
access: draft.access,
|
|
444
|
+
effect: draft.effect,
|
|
445
|
+
grantedBy,
|
|
446
|
+
expiresAt: draft.expiresAt,
|
|
447
|
+
reason: draft.reason,
|
|
448
|
+
sourcePrompt: draft.sourcePrompt,
|
|
449
|
+
createdAt: now(),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function detectProviders(normalized) {
|
|
454
|
+
const found = Object.entries(PROVIDER_ALIASES)
|
|
455
|
+
.filter(([, aliases]) => aliases.some((alias) => normalized.includes(alias)))
|
|
456
|
+
.map(([provider]) => provider);
|
|
457
|
+
return found.length > 0 ? found : ["*"];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function detectActions(normalized) {
|
|
461
|
+
const found = Object.entries(ACTION_ALIASES)
|
|
462
|
+
.filter(([, aliases]) => aliases.some((alias) => normalized.includes(alias)))
|
|
463
|
+
.map(([action]) => action);
|
|
464
|
+
if (found.includes("delete")) return ["delete"];
|
|
465
|
+
return found.length > 0 ? found : ["read"];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function detectEffect(normalized) {
|
|
469
|
+
if (/\b(approval|approve|validation|validate|valider|escalate|escalade|ask|demande)\b/.test(normalized)) {
|
|
470
|
+
return "approval";
|
|
471
|
+
}
|
|
472
|
+
if (/\b(block|bloque|bloquer|deny|interdit|interdire|jamais|never|pas acces|pas accès|no access|empeche|empecher|empêche|empêcher|prevent)\b/.test(normalized)) {
|
|
473
|
+
return "deny";
|
|
474
|
+
}
|
|
475
|
+
return "allow";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function detectToolPattern(normalized, actions, providers, resourcePattern) {
|
|
479
|
+
if (normalized.includes("issue")) return "issues.*";
|
|
480
|
+
if (normalized.includes("page") || providers.includes("notion") && resourcePattern.startsWith("workspace/")) return "pages.*";
|
|
481
|
+
if (normalized.includes("channel") || resourcePattern.startsWith("channel/")) return "channels.*";
|
|
482
|
+
if (normalized.includes("search") || normalized.includes("chercher")) return "search";
|
|
483
|
+
if (actions.includes("delete")) return "*.delete";
|
|
484
|
+
return "*";
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function detectResource(original, normalized) {
|
|
488
|
+
const quoted = original.match(/["'`](.+?)["'`]/);
|
|
489
|
+
if (quoted?.[1]) return quoted[1].trim();
|
|
490
|
+
const channel = original.match(/#([\w-]+)/);
|
|
491
|
+
if (channel?.[1]) return `channel/${channel[1]}`;
|
|
492
|
+
const repo = normalized.match(/\b([\w.-]+\/[\w.-]+)\b/);
|
|
493
|
+
if (repo?.[1]) return `repo/${repo[1]}`;
|
|
494
|
+
for (const marker of ["legal", "finance", "financial", "hr", "rh", "private", "prive", "privé"]) {
|
|
495
|
+
if (normalized.includes(marker)) return `sensitive/${marker}`;
|
|
496
|
+
}
|
|
497
|
+
return "*";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function inferMcpCall(provider, tool, args) {
|
|
501
|
+
if (provider === "github") return inferGithubCall(tool, args);
|
|
502
|
+
if (provider === "notion") return inferNotionCall(tool, args);
|
|
503
|
+
if (provider === "slack") return inferSlackCall(tool, args);
|
|
504
|
+
if (provider === "linear") return inferLinearCall(tool, args);
|
|
505
|
+
return {
|
|
506
|
+
access: inferAccess(tool),
|
|
507
|
+
resource: inferGenericResource(provider, tool, args),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function inferGithubCall(tool, args) {
|
|
512
|
+
if (tool.includes("collaborator")) return { access: inferAccess(tool), resource: githubRepo(args, "/collaborators/*") };
|
|
513
|
+
if (tool.includes("pull_request") || tool.includes("pull_requests")) {
|
|
514
|
+
const pull = cleanPart(args.pullNumber ?? args.pull_number);
|
|
515
|
+
return { access: inferAccess(tool), resource: githubRepo(args, `/pull_requests/${pull || "*"}`) };
|
|
516
|
+
}
|
|
517
|
+
if (tool.includes("branch")) {
|
|
518
|
+
const branch = cleanPart(args.branch ?? args.from_branch);
|
|
519
|
+
const suffix = branch ? `/branches/${branch}` : "/branches/*";
|
|
520
|
+
return { access: inferAccess(tool), resource: githubRepo(args, suffix) };
|
|
521
|
+
}
|
|
522
|
+
if (tool.includes("issue")) {
|
|
523
|
+
const issue = cleanPart(args.issue_number ?? args.issueNumber);
|
|
524
|
+
return { access: inferAccess(tool), resource: githubRepo(args, `/issues/${issue || "*"}`) };
|
|
525
|
+
}
|
|
526
|
+
if (tool === "push_files" || tool === "create_or_update_file" || tool === "delete_file") {
|
|
527
|
+
const file = cleanPath(args.path);
|
|
528
|
+
return { access: tool === "delete_file" ? "delete" : "write", resource: githubRepo(args, `/files/${file || "*"}`) };
|
|
529
|
+
}
|
|
530
|
+
return { access: inferAccess(tool), resource: githubRepo(args) };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function inferNotionCall(tool, args) {
|
|
534
|
+
const page = cleanPart(args.page_id ?? args.pageId ?? args.id);
|
|
535
|
+
const database = cleanPart(args.database_id ?? args.databaseId);
|
|
536
|
+
if (page) return { access: inferAccess(tool), resource: `notion/pages/${page}` };
|
|
537
|
+
if (database) return { access: inferAccess(tool), resource: `notion/databases/${database}` };
|
|
538
|
+
return { access: inferAccess(tool), resource: args.resource ? cleanPath(args.resource) : "notion/*" };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function inferSlackCall(tool, args) {
|
|
542
|
+
const channel = cleanPart(args.channel ?? args.channel_id ?? args.channelId);
|
|
543
|
+
return { access: inferAccess(tool), resource: channel ? `slack/channels/${channel}` : "slack/*" };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function inferLinearCall(tool, args) {
|
|
547
|
+
const issue = cleanPart(args.issue_id ?? args.issueId ?? args.id);
|
|
548
|
+
const team = cleanPart(args.team_id ?? args.teamId);
|
|
549
|
+
if (issue) return { access: inferAccess(tool), resource: `linear/issues/${issue}` };
|
|
550
|
+
if (team) return { access: inferAccess(tool), resource: `linear/teams/${team}` };
|
|
551
|
+
return { access: inferAccess(tool), resource: "linear/*" };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function inferGenericResource(provider, tool, args) {
|
|
555
|
+
if (args.resource) return cleanPath(args.resource);
|
|
556
|
+
if (args.owner && args.repo) return githubRepo(args);
|
|
557
|
+
if (args.channel) return `channel/${cleanPart(args.channel)}`;
|
|
558
|
+
return `${provider}/${tool}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function inferAccess(tool) {
|
|
562
|
+
if (/delete|remove|trash|archive/.test(tool)) return "delete";
|
|
563
|
+
if (/add|create|update|edit|write|push|merge|send|post|publish|invite|modify|modifier|touch|mv|cp|rm|>|tee/.test(tool)) return "write";
|
|
564
|
+
return "read";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function githubRepo(args, suffix = "") {
|
|
568
|
+
const owner = cleanPart(args.owner ?? args.repository_owner);
|
|
569
|
+
const repo = cleanPart(args.repo ?? args.repository);
|
|
570
|
+
return owner && repo ? `repo/${owner}/${repo}${suffix}` : "*";
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function reasonFor(effect, access, resourcePattern) {
|
|
574
|
+
if (effect === "deny") return `Block ${access} on ${resourcePattern}.`;
|
|
575
|
+
if (effect === "approval") return `Require human approval for ${access} on ${resourcePattern}.`;
|
|
576
|
+
return `Allow ${access} on ${resourcePattern}.`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function confidenceFor(provider, resourcePattern) {
|
|
580
|
+
let score = 0.55;
|
|
581
|
+
if (provider !== "*") score += 0.2;
|
|
582
|
+
if (resourcePattern !== "*") score += 0.2;
|
|
583
|
+
return Math.min(score, 0.95);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function wildcard(pattern, value) {
|
|
587
|
+
if (pattern === "*") return true;
|
|
588
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
589
|
+
return new RegExp(`^${escaped}$`, "i").test(value);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function providerFromConnectionId(connectionId) {
|
|
593
|
+
return String(connectionId).split(/[:_]/)[0] || "unknown";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function providerFromMcpTool(toolName) {
|
|
597
|
+
return cleanPart(String(toolName).split("__")[1] ?? providerFromConnectionId(toolName));
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function normalizeToolName(tool) {
|
|
601
|
+
return normalize(tool)
|
|
602
|
+
.replace(/^mcp__[^_]+__/, "")
|
|
603
|
+
.replace(/^mcp__[^.]+\./, "")
|
|
604
|
+
.replace(/[^a-z0-9_.-]+/g, "_");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function detectCommands(original, normalized) {
|
|
608
|
+
const commands = [];
|
|
609
|
+
if (normalized.includes("git push")) commands.push("git push*");
|
|
610
|
+
for (const match of original.matchAll(/[`'"]([^`'"]+)[`'"]/g)) {
|
|
611
|
+
const command = cleanCommand(match[1]);
|
|
612
|
+
if (/^(git|rm|mv|cp|npm|pnpm|bun|yarn|curl|ssh)\b/.test(command)) commands.push(`${command}*`);
|
|
613
|
+
}
|
|
614
|
+
return [...new Set(commands)];
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function detectFiles(original, normalized) {
|
|
618
|
+
const files = [];
|
|
619
|
+
for (const match of original.matchAll(/(?:^|\s)([.\w/-]+\.[a-z0-9]+)(?=\s|$|[.,;:!?])/gi)) {
|
|
620
|
+
files.push(cleanPath(match[1]));
|
|
621
|
+
}
|
|
622
|
+
if (normalized.includes("package.json")) files.push("package.json");
|
|
623
|
+
if (normalized.includes(".env")) files.push(".env");
|
|
624
|
+
return [...new Set(files.filter(Boolean))];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function touchedFiles(patch) {
|
|
628
|
+
const files = [];
|
|
629
|
+
for (const match of String(patch).matchAll(/^\*\*\* (Update|Add|Delete) File: (.+)$/gm)) {
|
|
630
|
+
files.push({ path: cleanPath(match[2]), deleted: match[1] === "Delete" });
|
|
631
|
+
}
|
|
632
|
+
return files;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function cleanCommand(command) {
|
|
636
|
+
return normalize(command).replace(/\s+/g, " ");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function uniqueDrafts(drafts) {
|
|
640
|
+
const seen = new Set();
|
|
641
|
+
return drafts.filter((draft) => {
|
|
642
|
+
const key = `${draft.connectionId}:${draft.toolPattern}:${draft.resourcePattern}:${draft.access}:${draft.effect}`;
|
|
643
|
+
if (seen.has(key)) return false;
|
|
644
|
+
seen.add(key);
|
|
645
|
+
return true;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function cleanPart(value) {
|
|
650
|
+
if (value === undefined || value === null || value === "") return "";
|
|
651
|
+
return normalize(value).replace(/[^a-z0-9_.-]+/g, "-").replace(/(^-|-$)/g, "");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function cleanPath(value) {
|
|
655
|
+
return String(value ?? "")
|
|
656
|
+
.split("/")
|
|
657
|
+
.map(cleanPart)
|
|
658
|
+
.filter(Boolean)
|
|
659
|
+
.join("/");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function slug(value) {
|
|
663
|
+
return normalize(value).replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "unknown";
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function normalize(value) {
|
|
667
|
+
return String(value).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function hash(value) {
|
|
671
|
+
let h = 2166136261;
|
|
672
|
+
for (const char of String(value)) {
|
|
673
|
+
h ^= char.charCodeAt(0);
|
|
674
|
+
h = Math.imul(h, 16777619);
|
|
675
|
+
}
|
|
676
|
+
return (h >>> 0).toString(16);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function now() {
|
|
680
|
+
return new Date().toISOString();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function clone(value) {
|
|
684
|
+
return JSON.parse(JSON.stringify(value));
|
|
685
|
+
}
|