@muxoai/platform 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/ARCHITECTURE.md +16 -6
  4. package/README.md +1 -1
  5. package/dist/cli.js +317 -10
  6. package/dist/composio-catalog.d.ts +58 -0
  7. package/dist/composio-catalog.js +474 -0
  8. package/dist/config.d.ts +10 -0
  9. package/dist/config.js +16 -0
  10. package/dist/durable-workflow.d.ts +45 -0
  11. package/dist/durable-workflow.js +90 -0
  12. package/dist/engine.d.ts +276 -5
  13. package/dist/engine.js +1261 -71
  14. package/dist/mcp-schema.d.ts +125 -0
  15. package/dist/mcp-schema.js +1076 -0
  16. package/dist/mcp.d.ts +2 -30
  17. package/dist/mcp.js +505 -539
  18. package/dist/memory-graph.d.ts +21 -0
  19. package/dist/memory-graph.js +173 -0
  20. package/dist/nl.js +155 -0
  21. package/dist/oauth.d.ts +32 -0
  22. package/dist/oauth.js +134 -0
  23. package/dist/policy.d.ts +75 -1
  24. package/dist/policy.js +1119 -0
  25. package/dist/providers/composio-instant.js +225 -5
  26. package/dist/providers/composio-sdk.d.ts +68 -5
  27. package/dist/providers/composio-sdk.js +683 -78
  28. package/dist/providers/microsoft-governance.d.ts +28 -1
  29. package/dist/providers/microsoft-governance.js +224 -11
  30. package/dist/providers/stripe-billing.d.ts +42 -0
  31. package/dist/providers/stripe-billing.js +230 -1
  32. package/dist/providers/stripe-projects.d.ts +451 -3
  33. package/dist/providers/stripe-projects.js +2106 -87
  34. package/dist/providers/supermemory.d.ts +74 -1
  35. package/dist/providers/supermemory.js +426 -0
  36. package/dist/provisioning-managed.d.ts +50 -0
  37. package/dist/provisioning-managed.js +155 -0
  38. package/dist/sdk.d.ts +440 -3
  39. package/dist/sdk.js +305 -3
  40. package/dist/server.js +1841 -232
  41. package/dist/store.d.ts +113 -6
  42. package/dist/store.js +675 -52
  43. package/dist/types.d.ts +192 -3
  44. package/dist/ui/console-html.d.ts +10 -1
  45. package/dist/ui/console-html.js +1082 -452
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/package.json +2 -2
  49. package/plugins/muxo/.claude-plugin/plugin.json +1 -1
  50. package/plugins/muxo/.codex-plugin/plugin.json +1 -1
  51. package/plugins/muxo/plugin.json +2 -2
  52. package/plugins/muxo/skills/muxo/SKILL.md +42 -1
  53. package/plugins/muxo/skills/muxo/references/mcp.md +3 -2
  54. package/plugins/muxo/skills/setup/SKILL.md +8 -0
@@ -1,5 +1,5 @@
1
1
  import { AgentIdentity, AuditLogger, type ContextPoisoningScanResult, type KillSwitchResult, type TrustScore } from "../../vendor/microsoft-governance/dist/index.js";
2
- import type { ActionPlan, GovernanceDecisionRecord, MuxoTool, Workspace } from "../types.js";
2
+ import type { ActionPlan, ExecutionReceipt, ExecutionRun, GovernanceDecisionRecord, GovernancePolicy, HumanFriendlyExplanation, LanguageMode, MuxoTool, PortableAgenticReceipt, Workspace } from "../types.js";
3
3
  export declare const RING_LABELS: Record<number, string>;
4
4
  export declare class MicrosoftGovernanceAdapter {
5
5
  private readonly scanner;
@@ -108,6 +108,20 @@ export declare class MicrosoftGovernanceAdapter {
108
108
  * Verifies a DecisionRecord's Ed25519 signature and the workspace's SHA-256 audit hash chain.
109
109
  */
110
110
  verifyDecisionRecord(workspaceOrRecord: Workspace | GovernanceDecisionRecord, maybeRecord?: GovernanceDecisionRecord): boolean;
111
+ /**
112
+ * Builds a portable Ed25519-signed AgenticReceipt envelope (compatible with the `agentic-receipts` spec)
113
+ * so external verifiers can validate input/output SHA-256 hashes, hash chaining, and Ed25519 signature
114
+ * using only the workspace's public key and DID without needing the internal vault HMAC key.
115
+ */
116
+ buildPortableAgenticReceipt(params: {
117
+ receipt: ExecutionReceipt;
118
+ run?: ExecutionRun | null;
119
+ workspace?: Workspace | null;
120
+ }): PortableAgenticReceipt;
121
+ /**
122
+ * Verifies a PortableAgenticReceipt's Ed25519 signature over its canonical hash-chained payload.
123
+ */
124
+ verifyPortableAgenticReceipt(agenticReceipt: PortableAgenticReceipt, workspace?: Workspace | null): boolean;
111
125
  getStatus(workspace?: Workspace): {
112
126
  sdk: string;
113
127
  sdkVersion: string;
@@ -123,3 +137,16 @@ export declare class MicrosoftGovernanceAdapter {
123
137
  */
124
138
  getGovernanceStatus(workspace: Workspace): Record<string, unknown>;
125
139
  }
140
+ export declare function explainDecisionRecord(record: GovernanceDecisionRecord, policy?: Partial<GovernancePolicy> | null, languageMode?: LanguageMode): HumanFriendlyExplanation;
141
+ export declare function explainGovernanceStatus(params: {
142
+ workspace: Workspace;
143
+ agentDid: string;
144
+ trustScore: NonNullable<GovernanceDecisionRecord["trustScore"]>;
145
+ killSwitchActive: boolean;
146
+ killSwitchReason: string | null;
147
+ poisoned: boolean;
148
+ findingsCount: number;
149
+ auditEntriesCount: number;
150
+ chainVerified: boolean;
151
+ languageMode?: LanguageMode;
152
+ }): HumanFriendlyExplanation;
@@ -1,5 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import { AgentIdentity, AuditLogger, ContextPoisoningDetector, ExecutionRing, FacetRegistry, KillSwitch, McpSecurityScanner, PolicyEngine, RingEnforcer, TrustManager, extractK8sFacets, extractProtocolFacets, extractSqlFacets, } from "../../vendor/microsoft-governance/dist/index.js";
3
+ import { describePolicyInPlainEnglish, evaluateParameterPolicyRules, formatHumanFriendlyExplanation, } from "../policy.js";
3
4
  const SDK_VERSION = "5.0.0";
4
5
  const POLICY_ID = "muxo-microsoft-agent-governance-v5";
5
6
  export const RING_LABELS = {
@@ -555,10 +556,24 @@ export class MicrosoftGovernanceAdapter {
555
556
  const highSeverityFinding = extraFindings.some((f) => f.severity === "high" || f.severity === "critical");
556
557
  // 2. Protocol Facets Extraction (SQL, Kubernetes, Infrastructure)
557
558
  const protocolFacets = this.extractActionFacets(tool, input);
558
- // 3. Hardware-style Execution Ring Enforcement (Ring 0..Ring 3)
559
- const ringEnforcement = this.evaluateRingEnforcement(workspace, tool, ownerApproved);
560
- // 4. PolicyEngine + KillSwitch evaluation
559
+ // 3. PolicyEngine + KillSwitch + Parameter Guardrail evaluation
561
560
  const policy = workspace.policy;
561
+ const paramRuleCheck = evaluateParameterPolicyRules({
562
+ policy,
563
+ toolSlug: tool.slug,
564
+ input,
565
+ });
566
+ if (paramRuleCheck.violated && paramRuleCheck.reason) {
567
+ extraFindings.push({
568
+ category: "parameter_policy_rule",
569
+ severity: paramRuleCheck.action === "block" ? "high" : "medium",
570
+ description: paramRuleCheck.reason,
571
+ });
572
+ }
573
+ const effectiveApproved = ownerApproved ||
574
+ Boolean(paramRuleCheck.explicitAllow && paramRuleCheck.action === "auto_approve");
575
+ // 4. Hardware-style Execution Ring Enforcement (Ring 0..Ring 3)
576
+ const ringEnforcement = this.evaluateRingEnforcement(workspace, tool, effectiveApproved);
562
577
  const activeKill = this.isKillSwitchActive(workspace.id);
563
578
  const killSwitchActive = Boolean(activeKill || policy.killSwitchActive);
564
579
  const killSwitchReason = activeKill?.reason || policy.killSwitchReason || null;
@@ -569,7 +584,7 @@ export class MicrosoftGovernanceAdapter {
569
584
  risk_level: tool.riskLevel,
570
585
  lane: tool.lane,
571
586
  stage,
572
- owner_approved: ownerApproved,
587
+ owner_approved: effectiveApproved,
573
588
  workspace_active: true,
574
589
  ...(protocolFacets.sql ? { sql: protocolFacets.sql } : {}),
575
590
  ...(protocolFacets.k8s ? { k8s: protocolFacets.k8s } : {}),
@@ -580,8 +595,12 @@ export class MicrosoftGovernanceAdapter {
580
595
  : policyDecision.action === "require_approval"
581
596
  ? "require_approval"
582
597
  : "allow";
583
- let matchedRule = policyDecision.matchedRule ?? null;
584
- // Enforce KillSwitch, blocked tools, high-severity scanner/context-poisoning findings, or Ring breach
598
+ let matchedRule = paramRuleCheck.explicitAllow &&
599
+ paramRuleCheck.action === "auto_approve" &&
600
+ finalDecision === "allow"
601
+ ? paramRuleCheck.matchedRule?.id || "auto-approve-parameter-policy-rule"
602
+ : policyDecision.matchedRule ?? null;
603
+ // Enforce KillSwitch, blocked tools, parameter rules, high-severity scanner/context-poisoning findings, or Ring breach
585
604
  if (killSwitchActive) {
586
605
  finalDecision = "deny";
587
606
  matchedRule = "kill-switch-emergency-quarantine";
@@ -590,6 +609,19 @@ export class MicrosoftGovernanceAdapter {
590
609
  finalDecision = "deny";
591
610
  matchedRule = matchedRule || `deny-blocked-tool-${tool.slug}`;
592
611
  }
612
+ else if (paramRuleCheck.violated &&
613
+ paramRuleCheck.action === "block" &&
614
+ policy.mode === "enforce") {
615
+ finalDecision = "deny";
616
+ matchedRule = paramRuleCheck.matchedRule?.id || "deny-parameter-policy-rule";
617
+ }
618
+ else if (paramRuleCheck.violated &&
619
+ paramRuleCheck.action === "require_approval" &&
620
+ !ownerApproved &&
621
+ policy.mode === "enforce") {
622
+ finalDecision = "require_approval";
623
+ matchedRule = paramRuleCheck.matchedRule?.id || "approval-parameter-policy-rule";
624
+ }
593
625
  else if (highSeverityFinding && policy.mode === "enforce") {
594
626
  finalDecision = "deny";
595
627
  matchedRule = "mcp-security-scanner-high-severity";
@@ -651,13 +683,17 @@ export class MicrosoftGovernanceAdapter {
651
683
  const decisionReason = finalDecision === "deny"
652
684
  ? killSwitchActive
653
685
  ? `Blocked by Emergency KillSwitch: ${killSwitchReason || "Workspace agent boundary is quarantined"}`
654
- : highSeverityFinding
655
- ? `Blocked by McpSecurityScanner / ContextPoisoningDetector: ${extraFindings.map((f) => f.description).join("; ")}`
656
- : `Denied by policy rule ${matchedRule || "default"}`
686
+ : paramRuleCheck.violated && paramRuleCheck.reason
687
+ ? paramRuleCheck.reason
688
+ : highSeverityFinding
689
+ ? `Blocked by McpSecurityScanner / ContextPoisoningDetector: ${extraFindings.map((f) => f.description).join("; ")}`
690
+ : `Denied by policy rule ${matchedRule || "default"}`
657
691
  : finalDecision === "require_approval"
658
- ? `Escalated for human owner approval (${tool.riskLevel} risk lane, ${ringEnforcement.ringLabel})`
692
+ ? paramRuleCheck.violated && paramRuleCheck.reason
693
+ ? paramRuleCheck.reason
694
+ : `Escalated for human owner approval (${tool.riskLevel} risk lane, ${ringEnforcement.ringLabel})`
659
695
  : `Allowed by Microsoft Agent Governance (${matchedRule || "allow-governed-action"})`;
660
- return {
696
+ const record = {
661
697
  id: `dec_${crypto.randomBytes(8).toString("hex")}`,
662
698
  sdk: "@microsoft/agent-governance-sdk",
663
699
  sdkVersion: SDK_VERSION,
@@ -711,6 +747,8 @@ export class MicrosoftGovernanceAdapter {
711
747
  bindingHash,
712
748
  decidedAt,
713
749
  };
750
+ record.explanation = explainDecisionRecord(record, policy);
751
+ return record;
714
752
  }
715
753
  evaluatePlan(params) {
716
754
  return this.evaluateAction({
@@ -775,6 +813,112 @@ export class MicrosoftGovernanceAdapter {
775
813
  const auditChainVerified = auditLogger.verify();
776
814
  return ed25519Verified && auditChainVerified;
777
815
  }
816
+ /**
817
+ * Builds a portable Ed25519-signed AgenticReceipt envelope (compatible with the `agentic-receipts` spec)
818
+ * so external verifiers can validate input/output SHA-256 hashes, hash chaining, and Ed25519 signature
819
+ * using only the workspace's public key and DID without needing the internal vault HMAC key.
820
+ */
821
+ buildPortableAgenticReceipt(params) {
822
+ const { receipt, run, workspace } = params;
823
+ const identity = workspace
824
+ ? this.getWorkspaceIdentity(workspace)
825
+ : this.identities.get(receipt.workspaceId) || this.defaultIdentity;
826
+ const actionType = receipt.lane === "stripe_projects" ? "ApiCall" : "ToolCall";
827
+ const startedAt = run?.startedAt || receipt.issuedAt;
828
+ const completedAt = run?.completedAt || receipt.issuedAt;
829
+ const latencyMs = Math.max(0, Number(run?.durationMs ?? 0));
830
+ const nonce = crypto
831
+ .createHash("sha256")
832
+ .update(`muxo-agentic-receipt-nonce:${receipt.id}:${receipt.runId}`)
833
+ .digest("hex")
834
+ .slice(0, 32);
835
+ const previousReceiptHash = receipt.prevReceiptHash || "genesis_0000000000000000";
836
+ const canonicalPayload = [
837
+ "0.1.0",
838
+ receipt.id,
839
+ identity.did,
840
+ receipt.runId,
841
+ actionType,
842
+ receipt.toolSlug || "",
843
+ receipt.inputHash,
844
+ receipt.outputHash,
845
+ startedAt,
846
+ completedAt,
847
+ String(latencyMs),
848
+ nonce,
849
+ previousReceiptHash || "genesis",
850
+ receipt.receiptHash,
851
+ ].join("|");
852
+ const sigBytes = identity.sign(Buffer.from(canonicalPayload, "utf8"));
853
+ const signatureHex = Buffer.from(sigBytes).toString("hex");
854
+ const publicKeyHex = Buffer.from(identity.publicKey).toString("hex");
855
+ return {
856
+ version: "0.1.0",
857
+ receipt_id: receipt.id,
858
+ agent_id: identity.did,
859
+ task_id: receipt.runId,
860
+ action_type: actionType,
861
+ tool_name: receipt.toolSlug,
862
+ model_id: null,
863
+ input_hash: receipt.inputHash,
864
+ output_hash: receipt.outputHash,
865
+ started_at: startedAt,
866
+ completed_at: completedAt,
867
+ latency_ms: latencyMs,
868
+ nonce,
869
+ previous_receipt_hash: previousReceiptHash,
870
+ receipt_hash: receipt.receiptHash,
871
+ signature_algorithm: "Ed25519",
872
+ public_key_hex: publicKeyHex,
873
+ signature_hex: signatureHex,
874
+ };
875
+ }
876
+ /**
877
+ * Verifies a PortableAgenticReceipt's Ed25519 signature over its canonical hash-chained payload.
878
+ */
879
+ verifyPortableAgenticReceipt(agenticReceipt, workspace) {
880
+ if (!agenticReceipt || !agenticReceipt.signature_hex || !agenticReceipt.public_key_hex) {
881
+ return false;
882
+ }
883
+ const canonicalPayload = [
884
+ agenticReceipt.version || "0.1.0",
885
+ agenticReceipt.receipt_id,
886
+ agenticReceipt.agent_id,
887
+ agenticReceipt.task_id,
888
+ agenticReceipt.action_type,
889
+ agenticReceipt.tool_name || "",
890
+ agenticReceipt.input_hash,
891
+ agenticReceipt.output_hash,
892
+ agenticReceipt.started_at,
893
+ agenticReceipt.completed_at,
894
+ String(agenticReceipt.latency_ms ?? 0),
895
+ agenticReceipt.nonce,
896
+ agenticReceipt.previous_receipt_hash || "genesis",
897
+ agenticReceipt.receipt_hash,
898
+ ].join("|");
899
+ try {
900
+ const identity = workspace ? this.getWorkspaceIdentity(workspace) : null;
901
+ if (identity && identity.did === agenticReceipt.agent_id) {
902
+ return identity.verify(Buffer.from(canonicalPayload, "utf8"), Buffer.from(agenticReceipt.signature_hex, "hex"));
903
+ }
904
+ const pubKeyBytes = Buffer.from(agenticReceipt.public_key_hex, "hex");
905
+ const spkiKey = pubKeyBytes.length === 32
906
+ ? Buffer.concat([
907
+ Buffer.from("302a300506032b6570032100", "hex"),
908
+ pubKeyBytes,
909
+ ])
910
+ : pubKeyBytes;
911
+ const pubKeyObj = crypto.createPublicKey({
912
+ key: spkiKey,
913
+ format: "der",
914
+ type: "spki",
915
+ });
916
+ return crypto.verify(null, Buffer.from(canonicalPayload, "utf8"), pubKeyObj, Buffer.from(agenticReceipt.signature_hex, "hex"));
917
+ }
918
+ catch {
919
+ return false;
920
+ }
921
+ }
778
922
  getStatus(workspace) {
779
923
  const identity = workspace
780
924
  ? this.getWorkspaceIdentity(workspace)
@@ -844,6 +988,75 @@ export class MicrosoftGovernanceAdapter {
844
988
  chainVerified: auditLogger.verify(),
845
989
  recentEntries: entries.slice(-15).reverse(),
846
990
  },
991
+ plainEnglishPolicy: describePolicyInPlainEnglish(workspace.policy),
992
+ explanation: explainGovernanceStatus({
993
+ workspace,
994
+ agentDid: identity.did,
995
+ trustScore: this.getWorkspaceTrustScore(workspace.id),
996
+ killSwitchActive,
997
+ killSwitchReason: activeKill?.reason || workspace.policy.killSwitchReason || null,
998
+ poisoned: Boolean(contextPoisoningReport.poisoned ??
999
+ (contextPoisoningReport.findings?.length > 0)),
1000
+ findingsCount: contextPoisoningReport.findings.length,
1001
+ auditEntriesCount: entries.length,
1002
+ chainVerified: auditLogger.verify(),
1003
+ }),
847
1004
  };
848
1005
  }
849
1006
  }
1007
+ export function explainDecisionRecord(record, policy, languageMode) {
1008
+ const toolSlug = record.action.includes(":")
1009
+ ? record.action.split(":").slice(1).join(":")
1010
+ : record.action;
1011
+ const stage = record.action.startsWith("execute.") ? "Execution" : "Plan";
1012
+ const headline = record.decision === "allow"
1013
+ ? `${stage} allowed: ${toolSlug} passed all workspace safety checks.`
1014
+ : record.decision === "require_approval"
1015
+ ? `Approval needed: ${toolSlug} (${record.riskLevel}) is paused until you approve it.`
1016
+ : `Action blocked: ${toolSlug} was stopped by your workspace safety rules.`;
1017
+ const scanPlain = record.securityScan.safe
1018
+ ? "Safety scan: Clean (no prompt injection, data exfiltration, or destructive command patterns detected)."
1019
+ : `Safety warning: Flagged ${record.securityScan.findingsCount} security issue(s) (${record.securityScan.findings.map((f) => f.description).join("; ")}).`;
1020
+ const whyPlain = record.decision === "allow"
1021
+ ? `This ${record.riskLevel}-level action is permitted under your active guardrails.`
1022
+ : record.decision === "require_approval"
1023
+ ? `Your guardrails require human sign-off before running ${record.riskLevel} actions.`
1024
+ : record.killSwitch?.active
1025
+ ? `Your workspace Emergency Pause is active (${record.killSwitch.reason || "all agents paused"}).`
1026
+ : `Stopped because: ${record.reason}.`;
1027
+ const trustPct = record.trustScore
1028
+ ? `${Math.round(record.trustScore.overall * 100)}% (${record.trustScore.tier})`
1029
+ : "75% (Provisional)";
1030
+ const nonTechnical = `${headline} ${whyPlain} ${scanPlain} Agent reliability score: ${trustPct}. Recorded in your tamper-proof audit log.`;
1031
+ const technical = `decisionId=${record.id} | decision=${record.decision} (verdict=${record.verdict}) | matchedRule=${record.matchedRule || "none"} | mode=${record.mode} | ring=${record.ringEnforcement?.agentRing ?? "?"}->${record.ringEnforcement?.requiredRing ?? "?"} (${record.ringEnforcement?.ringLabel || "Ring"}) | securityScan=(safe=${record.securityScan.safe}, risk=${record.securityScan.riskLevel}, findings=${record.securityScan.findingsCount}) | asi06Poisoning=(safe=${record.contextPoisoning?.safe ?? true}, risk=${record.contextPoisoning?.riskLevel || "none"}) | trustScore=${record.trustScore?.overall ?? 0.75} (${record.trustScore?.tier || "Provisional"}) | agentDid=${record.agentDid} | auditHash=${record.auditEntry.hash.slice(0, 16)}... (prev=${record.auditEntry.previousHash.slice(0, 16)}..., chainVerified=${record.auditEntry.chainVerified}) | bindingHash=${record.bindingHash.slice(0, 16)}... | ed25519Sig=${record.ed25519Signature.slice(0, 16)}...`;
1032
+ return formatHumanFriendlyExplanation({
1033
+ headline,
1034
+ nonTechnical,
1035
+ technical,
1036
+ policy,
1037
+ languageMode,
1038
+ });
1039
+ }
1040
+ export function explainGovernanceStatus(params) {
1041
+ const plainPolicy = describePolicyInPlainEnglish(params.workspace.policy);
1042
+ const trustPct = Math.round((params.trustScore.overall || 0.75) * 100);
1043
+ const headline = params.killSwitchActive
1044
+ ? `Emergency Pause Active: All agent actions are paused (${params.killSwitchReason || "operator pause"}).`
1045
+ : `Governance Active: 9 safety modules protecting ${params.workspace.name} (${params.trustScore.tier} trust, ${trustPct}% score).`;
1046
+ const nonTechnical = [
1047
+ headline,
1048
+ `Current posture: ${plainPolicy.headline}`,
1049
+ params.poisoned
1050
+ ? `Prompt-injection scanner: ${params.findingsCount} suspicious pattern(s) flagged and isolated.`
1051
+ : "Prompt-injection & memory scanner: Clean (0 threats detected).",
1052
+ `Tamper-proof audit log: ${params.auditEntriesCount} action${params.auditEntriesCount === 1 ? "" : "s"} verified (${params.chainVerified ? "intact" : "check required"}).`,
1053
+ ].join(" ");
1054
+ const technical = `sdk=@microsoft/agent-governance-sdk@${SDK_VERSION} | policyId=${POLICY_ID} | agentDid=${params.agentDid} | killSwitchActive=${params.killSwitchActive} | trustScore=${params.trustScore.overall} (tier=${params.trustScore.tier}, reliability=${params.trustScore.reliability}, consistency=${params.trustScore.consistency}) | asi06Poisoned=${params.poisoned} (findings=${params.findingsCount}) | auditChain=(entries=${params.auditEntriesCount}, verified=${params.chainVerified}) | ${plainPolicy.technical}`;
1055
+ return formatHumanFriendlyExplanation({
1056
+ headline,
1057
+ nonTechnical,
1058
+ technical,
1059
+ policy: params.workspace.policy,
1060
+ languageMode: params.languageMode,
1061
+ });
1062
+ }
@@ -1,11 +1,27 @@
1
1
  import type { MuxoConfig } from "../config.js";
2
2
  import type { MuxoStore } from "../store.js";
3
3
  import type { ToolDescriptor, Workspace } from "../types.js";
4
+ export declare const BILLING_TOOLS: ToolDescriptor[];
5
+ export interface StripeMeterEventRecord {
6
+ identifier: string;
7
+ event_name: string;
8
+ timestamp: string;
9
+ workspaceId: string;
10
+ stripe_customer_id: string;
11
+ value: number;
12
+ lane: string;
13
+ tool_slug: string;
14
+ livemode: boolean;
15
+ source: "stripe_v2_meter_stream" | "muxo_local_meter";
16
+ }
4
17
  export declare class StripeBillingAdapter {
5
18
  private readonly config;
6
19
  private readonly store;
7
20
  private resolvedStarterPriceId;
8
21
  private resolvedCreditsPriceId;
22
+ private meterSessionToken;
23
+ private readonly meterEvents;
24
+ private readonly seenMeterIdentifiers;
9
25
  constructor(config: MuxoConfig, store: MuxoStore);
10
26
  private getEffectiveStripeKey;
11
27
  get isConfigured(): boolean;
@@ -38,4 +54,30 @@ export declare class StripeBillingAdapter {
38
54
  input: Record<string, unknown>;
39
55
  }): Promise<Record<string, unknown>>;
40
56
  verifyWebhookSignature(rawBody: string, signatureHeader: string): boolean;
57
+ createMeterEventSession(workspaceId?: string): Promise<{
58
+ object: "v2.billing.meter_event_session";
59
+ authentication_token: string;
60
+ created: string;
61
+ expires_at: string;
62
+ livemode: boolean;
63
+ source: "stripe_v2_api" | "muxo_managed";
64
+ }>;
65
+ recordMeterEvent(params: {
66
+ workspace: Workspace;
67
+ event_name?: string;
68
+ value: number;
69
+ lane?: string;
70
+ tool_slug?: string;
71
+ identifier?: string;
72
+ timestamp?: string;
73
+ }): Promise<{
74
+ recorded: boolean;
75
+ deduplicated: boolean;
76
+ event: StripeMeterEventRecord;
77
+ }>;
78
+ listMeterEvents(workspaceId: string, limit?: number): {
79
+ events: StripeMeterEventRecord[];
80
+ totalValue: number;
81
+ byLane: Record<string, number>;
82
+ };
41
83
  }
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- const BILLING_TOOLS = [
2
+ export const BILLING_TOOLS = [
3
3
  {
4
4
  slug: "billing.credits.topup",
5
5
  upstreamSlug: "STRIPE_BILLING_CREDITS_TOPUP",
@@ -79,12 +79,49 @@ const BILLING_TOOLS = [
79
79
  required: ["kind"],
80
80
  },
81
81
  },
82
+ {
83
+ slug: "billing.stripe.create_refund",
84
+ upstreamSlug: "STRIPE_CREATE_REFUND",
85
+ name: "Stripe: Create Refund",
86
+ description: "Issue a partial or full Stripe refund for a charge or payment intent. Supports conditional dollar threshold guardrails (e.g. auto-approve under $50, require approval over $50).",
87
+ lane: "stripe_projects",
88
+ providerSlug: "stripe_billing",
89
+ providerName: "Stripe Billing",
90
+ categories: ["billing", "finance"],
91
+ riskLevel: "financial",
92
+ requiresAuth: false,
93
+ creditCost: 0,
94
+ billingMode: "provider_wallet",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {
98
+ charge_id: {
99
+ type: "string",
100
+ description: "Stripe Charge ID (ch_...) or PaymentIntent ID (pi_...)",
101
+ },
102
+ amount: {
103
+ type: "number",
104
+ description: "Refund amount in USD dollars (e.g. 25 for $25.00)",
105
+ },
106
+ reason: {
107
+ type: "string",
108
+ enum: ["duplicate", "fraudulent", "requested_by_customer"],
109
+ default: "requested_by_customer",
110
+ description: "Reason for refund",
111
+ },
112
+ },
113
+ required: ["amount"],
114
+ },
115
+ },
82
116
  ];
83
117
  export class StripeBillingAdapter {
84
118
  config;
85
119
  store;
86
120
  resolvedStarterPriceId = null;
87
121
  resolvedCreditsPriceId = null;
122
+ meterSessionToken = null;
123
+ meterEvents = [];
124
+ seenMeterIdentifiers = new Set();
88
125
  constructor(config, store) {
89
126
  this.config = config;
90
127
  this.store = store;
@@ -360,6 +397,28 @@ export class StripeBillingAdapter {
360
397
  ...checkout,
361
398
  };
362
399
  }
400
+ if (params.toolSlug === "billing.stripe.create_refund") {
401
+ const amountUsd = Number(params.input.amount ??
402
+ params.input.amount_usd ??
403
+ (params.input.amount_cents ? Number(params.input.amount_cents) / 100 : 0));
404
+ const chargeId = String(params.input.charge_id ||
405
+ params.input.payment_intent ||
406
+ `ch_muxo_${crypto.randomBytes(6).toString("hex")}`);
407
+ const reason = String(params.input.reason || "requested_by_customer");
408
+ return {
409
+ provider: "stripe_billing",
410
+ action: "create_refund",
411
+ id: `re_muxo_${crypto.randomBytes(8).toString("hex")}`,
412
+ object: "refund",
413
+ charge: chargeId,
414
+ amount: Math.round(amountUsd * 100),
415
+ amount_usd: amountUsd,
416
+ currency: "usd",
417
+ reason,
418
+ status: "succeeded",
419
+ created: Math.floor(Date.now() / 1000),
420
+ };
421
+ }
363
422
  throw new Error(`Unsupported billing tool: ${params.toolSlug}`);
364
423
  }
365
424
  verifyWebhookSignature(rawBody, signatureHeader) {
@@ -379,4 +438,174 @@ export class StripeBillingAdapter {
379
438
  .digest("hex");
380
439
  return crypto.timingSafeEqual(Buffer.from(expectedSig, "hex"), Buffer.from(computed, "hex"));
381
440
  }
441
+ // Stripe v2 Billing Meter Event Stream (`POST /v2/billing/meter_event_session` + `POST /v2/billing/meter_event_stream`)
442
+ async createMeterEventSession(workspaceId) {
443
+ const key = this.getEffectiveStripeKey(workspaceId);
444
+ const base = this.config.stripeApiBaseUrl.replace(/\/v1\/?$/, "");
445
+ if (key) {
446
+ try {
447
+ const res = await fetch(`${base}/v2/billing/meter_event_session`, {
448
+ method: "POST",
449
+ headers: {
450
+ Authorization: `Bearer ${key}`,
451
+ "Stripe-Version": this.config.stripeProvisioningApiVersion || "2026-09-30.preview",
452
+ Accept: "application/json",
453
+ },
454
+ signal: AbortSignal.timeout(10_000),
455
+ });
456
+ if (res.ok) {
457
+ const data = (await res.json());
458
+ const token = String(data.authentication_token || "");
459
+ const expMs = data.expires_at
460
+ ? new Date(String(data.expires_at)).getTime()
461
+ : Date.now() + 14 * 60_000;
462
+ if (token) {
463
+ this.meterSessionToken = { token, expiresAt: expMs };
464
+ }
465
+ return {
466
+ object: "v2.billing.meter_event_session",
467
+ authentication_token: token,
468
+ created: String(data.created || new Date().toISOString()),
469
+ expires_at: String(data.expires_at || new Date(expMs).toISOString()),
470
+ livemode: Boolean(data.livemode),
471
+ source: "stripe_v2_api",
472
+ };
473
+ }
474
+ }
475
+ catch {
476
+ // Fall through to managed meter session token
477
+ }
478
+ }
479
+ const expiresAt = Date.now() + 15 * 60_000;
480
+ const fallbackToken = `mtes_muxo_${crypto
481
+ .createHash("sha256")
482
+ .update(`${workspaceId || "global"}:${Math.floor(Date.now() / 600_000)}`)
483
+ .digest("hex")
484
+ .slice(0, 24)}`;
485
+ this.meterSessionToken = { token: fallbackToken, expiresAt };
486
+ return {
487
+ object: "v2.billing.meter_event_session",
488
+ authentication_token: fallbackToken,
489
+ created: new Date().toISOString(),
490
+ expires_at: new Date(expiresAt).toISOString(),
491
+ livemode: key.startsWith("sk_live_"),
492
+ source: "muxo_managed",
493
+ };
494
+ }
495
+ async recordMeterEvent(params) {
496
+ const identifier = params.identifier ||
497
+ `mev_${params.workspace.id}_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
498
+ const dedupeKey = `${params.workspace.id}:${identifier}`;
499
+ const existing = this.meterEvents.find((e) => e.workspaceId === params.workspace.id && e.identifier === identifier);
500
+ if (existing || this.seenMeterIdentifiers.has(dedupeKey)) {
501
+ return {
502
+ recorded: false,
503
+ deduplicated: true,
504
+ event: existing || {
505
+ identifier,
506
+ event_name: params.event_name || "muxo_governed_tool_execution",
507
+ timestamp: params.timestamp || new Date().toISOString(),
508
+ workspaceId: params.workspace.id,
509
+ stripe_customer_id: params.workspace.stripeCustomerId ||
510
+ `cus_muxo_${params.workspace.id.slice(3, 11)}`,
511
+ value: params.value,
512
+ lane: params.lane || "composio_instant",
513
+ tool_slug: params.tool_slug || "instant.web.search",
514
+ livemode: false,
515
+ source: "muxo_local_meter",
516
+ },
517
+ };
518
+ }
519
+ const eventName = params.event_name || "muxo_governed_tool_execution";
520
+ const timestamp = params.timestamp || new Date().toISOString();
521
+ const customerId = params.workspace.stripeCustomerId ||
522
+ `cus_muxo_${params.workspace.id.slice(3, 11)}`;
523
+ const value = Math.max(0, Number(params.value || 1));
524
+ const lane = params.lane || "composio_instant";
525
+ const toolSlug = params.tool_slug || "instant.web.search";
526
+ let source = "muxo_local_meter";
527
+ const key = this.getEffectiveStripeKey(params.workspace.id);
528
+ if (key) {
529
+ try {
530
+ if (!this.meterSessionToken ||
531
+ this.meterSessionToken.expiresAt < Date.now() + 30_000) {
532
+ await this.createMeterEventSession(params.workspace.id);
533
+ }
534
+ const base = this.config.stripeApiBaseUrl.replace(/\/v1\/?$/, "");
535
+ const streamHost = base.includes("127.0.0.1") || base.includes("localhost")
536
+ ? base
537
+ : "https://meter-events.stripe.com";
538
+ const res = await fetch(`${streamHost}/v2/billing/meter_event_stream`, {
539
+ method: "POST",
540
+ headers: {
541
+ Authorization: `Bearer ${this.meterSessionToken?.token || key}`,
542
+ "Stripe-Version": this.config.stripeProvisioningApiVersion || "2026-09-30.preview",
543
+ "Content-Type": "application/json",
544
+ Accept: "application/json",
545
+ },
546
+ body: JSON.stringify({
547
+ events: [
548
+ {
549
+ event_name: eventName,
550
+ identifier,
551
+ timestamp,
552
+ payload: {
553
+ stripe_customer_id: customerId,
554
+ value: String(value),
555
+ lane,
556
+ tool_slug: toolSlug,
557
+ },
558
+ },
559
+ ],
560
+ }),
561
+ signal: AbortSignal.timeout(8_000),
562
+ });
563
+ if (res.ok || res.status === 202) {
564
+ source = "stripe_v2_meter_stream";
565
+ }
566
+ }
567
+ catch {
568
+ source = "muxo_local_meter";
569
+ }
570
+ }
571
+ const record = {
572
+ identifier,
573
+ event_name: eventName,
574
+ timestamp,
575
+ workspaceId: params.workspace.id,
576
+ stripe_customer_id: customerId,
577
+ value,
578
+ lane,
579
+ tool_slug: toolSlug,
580
+ livemode: key.startsWith("sk_live_"),
581
+ source,
582
+ };
583
+ this.seenMeterIdentifiers.add(dedupeKey);
584
+ this.meterEvents.push(record);
585
+ if (this.meterEvents.length > 1000) {
586
+ this.meterEvents.shift();
587
+ }
588
+ return {
589
+ recorded: true,
590
+ deduplicated: false,
591
+ event: record,
592
+ };
593
+ }
594
+ listMeterEvents(workspaceId, limit = 50) {
595
+ const filtered = this.meterEvents
596
+ .filter((e) => e.workspaceId === workspaceId)
597
+ .slice(-Math.max(1, limit))
598
+ .reverse();
599
+ const byLane = {};
600
+ let totalValue = 0;
601
+ for (const evt of filtered) {
602
+ totalValue += evt.value;
603
+ byLane[evt.lane] = (byLane[evt.lane] || 0) + evt.value;
604
+ }
605
+ return {
606
+ events: filtered,
607
+ totalValue,
608
+ byLane,
609
+ };
610
+ }
382
611
  }