govyn 0.0.1 → 0.2.5

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 (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -1
  3. package/configs/multi-provider.yaml +68 -0
  4. package/configs/openai-only.yaml +45 -0
  5. package/configs/team-setup.yaml +88 -0
  6. package/dist/action-logger.d.ts +128 -0
  7. package/dist/action-logger.js +356 -0
  8. package/dist/action-logger.js.map +1 -0
  9. package/dist/admin-cli.d.ts +2 -0
  10. package/dist/admin-cli.js +36 -0
  11. package/dist/admin-cli.js.map +1 -0
  12. package/dist/agents.d.ts +23 -0
  13. package/dist/agents.js +59 -0
  14. package/dist/agents.js.map +1 -0
  15. package/dist/alert-api.d.ts +14 -0
  16. package/dist/alert-api.js +355 -0
  17. package/dist/alert-api.js.map +1 -0
  18. package/dist/alert-manager.d.ts +77 -0
  19. package/dist/alert-manager.js +267 -0
  20. package/dist/alert-manager.js.map +1 -0
  21. package/dist/approval-api.d.ts +19 -0
  22. package/dist/approval-api.js +82 -0
  23. package/dist/approval-api.js.map +1 -0
  24. package/dist/approval-timeout.d.ts +29 -0
  25. package/dist/approval-timeout.js +45 -0
  26. package/dist/approval-timeout.js.map +1 -0
  27. package/dist/approval.d.ts +78 -0
  28. package/dist/approval.js +101 -0
  29. package/dist/approval.js.map +1 -0
  30. package/dist/auth.d.ts +47 -0
  31. package/dist/auth.js +335 -0
  32. package/dist/auth.js.map +1 -0
  33. package/dist/budget-api.d.ts +20 -0
  34. package/dist/budget-api.js +85 -0
  35. package/dist/budget-api.js.map +1 -0
  36. package/dist/budget-enforcer.d.ts +102 -0
  37. package/dist/budget-enforcer.js +294 -0
  38. package/dist/budget-enforcer.js.map +1 -0
  39. package/dist/cli.d.ts +15 -0
  40. package/dist/cli.js +200 -0
  41. package/dist/cli.js.map +1 -0
  42. package/dist/config.d.ts +15 -0
  43. package/dist/config.js +267 -0
  44. package/dist/config.js.map +1 -0
  45. package/dist/cost-aggregator.d.ts +69 -0
  46. package/dist/cost-aggregator.js +305 -0
  47. package/dist/cost-aggregator.js.map +1 -0
  48. package/dist/cost-api.d.ts +29 -0
  49. package/dist/cost-api.js +128 -0
  50. package/dist/cost-api.js.map +1 -0
  51. package/dist/database-url.d.ts +6 -0
  52. package/dist/database-url.js +47 -0
  53. package/dist/database-url.js.map +1 -0
  54. package/dist/db-retention.d.ts +53 -0
  55. package/dist/db-retention.js +82 -0
  56. package/dist/db-retention.js.map +1 -0
  57. package/dist/db-schema.d.ts +17 -0
  58. package/dist/db-schema.js +167 -0
  59. package/dist/db-schema.js.map +1 -0
  60. package/dist/db-writer.d.ts +55 -0
  61. package/dist/db-writer.js +115 -0
  62. package/dist/db-writer.js.map +1 -0
  63. package/dist/db.d.ts +33 -0
  64. package/dist/db.js +78 -0
  65. package/dist/db.js.map +1 -0
  66. package/dist/events.d.ts +77 -0
  67. package/dist/events.js +12 -0
  68. package/dist/events.js.map +1 -0
  69. package/dist/health.d.ts +14 -0
  70. package/dist/health.js +49 -0
  71. package/dist/health.js.map +1 -0
  72. package/dist/index.d.ts +7 -0
  73. package/dist/index.js +14 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/init-wizard.d.ts +12 -0
  76. package/dist/init-wizard.js +206 -0
  77. package/dist/init-wizard.js.map +1 -0
  78. package/dist/log-api.d.ts +20 -0
  79. package/dist/log-api.js +371 -0
  80. package/dist/log-api.js.map +1 -0
  81. package/dist/log-rotator.d.ts +55 -0
  82. package/dist/log-rotator.js +157 -0
  83. package/dist/log-rotator.js.map +1 -0
  84. package/dist/loop-detector.d.ts +71 -0
  85. package/dist/loop-detector.js +122 -0
  86. package/dist/loop-detector.js.map +1 -0
  87. package/dist/persistence-types.d.ts +165 -0
  88. package/dist/persistence-types.js +2 -0
  89. package/dist/persistence-types.js.map +1 -0
  90. package/dist/persistence.d.ts +185 -0
  91. package/dist/persistence.js +785 -0
  92. package/dist/persistence.js.map +1 -0
  93. package/dist/policy-api.d.ts +25 -0
  94. package/dist/policy-api.js +347 -0
  95. package/dist/policy-api.js.map +1 -0
  96. package/dist/policy-engine.d.ts +76 -0
  97. package/dist/policy-engine.js +835 -0
  98. package/dist/policy-engine.js.map +1 -0
  99. package/dist/policy-file.d.ts +10 -0
  100. package/dist/policy-file.js +52 -0
  101. package/dist/policy-file.js.map +1 -0
  102. package/dist/policy-parser.d.ts +21 -0
  103. package/dist/policy-parser.js +560 -0
  104. package/dist/policy-parser.js.map +1 -0
  105. package/dist/policy-types.d.ts +216 -0
  106. package/dist/policy-types.js +8 -0
  107. package/dist/policy-types.js.map +1 -0
  108. package/dist/policy-watcher.d.ts +54 -0
  109. package/dist/policy-watcher.js +116 -0
  110. package/dist/policy-watcher.js.map +1 -0
  111. package/dist/pricing.d.ts +69 -0
  112. package/dist/pricing.js +93 -0
  113. package/dist/pricing.js.map +1 -0
  114. package/dist/prompt.d.ts +6 -0
  115. package/dist/prompt.js +47 -0
  116. package/dist/prompt.js.map +1 -0
  117. package/dist/providers/anthropic.d.ts +18 -0
  118. package/dist/providers/anthropic.js +61 -0
  119. package/dist/providers/anthropic.js.map +1 -0
  120. package/dist/providers/custom.d.ts +19 -0
  121. package/dist/providers/custom.js +54 -0
  122. package/dist/providers/custom.js.map +1 -0
  123. package/dist/providers/openai.d.ts +17 -0
  124. package/dist/providers/openai.js +48 -0
  125. package/dist/providers/openai.js.map +1 -0
  126. package/dist/proxy.d.ts +57 -0
  127. package/dist/proxy.js +477 -0
  128. package/dist/proxy.js.map +1 -0
  129. package/dist/router.d.ts +23 -0
  130. package/dist/router.js +89 -0
  131. package/dist/router.js.map +1 -0
  132. package/dist/runtime.d.ts +1 -0
  133. package/dist/runtime.js +139 -0
  134. package/dist/runtime.js.map +1 -0
  135. package/dist/security.d.ts +64 -0
  136. package/dist/security.js +422 -0
  137. package/dist/security.js.map +1 -0
  138. package/dist/server.d.ts +33 -0
  139. package/dist/server.js +1147 -0
  140. package/dist/server.js.map +1 -0
  141. package/dist/sqlite-schema.d.ts +6 -0
  142. package/dist/sqlite-schema.js +134 -0
  143. package/dist/sqlite-schema.js.map +1 -0
  144. package/dist/streaming.d.ts +24 -0
  145. package/dist/streaming.js +63 -0
  146. package/dist/streaming.js.map +1 -0
  147. package/dist/tokens.d.ts +45 -0
  148. package/dist/tokens.js +237 -0
  149. package/dist/tokens.js.map +1 -0
  150. package/dist/types.d.ts +344 -0
  151. package/dist/types.js +5 -0
  152. package/dist/types.js.map +1 -0
  153. package/package.json +66 -2
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Loop detection for the Govyn proxy server.
3
+ *
4
+ * Detects agents making repeated identical requests (same endpoint + same body hash)
5
+ * within a configurable time window. When the threshold is exceeded, the agent is
6
+ * flagged as looping and can be blocked via BudgetEnforcer.blockAgent().
7
+ *
8
+ * Sliding window implementation: per-agent, per-request-key array of timestamps.
9
+ * On each recordRequest, prune timestamps outside the window, then check count >= threshold.
10
+ */
11
+ import type { AgentConfig, LoopDetectionConfig } from './types.js';
12
+ /**
13
+ * LoopDetector tracks repeated identical requests per agent.
14
+ *
15
+ * Each "identical request" is identified by the combination of:
16
+ * - The upstream endpoint path
17
+ * - A hash of the request body (first 16 hex chars of SHA-256)
18
+ *
19
+ * When the same agent hits the same endpoint+bodyHash N times within M seconds,
20
+ * isLooping() returns true. Thresholds are configurable per agent.
21
+ */
22
+ export declare class LoopDetector {
23
+ private defaultConfig;
24
+ private agentConfigs;
25
+ /** Map of agentId -> array of request windows (one per unique endpoint+bodyHash) */
26
+ private windows;
27
+ constructor(defaultConfig: LoopDetectionConfig, agentConfigs: Map<string, AgentConfig>);
28
+ /**
29
+ * Compute a compact hash of the request body for identity comparison.
30
+ *
31
+ * Uses SHA-256 truncated to 16 hex chars (64-bit) — sufficient for loop detection
32
+ * (collision probability negligible for this use case).
33
+ *
34
+ * @param body - The raw request body buffer
35
+ * @returns 16-character hex string
36
+ */
37
+ getRequestHash(body: Buffer): string;
38
+ /**
39
+ * Record a request from an agent to a specific endpoint with the given body hash.
40
+ * Prunes timestamps outside the current window for this agent+key combination.
41
+ *
42
+ * @param agentId - The agent making the request
43
+ * @param endpoint - The upstream endpoint path
44
+ * @param bodyHash - Hash of the request body (from getRequestHash)
45
+ */
46
+ recordRequest(agentId: string, endpoint: string, bodyHash: string): void;
47
+ /**
48
+ * Check if the given agent is currently looping based on the threshold.
49
+ * Must be called AFTER recordRequest for the same request.
50
+ *
51
+ * @param agentId - The agent to check
52
+ * @param endpoint - The upstream endpoint path
53
+ * @param bodyHash - Hash of the request body (from getRequestHash)
54
+ * @returns true if the agent has exceeded the loop detection threshold
55
+ */
56
+ isLooping(agentId: string, endpoint: string, bodyHash: string): boolean;
57
+ /**
58
+ * Get the loop detection config for a specific agent.
59
+ * Returns the agent's per-agent config if available, otherwise the default.
60
+ *
61
+ * @param agentId - The agent to look up
62
+ * @returns LoopDetectionConfig to use for this agent
63
+ */
64
+ getAgentConfig(agentId: string): LoopDetectionConfig;
65
+ /**
66
+ * Clear loop detection state for one or all agents.
67
+ *
68
+ * @param agentId - If provided, clears only this agent's data. Otherwise clears all.
69
+ */
70
+ clear(agentId?: string): void;
71
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Loop detection for the Govyn proxy server.
3
+ *
4
+ * Detects agents making repeated identical requests (same endpoint + same body hash)
5
+ * within a configurable time window. When the threshold is exceeded, the agent is
6
+ * flagged as looping and can be blocked via BudgetEnforcer.blockAgent().
7
+ *
8
+ * Sliding window implementation: per-agent, per-request-key array of timestamps.
9
+ * On each recordRequest, prune timestamps outside the window, then check count >= threshold.
10
+ */
11
+ import * as crypto from 'node:crypto';
12
+ /**
13
+ * LoopDetector tracks repeated identical requests per agent.
14
+ *
15
+ * Each "identical request" is identified by the combination of:
16
+ * - The upstream endpoint path
17
+ * - A hash of the request body (first 16 hex chars of SHA-256)
18
+ *
19
+ * When the same agent hits the same endpoint+bodyHash N times within M seconds,
20
+ * isLooping() returns true. Thresholds are configurable per agent.
21
+ */
22
+ export class LoopDetector {
23
+ defaultConfig;
24
+ agentConfigs;
25
+ /** Map of agentId -> array of request windows (one per unique endpoint+bodyHash) */
26
+ windows;
27
+ constructor(defaultConfig, agentConfigs) {
28
+ this.defaultConfig = defaultConfig;
29
+ this.agentConfigs = agentConfigs;
30
+ this.windows = new Map();
31
+ }
32
+ /**
33
+ * Compute a compact hash of the request body for identity comparison.
34
+ *
35
+ * Uses SHA-256 truncated to 16 hex chars (64-bit) — sufficient for loop detection
36
+ * (collision probability negligible for this use case).
37
+ *
38
+ * @param body - The raw request body buffer
39
+ * @returns 16-character hex string
40
+ */
41
+ getRequestHash(body) {
42
+ return crypto.createHash('sha256').update(body).digest('hex').slice(0, 16);
43
+ }
44
+ /**
45
+ * Record a request from an agent to a specific endpoint with the given body hash.
46
+ * Prunes timestamps outside the current window for this agent+key combination.
47
+ *
48
+ * @param agentId - The agent making the request
49
+ * @param endpoint - The upstream endpoint path
50
+ * @param bodyHash - Hash of the request body (from getRequestHash)
51
+ */
52
+ recordRequest(agentId, endpoint, bodyHash) {
53
+ const config = this.getAgentConfig(agentId);
54
+ const windowMs = config.windowSeconds * 1000;
55
+ const now = Date.now();
56
+ const key = `${endpoint}:${bodyHash}`;
57
+ // Get or create windows array for this agent
58
+ if (!this.windows.has(agentId)) {
59
+ this.windows.set(agentId, []);
60
+ }
61
+ const agentWindows = this.windows.get(agentId);
62
+ // Find existing window for this key or create a new one
63
+ let window = agentWindows.find((w) => w.key === key);
64
+ if (!window) {
65
+ window = { key, timestamps: [] };
66
+ agentWindows.push(window);
67
+ }
68
+ // Add current timestamp
69
+ window.timestamps.push(now);
70
+ // Prune timestamps outside the window
71
+ const cutoff = now - windowMs;
72
+ window.timestamps = window.timestamps.filter((ts) => ts > cutoff);
73
+ }
74
+ /**
75
+ * Check if the given agent is currently looping based on the threshold.
76
+ * Must be called AFTER recordRequest for the same request.
77
+ *
78
+ * @param agentId - The agent to check
79
+ * @param endpoint - The upstream endpoint path
80
+ * @param bodyHash - Hash of the request body (from getRequestHash)
81
+ * @returns true if the agent has exceeded the loop detection threshold
82
+ */
83
+ isLooping(agentId, endpoint, bodyHash) {
84
+ const config = this.getAgentConfig(agentId);
85
+ const key = `${endpoint}:${bodyHash}`;
86
+ const agentWindows = this.windows.get(agentId);
87
+ if (!agentWindows)
88
+ return false;
89
+ const window = agentWindows.find((w) => w.key === key);
90
+ if (!window)
91
+ return false;
92
+ return window.timestamps.length >= config.threshold;
93
+ }
94
+ /**
95
+ * Get the loop detection config for a specific agent.
96
+ * Returns the agent's per-agent config if available, otherwise the default.
97
+ *
98
+ * @param agentId - The agent to look up
99
+ * @returns LoopDetectionConfig to use for this agent
100
+ */
101
+ getAgentConfig(agentId) {
102
+ const agentConfig = this.agentConfigs.get(agentId);
103
+ if (agentConfig?.loopDetection) {
104
+ return agentConfig.loopDetection;
105
+ }
106
+ return this.defaultConfig;
107
+ }
108
+ /**
109
+ * Clear loop detection state for one or all agents.
110
+ *
111
+ * @param agentId - If provided, clears only this agent's data. Otherwise clears all.
112
+ */
113
+ clear(agentId) {
114
+ if (agentId !== undefined) {
115
+ this.windows.delete(agentId);
116
+ }
117
+ else {
118
+ this.windows.clear();
119
+ }
120
+ }
121
+ }
122
+ //# sourceMappingURL=loop-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop-detector.js","sourceRoot":"","sources":["../src/loop-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAWtC;;;;;;;;;GASG;AACH,MAAM,OAAO,YAAY;IACf,aAAa,CAAsB;IACnC,YAAY,CAA2B;IAE/C,oFAAoF;IAC5E,OAAO,CAA+B;IAE9C,YAAY,aAAkC,EAAE,YAAsC;QACpF,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC;IAC3B,CAAC;IAED;;;;;;;;OAQG;IACH,cAAc,CAAC,IAAY;QACzB,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC7E,CAAC;IAED;;;;;;;OAOG;IACH,aAAa,CAAC,OAAe,EAAE,QAAgB,EAAE,QAAgB;QAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,QAAQ,GAAG,MAAM,CAAC,aAAa,GAAG,IAAI,CAAC;QAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,GAAG,QAAQ,IAAI,QAAQ,EAAE,CAAC;QAEtC,6CAA6C;QAC7C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAChC,CAAC;QACD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC;QAEhD,wDAAwD;QACxD,IAAI,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;QACrD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;YACjC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QAED,wBAAwB;QACxB,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAE5B,sCAAsC;QACtC,MAAM,MAAM,GAAG,GAAG,GAAG,QAAQ,CAAC;QAC9B,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC;IACpE,CAAC;IAED;;;;;;;;OAQG;IACH,SAAS,CAAC,OAAe,EAAE,QAAgB,EAAE,QAAgB;QAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,GAAG,QAAQ,IAAI,QAAQ,EAAE,CAAC;QAEtC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC/C,IAAI,CAAC,YAAY;YAAE,OAAO,KAAK,CAAC;QAEhC,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAE1B,OAAO,MAAM,CAAC,UAAU,CAAC,MAAM,IAAI,MAAM,CAAC,SAAS,CAAC;IACtD,CAAC;IAED;;;;;;OAMG;IACH,cAAc,CAAC,OAAe;QAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACnD,IAAI,WAAW,EAAE,aAAa,EAAE,CAAC;YAC/B,OAAO,WAAW,CAAC,aAAa,CAAC;QACnC,CAAC;QACD,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAgB;QACpB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,165 @@
1
+ import type { CostRecord } from './types.js';
2
+ export interface PolicyEvaluationRecord {
3
+ agentId: string;
4
+ provider: string;
5
+ path: string;
6
+ allowed: boolean;
7
+ evaluatedCount: number;
8
+ matchedCount: number;
9
+ deniedBy?: string;
10
+ deniedReason?: string;
11
+ evaluationTimeMs?: number;
12
+ }
13
+ export interface ApprovalStatusRecord {
14
+ id: string;
15
+ status: 'pending' | 'approved' | 'denied' | 'denied_timeout';
16
+ approvalToken?: string;
17
+ decidedAt?: string;
18
+ expiresAt: string;
19
+ }
20
+ export interface ApprovalListEntry {
21
+ id: string;
22
+ agent_id: string;
23
+ provider: string;
24
+ model: string | null;
25
+ target_path: string;
26
+ policy_name: string;
27
+ estimated_cost: number | null;
28
+ request_summary: string | null;
29
+ status: 'pending' | 'approved' | 'denied' | 'denied_timeout';
30
+ decided_by: string | null;
31
+ decision_notes: string | null;
32
+ decided_at: string | null;
33
+ expires_at: string;
34
+ created_at: string;
35
+ }
36
+ export interface ApprovalListResult {
37
+ approvals: ApprovalListEntry[];
38
+ total: number;
39
+ limit: number;
40
+ offset: number;
41
+ }
42
+ export interface ApprovalStore {
43
+ createApprovalRequest(params: {
44
+ agentId: string;
45
+ provider: string;
46
+ model?: string;
47
+ targetPath: string;
48
+ policyName: string;
49
+ policyRule?: string;
50
+ estimatedCost?: number;
51
+ requestSummary: string;
52
+ requestHash: string;
53
+ requestPayload?: unknown;
54
+ timeoutSeconds: number;
55
+ }): Promise<{
56
+ id: string;
57
+ pollingUrl: string;
58
+ expiresAt: string;
59
+ }>;
60
+ getApprovalStatus(id: string): Promise<ApprovalStatusRecord | null>;
61
+ validateAndConsumeToken(token: string, expected: {
62
+ agentId: string;
63
+ targetPath: string;
64
+ requestHash: string;
65
+ }): Promise<{
66
+ policyName: string;
67
+ } | null>;
68
+ approveRequest(id: string, decidedBy: string, notes?: string): Promise<boolean>;
69
+ denyRequest(id: string, decidedBy: string, notes?: string): Promise<boolean>;
70
+ listApprovals(statusFilters: string[], limit: number, offset: number, agentId: string | null): Promise<ApprovalListResult>;
71
+ expireTimedOutApprovals(now?: Date): Promise<number>;
72
+ }
73
+ export interface BudgetThresholdConfig {
74
+ agent_id: string;
75
+ metric: 'daily' | 'monthly';
76
+ threshold_percent: number;
77
+ }
78
+ export interface PolicyTriggerConfig {
79
+ policy_name: string;
80
+ agent_id: string;
81
+ }
82
+ export interface AlertRuleRecord {
83
+ id: string;
84
+ name: string;
85
+ type: 'budget_threshold' | 'policy_trigger';
86
+ enabled: boolean;
87
+ config: BudgetThresholdConfig | PolicyTriggerConfig;
88
+ webhookUrl: string;
89
+ cooldownMinutes: number;
90
+ lastFiredAt: Date | null;
91
+ createdAt: Date;
92
+ updatedAt: Date;
93
+ }
94
+ export interface AlertHistoryEntry {
95
+ id: string;
96
+ rule_id: string;
97
+ rule_name: string;
98
+ rule_type: string;
99
+ event_type: string;
100
+ event_payload: Record<string, unknown>;
101
+ webhook_url: string;
102
+ webhook_status: number | null;
103
+ webhook_error: string | null;
104
+ fired_at: string;
105
+ }
106
+ export interface AlertHistoryResult {
107
+ alerts: AlertHistoryEntry[];
108
+ total: number;
109
+ limit: number;
110
+ offset: number;
111
+ }
112
+ export interface AlertStore {
113
+ listRules(): Promise<AlertRuleRecord[]>;
114
+ createRule(input: {
115
+ name: string;
116
+ type: 'budget_threshold' | 'policy_trigger';
117
+ enabled: boolean;
118
+ config: BudgetThresholdConfig | PolicyTriggerConfig;
119
+ webhookUrl: string;
120
+ cooldownMinutes: number;
121
+ }): Promise<AlertRuleRecord>;
122
+ updateRule(input: {
123
+ id: string;
124
+ name?: string;
125
+ enabled?: boolean;
126
+ config?: BudgetThresholdConfig | PolicyTriggerConfig;
127
+ webhookUrl?: string;
128
+ cooldownMinutes?: number;
129
+ }): Promise<AlertRuleRecord | null>;
130
+ deleteRule(id: string): Promise<boolean>;
131
+ listHistory(limit: number, offset: number, ruleId?: string | null): Promise<AlertHistoryResult>;
132
+ recordAlertHistory(input: {
133
+ ruleId: string;
134
+ ruleName: string;
135
+ ruleType: string;
136
+ eventType: string;
137
+ eventPayload: Record<string, unknown>;
138
+ webhookUrl: string;
139
+ webhookStatus: number | null;
140
+ webhookError: string | null;
141
+ }): Promise<void>;
142
+ touchRuleLastFired(id: string, firedAt: Date): Promise<void>;
143
+ }
144
+ export interface PersistenceWriterStore {
145
+ insertCostRecord(record: CostRecord): Promise<void>;
146
+ insertPolicyEvaluation(record: PolicyEvaluationRecord): Promise<void>;
147
+ insertApprovalEvent(event: {
148
+ requestId: string;
149
+ action: 'created' | 'approved' | 'denied' | 'denied_timeout' | 'token_consumed';
150
+ decidedBy?: string;
151
+ notes?: string;
152
+ }): Promise<void>;
153
+ ping(): Promise<void>;
154
+ }
155
+ export interface RetentionStore {
156
+ cleanupCostRecords(cutoff: Date): Promise<{
157
+ aggregated: number;
158
+ deleted: number;
159
+ }>;
160
+ cleanupPolicyEvaluations(cutoff: Date): Promise<number>;
161
+ cleanupApprovalRecords(cutoff: Date): Promise<number>;
162
+ }
163
+ export interface PersistenceCloser {
164
+ close(): Promise<void>;
165
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=persistence-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence-types.js","sourceRoot":"","sources":["../src/persistence-types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,185 @@
1
+ import type Database from 'better-sqlite3';
2
+ import postgres from 'postgres';
3
+ import type { AlertHistoryResult, AlertRuleRecord, ApprovalListResult, ApprovalStatusRecord, ApprovalStore, AlertStore, PersistenceCloser, PersistenceWriterStore, PolicyEvaluationRecord, RetentionStore } from './persistence-types.js';
4
+ import type { CostRecord, DatabaseConfig } from './types.js';
5
+ type AlertRuleType = 'budget_threshold' | 'policy_trigger';
6
+ export interface PersistenceBackend extends ApprovalStore, AlertStore, PersistenceWriterStore, RetentionStore, PersistenceCloser {
7
+ readonly kind: 'sqlite' | 'postgres';
8
+ readonly url: string;
9
+ }
10
+ export declare function isPostgresSql(value: unknown): value is postgres.Sql;
11
+ export declare class PostgresPersistence implements PersistenceBackend {
12
+ private readonly sql;
13
+ readonly url: string;
14
+ private readonly retentionDays;
15
+ private readonly approvalRetentionDays;
16
+ readonly kind: "postgres";
17
+ constructor(sql: postgres.Sql, url: string, retentionDays: number, approvalRetentionDays: number);
18
+ static connect(config: DatabaseConfig): Promise<PostgresPersistence>;
19
+ close(): Promise<void>;
20
+ ping(): Promise<void>;
21
+ insertCostRecord(record: CostRecord): Promise<void>;
22
+ insertPolicyEvaluation(record: PolicyEvaluationRecord): Promise<void>;
23
+ insertApprovalEvent(event: {
24
+ requestId: string;
25
+ action: 'created' | 'approved' | 'denied' | 'denied_timeout' | 'token_consumed';
26
+ decidedBy?: string;
27
+ notes?: string;
28
+ }): Promise<void>;
29
+ createApprovalRequest(params: {
30
+ agentId: string;
31
+ provider: string;
32
+ model?: string;
33
+ targetPath: string;
34
+ policyName: string;
35
+ policyRule?: string;
36
+ estimatedCost?: number;
37
+ requestSummary: string;
38
+ requestHash: string;
39
+ requestPayload?: unknown;
40
+ timeoutSeconds: number;
41
+ }): Promise<{
42
+ id: string;
43
+ pollingUrl: string;
44
+ expiresAt: string;
45
+ }>;
46
+ getApprovalStatus(id: string): Promise<ApprovalStatusRecord | null>;
47
+ validateAndConsumeToken(token: string, expected: {
48
+ agentId: string;
49
+ targetPath: string;
50
+ requestHash: string;
51
+ }): Promise<{
52
+ policyName: string;
53
+ } | null>;
54
+ approveRequest(id: string, decidedBy: string, notes?: string): Promise<boolean>;
55
+ denyRequest(id: string, decidedBy: string, notes?: string): Promise<boolean>;
56
+ listApprovals(statusFilters: string[], limit: number, offset: number, agentId: string | null): Promise<ApprovalListResult>;
57
+ expireTimedOutApprovals(now?: Date): Promise<number>;
58
+ listRules(): Promise<AlertRuleRecord[]>;
59
+ createRule(input: {
60
+ name: string;
61
+ type: AlertRuleType;
62
+ enabled: boolean;
63
+ config: AlertRuleRecord['config'];
64
+ webhookUrl: string;
65
+ cooldownMinutes: number;
66
+ }): Promise<AlertRuleRecord>;
67
+ updateRule(input: {
68
+ id: string;
69
+ name?: string;
70
+ enabled?: boolean;
71
+ config?: AlertRuleRecord['config'];
72
+ webhookUrl?: string;
73
+ cooldownMinutes?: number;
74
+ }): Promise<AlertRuleRecord | null>;
75
+ deleteRule(id: string): Promise<boolean>;
76
+ listHistory(limit: number, offset: number, ruleId?: string | null): Promise<AlertHistoryResult>;
77
+ recordAlertHistory(input: {
78
+ ruleId: string;
79
+ ruleName: string;
80
+ ruleType: string;
81
+ eventType: string;
82
+ eventPayload: Record<string, unknown>;
83
+ webhookUrl: string;
84
+ webhookStatus: number | null;
85
+ webhookError: string | null;
86
+ }): Promise<void>;
87
+ touchRuleLastFired(id: string, firedAt: Date): Promise<void>;
88
+ cleanupCostRecords(cutoff: Date): Promise<{
89
+ aggregated: number;
90
+ deleted: number;
91
+ }>;
92
+ cleanupPolicyEvaluations(cutoff: Date): Promise<number>;
93
+ cleanupApprovalRecords(cutoff: Date): Promise<number>;
94
+ }
95
+ export declare class SqlitePersistence implements PersistenceBackend {
96
+ private readonly db;
97
+ readonly url: string;
98
+ private readonly retentionDays;
99
+ private readonly approvalRetentionDays;
100
+ readonly kind: "sqlite";
101
+ constructor(db: Database.Database, url: string, retentionDays: number, approvalRetentionDays: number);
102
+ static connect(config: DatabaseConfig): Promise<SqlitePersistence>;
103
+ close(): Promise<void>;
104
+ ping(): Promise<void>;
105
+ insertCostRecord(record: CostRecord): Promise<void>;
106
+ insertPolicyEvaluation(record: PolicyEvaluationRecord): Promise<void>;
107
+ insertApprovalEvent(event: {
108
+ requestId: string;
109
+ action: 'created' | 'approved' | 'denied' | 'denied_timeout' | 'token_consumed';
110
+ decidedBy?: string;
111
+ notes?: string;
112
+ }): Promise<void>;
113
+ createApprovalRequest(params: {
114
+ agentId: string;
115
+ provider: string;
116
+ model?: string;
117
+ targetPath: string;
118
+ policyName: string;
119
+ policyRule?: string;
120
+ estimatedCost?: number;
121
+ requestSummary: string;
122
+ requestHash: string;
123
+ requestPayload?: unknown;
124
+ timeoutSeconds: number;
125
+ }): Promise<{
126
+ id: string;
127
+ pollingUrl: string;
128
+ expiresAt: string;
129
+ }>;
130
+ getApprovalStatus(id: string): Promise<ApprovalStatusRecord | null>;
131
+ validateAndConsumeToken(token: string, expected: {
132
+ agentId: string;
133
+ targetPath: string;
134
+ requestHash: string;
135
+ }): Promise<{
136
+ policyName: string;
137
+ } | null>;
138
+ approveRequest(id: string, decidedBy: string, notes?: string): Promise<boolean>;
139
+ denyRequest(id: string, decidedBy: string, notes?: string): Promise<boolean>;
140
+ listApprovals(statusFilters: string[], limit: number, offset: number, agentId: string | null): Promise<ApprovalListResult>;
141
+ expireTimedOutApprovals(now?: Date): Promise<number>;
142
+ listRules(): Promise<AlertRuleRecord[]>;
143
+ createRule(input: {
144
+ name: string;
145
+ type: AlertRuleType;
146
+ enabled: boolean;
147
+ config: AlertRuleRecord['config'];
148
+ webhookUrl: string;
149
+ cooldownMinutes: number;
150
+ }): Promise<AlertRuleRecord>;
151
+ updateRule(input: {
152
+ id: string;
153
+ name?: string;
154
+ enabled?: boolean;
155
+ config?: AlertRuleRecord['config'];
156
+ webhookUrl?: string;
157
+ cooldownMinutes?: number;
158
+ }): Promise<AlertRuleRecord | null>;
159
+ deleteRule(id: string): Promise<boolean>;
160
+ listHistory(limit: number, offset: number, ruleId?: string | null): Promise<AlertHistoryResult>;
161
+ recordAlertHistory(input: {
162
+ ruleId: string;
163
+ ruleName: string;
164
+ ruleType: string;
165
+ eventType: string;
166
+ eventPayload: Record<string, unknown>;
167
+ webhookUrl: string;
168
+ webhookStatus: number | null;
169
+ webhookError: string | null;
170
+ }): Promise<void>;
171
+ touchRuleLastFired(id: string, firedAt: Date): Promise<void>;
172
+ cleanupCostRecords(cutoff: Date): Promise<{
173
+ aggregated: number;
174
+ deleted: number;
175
+ }>;
176
+ cleanupPolicyEvaluations(cutoff: Date): Promise<number>;
177
+ cleanupApprovalRecords(cutoff: Date): Promise<number>;
178
+ private getRuleById;
179
+ }
180
+ export declare function createPersistenceBackend(config: DatabaseConfig): Promise<PersistenceBackend>;
181
+ export declare function adaptApprovalStore(input: ApprovalStore | postgres.Sql): ApprovalStore;
182
+ export declare function adaptAlertStore(input: AlertStore | postgres.Sql): AlertStore;
183
+ export declare function adaptWriterStore(input: PersistenceWriterStore | postgres.Sql): PersistenceWriterStore;
184
+ export declare function adaptRetentionStore(input: RetentionStore | postgres.Sql, retentionDays: number, approvalRetentionDays: number): RetentionStore;
185
+ export {};