@muxoai/platform 1.4.0 → 1.4.2

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.
Files changed (62) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/ARCHITECTURE.md +16 -6
  4. package/README.md +2 -2
  5. package/dist/cli.js +323 -13
  6. package/dist/composio-catalog.d.ts +234 -0
  7. package/dist/composio-catalog.js +1894 -0
  8. package/dist/config.d.ts +16 -0
  9. package/dist/config.js +27 -1
  10. package/dist/durable-workflow.d.ts +45 -0
  11. package/dist/durable-workflow.js +90 -0
  12. package/dist/engine.d.ts +293 -5
  13. package/dist/engine.js +1502 -88
  14. package/dist/mcp-schema.d.ts +286 -0
  15. package/dist/mcp-schema.js +2559 -0
  16. package/dist/mcp.d.ts +2 -30
  17. package/dist/mcp.js +913 -558
  18. package/dist/memory-graph.d.ts +21 -0
  19. package/dist/memory-graph.js +173 -0
  20. package/dist/nl.js +155 -0
  21. package/dist/oauth.d.ts +32 -0
  22. package/dist/oauth.js +134 -0
  23. package/dist/policy.d.ts +182 -1
  24. package/dist/policy.js +1524 -0
  25. package/dist/providers/composio-instant.js +262 -7
  26. package/dist/providers/composio-sdk.d.ts +125 -5
  27. package/dist/providers/composio-sdk.js +901 -82
  28. package/dist/providers/microsoft-governance.d.ts +28 -1
  29. package/dist/providers/microsoft-governance.js +224 -11
  30. package/dist/providers/stripe-billing.d.ts +42 -0
  31. package/dist/providers/stripe-billing.js +230 -1
  32. package/dist/providers/stripe-projects.d.ts +455 -3
  33. package/dist/providers/stripe-projects.js +2570 -129
  34. package/dist/providers/supermemory.d.ts +74 -1
  35. package/dist/providers/supermemory.js +426 -0
  36. package/dist/provisioning-managed.d.ts +50 -0
  37. package/dist/provisioning-managed.js +155 -0
  38. package/dist/sdk.d.ts +440 -3
  39. package/dist/sdk.js +305 -3
  40. package/dist/server.js +2343 -264
  41. package/dist/store.d.ts +113 -6
  42. package/dist/store.js +677 -52
  43. package/dist/types.d.ts +207 -3
  44. package/dist/ui/console-html.d.ts +40 -1
  45. package/dist/ui/console-html.js +1813 -629
  46. package/dist/ui/design-tokens.js +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +4 -3
  50. package/plugins/muxo/.claude-plugin/plugin.json +2 -2
  51. package/plugins/muxo/.codex-plugin/plugin.json +15 -8
  52. package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
  53. package/plugins/muxo/hooks/session-start.sh +15 -3
  54. package/plugins/muxo/hooks/user-prompt-submit.sh +14 -1
  55. package/plugins/muxo/mcp.json +0 -10
  56. package/plugins/muxo/plugin.json +4 -4
  57. package/plugins/muxo/skills/muxo/SKILL.md +59 -9
  58. package/plugins/muxo/skills/muxo/references/cli.md +5 -1
  59. package/plugins/muxo/skills/muxo/references/mcp.md +13 -7
  60. package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
  61. package/plugins/muxo/skills/setup/SKILL.md +9 -1
  62. package/src/catalogues/stripe-projects-catalog.json +140 -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,1432 @@ 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
+ parameterRules: [],
243
+ languageMode: "hybrid",
244
+ customInstructions: [],
245
+ };
246
+ /**
247
+ * Compiles plain-English user guardrails (e.g. "Hey Muxo, don't go over 25 credits",
248
+ * "Only allow sending emails to @muxo.ai", or "Require approval for Stripe refunds over $50,
249
+ * auto-approve under $50") and/or structured MCP harness parameter rules into enforced
250
+ * GovernancePolicy fields while keeping human-readable rule descriptions.
251
+ */
252
+ export function parseNaturalLanguageGuardrail(prompt, currentPolicy = DEFAULT_WORKSPACE_POLICY, options) {
253
+ const cleanPrompt = String(prompt || "").trim();
254
+ const lower = cleanPrompt.toLowerCase();
255
+ const patch = {};
256
+ const appliedSummaries = [];
257
+ let compiledBy = options?.source === "mcp_harness"
258
+ ? "mcp_harness"
259
+ : options?.source === "llm"
260
+ ? "llm"
261
+ : "deterministic";
262
+ if (options?.policyPatch && typeof options.policyPatch === "object") {
263
+ try {
264
+ const sanitized = sanitizePolicyPatch(options.policyPatch);
265
+ Object.assign(patch, sanitized);
266
+ }
267
+ catch {
268
+ // Ignore invalid partial fields in optional patch
269
+ }
270
+ }
271
+ 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*:|(?:reset|clear|restore)\s+(?:all\s+)?(?:guardrails?|policy|safety\s+rules?))/i.test(cleanPrompt);
272
+ // 0. Full policy / guardrail reset ("reset guardrails", "clear all guardrails", "restore default policy")
273
+ if (/\b(reset|clear|restore)\s+(?:all\s+)?(?:custom\s+)?(?:guardrails?|policy|safety\s+rules?)(?:\s+to\s+defaults?)?\b/i.test(lower) &&
274
+ !/\bparameter\s+rules\b/i.test(lower)) {
275
+ patch.mode = DEFAULT_WORKSPACE_POLICY.mode;
276
+ patch.requireApprovalFor = [...DEFAULT_WORKSPACE_POLICY.requireApprovalFor];
277
+ patch.allowInstantWallet = DEFAULT_WORKSPACE_POLICY.allowInstantWallet;
278
+ patch.allowProvisioning = DEFAULT_WORKSPACE_POLICY.allowProvisioning;
279
+ patch.maxCreditsPerRun = DEFAULT_WORKSPACE_POLICY.maxCreditsPerRun;
280
+ patch.blockedToolSlugs = [];
281
+ patch.parameterRules = [];
282
+ patch.executionRing = DEFAULT_WORKSPACE_POLICY.executionRing;
283
+ patch.blockDestructiveSql = DEFAULT_WORKSPACE_POLICY.blockDestructiveSql;
284
+ patch.naturalLanguageRules = [];
285
+ appliedSummaries.push("Reset all workspace guardrails, parameter rules, and approval thresholds to Safe Default");
286
+ }
287
+ // Detect whether a dollar amount in the sentence refers to a tool-specific transaction
288
+ // (e.g. "Stripe refunds over $50", "payments over $100", "transfers above $250")
289
+ // rather than the workspace's Muxo credit budget per run.
290
+ 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);
291
+ // 1. Budget / Credit / Dollar limits ("don't go over 25 credits", "max $0.50", "under 100 credits")
292
+ 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) ||
293
+ cleanPrompt.match(/\b(\d+)\s*credits?\s*(?:max|limit|cap|budget|per\s+run)\b/i);
294
+ const dollarMatch = !isTransactionalThresholdPrompt
295
+ ? 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) ||
296
+ cleanPrompt.match(/\$\s*(\d+(?:\.\d+)?)\s*(?:max|limit|cap|budget|per\s+run)\b/i) ||
297
+ 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)
298
+ : null;
299
+ if (creditMatch) {
300
+ const credits = Math.max(0, Math.min(100_000, Math.round(Number(creditMatch[1]))));
301
+ patch.maxCreditsPerRun = credits;
302
+ appliedSummaries.push(`Budget cap set to ${credits} credits ($${(credits * 0.01).toFixed(2)}) per run`);
303
+ }
304
+ else if (dollarMatch) {
305
+ const dollars = Number(dollarMatch[1]);
306
+ const credits = Math.max(0, Math.min(100_000, Math.round(dollars * 100)));
307
+ patch.maxCreditsPerRun = credits;
308
+ appliedSummaries.push(`Budget cap set to $${dollars.toFixed(2)} (${credits} credits) per run`);
309
+ }
310
+ 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)) {
311
+ patch.maxCreditsPerRun = 0;
312
+ patch.allowInstantWallet = false;
313
+ appliedSummaries.push("Paid tools disabled (0 credits per run)");
314
+ }
315
+ // 2. Tool / Slug blocking ("don't use resource.projects.deprovision", "block app.gmail.send_email")
316
+ const existingBlocked = new Set(currentPolicy.blockedToolSlugs || []);
317
+ let blockedChanged = false;
318
+ if (/\b(unblock\s+all|allow\s+all\s+tools|clear\s+blocked\s+tools|reset\s+blocked)\b/i.test(lower)) {
319
+ existingBlocked.clear();
320
+ blockedChanged = true;
321
+ appliedSummaries.push("Cleared all blocked tools");
322
+ }
323
+ const slugMatches = cleanPrompt.match(/\b(?:instant|app|resource|billing)\.[a-z0-9_]+(?:\.[a-z0-9_]+)+\b/gi);
324
+ if (slugMatches && slugMatches.length > 0) {
325
+ const isUnblock = /\b(unblock|allow|enable|permit)\b/i.test(lower) &&
326
+ !/\b(don'?t|never|do\s+not|block|disable)\b/i.test(lower);
327
+ for (const rawSlug of slugMatches) {
328
+ const slug = rawSlug.toLowerCase();
329
+ if (isUnblock) {
330
+ existingBlocked.delete(slug);
331
+ blockedChanged = true;
332
+ appliedSummaries.push(`Allowed tool ${slug}`);
333
+ }
334
+ else if (hasGuardrailIntent ||
335
+ /\b(don'?t|never|do\s+not|block|disable|forbid|stop)\b/i.test(lower)) {
336
+ existingBlocked.add(slug);
337
+ blockedChanged = true;
338
+ appliedSummaries.push(`Blocked tool ${slug}`);
339
+ }
340
+ }
341
+ }
342
+ // Friendly unconditional tool blocks ("don't send emails", "don't post to slack", "never deprovision")
343
+ // Do NOT unconditionally block the entire tool when the prompt is a conditional rule
344
+ // like "Only allow sending emails to @muxo.ai" or "Never post to #general" or "refunds over $50".
345
+ 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);
346
+ if (!isConditionalScope &&
347
+ /\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)) {
348
+ existingBlocked.add("app.gmail.send_email");
349
+ blockedChanged = true;
350
+ appliedSummaries.push("Blocked sending emails (app.gmail.send_email)");
351
+ }
352
+ if (!isConditionalScope &&
353
+ /\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)) {
354
+ existingBlocked.add("app.slack.send_message");
355
+ blockedChanged = true;
356
+ appliedSummaries.push("Blocked sending Slack messages (app.slack.send_message)");
357
+ }
358
+ 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)) {
359
+ existingBlocked.add("resource.projects.deprovision");
360
+ blockedChanged = true;
361
+ appliedSummaries.push("Blocked cloud deprovisioning (resource.projects.deprovision)");
362
+ }
363
+ if (!isConditionalScope &&
364
+ /\b(don'?t|never|do\s+not|block|disable|stop)\b[\s\S]*\b(refunds?|issue\s+refund)\b/i.test(lower)) {
365
+ existingBlocked.add("billing.stripe.create_refund");
366
+ blockedChanged = true;
367
+ appliedSummaries.push("Blocked issuing Stripe refunds (billing.stripe.create_refund)");
368
+ }
369
+ if (blockedChanged) {
370
+ patch.blockedToolSlugs = Array.from(existingBlocked);
371
+ }
372
+ // 3. Provisioning & Instant Wallet toggles
373
+ 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) &&
374
+ !/\bask\s+(?:me\s+)?before\b/i.test(lower)) {
375
+ patch.allowProvisioning = false;
376
+ appliedSummaries.push("Disabled cloud infrastructure provisioning");
377
+ }
378
+ else if (/\b(enable|allow|turn\s+on)\s+(?:cloud\s+)?provisioning\b/i.test(lower)) {
379
+ patch.allowProvisioning = true;
380
+ appliedSummaries.push("Enabled cloud infrastructure provisioning");
381
+ }
382
+ // 4. Destructive SQL protection
383
+ 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)) {
384
+ patch.blockDestructiveSql = true;
385
+ appliedSummaries.push("Enabled destructive SQL protection (DROP/TRUNCATE blocked)");
386
+ }
387
+ // 5. Execution & Approval postures ("ask me before...", "read-only mode", "strict manual", "autonomous")
388
+ 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)) {
389
+ patch.executionRing = 3;
390
+ patch.mode = "enforce";
391
+ patch.allowProvisioning = false;
392
+ patch.requireApprovalFor = [
393
+ "write",
394
+ "admin",
395
+ "financial",
396
+ "provisioning",
397
+ "destructive",
398
+ ];
399
+ appliedSummaries.push("Switched to Read-Only Sandbox mode (only read/search tools can run)");
400
+ }
401
+ 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)) {
402
+ patch.mode = "enforce";
403
+ patch.executionRing = 1;
404
+ patch.requireApprovalFor = [
405
+ "read",
406
+ "write",
407
+ "admin",
408
+ "financial",
409
+ "provisioning",
410
+ "destructive",
411
+ ];
412
+ appliedSummaries.push("Set to Ask Every Time (manual approval required before any tool runs)");
413
+ }
414
+ 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)) {
415
+ patch.mode = "enforce";
416
+ patch.executionRing = 1;
417
+ patch.requireApprovalFor = [
418
+ "write",
419
+ "admin",
420
+ "financial",
421
+ "provisioning",
422
+ "destructive",
423
+ ];
424
+ appliedSummaries.push("Set to Safe Default (reads run automatically; writes, billing, provisioning, and destructive actions ask first)");
425
+ }
426
+ else if (/\b(autonomous(\s+mode)?|hands[- ]off(\s+mode)?|auto[- ]approve\s+writes)\b/i.test(lower)) {
427
+ patch.mode = "enforce";
428
+ patch.executionRing = 0;
429
+ patch.requireApprovalFor = ["financial", "provisioning", "destructive"];
430
+ appliedSummaries.push("Set to Hands-Off mode (reads and writes run automatically; financial, provisioning, and destructive actions still ask)");
431
+ }
432
+ else if (/\b(monitor\s+mode|observation\s+mode|log\s+only|observe\s+without\s+blocking)\b/i.test(lower)) {
433
+ patch.mode = "monitor";
434
+ appliedSummaries.push("Switched to Monitor (Observation) mode (Muxo logs safety checks without blocking actions)");
435
+ }
436
+ else if (/\b(enforce\s+mode|strict\s+enforcement)\b/i.test(lower)) {
437
+ patch.mode = "enforce";
438
+ appliedSummaries.push("Switched to Enforce mode (safety guardrails actively block violations)");
439
+ }
440
+ // 6. Explicit execution ring (technical customization: "set execution ring to 2", "ring 0")
441
+ const ringMatch = cleanPrompt.match(/\b(?:execution\s+)?ring\s*([0123])\b/i);
442
+ if (ringMatch && patch.executionRing === undefined) {
443
+ const ring = Number(ringMatch[1]);
444
+ patch.executionRing = ring;
445
+ appliedSummaries.push(`Set execution ring to Ring ${ring}`);
446
+ }
447
+ // 7. Customizable language mode ("use non-technical language", "explain in technical detail", "hybrid language")
448
+ 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) &&
449
+ !/\b(both|hybrid)\b/i.test(lower)) {
450
+ patch.languageMode = "non_technical";
451
+ appliedSummaries.push("Set language mode to Non-Technical (plain English explanations without jargon)");
452
+ }
453
+ else if (/\b(technical\s+(?:language|mode|detail|explanations?)|developer\s+mode|engineering\s+detail)\b/i.test(lower) &&
454
+ !/\b(non[- ]technical|both|hybrid)\b/i.test(lower)) {
455
+ patch.languageMode = "technical";
456
+ appliedSummaries.push("Set language mode to Technical (cryptographic hashes, rings, and protocol facets included)");
457
+ }
458
+ 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)) {
459
+ patch.languageMode = "hybrid";
460
+ appliedSummaries.push("Set language mode to Hybrid (both plain-English summary and technical detail)");
461
+ }
462
+ // 8. Custom communication / operator instructions ("custom instruction: ...", "always explain ...")
463
+ const customInstructionMatch = cleanPrompt.match(/(?:custom\s+instruction|communication\s+style|explanation\s+style)\s*[:=-]\s*(.+)$/i) ||
464
+ cleanPrompt.match(/^((?:always|whenever\s+you\s+explain|in\s+all\s+explanations)\s+(?:show|include|explain|format|mention|use)\b.+)$/i);
465
+ if (customInstructionMatch) {
466
+ const instruction = customInstructionMatch[1].trim().replace(/[.]+$/, "");
467
+ if (instruction) {
468
+ const existingInstructions = Array.isArray(currentPolicy.customInstructions)
469
+ ? [...currentPolicy.customInstructions]
470
+ : [];
471
+ if (!existingInstructions.includes(instruction)) {
472
+ existingInstructions.unshift(instruction);
473
+ }
474
+ patch.customInstructions = existingInstructions.slice(0, 25);
475
+ appliedSummaries.push(`Added custom instruction: "${instruction}"`);
476
+ }
477
+ }
478
+ // 9. Conditional & Parameter-level policy rules
479
+ // ("Only allow sending emails to @muxo.ai", "Require approval for Stripe refunds over $50, auto-approve under $50",
480
+ // "Never post to #general", "Only allow GitHub writes in muxo/platform", plus structured MCP harness rules)
481
+ const existingParamRules = Array.isArray(currentPolicy.parameterRules)
482
+ ? [...currentPolicy.parameterRules]
483
+ : [];
484
+ let paramRulesChanged = false;
485
+ const upsertParamRule = (rule) => {
486
+ const filtered = existingParamRules.filter((r) => r.id !== rule.id);
487
+ filtered.unshift(rule);
488
+ existingParamRules.splice(0, existingParamRules.length, ...filtered);
489
+ paramRulesChanged = true;
490
+ appliedSummaries.push(rule.description);
491
+ };
492
+ if (/\b(clear|reset|remove)\s+(?:all\s+)?parameter\s+rules\b/i.test(lower)) {
493
+ existingParamRules.length = 0;
494
+ paramRulesChanged = true;
495
+ appliedSummaries.push("Cleared all parameter-level policy rules");
496
+ }
497
+ // 9a. Pre-compiled structured rules supplied by an MCP LLM harness or API caller
498
+ if (Array.isArray(options?.parameterRules) && options.parameterRules.length > 0) {
499
+ for (let i = 0; i < options.parameterRules.length; i++) {
500
+ const raw = options.parameterRules[i];
501
+ if (raw && typeof raw === "object" && typeof raw.parameter === "string") {
502
+ const sanitized = sanitizeParameterPolicyRule({ ...raw, source: raw.source || options?.source || "mcp_harness" }, i);
503
+ upsertParamRule(sanitized);
504
+ }
505
+ }
506
+ compiledBy = options?.source === "llm" ? "llm" : "mcp_harness";
507
+ }
508
+ // 9b. Email domain allowlist / blocklist ("only allow sending emails to @muxo.ai", "never email @competitor.com")
509
+ const emailDomains = cleanPrompt.match(/@[a-z0-9.-]+\.[a-z]{2,}/gi);
510
+ if (emailDomains &&
511
+ emailDomains.length > 0 &&
512
+ /\b(emails?|gmail|outlook|resend|sendgrid|recipients?|mail)\b/i.test(lower)) {
513
+ const normalizedDomains = [
514
+ ...new Set(emailDomains.map((d) => d.toLowerCase())),
515
+ ];
516
+ const isBlocklist = /\b(don'?t|never|do\s+not|block|forbid)\b/i.test(lower) &&
517
+ !/\b(only|must|restrict)\b/i.test(lower);
518
+ const action = /\b(ask|require\s+approval)\b/i.test(lower)
519
+ ? "require_approval"
520
+ : "block";
521
+ if (isBlocklist) {
522
+ upsertParamRule({
523
+ id: `prule_email_block_${normalizedDomains.join("_").replace(/[^a-z0-9]/g, "_")}`,
524
+ toolPattern: "*email*,*gmail*,*outlook*,*resend*,*sendgrid*",
525
+ parameter: "to,recipient,email,recipients,cc,bcc",
526
+ operator: "domain_not_in",
527
+ value: normalizedDomains,
528
+ action,
529
+ description: `Block sending emails to ${normalizedDomains.join(", ")}`,
530
+ source: "deterministic",
531
+ });
532
+ }
533
+ else if (normalizedDomains.length === 1) {
534
+ const domain = normalizedDomains[0];
535
+ upsertParamRule({
536
+ id: `prule_email_${domain.replace(/[^a-z0-9]/g, "_")}`,
537
+ toolPattern: "*email*,*gmail*,*outlook*,*resend*,*sendgrid*",
538
+ parameter: "to,recipient,email,recipients,cc,bcc",
539
+ operator: "must_end_with",
540
+ value: domain,
541
+ action,
542
+ description: `Email recipient (to) must end with ${domain}`,
543
+ source: "deterministic",
544
+ });
545
+ }
546
+ else {
547
+ upsertParamRule({
548
+ id: `prule_email_domains_${normalizedDomains.join("_").replace(/[^a-z0-9]/g, "_")}`,
549
+ toolPattern: "*email*,*gmail*,*outlook*,*resend*,*sendgrid*",
550
+ parameter: "to,recipient,email,recipients,cc,bcc",
551
+ operator: "domain_in",
552
+ value: normalizedDomains,
553
+ action,
554
+ description: `Only allow sending emails to ${normalizedDomains.join(", ")}`,
555
+ source: "deterministic",
556
+ });
557
+ }
558
+ }
559
+ // 9c. Transactional / Financial conditional thresholds
560
+ // e.g. "Require approval for Stripe refunds over $50, auto-approve under $50"
561
+ // or "Block refunds over $200" or "Auto-approve refunds under $50"
562
+ if (isTransactionalThresholdPrompt) {
563
+ 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);
564
+ 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);
565
+ const targetNoun = /\brefunds?\b/i.test(lower)
566
+ ? { slug: "refund", pattern: "*refund*", label: "Stripe refunds" }
567
+ : /\b(transfers?|payouts?)\b/i.test(lower)
568
+ ? { slug: "transfer", pattern: "*transfer*,*payout*", label: "transfers/payouts" }
569
+ : /\binvoices?\b/i.test(lower)
570
+ ? { slug: "invoice", pattern: "*invoice*", label: "invoices" }
571
+ : {
572
+ slug: "payment",
573
+ pattern: "*refund*,*stripe*,*payment*,*charge*",
574
+ label: "Stripe transactions",
575
+ };
576
+ if (overMatch || underAutoMatch) {
577
+ const thresholdVal = overMatch
578
+ ? Number(overMatch[2])
579
+ : Number(underAutoMatch[1]);
580
+ const isBlockOver = Boolean(overMatch?.[1] && /\b(block|deny|forbid)\b/i.test(overMatch[1])) ||
581
+ (/\b(block|deny|forbid|never\s+allow)\b/i.test(lower) &&
582
+ !/\b(require\s+approval|ask)\b/i.test(lower));
583
+ const hasAutoApproveUnder = Boolean(underAutoMatch) || /\bauto[- ]?approve\b/i.test(lower);
584
+ const action = isBlockOver ? "block" : "require_approval";
585
+ const elseAction = hasAutoApproveUnder
586
+ ? "auto_approve"
587
+ : undefined;
588
+ upsertParamRule({
589
+ id: `prule_${targetNoun.slug}_usd_${thresholdVal}`,
590
+ toolPattern: targetNoun.pattern,
591
+ parameter: "amount,amount_usd,amount_cents,refund_amount,total,value",
592
+ operator: "max_number",
593
+ value: thresholdVal,
594
+ unit: "usd",
595
+ action,
596
+ ...(elseAction ? { elseAction } : {}),
597
+ description: `${action === "block" ? "Block" : "Require approval for"} ${targetNoun.label} over $${thresholdVal}${elseAction ? `, auto-approve $${thresholdVal} and under` : ""}`,
598
+ source: "deterministic",
599
+ });
600
+ }
601
+ }
602
+ // 9d. Slack channel allowlist / blocklist ("never post to #general", "only allow Slack messages to #deployments")
603
+ const slackChannels = cleanPrompt.match(/#[a-z0-9_-]+/gi);
604
+ if (slackChannels && slackChannels.length > 0) {
605
+ const normalizedChannels = [
606
+ ...new Set(slackChannels.map((c) => c.toLowerCase())),
607
+ ];
608
+ const isOnlyAllow = /\b(only\s+(?:allow|post|send)|restrict\s+(?:slack|messages?)\s+to)\b/i.test(lower);
609
+ if (isOnlyAllow) {
610
+ upsertParamRule({
611
+ id: `prule_slack_allow_${normalizedChannels.join("_").replace(/[^a-z0-9]/g, "_")}`,
612
+ toolPattern: "*slack*",
613
+ parameter: "channel,channel_name",
614
+ operator: normalizedChannels.length === 1 ? "must_equal" : "in",
615
+ value: normalizedChannels.length === 1
616
+ ? normalizedChannels[0]
617
+ : normalizedChannels,
618
+ action: "block",
619
+ description: `Only allow Slack messages to ${normalizedChannels.join(", ")}`,
620
+ source: "deterministic",
621
+ });
622
+ }
623
+ else if (/\b(don'?t|never|do\s+not|block)\b/i.test(lower)) {
624
+ upsertParamRule({
625
+ id: `prule_slack_${normalizedChannels.join("_").replace(/[^a-z0-9]/g, "_")}`,
626
+ toolPattern: "*slack*",
627
+ parameter: "channel,channel_name",
628
+ operator: normalizedChannels.length === 1 ? "not_equals" : "not_in",
629
+ value: normalizedChannels.length === 1
630
+ ? normalizedChannels[0]
631
+ : normalizedChannels,
632
+ action: "block",
633
+ description: `Block Slack messages to ${normalizedChannels.join(", ")}`,
634
+ source: "deterministic",
635
+ });
636
+ }
637
+ }
638
+ // 9e. GitHub repository restriction ("only allow GitHub writes in muxo/platform")
639
+ 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);
640
+ if (githubRepoMatch) {
641
+ const repo = githubRepoMatch[1].toLowerCase();
642
+ upsertParamRule({
643
+ id: `prule_github_repo_${repo.replace(/[^a-z0-9]/g, "_")}`,
644
+ toolPattern: "*github*",
645
+ parameter: "repo,repository,full_name",
646
+ operator: "must_equal",
647
+ value: repo,
648
+ action: "block",
649
+ description: `Only allow GitHub operations on repository ${repo}`,
650
+ source: "deterministic",
651
+ });
652
+ }
653
+ // 9f. Generic parameter numeric threshold ("block when amount exceeds 500", "require approval when parameter count > 10, auto-approve under 10")
654
+ const paramCapMatch = !isTransactionalThresholdPrompt
655
+ ? 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)
656
+ : null;
657
+ if (paramCapMatch) {
658
+ const actionVerb = paramCapMatch[1].toLowerCase();
659
+ const paramName = paramCapMatch[2];
660
+ const maxVal = Number(paramCapMatch[3]);
661
+ const action = actionVerb.startsWith("block")
662
+ ? "block"
663
+ : "require_approval";
664
+ const hasAutoApprove = /\bauto[- ]?approve\b/i.test(lower);
665
+ upsertParamRule({
666
+ id: `prule_max_${paramName.toLowerCase()}`,
667
+ toolPattern: "*",
668
+ parameter: paramName,
669
+ operator: "max_number",
670
+ value: maxVal,
671
+ action,
672
+ ...(hasAutoApprove ? { elseAction: "auto_approve" } : {}),
673
+ description: `${action === "block" ? "Block" : "Require approval"} when parameter '${paramName}' exceeds ${maxVal}${hasAutoApprove ? ` (auto-approve ${maxVal} and under)` : ""}`,
674
+ source: "deterministic",
675
+ });
676
+ }
677
+ if (paramRulesChanged) {
678
+ patch.parameterRules = existingParamRules.slice(0, 50);
679
+ }
680
+ const isGuardrail = Boolean(cleanPrompt || paramRulesChanged) &&
681
+ (hasGuardrailIntent || appliedSummaries.length > 0);
682
+ if (isGuardrail && cleanPrompt) {
683
+ const existingRules = Array.isArray(currentPolicy.naturalLanguageRules)
684
+ ? [...currentPolicy.naturalLanguageRules]
685
+ : [];
686
+ const normalizedRule = cleanPrompt.replace(/^hey\s+muxo[,:\s]*/i, "").trim();
687
+ const displayRule = normalizedRule.charAt(0).toUpperCase() + normalizedRule.slice(1);
688
+ if (displayRule && !existingRules.includes(displayRule)) {
689
+ existingRules.unshift(displayRule);
690
+ }
691
+ patch.naturalLanguageRules = existingRules.slice(0, 25);
692
+ if (!patch.mode && !currentPolicy.mode) {
693
+ patch.mode = "enforce";
694
+ }
695
+ }
696
+ const nextPolicy = {
697
+ ...DEFAULT_WORKSPACE_POLICY,
698
+ ...currentPolicy,
699
+ ...patch,
700
+ };
701
+ const plain = describePolicyInPlainEnglish(nextPolicy);
702
+ const summary = appliedSummaries.length > 0
703
+ ? appliedSummaries.join(" · ")
704
+ : `Saved workspace guardrail: "${cleanPrompt}"`;
705
+ return {
706
+ isGuardrail,
707
+ patch,
708
+ ruleText: cleanPrompt,
709
+ summary,
710
+ humanRules: plain.rules,
711
+ compiledBy,
712
+ memoryFact: {
713
+ title: `Workspace Guardrail: ${summary.slice(0, 80)}`,
714
+ content: `User instructed Muxo guardrail: "${cleanPrompt}". Enforced effect: ${summary}.`,
715
+ subject: "workspace_guardrail",
716
+ predicate: "enforces_rule",
717
+ object: summary,
718
+ tags: ["guardrail", "safety", "user_rule"],
719
+ },
720
+ };
721
+ }
722
+ /**
723
+ * Async LLM-assisted policy compiler: first runs deterministic semantic compilation
724
+ * (plus any structured `parameterRules` supplied by an MCP LLM harness), and when an
725
+ * OpenAI or Anthropic key is available and a complex natural-language rule produced no
726
+ * structured patch fields, invokes the LLM to synthesize validated `ParameterPolicyRule[]`.
727
+ */
728
+ export async function compileNaturalLanguageGuardrailWithLlm(prompt, currentPolicy = DEFAULT_WORKSPACE_POLICY, options) {
729
+ const deterministic = parseNaturalLanguageGuardrail(prompt, currentPolicy, options);
730
+ const hasCompiledEffect = Object.keys(deterministic.patch).some((k) => k !== "naturalLanguageRules" && k !== "mode") || Boolean(options?.parameterRules?.length);
731
+ const apiKey = options?.llmApiKey ||
732
+ (typeof process !== "undefined"
733
+ ? process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY
734
+ : undefined);
735
+ if (hasCompiledEffect || !apiKey || !prompt.trim()) {
736
+ return deterministic;
737
+ }
738
+ const systemPrompt = [
739
+ "You compile plain-English AI agent safety guardrails into JSON policy rules for Muxo.",
740
+ "Return ONLY a JSON object with optional fields:",
741
+ "- parameterRules: array of { id, toolPattern, parameter, operator, value, unit, action, elseAction, description }",
742
+ " 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",
743
+ " and action / elseAction are one of: block, require_approval, auto_approve, allow",
744
+ " and unit is optional: usd, cents, credits, raw.",
745
+ "- maxCreditsPerRun: optional number (100 credits = $1.00)",
746
+ "- blockedToolSlugs: optional string[]",
747
+ ].join("\n");
748
+ try {
749
+ const isAnthropic = options?.llmProvider === "anthropic" || apiKey.startsWith("sk-ant-");
750
+ let rawJsonText = "";
751
+ if (isAnthropic) {
752
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
753
+ method: "POST",
754
+ headers: {
755
+ "Content-Type": "application/json",
756
+ "x-api-key": apiKey,
757
+ "anthropic-version": "2023-06-01",
758
+ },
759
+ body: JSON.stringify({
760
+ model: "claude-3-5-haiku-latest",
761
+ max_tokens: 512,
762
+ system: systemPrompt,
763
+ messages: [{ role: "user", content: prompt }],
764
+ }),
765
+ });
766
+ if (res.ok) {
767
+ const data = (await res.json());
768
+ rawJsonText = String(data?.content?.[0]?.text || "");
769
+ }
770
+ }
771
+ else {
772
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
773
+ method: "POST",
774
+ headers: {
775
+ "Content-Type": "application/json",
776
+ Authorization: `Bearer ${apiKey}`,
777
+ },
778
+ body: JSON.stringify({
779
+ model: "gpt-4o-mini",
780
+ temperature: 0,
781
+ response_format: { type: "json_object" },
782
+ messages: [
783
+ { role: "system", content: systemPrompt },
784
+ { role: "user", content: prompt },
785
+ ],
786
+ }),
787
+ });
788
+ if (res.ok) {
789
+ const data = (await res.json());
790
+ rawJsonText = String(data?.choices?.[0]?.message?.content || "");
791
+ }
792
+ }
793
+ const jsonMatch = rawJsonText.match(/\{[\s\S]*\}/);
794
+ if (jsonMatch) {
795
+ const parsedLlm = JSON.parse(jsonMatch[0]);
796
+ return parseNaturalLanguageGuardrail(prompt, currentPolicy, {
797
+ ...options,
798
+ parameterRules: Array.isArray(parsedLlm.parameterRules)
799
+ ? parsedLlm.parameterRules
800
+ : undefined,
801
+ policyPatch: parsedLlm,
802
+ source: "llm",
803
+ });
804
+ }
805
+ }
806
+ catch {
807
+ // Fall back to deterministic compilation if LLM call fails or times out
808
+ }
809
+ return deterministic;
810
+ }
811
+ export function describePolicyInPlainEnglish(policy = DEFAULT_WORKSPACE_POLICY) {
812
+ const rules = [];
813
+ const badges = [];
814
+ const reqSet = new Set(policy.requireApprovalFor || []);
815
+ if (policy.killSwitchActive) {
816
+ rules.push(`All actions paused: ${policy.killSwitchReason || "Workspace pause is active."}`);
817
+ badges.push({ label: "Paused", tone: "strict" });
818
+ }
819
+ else if (policy.executionRing === 3) {
820
+ rules.push("Read-only mode: Muxo can search and read data, but cannot modify apps, spend money, or provision infrastructure.");
821
+ badges.push({ label: "Read-only sandbox", tone: "strict" });
822
+ }
823
+ else if (reqSet.has("read")) {
824
+ rules.push("Ask every time: Muxo will ask for your approval before running any tool.");
825
+ badges.push({ label: "Ask every time", tone: "strict" });
826
+ }
827
+ else if (!reqSet.has("write") && policy.mode === "enforce") {
828
+ rules.push("Hands-off mode: Muxo runs read and write tasks automatically, and asks before financial, cloud provisioning, or destructive actions.");
829
+ badges.push({ label: "Hands-off mode", tone: "warn" });
830
+ }
831
+ else if (policy.mode === "monitor") {
832
+ rules.push("Observation mode: Muxo logs safety checks without blocking actions.");
833
+ badges.push({ label: "Monitor mode", tone: "warn" });
834
+ }
835
+ else {
836
+ rules.push("Safe default: Searches and reads run automatically; Muxo asks before changing data, spending money, or provisioning cloud services.");
837
+ badges.push({ label: "Safe default", tone: "safe" });
838
+ }
839
+ const maxCredits = Number(policy.maxCreditsPerRun ?? 50);
840
+ const dollars = (maxCredits * 0.01).toFixed(2);
841
+ rules.push(`Budget limit: Don't spend more than ${maxCredits} credits ($${dollars}) on a single run.`);
842
+ badges.push({
843
+ label: `Max ${maxCredits} cr ($${dollars})/run`,
844
+ tone: "safe",
845
+ });
846
+ if (policy.allowProvisioning === false) {
847
+ rules.push("Cloud infrastructure: Provisioning new databases or cloud services is turned off.");
848
+ badges.push({ label: "No cloud provisioning", tone: "strict" });
849
+ }
850
+ if (policy.allowInstantWallet === false) {
851
+ rules.push("Paid instant tools: Turned off (only free and connected OAuth tools are allowed).");
852
+ }
853
+ if (policy.blockDestructiveSql !== false) {
854
+ rules.push("Database safety: Destructive SQL statements (DROP, TRUNCATE, DELETE without WHERE) are blocked.");
855
+ badges.push({ label: "SQL protected", tone: "safe" });
856
+ }
857
+ if (policy.blockedToolSlugs && policy.blockedToolSlugs.length > 0) {
858
+ rules.push(`Off-limits tools: Never use ${policy.blockedToolSlugs.join(", ")}.`);
859
+ badges.push({
860
+ label: `${policy.blockedToolSlugs.length} blocked tool${policy.blockedToolSlugs.length === 1 ? "" : "s"}`,
861
+ tone: "strict",
862
+ });
863
+ }
864
+ if (policy.parameterRules && policy.parameterRules.length > 0) {
865
+ for (const pr of policy.parameterRules) {
866
+ rules.push(`Parameter guardrail: ${pr.description}.`);
867
+ }
868
+ badges.push({
869
+ label: `${policy.parameterRules.length} param rule${policy.parameterRules.length === 1 ? "" : "s"}`,
870
+ tone: "strict",
871
+ });
872
+ }
873
+ const languageMode = policy.languageMode || "hybrid";
874
+ const customInstructions = Array.isArray(policy.customInstructions)
875
+ ? policy.customInstructions
876
+ : [];
877
+ if (languageMode === "non_technical") {
878
+ badges.push({ label: "Plain-English mode", tone: "safe" });
879
+ }
880
+ else if (languageMode === "technical") {
881
+ badges.push({ label: "Technical mode", tone: "safe" });
882
+ }
883
+ const activeCustomRules = Array.isArray(policy.naturalLanguageRules)
884
+ ? policy.naturalLanguageRules
885
+ : [];
886
+ const headline = rules[0] || "Safe default guardrails active.";
887
+ const nonTechnical = [
888
+ ...rules,
889
+ ...(activeCustomRules.length > 0
890
+ ? [`Custom rules: ${activeCustomRules.join(" · ")}`]
891
+ : []),
892
+ ].join(" ");
893
+ const ringNum = policy.executionRing ?? 1;
894
+ const ringNames = {
895
+ 0: "Ring 0 (Root/Admin)",
896
+ 1: "Ring 1 (Privileged Specialist)",
897
+ 2: "Ring 2 (Standard Read/Write)",
898
+ 3: "Ring 3 (Sandboxed Read-Only)",
899
+ };
900
+ 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}`;
901
+ const explanation = formatHumanFriendlyExplanation({
902
+ headline,
903
+ nonTechnical,
904
+ technical,
905
+ policy,
906
+ languageMode,
907
+ });
908
+ return {
909
+ headline,
910
+ rules,
911
+ activeCustomRules,
912
+ badges,
913
+ languageMode,
914
+ customInstructions,
915
+ nonTechnical: explanation.nonTechnical,
916
+ technical: explanation.technical,
917
+ preferred: explanation.preferred,
918
+ explanation,
919
+ };
920
+ }
921
+ function matchesSingleToolPattern(toolSlug, pattern = "*") {
922
+ const slug = toolSlug.toLowerCase();
923
+ const pat = (pattern || "*").toLowerCase().trim();
924
+ if (!pat || pat === "*")
925
+ return true;
926
+ if (pat.startsWith("*") && pat.endsWith("*")) {
927
+ return slug.includes(pat.slice(1, -1));
928
+ }
929
+ if (pat.startsWith("*")) {
930
+ return slug.endsWith(pat.slice(1));
931
+ }
932
+ if (pat.endsWith("*")) {
933
+ return slug.startsWith(pat.slice(0, -1));
934
+ }
935
+ return slug === pat || slug.includes(pat);
936
+ }
937
+ function matchesToolPattern(toolSlug, pattern = "*") {
938
+ const raw = (pattern || "*").trim();
939
+ if (!raw || raw === "*")
940
+ return true;
941
+ const subpatterns = raw
942
+ .split(",")
943
+ .map((s) => s.trim())
944
+ .filter(Boolean);
945
+ if (subpatterns.length === 0)
946
+ return true;
947
+ return subpatterns.some((sub) => matchesSingleToolPattern(toolSlug, sub));
948
+ }
949
+ export function evaluateParameterPolicyRules(paramsOrToolSlug, maybeInput, maybePolicy) {
950
+ const params = typeof paramsOrToolSlug === "string"
951
+ ? {
952
+ toolSlug: paramsOrToolSlug,
953
+ input: maybeInput || {},
954
+ policy: maybePolicy,
955
+ }
956
+ : paramsOrToolSlug;
957
+ const rules = params.policy?.parameterRules;
958
+ if (!Array.isArray(rules) || rules.length === 0) {
959
+ return {
960
+ violated: false,
961
+ allowed: true,
962
+ explicitAllow: false,
963
+ action: "allow",
964
+ matchedRule: null,
965
+ violatedRule: null,
966
+ reason: null,
967
+ };
968
+ }
969
+ const input = params.input || {};
970
+ let explicitAllowRule = null;
971
+ let explicitAllowAction = "allow";
972
+ for (const rule of rules) {
973
+ if (!matchesToolPattern(params.toolSlug, rule.toolPattern))
974
+ continue;
975
+ const baseCandidates = String(rule.parameter || "")
976
+ .split(",")
977
+ .map((s) => s.trim())
978
+ .filter(Boolean);
979
+ const candidateSet = new Set(baseCandidates);
980
+ if (candidateSet.has("to")) {
981
+ for (const alias of [
982
+ "to",
983
+ "recipient",
984
+ "email",
985
+ "recipient_email",
986
+ "recipients",
987
+ "cc",
988
+ "bcc",
989
+ ]) {
990
+ candidateSet.add(alias);
991
+ }
992
+ }
993
+ if (candidateSet.has("amount")) {
994
+ for (const alias of [
995
+ "amount",
996
+ "amount_usd",
997
+ "amount_cents",
998
+ "total",
999
+ "value",
1000
+ ]) {
1001
+ candidateSet.add(alias);
1002
+ }
1003
+ }
1004
+ const paramCandidates = Array.from(candidateSet);
1005
+ const isRecipientRule = candidateSet.has("to");
1006
+ const matchedEntries = [];
1007
+ for (const key of paramCandidates) {
1008
+ if (input[key] !== undefined && input[key] !== null) {
1009
+ const val = input[key];
1010
+ if (isRecipientRule && Array.isArray(val)) {
1011
+ for (const item of val) {
1012
+ matchedEntries.push({ key, value: item });
1013
+ }
1014
+ }
1015
+ else if (isRecipientRule &&
1016
+ typeof val === "string" &&
1017
+ val.includes(",")) {
1018
+ for (const part of val
1019
+ .split(",")
1020
+ .map((s) => s.trim())
1021
+ .filter(Boolean)) {
1022
+ matchedEntries.push({ key, value: part });
1023
+ }
1024
+ }
1025
+ else {
1026
+ matchedEntries.push({ key, value: val });
1027
+ }
1028
+ if (!isRecipientRule)
1029
+ break;
1030
+ }
1031
+ }
1032
+ if (matchedEntries.length === 0)
1033
+ continue;
1034
+ for (const entry of matchedEntries) {
1035
+ const rawVal = entry.value;
1036
+ const matchedKey = entry.key;
1037
+ const strVal = String(rawVal).trim().toLowerCase();
1038
+ const targetStr = String(rule.value ?? "").trim().toLowerCase();
1039
+ let rawNum = Number(rawVal);
1040
+ if (Number.isFinite(rawNum)) {
1041
+ if (rule.unit === "usd" && matchedKey === "amount_cents") {
1042
+ rawNum = rawNum / 100;
1043
+ }
1044
+ else if (rule.unit === "cents" && matchedKey === "amount_usd") {
1045
+ rawNum = rawNum * 100;
1046
+ }
1047
+ }
1048
+ const thresholdNum = Number(rule.value);
1049
+ const hasValidNumbers = Number.isFinite(rawNum) && Number.isFinite(thresholdNum);
1050
+ let conditionTriggered = false;
1051
+ switch (rule.operator) {
1052
+ case "ends_with":
1053
+ case "must_end_with":
1054
+ conditionTriggered =
1055
+ rule.action === "allow" || rule.action === "auto_approve"
1056
+ ? strVal.endsWith(targetStr)
1057
+ : !strVal.endsWith(targetStr);
1058
+ break;
1059
+ case "starts_with":
1060
+ case "must_start_with":
1061
+ conditionTriggered =
1062
+ rule.action === "allow" || rule.action === "auto_approve"
1063
+ ? strVal.startsWith(targetStr)
1064
+ : !strVal.startsWith(targetStr);
1065
+ break;
1066
+ case "equals":
1067
+ case "must_equal":
1068
+ conditionTriggered =
1069
+ rule.action === "allow" || rule.action === "auto_approve"
1070
+ ? strVal === targetStr
1071
+ : strVal !== targetStr;
1072
+ break;
1073
+ case "not_equals":
1074
+ conditionTriggered = strVal === targetStr;
1075
+ break;
1076
+ case "contains":
1077
+ conditionTriggered =
1078
+ rule.action === "allow" || rule.action === "auto_approve"
1079
+ ? strVal.includes(targetStr)
1080
+ : !strVal.includes(targetStr);
1081
+ break;
1082
+ case "not_contains":
1083
+ conditionTriggered = strVal.includes(targetStr);
1084
+ break;
1085
+ case "domain_in": {
1086
+ const allowedDomains = (Array.isArray(rule.value)
1087
+ ? rule.value
1088
+ : String(rule.value).split(","))
1089
+ .map((v) => String(v).trim().toLowerCase().replace(/^@/, ""))
1090
+ .filter(Boolean);
1091
+ const inDomain = allowedDomains.some((d) => strVal.endsWith(`@${d}`) || strVal === d);
1092
+ conditionTriggered =
1093
+ rule.action === "allow" || rule.action === "auto_approve"
1094
+ ? inDomain
1095
+ : !inDomain;
1096
+ break;
1097
+ }
1098
+ case "domain_not_in": {
1099
+ const blockedDomains = (Array.isArray(rule.value)
1100
+ ? rule.value
1101
+ : String(rule.value).split(","))
1102
+ .map((v) => String(v).trim().toLowerCase().replace(/^@/, ""))
1103
+ .filter(Boolean);
1104
+ conditionTriggered = blockedDomains.some((d) => strVal.endsWith(`@${d}`) || strVal === d);
1105
+ break;
1106
+ }
1107
+ case "max_number":
1108
+ if (hasValidNumbers) {
1109
+ conditionTriggered = rawNum > thresholdNum;
1110
+ }
1111
+ break;
1112
+ case "min_number":
1113
+ if (hasValidNumbers) {
1114
+ conditionTriggered = rawNum < thresholdNum;
1115
+ }
1116
+ break;
1117
+ case "gt":
1118
+ if (hasValidNumbers) {
1119
+ conditionTriggered = rawNum > thresholdNum;
1120
+ }
1121
+ break;
1122
+ case "gte":
1123
+ if (hasValidNumbers) {
1124
+ conditionTriggered = rawNum >= thresholdNum;
1125
+ }
1126
+ break;
1127
+ case "lt":
1128
+ if (hasValidNumbers) {
1129
+ conditionTriggered = rawNum < thresholdNum;
1130
+ }
1131
+ break;
1132
+ case "lte":
1133
+ if (hasValidNumbers) {
1134
+ conditionTriggered = rawNum <= thresholdNum;
1135
+ }
1136
+ break;
1137
+ case "in": {
1138
+ const allowed = (Array.isArray(rule.value)
1139
+ ? rule.value
1140
+ : String(rule.value).split(","))
1141
+ .map((v) => String(v).trim().toLowerCase())
1142
+ .filter(Boolean);
1143
+ const isMember = allowed.includes(strVal);
1144
+ conditionTriggered =
1145
+ rule.action === "allow" || rule.action === "auto_approve"
1146
+ ? isMember
1147
+ : !isMember;
1148
+ break;
1149
+ }
1150
+ case "not_in": {
1151
+ const blocked = (Array.isArray(rule.value)
1152
+ ? rule.value
1153
+ : String(rule.value).split(","))
1154
+ .map((v) => String(v).trim().toLowerCase())
1155
+ .filter(Boolean);
1156
+ conditionTriggered = blocked.includes(strVal);
1157
+ break;
1158
+ }
1159
+ case "regex": {
1160
+ try {
1161
+ const matches = new RegExp(String(rule.value), "i").test(String(rawVal));
1162
+ conditionTriggered =
1163
+ rule.action === "allow" || rule.action === "auto_approve"
1164
+ ? matches
1165
+ : !matches;
1166
+ }
1167
+ catch {
1168
+ conditionTriggered = false;
1169
+ }
1170
+ break;
1171
+ }
1172
+ }
1173
+ const effectiveAction = conditionTriggered
1174
+ ? rule.action
1175
+ : rule.elseAction;
1176
+ if (effectiveAction === "block" ||
1177
+ effectiveAction === "require_approval") {
1178
+ return {
1179
+ violated: true,
1180
+ allowed: false,
1181
+ explicitAllow: false,
1182
+ action: effectiveAction,
1183
+ matchedRule: rule,
1184
+ violatedRule: rule,
1185
+ reason: `Parameter policy rule triggered (${rule.description}): ${matchedKey}=${JSON.stringify(rawVal)}`,
1186
+ };
1187
+ }
1188
+ if (effectiveAction === "auto_approve" ||
1189
+ effectiveAction === "allow") {
1190
+ explicitAllowRule = rule;
1191
+ if (effectiveAction === "auto_approve") {
1192
+ explicitAllowAction = "auto_approve";
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+ return {
1198
+ violated: false,
1199
+ allowed: true,
1200
+ explicitAllow: Boolean(explicitAllowRule),
1201
+ action: explicitAllowRule ? explicitAllowAction : "allow",
1202
+ matchedRule: explicitAllowRule,
1203
+ violatedRule: null,
1204
+ reason: explicitAllowRule
1205
+ ? `Parameter policy rule satisfied (${explicitAllowRule.description})`
1206
+ : null,
1207
+ };
1208
+ }
1209
+ export function describeParameterRuleCompliance(rule) {
1210
+ const paramLabel = rule.parameter.split(",")[0] || rule.parameter;
1211
+ const valStr = Array.isArray(rule.value)
1212
+ ? rule.value.join(", ")
1213
+ : String(rule.value);
1214
+ const unitSuffix = rule.unit && rule.unit !== "raw" ? ` ${rule.unit.toUpperCase()}` : "";
1215
+ const elseNote = rule.elseAction
1216
+ ? `; otherwise action is '${rule.elseAction}'`
1217
+ : "";
1218
+ switch (rule.operator) {
1219
+ case "must_end_with":
1220
+ case "ends_with":
1221
+ return `For tools matching '${rule.toolPattern}', parameter '${paramLabel}' must end with '${valStr}' (violation -> ${rule.action}${elseNote}).`;
1222
+ case "domain_in":
1223
+ return `For tools matching '${rule.toolPattern}', recipient domain in '${paramLabel}' must be in [${valStr}] (violation -> ${rule.action}${elseNote}).`;
1224
+ case "domain_not_in":
1225
+ return `For tools matching '${rule.toolPattern}', recipient domain in '${paramLabel}' must NOT be in [${valStr}] (violation -> ${rule.action}${elseNote}).`;
1226
+ case "max_number":
1227
+ case "gt":
1228
+ case "gte":
1229
+ return `For tools matching '${rule.toolPattern}', keep '${paramLabel}' <= ${valStr}${unitSuffix} (exceeding ${valStr}${unitSuffix} triggers '${rule.action}'${elseNote}).`;
1230
+ case "min_number":
1231
+ case "lt":
1232
+ case "lte":
1233
+ return `For tools matching '${rule.toolPattern}', keep '${paramLabel}' >= ${valStr}${unitSuffix} (below ${valStr}${unitSuffix} triggers '${rule.action}'${elseNote}).`;
1234
+ case "must_equal":
1235
+ case "equals":
1236
+ return `For tools matching '${rule.toolPattern}', parameter '${paramLabel}' must equal '${valStr}' (violation -> ${rule.action}${elseNote}).`;
1237
+ case "not_equals":
1238
+ return `For tools matching '${rule.toolPattern}', parameter '${paramLabel}' must NOT equal '${valStr}' (violation -> ${rule.action}${elseNote}).`;
1239
+ case "in":
1240
+ return `For tools matching '${rule.toolPattern}', parameter '${paramLabel}' must be one of [${valStr}] (violation -> ${rule.action}${elseNote}).`;
1241
+ case "not_in":
1242
+ return `For tools matching '${rule.toolPattern}', parameter '${paramLabel}' must NOT be in [${valStr}] (violation -> ${rule.action}${elseNote}).`;
1243
+ default:
1244
+ return `${rule.description} (toolPattern='${rule.toolPattern}', parameter='${rule.parameter}', operator='${rule.operator}', value=${JSON.stringify(rule.value)}, action='${rule.action}'${elseNote}).`;
1245
+ }
1246
+ }
1247
+ export function buildLlmPolicyAndGovernanceBrief(policy = DEFAULT_WORKSPACE_POLICY, options) {
1248
+ const plain = describePolicyInPlainEnglish(policy);
1249
+ const reqSet = new Set(policy.requireApprovalFor || []);
1250
+ const ring = (policy.executionRing ?? 1);
1251
+ const isPaused = Boolean(policy.killSwitchActive);
1252
+ const isReadOnly = ring === 3;
1253
+ const isMonitor = policy.mode === "monitor";
1254
+ const posture = isPaused
1255
+ ? "paused"
1256
+ : isReadOnly
1257
+ ? "read_only_sandbox"
1258
+ : isMonitor
1259
+ ? "monitor"
1260
+ : reqSet.has("read")
1261
+ ? "ask_every_time"
1262
+ : !reqSet.has("write")
1263
+ ? "hands_off"
1264
+ : "safe_default";
1265
+ const riskLaneMatrix = {};
1266
+ for (const risk of RISK_LEVELS) {
1267
+ const executeTool = `execute_${risk}`;
1268
+ if (isPaused && !isMonitor) {
1269
+ riskLaneMatrix[risk] = {
1270
+ riskLevel: risk,
1271
+ status: "blocked",
1272
+ executeTool,
1273
+ reason: `Blocked while workspace kill switch is active (${policy.killSwitchReason || "emergency pause"}).`,
1274
+ };
1275
+ }
1276
+ else if (isReadOnly && risk !== "read" && !isMonitor) {
1277
+ riskLaneMatrix[risk] = {
1278
+ riskLevel: risk,
1279
+ status: "blocked",
1280
+ executeTool,
1281
+ reason: "Blocked by Ring 3 Read-Only Sandbox mode (only 'read' tools are permitted).",
1282
+ };
1283
+ }
1284
+ else if (risk === "provisioning" && policy.allowProvisioning === false && !isMonitor) {
1285
+ riskLaneMatrix[risk] = {
1286
+ riskLevel: risk,
1287
+ status: "blocked",
1288
+ executeTool,
1289
+ reason: "Blocked because cloud infrastructure provisioning (allowProvisioning=false) is disabled.",
1290
+ };
1291
+ }
1292
+ else if (reqSet.has(risk) && !isMonitor) {
1293
+ riskLaneMatrix[risk] = {
1294
+ riskLevel: risk,
1295
+ status: "requires_approval",
1296
+ executeTool,
1297
+ reason: `Requires human owner approval before ${executeTool} can run (unless a conditional parameterRule explicitly sets elseAction='auto_approve').`,
1298
+ };
1299
+ }
1300
+ else {
1301
+ riskLaneMatrix[risk] = {
1302
+ riskLevel: risk,
1303
+ status: "auto_approved",
1304
+ executeTool,
1305
+ reason: isMonitor
1306
+ ? `Auto-approved in monitor mode; call ${executeTool} immediately after plan_action.`
1307
+ : `Auto-approved by workspace policy; call ${executeTool} immediately after plan_action.`,
1308
+ };
1309
+ }
1310
+ }
1311
+ const parameterRules = (policy.parameterRules || []).map((r) => ({
1312
+ ...r,
1313
+ howToComply: describeParameterRuleCompliance(r),
1314
+ }));
1315
+ const maxCredits = Number(policy.maxCreditsPerRun ?? 50);
1316
+ const maxUsd = Number((maxCredits * 0.01).toFixed(2));
1317
+ const activeDefaults = options?.activeDefaults || {};
1318
+ const defaultsPairs = Object.entries(activeDefaults).map(([k, v]) => `${k}=${JSON.stringify(v)}`);
1319
+ const autoApproveRiskLevels = RISK_LEVELS.filter((r) => riskLaneMatrix[r].status === "auto_approved");
1320
+ const requireApprovalRiskLevels = RISK_LEVELS.filter((r) => riskLaneMatrix[r].status === "requires_approval");
1321
+ const blockedLanes = RISK_LEVELS.filter((r) => riskLaneMatrix[r].status === "blocked");
1322
+ const directiveLines = [
1323
+ `## Muxo Policy & Governance Contract${options?.workspaceName ? ` (${options.workspaceName})` : ""}`,
1324
+ `- **Posture**: \`${posture}\` (mode=\`${policy.mode || "enforce"}\`, ring=\`Ring ${ring}\`, languageMode=\`${plain.languageMode}\`)`,
1325
+ `- **Summary**: ${plain.preferred}`,
1326
+ `- **Per-Run Budget Cap**: ${maxCredits} credits ($${maxUsd.toFixed(2)}) max per run`,
1327
+ `- **Auto-Approved Risk Lanes**: ${autoApproveRiskLevels.join(", ") || "none"}`,
1328
+ `- **Human-Approval Risk Lanes**: ${requireApprovalRiskLevels.join(", ") || "none"}`,
1329
+ ];
1330
+ if (blockedLanes.length > 0) {
1331
+ directiveLines.push(`- **Blocked Risk Lanes**: ${blockedLanes.join(", ")}`);
1332
+ }
1333
+ if (policy.blockedToolSlugs && policy.blockedToolSlugs.length > 0) {
1334
+ directiveLines.push(`- **Blocked Tools (Never Call)**: ${policy.blockedToolSlugs.join(", ")}`);
1335
+ }
1336
+ if (parameterRules.length > 0) {
1337
+ directiveLines.push(`- **Conditional Parameter Guardrails (${parameterRules.length})**:`);
1338
+ for (const pr of parameterRules) {
1339
+ directiveLines.push(` - \`${pr.id}\`: ${pr.howToComply}`);
1340
+ }
1341
+ }
1342
+ if (defaultsPairs.length > 0) {
1343
+ directiveLines.push(`- **Learned Memory Defaults (Auto-Filled on plan_action)**: ${defaultsPairs.join(", ")}`);
1344
+ }
1345
+ if (plain.customInstructions.length > 0) {
1346
+ directiveLines.push(`- **Operator Custom Instructions**: ${plain.customInstructions.join(" | ")}`);
1347
+ }
1348
+ return {
1349
+ ...(options?.workspaceId ? { workspaceId: options.workspaceId } : {}),
1350
+ posture,
1351
+ mode: (policy.mode || "enforce"),
1352
+ executionRing: ring,
1353
+ headline: plain.headline,
1354
+ preferredExplanation: plain.preferred,
1355
+ plainEnglish: plain,
1356
+ languageMode: plain.languageMode,
1357
+ customInstructions: plain.customInstructions,
1358
+ killSwitch: {
1359
+ active: isPaused,
1360
+ reason: policy.killSwitchReason || null,
1361
+ howToToggleMcp: {
1362
+ pause: {
1363
+ tool: "get_run_context",
1364
+ arguments: {
1365
+ action: "kill_switch",
1366
+ enabled: true,
1367
+ prompt: "Pause all agent executions",
1368
+ },
1369
+ },
1370
+ resume: {
1371
+ tool: "get_run_context",
1372
+ arguments: {
1373
+ action: "kill_switch",
1374
+ enabled: false,
1375
+ prompt: "Resume agent executions",
1376
+ },
1377
+ },
1378
+ },
1379
+ },
1380
+ budgetGuardrail: {
1381
+ maxCreditsPerRun: maxCredits,
1382
+ maxUsdPerRun: maxUsd,
1383
+ allowInstantWallet: policy.allowInstantWallet !== false,
1384
+ allowProvisioning: policy.allowProvisioning !== false,
1385
+ blockDestructiveSql: policy.blockDestructiveSql !== false,
1386
+ },
1387
+ riskLaneMatrix,
1388
+ autoApproveRiskLevels,
1389
+ requireApprovalRiskLevels,
1390
+ blockedRiskLevels: blockedLanes,
1391
+ blockedToolSlugs: policy.blockedToolSlugs || [],
1392
+ parameterRules,
1393
+ naturalLanguageRules: policy.naturalLanguageRules || [],
1394
+ activeDefaults,
1395
+ ...(options?.trustScore ? { trustScore: options.trustScore } : {}),
1396
+ ...(options?.agentDid ? { agentDid: options.agentDid } : {}),
1397
+ llmSystemDirective: directiveLines.join("\n"),
1398
+ agentControlCheatsheet: [
1399
+ {
1400
+ intent: "Inspect active policy, guardrails, and learned memory defaults",
1401
+ mcpTool: "get_run_context",
1402
+ arguments: { action: "governance_status" },
1403
+ },
1404
+ {
1405
+ intent: "Add or update a guardrail in plain English or structured parameter_rules",
1406
+ mcpTool: "get_run_context",
1407
+ arguments: {
1408
+ action: "set_guardrail",
1409
+ prompt: "Require approval for Stripe refunds over $50, auto-approve under $50",
1410
+ },
1411
+ },
1412
+ {
1413
+ intent: "Dry-run test if a tool + input will pass policy before creating a plan",
1414
+ mcpTool: "get_run_context",
1415
+ arguments: {
1416
+ action: "check_policy",
1417
+ tool_slug: "app.gmail.send_email",
1418
+ input: { to: "team@muxo.ai", subject: "Status" },
1419
+ },
1420
+ },
1421
+ {
1422
+ intent: "Reset custom guardrails and parameter rules back to Safe Default",
1423
+ mcpTool: "get_run_context",
1424
+ arguments: { action: "reset_guardrails" },
1425
+ },
1426
+ {
1427
+ intent: "Pause or resume workspace execution via the emergency kill switch",
1428
+ mcpTool: "get_run_context",
1429
+ arguments: { action: "kill_switch", enabled: false },
1430
+ },
1431
+ {
1432
+ intent: "Save a workspace fact or default parameter for automatic recall",
1433
+ mcpTool: "get_run_context",
1434
+ arguments: {
1435
+ action: "remember",
1436
+ prompt: "Remember our default GitHub repo is muxo-ai/muxo",
1437
+ },
1438
+ },
1439
+ ],
1440
+ };
1441
+ }
1442
+ export function simulatePolicyEvaluation(params) {
1443
+ const policy = {
1444
+ ...DEFAULT_WORKSPACE_POLICY,
1445
+ ...(params.policy || {}),
1446
+ };
1447
+ const toolSlug = String(params.toolSlug || "instant.web.search").trim();
1448
+ const riskLevel = params.riskLevel ||
1449
+ (toolSlug.startsWith("instant.")
1450
+ ? "read"
1451
+ : toolSlug.startsWith("resource.")
1452
+ ? "provisioning"
1453
+ : toolSlug.startsWith("billing.")
1454
+ ? "financial"
1455
+ : "write");
1456
+ const creditCost = Number(params.creditCost ?? (riskLevel === "read" ? 2 : 5));
1457
+ const rawInput = { ...(params.input || {}) };
1458
+ const defaults = params.activeDefaults || {};
1459
+ const effectiveInput = { ...rawInput };
1460
+ const autoFilledInput = {};
1461
+ for (const [k, v] of Object.entries(defaults)) {
1462
+ if (effectiveInput[k] === undefined && v !== undefined) {
1463
+ effectiveInput[k] = v;
1464
+ autoFilledInput[k] = v;
1465
+ }
1466
+ }
1467
+ const reasons = [];
1468
+ let decision = "auto_approved";
1469
+ const isMonitor = policy.mode === "monitor";
1470
+ if (policy.killSwitchActive && !isMonitor) {
1471
+ decision = "blocked";
1472
+ reasons.push(`Workspace kill switch is active (${policy.killSwitchReason || "emergency pause"}).`);
1473
+ }
1474
+ if (decision !== "blocked" &&
1475
+ (policy.blockedToolSlugs || []).some((s) => s.toLowerCase() === toolSlug.toLowerCase()) &&
1476
+ !isMonitor) {
1477
+ decision = "blocked";
1478
+ reasons.push(`Tool '${toolSlug}' is on the workspace blockedToolSlugs list.`);
1479
+ }
1480
+ if (decision !== "blocked" &&
1481
+ policy.executionRing === 3 &&
1482
+ riskLevel !== "read" &&
1483
+ !isMonitor) {
1484
+ decision = "blocked";
1485
+ reasons.push(`Workspace is in Ring 3 (Read-Only Sandbox mode), which blocks '${riskLevel}' tools.`);
1486
+ }
1487
+ if (decision !== "blocked" &&
1488
+ riskLevel === "provisioning" &&
1489
+ policy.allowProvisioning === false &&
1490
+ !isMonitor) {
1491
+ decision = "blocked";
1492
+ reasons.push("Cloud infrastructure provisioning is disabled (allowProvisioning=false).");
1493
+ }
1494
+ const maxCredits = Number(policy.maxCreditsPerRun ?? 50);
1495
+ if (decision !== "blocked" && creditCost > maxCredits && !isMonitor) {
1496
+ decision = "blocked";
1497
+ reasons.push(`Tool credit cost (${creditCost} cr) exceeds policy maxCreditsPerRun (${maxCredits} cr).`);
1498
+ }
1499
+ if (decision !== "blocked" &&
1500
+ typeof params.creditsBalance === "number" &&
1501
+ creditCost > params.creditsBalance) {
1502
+ decision = "blocked";
1503
+ reasons.push(`Insufficient workspace credits: tool requires ${creditCost} cr, balance is ${params.creditsBalance} cr.`);
1504
+ }
1505
+ const paramEval = evaluateParameterPolicyRules({
1506
+ policy,
1507
+ toolSlug,
1508
+ input: effectiveInput,
1509
+ });
1510
+ if (decision !== "blocked") {
1511
+ if (paramEval.violated && paramEval.action === "block" && !isMonitor) {
1512
+ decision = "blocked";
1513
+ reasons.push(paramEval.reason || "Blocked by conditional parameter guardrail.");
1514
+ }
1515
+ else if (paramEval.violated &&
1516
+ paramEval.action === "require_approval" &&
1517
+ !isMonitor) {
1518
+ decision = "pending_approval";
1519
+ reasons.push(paramEval.reason || "Parameter guardrail requires human owner approval.");
1520
+ }
1521
+ else if (paramEval.explicitAllow && paramEval.action === "auto_approve") {
1522
+ decision = "auto_approved";
1523
+ reasons.push(paramEval.reason ||
1524
+ "Auto-approved by conditional parameter guardrail (elseAction='auto_approve').");
1525
+ }
1526
+ else if ((policy.requireApprovalFor || []).includes(riskLevel) &&
1527
+ !isMonitor) {
1528
+ decision = "pending_approval";
1529
+ reasons.push(`Risk level '${riskLevel}' requires human owner approval under active workspace policy.`);
1530
+ }
1531
+ else {
1532
+ decision = "auto_approved";
1533
+ reasons.push(`Allowed and auto-approved under workspace policy (riskLevel='${riskLevel}', cost=${creditCost} cr <= ${maxCredits} cr).`);
1534
+ }
1535
+ }
1536
+ const executeTool = `execute_${riskLevel}`;
1537
+ let remediationForLlm = "";
1538
+ const nextToolCalls = [];
1539
+ if (decision === "auto_approved") {
1540
+ remediationForLlm = `Action will be auto_approved. Call plan_action({ tool_slug: '${toolSlug}', input: ${JSON.stringify(effectiveInput)} }) and then ${executeTool}.`;
1541
+ nextToolCalls.push({
1542
+ tool: "plan_action",
1543
+ input: {
1544
+ tool_slug: toolSlug,
1545
+ input: effectiveInput,
1546
+ reason: `Execute ${toolSlug}`,
1547
+ },
1548
+ when: "Ready to create an auto-approved plan",
1549
+ });
1550
+ }
1551
+ else if (decision === "pending_approval") {
1552
+ remediationForLlm = `Action is allowed to be planned but will enter 'pending_approval' (${reasons.join("; ")}). Call plan_action, share humanHandoff.url with the user, and call get_plan({ plan_id, wait_for_approval: true }).`;
1553
+ nextToolCalls.push({
1554
+ tool: "plan_action",
1555
+ input: {
1556
+ tool_slug: toolSlug,
1557
+ input: effectiveInput,
1558
+ reason: `Request approval for ${toolSlug}`,
1559
+ },
1560
+ when: "To generate the plan and human approval URL",
1561
+ });
1562
+ }
1563
+ else {
1564
+ const ruleHint = paramEval.violatedRule
1565
+ ? ` Adjust arguments to satisfy: ${describeParameterRuleCompliance(paramEval.violatedRule)}`
1566
+ : "";
1567
+ remediationForLlm = `Action is currently BLOCKED (${reasons.join("; ")}).${ruleHint} Or ask the user if they want to update the guardrail via get_run_context({ action: 'set_guardrail', prompt: '...' }).`;
1568
+ nextToolCalls.push({
1569
+ tool: "get_run_context",
1570
+ input: {
1571
+ action: "governance_status",
1572
+ },
1573
+ when: "To inspect active guardrails or adjust workspace policy with the user's permission",
1574
+ });
1575
+ }
1576
+ return {
1577
+ ok: true,
1578
+ action: "check_policy",
1579
+ toolSlug,
1580
+ ...(params.toolName ? { toolName: params.toolName } : {}),
1581
+ riskLevel,
1582
+ creditCost,
1583
+ decision,
1584
+ approvalStatus: decision,
1585
+ allowed: decision !== "blocked",
1586
+ requiresApproval: decision === "pending_approval",
1587
+ blocked: decision === "blocked",
1588
+ executeTool,
1589
+ effectiveInput,
1590
+ autoFilledInput,
1591
+ reasons,
1592
+ matchedParameterRule: paramEval.matchedRule,
1593
+ violatedParameterRule: paramEval.violatedRule,
1594
+ remediationForLlm,
1595
+ nextToolCalls,
1596
+ };
1597
+ }