@muxoai/sdk 1.2.3 → 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.
- package/.agents/plugins/marketplace.json +22 -0
- package/.claude-plugin/marketplace.json +32 -0
- package/.codex/config.toml +12 -0
- package/ARCHITECTURE.md +121 -0
- package/README.md +85 -31
- package/dist/clerk.d.ts +35 -0
- package/dist/clerk.js +201 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2380 -0
- package/dist/composio-catalog.d.ts +234 -0
- package/dist/composio-catalog.js +1894 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +115 -0
- package/dist/durable-workflow.d.ts +45 -0
- package/dist/durable-workflow.js +90 -0
- package/dist/engine.d.ts +465 -0
- package/dist/engine.js +2560 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/mcp-schema.d.ts +286 -0
- package/dist/mcp-schema.js +2559 -0
- package/dist/mcp.d.ts +22 -0
- package/dist/mcp.js +1356 -0
- package/dist/memory-graph.d.ts +21 -0
- package/dist/memory-graph.js +173 -0
- package/dist/nl.d.ts +34 -0
- package/dist/nl.js +756 -0
- package/dist/oauth.d.ts +32 -0
- package/dist/oauth.js +134 -0
- package/dist/policy.d.ts +190 -0
- package/dist/policy.js +1597 -0
- package/dist/providers/composio-instant.d.ts +18 -0
- package/dist/providers/composio-instant.js +1169 -0
- package/dist/providers/composio-sdk.d.ts +162 -0
- package/dist/providers/composio-sdk.js +1395 -0
- package/dist/providers/microsoft-governance.d.ts +152 -0
- package/dist/providers/microsoft-governance.js +1062 -0
- package/dist/providers/stripe-billing.d.ts +83 -0
- package/dist/providers/stripe-billing.js +611 -0
- package/dist/providers/stripe-projects.d.ts +508 -0
- package/dist/providers/stripe-projects.js +3188 -0
- package/dist/providers/supermemory.d.ts +160 -0
- package/dist/providers/supermemory.js +778 -0
- package/dist/provisioning-managed.d.ts +50 -0
- package/dist/provisioning-managed.js +155 -0
- package/dist/sdk.d.ts +951 -0
- package/dist/sdk.js +748 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +3575 -0
- package/dist/store.d.ts +348 -0
- package/dist/store.js +2323 -0
- package/dist/types.d.ts +593 -0
- package/dist/types.js +1 -0
- package/dist/ui/console-html.d.ts +64 -0
- package/dist/ui/console-html.js +3857 -0
- package/dist/ui/design-tokens.d.ts +177 -0
- package/dist/ui/design-tokens.js +210 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +68 -7
- package/plugins/muxo/.app.json +8 -0
- package/plugins/muxo/.claude-plugin/plugin.json +16 -0
- package/plugins/muxo/.codex-plugin/plugin.json +64 -0
- package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
- package/plugins/muxo/.mcp.json +8 -0
- package/plugins/muxo/assets/logo.png +0 -0
- package/plugins/muxo/assets/logo.svg +6 -0
- package/plugins/muxo/hooks/hooks.json +27 -0
- package/plugins/muxo/hooks/session-start.sh +97 -0
- package/plugins/muxo/hooks/user-prompt-submit.sh +58 -0
- package/plugins/muxo/mcp.json +28 -0
- package/plugins/muxo/plugin.json +116 -0
- package/plugins/muxo/skills/muxo/SKILL.md +153 -0
- package/plugins/muxo/skills/muxo/agents/openai.yaml +12 -0
- package/plugins/muxo/skills/muxo/references/cli.md +63 -0
- package/plugins/muxo/skills/muxo/references/mcp.md +37 -0
- package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
- package/plugins/muxo/skills/setup/SKILL.md +42 -0
- package/plugins/muxo/skills/setup/agents/openai.yaml +12 -0
- package/src/catalogues/stripe-projects-catalog.json +9972 -0
- package/vendor/microsoft-governance/README.md +363 -0
- package/vendor/microsoft-governance/dist/audit.d.ts +26 -0
- package/vendor/microsoft-governance/dist/audit.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/audit.js +107 -0
- package/vendor/microsoft-governance/dist/audit.js.map +1 -0
- package/vendor/microsoft-governance/dist/cascade-containment.d.ts +84 -0
- package/vendor/microsoft-governance/dist/cascade-containment.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/cascade-containment.js +387 -0
- package/vendor/microsoft-governance/dist/cascade-containment.js.map +1 -0
- package/vendor/microsoft-governance/dist/client.d.ts +34 -0
- package/vendor/microsoft-governance/dist/client.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/client.js +112 -0
- package/vendor/microsoft-governance/dist/client.js.map +1 -0
- package/vendor/microsoft-governance/dist/context-poisoning.d.ts +58 -0
- package/vendor/microsoft-governance/dist/context-poisoning.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/context-poisoning.js +410 -0
- package/vendor/microsoft-governance/dist/context-poisoning.js.map +1 -0
- package/vendor/microsoft-governance/dist/credential-vault.d.ts +213 -0
- package/vendor/microsoft-governance/dist/credential-vault.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/credential-vault.js +567 -0
- package/vendor/microsoft-governance/dist/credential-vault.js.map +1 -0
- package/vendor/microsoft-governance/dist/discovery.d.ts +80 -0
- package/vendor/microsoft-governance/dist/discovery.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/discovery.js +404 -0
- package/vendor/microsoft-governance/dist/discovery.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/channel.d.ts +29 -0
- package/vendor/microsoft-governance/dist/encryption/channel.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/channel.js +78 -0
- package/vendor/microsoft-governance/dist/encryption/channel.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/index.d.ts +9 -0
- package/vendor/microsoft-governance/dist/encryption/index.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/index.js +23 -0
- package/vendor/microsoft-governance/dist/encryption/index.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/mesh-client.d.ts +267 -0
- package/vendor/microsoft-governance/dist/encryption/mesh-client.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/mesh-client.js +1001 -0
- package/vendor/microsoft-governance/dist/encryption/mesh-client.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/ratchet.d.ts +38 -0
- package/vendor/microsoft-governance/dist/encryption/ratchet.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/ratchet.js +223 -0
- package/vendor/microsoft-governance/dist/encryption/ratchet.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/registry-client.d.ts +136 -0
- package/vendor/microsoft-governance/dist/encryption/registry-client.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/registry-client.js +369 -0
- package/vendor/microsoft-governance/dist/encryption/registry-client.js.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/x3dh.d.ts +80 -0
- package/vendor/microsoft-governance/dist/encryption/x3dh.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/encryption/x3dh.js +238 -0
- package/vendor/microsoft-governance/dist/encryption/x3dh.js.map +1 -0
- package/vendor/microsoft-governance/dist/framework-adapter.d.ts +63 -0
- package/vendor/microsoft-governance/dist/framework-adapter.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/framework-adapter.js +253 -0
- package/vendor/microsoft-governance/dist/framework-adapter.js.map +1 -0
- package/vendor/microsoft-governance/dist/identity.d.ts +124 -0
- package/vendor/microsoft-governance/dist/identity.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/identity.js +446 -0
- package/vendor/microsoft-governance/dist/identity.js.map +1 -0
- package/vendor/microsoft-governance/dist/index.d.ts +39 -0
- package/vendor/microsoft-governance/dist/index.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/index.js +95 -0
- package/vendor/microsoft-governance/dist/index.js.map +1 -0
- package/vendor/microsoft-governance/dist/kill-switch.d.ts +22 -0
- package/vendor/microsoft-governance/dist/kill-switch.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/kill-switch.js +60 -0
- package/vendor/microsoft-governance/dist/kill-switch.js.map +1 -0
- package/vendor/microsoft-governance/dist/lifecycle.d.ts +50 -0
- package/vendor/microsoft-governance/dist/lifecycle.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/lifecycle.js +113 -0
- package/vendor/microsoft-governance/dist/lifecycle.js.map +1 -0
- package/vendor/microsoft-governance/dist/mcp.d.ts +41 -0
- package/vendor/microsoft-governance/dist/mcp.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/mcp.js +233 -0
- package/vendor/microsoft-governance/dist/mcp.js.map +1 -0
- package/vendor/microsoft-governance/dist/metrics.d.ts +123 -0
- package/vendor/microsoft-governance/dist/metrics.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/metrics.js +289 -0
- package/vendor/microsoft-governance/dist/metrics.js.map +1 -0
- package/vendor/microsoft-governance/dist/oci-manifest.d.ts +49 -0
- package/vendor/microsoft-governance/dist/oci-manifest.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/oci-manifest.js +182 -0
- package/vendor/microsoft-governance/dist/oci-manifest.js.map +1 -0
- package/vendor/microsoft-governance/dist/policy-backends/cedar.d.ts +15 -0
- package/vendor/microsoft-governance/dist/policy-backends/cedar.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/policy-backends/cedar.js +88 -0
- package/vendor/microsoft-governance/dist/policy-backends/cedar.js.map +1 -0
- package/vendor/microsoft-governance/dist/policy-backends/opa.d.ts +17 -0
- package/vendor/microsoft-governance/dist/policy-backends/opa.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/policy-backends/opa.js +74 -0
- package/vendor/microsoft-governance/dist/policy-backends/opa.js.map +1 -0
- package/vendor/microsoft-governance/dist/policy.d.ts +77 -0
- package/vendor/microsoft-governance/dist/policy.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/policy.js +760 -0
- package/vendor/microsoft-governance/dist/policy.js.map +1 -0
- package/vendor/microsoft-governance/dist/prompt-defense.d.ts +37 -0
- package/vendor/microsoft-governance/dist/prompt-defense.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/prompt-defense.js +229 -0
- package/vendor/microsoft-governance/dist/prompt-defense.js.map +1 -0
- package/vendor/microsoft-governance/dist/protocol-facets.d.ts +87 -0
- package/vendor/microsoft-governance/dist/protocol-facets.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/protocol-facets.js +728 -0
- package/vendor/microsoft-governance/dist/protocol-facets.js.map +1 -0
- package/vendor/microsoft-governance/dist/rings.d.ts +17 -0
- package/vendor/microsoft-governance/dist/rings.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/rings.js +61 -0
- package/vendor/microsoft-governance/dist/rings.js.map +1 -0
- package/vendor/microsoft-governance/dist/sandbox.d.ts +61 -0
- package/vendor/microsoft-governance/dist/sandbox.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/sandbox.js +199 -0
- package/vendor/microsoft-governance/dist/sandbox.js.map +1 -0
- package/vendor/microsoft-governance/dist/surface-parity.d.ts +45 -0
- package/vendor/microsoft-governance/dist/surface-parity.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/surface-parity.js +216 -0
- package/vendor/microsoft-governance/dist/surface-parity.js.map +1 -0
- package/vendor/microsoft-governance/dist/trust.d.ts +28 -0
- package/vendor/microsoft-governance/dist/trust.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/trust.js +209 -0
- package/vendor/microsoft-governance/dist/trust.js.map +1 -0
- package/vendor/microsoft-governance/dist/types.d.ts +422 -0
- package/vendor/microsoft-governance/dist/types.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/types.js +29 -0
- package/vendor/microsoft-governance/dist/types.js.map +1 -0
- package/vendor/microsoft-governance/dist/verify.d.ts +135 -0
- package/vendor/microsoft-governance/dist/verify.d.ts.map +1 -0
- package/vendor/microsoft-governance/dist/verify.js +356 -0
- package/vendor/microsoft-governance/dist/verify.js.map +1 -0
- package/vendor/microsoft-governance/package.json +64 -0
- package/index.d.ts +0 -177
- package/index.js +0 -1
package/dist/policy.js
ADDED
|
@@ -0,0 +1,1597 @@
|
|
|
1
|
+
const RISK_LEVELS = [
|
|
2
|
+
"read",
|
|
3
|
+
"write",
|
|
4
|
+
"admin",
|
|
5
|
+
"financial",
|
|
6
|
+
"provisioning",
|
|
7
|
+
"destructive",
|
|
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
|
+
}
|
|
85
|
+
export class PolicyValidationError extends Error {
|
|
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
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Validates a client-supplied policy patch. Kill-switch fields are rejected so
|
|
113
|
+
* that pausing and resuming always goes through /api/v1/governance/kill-switch,
|
|
114
|
+
* which records the trust-score penalty and audit entry.
|
|
115
|
+
*/
|
|
116
|
+
export function sanitizePolicyPatch(body) {
|
|
117
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
118
|
+
throw new PolicyValidationError("Policy update must be a JSON object");
|
|
119
|
+
}
|
|
120
|
+
const input = body;
|
|
121
|
+
if ("killSwitchActive" in input || "killSwitchReason" in input) {
|
|
122
|
+
throw new PolicyValidationError("Use POST /api/v1/governance/kill-switch to pause or resume agents");
|
|
123
|
+
}
|
|
124
|
+
const patch = {};
|
|
125
|
+
if (input.mode !== undefined) {
|
|
126
|
+
if (input.mode !== "monitor" && input.mode !== "enforce") {
|
|
127
|
+
throw new PolicyValidationError('mode must be "monitor" or "enforce"');
|
|
128
|
+
}
|
|
129
|
+
patch.mode = input.mode;
|
|
130
|
+
}
|
|
131
|
+
if (input.requireApprovalFor !== undefined) {
|
|
132
|
+
if (!Array.isArray(input.requireApprovalFor) ||
|
|
133
|
+
!input.requireApprovalFor.every((r) => RISK_LEVELS.includes(r))) {
|
|
134
|
+
throw new PolicyValidationError(`requireApprovalFor must be a list of: ${RISK_LEVELS.join(", ")}`);
|
|
135
|
+
}
|
|
136
|
+
patch.requireApprovalFor = [...new Set(input.requireApprovalFor)];
|
|
137
|
+
}
|
|
138
|
+
if (input.executionRing !== undefined) {
|
|
139
|
+
const ring = Number(input.executionRing);
|
|
140
|
+
if (![0, 1, 2, 3].includes(ring)) {
|
|
141
|
+
throw new PolicyValidationError("executionRing must be 0, 1, 2, or 3");
|
|
142
|
+
}
|
|
143
|
+
patch.executionRing = ring;
|
|
144
|
+
}
|
|
145
|
+
if (input.maxCreditsPerRun !== undefined) {
|
|
146
|
+
const max = Number(input.maxCreditsPerRun);
|
|
147
|
+
if (!Number.isFinite(max) || max < 0) {
|
|
148
|
+
throw new PolicyValidationError("maxCreditsPerRun must be a non-negative number");
|
|
149
|
+
}
|
|
150
|
+
patch.maxCreditsPerRun = max;
|
|
151
|
+
}
|
|
152
|
+
for (const key of ["allowInstantWallet", "allowProvisioning", "blockDestructiveSql"]) {
|
|
153
|
+
if (input[key] !== undefined) {
|
|
154
|
+
if (typeof input[key] !== "boolean") {
|
|
155
|
+
throw new PolicyValidationError(`${key} must be true or false`);
|
|
156
|
+
}
|
|
157
|
+
patch[key] = input[key];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (input.blockedToolSlugs !== undefined) {
|
|
161
|
+
if (!Array.isArray(input.blockedToolSlugs) ||
|
|
162
|
+
!input.blockedToolSlugs.every((s) => typeof s === "string" && s.trim())) {
|
|
163
|
+
throw new PolicyValidationError("blockedToolSlugs must be a list of tool slugs");
|
|
164
|
+
}
|
|
165
|
+
patch.blockedToolSlugs = [
|
|
166
|
+
...new Set(input.blockedToolSlugs.map((s) => s.trim())),
|
|
167
|
+
];
|
|
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
|
+
}
|
|
219
|
+
if (Object.keys(patch).length === 0) {
|
|
220
|
+
throw new PolicyValidationError("No supported policy fields in request");
|
|
221
|
+
}
|
|
222
|
+
return patch;
|
|
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
|
+
}
|