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,267 @@
1
+ /**
2
+ * Alert manager for the Govyn proxy.
3
+ *
4
+ * Subscribes to govynEvents and evaluates incoming events against configured
5
+ * alert rules. When a rule matches, delivers a webhook POST to the configured
6
+ * URL and records the alert in the alert_history table.
7
+ *
8
+ * Supports two rule types:
9
+ * - budget_threshold: fires on budget_warning/budget_exceeded events
10
+ * - policy_trigger: fires on policy_denied events
11
+ */
12
+ import { govynEvents } from './events.js';
13
+ import { deliverWebhookJson, resolveWebhookTarget } from './security.js';
14
+ import { adaptAlertStore } from './persistence.js';
15
+ export class AlertManager {
16
+ store;
17
+ rules = [];
18
+ cooldownCache = new Map();
19
+ eventHandler = null;
20
+ constructor(storeOrSql) {
21
+ this.store = adaptAlertStore(storeOrSql);
22
+ }
23
+ /**
24
+ * Load rules from DB and subscribe to govynEvents.
25
+ */
26
+ async start() {
27
+ await this.reloadRules();
28
+ this.eventHandler = (event) => {
29
+ this.handleEvent(event).catch((err) => {
30
+ const msg = err instanceof Error ? err.message : String(err);
31
+ console.error('[alert-manager] Error handling event:', msg);
32
+ });
33
+ };
34
+ govynEvents.on('event', this.eventHandler);
35
+ }
36
+ /**
37
+ * Unsubscribe from events.
38
+ */
39
+ stop() {
40
+ if (this.eventHandler) {
41
+ govynEvents.removeListener('event', this.eventHandler);
42
+ this.eventHandler = null;
43
+ }
44
+ }
45
+ /**
46
+ * Reload all rules from the database.
47
+ */
48
+ async reloadRules() {
49
+ this.rules = await this.store.listRules();
50
+ // Populate cooldown cache from loaded rules
51
+ for (const rule of this.rules) {
52
+ if (rule.lastFiredAt) {
53
+ this.cooldownCache.set(rule.id, rule.lastFiredAt);
54
+ }
55
+ }
56
+ }
57
+ /**
58
+ * Handle an incoming event by evaluating all enabled rules.
59
+ */
60
+ async handleEvent(event) {
61
+ for (const rule of this.rules) {
62
+ if (!rule.enabled)
63
+ continue;
64
+ if (this.isInCooldown(rule))
65
+ continue;
66
+ if (this.evaluateRule(rule, event)) {
67
+ await this.fireAlert(rule, event);
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * Evaluate whether a rule matches the given event.
73
+ */
74
+ evaluateRule(rule, event) {
75
+ if (rule.type === 'budget_threshold') {
76
+ return this.evaluateBudgetThreshold(rule, event);
77
+ }
78
+ if (rule.type === 'policy_trigger') {
79
+ return this.evaluatePolicyTrigger(rule, event);
80
+ }
81
+ return false;
82
+ }
83
+ /**
84
+ * Check if a rule is within its cooldown window.
85
+ */
86
+ isInCooldown(rule) {
87
+ const lastFired = this.cooldownCache.get(rule.id) ?? rule.lastFiredAt;
88
+ if (!lastFired)
89
+ return false;
90
+ const cooldownMs = rule.cooldownMinutes * 60 * 1000;
91
+ const cooldownEndsAt = new Date(lastFired.getTime() + cooldownMs);
92
+ return Date.now() < cooldownEndsAt.getTime();
93
+ }
94
+ /**
95
+ * Fire an alert: deliver webhook, record history, update cooldown.
96
+ */
97
+ async fireAlert(rule, event) {
98
+ const firedAt = new Date().toISOString();
99
+ let webhookStatus = null;
100
+ let webhookError = null;
101
+ // Build webhook payload
102
+ const payload = {
103
+ alert: {
104
+ rule_id: rule.id,
105
+ rule_name: rule.name,
106
+ rule_type: rule.type,
107
+ },
108
+ event: this.serializeEvent(event),
109
+ fired_at: firedAt,
110
+ source: 'govyn',
111
+ };
112
+ // Send webhook
113
+ const webhookTarget = await resolveWebhookTarget(rule.webhookUrl);
114
+ if (!webhookTarget.ok) {
115
+ webhookError = webhookTarget.error;
116
+ }
117
+ else {
118
+ try {
119
+ const response = await deliverWebhookJson(webhookTarget.target, payload);
120
+ webhookStatus = response.status;
121
+ }
122
+ catch (err) {
123
+ webhookError = err instanceof Error ? err.message : String(err);
124
+ }
125
+ }
126
+ // Record in alert_history (even on webhook failure)
127
+ try {
128
+ await this.store.recordAlertHistory({
129
+ ruleId: rule.id,
130
+ ruleName: rule.name,
131
+ ruleType: rule.type,
132
+ eventType: event.type,
133
+ eventPayload: this.serializeEvent(event),
134
+ webhookUrl: rule.webhookUrl,
135
+ webhookStatus,
136
+ webhookError,
137
+ });
138
+ }
139
+ catch (err) {
140
+ const msg = err instanceof Error ? err.message : String(err);
141
+ console.error('[alert-manager] Failed to record alert history:', msg);
142
+ }
143
+ // Update last_fired_at
144
+ try {
145
+ await this.store.touchRuleLastFired(rule.id, new Date(firedAt));
146
+ }
147
+ catch (err) {
148
+ const msg = err instanceof Error ? err.message : String(err);
149
+ console.error('[alert-manager] Failed to update last_fired_at:', msg);
150
+ }
151
+ // Update in-memory cooldown cache
152
+ this.cooldownCache.set(rule.id, new Date());
153
+ // Emit alert_fired event
154
+ govynEvents.emit('event', {
155
+ type: 'alert_fired',
156
+ ruleId: rule.id,
157
+ ruleName: rule.name,
158
+ ruleType: rule.type,
159
+ webhookUrl: rule.webhookUrl,
160
+ webhookStatus,
161
+ });
162
+ }
163
+ async listRules() {
164
+ return this.store.listRules();
165
+ }
166
+ async createRule(input) {
167
+ const rule = await this.store.createRule(input);
168
+ await this.reloadRules();
169
+ return rule;
170
+ }
171
+ async updateRule(input) {
172
+ const rule = await this.store.updateRule(input);
173
+ if (rule) {
174
+ await this.reloadRules();
175
+ }
176
+ return rule;
177
+ }
178
+ async deleteRule(id) {
179
+ const deleted = await this.store.deleteRule(id);
180
+ if (deleted) {
181
+ await this.reloadRules();
182
+ }
183
+ return deleted;
184
+ }
185
+ async listHistory(limit, offset, ruleId) {
186
+ return this.store.listHistory(limit, offset, ruleId);
187
+ }
188
+ // ---- Private helpers ----
189
+ evaluateBudgetThreshold(rule, event) {
190
+ const config = rule.config;
191
+ if (event.type === 'budget_warning') {
192
+ // Check agent match
193
+ if (config.agent_id !== '*' && config.agent_id !== event.agentId)
194
+ return false;
195
+ // Check metric matches limitPeriod
196
+ if (config.metric !== event.limitPeriod)
197
+ return false;
198
+ // Check threshold
199
+ return event.percentUsed >= config.threshold_percent;
200
+ }
201
+ if (event.type === 'budget_exceeded') {
202
+ // Check agent match
203
+ if (config.agent_id !== '*' && config.agent_id !== event.agentId)
204
+ return false;
205
+ // budget_exceeded always exceeds any threshold
206
+ // Check metric by inspecting the code field
207
+ const isDaily = event.code.includes('daily');
208
+ if (config.metric === 'daily' && !isDaily)
209
+ return false;
210
+ if (config.metric === 'monthly' && isDaily)
211
+ return false;
212
+ return true;
213
+ }
214
+ return false;
215
+ }
216
+ evaluatePolicyTrigger(rule, event) {
217
+ if (event.type !== 'policy_denied')
218
+ return false;
219
+ const config = rule.config;
220
+ // Check agent match
221
+ if (config.agent_id !== '*' && config.agent_id !== event.agentId)
222
+ return false;
223
+ // Check policy name match
224
+ if (config.policy_name !== '*' && config.policy_name !== event.policyName)
225
+ return false;
226
+ return true;
227
+ }
228
+ /**
229
+ * Convert a GovynEvent to a plain object for webhook payload / DB storage.
230
+ * Converts camelCase event fields to snake_case for external consumption.
231
+ */
232
+ serializeEvent(event) {
233
+ const result = { type: event.type };
234
+ // Convert each event type's fields to snake_case
235
+ if (event.type === 'budget_warning') {
236
+ result.agent_id = event.agentId;
237
+ result.percent_used = event.percentUsed;
238
+ result.current_spend = event.currentSpend;
239
+ result.limit = event.limit;
240
+ result.resets_at = event.resetsAt;
241
+ result.limit_period = event.limitPeriod;
242
+ }
243
+ else if (event.type === 'budget_exceeded') {
244
+ result.agent_id = event.agentId;
245
+ result.code = event.code;
246
+ result.limit_amount = event.limitAmount;
247
+ result.current_spend = event.currentSpend;
248
+ result.reset_time = event.resetTime;
249
+ }
250
+ else if (event.type === 'policy_denied') {
251
+ result.agent_id = event.agentId;
252
+ result.provider = event.provider;
253
+ result.path = event.path;
254
+ result.policy_name = event.policyName;
255
+ result.policy_type = event.policyType;
256
+ result.reason = event.reason;
257
+ result.evaluation_time_ms = event.evaluationTimeMs;
258
+ result.allowed = event.allowed;
259
+ }
260
+ else {
261
+ // For other event types, spread all fields
262
+ Object.assign(result, event);
263
+ }
264
+ return result;
265
+ }
266
+ }
267
+ //# sourceMappingURL=alert-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alert-manager.js","sourceRoot":"","sources":["../src/alert-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Approval list API handler for the Govyn proxy.
3
+ *
4
+ * Handles GET /api/approvals — returns a paginated, filtered list of
5
+ * approval requests from the database. Complements the existing
6
+ * per-request approve/deny/poll endpoints in server.ts.
7
+ */
8
+ import type http from 'node:http';
9
+ import type { ApprovalManager } from './approval.js';
10
+ /**
11
+ * Handle GET /api/approvals with status filtering and pagination.
12
+ *
13
+ * Query parameters:
14
+ * - status: 'pending' | 'approved' | 'denied' | 'denied_timeout' | 'all' (default 'all'), supports comma-separated
15
+ * - limit: 1-200 (default 50)
16
+ * - offset: >= 0 (default 0)
17
+ * - agent_id: optional exact match filter
18
+ */
19
+ export declare function handleApprovalApi(req: http.IncomingMessage, res: http.ServerResponse, approvalManager: ApprovalManager): void;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Approval list API handler for the Govyn proxy.
3
+ *
4
+ * Handles GET /api/approvals — returns a paginated, filtered list of
5
+ * approval requests from the database. Complements the existing
6
+ * per-request approve/deny/poll endpoints in server.ts.
7
+ */
8
+ const VALID_STATUSES = new Set(['pending', 'approved', 'denied', 'denied_timeout']);
9
+ const DEFAULT_LIMIT = 50;
10
+ const MAX_LIMIT = 200;
11
+ /**
12
+ * Handle GET /api/approvals with status filtering and pagination.
13
+ *
14
+ * Query parameters:
15
+ * - status: 'pending' | 'approved' | 'denied' | 'denied_timeout' | 'all' (default 'all'), supports comma-separated
16
+ * - limit: 1-200 (default 50)
17
+ * - offset: >= 0 (default 0)
18
+ * - agent_id: optional exact match filter
19
+ */
20
+ export function handleApprovalApi(req, res, approvalManager) {
21
+ const parsed = new URL(req.url, 'http://localhost');
22
+ const params = parsed.searchParams;
23
+ // Parse and validate status filter
24
+ const statusParam = params.get('status') ?? 'all';
25
+ let statusFilters = [];
26
+ if (statusParam !== 'all') {
27
+ const parts = statusParam.split(',').map((s) => s.trim()).filter(Boolean);
28
+ for (const part of parts) {
29
+ if (!VALID_STATUSES.has(part)) {
30
+ sendError(res, 400, `Invalid status value: "${part}". Valid values: pending, approved, denied, denied_timeout, all`);
31
+ return;
32
+ }
33
+ }
34
+ statusFilters = parts;
35
+ }
36
+ // Parse and validate limit
37
+ const limitStr = params.get('limit');
38
+ let limit = DEFAULT_LIMIT;
39
+ if (limitStr !== null) {
40
+ limit = parseInt(limitStr, 10);
41
+ if (isNaN(limit) || limit < 1 || limit > MAX_LIMIT) {
42
+ sendError(res, 400, `Invalid limit: must be between 1 and ${MAX_LIMIT}`);
43
+ return;
44
+ }
45
+ }
46
+ // Parse and validate offset
47
+ const offsetStr = params.get('offset');
48
+ let offset = 0;
49
+ if (offsetStr !== null) {
50
+ offset = parseInt(offsetStr, 10);
51
+ if (isNaN(offset) || offset < 0) {
52
+ sendError(res, 400, 'Invalid offset: must be >= 0');
53
+ return;
54
+ }
55
+ }
56
+ // Optional agent_id filter
57
+ const agentId = params.get('agent_id');
58
+ // Build and execute query
59
+ approvalManager.listApprovals(statusFilters, limit, offset, agentId)
60
+ .then((result) => {
61
+ const body = JSON.stringify(result);
62
+ res.writeHead(200, {
63
+ 'content-type': 'application/json',
64
+ 'content-length': Buffer.byteLength(body).toString(),
65
+ });
66
+ res.end(body);
67
+ })
68
+ .catch((err) => {
69
+ const message = err instanceof Error ? err.message : 'Unknown error';
70
+ console.error('[approval-api] Database error:', message);
71
+ sendError(res, 500, 'Failed to fetch approvals');
72
+ });
73
+ }
74
+ function sendError(res, statusCode, message) {
75
+ const body = JSON.stringify({ error: { message, code: statusCode === 400 ? 'invalid_request' : 'internal_error' } });
76
+ res.writeHead(statusCode, {
77
+ 'content-type': 'application/json',
78
+ 'content-length': Buffer.byteLength(body).toString(),
79
+ });
80
+ res.end(body);
81
+ }
82
+ //# sourceMappingURL=approval-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-api.js","sourceRoot":"","sources":["../src/approval-api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Background job that auto-denies expired approval requests.
3
+ *
4
+ * Runs on a configurable interval (default 30 seconds) to mark pending
5
+ * approval requests that have passed their expires_at timestamp as
6
+ * 'denied_timeout'. This is distinct from 'denied' — agents can
7
+ * programmatically differentiate between human denial and timeout expiry.
8
+ */
9
+ import type postgres from 'postgres';
10
+ import type { ApprovalStore } from './persistence-types.js';
11
+ export declare class ApprovalTimeoutChecker {
12
+ private interval;
13
+ private readonly store;
14
+ constructor(storeOrSql: ApprovalStore | postgres.Sql);
15
+ /**
16
+ * Start checking for expired approvals every 30 seconds.
17
+ * The interval is unref'd so it doesn't keep the process alive.
18
+ */
19
+ start(): void;
20
+ /**
21
+ * Mark all expired pending approvals as denied_timeout.
22
+ * Returns the number of records updated.
23
+ */
24
+ expireTimedOut(): Promise<number>;
25
+ /**
26
+ * Stop the interval checker.
27
+ */
28
+ stop(): void;
29
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Background job that auto-denies expired approval requests.
3
+ *
4
+ * Runs on a configurable interval (default 30 seconds) to mark pending
5
+ * approval requests that have passed their expires_at timestamp as
6
+ * 'denied_timeout'. This is distinct from 'denied' — agents can
7
+ * programmatically differentiate between human denial and timeout expiry.
8
+ */
9
+ import { adaptApprovalStore } from './persistence.js';
10
+ export class ApprovalTimeoutChecker {
11
+ interval = null;
12
+ store;
13
+ constructor(storeOrSql) {
14
+ this.store = adaptApprovalStore(storeOrSql);
15
+ }
16
+ /**
17
+ * Start checking for expired approvals every 30 seconds.
18
+ * The interval is unref'd so it doesn't keep the process alive.
19
+ */
20
+ start() {
21
+ this.interval = setInterval(() => this.expireTimedOut(), 30_000);
22
+ this.interval.unref();
23
+ }
24
+ /**
25
+ * Mark all expired pending approvals as denied_timeout.
26
+ * Returns the number of records updated.
27
+ */
28
+ async expireTimedOut() {
29
+ const count = await this.store.expireTimedOutApprovals();
30
+ if (count > 0) {
31
+ console.log(`[govyn] Auto-denied ${count} expired approval request(s)`);
32
+ }
33
+ return count;
34
+ }
35
+ /**
36
+ * Stop the interval checker.
37
+ */
38
+ stop() {
39
+ if (this.interval) {
40
+ clearInterval(this.interval);
41
+ this.interval = null;
42
+ }
43
+ }
44
+ }
45
+ //# sourceMappingURL=approval-timeout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-timeout.js","sourceRoot":"","sources":["../src/approval-timeout.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,OAAO,sBAAsB;IACzB,QAAQ,GAA0C,IAAI,CAAC;IAC9C,KAAK,CAAgB;IAEtC,YAAY,UAAwC;QAClD,IAAI,CAAC,KAAK,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAC9C,CAAC;IAED;;;OAGG;IACH,KAAK;QACH,IAAI,CAAC,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,MAAM,CAAC,CAAC;QACjE,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,cAAc;QAClB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,uBAAuB,EAAE,CAAC;QACzD,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,8BAA8B,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;OAEG;IACH,IAAI;QACF,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC7B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACvB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Approval queue manager for the Govyn proxy.
3
+ *
4
+ * When a require_approval policy flags a request, the proxy creates an
5
+ * approval request in the database. Agents poll for the decision status.
6
+ * Once approved, the agent re-sends with a single-use approval token.
7
+ *
8
+ * Phase 14 (Approval Queue UI) will build a dashboard on top of these APIs.
9
+ * Until then, approvals can be resolved via direct DB updates or API calls.
10
+ */
11
+ import type postgres from 'postgres';
12
+ import type { ApprovalStore } from './persistence-types.js';
13
+ export declare function hashApprovalRequest(body: string | Buffer | undefined): string;
14
+ /**
15
+ * Generate a truncated summary of a request body for approval metadata.
16
+ * Extracts the first user message content or body content, truncated to maxLength chars.
17
+ */
18
+ export declare function generateRequestSummary(body: string | undefined, maxLength?: number): string;
19
+ export declare class ApprovalManager {
20
+ private readonly store;
21
+ constructor(storeOrSql: ApprovalStore | postgres.Sql);
22
+ /**
23
+ * Create a new approval request in the database.
24
+ * Returns the approval request ID, polling URL, and expiration time.
25
+ */
26
+ createApprovalRequest(params: {
27
+ agentId: string;
28
+ provider: string;
29
+ model?: string;
30
+ targetPath: string;
31
+ policyName: string;
32
+ policyRule?: string;
33
+ estimatedCost?: number;
34
+ requestSummary: string;
35
+ requestHash: string;
36
+ requestPayload?: unknown;
37
+ timeoutSeconds: number;
38
+ }): Promise<{
39
+ id: string;
40
+ pollingUrl: string;
41
+ expiresAt: string;
42
+ }>;
43
+ /**
44
+ * Get approval status for polling.
45
+ * Returns status and approval token (if approved), or null if not found.
46
+ */
47
+ getApprovalStatus(id: string): Promise<{
48
+ id: string;
49
+ status: 'pending' | 'approved' | 'denied' | 'denied_timeout';
50
+ approvalToken?: string;
51
+ decidedAt?: string;
52
+ expiresAt: string;
53
+ } | null>;
54
+ /**
55
+ * Validate an approval token for re-send flow.
56
+ * Atomically finds the token and marks it as used in one query.
57
+ * Returns the original request context if valid, null if invalid/expired/used.
58
+ */
59
+ validateAndConsumeToken(token: string, expected: {
60
+ agentId: string;
61
+ targetPath: string;
62
+ requestHash: string;
63
+ }): Promise<{
64
+ policyName: string;
65
+ } | null>;
66
+ /**
67
+ * Approve a request. Generates a single-use UUID approval token.
68
+ * Returns true if the request was pending and is now approved.
69
+ */
70
+ approveRequest(id: string, decidedBy: string, notes?: string): Promise<boolean>;
71
+ /**
72
+ * Deny a request. No token is generated.
73
+ * Returns true if the request was pending and is now denied.
74
+ */
75
+ denyRequest(id: string, decidedBy: string, notes?: string): Promise<boolean>;
76
+ listApprovals(statusFilters: string[], limit: number, offset: number, agentId: string | null): Promise<import("./persistence-types.js").ApprovalListResult>;
77
+ expireTimedOutApprovals(now?: Date): Promise<number>;
78
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Approval queue manager for the Govyn proxy.
3
+ *
4
+ * When a require_approval policy flags a request, the proxy creates an
5
+ * approval request in the database. Agents poll for the decision status.
6
+ * Once approved, the agent re-sends with a single-use approval token.
7
+ *
8
+ * Phase 14 (Approval Queue UI) will build a dashboard on top of these APIs.
9
+ * Until then, approvals can be resolved via direct DB updates or API calls.
10
+ */
11
+ import * as crypto from 'node:crypto';
12
+ import { adaptApprovalStore } from './persistence.js';
13
+ export function hashApprovalRequest(body) {
14
+ const data = Buffer.isBuffer(body)
15
+ ? body
16
+ : Buffer.from(body ?? '', 'utf8');
17
+ return crypto.createHash('sha256').update(data).digest('hex');
18
+ }
19
+ /**
20
+ * Generate a truncated summary of a request body for approval metadata.
21
+ * Extracts the first user message content or body content, truncated to maxLength chars.
22
+ */
23
+ export function generateRequestSummary(body, maxLength = 500) {
24
+ if (!body)
25
+ return '(empty request)';
26
+ try {
27
+ const parsed = JSON.parse(body);
28
+ // Try to extract user message content (OpenAI/Anthropic format)
29
+ if (Array.isArray(parsed.messages)) {
30
+ for (let i = parsed.messages.length - 1; i >= 0; i--) {
31
+ const msg = parsed.messages[i];
32
+ if (msg?.role === 'user' && typeof msg.content === 'string') {
33
+ const content = msg.content;
34
+ if (content.length <= maxLength)
35
+ return content;
36
+ return content.slice(0, maxLength) + '...';
37
+ }
38
+ }
39
+ }
40
+ // Fall back to stringified body
41
+ const str = JSON.stringify(parsed);
42
+ if (str.length <= maxLength)
43
+ return str;
44
+ return str.slice(0, maxLength) + '...';
45
+ }
46
+ catch {
47
+ // Non-JSON body — truncate raw text
48
+ if (body.length <= maxLength)
49
+ return body;
50
+ return body.slice(0, maxLength) + '...';
51
+ }
52
+ }
53
+ export class ApprovalManager {
54
+ store;
55
+ constructor(storeOrSql) {
56
+ this.store = adaptApprovalStore(storeOrSql);
57
+ }
58
+ /**
59
+ * Create a new approval request in the database.
60
+ * Returns the approval request ID, polling URL, and expiration time.
61
+ */
62
+ async createApprovalRequest(params) {
63
+ return this.store.createApprovalRequest(params);
64
+ }
65
+ /**
66
+ * Get approval status for polling.
67
+ * Returns status and approval token (if approved), or null if not found.
68
+ */
69
+ async getApprovalStatus(id) {
70
+ return this.store.getApprovalStatus(id);
71
+ }
72
+ /**
73
+ * Validate an approval token for re-send flow.
74
+ * Atomically finds the token and marks it as used in one query.
75
+ * Returns the original request context if valid, null if invalid/expired/used.
76
+ */
77
+ async validateAndConsumeToken(token, expected) {
78
+ return this.store.validateAndConsumeToken(token, expected);
79
+ }
80
+ /**
81
+ * Approve a request. Generates a single-use UUID approval token.
82
+ * Returns true if the request was pending and is now approved.
83
+ */
84
+ async approveRequest(id, decidedBy, notes) {
85
+ return this.store.approveRequest(id, decidedBy, notes);
86
+ }
87
+ /**
88
+ * Deny a request. No token is generated.
89
+ * Returns true if the request was pending and is now denied.
90
+ */
91
+ async denyRequest(id, decidedBy, notes) {
92
+ return this.store.denyRequest(id, decidedBy, notes);
93
+ }
94
+ async listApprovals(statusFilters, limit, offset, agentId) {
95
+ return this.store.listApprovals(statusFilters, limit, offset, agentId);
96
+ }
97
+ async expireTimedOutApprovals(now) {
98
+ return this.store.expireTimedOutApprovals(now);
99
+ }
100
+ }
101
+ //# sourceMappingURL=approval.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval.js","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"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"}