agent-relay-server 0.76.0 → 0.77.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 (135) hide show
  1. package/docs/openapi.json +94 -1
  2. package/package.json +2 -2
  3. package/public/assets/MessageBubble-Bkzw574C.js +3 -0
  4. package/public/assets/{MessageBubble-DzhG1x8z.js.map → MessageBubble-Bkzw574C.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-brNJanGs.js → PlanGraphPanel-DaLJ5fKn.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-brNJanGs.js.map → PlanGraphPanel-DaLJ5fKn.js.map} +1 -1
  7. package/public/assets/{activity-DIXJ-CVu.js → activity-BLFdfuxj.js} +2 -2
  8. package/public/assets/{activity-DIXJ-CVu.js.map → activity-BLFdfuxj.js.map} +1 -1
  9. package/public/assets/{agent-profiles-D1X5BBW_.js → agent-profiles-DwR6zuEJ.js} +2 -2
  10. package/public/assets/{agent-profiles-D1X5BBW_.js.map → agent-profiles-DwR6zuEJ.js.map} +1 -1
  11. package/public/assets/{agents-BG9XZcVD.js → agents-CEnKjxMD.js} +2 -2
  12. package/public/assets/{agents-BG9XZcVD.js.map → agents-CEnKjxMD.js.map} +1 -1
  13. package/public/assets/{analytics-e_HOZBPx.js → analytics-BTq4XQiF.js} +3 -3
  14. package/public/assets/{analytics-e_HOZBPx.js.map → analytics-BTq4XQiF.js.map} +1 -1
  15. package/public/assets/{automation-jVNnsYV7.js → automation-Crxi1JGO.js} +2 -2
  16. package/public/assets/{automation-jVNnsYV7.js.map → automation-Crxi1JGO.js.map} +1 -1
  17. package/public/assets/{badge-YQhT30dy.js → badge-BxG6_yfp.js} +2 -2
  18. package/public/assets/{badge-YQhT30dy.js.map → badge-BxG6_yfp.js.map} +1 -1
  19. package/public/assets/branch-state-badge-DaCW_qGP.js +2 -0
  20. package/public/assets/{branch-state-badge-B7T81Oct.js.map → branch-state-badge-DaCW_qGP.js.map} +1 -1
  21. package/public/assets/{button-CaFI76Ja.js → button-D5k3GbF6.js} +2 -2
  22. package/public/assets/{button-CaFI76Ja.js.map → button-D5k3GbF6.js.map} +1 -1
  23. package/public/assets/{card-B_fQtKce.js → card-DKyFtrez.js} +2 -2
  24. package/public/assets/{card-B_fQtKce.js.map → card-DKyFtrez.js.map} +1 -1
  25. package/public/assets/{channels-BAD_kIo7.js → channels-EHZtUB_U.js} +2 -2
  26. package/public/assets/{channels-BAD_kIo7.js.map → channels-EHZtUB_U.js.map} +1 -1
  27. package/public/assets/chat-CWmxVsVA.js +2 -0
  28. package/public/assets/{chat-B7EMdwIg.js.map → chat-CWmxVsVA.js.map} +1 -1
  29. package/public/assets/{connectors-Cdjtxly2.js → connectors-B3BbETXb.js} +2 -2
  30. package/public/assets/{connectors-Cdjtxly2.js.map → connectors-B3BbETXb.js.map} +1 -1
  31. package/public/assets/copy-button-D3XAkVC2.js +2 -0
  32. package/public/assets/{copy-button-m00JmflM.js.map → copy-button-D3XAkVC2.js.map} +1 -1
  33. package/public/assets/dist-B9_RgXN9.js +2 -0
  34. package/public/assets/{dist-DfQP9-VC.js.map → dist-B9_RgXN9.js.map} +1 -1
  35. package/public/assets/dist-BuYGwGZ-.js +2 -0
  36. package/public/assets/{dist-CydxJz3m.js.map → dist-BuYGwGZ-.js.map} +1 -1
  37. package/public/assets/{dist-ChP4KvKi.js → dist-CAu7-DrN.js} +2 -2
  38. package/public/assets/{dist-ChP4KvKi.js.map → dist-CAu7-DrN.js.map} +1 -1
  39. package/public/assets/{dist-DeAdFM5_.js → dist-CF2meFCY.js} +2 -2
  40. package/public/assets/{dist-DeAdFM5_.js.map → dist-CF2meFCY.js.map} +1 -1
  41. package/public/assets/{dist-B_yrHbsb.js → dist-CR7zKC1F.js} +2 -2
  42. package/public/assets/{dist-B_yrHbsb.js.map → dist-CR7zKC1F.js.map} +1 -1
  43. package/public/assets/{dist-C-viWls-.js → dist-CUxt3gHd.js} +2 -2
  44. package/public/assets/{dist-C-viWls-.js.map → dist-CUxt3gHd.js.map} +1 -1
  45. package/public/assets/{dist-D8Yr1AIJ.js → dist-D1Bwx4cc.js} +2 -2
  46. package/public/assets/{dist-D8Yr1AIJ.js.map → dist-D1Bwx4cc.js.map} +1 -1
  47. package/public/assets/{dist-Cnlh_NpI.js → dist-D32lF-j6.js} +2 -2
  48. package/public/assets/{dist-Cnlh_NpI.js.map → dist-D32lF-j6.js.map} +1 -1
  49. package/public/assets/{dist-DeM72J6a.js → dist-DDrB3y1I.js} +2 -2
  50. package/public/assets/{dist-DeM72J6a.js.map → dist-DDrB3y1I.js.map} +1 -1
  51. package/public/assets/dist-DPQKhU73.js +2 -0
  52. package/public/assets/{dist-CYS2puT9.js.map → dist-DPQKhU73.js.map} +1 -1
  53. package/public/assets/{dist-D7K3qgp7.js → dist-YjHj0NpU.js} +2 -2
  54. package/public/assets/{dist-D7K3qgp7.js.map → dist-YjHj0NpU.js.map} +1 -1
  55. package/public/assets/{es2015-C3DnXBqu.js → es2015-WF4MRcZQ.js} +2 -2
  56. package/public/assets/{es2015-C3DnXBqu.js.map → es2015-WF4MRcZQ.js.map} +1 -1
  57. package/public/assets/{formatted-body-impl-DBWLuF0p.js → formatted-body-impl-CCA5xER4.js} +2 -2
  58. package/public/assets/{formatted-body-impl-DBWLuF0p.js.map → formatted-body-impl-CCA5xER4.js.map} +1 -1
  59. package/public/assets/index-Du_Njun0.js +22 -0
  60. package/public/assets/index-Du_Njun0.js.map +1 -0
  61. package/public/assets/index-DvNy9IVg.css +2 -0
  62. package/public/assets/{input-Cqqq0OFl.js → input-DSJQbsdS.js} +2 -2
  63. package/public/assets/{input-Cqqq0OFl.js.map → input-DSJQbsdS.js.map} +1 -1
  64. package/public/assets/{insights-CzGE5EyU.js → insights-Dy4k6fpi.js} +2 -2
  65. package/public/assets/{insights-CzGE5EyU.js.map → insights-Dy4k6fpi.js.map} +1 -1
  66. package/public/assets/{integrations-DzRla8mj.js → integrations-BWBxWl3k.js} +2 -2
  67. package/public/assets/{integrations-DzRla8mj.js.map → integrations-BWBxWl3k.js.map} +1 -1
  68. package/public/assets/{lib-6L4m0E3M.js → lib-BxQSXiQ7.js} +3 -3
  69. package/public/assets/{lib-6L4m0E3M.js.map → lib-BxQSXiQ7.js.map} +1 -1
  70. package/public/assets/{lucide-react-DI8kTavp.js → lucide-react-BH2ROV8n.js} +2 -2
  71. package/public/assets/{lucide-react-DI8kTavp.js.map → lucide-react-BH2ROV8n.js.map} +1 -1
  72. package/public/assets/{maintenance-CGWGOhDP.js → maintenance-Bgaxr-3J.js} +2 -2
  73. package/public/assets/{maintenance-CGWGOhDP.js.map → maintenance-Bgaxr-3J.js.map} +1 -1
  74. package/public/assets/{managed-agents-DyYn12ir.js → managed-agents-DKD0Ovct.js} +2 -2
  75. package/public/assets/{managed-agents-DyYn12ir.js.map → managed-agents-DKD0Ovct.js.map} +1 -1
  76. package/public/assets/{markdown-preview-impl-BA37v8OC.js → markdown-preview-impl-OM7F_OtZ.js} +2 -2
  77. package/public/assets/{markdown-preview-impl-BA37v8OC.js.map → markdown-preview-impl-OM7F_OtZ.js.map} +1 -1
  78. package/public/assets/{memory-CoIM-761.js → memory-BSvQoXx6.js} +2 -2
  79. package/public/assets/{memory-CoIM-761.js.map → memory-BSvQoXx6.js.map} +1 -1
  80. package/public/assets/{messages-D-TOb6_t.js → messages-D-WqWrGU.js} +2 -2
  81. package/public/assets/{messages-D-TOb6_t.js.map → messages-D-WqWrGU.js.map} +1 -1
  82. package/public/assets/{orchestrators-XLGUAnKs.js → orchestrators-DZrmTmOK.js} +2 -2
  83. package/public/assets/{orchestrators-XLGUAnKs.js.map → orchestrators-DZrmTmOK.js.map} +1 -1
  84. package/public/assets/{overview-C4Netp8S.js → overview-C8Q92ay4.js} +2 -2
  85. package/public/assets/{overview-C4Netp8S.js.map → overview-C8Q92ay4.js.map} +1 -1
  86. package/public/assets/{pairs-1X219PK6.js → pairs-B_3Q2kaN.js} +2 -2
  87. package/public/assets/{pairs-1X219PK6.js.map → pairs-B_3Q2kaN.js.map} +1 -1
  88. package/public/assets/{projects-DluLrkwe.js → projects-DIN_O7AJ.js} +2 -2
  89. package/public/assets/{projects-DluLrkwe.js.map → projects-DIN_O7AJ.js.map} +1 -1
  90. package/public/assets/{react-dom-Ct8qXivQ.js → react-dom-B_tGtv3Y.js} +2 -2
  91. package/public/assets/{react-dom-Ct8qXivQ.js.map → react-dom-B_tGtv3Y.js.map} +1 -1
  92. package/public/assets/{security-CCfKm-ow.js → security-CUiGx0a5.js} +2 -2
  93. package/public/assets/{security-CCfKm-ow.js.map → security-CUiGx0a5.js.map} +1 -1
  94. package/public/assets/{select-BpPDknSO.js → select-Bh9XLp7J.js} +2 -2
  95. package/public/assets/{select-BpPDknSO.js.map → select-Bh9XLp7J.js.map} +1 -1
  96. package/public/assets/settings-Dox7P4g0.js +4 -0
  97. package/public/assets/{settings-BUD05ZM3.js.map → settings-Dox7P4g0.js.map} +1 -1
  98. package/public/assets/{store-D1K4_xId.js → store-m74m5eNF.js} +3 -3
  99. package/public/assets/{store-D1K4_xId.js.map → store-m74m5eNF.js.map} +1 -1
  100. package/public/assets/{tasks-Dvqe1bbe.js → tasks-BIP2A8PU.js} +2 -2
  101. package/public/assets/{tasks-Dvqe1bbe.js.map → tasks-BIP2A8PU.js.map} +1 -1
  102. package/public/assets/{teams-I0UCuF9H.js → teams-pbVU7Y_N.js} +2 -2
  103. package/public/assets/{teams-I0UCuF9H.js.map → teams-pbVU7Y_N.js.map} +1 -1
  104. package/public/assets/{terminal-viewer-impl-CKM8zVGQ.js → terminal-viewer-impl-M_3TITNG.js} +3 -3
  105. package/public/assets/{terminal-viewer-impl-CKM8zVGQ.js.map → terminal-viewer-impl-M_3TITNG.js.map} +1 -1
  106. package/public/assets/{use-keyboard-viewport-DxmCAbQi.js → use-keyboard-viewport-BVAU-Ud8.js} +2 -2
  107. package/public/assets/{use-keyboard-viewport-DxmCAbQi.js.map → use-keyboard-viewport-BVAU-Ud8.js.map} +1 -1
  108. package/public/assets/work-queue-D12yWpsi.js +2 -0
  109. package/public/assets/{work-queue-xWRsNNPk.js.map → work-queue-D12yWpsi.js.map} +1 -1
  110. package/public/assets/workspaces-CIGMb3ST.js +3 -0
  111. package/public/assets/{workspaces-B_nIm6Tw.js.map → workspaces-CIGMb3ST.js.map} +1 -1
  112. package/public/index.html +21 -21
  113. package/src/db/agents.ts +4 -46
  114. package/src/db/index.ts +1 -0
  115. package/src/db/migrations.ts +93 -1
  116. package/src/db/provider-quotas.ts +358 -0
  117. package/src/db/schema.ts +16 -1
  118. package/src/quota.ts +14 -1
  119. package/src/routes/index.ts +3 -0
  120. package/src/routes/provider-quotas.ts +48 -0
  121. package/src/security.ts +1 -0
  122. package/src/token-db.ts +3 -3
  123. package/public/assets/MessageBubble-DzhG1x8z.js +0 -3
  124. package/public/assets/branch-state-badge-B7T81Oct.js +0 -2
  125. package/public/assets/chat-B7EMdwIg.js +0 -2
  126. package/public/assets/copy-button-m00JmflM.js +0 -2
  127. package/public/assets/dist-CYS2puT9.js +0 -2
  128. package/public/assets/dist-CydxJz3m.js +0 -2
  129. package/public/assets/dist-DfQP9-VC.js +0 -2
  130. package/public/assets/index-DB6XGZy9.js +0 -23
  131. package/public/assets/index-DB6XGZy9.js.map +0 -1
  132. package/public/assets/index-DepEqGST.css +0 -2
  133. package/public/assets/settings-BUD05ZM3.js +0 -4
  134. package/public/assets/work-queue-xWRsNNPk.js +0 -2
  135. package/public/assets/workspaces-B_nIm6Tw.js +0 -3
@@ -17,6 +17,7 @@ import { matchAgents } from "../agent-ref";
17
17
  import { getDb } from "./connection.ts";
18
18
  import { ensureContinuationArchiveSearchSchema } from "./continuation-archives.ts";
19
19
  import { normalizeReactionEmoji } from "./mappers.ts";
20
+ import { normalizeProviderQuotaAccountKeys } from "./provider-quotas.ts";
20
21
  import { projectRootsMissingIssueRepo, setProjectIssueRepo } from "./projects.ts";
21
22
  import { repoFromGitRemote } from "../services/git-remote.ts";
22
23
  import type {
@@ -602,14 +603,105 @@ export function applyMigrations(): void {
602
603
  getDb().run(`
603
604
  CREATE TABLE IF NOT EXISTS quota_snapshots (
604
605
  id INTEGER PRIMARY KEY AUTOINCREMENT,
605
- agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
606
+ provider TEXT NOT NULL,
607
+ account_key TEXT NOT NULL,
608
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
606
609
  windows TEXT NOT NULL,
607
610
  source TEXT NOT NULL,
608
611
  quota_state TEXT NOT NULL,
609
612
  captured_at INTEGER NOT NULL
610
613
  )
611
614
  `);
615
+ const quotaSnapshotCols = (getDb().query("PRAGMA table_info(quota_snapshots)").all() as Array<{ name: string }>).map((col) => col.name);
616
+ if (!quotaSnapshotCols.includes("provider") || !quotaSnapshotCols.includes("account_key")) {
617
+ getDb().run("ALTER TABLE quota_snapshots RENAME TO quota_snapshots_legacy_agent");
618
+ getDb().run(`
619
+ CREATE TABLE quota_snapshots (
620
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
621
+ provider TEXT NOT NULL,
622
+ account_key TEXT NOT NULL,
623
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
624
+ windows TEXT NOT NULL,
625
+ source TEXT NOT NULL,
626
+ quota_state TEXT NOT NULL,
627
+ captured_at INTEGER NOT NULL
628
+ )
629
+ `);
630
+ getDb().run(`
631
+ INSERT INTO quota_snapshots (id, provider, account_key, agent_id, windows, source, quota_state, captured_at)
632
+ SELECT
633
+ qs.id,
634
+ CASE
635
+ WHEN json_extract(a.meta, '$.provider') IS NOT NULL THEN lower(json_extract(a.meta, '$.provider'))
636
+ WHEN a.tags LIKE '%"codex"%' OR lower(a.id) LIKE '%codex%' OR lower(a.name) LIKE '%codex%' THEN 'codex'
637
+ ELSE 'claude'
638
+ END,
639
+ coalesce(
640
+ nullif(json_extract(a.meta, '$.accountKey'), ''),
641
+ nullif(json_extract(a.meta, '$.accountId'), ''),
642
+ nullif(json_extract(a.meta, '$.orgId'), ''),
643
+ 'host:' || coalesce(a.machine, a.rig, 'unknown')
644
+ ),
645
+ qs.agent_id,
646
+ qs.windows,
647
+ qs.source,
648
+ qs.quota_state,
649
+ qs.captured_at
650
+ FROM quota_snapshots_legacy_agent qs
651
+ LEFT JOIN agents a ON a.id = qs.agent_id
652
+ `);
653
+ getDb().run("DROP TABLE quota_snapshots_legacy_agent");
654
+ }
655
+ getDb().run(`
656
+ CREATE TABLE IF NOT EXISTS provider_quotas (
657
+ provider TEXT NOT NULL,
658
+ account_key TEXT NOT NULL,
659
+ quota_state TEXT,
660
+ source_agent_ids TEXT NOT NULL DEFAULT '[]',
661
+ last_updated_at INTEGER,
662
+ last_attempt_at INTEGER NOT NULL,
663
+ last_error TEXT,
664
+ updated_at INTEGER NOT NULL,
665
+ created_at INTEGER NOT NULL,
666
+ PRIMARY KEY (provider, account_key)
667
+ )
668
+ `);
669
+ getDb().run(`
670
+ INSERT INTO provider_quotas (
671
+ provider, account_key, quota_state, source_agent_ids, last_updated_at,
672
+ last_attempt_at, last_error, updated_at, created_at
673
+ )
674
+ SELECT
675
+ CASE
676
+ WHEN json_extract(meta, '$.provider') IS NOT NULL THEN lower(json_extract(meta, '$.provider'))
677
+ WHEN tags LIKE '%"codex"%' OR lower(id) LIKE '%codex%' OR lower(name) LIKE '%codex%' THEN 'codex'
678
+ ELSE 'claude'
679
+ END AS provider,
680
+ coalesce(
681
+ nullif(json_extract(meta, '$.accountKey'), ''),
682
+ nullif(json_extract(meta, '$.accountId'), ''),
683
+ nullif(json_extract(meta, '$.orgId'), ''),
684
+ 'host:' || coalesce(machine, rig, 'unknown')
685
+ ) AS account_key,
686
+ quota_state,
687
+ json_array(id),
688
+ coalesce(json_extract(quota_state, '$.updatedAt'), last_seen),
689
+ coalesce(json_extract(quota_state, '$.updatedAt'), last_seen),
690
+ NULL,
691
+ last_seen,
692
+ created_at
693
+ FROM agents
694
+ WHERE quota_state IS NOT NULL
695
+ ON CONFLICT(provider, account_key) DO UPDATE SET
696
+ quota_state = excluded.quota_state,
697
+ source_agent_ids = excluded.source_agent_ids,
698
+ last_updated_at = max(coalesce(provider_quotas.last_updated_at, 0), coalesce(excluded.last_updated_at, 0)),
699
+ last_attempt_at = max(provider_quotas.last_attempt_at, excluded.last_attempt_at),
700
+ updated_at = max(provider_quotas.updated_at, excluded.updated_at)
701
+ `);
702
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_quota_snapshots_account_time ON quota_snapshots(provider, account_key, captured_at DESC)");
612
703
  getDb().run("CREATE INDEX IF NOT EXISTS idx_quota_snapshots_agent_time ON quota_snapshots(agent_id, captured_at DESC)");
704
+ normalizeProviderQuotaAccountKeys();
613
705
 
614
706
  getDb().run(`
615
707
  CREATE TABLE IF NOT EXISTS memories (
@@ -0,0 +1,358 @@
1
+ import { DAY_MS } from "../config";
2
+ import { parseJson } from "../utils";
3
+ import { getDb } from "./connection.ts";
4
+ import { parseStringArray } from "./shared.ts";
5
+ import type { AgentCard, ProviderQuotaError, ProviderQuotaMatrix, ProviderQuotaRecord, ProviderQuotaSnapshot, ProviderQuotaUpdateInput, QuotaState } from "../types";
6
+
7
+ const PROVIDER_QUOTA_STALE_AFTER_MS = 10 * 60_000;
8
+
9
+ type ProviderQuotaRow = {
10
+ provider: string;
11
+ account_key: string;
12
+ quota_state?: string | null;
13
+ source_agent_ids?: string | null;
14
+ last_updated_at?: number | null;
15
+ last_attempt_at: number;
16
+ last_error?: string | null;
17
+ updated_at: number;
18
+ created_at: number;
19
+ };
20
+
21
+ type ProviderQuotaSnapshotRow = {
22
+ provider: string;
23
+ account_key: string;
24
+ agent_id?: string | null;
25
+ quota_state: string;
26
+ captured_at: number;
27
+ };
28
+
29
+ function cleanKey(value: string): string {
30
+ return value.trim();
31
+ }
32
+
33
+ function normalizeProvider(value: string): string {
34
+ return cleanKey(value).toLowerCase();
35
+ }
36
+
37
+ function canonicalProviderQuotaAccountKey(provider: string, value: string): string {
38
+ const accountKey = cleanKey(value);
39
+ const legacySuffix = `:${normalizeProvider(provider)}`;
40
+ return accountKey.startsWith("host:") && accountKey.endsWith(legacySuffix)
41
+ ? accountKey.slice(0, -legacySuffix.length)
42
+ : accountKey;
43
+ }
44
+
45
+ function sourceAgentHost(sourceAgentId: string | undefined): string | undefined {
46
+ if (!sourceAgentId) return undefined;
47
+ const row = getDb().query("SELECT machine, rig FROM agents WHERE id = ?").get(sourceAgentId) as { machine?: string | null; rig?: string | null } | undefined;
48
+ const host = row?.machine ?? row?.rig ?? undefined;
49
+ return host?.trim() || undefined;
50
+ }
51
+
52
+ function hostFromAccountKey(accountKey: string): string | undefined {
53
+ if (!accountKey.startsWith("host:")) return undefined;
54
+ const host = accountKey.slice("host:".length);
55
+ return host && !host.includes(":") ? host : undefined;
56
+ }
57
+
58
+ function normalizeSourceAgentIds(value: unknown): string[] {
59
+ return [...new Set(parseStringArray(typeof value === "string" ? value : JSON.stringify(value ?? [])).filter(Boolean))];
60
+ }
61
+
62
+ function quotaRowsForProvider(provider: string): ProviderQuotaRow[] {
63
+ return getDb().query("SELECT * FROM provider_quotas WHERE provider = ?").all(provider) as ProviderQuotaRow[];
64
+ }
65
+
66
+ function supersededAccountKeys(provider: string, accountKey: string, rawAccountKey: string, sourceAgentId: string | undefined): string[] {
67
+ const candidates = new Set<string>();
68
+ if (rawAccountKey && rawAccountKey !== accountKey) candidates.add(rawAccountKey);
69
+ const host = hostFromAccountKey(accountKey) ?? sourceAgentHost(sourceAgentId);
70
+ if (host) {
71
+ candidates.add(`host:${host}:${provider}`);
72
+ if (accountKey !== `host:${host}`) candidates.add(`host:${host}`);
73
+ }
74
+ if (sourceAgentId) {
75
+ for (const row of quotaRowsForProvider(provider)) {
76
+ if (row.account_key === accountKey) continue;
77
+ if (!normalizeSourceAgentIds(row.source_agent_ids).includes(sourceAgentId)) continue;
78
+ candidates.add(row.account_key);
79
+ }
80
+ }
81
+ candidates.delete(accountKey);
82
+ return [...candidates];
83
+ }
84
+
85
+ function newestQuotaState(rows: ProviderQuotaRow[]): string | null {
86
+ const row = rows
87
+ .filter((candidate) => candidate.quota_state)
88
+ .sort((a, b) => (b.last_updated_at ?? 0) - (a.last_updated_at ?? 0))[0];
89
+ return row?.quota_state ?? null;
90
+ }
91
+
92
+ function newestLastError(rows: ProviderQuotaRow[]): string | null {
93
+ const row = rows
94
+ .filter((candidate) => candidate.last_error)
95
+ .sort((a, b) => b.last_attempt_at - a.last_attempt_at)[0];
96
+ return row?.last_error ?? null;
97
+ }
98
+
99
+ function mergeProviderQuotaRows(provider: string, accountKey: string, oldAccountKeys: string[], now: number): number {
100
+ const keys = [...new Set(oldAccountKeys.filter((key) => key && key !== accountKey))];
101
+ if (keys.length === 0) return 0;
102
+ const placeholders = [accountKey, ...keys].map(() => "?").join(", ");
103
+ const rows = getDb().query(`SELECT * FROM provider_quotas WHERE provider = ? AND account_key IN (${placeholders})`).all(provider, accountKey, ...keys) as ProviderQuotaRow[];
104
+ const oldRows = rows.filter((row) => row.account_key !== accountKey);
105
+ if (oldRows.length === 0) return 0;
106
+
107
+ const sourceAgentIds = [...new Set(rows.flatMap((row) => normalizeSourceAgentIds(row.source_agent_ids)))];
108
+ const quotaState = newestQuotaState(rows);
109
+ const lastUpdatedAt = rows.reduce<number | undefined>((latest, row) => {
110
+ if (row.last_updated_at === null || row.last_updated_at === undefined) return latest;
111
+ return latest === undefined ? row.last_updated_at : Math.max(latest, row.last_updated_at);
112
+ }, undefined);
113
+ const lastAttemptAt = rows.reduce((latest, row) => Math.max(latest, row.last_attempt_at), 0);
114
+ const updatedAt = Math.max(now, ...rows.map((row) => row.updated_at));
115
+ const createdAt = Math.min(...rows.map((row) => row.created_at));
116
+
117
+ getDb().query(`
118
+ INSERT INTO provider_quotas (
119
+ provider, account_key, quota_state, source_agent_ids, last_updated_at,
120
+ last_attempt_at, last_error, updated_at, created_at
121
+ )
122
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
123
+ ON CONFLICT(provider, account_key) DO UPDATE SET
124
+ quota_state = excluded.quota_state,
125
+ source_agent_ids = excluded.source_agent_ids,
126
+ last_updated_at = excluded.last_updated_at,
127
+ last_attempt_at = excluded.last_attempt_at,
128
+ last_error = excluded.last_error,
129
+ updated_at = excluded.updated_at
130
+ `).run(
131
+ provider,
132
+ accountKey,
133
+ quotaState,
134
+ JSON.stringify(sourceAgentIds),
135
+ lastUpdatedAt ?? null,
136
+ lastAttemptAt || now,
137
+ newestLastError(rows),
138
+ updatedAt,
139
+ createdAt,
140
+ );
141
+
142
+ const oldPlaceholders = keys.map(() => "?").join(", ");
143
+ getDb().query(`UPDATE quota_snapshots SET account_key = ? WHERE provider = ? AND account_key IN (${oldPlaceholders})`).run(accountKey, provider, ...keys);
144
+ const result = getDb().query(`DELETE FROM provider_quotas WHERE provider = ? AND account_key IN (${oldPlaceholders})`).run(provider, ...keys);
145
+ return result.changes;
146
+ }
147
+
148
+ function quotaFingerprint(quota: QuotaState): string {
149
+ return JSON.stringify(quota.windows.map((window) => ({
150
+ name: window.name,
151
+ unit: window.unit,
152
+ utilization: window.utilization,
153
+ })).sort((a, b) => a.name.localeCompare(b.name) || a.unit.localeCompare(b.unit)));
154
+ }
155
+
156
+ function rowToSnapshot(row: ProviderQuotaSnapshotRow): ProviderQuotaSnapshot {
157
+ return {
158
+ provider: row.provider,
159
+ accountKey: row.account_key,
160
+ capturedAt: row.captured_at,
161
+ ...(row.agent_id ? { sourceAgentId: row.agent_id } : {}),
162
+ quota: parseJson<QuotaState>(row.quota_state, { windows: [], source: "probe", updatedAt: row.captured_at }),
163
+ };
164
+ }
165
+
166
+ function rowToRecord(row: ProviderQuotaRow, history: ProviderQuotaSnapshot[], now: number): ProviderQuotaRecord {
167
+ const lastError = parseJson<ProviderQuotaError | undefined>(row.last_error, undefined);
168
+ const lastUpdatedAt = row.last_updated_at ?? undefined;
169
+ return {
170
+ provider: row.provider,
171
+ accountKey: row.account_key,
172
+ ...(row.quota_state ? { quota: parseJson<QuotaState>(row.quota_state, { windows: [], source: "probe", updatedAt: lastUpdatedAt ?? row.last_attempt_at }) } : {}),
173
+ sourceAgentIds: normalizeSourceAgentIds(row.source_agent_ids),
174
+ ...(lastUpdatedAt !== undefined ? { lastUpdatedAt } : {}),
175
+ lastAttemptAt: row.last_attempt_at,
176
+ ...(lastError ? { lastError } : {}),
177
+ stale: lastUpdatedAt === undefined || now - lastUpdatedAt > PROVIDER_QUOTA_STALE_AFTER_MS,
178
+ history,
179
+ };
180
+ }
181
+
182
+ function latestSnapshotFingerprint(provider: string, accountKey: string): string | undefined {
183
+ const row = getDb().query(`
184
+ SELECT quota_state FROM quota_snapshots
185
+ WHERE provider = ? AND account_key = ?
186
+ ORDER BY captured_at DESC, id DESC
187
+ LIMIT 1
188
+ `).get(provider, accountKey) as { quota_state: string } | undefined;
189
+ return row ? quotaFingerprint(parseJson<QuotaState>(row.quota_state, { windows: [], source: "probe", updatedAt: 0 })) : undefined;
190
+ }
191
+
192
+ function providerQuotaIdentityForAgent(agent: Pick<AgentCard, "id" | "name" | "tags" | "machine" | "rig" | "meta">): { provider: string; accountKey: string } {
193
+ const meta = agent.meta ?? {};
194
+ const provider = typeof meta.provider === "string" && meta.provider.trim()
195
+ ? meta.provider
196
+ : agent.tags.includes("codex") || agent.id.toLowerCase().includes("codex") || agent.name.toLowerCase().includes("codex")
197
+ ? "codex"
198
+ : "claude";
199
+ const accountKey = typeof meta.accountKey === "string" && meta.accountKey.trim()
200
+ ? meta.accountKey
201
+ : typeof meta.accountId === "string" && meta.accountId.trim()
202
+ ? meta.accountId
203
+ : typeof meta.orgId === "string" && meta.orgId.trim()
204
+ ? meta.orgId
205
+ : `host:${agent.machine ?? agent.rig ?? "unknown"}`;
206
+ return { provider: normalizeProvider(provider), accountKey: cleanKey(accountKey) };
207
+ }
208
+
209
+ export function upsertProviderQuota(input: ProviderQuotaUpdateInput, now = Date.now()): ProviderQuotaRecord {
210
+ const provider = normalizeProvider(input.provider);
211
+ const rawAccountKey = cleanKey(input.accountKey);
212
+ const accountKey = canonicalProviderQuotaAccountKey(provider, rawAccountKey);
213
+ if (!provider) throw new Error("provider required");
214
+ if (!accountKey) throw new Error("accountKey required");
215
+ mergeProviderQuotaRows(provider, accountKey, supersededAccountKeys(provider, accountKey, rawAccountKey, input.sourceAgentId), now);
216
+ const existing = getDb().query("SELECT * FROM provider_quotas WHERE provider = ? AND account_key = ?").get(provider, accountKey) as ProviderQuotaRow | undefined;
217
+ const sourceAgentIds = normalizeSourceAgentIds(existing?.source_agent_ids);
218
+ if (input.sourceAgentId && !sourceAgentIds.includes(input.sourceAgentId)) sourceAgentIds.push(input.sourceAgentId);
219
+ const lastUpdatedAt = input.quota ? input.quota.updatedAt : existing?.last_updated_at ?? undefined;
220
+ const lastAttemptAt = input.lastAttemptAt ?? input.quota?.updatedAt ?? now;
221
+ const lastError = input.lastError === undefined
222
+ ? input.quota ? null : existing?.last_error ?? null
223
+ : input.lastError;
224
+ const quotaState = input.quota ? JSON.stringify(input.quota) : existing?.quota_state ?? null;
225
+
226
+ getDb().query(`
227
+ INSERT INTO provider_quotas (
228
+ provider, account_key, quota_state, source_agent_ids, last_updated_at,
229
+ last_attempt_at, last_error, updated_at, created_at
230
+ )
231
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
232
+ ON CONFLICT(provider, account_key) DO UPDATE SET
233
+ quota_state = excluded.quota_state,
234
+ source_agent_ids = excluded.source_agent_ids,
235
+ last_updated_at = excluded.last_updated_at,
236
+ last_attempt_at = excluded.last_attempt_at,
237
+ last_error = excluded.last_error,
238
+ updated_at = excluded.updated_at
239
+ `).run(
240
+ provider,
241
+ accountKey,
242
+ quotaState,
243
+ JSON.stringify(sourceAgentIds),
244
+ lastUpdatedAt ?? null,
245
+ lastAttemptAt,
246
+ lastError ? JSON.stringify(lastError) : null,
247
+ now,
248
+ now,
249
+ );
250
+
251
+ if (input.quota && latestSnapshotFingerprint(provider, accountKey) !== quotaFingerprint(input.quota)) {
252
+ recordProviderQuotaSnapshot({ provider, accountKey, quota: input.quota, sourceAgentId: input.sourceAgentId }, now);
253
+ }
254
+ pruneProviderQuotaRecords(provider, undefined, now - DAY_MS);
255
+
256
+ return listProviderQuotas({ provider, accountKey, historyLimit: 24, now }).quotas[0]!;
257
+ }
258
+
259
+ function recordProviderQuotaSnapshot(input: { provider: string; accountKey: string; quota: QuotaState; sourceAgentId?: string }, now: number): void {
260
+ const sourceAgentId = input.sourceAgentId && getDb().query("SELECT 1 FROM agents WHERE id = ?").get(input.sourceAgentId)
261
+ ? input.sourceAgentId
262
+ : undefined;
263
+ getDb().query(`
264
+ INSERT INTO quota_snapshots (provider, account_key, agent_id, windows, source, quota_state, captured_at)
265
+ VALUES (?, ?, ?, ?, ?, ?, ?)
266
+ `).run(
267
+ input.provider,
268
+ input.accountKey,
269
+ sourceAgentId ?? null,
270
+ JSON.stringify(input.quota.windows),
271
+ input.quota.source,
272
+ JSON.stringify(input.quota),
273
+ now,
274
+ );
275
+ pruneProviderQuotaSnapshots(input.provider, input.accountKey, now - DAY_MS);
276
+ }
277
+
278
+ function pruneProviderQuotaSnapshots(provider?: string, accountKey?: string, olderThan = Date.now() - DAY_MS): number {
279
+ const result = provider && accountKey
280
+ ? getDb().query("DELETE FROM quota_snapshots WHERE provider = ? AND account_key = ? AND captured_at < ?").run(provider, accountKey, olderThan)
281
+ : getDb().query("DELETE FROM quota_snapshots WHERE captured_at < ?").run(olderThan);
282
+ return result.changes;
283
+ }
284
+
285
+ function pruneProviderQuotaRecords(provider?: string, accountKey?: string, olderThan = Date.now() - DAY_MS): number {
286
+ const result = provider && accountKey
287
+ ? getDb().query("DELETE FROM provider_quotas WHERE provider = ? AND account_key = ? AND updated_at < ?").run(provider, accountKey, olderThan)
288
+ : provider
289
+ ? getDb().query("DELETE FROM provider_quotas WHERE provider = ? AND updated_at < ?").run(provider, olderThan)
290
+ : getDb().query("DELETE FROM provider_quotas WHERE updated_at < ?").run(olderThan);
291
+ return result.changes;
292
+ }
293
+
294
+ export function normalizeProviderQuotaAccountKeys(now = Date.now()): number {
295
+ let changes = 0;
296
+ for (const row of getDb().query("SELECT * FROM provider_quotas").all() as ProviderQuotaRow[]) {
297
+ let accountKey = canonicalProviderQuotaAccountKey(row.provider, row.account_key);
298
+ if (accountKey.startsWith("credential:")) {
299
+ const sourceAgentId = normalizeSourceAgentIds(row.source_agent_ids)[0];
300
+ const host = sourceAgentHost(sourceAgentId);
301
+ if (host) accountKey = `host:${host}`;
302
+ }
303
+ if (accountKey !== row.account_key) {
304
+ changes += mergeProviderQuotaRows(row.provider, accountKey, [row.account_key], now);
305
+ }
306
+ }
307
+ changes += pruneProviderQuotaRecords(undefined, undefined, now - DAY_MS);
308
+ return changes;
309
+ }
310
+
311
+ function listProviderQuotaSnapshots(provider: string, accountKey: string, options: { since?: number; limit?: number } = {}): ProviderQuotaSnapshot[] {
312
+ const limit = Math.max(1, Math.min(options.limit ?? 288, 1000));
313
+ const params: any[] = [normalizeProvider(provider), cleanKey(accountKey)];
314
+ let sql = "SELECT * FROM quota_snapshots WHERE provider = ? AND account_key = ?";
315
+ if (options.since !== undefined) {
316
+ sql += " AND captured_at >= ?";
317
+ params.push(options.since);
318
+ }
319
+ sql += " ORDER BY captured_at ASC, id ASC LIMIT ?";
320
+ params.push(limit);
321
+ return (getDb().query(sql).all(...params) as ProviderQuotaSnapshotRow[]).map(rowToSnapshot);
322
+ }
323
+
324
+ export function listAgentQuotaSnapshotsFromProviderStore(agent: AgentCard, options: { since?: number; limit?: number } = {}) {
325
+ const identity = providerQuotaIdentityForAgent(agent);
326
+ return listProviderQuotaSnapshots(identity.provider, identity.accountKey, options).map((snapshot) => ({
327
+ agentId: agent.id,
328
+ capturedAt: snapshot.capturedAt,
329
+ windows: snapshot.quota.windows,
330
+ source: snapshot.quota.source,
331
+ updatedAt: snapshot.quota.updatedAt,
332
+ }));
333
+ }
334
+
335
+ export function listProviderQuotas(options: { provider?: string; accountKey?: string; since?: number; historyLimit?: number; now?: number } = {}): ProviderQuotaMatrix {
336
+ const historyLimit = Math.max(0, Math.min(options.historyLimit ?? 24, 288));
337
+ const now = options.now ?? Date.now();
338
+ const params: any[] = [];
339
+ let sql = "SELECT * FROM provider_quotas WHERE 1=1";
340
+ if (options.provider) {
341
+ sql += " AND provider = ?";
342
+ params.push(normalizeProvider(options.provider));
343
+ }
344
+ if (options.accountKey) {
345
+ sql += " AND account_key = ?";
346
+ params.push(cleanKey(options.accountKey));
347
+ }
348
+ sql += " ORDER BY provider ASC, account_key ASC";
349
+ const rows = getDb().query(sql).all(...params) as ProviderQuotaRow[];
350
+ return {
351
+ staleAfterMs: PROVIDER_QUOTA_STALE_AFTER_MS,
352
+ quotas: rows.map((row) => rowToRecord(
353
+ row,
354
+ historyLimit > 0 ? listProviderQuotaSnapshots(row.provider, row.account_key, { since: options.since, limit: historyLimit }) : [],
355
+ now,
356
+ )),
357
+ };
358
+ }
package/src/db/schema.ts CHANGED
@@ -292,7 +292,9 @@ export function initDb(path: string = "agent-relay.db"): Database {
292
292
 
293
293
  CREATE TABLE IF NOT EXISTS quota_snapshots (
294
294
  id INTEGER PRIMARY KEY AUTOINCREMENT,
295
- agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
295
+ provider TEXT NOT NULL,
296
+ account_key TEXT NOT NULL,
297
+ agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL,
296
298
  windows TEXT NOT NULL,
297
299
  source TEXT NOT NULL,
298
300
  quota_state TEXT NOT NULL,
@@ -300,6 +302,19 @@ export function initDb(path: string = "agent-relay.db"): Database {
300
302
  );
301
303
  CREATE INDEX IF NOT EXISTS idx_quota_snapshots_agent_time ON quota_snapshots(agent_id, captured_at DESC);
302
304
 
305
+ CREATE TABLE IF NOT EXISTS provider_quotas (
306
+ provider TEXT NOT NULL,
307
+ account_key TEXT NOT NULL,
308
+ quota_state TEXT,
309
+ source_agent_ids TEXT NOT NULL DEFAULT '[]',
310
+ last_updated_at INTEGER,
311
+ last_attempt_at INTEGER NOT NULL,
312
+ last_error TEXT,
313
+ updated_at INTEGER NOT NULL,
314
+ created_at INTEGER NOT NULL,
315
+ PRIMARY KEY (provider, account_key)
316
+ );
317
+
303
318
  CREATE TABLE IF NOT EXISTS agent_continuations (
304
319
  agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
305
320
  objective TEXT NOT NULL,
package/src/quota.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isRecord } from "agent-relay-sdk";
2
- import type { QuotaState, QuotaWindowName, QuotaSource, QuotaUnit } from "./types";
2
+ import type { ProviderQuotaError, QuotaState, QuotaWindowName, QuotaSource, QuotaUnit } from "./types";
3
3
 
4
4
  const QUOTA_SOURCES = new Set<QuotaSource>(["header", "probe", "ingest"]);
5
5
  const QUOTA_UNITS = new Set<QuotaUnit>(["tokens", "requests", "%"]);
@@ -38,3 +38,16 @@ export function quotaStateFromUnknown(value: unknown): QuotaState | undefined {
38
38
  if (windows.length === 0) return undefined;
39
39
  return { windows, source, updatedAt };
40
40
  }
41
+
42
+ export function providerQuotaErrorFromUnknown(value: unknown): ProviderQuotaError | undefined {
43
+ if (!isRecord(value)) return undefined;
44
+ const type = typeof value.type === "string" && value.type.trim() ? value.type.trim() : undefined;
45
+ const message = typeof value.message === "string" && value.message.trim() ? value.message.trim() : undefined;
46
+ if (!type || !message) return undefined;
47
+ const retryAfterMs = numberValue(value.retryAfterMs);
48
+ return {
49
+ type,
50
+ message,
51
+ ...(retryAfterMs !== undefined && retryAfterMs >= 0 ? { retryAfterMs } : {}),
52
+ };
53
+ }
@@ -5,6 +5,7 @@ import { deleteAgentTerminalSession, postAgentAction, postAgentPermissionDecisio
5
5
  import { deleteArtifactById, getArtifactById, getArtifactContent, getArtifacts, headArtifactContent, postArtifact } from "./artifacts";
6
6
  import { deleteAutomationById, getAutomationById, getAutomationRuns, getAutomations, patchAutomation, postAutomation, postAutomationRun } from "./automations";
7
7
  import { deleteCommandById, deleteProviderModelRoute, getProviderConfigRoute, getProviderConfigsRoute, getProvidersRoute, patchProviderModelRoute, postProviderConfigTestRoute, putProviderConfigRoute, putProviderModelRoute } from "./provider-config";
8
+ import { getProviderQuotasRoute, postProviderQuotaRoute } from "./provider-quotas";
8
9
  import { deleteConfigKey, getConfigKey, getConfigKeyHistory, getConfigNamespace, putConfigKey } from "./config";
9
10
  import { deleteInboxDraftRoute, getChatHistoryImportsRoute, getInboxStateRoute, patchInboxThreadState, postChatHistoryImportRoute, putInboxDraft } from "./inbox";
10
11
  import { deleteMemoryRoute, getMemories, getMemoryBrokerInfo, getMemoryById, getMemoryStats, patchMemory, postMemory, postMemoryInject, postMemorySearch } from "./memory";
@@ -87,6 +88,8 @@ const routes: Route[] = [
87
88
  route("GET", "/api/agents", getAgents),
88
89
  route("GET", "/api/agents/find", findAgents),
89
90
  route("POST", "/api/route", postRouteAdvice),
91
+ route("GET", "/api/provider-quotas", getProviderQuotasRoute),
92
+ route("POST", "/api/provider-quotas", postProviderQuotaRoute),
90
93
  route("GET", "/api/agents/spawn/directories", getHostDirectories),
91
94
  route("GET", "/api/agents/:id/chat", getAgentChatHistory),
92
95
  route("GET", "/api/agents/:id/context-snapshots", getAgentContextSnapshots),
@@ -0,0 +1,48 @@
1
+ import { isRecord } from "agent-relay-sdk";
2
+ import { listProviderQuotas, upsertProviderQuota } from "../db";
3
+ import { quotaStateFromUnknown, providerQuotaErrorFromUnknown } from "../quota";
4
+ import { cleanString } from "../validation";
5
+ import { error, json, parseBody, parseQueryInt, type Handler } from "./_shared";
6
+ import type { ProviderQuotaUpdateInput } from "../types";
7
+
8
+ function cleanProviderQuotaUpdate(body: unknown): ProviderQuotaUpdateInput | string {
9
+ if (!isRecord(body)) return "JSON object body required";
10
+ const provider = cleanString(body.provider, "provider", { required: true, max: 80 })!;
11
+ const accountKey = cleanString(body.accountKey, "accountKey", { required: true, max: 240 })!;
12
+ const quota = quotaStateFromUnknown(body.quota);
13
+ const lastAttemptAt = typeof body.lastAttemptAt === "number" && Number.isFinite(body.lastAttemptAt) ? body.lastAttemptAt : undefined;
14
+ const lastError = body.lastError === null ? null : providerQuotaErrorFromUnknown(body.lastError);
15
+ if (!quota && lastError === undefined) return "quota or lastError required";
16
+ const sourceAgentId = cleanString(body.sourceAgentId, "sourceAgentId", { max: 200 });
17
+ return {
18
+ provider,
19
+ accountKey,
20
+ ...(quota ? { quota } : {}),
21
+ ...(lastAttemptAt !== undefined ? { lastAttemptAt } : {}),
22
+ ...(lastError !== undefined ? { lastError } : {}),
23
+ ...(sourceAgentId ? { sourceAgentId } : {}),
24
+ };
25
+ }
26
+
27
+ export const getProviderQuotasRoute: Handler = (req) => {
28
+ const url = new URL(req.url);
29
+ const historyLimit = parseQueryInt(url.searchParams.get("historyLimit") ?? url.searchParams.get("limit"), { min: 0, max: 288, clamp: true });
30
+ if (Number.isNaN(historyLimit)) return error("historyLimit must be an integer between 0 and 288");
31
+ const sinceRaw = url.searchParams.get("since");
32
+ const since = sinceRaw ? Number(sinceRaw) : undefined;
33
+ if (sinceRaw && (!Number.isFinite(since) || since! < 0)) return error("since must be a positive timestamp");
34
+ return json(listProviderQuotas({
35
+ provider: url.searchParams.get("provider") ?? undefined,
36
+ accountKey: url.searchParams.get("accountKey") ?? undefined,
37
+ since,
38
+ historyLimit: historyLimit ?? undefined,
39
+ }));
40
+ };
41
+
42
+ export const postProviderQuotaRoute: Handler = async (req) => {
43
+ const parsed = await parseBody<unknown>(req);
44
+ if (!parsed.ok) return error(parsed.error, parsed.status);
45
+ const input = cleanProviderQuotaUpdate(parsed.body);
46
+ if (typeof input === "string") return error(input);
47
+ return json(upsertProviderQuota(input), 201);
48
+ };
package/src/security.ts CHANGED
@@ -274,6 +274,7 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
274
274
  // Spawn requires the spawn capability (see requiredScopeFor above) — matches the in-service gate.
275
275
  if (pathname === "/api/agents/spawn") return "command:spawn";
276
276
  if (pathname.startsWith("/api/agents")) return method === "GET" ? "agent:read" : "agent:write";
277
+ if (pathname === "/api/provider-quotas") return method === "GET" ? "agent:read" : "quota:write";
277
278
  if (pathname.startsWith("/api/messages") || pathname.startsWith("/api/inbox")) return method === "GET" ? "message:read" : "message:send";
278
279
  if (pathname.startsWith("/api/agent-profiles")) return method === "GET" ? "agent:read" : "agent:write";
279
280
  if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
package/src/token-db.ts CHANGED
@@ -78,7 +78,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
78
78
  name: "Provider Agent",
79
79
  description: "Coding-agent runtime access for messages, commands, tasks, and scoped memory reads.",
80
80
  role: "provider",
81
- scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write"],
81
+ scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "plans:read", "plans:write", "teams:read", "teams:write", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write", "quota:write"],
82
82
  ttlSeconds: 24 * 60 * 60,
83
83
  builtIn: true,
84
84
  createdBy: "system",
@@ -88,7 +88,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
88
88
  name: "Provider Child Agent",
89
89
  description: "Delegated child-agent runtime access, constrained to its parent and spawn request.",
90
90
  role: "provider",
91
- scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write"],
91
+ scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write", "quota:write"],
92
92
  constraints: { canDelegate: false },
93
93
  ttlSeconds: 2 * 60 * 60,
94
94
  builtIn: true,
@@ -99,7 +99,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
99
99
  name: "Provider Interactive Agent",
100
100
  description: "User-launched provider runtime access constrained to its own agent and cwd for long interactive sessions.",
101
101
  role: "provider",
102
- scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write"],
102
+ scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use", "teams:read", "blackboard:read", "blackboard:write", "project:read", "project:write", "issue:read", "issue:write", "notifications:read", "notifications:write", "insights:write", "quota:write"],
103
103
  constraints: { terminalAttach: false, logsRead: false, canDelegate: false },
104
104
  ttlSeconds: 30 * 24 * 60 * 60,
105
105
  builtIn: true,