@muxoai/platform 1.3.3 → 1.4.1
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 +2 -2
- package/.claude-plugin/marketplace.json +2 -2
- package/ARCHITECTURE.md +16 -6
- package/README.md +7 -5
- package/dist/cli.js +393 -13
- package/dist/composio-catalog.d.ts +58 -0
- package/dist/composio-catalog.js +474 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +16 -0
- package/dist/durable-workflow.d.ts +45 -0
- package/dist/durable-workflow.js +90 -0
- package/dist/engine.d.ts +276 -5
- package/dist/engine.js +1261 -71
- package/dist/mcp-schema.d.ts +125 -0
- package/dist/mcp-schema.js +1076 -0
- package/dist/mcp.d.ts +2 -30
- package/dist/mcp.js +505 -539
- package/dist/memory-graph.d.ts +21 -0
- package/dist/memory-graph.js +173 -0
- package/dist/nl.js +155 -0
- package/dist/oauth.d.ts +32 -0
- package/dist/oauth.js +134 -0
- package/dist/policy.d.ts +75 -1
- package/dist/policy.js +1119 -0
- package/dist/providers/composio-instant.js +225 -5
- package/dist/providers/composio-sdk.d.ts +68 -5
- package/dist/providers/composio-sdk.js +683 -78
- package/dist/providers/microsoft-governance.d.ts +28 -1
- package/dist/providers/microsoft-governance.js +224 -11
- package/dist/providers/stripe-billing.d.ts +42 -0
- package/dist/providers/stripe-billing.js +230 -1
- package/dist/providers/stripe-projects.d.ts +451 -3
- package/dist/providers/stripe-projects.js +2106 -87
- package/dist/providers/supermemory.d.ts +74 -1
- package/dist/providers/supermemory.js +426 -0
- package/dist/provisioning-managed.d.ts +50 -0
- package/dist/provisioning-managed.js +155 -0
- package/dist/sdk.d.ts +538 -8
- package/dist/sdk.js +377 -10
- package/dist/server.js +1841 -232
- package/dist/store.d.ts +113 -6
- package/dist/store.js +675 -52
- package/dist/types.d.ts +212 -5
- package/dist/ui/console-html.d.ts +10 -1
- package/dist/ui/console-html.js +1188 -462
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/plugins/muxo/.claude-plugin/plugin.json +1 -1
- package/plugins/muxo/.codex-plugin/plugin.json +1 -1
- package/plugins/muxo/plugin.json +2 -2
- package/plugins/muxo/skills/muxo/SKILL.md +42 -1
- package/plugins/muxo/skills/muxo/references/mcp.md +3 -2
- package/plugins/muxo/skills/setup/SKILL.md +8 -0
package/dist/policy.js
CHANGED
|
@@ -6,8 +6,108 @@ const RISK_LEVELS = [
|
|
|
6
6
|
"provisioning",
|
|
7
7
|
"destructive",
|
|
8
8
|
];
|
|
9
|
+
const LANGUAGE_MODES = [
|
|
10
|
+
"non_technical",
|
|
11
|
+
"technical",
|
|
12
|
+
"hybrid",
|
|
13
|
+
];
|
|
14
|
+
const PARAMETER_RULE_OPERATORS = [
|
|
15
|
+
"equals",
|
|
16
|
+
"must_equal",
|
|
17
|
+
"not_equals",
|
|
18
|
+
"contains",
|
|
19
|
+
"not_contains",
|
|
20
|
+
"starts_with",
|
|
21
|
+
"must_start_with",
|
|
22
|
+
"ends_with",
|
|
23
|
+
"must_end_with",
|
|
24
|
+
"domain_in",
|
|
25
|
+
"domain_not_in",
|
|
26
|
+
"max_number",
|
|
27
|
+
"min_number",
|
|
28
|
+
"gt",
|
|
29
|
+
"gte",
|
|
30
|
+
"lt",
|
|
31
|
+
"lte",
|
|
32
|
+
"in",
|
|
33
|
+
"not_in",
|
|
34
|
+
"regex",
|
|
35
|
+
];
|
|
36
|
+
export function sanitizeParameterPolicyRule(r, idx = 0) {
|
|
37
|
+
const rawAction = String(r.action || "block").toLowerCase();
|
|
38
|
+
const action = rawAction === "require_approval" || rawAction === "escalate"
|
|
39
|
+
? "require_approval"
|
|
40
|
+
: rawAction === "auto_approve"
|
|
41
|
+
? "auto_approve"
|
|
42
|
+
: rawAction === "allow"
|
|
43
|
+
? "allow"
|
|
44
|
+
: "block";
|
|
45
|
+
const rawElse = r.elseAction ? String(r.elseAction).toLowerCase() : "";
|
|
46
|
+
const elseAction = rawElse === "allow"
|
|
47
|
+
? "allow"
|
|
48
|
+
: rawElse === "auto_approve"
|
|
49
|
+
? "auto_approve"
|
|
50
|
+
: rawElse === "require_approval" || rawElse === "escalate"
|
|
51
|
+
? "require_approval"
|
|
52
|
+
: rawElse === "block"
|
|
53
|
+
? "block"
|
|
54
|
+
: undefined;
|
|
55
|
+
const rawOp = String(r.operator || "equals").toLowerCase();
|
|
56
|
+
const operator = PARAMETER_RULE_OPERATORS.includes(rawOp)
|
|
57
|
+
? rawOp
|
|
58
|
+
: "equals";
|
|
59
|
+
const rawUnit = r.unit ? String(r.unit).toLowerCase() : undefined;
|
|
60
|
+
const unit = rawUnit === "usd" || rawUnit === "cents" || rawUnit === "credits" || rawUnit === "raw"
|
|
61
|
+
? rawUnit
|
|
62
|
+
: undefined;
|
|
63
|
+
const rawSource = r.source ? String(r.source).toLowerCase() : undefined;
|
|
64
|
+
const source = rawSource === "deterministic" ||
|
|
65
|
+
rawSource === "llm" ||
|
|
66
|
+
rawSource === "mcp_harness" ||
|
|
67
|
+
rawSource === "user"
|
|
68
|
+
? rawSource
|
|
69
|
+
: undefined;
|
|
70
|
+
const parameter = String(r.parameter || "").trim();
|
|
71
|
+
return {
|
|
72
|
+
id: String(r.id || `prule_${idx + 1}`),
|
|
73
|
+
toolPattern: String(r.toolPattern || "*"),
|
|
74
|
+
parameter,
|
|
75
|
+
operator,
|
|
76
|
+
value: r.value ?? "",
|
|
77
|
+
...(unit ? { unit } : {}),
|
|
78
|
+
action,
|
|
79
|
+
...(elseAction ? { elseAction } : {}),
|
|
80
|
+
description: String(r.description ||
|
|
81
|
+
`Parameter rule on ${parameter}: ${operator} ${JSON.stringify(r.value)}${elseAction ? ` (else ${elseAction})` : ""}`),
|
|
82
|
+
...(source ? { source } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
9
85
|
export class PolicyValidationError extends Error {
|
|
10
86
|
}
|
|
87
|
+
export function formatHumanFriendlyExplanation(params) {
|
|
88
|
+
const mode = params.languageMode ||
|
|
89
|
+
params.policy?.languageMode ||
|
|
90
|
+
"hybrid";
|
|
91
|
+
const customInstructions = Array.isArray(params.policy?.customInstructions)
|
|
92
|
+
? params.policy.customInstructions.filter(Boolean)
|
|
93
|
+
: [];
|
|
94
|
+
const customNote = customInstructions.length > 0
|
|
95
|
+
? ` [Custom instructions active: ${customInstructions.join("; ")}]`
|
|
96
|
+
: "";
|
|
97
|
+
const preferred = mode === "non_technical"
|
|
98
|
+
? `${params.nonTechnical}${customNote}`
|
|
99
|
+
: mode === "technical"
|
|
100
|
+
? `${params.technical}${customNote}`
|
|
101
|
+
: `${params.nonTechnical}\n\nTechnical detail: ${params.technical}${customNote}`;
|
|
102
|
+
return {
|
|
103
|
+
headline: params.headline,
|
|
104
|
+
preferred,
|
|
105
|
+
nonTechnical: params.nonTechnical,
|
|
106
|
+
technical: params.technical,
|
|
107
|
+
languageMode: mode,
|
|
108
|
+
...(customInstructions.length > 0 ? { customInstructions } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
11
111
|
/**
|
|
12
112
|
* Validates a client-supplied policy patch. Kill-switch fields are rejected so
|
|
13
113
|
* that pausing and resuming always goes through /api/v1/governance/kill-switch,
|
|
@@ -66,8 +166,1027 @@ export function sanitizePolicyPatch(body) {
|
|
|
66
166
|
...new Set(input.blockedToolSlugs.map((s) => s.trim())),
|
|
67
167
|
];
|
|
68
168
|
}
|
|
169
|
+
if (input.parameterRules !== undefined) {
|
|
170
|
+
if (!Array.isArray(input.parameterRules)) {
|
|
171
|
+
throw new PolicyValidationError("parameterRules must be an array of parameter policy rules");
|
|
172
|
+
}
|
|
173
|
+
patch.parameterRules = input.parameterRules
|
|
174
|
+
.filter((r) => r && typeof r === "object" && typeof r.parameter === "string")
|
|
175
|
+
.map((r, idx) => sanitizeParameterPolicyRule(r, idx))
|
|
176
|
+
.slice(0, 50);
|
|
177
|
+
}
|
|
178
|
+
if (input.naturalLanguageRules !== undefined) {
|
|
179
|
+
if (!Array.isArray(input.naturalLanguageRules) ||
|
|
180
|
+
!input.naturalLanguageRules.every((r) => typeof r === "string")) {
|
|
181
|
+
throw new PolicyValidationError("naturalLanguageRules must be a list of rule strings");
|
|
182
|
+
}
|
|
183
|
+
patch.naturalLanguageRules = [
|
|
184
|
+
...new Set(input.naturalLanguageRules
|
|
185
|
+
.map((r) => r.trim())
|
|
186
|
+
.filter((r) => r.length > 0 && r.length <= 280)),
|
|
187
|
+
].slice(0, 50);
|
|
188
|
+
}
|
|
189
|
+
if (input.languageMode !== undefined) {
|
|
190
|
+
if (!LANGUAGE_MODES.includes(input.languageMode)) {
|
|
191
|
+
throw new PolicyValidationError(`languageMode must be one of: ${LANGUAGE_MODES.join(", ")}`);
|
|
192
|
+
}
|
|
193
|
+
patch.languageMode = input.languageMode;
|
|
194
|
+
}
|
|
195
|
+
if (input.customInstructions !== undefined) {
|
|
196
|
+
if (!Array.isArray(input.customInstructions) ||
|
|
197
|
+
!input.customInstructions.every((r) => typeof r === "string")) {
|
|
198
|
+
throw new PolicyValidationError("customInstructions must be a list of instruction strings");
|
|
199
|
+
}
|
|
200
|
+
patch.customInstructions = [
|
|
201
|
+
...new Set(input.customInstructions
|
|
202
|
+
.map((r) => r.trim())
|
|
203
|
+
.filter((r) => r.length > 0 && r.length <= 280)),
|
|
204
|
+
].slice(0, 25);
|
|
205
|
+
}
|
|
206
|
+
const inlineRule = typeof input.naturalLanguageRule === "string"
|
|
207
|
+
? input.naturalLanguageRule.trim()
|
|
208
|
+
: typeof input.rule === "string"
|
|
209
|
+
? input.rule.trim()
|
|
210
|
+
: typeof input.prompt === "string"
|
|
211
|
+
? input.prompt.trim()
|
|
212
|
+
: typeof input.instruction === "string"
|
|
213
|
+
? input.instruction.trim()
|
|
214
|
+
: "";
|
|
215
|
+
if (inlineRule) {
|
|
216
|
+
const parsed = parseNaturalLanguageGuardrail(inlineRule);
|
|
217
|
+
Object.assign(patch, parsed.patch);
|
|
218
|
+
}
|
|
69
219
|
if (Object.keys(patch).length === 0) {
|
|
70
220
|
throw new PolicyValidationError("No supported policy fields in request");
|
|
71
221
|
}
|
|
72
222
|
return patch;
|
|
73
223
|
}
|
|
224
|
+
export const DEFAULT_WORKSPACE_POLICY = {
|
|
225
|
+
mode: "enforce",
|
|
226
|
+
requireApprovalFor: [
|
|
227
|
+
"write",
|
|
228
|
+
"admin",
|
|
229
|
+
"financial",
|
|
230
|
+
"provisioning",
|
|
231
|
+
"destructive",
|
|
232
|
+
],
|
|
233
|
+
allowInstantWallet: true,
|
|
234
|
+
allowProvisioning: true,
|
|
235
|
+
maxCreditsPerRun: 50,
|
|
236
|
+
blockedToolSlugs: [],
|
|
237
|
+
executionRing: 1,
|
|
238
|
+
killSwitchActive: false,
|
|
239
|
+
killSwitchReason: null,
|
|
240
|
+
blockDestructiveSql: true,
|
|
241
|
+
naturalLanguageRules: [],
|
|
242
|
+
languageMode: "hybrid",
|
|
243
|
+
customInstructions: [],
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* Compiles plain-English user guardrails (e.g. "Hey Muxo, don't go over 25 credits",
|
|
247
|
+
* "Only allow sending emails to @muxo.ai", or "Require approval for Stripe refunds over $50,
|
|
248
|
+
* auto-approve under $50") and/or structured MCP harness parameter rules into enforced
|
|
249
|
+
* GovernancePolicy fields while keeping human-readable rule descriptions.
|
|
250
|
+
*/
|
|
251
|
+
export function parseNaturalLanguageGuardrail(prompt, currentPolicy = DEFAULT_WORKSPACE_POLICY, options) {
|
|
252
|
+
const cleanPrompt = String(prompt || "").trim();
|
|
253
|
+
const lower = cleanPrompt.toLowerCase();
|
|
254
|
+
const patch = {};
|
|
255
|
+
const appliedSummaries = [];
|
|
256
|
+
let compiledBy = options?.source === "mcp_harness"
|
|
257
|
+
? "mcp_harness"
|
|
258
|
+
: options?.source === "llm"
|
|
259
|
+
? "llm"
|
|
260
|
+
: "deterministic";
|
|
261
|
+
if (options?.policyPatch && typeof options.policyPatch === "object") {
|
|
262
|
+
try {
|
|
263
|
+
const sanitized = sanitizePolicyPatch(options.policyPatch);
|
|
264
|
+
Object.assign(patch, sanitized);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Ignore invalid partial fields in optional patch
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const hasGuardrailIntent = /\b(don'?t\s+(?:go\s+over|spend|exceed|use|run|call|allow|provision|delete|drop|send|post)|never\s+(?:go\s+over|spend|exceed|use|run|call|allow|provision|delete|drop|send|post)|do\s+not\s+(?:go\s+over|spend|exceed|use|run|call|allow|provision|delete|drop|send|post)|cap\s+(?:budget|spend|credits)|budget\s+(?:cap|limit|under|below)|max(?:imum)?\s+\d+\s*credits|limit\s+(?:spend|budget|credits)|block\s+(?:tool|destructive|sql|provisioning|app\.|resource\.|instant\.|billing\.|refunds?|payments?|transfers?|emails?|messages?)|unblock\s+(?:all\s+)?tools|disable\s+(?:tool|provisioning|instant)|enable\s+provisioning|ask\s+(?:me\s+)?before|require\s+approval|auto[- ]?approve|only\s+(?:allow|send|post|permit)|restrict\s+(?:emails?|messages?|slack|github|refunds?)|read[- ]only\s+mode|sandbox\s+mode|strict\s+manual|hands[- ]off\s+mode|safe\s+default|monitor\s+mode|observation\s+mode|enforce\s+mode|execution\s+ring|ring\s+[0123]|non[- ]technical\s+(?:mode|language|explanations?)|technical\s+(?:mode|language|explanations?|detail)|plain\s+english|hybrid\s+(?:mode|language)|custom\s+instruction\s*:|always\s+(?:explain|show|include|format)|guardrail\s*:|rule\s*:|policy\s*:)/i.test(cleanPrompt);
|
|
271
|
+
// Detect whether a dollar amount in the sentence refers to a tool-specific transaction
|
|
272
|
+
// (e.g. "Stripe refunds over $50", "payments over $100", "transfers above $250")
|
|
273
|
+
// rather than the workspace's Muxo credit budget per run.
|
|
274
|
+
const isTransactionalThresholdPrompt = /\b(refunds?|payments?|charges?|transfers?|payouts?|invoices?|transactions?|wire|ach|send(?:ing)?\s+money)\b/i.test(lower) && !/\b(per\s+run|credits?\s+per\s+run|run\s+budget|muxo\s+credits?)\b/i.test(lower);
|
|
275
|
+
// 1. Budget / Credit / Dollar limits ("don't go over 25 credits", "max $0.50", "under 100 credits")
|
|
276
|
+
const creditMatch = cleanPrompt.match(/(?:over|above|exceed|max(?:imum)?|limit(?:\s+of|\s+to)?|cap(?:\s+at|\s+of)?|under|below|up\s+to|more\s+than|budget(?:\s+of|\s+is|\s+to)?)\s*(\d+(?:\.\d+)?)\s*credits?\b/i) ||
|
|
277
|
+
cleanPrompt.match(/\b(\d+)\s*credits?\s*(?:max|limit|cap|budget|per\s+run)\b/i);
|
|
278
|
+
const dollarMatch = !isTransactionalThresholdPrompt
|
|
279
|
+
? cleanPrompt.match(/(?:over|above|exceed|max(?:imum)?|limit(?:\s+of|\s+to)?|cap(?:\s+at|\s+of)?|under|below|up\s+to|more\s+than|budget(?:\s+of|\s+is|\s+to)?)\s*\$\s*(\d+(?:\.\d+)?)/i) ||
|
|
280
|
+
cleanPrompt.match(/\$\s*(\d+(?:\.\d+)?)\s*(?:max|limit|cap|budget|per\s+run)\b/i) ||
|
|
281
|
+
cleanPrompt.match(/(?:over|above|exceed|max(?:imum)?|limit(?:\s+of|\s+to)?|cap(?:\s+at|\s+of)?|under|below|up\s+to|more\s+than|budget(?:\s+of|\s+is|\s+to)?)\s*(\d+(?:\.\d+)?)\s*(?:dollars?|usd)\b/i)
|
|
282
|
+
: null;
|
|
283
|
+
if (creditMatch) {
|
|
284
|
+
const credits = Math.max(0, Math.min(100_000, Math.round(Number(creditMatch[1]))));
|
|
285
|
+
patch.maxCreditsPerRun = credits;
|
|
286
|
+
appliedSummaries.push(`Budget cap set to ${credits} credits ($${(credits * 0.01).toFixed(2)}) per run`);
|
|
287
|
+
}
|
|
288
|
+
else if (dollarMatch) {
|
|
289
|
+
const dollars = Number(dollarMatch[1]);
|
|
290
|
+
const credits = Math.max(0, Math.min(100_000, Math.round(dollars * 100)));
|
|
291
|
+
patch.maxCreditsPerRun = credits;
|
|
292
|
+
appliedSummaries.push(`Budget cap set to $${dollars.toFixed(2)} (${credits} credits) per run`);
|
|
293
|
+
}
|
|
294
|
+
else if (/\b(free\s+tools?\s+only|zero\s+budget|no\s+paid\s+tools?|don'?t\s+spend\s+any\s+(?:money|credits))\b/i.test(lower)) {
|
|
295
|
+
patch.maxCreditsPerRun = 0;
|
|
296
|
+
patch.allowInstantWallet = false;
|
|
297
|
+
appliedSummaries.push("Paid tools disabled (0 credits per run)");
|
|
298
|
+
}
|
|
299
|
+
// 2. Tool / Slug blocking ("don't use resource.projects.deprovision", "block app.gmail.send_email")
|
|
300
|
+
const existingBlocked = new Set(currentPolicy.blockedToolSlugs || []);
|
|
301
|
+
let blockedChanged = false;
|
|
302
|
+
if (/\b(unblock\s+all|allow\s+all\s+tools|clear\s+blocked\s+tools|reset\s+blocked)\b/i.test(lower)) {
|
|
303
|
+
existingBlocked.clear();
|
|
304
|
+
blockedChanged = true;
|
|
305
|
+
appliedSummaries.push("Cleared all blocked tools");
|
|
306
|
+
}
|
|
307
|
+
const slugMatches = cleanPrompt.match(/\b(?:instant|app|resource|billing)\.[a-z0-9_]+(?:\.[a-z0-9_]+)+\b/gi);
|
|
308
|
+
if (slugMatches && slugMatches.length > 0) {
|
|
309
|
+
const isUnblock = /\b(unblock|allow|enable|permit)\b/i.test(lower) &&
|
|
310
|
+
!/\b(don'?t|never|do\s+not|block|disable)\b/i.test(lower);
|
|
311
|
+
for (const rawSlug of slugMatches) {
|
|
312
|
+
const slug = rawSlug.toLowerCase();
|
|
313
|
+
if (isUnblock) {
|
|
314
|
+
existingBlocked.delete(slug);
|
|
315
|
+
blockedChanged = true;
|
|
316
|
+
appliedSummaries.push(`Allowed tool ${slug}`);
|
|
317
|
+
}
|
|
318
|
+
else if (hasGuardrailIntent ||
|
|
319
|
+
/\b(don'?t|never|do\s+not|block|disable|forbid|stop)\b/i.test(lower)) {
|
|
320
|
+
existingBlocked.add(slug);
|
|
321
|
+
blockedChanged = true;
|
|
322
|
+
appliedSummaries.push(`Blocked tool ${slug}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Friendly unconditional tool blocks ("don't send emails", "don't post to slack", "never deprovision")
|
|
327
|
+
// Do NOT unconditionally block the entire tool when the prompt is a conditional rule
|
|
328
|
+
// like "Only allow sending emails to @muxo.ai" or "Never post to #general" or "refunds over $50".
|
|
329
|
+
const isConditionalScope = /\b(only\s+(?:allow|send|post|permit)|restrict\s+to|must\s+end\s+with|over\s+\$?\d|above\s+\$?\d|under\s+\$?\d|below\s+\$?\d|exceeds?\s+\$?\d|more\s+than\s+\$?\d|to\s+@[a-z0-9.-]+|to\s+#[a-z0-9_-]+|in\s+[a-z0-9_.-]+\/[a-z0-9_.-]+)\b/i.test(lower);
|
|
330
|
+
if (!isConditionalScope &&
|
|
331
|
+
/\b(don'?t|never|do\s+not|block|disable|stop)\b[\s\S]*\b(send(?:ing)?\s+emails?|gmail\s+send)\b/i.test(lower)) {
|
|
332
|
+
existingBlocked.add("app.gmail.send_email");
|
|
333
|
+
blockedChanged = true;
|
|
334
|
+
appliedSummaries.push("Blocked sending emails (app.gmail.send_email)");
|
|
335
|
+
}
|
|
336
|
+
if (!isConditionalScope &&
|
|
337
|
+
/\b(don'?t|never|do\s+not|block|disable|stop)\b[\s\S]*\b(post(?:ing)?\s+to\s+slack|send(?:ing)?\s+slack|slack\s+messages?)\b/i.test(lower)) {
|
|
338
|
+
existingBlocked.add("app.slack.send_message");
|
|
339
|
+
blockedChanged = true;
|
|
340
|
+
appliedSummaries.push("Blocked sending Slack messages (app.slack.send_message)");
|
|
341
|
+
}
|
|
342
|
+
if (/\b(don'?t|never|do\s+not|block|disable|stop)\b[\s\S]*\b(deprovision|delete\s+databases?|destroy\s+projects?|teardown|destructive\s+tools?)\b/i.test(lower)) {
|
|
343
|
+
existingBlocked.add("resource.projects.deprovision");
|
|
344
|
+
blockedChanged = true;
|
|
345
|
+
appliedSummaries.push("Blocked cloud deprovisioning (resource.projects.deprovision)");
|
|
346
|
+
}
|
|
347
|
+
if (!isConditionalScope &&
|
|
348
|
+
/\b(don'?t|never|do\s+not|block|disable|stop)\b[\s\S]*\b(refunds?|issue\s+refund)\b/i.test(lower)) {
|
|
349
|
+
existingBlocked.add("billing.stripe.create_refund");
|
|
350
|
+
blockedChanged = true;
|
|
351
|
+
appliedSummaries.push("Blocked issuing Stripe refunds (billing.stripe.create_refund)");
|
|
352
|
+
}
|
|
353
|
+
if (blockedChanged) {
|
|
354
|
+
patch.blockedToolSlugs = Array.from(existingBlocked);
|
|
355
|
+
}
|
|
356
|
+
// 3. Provisioning & Instant Wallet toggles
|
|
357
|
+
if (/\b(don'?t|never|do\s+not|disable|block|stop|no)\b[\s\S]*\b(provision(?:ing)?|cloud\s+infrastructure|new\s+databases?)\b/i.test(lower) &&
|
|
358
|
+
!/\bask\s+(?:me\s+)?before\b/i.test(lower)) {
|
|
359
|
+
patch.allowProvisioning = false;
|
|
360
|
+
appliedSummaries.push("Disabled cloud infrastructure provisioning");
|
|
361
|
+
}
|
|
362
|
+
else if (/\b(enable|allow|turn\s+on)\s+(?:cloud\s+)?provisioning\b/i.test(lower)) {
|
|
363
|
+
patch.allowProvisioning = true;
|
|
364
|
+
appliedSummaries.push("Enabled cloud infrastructure provisioning");
|
|
365
|
+
}
|
|
366
|
+
// 4. Destructive SQL protection
|
|
367
|
+
if (/\b(block|prevent|disable|no|don'?t\s+(?:allow|run|use)|never\s+(?:allow|run|use))\b[\s\S]*\b(destructive\s+sql|drop\s+table|truncate|sql\s+delete|destructive\s+tools?)\b/i.test(lower)) {
|
|
368
|
+
patch.blockDestructiveSql = true;
|
|
369
|
+
appliedSummaries.push("Enabled destructive SQL protection (DROP/TRUNCATE blocked)");
|
|
370
|
+
}
|
|
371
|
+
// 5. Execution & Approval postures ("ask me before...", "read-only mode", "strict manual", "autonomous")
|
|
372
|
+
if (/\b(read[- ]only(\s+mode|\s+sandbox)?|sandbox\s+mode|only\s+allow\s+read(?:ing|s)?|don'?t\s+change\s+anything)\b/i.test(lower)) {
|
|
373
|
+
patch.executionRing = 3;
|
|
374
|
+
patch.mode = "enforce";
|
|
375
|
+
patch.allowProvisioning = false;
|
|
376
|
+
patch.requireApprovalFor = [
|
|
377
|
+
"write",
|
|
378
|
+
"admin",
|
|
379
|
+
"financial",
|
|
380
|
+
"provisioning",
|
|
381
|
+
"destructive",
|
|
382
|
+
];
|
|
383
|
+
appliedSummaries.push("Switched to Read-Only Sandbox mode (only read/search tools can run)");
|
|
384
|
+
}
|
|
385
|
+
else if (/\b(ask\s+(?:me\s+)?(?:before\s+)?(?:every\s*time|everything|all\s+actions|all\s+tools)|strict\s+manual|require\s+approval\s+for\s+everything)\b/i.test(lower)) {
|
|
386
|
+
patch.mode = "enforce";
|
|
387
|
+
patch.executionRing = 1;
|
|
388
|
+
patch.requireApprovalFor = [
|
|
389
|
+
"read",
|
|
390
|
+
"write",
|
|
391
|
+
"admin",
|
|
392
|
+
"financial",
|
|
393
|
+
"provisioning",
|
|
394
|
+
"destructive",
|
|
395
|
+
];
|
|
396
|
+
appliedSummaries.push("Set to Ask Every Time (manual approval required before any tool runs)");
|
|
397
|
+
}
|
|
398
|
+
else if (/\b(safe\s+default|ask\s+(?:me\s+)?before\s+(?:writing|changing|modifying|sending|provisioning|spending|destructive)|auto\s+read\s+approve\s+writes)\b/i.test(lower)) {
|
|
399
|
+
patch.mode = "enforce";
|
|
400
|
+
patch.executionRing = 1;
|
|
401
|
+
patch.requireApprovalFor = [
|
|
402
|
+
"write",
|
|
403
|
+
"admin",
|
|
404
|
+
"financial",
|
|
405
|
+
"provisioning",
|
|
406
|
+
"destructive",
|
|
407
|
+
];
|
|
408
|
+
appliedSummaries.push("Set to Safe Default (reads run automatically; writes, billing, provisioning, and destructive actions ask first)");
|
|
409
|
+
}
|
|
410
|
+
else if (/\b(autonomous(\s+mode)?|hands[- ]off(\s+mode)?|auto[- ]approve\s+writes)\b/i.test(lower)) {
|
|
411
|
+
patch.mode = "enforce";
|
|
412
|
+
patch.executionRing = 0;
|
|
413
|
+
patch.requireApprovalFor = ["financial", "provisioning", "destructive"];
|
|
414
|
+
appliedSummaries.push("Set to Hands-Off mode (reads and writes run automatically; financial, provisioning, and destructive actions still ask)");
|
|
415
|
+
}
|
|
416
|
+
else if (/\b(monitor\s+mode|observation\s+mode|log\s+only|observe\s+without\s+blocking)\b/i.test(lower)) {
|
|
417
|
+
patch.mode = "monitor";
|
|
418
|
+
appliedSummaries.push("Switched to Monitor (Observation) mode (Muxo logs safety checks without blocking actions)");
|
|
419
|
+
}
|
|
420
|
+
else if (/\b(enforce\s+mode|strict\s+enforcement)\b/i.test(lower)) {
|
|
421
|
+
patch.mode = "enforce";
|
|
422
|
+
appliedSummaries.push("Switched to Enforce mode (safety guardrails actively block violations)");
|
|
423
|
+
}
|
|
424
|
+
// 6. Explicit execution ring (technical customization: "set execution ring to 2", "ring 0")
|
|
425
|
+
const ringMatch = cleanPrompt.match(/\b(?:execution\s+)?ring\s*([0123])\b/i);
|
|
426
|
+
if (ringMatch && patch.executionRing === undefined) {
|
|
427
|
+
const ring = Number(ringMatch[1]);
|
|
428
|
+
patch.executionRing = ring;
|
|
429
|
+
appliedSummaries.push(`Set execution ring to Ring ${ring}`);
|
|
430
|
+
}
|
|
431
|
+
// 7. Customizable language mode ("use non-technical language", "explain in technical detail", "hybrid language")
|
|
432
|
+
if (/\b(non[- ]technical(?:\s+language|\s+mode|\s+explanations?)?|plain[- ]english(?:\s+mode|\s+only)?|simple\s+language|explain\s+simply|no\s+jargon)\b/i.test(lower) &&
|
|
433
|
+
!/\b(both|hybrid)\b/i.test(lower)) {
|
|
434
|
+
patch.languageMode = "non_technical";
|
|
435
|
+
appliedSummaries.push("Set language mode to Non-Technical (plain English explanations without jargon)");
|
|
436
|
+
}
|
|
437
|
+
else if (/\b(technical\s+(?:language|mode|detail|explanations?)|developer\s+mode|engineering\s+detail)\b/i.test(lower) &&
|
|
438
|
+
!/\b(non[- ]technical|both|hybrid)\b/i.test(lower)) {
|
|
439
|
+
patch.languageMode = "technical";
|
|
440
|
+
appliedSummaries.push("Set language mode to Technical (cryptographic hashes, rings, and protocol facets included)");
|
|
441
|
+
}
|
|
442
|
+
else if (/\b(hybrid\s+(?:language|mode)|both\s+technical\s+and\s+non[- ]technical|both\s+plain\s+english\s+and\s+technical)\b/i.test(lower)) {
|
|
443
|
+
patch.languageMode = "hybrid";
|
|
444
|
+
appliedSummaries.push("Set language mode to Hybrid (both plain-English summary and technical detail)");
|
|
445
|
+
}
|
|
446
|
+
// 8. Custom communication / operator instructions ("custom instruction: ...", "always explain ...")
|
|
447
|
+
const customInstructionMatch = cleanPrompt.match(/(?:custom\s+instruction|communication\s+style|explanation\s+style)\s*[:=-]\s*(.+)$/i) ||
|
|
448
|
+
cleanPrompt.match(/^((?:always|whenever\s+you\s+explain|in\s+all\s+explanations)\s+(?:show|include|explain|format|mention|use)\b.+)$/i);
|
|
449
|
+
if (customInstructionMatch) {
|
|
450
|
+
const instruction = customInstructionMatch[1].trim().replace(/[.]+$/, "");
|
|
451
|
+
if (instruction) {
|
|
452
|
+
const existingInstructions = Array.isArray(currentPolicy.customInstructions)
|
|
453
|
+
? [...currentPolicy.customInstructions]
|
|
454
|
+
: [];
|
|
455
|
+
if (!existingInstructions.includes(instruction)) {
|
|
456
|
+
existingInstructions.unshift(instruction);
|
|
457
|
+
}
|
|
458
|
+
patch.customInstructions = existingInstructions.slice(0, 25);
|
|
459
|
+
appliedSummaries.push(`Added custom instruction: "${instruction}"`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// 9. Conditional & Parameter-level policy rules
|
|
463
|
+
// ("Only allow sending emails to @muxo.ai", "Require approval for Stripe refunds over $50, auto-approve under $50",
|
|
464
|
+
// "Never post to #general", "Only allow GitHub writes in muxo/platform", plus structured MCP harness rules)
|
|
465
|
+
const existingParamRules = Array.isArray(currentPolicy.parameterRules)
|
|
466
|
+
? [...currentPolicy.parameterRules]
|
|
467
|
+
: [];
|
|
468
|
+
let paramRulesChanged = false;
|
|
469
|
+
const upsertParamRule = (rule) => {
|
|
470
|
+
const filtered = existingParamRules.filter((r) => r.id !== rule.id);
|
|
471
|
+
filtered.unshift(rule);
|
|
472
|
+
existingParamRules.splice(0, existingParamRules.length, ...filtered);
|
|
473
|
+
paramRulesChanged = true;
|
|
474
|
+
appliedSummaries.push(rule.description);
|
|
475
|
+
};
|
|
476
|
+
if (/\b(clear|reset|remove)\s+(?:all\s+)?parameter\s+rules\b/i.test(lower)) {
|
|
477
|
+
existingParamRules.length = 0;
|
|
478
|
+
paramRulesChanged = true;
|
|
479
|
+
appliedSummaries.push("Cleared all parameter-level policy rules");
|
|
480
|
+
}
|
|
481
|
+
// 9a. Pre-compiled structured rules supplied by an MCP LLM harness or API caller
|
|
482
|
+
if (Array.isArray(options?.parameterRules) && options.parameterRules.length > 0) {
|
|
483
|
+
for (let i = 0; i < options.parameterRules.length; i++) {
|
|
484
|
+
const raw = options.parameterRules[i];
|
|
485
|
+
if (raw && typeof raw === "object" && typeof raw.parameter === "string") {
|
|
486
|
+
const sanitized = sanitizeParameterPolicyRule({ ...raw, source: raw.source || options?.source || "mcp_harness" }, i);
|
|
487
|
+
upsertParamRule(sanitized);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
compiledBy = options?.source === "llm" ? "llm" : "mcp_harness";
|
|
491
|
+
}
|
|
492
|
+
// 9b. Email domain allowlist / blocklist ("only allow sending emails to @muxo.ai", "never email @competitor.com")
|
|
493
|
+
const emailDomains = cleanPrompt.match(/@[a-z0-9.-]+\.[a-z]{2,}/gi);
|
|
494
|
+
if (emailDomains &&
|
|
495
|
+
emailDomains.length > 0 &&
|
|
496
|
+
/\b(emails?|gmail|outlook|resend|sendgrid|recipients?|mail)\b/i.test(lower)) {
|
|
497
|
+
const normalizedDomains = [
|
|
498
|
+
...new Set(emailDomains.map((d) => d.toLowerCase())),
|
|
499
|
+
];
|
|
500
|
+
const isBlocklist = /\b(don'?t|never|do\s+not|block|forbid)\b/i.test(lower) &&
|
|
501
|
+
!/\b(only|must|restrict)\b/i.test(lower);
|
|
502
|
+
const action = /\b(ask|require\s+approval)\b/i.test(lower)
|
|
503
|
+
? "require_approval"
|
|
504
|
+
: "block";
|
|
505
|
+
if (isBlocklist) {
|
|
506
|
+
upsertParamRule({
|
|
507
|
+
id: `prule_email_block_${normalizedDomains.join("_").replace(/[^a-z0-9]/g, "_")}`,
|
|
508
|
+
toolPattern: "*email*,*gmail*,*outlook*,*resend*,*sendgrid*",
|
|
509
|
+
parameter: "to,recipient,email,recipients,cc,bcc",
|
|
510
|
+
operator: "domain_not_in",
|
|
511
|
+
value: normalizedDomains,
|
|
512
|
+
action,
|
|
513
|
+
description: `Block sending emails to ${normalizedDomains.join(", ")}`,
|
|
514
|
+
source: "deterministic",
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
else if (normalizedDomains.length === 1) {
|
|
518
|
+
const domain = normalizedDomains[0];
|
|
519
|
+
upsertParamRule({
|
|
520
|
+
id: `prule_email_${domain.replace(/[^a-z0-9]/g, "_")}`,
|
|
521
|
+
toolPattern: "*email*,*gmail*,*outlook*,*resend*,*sendgrid*",
|
|
522
|
+
parameter: "to,recipient,email,recipients,cc,bcc",
|
|
523
|
+
operator: "must_end_with",
|
|
524
|
+
value: domain,
|
|
525
|
+
action,
|
|
526
|
+
description: `Email recipient (to) must end with ${domain}`,
|
|
527
|
+
source: "deterministic",
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
upsertParamRule({
|
|
532
|
+
id: `prule_email_domains_${normalizedDomains.join("_").replace(/[^a-z0-9]/g, "_")}`,
|
|
533
|
+
toolPattern: "*email*,*gmail*,*outlook*,*resend*,*sendgrid*",
|
|
534
|
+
parameter: "to,recipient,email,recipients,cc,bcc",
|
|
535
|
+
operator: "domain_in",
|
|
536
|
+
value: normalizedDomains,
|
|
537
|
+
action,
|
|
538
|
+
description: `Only allow sending emails to ${normalizedDomains.join(", ")}`,
|
|
539
|
+
source: "deterministic",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// 9c. Transactional / Financial conditional thresholds
|
|
544
|
+
// e.g. "Require approval for Stripe refunds over $50, auto-approve under $50"
|
|
545
|
+
// or "Block refunds over $200" or "Auto-approve refunds under $50"
|
|
546
|
+
if (isTransactionalThresholdPrompt) {
|
|
547
|
+
const overMatch = cleanPrompt.match(/(?:(block|deny|forbid|require\s+approval|ask(?:\s+me)?)\s+(?:for\s+)?[\w\s]*?)?(?:over|above|exceed(?:s|ing)?|more\s+than|greater\s+than|>\s*)\$?\s*(\d+(?:\.\d+)?)/i);
|
|
548
|
+
const underAutoMatch = cleanPrompt.match(/(?:auto[- ]?approve|allow(?:\s+automatically)?|no\s+approval(?:\s+needed)?(?:\s+for)?)\s+[\w\s]*?(?:under|below|less\s+than|up\s+to|<=\s*|<\s*)\$?\s*(\d+(?:\.\d+)?)/i);
|
|
549
|
+
const targetNoun = /\brefunds?\b/i.test(lower)
|
|
550
|
+
? { slug: "refund", pattern: "*refund*", label: "Stripe refunds" }
|
|
551
|
+
: /\b(transfers?|payouts?)\b/i.test(lower)
|
|
552
|
+
? { slug: "transfer", pattern: "*transfer*,*payout*", label: "transfers/payouts" }
|
|
553
|
+
: /\binvoices?\b/i.test(lower)
|
|
554
|
+
? { slug: "invoice", pattern: "*invoice*", label: "invoices" }
|
|
555
|
+
: {
|
|
556
|
+
slug: "payment",
|
|
557
|
+
pattern: "*refund*,*stripe*,*payment*,*charge*",
|
|
558
|
+
label: "Stripe transactions",
|
|
559
|
+
};
|
|
560
|
+
if (overMatch || underAutoMatch) {
|
|
561
|
+
const thresholdVal = overMatch
|
|
562
|
+
? Number(overMatch[2])
|
|
563
|
+
: Number(underAutoMatch[1]);
|
|
564
|
+
const isBlockOver = Boolean(overMatch?.[1] && /\b(block|deny|forbid)\b/i.test(overMatch[1])) ||
|
|
565
|
+
(/\b(block|deny|forbid|never\s+allow)\b/i.test(lower) &&
|
|
566
|
+
!/\b(require\s+approval|ask)\b/i.test(lower));
|
|
567
|
+
const hasAutoApproveUnder = Boolean(underAutoMatch) || /\bauto[- ]?approve\b/i.test(lower);
|
|
568
|
+
const action = isBlockOver ? "block" : "require_approval";
|
|
569
|
+
const elseAction = hasAutoApproveUnder
|
|
570
|
+
? "auto_approve"
|
|
571
|
+
: undefined;
|
|
572
|
+
upsertParamRule({
|
|
573
|
+
id: `prule_${targetNoun.slug}_usd_${thresholdVal}`,
|
|
574
|
+
toolPattern: targetNoun.pattern,
|
|
575
|
+
parameter: "amount,amount_usd,amount_cents,refund_amount,total,value",
|
|
576
|
+
operator: "max_number",
|
|
577
|
+
value: thresholdVal,
|
|
578
|
+
unit: "usd",
|
|
579
|
+
action,
|
|
580
|
+
...(elseAction ? { elseAction } : {}),
|
|
581
|
+
description: `${action === "block" ? "Block" : "Require approval for"} ${targetNoun.label} over $${thresholdVal}${elseAction ? `, auto-approve $${thresholdVal} and under` : ""}`,
|
|
582
|
+
source: "deterministic",
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// 9d. Slack channel allowlist / blocklist ("never post to #general", "only allow Slack messages to #deployments")
|
|
587
|
+
const slackChannels = cleanPrompt.match(/#[a-z0-9_-]+/gi);
|
|
588
|
+
if (slackChannels && slackChannels.length > 0) {
|
|
589
|
+
const normalizedChannels = [
|
|
590
|
+
...new Set(slackChannels.map((c) => c.toLowerCase())),
|
|
591
|
+
];
|
|
592
|
+
const isOnlyAllow = /\b(only\s+(?:allow|post|send)|restrict\s+(?:slack|messages?)\s+to)\b/i.test(lower);
|
|
593
|
+
if (isOnlyAllow) {
|
|
594
|
+
upsertParamRule({
|
|
595
|
+
id: `prule_slack_allow_${normalizedChannels.join("_").replace(/[^a-z0-9]/g, "_")}`,
|
|
596
|
+
toolPattern: "*slack*",
|
|
597
|
+
parameter: "channel,channel_name",
|
|
598
|
+
operator: normalizedChannels.length === 1 ? "must_equal" : "in",
|
|
599
|
+
value: normalizedChannels.length === 1
|
|
600
|
+
? normalizedChannels[0]
|
|
601
|
+
: normalizedChannels,
|
|
602
|
+
action: "block",
|
|
603
|
+
description: `Only allow Slack messages to ${normalizedChannels.join(", ")}`,
|
|
604
|
+
source: "deterministic",
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
else if (/\b(don'?t|never|do\s+not|block)\b/i.test(lower)) {
|
|
608
|
+
upsertParamRule({
|
|
609
|
+
id: `prule_slack_${normalizedChannels.join("_").replace(/[^a-z0-9]/g, "_")}`,
|
|
610
|
+
toolPattern: "*slack*",
|
|
611
|
+
parameter: "channel,channel_name",
|
|
612
|
+
operator: normalizedChannels.length === 1 ? "not_equals" : "not_in",
|
|
613
|
+
value: normalizedChannels.length === 1
|
|
614
|
+
? normalizedChannels[0]
|
|
615
|
+
: normalizedChannels,
|
|
616
|
+
action: "block",
|
|
617
|
+
description: `Block Slack messages to ${normalizedChannels.join(", ")}`,
|
|
618
|
+
source: "deterministic",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// 9e. GitHub repository restriction ("only allow GitHub writes in muxo/platform")
|
|
623
|
+
const githubRepoMatch = cleanPrompt.match(/\b(?:only\s+(?:allow|permit|use)|restrict)\s+(?:github[\w\s]*?|issues?[\w\s]*?|prs?[\w\s]*?|pull\s+requests?[\w\s]*?|repos?(?:itory)?[\w\s]*?)(?:in|to|on)\s+([a-z0-9_.-]+\/[a-z0-9_.-]+)\b/i);
|
|
624
|
+
if (githubRepoMatch) {
|
|
625
|
+
const repo = githubRepoMatch[1].toLowerCase();
|
|
626
|
+
upsertParamRule({
|
|
627
|
+
id: `prule_github_repo_${repo.replace(/[^a-z0-9]/g, "_")}`,
|
|
628
|
+
toolPattern: "*github*",
|
|
629
|
+
parameter: "repo,repository,full_name",
|
|
630
|
+
operator: "must_equal",
|
|
631
|
+
value: repo,
|
|
632
|
+
action: "block",
|
|
633
|
+
description: `Only allow GitHub operations on repository ${repo}`,
|
|
634
|
+
source: "deterministic",
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
// 9f. Generic parameter numeric threshold ("block when amount exceeds 500", "require approval when parameter count > 10, auto-approve under 10")
|
|
638
|
+
const paramCapMatch = !isTransactionalThresholdPrompt
|
|
639
|
+
? cleanPrompt.match(/\b(block|require\s+approval|ask(?:\s+me)?)\s+(?:when|if)\s+(?:parameter\s+)?([a-zA-Z0-9_]+)\s+(?:is\s+over|exceeds|>\s*|is\s+greater\s+than|more\s+than)\s*\$?\s*(\d+(?:\.\d+)?)\b/i)
|
|
640
|
+
: null;
|
|
641
|
+
if (paramCapMatch) {
|
|
642
|
+
const actionVerb = paramCapMatch[1].toLowerCase();
|
|
643
|
+
const paramName = paramCapMatch[2];
|
|
644
|
+
const maxVal = Number(paramCapMatch[3]);
|
|
645
|
+
const action = actionVerb.startsWith("block")
|
|
646
|
+
? "block"
|
|
647
|
+
: "require_approval";
|
|
648
|
+
const hasAutoApprove = /\bauto[- ]?approve\b/i.test(lower);
|
|
649
|
+
upsertParamRule({
|
|
650
|
+
id: `prule_max_${paramName.toLowerCase()}`,
|
|
651
|
+
toolPattern: "*",
|
|
652
|
+
parameter: paramName,
|
|
653
|
+
operator: "max_number",
|
|
654
|
+
value: maxVal,
|
|
655
|
+
action,
|
|
656
|
+
...(hasAutoApprove ? { elseAction: "auto_approve" } : {}),
|
|
657
|
+
description: `${action === "block" ? "Block" : "Require approval"} when parameter '${paramName}' exceeds ${maxVal}${hasAutoApprove ? ` (auto-approve ${maxVal} and under)` : ""}`,
|
|
658
|
+
source: "deterministic",
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (paramRulesChanged) {
|
|
662
|
+
patch.parameterRules = existingParamRules.slice(0, 50);
|
|
663
|
+
}
|
|
664
|
+
const isGuardrail = Boolean(cleanPrompt || paramRulesChanged) &&
|
|
665
|
+
(hasGuardrailIntent || appliedSummaries.length > 0);
|
|
666
|
+
if (isGuardrail && cleanPrompt) {
|
|
667
|
+
const existingRules = Array.isArray(currentPolicy.naturalLanguageRules)
|
|
668
|
+
? [...currentPolicy.naturalLanguageRules]
|
|
669
|
+
: [];
|
|
670
|
+
const normalizedRule = cleanPrompt.replace(/^hey\s+muxo[,:\s]*/i, "").trim();
|
|
671
|
+
const displayRule = normalizedRule.charAt(0).toUpperCase() + normalizedRule.slice(1);
|
|
672
|
+
if (displayRule && !existingRules.includes(displayRule)) {
|
|
673
|
+
existingRules.unshift(displayRule);
|
|
674
|
+
}
|
|
675
|
+
patch.naturalLanguageRules = existingRules.slice(0, 25);
|
|
676
|
+
if (!patch.mode && !currentPolicy.mode) {
|
|
677
|
+
patch.mode = "enforce";
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const nextPolicy = {
|
|
681
|
+
...DEFAULT_WORKSPACE_POLICY,
|
|
682
|
+
...currentPolicy,
|
|
683
|
+
...patch,
|
|
684
|
+
};
|
|
685
|
+
const plain = describePolicyInPlainEnglish(nextPolicy);
|
|
686
|
+
const summary = appliedSummaries.length > 0
|
|
687
|
+
? appliedSummaries.join(" · ")
|
|
688
|
+
: `Saved workspace guardrail: "${cleanPrompt}"`;
|
|
689
|
+
return {
|
|
690
|
+
isGuardrail,
|
|
691
|
+
patch,
|
|
692
|
+
ruleText: cleanPrompt,
|
|
693
|
+
summary,
|
|
694
|
+
humanRules: plain.rules,
|
|
695
|
+
compiledBy,
|
|
696
|
+
memoryFact: {
|
|
697
|
+
title: `Workspace Guardrail: ${summary.slice(0, 80)}`,
|
|
698
|
+
content: `User instructed Muxo guardrail: "${cleanPrompt}". Enforced effect: ${summary}.`,
|
|
699
|
+
subject: "workspace_guardrail",
|
|
700
|
+
predicate: "enforces_rule",
|
|
701
|
+
object: summary,
|
|
702
|
+
tags: ["guardrail", "safety", "user_rule"],
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Async LLM-assisted policy compiler: first runs deterministic semantic compilation
|
|
708
|
+
* (plus any structured `parameterRules` supplied by an MCP LLM harness), and when an
|
|
709
|
+
* OpenAI or Anthropic key is available and a complex natural-language rule produced no
|
|
710
|
+
* structured patch fields, invokes the LLM to synthesize validated `ParameterPolicyRule[]`.
|
|
711
|
+
*/
|
|
712
|
+
export async function compileNaturalLanguageGuardrailWithLlm(prompt, currentPolicy = DEFAULT_WORKSPACE_POLICY, options) {
|
|
713
|
+
const deterministic = parseNaturalLanguageGuardrail(prompt, currentPolicy, options);
|
|
714
|
+
const hasCompiledEffect = Object.keys(deterministic.patch).some((k) => k !== "naturalLanguageRules" && k !== "mode") || Boolean(options?.parameterRules?.length);
|
|
715
|
+
const apiKey = options?.llmApiKey ||
|
|
716
|
+
(typeof process !== "undefined"
|
|
717
|
+
? process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY
|
|
718
|
+
: undefined);
|
|
719
|
+
if (hasCompiledEffect || !apiKey || !prompt.trim()) {
|
|
720
|
+
return deterministic;
|
|
721
|
+
}
|
|
722
|
+
const systemPrompt = [
|
|
723
|
+
"You compile plain-English AI agent safety guardrails into JSON policy rules for Muxo.",
|
|
724
|
+
"Return ONLY a JSON object with optional fields:",
|
|
725
|
+
"- parameterRules: array of { id, toolPattern, parameter, operator, value, unit, action, elseAction, description }",
|
|
726
|
+
" where operator is one of: equals, must_equal, not_equals, contains, not_contains, starts_with, must_start_with, ends_with, must_end_with, domain_in, domain_not_in, max_number, min_number, gt, gte, lt, lte, in, not_in, regex",
|
|
727
|
+
" and action / elseAction are one of: block, require_approval, auto_approve, allow",
|
|
728
|
+
" and unit is optional: usd, cents, credits, raw.",
|
|
729
|
+
"- maxCreditsPerRun: optional number (100 credits = $1.00)",
|
|
730
|
+
"- blockedToolSlugs: optional string[]",
|
|
731
|
+
].join("\n");
|
|
732
|
+
try {
|
|
733
|
+
const isAnthropic = options?.llmProvider === "anthropic" || apiKey.startsWith("sk-ant-");
|
|
734
|
+
let rawJsonText = "";
|
|
735
|
+
if (isAnthropic) {
|
|
736
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
737
|
+
method: "POST",
|
|
738
|
+
headers: {
|
|
739
|
+
"Content-Type": "application/json",
|
|
740
|
+
"x-api-key": apiKey,
|
|
741
|
+
"anthropic-version": "2023-06-01",
|
|
742
|
+
},
|
|
743
|
+
body: JSON.stringify({
|
|
744
|
+
model: "claude-3-5-haiku-latest",
|
|
745
|
+
max_tokens: 512,
|
|
746
|
+
system: systemPrompt,
|
|
747
|
+
messages: [{ role: "user", content: prompt }],
|
|
748
|
+
}),
|
|
749
|
+
});
|
|
750
|
+
if (res.ok) {
|
|
751
|
+
const data = (await res.json());
|
|
752
|
+
rawJsonText = String(data?.content?.[0]?.text || "");
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
757
|
+
method: "POST",
|
|
758
|
+
headers: {
|
|
759
|
+
"Content-Type": "application/json",
|
|
760
|
+
Authorization: `Bearer ${apiKey}`,
|
|
761
|
+
},
|
|
762
|
+
body: JSON.stringify({
|
|
763
|
+
model: "gpt-4o-mini",
|
|
764
|
+
temperature: 0,
|
|
765
|
+
response_format: { type: "json_object" },
|
|
766
|
+
messages: [
|
|
767
|
+
{ role: "system", content: systemPrompt },
|
|
768
|
+
{ role: "user", content: prompt },
|
|
769
|
+
],
|
|
770
|
+
}),
|
|
771
|
+
});
|
|
772
|
+
if (res.ok) {
|
|
773
|
+
const data = (await res.json());
|
|
774
|
+
rawJsonText = String(data?.choices?.[0]?.message?.content || "");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const jsonMatch = rawJsonText.match(/\{[\s\S]*\}/);
|
|
778
|
+
if (jsonMatch) {
|
|
779
|
+
const parsedLlm = JSON.parse(jsonMatch[0]);
|
|
780
|
+
return parseNaturalLanguageGuardrail(prompt, currentPolicy, {
|
|
781
|
+
...options,
|
|
782
|
+
parameterRules: Array.isArray(parsedLlm.parameterRules)
|
|
783
|
+
? parsedLlm.parameterRules
|
|
784
|
+
: undefined,
|
|
785
|
+
policyPatch: parsedLlm,
|
|
786
|
+
source: "llm",
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
// Fall back to deterministic compilation if LLM call fails or times out
|
|
792
|
+
}
|
|
793
|
+
return deterministic;
|
|
794
|
+
}
|
|
795
|
+
export function describePolicyInPlainEnglish(policy = DEFAULT_WORKSPACE_POLICY) {
|
|
796
|
+
const rules = [];
|
|
797
|
+
const badges = [];
|
|
798
|
+
const reqSet = new Set(policy.requireApprovalFor || []);
|
|
799
|
+
if (policy.killSwitchActive) {
|
|
800
|
+
rules.push(`All actions paused: ${policy.killSwitchReason || "Workspace pause is active."}`);
|
|
801
|
+
badges.push({ label: "Paused", tone: "strict" });
|
|
802
|
+
}
|
|
803
|
+
else if (policy.executionRing === 3) {
|
|
804
|
+
rules.push("Read-only mode: Muxo can search and read data, but cannot modify apps, spend money, or provision infrastructure.");
|
|
805
|
+
badges.push({ label: "Read-only sandbox", tone: "strict" });
|
|
806
|
+
}
|
|
807
|
+
else if (reqSet.has("read")) {
|
|
808
|
+
rules.push("Ask every time: Muxo will ask for your approval before running any tool.");
|
|
809
|
+
badges.push({ label: "Ask every time", tone: "strict" });
|
|
810
|
+
}
|
|
811
|
+
else if (!reqSet.has("write") && policy.mode === "enforce") {
|
|
812
|
+
rules.push("Hands-off mode: Muxo runs read and write tasks automatically, and asks before financial, cloud provisioning, or destructive actions.");
|
|
813
|
+
badges.push({ label: "Hands-off mode", tone: "warn" });
|
|
814
|
+
}
|
|
815
|
+
else if (policy.mode === "monitor") {
|
|
816
|
+
rules.push("Observation mode: Muxo logs safety checks without blocking actions.");
|
|
817
|
+
badges.push({ label: "Monitor mode", tone: "warn" });
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
rules.push("Safe default: Searches and reads run automatically; Muxo asks before changing data, spending money, or provisioning cloud services.");
|
|
821
|
+
badges.push({ label: "Safe default", tone: "safe" });
|
|
822
|
+
}
|
|
823
|
+
const maxCredits = Number(policy.maxCreditsPerRun ?? 50);
|
|
824
|
+
const dollars = (maxCredits * 0.01).toFixed(2);
|
|
825
|
+
rules.push(`Budget limit: Don't spend more than ${maxCredits} credits ($${dollars}) on a single run.`);
|
|
826
|
+
badges.push({
|
|
827
|
+
label: `Max ${maxCredits} cr ($${dollars})/run`,
|
|
828
|
+
tone: "safe",
|
|
829
|
+
});
|
|
830
|
+
if (policy.allowProvisioning === false) {
|
|
831
|
+
rules.push("Cloud infrastructure: Provisioning new databases or cloud services is turned off.");
|
|
832
|
+
badges.push({ label: "No cloud provisioning", tone: "strict" });
|
|
833
|
+
}
|
|
834
|
+
if (policy.allowInstantWallet === false) {
|
|
835
|
+
rules.push("Paid instant tools: Turned off (only free and connected OAuth tools are allowed).");
|
|
836
|
+
}
|
|
837
|
+
if (policy.blockDestructiveSql !== false) {
|
|
838
|
+
rules.push("Database safety: Destructive SQL statements (DROP, TRUNCATE, DELETE without WHERE) are blocked.");
|
|
839
|
+
badges.push({ label: "SQL protected", tone: "safe" });
|
|
840
|
+
}
|
|
841
|
+
if (policy.blockedToolSlugs && policy.blockedToolSlugs.length > 0) {
|
|
842
|
+
rules.push(`Off-limits tools: Never use ${policy.blockedToolSlugs.join(", ")}.`);
|
|
843
|
+
badges.push({
|
|
844
|
+
label: `${policy.blockedToolSlugs.length} blocked tool${policy.blockedToolSlugs.length === 1 ? "" : "s"}`,
|
|
845
|
+
tone: "strict",
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
if (policy.parameterRules && policy.parameterRules.length > 0) {
|
|
849
|
+
for (const pr of policy.parameterRules) {
|
|
850
|
+
rules.push(`Parameter guardrail: ${pr.description}.`);
|
|
851
|
+
}
|
|
852
|
+
badges.push({
|
|
853
|
+
label: `${policy.parameterRules.length} param rule${policy.parameterRules.length === 1 ? "" : "s"}`,
|
|
854
|
+
tone: "strict",
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
const languageMode = policy.languageMode || "hybrid";
|
|
858
|
+
const customInstructions = Array.isArray(policy.customInstructions)
|
|
859
|
+
? policy.customInstructions
|
|
860
|
+
: [];
|
|
861
|
+
if (languageMode === "non_technical") {
|
|
862
|
+
badges.push({ label: "Plain-English mode", tone: "safe" });
|
|
863
|
+
}
|
|
864
|
+
else if (languageMode === "technical") {
|
|
865
|
+
badges.push({ label: "Technical mode", tone: "safe" });
|
|
866
|
+
}
|
|
867
|
+
const activeCustomRules = Array.isArray(policy.naturalLanguageRules)
|
|
868
|
+
? policy.naturalLanguageRules
|
|
869
|
+
: [];
|
|
870
|
+
const headline = rules[0] || "Safe default guardrails active.";
|
|
871
|
+
const nonTechnical = [
|
|
872
|
+
...rules,
|
|
873
|
+
...(activeCustomRules.length > 0
|
|
874
|
+
? [`Custom rules: ${activeCustomRules.join(" · ")}`]
|
|
875
|
+
: []),
|
|
876
|
+
].join(" ");
|
|
877
|
+
const ringNum = policy.executionRing ?? 1;
|
|
878
|
+
const ringNames = {
|
|
879
|
+
0: "Ring 0 (Root/Admin)",
|
|
880
|
+
1: "Ring 1 (Privileged Specialist)",
|
|
881
|
+
2: "Ring 2 (Standard Read/Write)",
|
|
882
|
+
3: "Ring 3 (Sandboxed Read-Only)",
|
|
883
|
+
};
|
|
884
|
+
const technical = `mode=${policy.mode || "enforce"} | executionRing=${ringNames[ringNum] || `Ring ${ringNum}`} | requireApprovalFor=[${(policy.requireApprovalFor || []).join(", ")}] | maxCreditsPerRun=${maxCredits} ($${dollars}) | allowInstantWallet=${policy.allowInstantWallet !== false} | allowProvisioning=${policy.allowProvisioning !== false} | blockDestructiveSql=${policy.blockDestructiveSql !== false} | blockedToolSlugs=[${(policy.blockedToolSlugs || []).join(", ")}] | killSwitchActive=${Boolean(policy.killSwitchActive)} | languageMode=${languageMode}`;
|
|
885
|
+
const explanation = formatHumanFriendlyExplanation({
|
|
886
|
+
headline,
|
|
887
|
+
nonTechnical,
|
|
888
|
+
technical,
|
|
889
|
+
policy,
|
|
890
|
+
languageMode,
|
|
891
|
+
});
|
|
892
|
+
return {
|
|
893
|
+
headline,
|
|
894
|
+
rules,
|
|
895
|
+
activeCustomRules,
|
|
896
|
+
badges,
|
|
897
|
+
languageMode,
|
|
898
|
+
customInstructions,
|
|
899
|
+
nonTechnical: explanation.nonTechnical,
|
|
900
|
+
technical: explanation.technical,
|
|
901
|
+
preferred: explanation.preferred,
|
|
902
|
+
explanation,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function matchesSingleToolPattern(toolSlug, pattern = "*") {
|
|
906
|
+
const slug = toolSlug.toLowerCase();
|
|
907
|
+
const pat = (pattern || "*").toLowerCase().trim();
|
|
908
|
+
if (!pat || pat === "*")
|
|
909
|
+
return true;
|
|
910
|
+
if (pat.startsWith("*") && pat.endsWith("*")) {
|
|
911
|
+
return slug.includes(pat.slice(1, -1));
|
|
912
|
+
}
|
|
913
|
+
if (pat.startsWith("*")) {
|
|
914
|
+
return slug.endsWith(pat.slice(1));
|
|
915
|
+
}
|
|
916
|
+
if (pat.endsWith("*")) {
|
|
917
|
+
return slug.startsWith(pat.slice(0, -1));
|
|
918
|
+
}
|
|
919
|
+
return slug === pat || slug.includes(pat);
|
|
920
|
+
}
|
|
921
|
+
function matchesToolPattern(toolSlug, pattern = "*") {
|
|
922
|
+
const raw = (pattern || "*").trim();
|
|
923
|
+
if (!raw || raw === "*")
|
|
924
|
+
return true;
|
|
925
|
+
const subpatterns = raw
|
|
926
|
+
.split(",")
|
|
927
|
+
.map((s) => s.trim())
|
|
928
|
+
.filter(Boolean);
|
|
929
|
+
if (subpatterns.length === 0)
|
|
930
|
+
return true;
|
|
931
|
+
return subpatterns.some((sub) => matchesSingleToolPattern(toolSlug, sub));
|
|
932
|
+
}
|
|
933
|
+
export function evaluateParameterPolicyRules(paramsOrToolSlug, maybeInput, maybePolicy) {
|
|
934
|
+
const params = typeof paramsOrToolSlug === "string"
|
|
935
|
+
? {
|
|
936
|
+
toolSlug: paramsOrToolSlug,
|
|
937
|
+
input: maybeInput || {},
|
|
938
|
+
policy: maybePolicy,
|
|
939
|
+
}
|
|
940
|
+
: paramsOrToolSlug;
|
|
941
|
+
const rules = params.policy?.parameterRules;
|
|
942
|
+
if (!Array.isArray(rules) || rules.length === 0) {
|
|
943
|
+
return {
|
|
944
|
+
violated: false,
|
|
945
|
+
allowed: true,
|
|
946
|
+
explicitAllow: false,
|
|
947
|
+
action: "allow",
|
|
948
|
+
matchedRule: null,
|
|
949
|
+
violatedRule: null,
|
|
950
|
+
reason: null,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
const input = params.input || {};
|
|
954
|
+
let explicitAllowRule = null;
|
|
955
|
+
let explicitAllowAction = "allow";
|
|
956
|
+
for (const rule of rules) {
|
|
957
|
+
if (!matchesToolPattern(params.toolSlug, rule.toolPattern))
|
|
958
|
+
continue;
|
|
959
|
+
const baseCandidates = String(rule.parameter || "")
|
|
960
|
+
.split(",")
|
|
961
|
+
.map((s) => s.trim())
|
|
962
|
+
.filter(Boolean);
|
|
963
|
+
const candidateSet = new Set(baseCandidates);
|
|
964
|
+
if (candidateSet.has("to")) {
|
|
965
|
+
for (const alias of [
|
|
966
|
+
"to",
|
|
967
|
+
"recipient",
|
|
968
|
+
"email",
|
|
969
|
+
"recipient_email",
|
|
970
|
+
"recipients",
|
|
971
|
+
"cc",
|
|
972
|
+
"bcc",
|
|
973
|
+
]) {
|
|
974
|
+
candidateSet.add(alias);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (candidateSet.has("amount")) {
|
|
978
|
+
for (const alias of [
|
|
979
|
+
"amount",
|
|
980
|
+
"amount_usd",
|
|
981
|
+
"amount_cents",
|
|
982
|
+
"total",
|
|
983
|
+
"value",
|
|
984
|
+
]) {
|
|
985
|
+
candidateSet.add(alias);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const paramCandidates = Array.from(candidateSet);
|
|
989
|
+
const isRecipientRule = candidateSet.has("to");
|
|
990
|
+
const matchedEntries = [];
|
|
991
|
+
for (const key of paramCandidates) {
|
|
992
|
+
if (input[key] !== undefined && input[key] !== null) {
|
|
993
|
+
const val = input[key];
|
|
994
|
+
if (isRecipientRule && Array.isArray(val)) {
|
|
995
|
+
for (const item of val) {
|
|
996
|
+
matchedEntries.push({ key, value: item });
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
else if (isRecipientRule &&
|
|
1000
|
+
typeof val === "string" &&
|
|
1001
|
+
val.includes(",")) {
|
|
1002
|
+
for (const part of val
|
|
1003
|
+
.split(",")
|
|
1004
|
+
.map((s) => s.trim())
|
|
1005
|
+
.filter(Boolean)) {
|
|
1006
|
+
matchedEntries.push({ key, value: part });
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
matchedEntries.push({ key, value: val });
|
|
1011
|
+
}
|
|
1012
|
+
if (!isRecipientRule)
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (matchedEntries.length === 0)
|
|
1017
|
+
continue;
|
|
1018
|
+
for (const entry of matchedEntries) {
|
|
1019
|
+
const rawVal = entry.value;
|
|
1020
|
+
const matchedKey = entry.key;
|
|
1021
|
+
const strVal = String(rawVal).trim().toLowerCase();
|
|
1022
|
+
const targetStr = String(rule.value ?? "").trim().toLowerCase();
|
|
1023
|
+
let rawNum = Number(rawVal);
|
|
1024
|
+
if (Number.isFinite(rawNum)) {
|
|
1025
|
+
if (rule.unit === "usd" && matchedKey === "amount_cents") {
|
|
1026
|
+
rawNum = rawNum / 100;
|
|
1027
|
+
}
|
|
1028
|
+
else if (rule.unit === "cents" && matchedKey === "amount_usd") {
|
|
1029
|
+
rawNum = rawNum * 100;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
const thresholdNum = Number(rule.value);
|
|
1033
|
+
const hasValidNumbers = Number.isFinite(rawNum) && Number.isFinite(thresholdNum);
|
|
1034
|
+
let conditionTriggered = false;
|
|
1035
|
+
switch (rule.operator) {
|
|
1036
|
+
case "ends_with":
|
|
1037
|
+
case "must_end_with":
|
|
1038
|
+
conditionTriggered =
|
|
1039
|
+
rule.action === "allow" || rule.action === "auto_approve"
|
|
1040
|
+
? strVal.endsWith(targetStr)
|
|
1041
|
+
: !strVal.endsWith(targetStr);
|
|
1042
|
+
break;
|
|
1043
|
+
case "starts_with":
|
|
1044
|
+
case "must_start_with":
|
|
1045
|
+
conditionTriggered =
|
|
1046
|
+
rule.action === "allow" || rule.action === "auto_approve"
|
|
1047
|
+
? strVal.startsWith(targetStr)
|
|
1048
|
+
: !strVal.startsWith(targetStr);
|
|
1049
|
+
break;
|
|
1050
|
+
case "equals":
|
|
1051
|
+
case "must_equal":
|
|
1052
|
+
conditionTriggered =
|
|
1053
|
+
rule.action === "allow" || rule.action === "auto_approve"
|
|
1054
|
+
? strVal === targetStr
|
|
1055
|
+
: strVal !== targetStr;
|
|
1056
|
+
break;
|
|
1057
|
+
case "not_equals":
|
|
1058
|
+
conditionTriggered = strVal === targetStr;
|
|
1059
|
+
break;
|
|
1060
|
+
case "contains":
|
|
1061
|
+
conditionTriggered =
|
|
1062
|
+
rule.action === "allow" || rule.action === "auto_approve"
|
|
1063
|
+
? strVal.includes(targetStr)
|
|
1064
|
+
: !strVal.includes(targetStr);
|
|
1065
|
+
break;
|
|
1066
|
+
case "not_contains":
|
|
1067
|
+
conditionTriggered = strVal.includes(targetStr);
|
|
1068
|
+
break;
|
|
1069
|
+
case "domain_in": {
|
|
1070
|
+
const allowedDomains = (Array.isArray(rule.value)
|
|
1071
|
+
? rule.value
|
|
1072
|
+
: String(rule.value).split(","))
|
|
1073
|
+
.map((v) => String(v).trim().toLowerCase().replace(/^@/, ""))
|
|
1074
|
+
.filter(Boolean);
|
|
1075
|
+
const inDomain = allowedDomains.some((d) => strVal.endsWith(`@${d}`) || strVal === d);
|
|
1076
|
+
conditionTriggered =
|
|
1077
|
+
rule.action === "allow" || rule.action === "auto_approve"
|
|
1078
|
+
? inDomain
|
|
1079
|
+
: !inDomain;
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
case "domain_not_in": {
|
|
1083
|
+
const blockedDomains = (Array.isArray(rule.value)
|
|
1084
|
+
? rule.value
|
|
1085
|
+
: String(rule.value).split(","))
|
|
1086
|
+
.map((v) => String(v).trim().toLowerCase().replace(/^@/, ""))
|
|
1087
|
+
.filter(Boolean);
|
|
1088
|
+
conditionTriggered = blockedDomains.some((d) => strVal.endsWith(`@${d}`) || strVal === d);
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
case "max_number":
|
|
1092
|
+
if (hasValidNumbers) {
|
|
1093
|
+
conditionTriggered = rawNum > thresholdNum;
|
|
1094
|
+
}
|
|
1095
|
+
break;
|
|
1096
|
+
case "min_number":
|
|
1097
|
+
if (hasValidNumbers) {
|
|
1098
|
+
conditionTriggered = rawNum < thresholdNum;
|
|
1099
|
+
}
|
|
1100
|
+
break;
|
|
1101
|
+
case "gt":
|
|
1102
|
+
if (hasValidNumbers) {
|
|
1103
|
+
conditionTriggered = rawNum > thresholdNum;
|
|
1104
|
+
}
|
|
1105
|
+
break;
|
|
1106
|
+
case "gte":
|
|
1107
|
+
if (hasValidNumbers) {
|
|
1108
|
+
conditionTriggered = rawNum >= thresholdNum;
|
|
1109
|
+
}
|
|
1110
|
+
break;
|
|
1111
|
+
case "lt":
|
|
1112
|
+
if (hasValidNumbers) {
|
|
1113
|
+
conditionTriggered = rawNum < thresholdNum;
|
|
1114
|
+
}
|
|
1115
|
+
break;
|
|
1116
|
+
case "lte":
|
|
1117
|
+
if (hasValidNumbers) {
|
|
1118
|
+
conditionTriggered = rawNum <= thresholdNum;
|
|
1119
|
+
}
|
|
1120
|
+
break;
|
|
1121
|
+
case "in": {
|
|
1122
|
+
const allowed = (Array.isArray(rule.value)
|
|
1123
|
+
? rule.value
|
|
1124
|
+
: String(rule.value).split(","))
|
|
1125
|
+
.map((v) => String(v).trim().toLowerCase())
|
|
1126
|
+
.filter(Boolean);
|
|
1127
|
+
const isMember = allowed.includes(strVal);
|
|
1128
|
+
conditionTriggered =
|
|
1129
|
+
rule.action === "allow" || rule.action === "auto_approve"
|
|
1130
|
+
? isMember
|
|
1131
|
+
: !isMember;
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
case "not_in": {
|
|
1135
|
+
const blocked = (Array.isArray(rule.value)
|
|
1136
|
+
? rule.value
|
|
1137
|
+
: String(rule.value).split(","))
|
|
1138
|
+
.map((v) => String(v).trim().toLowerCase())
|
|
1139
|
+
.filter(Boolean);
|
|
1140
|
+
conditionTriggered = blocked.includes(strVal);
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
1143
|
+
case "regex": {
|
|
1144
|
+
try {
|
|
1145
|
+
const matches = new RegExp(String(rule.value), "i").test(String(rawVal));
|
|
1146
|
+
conditionTriggered =
|
|
1147
|
+
rule.action === "allow" || rule.action === "auto_approve"
|
|
1148
|
+
? matches
|
|
1149
|
+
: !matches;
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
conditionTriggered = false;
|
|
1153
|
+
}
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
const effectiveAction = conditionTriggered
|
|
1158
|
+
? rule.action
|
|
1159
|
+
: rule.elseAction;
|
|
1160
|
+
if (effectiveAction === "block" ||
|
|
1161
|
+
effectiveAction === "require_approval") {
|
|
1162
|
+
return {
|
|
1163
|
+
violated: true,
|
|
1164
|
+
allowed: false,
|
|
1165
|
+
explicitAllow: false,
|
|
1166
|
+
action: effectiveAction,
|
|
1167
|
+
matchedRule: rule,
|
|
1168
|
+
violatedRule: rule,
|
|
1169
|
+
reason: `Parameter policy rule triggered (${rule.description}): ${matchedKey}=${JSON.stringify(rawVal)}`,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
if (effectiveAction === "auto_approve" ||
|
|
1173
|
+
effectiveAction === "allow") {
|
|
1174
|
+
explicitAllowRule = rule;
|
|
1175
|
+
if (effectiveAction === "auto_approve") {
|
|
1176
|
+
explicitAllowAction = "auto_approve";
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
violated: false,
|
|
1183
|
+
allowed: true,
|
|
1184
|
+
explicitAllow: Boolean(explicitAllowRule),
|
|
1185
|
+
action: explicitAllowRule ? explicitAllowAction : "allow",
|
|
1186
|
+
matchedRule: explicitAllowRule,
|
|
1187
|
+
violatedRule: null,
|
|
1188
|
+
reason: explicitAllowRule
|
|
1189
|
+
? `Parameter policy rule satisfied (${explicitAllowRule.description})`
|
|
1190
|
+
: null,
|
|
1191
|
+
};
|
|
1192
|
+
}
|