@muxoai/sdk 1.3.0 → 1.4.3

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 (76) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +19 -4
  3. package/.codex/config.toml +9 -2
  4. package/ARCHITECTURE.md +16 -6
  5. package/README.md +69 -24
  6. package/dist/clerk.d.ts +35 -0
  7. package/dist/clerk.js +201 -0
  8. package/dist/cli.js +1677 -103
  9. package/dist/composio-catalog.d.ts +234 -0
  10. package/dist/composio-catalog.js +1894 -0
  11. package/dist/config.d.ts +16 -0
  12. package/dist/config.js +27 -1
  13. package/dist/durable-workflow.d.ts +112 -0
  14. package/dist/durable-workflow.js +284 -0
  15. package/dist/engine.d.ts +377 -6
  16. package/dist/engine.js +1919 -94
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +3 -0
  19. package/dist/mcp-schema.d.ts +286 -0
  20. package/dist/mcp-schema.js +2580 -0
  21. package/dist/mcp.d.ts +2 -25
  22. package/dist/mcp.js +1008 -507
  23. package/dist/memory-graph.d.ts +45 -0
  24. package/dist/memory-graph.js +293 -0
  25. package/dist/nl.js +214 -35
  26. package/dist/oauth.d.ts +32 -0
  27. package/dist/oauth.js +134 -0
  28. package/dist/oss-suite.d.ts +217 -0
  29. package/dist/oss-suite.js +836 -0
  30. package/dist/policy.d.ts +190 -0
  31. package/dist/policy.js +1597 -0
  32. package/dist/providers/composio-instant.d.ts +4 -0
  33. package/dist/providers/composio-instant.js +890 -535
  34. package/dist/providers/composio-sdk.d.ts +126 -5
  35. package/dist/providers/composio-sdk.js +902 -79
  36. package/dist/providers/microsoft-governance.d.ts +89 -3
  37. package/dist/providers/microsoft-governance.js +673 -26
  38. package/dist/providers/stripe-billing.d.ts +42 -0
  39. package/dist/providers/stripe-billing.js +244 -5
  40. package/dist/providers/stripe-projects.d.ts +459 -4
  41. package/dist/providers/stripe-projects.js +2674 -122
  42. package/dist/providers/supermemory.d.ts +102 -6
  43. package/dist/providers/supermemory.js +609 -55
  44. package/dist/provisioning-managed.d.ts +50 -0
  45. package/dist/provisioning-managed.js +155 -0
  46. package/dist/sdk.d.ts +728 -7
  47. package/dist/sdk.js +539 -10
  48. package/dist/server.js +2744 -321
  49. package/dist/store.d.ts +138 -6
  50. package/dist/store.js +932 -58
  51. package/dist/types.d.ts +330 -4
  52. package/dist/ui/console-html.d.ts +62 -0
  53. package/dist/ui/console-html.js +3742 -2228
  54. package/dist/ui/design-tokens.d.ts +177 -0
  55. package/dist/ui/design-tokens.js +210 -0
  56. package/dist/version.d.ts +1 -0
  57. package/dist/version.js +1 -0
  58. package/package.json +6 -5
  59. package/plugins/muxo/.app.json +1 -1
  60. package/plugins/muxo/.claude-plugin/plugin.json +3 -3
  61. package/plugins/muxo/.codex-plugin/plugin.json +19 -10
  62. package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
  63. package/plugins/muxo/.mcp.json +1 -33
  64. package/plugins/muxo/assets/logo.png +0 -0
  65. package/plugins/muxo/hooks/session-start.sh +16 -4
  66. package/plugins/muxo/hooks/user-prompt-submit.sh +14 -1
  67. package/plugins/muxo/mcp.json +0 -10
  68. package/plugins/muxo/plugin.json +65 -8
  69. package/plugins/muxo/skills/muxo/SKILL.md +142 -14
  70. package/plugins/muxo/skills/muxo/agents/openai.yaml +12 -0
  71. package/plugins/muxo/skills/muxo/references/cli.md +9 -3
  72. package/plugins/muxo/skills/muxo/references/mcp.md +22 -10
  73. package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
  74. package/plugins/muxo/skills/setup/SKILL.md +17 -3
  75. package/plugins/muxo/skills/setup/agents/openai.yaml +12 -0
  76. package/src/catalogues/stripe-projects-catalog.json +140 -0
package/dist/store.js CHANGED
@@ -18,16 +18,58 @@ const DEFAULT_POLICY = {
18
18
  allowProvisioning: true,
19
19
  maxCreditsPerRun: 250,
20
20
  };
21
+ const RECEIPTS_COLUMN_NAMES = [
22
+ "id",
23
+ "run_id",
24
+ "plan_id",
25
+ "workspace_id",
26
+ "actor_id",
27
+ "tool_slug",
28
+ "lane",
29
+ "risk_level",
30
+ "status",
31
+ "actions_metered",
32
+ "credits_metered",
33
+ "input_hash",
34
+ "output_hash",
35
+ "prev_receipt_hash",
36
+ "receipt_hash",
37
+ "signature",
38
+ "decision_record_json",
39
+ "issued_at",
40
+ ];
41
+ const RECEIPTS_TABLE_COLUMNS = `
42
+ id TEXT PRIMARY KEY,
43
+ run_id TEXT NOT NULL,
44
+ plan_id TEXT NOT NULL,
45
+ workspace_id TEXT NOT NULL,
46
+ actor_id TEXT NOT NULL,
47
+ tool_slug TEXT NOT NULL,
48
+ lane TEXT NOT NULL,
49
+ risk_level TEXT NOT NULL,
50
+ status TEXT NOT NULL,
51
+ actions_metered INTEGER NOT NULL,
52
+ credits_metered INTEGER NOT NULL,
53
+ input_hash TEXT NOT NULL,
54
+ output_hash TEXT NOT NULL,
55
+ prev_receipt_hash TEXT NOT NULL,
56
+ receipt_hash TEXT NOT NULL,
57
+ signature TEXT NOT NULL,
58
+ decision_record_json TEXT,
59
+ issued_at TEXT NOT NULL
60
+ `;
21
61
  export class MuxoStore {
22
62
  db;
23
63
  vaultKey;
24
64
  constructor(dbPath, vaultKey) {
25
65
  this.db = new DatabaseSync(dbPath);
66
+ this.db.exec("PRAGMA busy_timeout = 10000;");
26
67
  this.vaultKey = vaultKey;
27
68
  this.initSchema();
28
69
  }
29
70
  initSchema() {
30
71
  this.db.exec(`
72
+ PRAGMA busy_timeout = 10000;
31
73
  PRAGMA journal_mode = WAL;
32
74
  PRAGMA foreign_keys = ON;
33
75
 
@@ -153,26 +195,7 @@ export class MuxoStore {
153
195
  completed_at TEXT
154
196
  );
155
197
 
156
- CREATE TABLE IF NOT EXISTS receipts (
157
- id TEXT PRIMARY KEY,
158
- run_id TEXT NOT NULL UNIQUE,
159
- plan_id TEXT NOT NULL,
160
- workspace_id TEXT NOT NULL,
161
- actor_id TEXT NOT NULL,
162
- tool_slug TEXT NOT NULL,
163
- lane TEXT NOT NULL,
164
- risk_level TEXT NOT NULL,
165
- status TEXT NOT NULL,
166
- actions_metered INTEGER NOT NULL,
167
- credits_metered INTEGER NOT NULL,
168
- input_hash TEXT NOT NULL,
169
- output_hash TEXT NOT NULL,
170
- prev_receipt_hash TEXT NOT NULL,
171
- receipt_hash TEXT NOT NULL,
172
- signature TEXT NOT NULL,
173
- decision_record_json TEXT,
174
- issued_at TEXT NOT NULL
175
- );
198
+ CREATE TABLE IF NOT EXISTS receipts (${RECEIPTS_TABLE_COLUMNS});
176
199
 
177
200
  CREATE TABLE IF NOT EXISTS memories (
178
201
  id TEXT PRIMARY KEY,
@@ -186,6 +209,23 @@ export class MuxoStore {
186
209
  created_at TEXT NOT NULL
187
210
  );
188
211
 
212
+ CREATE TABLE IF NOT EXISTS temporal_facts (
213
+ id TEXT PRIMARY KEY,
214
+ workspace_id TEXT NOT NULL,
215
+ subject TEXT NOT NULL,
216
+ predicate TEXT NOT NULL,
217
+ object TEXT NOT NULL,
218
+ summary TEXT NOT NULL,
219
+ confidence REAL NOT NULL DEFAULT 0.95,
220
+ valid_from TEXT NOT NULL,
221
+ valid_to TEXT,
222
+ superseded_by_id TEXT,
223
+ source_kind TEXT NOT NULL DEFAULT 'fact',
224
+ source_reference_id TEXT,
225
+ metadata_json TEXT NOT NULL DEFAULT '{}',
226
+ created_at TEXT NOT NULL
227
+ );
228
+
189
229
  CREATE TABLE IF NOT EXISTS agent_claims (
190
230
  claim_slug TEXT PRIMARY KEY,
191
231
  workspace_id TEXT NOT NULL,
@@ -223,12 +263,42 @@ export class MuxoStore {
223
263
  updated_at TEXT NOT NULL,
224
264
  PRIMARY KEY (workspace_id, provider)
225
265
  );
266
+
267
+ CREATE TABLE IF NOT EXISTS workspace_members (
268
+ id TEXT PRIMARY KEY,
269
+ workspace_id TEXT NOT NULL,
270
+ user_id TEXT NOT NULL,
271
+ role TEXT NOT NULL DEFAULT 'operator',
272
+ team_name TEXT,
273
+ joined_via_key_id TEXT,
274
+ created_at TEXT NOT NULL,
275
+ UNIQUE(workspace_id, user_id)
276
+ );
277
+
278
+ CREATE TABLE IF NOT EXISTS clerk_users (
279
+ clerk_user_id TEXT PRIMARY KEY,
280
+ user_id TEXT NOT NULL,
281
+ created_at TEXT NOT NULL
282
+ );
283
+
284
+ CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
285
+ token_hash TEXT PRIMARY KEY,
286
+ key_id TEXT NOT NULL,
287
+ workspace_id TEXT NOT NULL,
288
+ client_id TEXT NOT NULL,
289
+ scope TEXT NOT NULL,
290
+ encrypted_access_token TEXT NOT NULL,
291
+ created_at TEXT NOT NULL
292
+ );
226
293
  `);
227
294
  // Backwards-compatible column migrations for existing databases
228
295
  for (const stmt of [
229
296
  "ALTER TABLE plans ADD COLUMN decision_record_json TEXT",
230
297
  "ALTER TABLE plans ADD COLUMN memory_context_json TEXT",
231
298
  "ALTER TABLE receipts ADD COLUMN decision_record_json TEXT",
299
+ "ALTER TABLE api_keys ADD COLUMN team_name TEXT",
300
+ "ALTER TABLE api_keys ADD COLUMN role TEXT DEFAULT 'operator'",
301
+ "ALTER TABLE api_keys ADD COLUMN created_by_email TEXT",
232
302
  ]) {
233
303
  try {
234
304
  this.db.exec(stmt);
@@ -237,6 +307,38 @@ export class MuxoStore {
237
307
  // Column already exists
238
308
  }
239
309
  }
310
+ this.migrateReceiptsAllowMultiplePerRun();
311
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_receipts_workspace_run ON receipts(workspace_id, run_id)");
312
+ }
313
+ /**
314
+ * Older databases declared receipts.run_id UNIQUE, which blocks cancellation receipts from
315
+ * superseding a run's original receipt. SQLite cannot drop a constraint, so rebuild the table.
316
+ */
317
+ migrateReceiptsAllowMultiplePerRun() {
318
+ const indexes = this.db.prepare("PRAGMA index_list(receipts)").all();
319
+ const hasUniqueRunId = indexes.some((idx) => {
320
+ if (!idx.unique || idx.origin !== "u")
321
+ return false;
322
+ const cols = this.db.prepare(`PRAGMA index_info("${idx.name}")`).all();
323
+ return cols.length === 1 && cols[0].name === "run_id";
324
+ });
325
+ if (!hasUniqueRunId)
326
+ return;
327
+ const columns = RECEIPTS_COLUMN_NAMES.join(", ");
328
+ this.db.exec("BEGIN");
329
+ try {
330
+ this.db.exec(`
331
+ CREATE TABLE receipts_migrated (${RECEIPTS_TABLE_COLUMNS});
332
+ INSERT INTO receipts_migrated (${columns}) SELECT ${columns} FROM receipts ORDER BY rowid;
333
+ DROP TABLE receipts;
334
+ ALTER TABLE receipts_migrated RENAME TO receipts;
335
+ `);
336
+ this.db.exec("COMMIT");
337
+ }
338
+ catch (err) {
339
+ this.db.exec("ROLLBACK");
340
+ throw err;
341
+ }
240
342
  }
241
343
  // --- Encryption Vault (AES-256-GCM) ---
242
344
  encryptSecrets(workspaceId, secrets) {
@@ -332,11 +434,24 @@ export class MuxoStore {
332
434
  referenceId: workspaceId,
333
435
  description: "Free Plan initial monthly allowance (1,000 actions + 250 trial credits)",
334
436
  });
437
+ this.ensureWorkspaceMembership({
438
+ workspaceId,
439
+ userId,
440
+ role: "owner",
441
+ });
335
442
  const sessionToken = this.createSession(userId, workspaceId);
443
+ const defaultKey = this.createApiKey({
444
+ workspaceId,
445
+ name: "Default Live Key",
446
+ environment: "live",
447
+ role: "owner",
448
+ createdByEmail: params.email.toLowerCase().trim(),
449
+ });
336
450
  return {
337
451
  user: this.getUser(userId),
338
452
  workspace: this.getWorkspace(workspaceId),
339
453
  sessionToken,
454
+ apiKey: defaultKey.secretKey,
340
455
  };
341
456
  }
342
457
  loginUser(email, password) {
@@ -349,6 +464,11 @@ export class MuxoStore {
349
464
  return null;
350
465
  const user = this.mapUser(row);
351
466
  const workspace = this.getWorkspace(user.workspaceId);
467
+ this.ensureWorkspaceMembership({
468
+ workspaceId: workspace.id,
469
+ userId: user.id,
470
+ role: user.role,
471
+ });
352
472
  const sessionToken = this.createSession(user.id, workspace.id);
353
473
  return { user, workspace, sessionToken };
354
474
  }
@@ -362,7 +482,7 @@ export class MuxoStore {
362
482
  .run(this.hashToken(rawToken), userId, workspaceId, expires.toISOString(), now.toISOString());
363
483
  return rawToken;
364
484
  }
365
- resolveAuthToken(token) {
485
+ resolveAuthToken(token, workspaceOverrideId) {
366
486
  const trimmed = token.trim();
367
487
  if (!trimmed)
368
488
  return null;
@@ -400,11 +520,22 @@ export class MuxoStore {
400
520
  return null;
401
521
  }
402
522
  const user = this.getUser(String(sessionRow.user_id));
403
- const workspace = this.getWorkspace(String(sessionRow.workspace_id));
404
- if (!user || !workspace)
523
+ if (!user)
524
+ return null;
525
+ let targetWorkspaceId = String(sessionRow.workspace_id || user.workspaceId);
526
+ if (workspaceOverrideId &&
527
+ workspaceOverrideId.trim() &&
528
+ this.userCanAccessWorkspace(user.id, workspaceOverrideId.trim())) {
529
+ targetWorkspaceId = workspaceOverrideId.trim();
530
+ }
531
+ else if (user.workspaceId && this.userCanAccessWorkspace(user.id, user.workspaceId)) {
532
+ targetWorkspaceId = user.workspaceId;
533
+ }
534
+ const workspace = this.getWorkspace(targetWorkspaceId);
535
+ if (!workspace)
405
536
  return null;
406
537
  return {
407
- user,
538
+ user: { ...user, workspaceId: workspace.id },
408
539
  workspace,
409
540
  authType: "session",
410
541
  actorId: user.id,
@@ -422,6 +553,326 @@ export class MuxoStore {
422
553
  .get(workspaceId);
423
554
  return row ? this.mapWorkspace(row) : null;
424
555
  }
556
+ ensureWorkspaceMembership(params) {
557
+ const now = new Date().toISOString();
558
+ const role = params.role || "operator";
559
+ const existing = this.db
560
+ .prepare("SELECT * FROM workspace_members WHERE workspace_id = ? AND user_id = ?")
561
+ .get(params.workspaceId, params.userId);
562
+ if (existing) {
563
+ if (params.teamName || params.role) {
564
+ this.db
565
+ .prepare("UPDATE workspace_members SET role = COALESCE(?, role), team_name = COALESCE(?, team_name) WHERE id = ?")
566
+ .run(params.role || null, params.teamName || null, String(existing.id));
567
+ }
568
+ return {
569
+ id: String(existing.id),
570
+ workspaceId: String(existing.workspace_id),
571
+ userId: String(existing.user_id),
572
+ role: (params.role || String(existing.role) || "operator"),
573
+ teamName: params.teamName ?? (existing.team_name ? String(existing.team_name) : null),
574
+ joinedViaKeyId: existing.joined_via_key_id ? String(existing.joined_via_key_id) : null,
575
+ createdAt: String(existing.created_at),
576
+ };
577
+ }
578
+ const id = `wsm_${crypto.randomBytes(8).toString("hex")}`;
579
+ this.db
580
+ .prepare(`INSERT INTO workspace_members (
581
+ id, workspace_id, user_id, role, team_name, joined_via_key_id, created_at
582
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`)
583
+ .run(id, params.workspaceId, params.userId, role, params.teamName || null, params.joinedViaKeyId || null, now);
584
+ return {
585
+ id,
586
+ workspaceId: params.workspaceId,
587
+ userId: params.userId,
588
+ role,
589
+ teamName: params.teamName || null,
590
+ joinedViaKeyId: params.joinedViaKeyId || null,
591
+ createdAt: now,
592
+ };
593
+ }
594
+ userCanAccessWorkspace(userId, workspaceId) {
595
+ const ws = this.getWorkspace(workspaceId);
596
+ if (!ws)
597
+ return false;
598
+ if (ws.ownerUserId === userId)
599
+ return true;
600
+ const user = this.getUser(userId);
601
+ if (user?.workspaceId === workspaceId)
602
+ return true;
603
+ const mem = this.db
604
+ .prepare("SELECT id FROM workspace_members WHERE workspace_id = ? AND user_id = ?")
605
+ .get(workspaceId, userId);
606
+ return Boolean(mem);
607
+ }
608
+ listUserWorkspaces(userId, activeWorkspaceId) {
609
+ const user = this.getUser(userId);
610
+ const currentId = activeWorkspaceId || user?.workspaceId || "";
611
+ const ownedRows = this.db
612
+ .prepare("SELECT * FROM workspaces WHERE owner_user_id = ? ORDER BY created_at ASC")
613
+ .all(userId);
614
+ const memberRows = this.db
615
+ .prepare(`SELECT w.*, m.role AS member_role, m.team_name AS member_team_name
616
+ FROM workspace_members m
617
+ JOIN workspaces w ON w.id = m.workspace_id
618
+ WHERE m.user_id = ?
619
+ ORDER BY m.created_at ASC`)
620
+ .all(userId);
621
+ const byId = new Map();
622
+ for (const r of ownedRows) {
623
+ const ws = this.mapWorkspace(r);
624
+ byId.set(ws.id, {
625
+ id: ws.id,
626
+ name: ws.name,
627
+ slug: ws.slug,
628
+ tier: ws.tier,
629
+ creditsBalance: ws.creditsBalance,
630
+ monthlyActionsUsed: ws.monthlyActionsUsed,
631
+ monthlyActionsLimit: ws.monthlyActionsLimit,
632
+ role: "owner",
633
+ teamName: null,
634
+ isCurrent: ws.id === currentId,
635
+ createdAt: ws.createdAt,
636
+ });
637
+ }
638
+ for (const r of memberRows) {
639
+ const ws = this.mapWorkspace(r);
640
+ const existing = byId.get(ws.id);
641
+ byId.set(ws.id, {
642
+ id: ws.id,
643
+ name: ws.name,
644
+ slug: ws.slug,
645
+ tier: ws.tier,
646
+ creditsBalance: ws.creditsBalance,
647
+ monthlyActionsUsed: ws.monthlyActionsUsed,
648
+ monthlyActionsLimit: ws.monthlyActionsLimit,
649
+ role: existing?.role === "owner" ? "owner" : String(r.member_role || "operator"),
650
+ teamName: r.member_team_name ? String(r.member_team_name) : existing?.teamName || null,
651
+ isCurrent: ws.id === currentId,
652
+ createdAt: ws.createdAt,
653
+ });
654
+ }
655
+ if (currentId && !byId.has(currentId)) {
656
+ const ws = this.getWorkspace(currentId);
657
+ if (ws) {
658
+ byId.set(ws.id, {
659
+ id: ws.id,
660
+ name: ws.name,
661
+ slug: ws.slug,
662
+ tier: ws.tier,
663
+ creditsBalance: ws.creditsBalance,
664
+ monthlyActionsUsed: ws.monthlyActionsUsed,
665
+ monthlyActionsLimit: ws.monthlyActionsLimit,
666
+ role: ws.ownerUserId === userId ? "owner" : "operator",
667
+ teamName: null,
668
+ isCurrent: true,
669
+ createdAt: ws.createdAt,
670
+ });
671
+ }
672
+ }
673
+ return Array.from(byId.values());
674
+ }
675
+ switchUserWorkspace(userId, workspaceId) {
676
+ if (!this.userCanAccessWorkspace(userId, workspaceId))
677
+ return null;
678
+ const workspace = this.getWorkspace(workspaceId);
679
+ if (!workspace)
680
+ return null;
681
+ this.db
682
+ .prepare("UPDATE users SET workspace_id = ? WHERE id = ?")
683
+ .run(workspaceId, userId);
684
+ this.db
685
+ .prepare("UPDATE sessions SET workspace_id = ? WHERE user_id = ?")
686
+ .run(workspaceId, userId);
687
+ const user = this.getUser(userId);
688
+ if (!user)
689
+ return null;
690
+ return { user, workspace };
691
+ }
692
+ createWorkspaceForUser(params) {
693
+ const now = new Date().toISOString();
694
+ const workspaceId = `ws_${crypto.randomBytes(8).toString("hex")}`;
695
+ const cleanName = params.name.trim() || "Team Workspace";
696
+ const slugBase = cleanName
697
+ .toLowerCase()
698
+ .replace(/[^a-z0-9]+/g, "-")
699
+ .replace(/^-|-$/g, "");
700
+ const slug = `${slugBase || "ws"}-${crypto.randomBytes(3).toString("hex")}`;
701
+ const projectsEnvName = `muxo_${workspaceId.slice(3, 11)}`;
702
+ const starterCredits = 250;
703
+ this.db
704
+ .prepare(`INSERT INTO workspaces (
705
+ id, name, slug, owner_user_id, tier,
706
+ monthly_actions_limit, monthly_actions_used, credits_balance,
707
+ stripe_customer_id, stripe_subscription_id, projects_env_name,
708
+ policy_json, created_at, updated_at
709
+ ) VALUES (?, ?, ?, ?, 'free', 1000, 0, ?, NULL, NULL, ?, ?, ?, ?)`)
710
+ .run(workspaceId, cleanName, slug, params.userId, starterCredits, projectsEnvName, JSON.stringify(DEFAULT_POLICY), now, now);
711
+ this.addLedgerEntry({
712
+ workspaceId,
713
+ kind: "plan_grant",
714
+ actionsDelta: 1000,
715
+ creditsDelta: starterCredits,
716
+ balanceAfter: starterCredits,
717
+ referenceId: workspaceId,
718
+ description: `Workspace '${cleanName}' initial monthly allowance (1,000 actions + 250 trial credits)`,
719
+ });
720
+ const user = this.getUser(params.userId);
721
+ const keyRes = this.createApiKey({
722
+ workspaceId,
723
+ name: params.teamName ? `${params.teamName} — Team Key` : `${cleanName} — Default Key`,
724
+ environment: "live",
725
+ teamName: params.teamName || cleanName,
726
+ role: params.role || "owner",
727
+ createdByEmail: user?.email || null,
728
+ });
729
+ const membership = this.ensureWorkspaceMembership({
730
+ workspaceId,
731
+ userId: params.userId,
732
+ role: "owner",
733
+ teamName: params.teamName || null,
734
+ joinedViaKeyId: keyRes.record.id,
735
+ });
736
+ if (params.switchActive !== false) {
737
+ this.switchUserWorkspace(params.userId, workspaceId);
738
+ }
739
+ return {
740
+ workspace: this.getWorkspace(workspaceId),
741
+ apiKey: keyRes.secretKey,
742
+ keyRecord: keyRes.record,
743
+ membership,
744
+ };
745
+ }
746
+ joinWorkspaceWithApiKey(params) {
747
+ const trimmed = String(params.apiKey || "").trim();
748
+ if (!trimmed.startsWith("mk_live_") && !trimmed.startsWith("mk_test_")) {
749
+ return null;
750
+ }
751
+ const hash = this.hashToken(trimmed);
752
+ const row = this.db
753
+ .prepare("SELECT * FROM api_keys WHERE key_hash = ? AND revoked_at IS NULL")
754
+ .get(hash);
755
+ if (!row)
756
+ return null;
757
+ const workspace = this.getWorkspace(String(row.workspace_id));
758
+ if (!workspace)
759
+ return null;
760
+ const keyRecord = {
761
+ id: String(row.id),
762
+ workspaceId: String(row.workspace_id),
763
+ name: String(row.name),
764
+ keyPrefix: String(row.key_prefix),
765
+ environment: String(row.environment),
766
+ scopes: JSON.parse(String(row.scopes_json || "[]")),
767
+ teamName: row.team_name ? String(row.team_name) : null,
768
+ role: (row.role ? String(row.role) : "operator"),
769
+ createdByEmail: row.created_by_email ? String(row.created_by_email) : null,
770
+ lastUsedAt: row.last_used_at ? String(row.last_used_at) : null,
771
+ revokedAt: row.revoked_at ? String(row.revoked_at) : null,
772
+ createdAt: String(row.created_at),
773
+ };
774
+ let membership = null;
775
+ let user = null;
776
+ if (params.userId) {
777
+ membership = this.ensureWorkspaceMembership({
778
+ workspaceId: workspace.id,
779
+ userId: params.userId,
780
+ role: workspace.ownerUserId === params.userId ? "owner" : (keyRecord.role || "operator"),
781
+ teamName: keyRecord.teamName || keyRecord.name,
782
+ joinedViaKeyId: keyRecord.id,
783
+ });
784
+ if (params.switchActive !== false) {
785
+ this.switchUserWorkspace(params.userId, workspace.id);
786
+ }
787
+ user = this.getUser(params.userId);
788
+ }
789
+ else {
790
+ user = this.getUser(workspace.ownerUserId);
791
+ }
792
+ return {
793
+ workspace,
794
+ keyRecord,
795
+ user,
796
+ membership,
797
+ };
798
+ }
799
+ onboardTeam(params) {
800
+ const cleanTeam = params.teamName.trim() || "Engineering Team";
801
+ const role = params.role || "operator";
802
+ const mode = params.mode === "dedicated_workspace" ? "dedicated_workspace" : "shared_workspace";
803
+ const cleanBase = params.baseUrl.replace(/\/$/, "");
804
+ const env = params.environment === "test" ? "test" : "live";
805
+ let targetWorkspace;
806
+ let apiKey;
807
+ let keyRecord;
808
+ if (mode === "dedicated_workspace") {
809
+ const currentWs = this.getWorkspace(params.currentWorkspaceId);
810
+ const wsName = currentWs ? `${currentWs.name} — ${cleanTeam}` : `${cleanTeam} Workspace`;
811
+ const created = this.createWorkspaceForUser({
812
+ userId: params.userId,
813
+ name: wsName,
814
+ teamName: cleanTeam,
815
+ role,
816
+ switchActive: false,
817
+ });
818
+ targetWorkspace = created.workspace;
819
+ apiKey = created.apiKey;
820
+ keyRecord = created.keyRecord;
821
+ }
822
+ else {
823
+ const currentWs = this.getWorkspace(params.currentWorkspaceId);
824
+ if (!currentWs)
825
+ throw new Error(`Workspace ${params.currentWorkspaceId} not found`);
826
+ targetWorkspace = currentWs;
827
+ const createdKey = this.createApiKey({
828
+ workspaceId: targetWorkspace.id,
829
+ name: `${cleanTeam} (${role})`,
830
+ environment: env,
831
+ scopes: params.scopes,
832
+ teamName: cleanTeam,
833
+ role,
834
+ createdByEmail: params.userEmail || null,
835
+ });
836
+ apiKey = createdKey.secretKey;
837
+ keyRecord = createdKey.record;
838
+ }
839
+ let claimUrl = null;
840
+ if (params.inviteEmail && params.inviteEmail.includes("@")) {
841
+ const claim = this.createAgentClaim({
842
+ workspaceId: targetWorkspace.id,
843
+ email: params.inviteEmail,
844
+ baseUrl: cleanBase,
845
+ });
846
+ claimUrl = claim.claimUrl;
847
+ }
848
+ return {
849
+ mode,
850
+ workspace: targetWorkspace,
851
+ teamName: cleanTeam,
852
+ role,
853
+ apiKey,
854
+ keyRecord,
855
+ claimUrl,
856
+ consoleUrl: `${cleanBase}/login`,
857
+ cliSetupCommand: `npx muxo setup --api-key ${apiKey} --url ${cleanBase}`,
858
+ mcpConfig: {
859
+ mcpServers: {
860
+ muxo: {
861
+ url: `${cleanBase}/mcp`,
862
+ headers: {
863
+ Authorization: `Bearer ${apiKey}`,
864
+ },
865
+ },
866
+ },
867
+ },
868
+ instructions: [
869
+ `Share this Team API Key with ${cleanTeam} (${role} access to '${targetWorkspace.name}').`,
870
+ `Console: Open ${cleanBase}/login, click "Join with a Team API Key", and paste the key (if signed in with Clerk, the workspace is permanently linked to their switcher).`,
871
+ `CLI & MCP: Run \`npx -y @muxoai/platform setup --api-key ${apiKey}\` to configure Claude Code, Cursor, Windsurf, or Codex in one command.`,
872
+ `Approvals: Team members can also paste this key directly on any ${cleanBase}/approve/<planId> link to review and approve actions.`,
873
+ ],
874
+ };
875
+ }
425
876
  updateWorkspacePolicy(workspaceId, policy) {
426
877
  const ws = this.getWorkspace(workspaceId);
427
878
  if (!ws)
@@ -443,12 +894,16 @@ export class MuxoStore {
443
894
  const rawSecret = `mk_${env}_${id.slice(4)}_${crypto.randomBytes(16).toString("hex")}`;
444
895
  const keyPrefix = `${rawSecret.slice(0, 16)}...${rawSecret.slice(-4)}`;
445
896
  const scopes = params.scopes || ["tools:read", "tools:execute", "resources:provision"];
897
+ const teamName = params.teamName ? params.teamName.trim() : null;
898
+ const role = params.role || "operator";
899
+ const createdByEmail = params.createdByEmail ? params.createdByEmail.trim() : null;
446
900
  const now = new Date().toISOString();
447
901
  this.db
448
902
  .prepare(`INSERT INTO api_keys (
449
- id, workspace_id, name, key_prefix, key_hash, environment, scopes_json, created_at
450
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
451
- .run(id, params.workspaceId, params.name, keyPrefix, this.hashToken(rawSecret), env, JSON.stringify(scopes), now);
903
+ id, workspace_id, name, key_prefix, key_hash, environment, scopes_json,
904
+ team_name, role, created_by_email, created_at
905
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
906
+ .run(id, params.workspaceId, params.name, keyPrefix, this.hashToken(rawSecret), env, JSON.stringify(scopes), teamName, role, createdByEmail, now);
452
907
  return {
453
908
  record: {
454
909
  id,
@@ -457,6 +912,9 @@ export class MuxoStore {
457
912
  keyPrefix,
458
913
  environment: env,
459
914
  scopes,
915
+ teamName,
916
+ role,
917
+ createdByEmail,
460
918
  lastUsedAt: null,
461
919
  revokedAt: null,
462
920
  createdAt: now,
@@ -475,6 +933,9 @@ export class MuxoStore {
475
933
  keyPrefix: String(r.key_prefix),
476
934
  environment: String(r.environment),
477
935
  scopes: JSON.parse(String(r.scopes_json || "[]")),
936
+ teamName: r.team_name ? String(r.team_name) : null,
937
+ role: (r.role ? String(r.role) : "operator"),
938
+ createdByEmail: r.created_by_email ? String(r.created_by_email) : null,
478
939
  lastUsedAt: r.last_used_at ? String(r.last_used_at) : null,
479
940
  revokedAt: r.revoked_at ? String(r.revoked_at) : null,
480
941
  createdAt: String(r.created_at),
@@ -485,8 +946,95 @@ export class MuxoStore {
485
946
  const res = this.db
486
947
  .prepare("UPDATE api_keys SET revoked_at = ? WHERE id = ? AND workspace_id = ?")
487
948
  .run(now, keyId, workspaceId);
949
+ if (res.changes > 0) {
950
+ this.db.prepare("DELETE FROM oauth_refresh_tokens WHERE key_id = ?").run(keyId);
951
+ }
488
952
  return res.changes > 0;
489
953
  }
954
+ /** Looks up an active workspace API key by its raw secret. */
955
+ findActiveApiKey(secret) {
956
+ const row = this.db
957
+ .prepare("SELECT id, workspace_id FROM api_keys WHERE key_hash = ? AND revoked_at IS NULL")
958
+ .get(this.hashToken(secret.trim()));
959
+ return row ? { id: String(row.id), workspaceId: String(row.workspace_id) } : null;
960
+ }
961
+ // --- OAuth refresh tokens (hashed; the access token is kept encrypted so refresh can return it) ---
962
+ saveOAuthRefreshToken(params) {
963
+ this.db
964
+ .prepare(`INSERT INTO oauth_refresh_tokens (
965
+ token_hash, key_id, workspace_id, client_id, scope, encrypted_access_token, created_at
966
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`)
967
+ .run(this.hashToken(params.refreshToken), params.keyId, params.workspaceId, params.clientId, params.scope, this.encryptSecrets(params.workspaceId, { accessToken: params.accessToken }), new Date().toISOString());
968
+ }
969
+ getOAuthRefreshToken(refreshToken) {
970
+ const row = this.db
971
+ .prepare("SELECT * FROM oauth_refresh_tokens WHERE token_hash = ?")
972
+ .get(this.hashToken(refreshToken));
973
+ if (!row)
974
+ return null;
975
+ const workspaceId = String(row.workspace_id);
976
+ let accessToken = "";
977
+ try {
978
+ accessToken = this.decryptSecrets(workspaceId, String(row.encrypted_access_token)).accessToken || "";
979
+ }
980
+ catch {
981
+ return null;
982
+ }
983
+ return {
984
+ keyId: String(row.key_id),
985
+ workspaceId,
986
+ clientId: String(row.client_id),
987
+ scope: String(row.scope),
988
+ accessToken,
989
+ };
990
+ }
991
+ deleteOAuthRefreshToken(refreshToken) {
992
+ this.db.prepare("DELETE FROM oauth_refresh_tokens WHERE token_hash = ?").run(this.hashToken(refreshToken));
993
+ }
994
+ // --- Clerk sign-in ---
995
+ /**
996
+ * Maps a verified Clerk session to a local user. Only a verified email can link to an
997
+ * existing account; otherwise a fresh user and workspace are created so an unverified
998
+ * address can never take over someone else's account.
999
+ */
1000
+ provisionClerkUser(clerkUserId, identity, ownerWorkspaceId) {
1001
+ const mappedUser = this.getClerkLinkedUser(clerkUserId);
1002
+ if (mappedUser)
1003
+ return mappedUser;
1004
+ const email = identity.email ? identity.email.trim().toLowerCase() : null;
1005
+ const verifiedEmail = email && identity.emailVerified ? email : null;
1006
+ let user = verifiedEmail ? this.getUserByEmail(verifiedEmail) : null;
1007
+ if (!user) {
1008
+ const displayName = identity.name || (verifiedEmail ? verifiedEmail.split("@")[0] : "Muxo user");
1009
+ const placeholder = `${clerkUserId.toLowerCase()}@clerk.users.muxo.invalid`;
1010
+ user = this.registerUser({
1011
+ email: verifiedEmail || placeholder,
1012
+ name: displayName,
1013
+ password: crypto.randomBytes(24).toString("hex"),
1014
+ }).user;
1015
+ }
1016
+ if (ownerWorkspaceId && verifiedEmail && this.getWorkspace(ownerWorkspaceId)) {
1017
+ this.ensureWorkspaceMembership({ workspaceId: ownerWorkspaceId, userId: user.id, role: "owner" });
1018
+ this.db.prepare("UPDATE users SET workspace_id = ? WHERE id = ?").run(ownerWorkspaceId, user.id);
1019
+ user = this.getUser(user.id);
1020
+ }
1021
+ this.db
1022
+ .prepare("INSERT OR REPLACE INTO clerk_users (clerk_user_id, user_id, created_at) VALUES (?, ?, ?)")
1023
+ .run(clerkUserId, user.id, new Date().toISOString());
1024
+ return user;
1025
+ }
1026
+ getClerkLinkedUser(clerkUserId) {
1027
+ const mapped = this.db
1028
+ .prepare("SELECT user_id FROM clerk_users WHERE clerk_user_id = ?")
1029
+ .get(clerkUserId);
1030
+ return mapped ? this.getUser(mapped.user_id) : null;
1031
+ }
1032
+ getUserByEmail(email) {
1033
+ const row = this.db
1034
+ .prepare("SELECT * FROM users WHERE email = ?")
1035
+ .get(email.trim().toLowerCase());
1036
+ return row ? this.mapUser(row) : null;
1037
+ }
490
1038
  // --- Connections ---
491
1039
  upsertConnection(params) {
492
1040
  const now = new Date().toISOString();
@@ -510,7 +1058,23 @@ export class MuxoStore {
510
1058
  metadata_json = excluded.metadata_json,
511
1059
  updated_at = excluded.updated_at`)
512
1060
  .run(id, params.workspaceId, params.lane, params.providerSlug, params.providerName, params.status, params.externalAccountId ?? null, params.redirectUrl ?? null, JSON.stringify(params.metadata || {}), now, now);
513
- return this.getConnection(id);
1061
+ const connRecord = this.getConnection(id);
1062
+ this.upsertTemporalFact({
1063
+ workspaceId: params.workspaceId,
1064
+ subject: `app:${params.providerSlug}`,
1065
+ predicate: "CONNECTION_STATUS",
1066
+ object: `${params.lane}:${params.status}`,
1067
+ summary: `App ${params.providerName} (${params.providerSlug}) connection on ${params.lane} is ${params.status}`,
1068
+ confidence: 1.0,
1069
+ sourceKind: "resource_topology",
1070
+ sourceReferenceId: connRecord.id,
1071
+ metadata: {
1072
+ lane: params.lane,
1073
+ providerSlug: params.providerSlug,
1074
+ status: params.status,
1075
+ },
1076
+ });
1077
+ return connRecord;
514
1078
  }
515
1079
  getConnection(id) {
516
1080
  const row = this.db
@@ -560,14 +1124,46 @@ export class MuxoStore {
560
1124
  metadata_json = excluded.metadata_json,
561
1125
  updated_at = excluded.updated_at`)
562
1126
  .run(id, params.workspaceId, params.resourceName, params.providerSlug, params.providerName, params.serviceSlug, params.planSlug ?? null, params.status, JSON.stringify(envVarKeys), encryptedHex, params.externalResourceId ?? null, JSON.stringify(params.metadata || {}), now, now);
563
- return this.getResource(params.workspaceId, params.resourceName);
1127
+ const resRecord = this.getResource(params.workspaceId, params.resourceName);
1128
+ this.upsertTemporalFact({
1129
+ workspaceId: params.workspaceId,
1130
+ subject: `resource:${params.resourceName}`,
1131
+ predicate: "PROVISIONED_ON",
1132
+ object: `${params.serviceSlug}:${params.status}`,
1133
+ summary: `Resource "${params.resourceName}" provisioned on ${params.serviceSlug} (${params.providerName}) with status ${params.status} [${resRecord.envVarKeys.join(", ") || "no env keys"}]`,
1134
+ confidence: 1.0,
1135
+ sourceKind: "resource_topology",
1136
+ sourceReferenceId: resRecord.id,
1137
+ metadata: {
1138
+ providerSlug: params.providerSlug,
1139
+ serviceSlug: params.serviceSlug,
1140
+ planSlug: params.planSlug ?? null,
1141
+ status: params.status,
1142
+ envVarKeys: resRecord.envVarKeys,
1143
+ },
1144
+ });
1145
+ return resRecord;
564
1146
  }
565
1147
  getResource(workspaceId, resourceNameOrId) {
566
1148
  const row = this.db
567
- .prepare("SELECT * FROM resources WHERE workspace_id = ? AND (resource_name = ? OR id = ?)")
568
- .get(workspaceId, resourceNameOrId, resourceNameOrId);
1149
+ .prepare("SELECT * FROM resources WHERE workspace_id = ? AND (resource_name = ? OR id = ? OR external_resource_id = ?)")
1150
+ .get(workspaceId, resourceNameOrId, resourceNameOrId, resourceNameOrId);
569
1151
  return row ? this.mapResource(row) : null;
570
1152
  }
1153
+ findResourceAnyWorkspace(resourceNameOrId) {
1154
+ const row = this.db
1155
+ .prepare("SELECT * FROM resources WHERE id = ? OR external_resource_id = ? OR resource_name = ? LIMIT 1")
1156
+ .get(resourceNameOrId, resourceNameOrId, resourceNameOrId);
1157
+ return row ? this.mapResource(row) : null;
1158
+ }
1159
+ getDecryptedResourceSecrets(workspaceId, resourceNameOrId) {
1160
+ const row = this.db
1161
+ .prepare("SELECT encrypted_secrets_hex FROM resources WHERE workspace_id = ? AND (id = ? OR external_resource_id = ? OR resource_name = ?) LIMIT 1")
1162
+ .get(workspaceId, resourceNameOrId, resourceNameOrId, resourceNameOrId);
1163
+ if (!row)
1164
+ return {};
1165
+ return this.decryptSecrets(workspaceId, row.encrypted_secrets_hex);
1166
+ }
571
1167
  listResources(workspaceId) {
572
1168
  const rows = this.db
573
1169
  .prepare("SELECT * FROM resources WHERE workspace_id = ? ORDER BY updated_at DESC")
@@ -742,6 +1338,12 @@ export class MuxoStore {
742
1338
  .get(planId, workspaceId);
743
1339
  return row ? this.mapPlan(row) : null;
744
1340
  }
1341
+ getPlanById(planId) {
1342
+ const row = this.db
1343
+ .prepare("SELECT * FROM plans WHERE id = ?")
1344
+ .get(planId);
1345
+ return row ? this.mapPlan(row) : null;
1346
+ }
745
1347
  listPlans(workspaceId, limit = 50) {
746
1348
  const rows = this.db
747
1349
  .prepare("SELECT * FROM plans WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?")
@@ -787,7 +1389,9 @@ export class MuxoStore {
787
1389
  SET monthly_actions_used = ?, credits_balance = ?, updated_at = ?
788
1390
  WHERE id = ?`)
789
1391
  .run(newActionsUsed, newCreditsBalance, now, params.workspaceId);
790
- const settlement = this.computeSupplierSettlement(run.lane, run.toolSlug, params.creditsMetered, "settled_by_muxo_master_wallet");
1392
+ const byokUsed = params.creditsMetered === 0 &&
1393
+ this.hasByokForLane(params.workspaceId, run.lane, run.toolSlug);
1394
+ const settlement = this.computeSupplierSettlement(run.lane, run.toolSlug, params.creditsMetered, byokUsed ? "settled_by_customer_byok" : "settled_by_muxo_master_wallet", params.workspaceId);
791
1395
  this.addLedgerEntry({
792
1396
  workspaceId: params.workspaceId,
793
1397
  kind: "run_deduction",
@@ -795,7 +1399,9 @@ export class MuxoStore {
795
1399
  creditsDelta: -params.creditsMetered,
796
1400
  balanceAfter: newCreditsBalance,
797
1401
  referenceId: run.id,
798
- description: `Executed ${run.toolSlug} (${run.lane}) — User paid Muxo ${params.creditsMetered} credits ($${(settlement.userRetailChargeCents / 100).toFixed(2)}) -> Muxo paid ${settlement.supplierName} ($${(settlement.supplierWholesalePayableCents / 100).toFixed(2)})`,
1402
+ description: byokUsed
1403
+ ? `Executed ${run.toolSlug} (${run.lane}) — BYOK 0-credit execution (Customer's own ${settlement.supplierName} key)`
1404
+ : `Executed ${run.toolSlug} (${run.lane}) — User paid Muxo ${params.creditsMetered} credits ($${(settlement.userRetailChargeCents / 100).toFixed(2)}) -> Muxo paid ${settlement.supplierName} ($${(settlement.supplierWholesalePayableCents / 100).toFixed(2)})`,
799
1405
  });
800
1406
  }
801
1407
  const receipt = this.issueReceipt({
@@ -818,10 +1424,41 @@ export class MuxoStore {
818
1424
  receipt,
819
1425
  };
820
1426
  }
1427
+ cancelRun(params) {
1428
+ const run = this.getRun(params.workspaceId, params.runId);
1429
+ if (!run)
1430
+ throw new Error(`Run ${params.runId} not found`);
1431
+ const now = new Date().toISOString();
1432
+ const cancellation = {
1433
+ cancelled: true,
1434
+ cancelledBy: params.cancelledBy,
1435
+ cancelledAt: now,
1436
+ previousStatus: run.status,
1437
+ supersedesReceiptId: run.receiptId,
1438
+ ...(params.reason ? { reason: params.reason } : {}),
1439
+ };
1440
+ const receipt = this.issueReceipt({
1441
+ run,
1442
+ status: "cancelled",
1443
+ output: cancellation,
1444
+ actionsMetered: 0,
1445
+ creditsMetered: 0,
1446
+ decisionRecord: null,
1447
+ issuedAt: now,
1448
+ });
1449
+ this.db
1450
+ .prepare(`UPDATE runs
1451
+ SET status = 'cancelled', output_json = COALESCE(output_json, ?), error = ?,
1452
+ receipt_id = ?, completed_at = COALESCE(completed_at, ?),
1453
+ duration_ms = COALESCE(duration_ms, ?)
1454
+ WHERE id = ? AND workspace_id = ?`)
1455
+ .run(JSON.stringify(cancellation), params.reason || "Cancelled by operator", receipt.id, now, Math.max(0, Date.now() - new Date(run.startedAt).getTime()), run.id, params.workspaceId);
1456
+ return { run: this.getRun(params.workspaceId, run.id), receipt };
1457
+ }
821
1458
  issueReceipt(params) {
822
1459
  const id = `rcpt_${crypto.randomBytes(8).toString("hex")}`;
823
1460
  const lastReceipt = this.db
824
- .prepare("SELECT receipt_hash FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC LIMIT 1")
1461
+ .prepare("SELECT receipt_hash FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC, rowid DESC LIMIT 1")
825
1462
  .get(params.run.workspaceId);
826
1463
  const prevReceiptHash = lastReceipt?.receipt_hash || "genesis_0000000000000000";
827
1464
  const inputHash = crypto
@@ -903,14 +1540,17 @@ export class MuxoStore {
903
1540
  return rows.map((r) => this.mapRun(r));
904
1541
  }
905
1542
  getReceipt(workspaceId, receiptIdOrRunId) {
1543
+ // A cancelled run has both its original and its cancellation receipt; prefer an exact
1544
+ // receipt id match, otherwise the newest receipt for the run.
906
1545
  const row = this.db
907
- .prepare("SELECT * FROM receipts WHERE workspace_id = ? AND (id = ? OR run_id = ?)")
908
- .get(workspaceId, receiptIdOrRunId, receiptIdOrRunId);
1546
+ .prepare(`SELECT * FROM receipts WHERE workspace_id = ? AND (id = ? OR run_id = ?)
1547
+ ORDER BY CASE WHEN id = ? THEN 0 ELSE 1 END, issued_at DESC, rowid DESC LIMIT 1`)
1548
+ .get(workspaceId, receiptIdOrRunId, receiptIdOrRunId, receiptIdOrRunId);
909
1549
  return row ? this.mapReceipt(row) : null;
910
1550
  }
911
1551
  listReceipts(workspaceId, limit = 50) {
912
1552
  const rows = this.db
913
- .prepare("SELECT * FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC LIMIT ?")
1553
+ .prepare("SELECT * FROM receipts WHERE workspace_id = ? ORDER BY issued_at DESC, rowid DESC LIMIT ?")
914
1554
  .all(workspaceId, limit);
915
1555
  return rows.map((r) => this.mapReceipt(r));
916
1556
  }
@@ -1004,7 +1644,9 @@ export class MuxoStore {
1004
1644
  computeSupplierSettlement(lane, toolSlug, creditCost, settlementStatus = "settled_by_muxo_master_wallet", workspaceId) {
1005
1645
  const useByok = settlementStatus === "settled_by_customer_byok" ||
1006
1646
  settlementStatus === "planned_customer_byok" ||
1007
- (workspaceId ? this.hasByokForLane(workspaceId, lane, toolSlug) : false);
1647
+ (Number(creditCost || 0) === 0 && workspaceId
1648
+ ? this.hasByokForLane(workspaceId, lane, toolSlug)
1649
+ : false);
1008
1650
  const upstreamProvider = toolSlug.split(".")[1] || lane;
1009
1651
  if (useByok) {
1010
1652
  const isPlanned = settlementStatus === "planned_muxo_master_wallet" ||
@@ -1127,6 +1769,17 @@ export class MuxoStore {
1127
1769
  totalSupplierWholesalePaidCents,
1128
1770
  totalMuxoNetMarginCents,
1129
1771
  bySupplier,
1772
+ masterWalletProtection: {
1773
+ status: process.env.MUXO_MASTER_WALLET_DEPLETED === "true"
1774
+ ? "failover_active"
1775
+ : "healthy",
1776
+ zeroChargeOnFailure: true,
1777
+ instantFailoverMode: "zero_cost_native_edge",
1778
+ paidProvisioningPolicy: "muxo_master_wallet_or_customer_stripe_byok",
1779
+ retailToWholesaleCoverageRatio: totalSupplierWholesalePaidCents > 0
1780
+ ? Math.round((totalUserRetailSpendCents / totalSupplierWholesalePaidCents) * 100) / 100
1781
+ : 1.67,
1782
+ },
1130
1783
  };
1131
1784
  }
1132
1785
  listLedgerEntries(workspaceId, limit = 50) {
@@ -1141,7 +1794,12 @@ export class MuxoStore {
1141
1794
  if (kind === "run_deduction" && refId) {
1142
1795
  const run = this.getRun(workspaceId, refId);
1143
1796
  if (run) {
1144
- supplierSettlement = this.computeSupplierSettlement(run.lane, run.toolSlug, Math.abs(creditsDelta), "settled_by_muxo_master_wallet");
1797
+ const isByokEntry = creditsDelta === 0 &&
1798
+ (String(r.description || "").includes("BYOK") ||
1799
+ this.hasByokForLane(workspaceId, run.lane, run.toolSlug));
1800
+ supplierSettlement = this.computeSupplierSettlement(run.lane, run.toolSlug, Math.abs(creditsDelta), isByokEntry
1801
+ ? "settled_by_customer_byok"
1802
+ : "settled_by_muxo_master_wallet", workspaceId);
1145
1803
  }
1146
1804
  }
1147
1805
  return {
@@ -1204,6 +1862,15 @@ export class MuxoStore {
1204
1862
  this.db
1205
1863
  .prepare("DELETE FROM memories WHERE id = ? AND workspace_id = ?")
1206
1864
  .run(String(row.id), workspaceId);
1865
+ // Also mark any temporal facts sourced from this memory record as invalidated (valid_to = now)
1866
+ const now = new Date().toISOString();
1867
+ this.db
1868
+ .prepare(`UPDATE temporal_facts
1869
+ SET valid_to = ?
1870
+ WHERE workspace_id = ?
1871
+ AND valid_to IS NULL
1872
+ AND (source_reference_id = ? OR source_reference_id = ? OR id = ?)`)
1873
+ .run(now, workspaceId, String(row.id), String(row.document_id), idOrDocId);
1207
1874
  return {
1208
1875
  id: String(row.id),
1209
1876
  workspaceId: String(row.workspace_id),
@@ -1216,6 +1883,166 @@ export class MuxoStore {
1216
1883
  createdAt: String(row.created_at),
1217
1884
  };
1218
1885
  }
1886
+ // --- Bi-Temporal Resource & Fact Lineage (Zero-Dependency Graphiti Alternative) ---
1887
+ upsertTemporalFact(params) {
1888
+ const now = new Date().toISOString();
1889
+ const subject = params.subject.trim();
1890
+ const predicate = params.predicate.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
1891
+ const object = params.object.trim();
1892
+ const confidence = params.confidence !== undefined ? Number(params.confidence) : 0.95;
1893
+ const sourceKind = params.sourceKind || "fact";
1894
+ // Check if an active fact already exists for (workspace_id, subject, predicate)
1895
+ const activeRow = this.db
1896
+ .prepare(`SELECT * FROM temporal_facts
1897
+ WHERE workspace_id = ? AND subject = ? AND predicate = ? AND valid_to IS NULL
1898
+ ORDER BY created_at DESC LIMIT 1`)
1899
+ .get(params.workspaceId, subject, predicate);
1900
+ if (activeRow && String(activeRow.object) === object) {
1901
+ // Same object is still valid — update summary/confidence/metadata in place
1902
+ const existingId = String(activeRow.id);
1903
+ this.db
1904
+ .prepare(`UPDATE temporal_facts
1905
+ SET summary = ?, confidence = ?, metadata_json = ?, source_reference_id = COALESCE(?, source_reference_id)
1906
+ WHERE id = ?`)
1907
+ .run(params.summary, confidence, JSON.stringify(params.metadata || {}), params.sourceReferenceId ?? null, existingId);
1908
+ return this.mapTemporalFact({
1909
+ ...activeRow,
1910
+ summary: params.summary,
1911
+ confidence,
1912
+ metadata_json: JSON.stringify(params.metadata || {}),
1913
+ source_reference_id: params.sourceReferenceId ?? activeRow.source_reference_id,
1914
+ });
1915
+ }
1916
+ const newId = `tf_${crypto.randomBytes(8).toString("hex")}`;
1917
+ if (activeRow && params.supersedeExisting !== false) {
1918
+ this.db
1919
+ .prepare(`UPDATE temporal_facts
1920
+ SET valid_to = ?, superseded_by_id = ?
1921
+ WHERE id = ? AND workspace_id = ?`)
1922
+ .run(now, newId, String(activeRow.id), params.workspaceId);
1923
+ }
1924
+ this.db
1925
+ .prepare(`INSERT INTO temporal_facts (
1926
+ id, workspace_id, subject, predicate, object, summary, confidence,
1927
+ valid_from, valid_to, superseded_by_id, source_kind, source_reference_id,
1928
+ metadata_json, created_at
1929
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?)`)
1930
+ .run(newId, params.workspaceId, subject, predicate, object, params.summary, confidence, now, sourceKind, params.sourceReferenceId ?? null, JSON.stringify(params.metadata || {}), now);
1931
+ return {
1932
+ id: newId,
1933
+ workspaceId: params.workspaceId,
1934
+ subject,
1935
+ predicate,
1936
+ object,
1937
+ summary: params.summary,
1938
+ confidence,
1939
+ validFrom: now,
1940
+ validTo: null,
1941
+ supersededById: null,
1942
+ sourceKind,
1943
+ sourceReferenceId: params.sourceReferenceId ?? null,
1944
+ metadata: params.metadata || {},
1945
+ createdAt: now,
1946
+ };
1947
+ }
1948
+ listTemporalFacts(workspaceId, opts) {
1949
+ const limit = Math.min(200, Math.max(1, Number(opts?.limit || 50)));
1950
+ const includeHistorical = Boolean(opts?.includeHistorical);
1951
+ const rows = this.db
1952
+ .prepare(includeHistorical
1953
+ ? `SELECT * FROM temporal_facts WHERE workspace_id = ? ORDER BY created_at DESC LIMIT ?`
1954
+ : `SELECT * FROM temporal_facts WHERE workspace_id = ? AND valid_to IS NULL ORDER BY created_at DESC LIMIT ?`)
1955
+ .all(workspaceId, limit);
1956
+ let facts = rows.map((r) => this.mapTemporalFact(r));
1957
+ if (opts?.subject) {
1958
+ const subj = opts.subject.toLowerCase();
1959
+ facts = facts.filter((f) => f.subject.toLowerCase().includes(subj));
1960
+ }
1961
+ if (opts?.predicate) {
1962
+ const pred = opts.predicate.toUpperCase();
1963
+ facts = facts.filter((f) => f.predicate.toUpperCase().includes(pred));
1964
+ }
1965
+ if (opts?.query && opts.query.trim()) {
1966
+ const tokens = opts.query
1967
+ .toLowerCase()
1968
+ .split(/\s+/)
1969
+ .filter((t) => t.length >= 2);
1970
+ if (tokens.length > 0) {
1971
+ facts = facts.filter((f) => {
1972
+ const hay = `${f.subject} ${f.predicate} ${f.object} ${f.summary}`.toLowerCase();
1973
+ return tokens.some((tok) => hay.includes(tok));
1974
+ });
1975
+ }
1976
+ }
1977
+ return facts;
1978
+ }
1979
+ forgetMatchingMemoryRecords(workspaceId, query) {
1980
+ const trimmed = query.trim().toLowerCase();
1981
+ if (!trimmed) {
1982
+ return { deletedMemories: [], invalidatedFacts: [] };
1983
+ }
1984
+ const tokens = trimmed.split(/\s+/).filter((t) => t.length >= 2);
1985
+ const allMemories = this.listMemoryRecords(workspaceId, 200);
1986
+ const matchingMemories = allMemories.filter((m) => {
1987
+ const hay = `${m.title} ${m.content} ${m.kind}`.toLowerCase();
1988
+ return (hay.includes(trimmed) ||
1989
+ (tokens.length > 0 && tokens.every((tok) => hay.includes(tok))));
1990
+ });
1991
+ // Snapshot before deleting: deleteMemoryRecord retires sourced facts itself,
1992
+ // and those must still be reported as invalidated.
1993
+ const activeFacts = this.listTemporalFacts(workspaceId, {
1994
+ includeHistorical: false,
1995
+ limit: 200,
1996
+ });
1997
+ const deletedMemories = [];
1998
+ for (const mem of matchingMemories) {
1999
+ const deleted = this.deleteMemoryRecord(workspaceId, mem.id);
2000
+ if (deleted)
2001
+ deletedMemories.push(deleted);
2002
+ }
2003
+ const deletedSourceIds = new Set(deletedMemories.flatMap((m) => [m.id, m.documentId]));
2004
+ const matchingFacts = activeFacts.filter((f) => {
2005
+ const hay = `${f.subject} ${f.predicate} ${f.object} ${f.summary}`.toLowerCase();
2006
+ return (hay.includes(trimmed) ||
2007
+ (tokens.length > 0 && tokens.every((tok) => hay.includes(tok))) ||
2008
+ Boolean(f.sourceReferenceId && deletedSourceIds.has(f.sourceReferenceId)));
2009
+ });
2010
+ const now = new Date().toISOString();
2011
+ const invalidatedFacts = [];
2012
+ for (const fact of matchingFacts) {
2013
+ this.db
2014
+ .prepare(`UPDATE temporal_facts SET valid_to = ? WHERE id = ? AND workspace_id = ?`)
2015
+ .run(now, fact.id, workspaceId);
2016
+ invalidatedFacts.push({
2017
+ ...fact,
2018
+ validTo: now,
2019
+ });
2020
+ }
2021
+ return {
2022
+ deletedMemories,
2023
+ invalidatedFacts,
2024
+ };
2025
+ }
2026
+ mapTemporalFact(row) {
2027
+ return {
2028
+ id: String(row.id),
2029
+ workspaceId: String(row.workspace_id),
2030
+ subject: String(row.subject),
2031
+ predicate: String(row.predicate),
2032
+ object: String(row.object),
2033
+ summary: String(row.summary),
2034
+ confidence: Number(row.confidence ?? 0.95),
2035
+ validFrom: String(row.valid_from),
2036
+ validTo: row.valid_to ? String(row.valid_to) : null,
2037
+ supersededById: row.superseded_by_id ? String(row.superseded_by_id) : null,
2038
+ sourceKind: String(row.source_kind || "fact"),
2039
+ sourceReferenceId: row.source_reference_id
2040
+ ? String(row.source_reference_id)
2041
+ : null,
2042
+ metadata: JSON.parse(String(row.metadata_json || "{}")),
2043
+ createdAt: String(row.created_at),
2044
+ };
2045
+ }
1219
2046
  // --- Autonomous Agent Onboarding (agents.composio.dev pattern) ---
1220
2047
  createAutonomousAgentSignup(params) {
1221
2048
  const suffix = crypto.randomBytes(4).toString("hex");
@@ -1259,46 +2086,92 @@ export class MuxoStore {
1259
2086
  expiresAt: String(row.expires_at),
1260
2087
  };
1261
2088
  }
2089
+ /**
2090
+ * Creates a claim link for a workspace. A claim minted without an email (at agent signup)
2091
+ * stays unusable until the agent names the owner; naming one reuses that same link.
2092
+ */
1262
2093
  createAgentClaim(params) {
1263
- const claimSlug = crypto.randomBytes(9).toString("base64url");
2094
+ const targetEmail = (params.email || "").trim().toLowerCase();
1264
2095
  const now = new Date();
1265
2096
  const expires = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
1266
- this.db
1267
- .prepare(`INSERT INTO agent_claims (
1268
- claim_slug, workspace_id, target_email, status, claimed_at, expires_at, created_at
1269
- ) VALUES (?, ?, ?, 'invited', NULL, ?, ?)`)
1270
- .run(claimSlug, params.workspaceId, params.email.trim().toLowerCase(), expires.toISOString(), now.toISOString());
2097
+ const unbound = targetEmail
2098
+ ? this.db
2099
+ .prepare(`SELECT claim_slug FROM agent_claims
2100
+ WHERE workspace_id = ? AND status = 'invited' AND target_email = ''
2101
+ ORDER BY created_at DESC LIMIT 1`)
2102
+ .get(params.workspaceId)
2103
+ : undefined;
2104
+ const claimSlug = unbound?.claim_slug || crypto.randomBytes(24).toString("base64url");
2105
+ if (unbound) {
2106
+ this.db
2107
+ .prepare("UPDATE agent_claims SET target_email = ?, expires_at = ? WHERE claim_slug = ?")
2108
+ .run(targetEmail, expires.toISOString(), claimSlug);
2109
+ }
2110
+ else {
2111
+ this.db
2112
+ .prepare(`INSERT INTO agent_claims (
2113
+ claim_slug, workspace_id, target_email, status, claimed_at, expires_at, created_at
2114
+ ) VALUES (?, ?, ?, 'invited', NULL, ?, ?)`)
2115
+ .run(claimSlug, params.workspaceId, targetEmail, expires.toISOString(), now.toISOString());
2116
+ }
1271
2117
  return {
1272
2118
  claimSlug,
1273
2119
  claimUrl: `${params.baseUrl.replace(/\/$/, "")}/claim/${claimSlug}`,
1274
- targetEmail: params.email.trim().toLowerCase(),
2120
+ targetEmail,
1275
2121
  status: "invited",
1276
2122
  expiresAt: expires.toISOString(),
1277
2123
  };
1278
2124
  }
1279
- redeemAgentClaim(claimSlug) {
2125
+ /** Returns a claim only while it can still be accepted (invited and not expired). */
2126
+ getOpenAgentClaim(claimSlug) {
1280
2127
  const row = this.db
1281
2128
  .prepare("SELECT * FROM agent_claims WHERE claim_slug = ?")
1282
2129
  .get(claimSlug);
1283
- if (!row)
2130
+ if (!row || String(row.status) !== "invited" || !row.target_email)
1284
2131
  return null;
1285
- const now = new Date().toISOString();
1286
- const workspaceId = String(row.workspace_id);
1287
- const targetEmail = String(row.target_email);
1288
- this.db
1289
- .prepare("UPDATE agent_claims SET status = 'claimed', claimed_at = ? WHERE claim_slug = ?")
1290
- .run(now, claimSlug);
1291
- const workspace = this.getWorkspace(workspaceId);
2132
+ if (Date.parse(String(row.expires_at)) <= Date.now())
2133
+ return null;
2134
+ const workspace = this.getWorkspace(String(row.workspace_id));
1292
2135
  if (!workspace)
1293
2136
  return null;
1294
2137
  return {
1295
2138
  claimSlug,
1296
2139
  workspace,
1297
- targetEmail,
1298
- status: "claimed",
1299
- claimedAt: now,
2140
+ targetEmail: String(row.target_email),
2141
+ status: "invited",
2142
+ expiresAt: String(row.expires_at),
1300
2143
  };
1301
2144
  }
2145
+ /** Transfers workspace ownership to the verified claimer and closes the claim. */
2146
+ acceptAgentClaim(claimSlug, userId) {
2147
+ const claim = this.getOpenAgentClaim(claimSlug);
2148
+ if (!claim || !this.getUser(userId))
2149
+ return null;
2150
+ const now = new Date().toISOString();
2151
+ this.db.exec("BEGIN");
2152
+ try {
2153
+ const closed = this.db
2154
+ .prepare("UPDATE agent_claims SET status = 'claimed', claimed_at = ? WHERE claim_slug = ? AND status = 'invited'")
2155
+ .run(now, claimSlug);
2156
+ if (closed.changes === 0) {
2157
+ this.db.exec("ROLLBACK");
2158
+ return null;
2159
+ }
2160
+ this.db
2161
+ .prepare("UPDATE workspaces SET owner_user_id = ?, updated_at = ? WHERE id = ?")
2162
+ .run(userId, now, claim.workspace.id);
2163
+ this.db
2164
+ .prepare("UPDATE users SET workspace_id = ?, role = 'owner' WHERE id = ?")
2165
+ .run(claim.workspace.id, userId);
2166
+ this.ensureWorkspaceMembership({ workspaceId: claim.workspace.id, userId, role: "owner" });
2167
+ this.db.exec("COMMIT");
2168
+ }
2169
+ catch (err) {
2170
+ this.db.exec("ROLLBACK");
2171
+ throw err;
2172
+ }
2173
+ return this.getWorkspace(claim.workspace.id);
2174
+ }
1302
2175
  // --- Row Mappers ---
1303
2176
  mapUser(row) {
1304
2177
  return {
@@ -1410,6 +2283,7 @@ export class MuxoStore {
1410
2283
  input: JSON.parse(String(row.input_json || "{}")),
1411
2284
  output: row.output_json ? JSON.parse(String(row.output_json)) : null,
1412
2285
  error: row.error ? String(row.error) : null,
2286
+ errorMessage: row.error ? String(row.error) : null,
1413
2287
  actionsMetered: Number(row.actions_metered),
1414
2288
  creditsMetered: Number(row.credits_metered),
1415
2289
  durationMs: row.duration_ms !== null ? Number(row.duration_ms) : null,