@unrdf/kgc-runtime 26.4.2
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.
- package/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Enhanced Bounds - Soft limits, quotas, and rate limiting
|
|
3
|
+
* @module @unrdf/kgc-runtime/enhanced-bounds
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Advanced bounds enforcement with:
|
|
7
|
+
* - Soft limits with warning receipts at 80% threshold
|
|
8
|
+
* - Per-agent/per-user quotas
|
|
9
|
+
* - Sliding window rate limiting
|
|
10
|
+
* - Integration with existing receipt system
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Schemas
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Enhanced bounds schema with soft limits
|
|
22
|
+
*/
|
|
23
|
+
const EnhancedBoundsSchema = z.object({
|
|
24
|
+
// Hard limits (100%)
|
|
25
|
+
max_files_touched: z.number().int().positive().optional(),
|
|
26
|
+
max_bytes_changed: z.number().int().positive().optional(),
|
|
27
|
+
max_tool_ops: z.number().int().positive().optional(),
|
|
28
|
+
max_runtime_ms: z.number().int().positive().optional(),
|
|
29
|
+
max_graph_rewrites: z.number().int().positive().optional(),
|
|
30
|
+
|
|
31
|
+
// Warning thresholds (0.0-1.0 as fraction of limit)
|
|
32
|
+
warnings: z
|
|
33
|
+
.object({
|
|
34
|
+
filesThreshold: z.number().min(0).max(1).default(0.8),
|
|
35
|
+
bytesThreshold: z.number().min(0).max(1).default(0.9),
|
|
36
|
+
opsThreshold: z.number().min(0).max(1).default(0.75),
|
|
37
|
+
runtimeThreshold: z.number().min(0).max(1).default(0.9),
|
|
38
|
+
graphRewritesThreshold: z.number().min(0).max(1).default(0.8),
|
|
39
|
+
})
|
|
40
|
+
.optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Per-agent quota schema
|
|
45
|
+
*/
|
|
46
|
+
const AgentQuotaSchema = z.object({
|
|
47
|
+
agentId: z.string(),
|
|
48
|
+
max_files: z.number().int().positive().optional(),
|
|
49
|
+
max_bytes: z.number().int().positive().optional(),
|
|
50
|
+
max_ops: z.number().int().positive().optional(),
|
|
51
|
+
max_runtime_ms: z.number().int().positive().optional(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Rate limit schema
|
|
56
|
+
*/
|
|
57
|
+
const RateLimitSchema = z.object({
|
|
58
|
+
maxOpsPerSecond: z.number().positive(),
|
|
59
|
+
windowSizeMs: z.number().int().positive().default(1000),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Enhanced Bounds Checker
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Enhanced bounds checker with soft limits, quotas, and rate limiting
|
|
68
|
+
*
|
|
69
|
+
* @class
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* const checker = new EnhancedBoundsChecker({
|
|
73
|
+
* max_files_touched: 100,
|
|
74
|
+
* max_bytes_changed: 10485760,
|
|
75
|
+
* warnings: { filesThreshold: 0.8, bytesThreshold: 0.9 }
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* // Set per-agent quota
|
|
79
|
+
* checker.setAgentQuota('agent:backend-dev', {
|
|
80
|
+
* max_files: 50,
|
|
81
|
+
* max_bytes: 5242880
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* // Enable rate limiting
|
|
85
|
+
* checker.setRateLimit({ maxOpsPerSecond: 10 });
|
|
86
|
+
*
|
|
87
|
+
* // Check operation
|
|
88
|
+
* const receipt = await checker.checkAndRecord({
|
|
89
|
+
* type: 'file_write',
|
|
90
|
+
* files: 1,
|
|
91
|
+
* bytes: 1024
|
|
92
|
+
* }, 'agent:backend-dev');
|
|
93
|
+
*/
|
|
94
|
+
export class EnhancedBoundsChecker {
|
|
95
|
+
/**
|
|
96
|
+
* @param {Object} bounds - Bounds configuration
|
|
97
|
+
*/
|
|
98
|
+
constructor(bounds = {}) {
|
|
99
|
+
this.bounds = EnhancedBoundsSchema.parse(bounds);
|
|
100
|
+
|
|
101
|
+
// Global usage tracking
|
|
102
|
+
this.usage = {
|
|
103
|
+
files_touched: 0,
|
|
104
|
+
bytes_changed: 0,
|
|
105
|
+
tool_ops: 0,
|
|
106
|
+
runtime_ms: 0,
|
|
107
|
+
graph_rewrites: 0,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Per-agent usage tracking
|
|
111
|
+
/** @type {Map<string, Object>} */
|
|
112
|
+
this.agentUsage = new Map();
|
|
113
|
+
|
|
114
|
+
// Per-agent quotas
|
|
115
|
+
/** @type {Map<string, Object>} */
|
|
116
|
+
this.agentQuotas = new Map();
|
|
117
|
+
|
|
118
|
+
// Rate limiting state
|
|
119
|
+
/** @type {Map<string, Array<number>>} */
|
|
120
|
+
this.rateLimitWindows = new Map();
|
|
121
|
+
|
|
122
|
+
/** @type {Object|null} */
|
|
123
|
+
this.rateLimit = null;
|
|
124
|
+
|
|
125
|
+
// Receipt log
|
|
126
|
+
this.receipts = [];
|
|
127
|
+
|
|
128
|
+
// Warning thresholds (defaults)
|
|
129
|
+
this.warnings = this.bounds.warnings || {
|
|
130
|
+
filesThreshold: 0.8,
|
|
131
|
+
bytesThreshold: 0.9,
|
|
132
|
+
opsThreshold: 0.75,
|
|
133
|
+
runtimeThreshold: 0.9,
|
|
134
|
+
graphRewritesThreshold: 0.8,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Set per-agent quota
|
|
140
|
+
*
|
|
141
|
+
* @param {string} agentId - Agent identifier
|
|
142
|
+
* @param {Object} quota - Quota limits
|
|
143
|
+
*/
|
|
144
|
+
setAgentQuota(agentId, quota) {
|
|
145
|
+
const validatedQuota = AgentQuotaSchema.parse({ agentId, ...quota });
|
|
146
|
+
this.agentQuotas.set(agentId, validatedQuota);
|
|
147
|
+
|
|
148
|
+
// Initialize usage if not exists
|
|
149
|
+
if (!this.agentUsage.has(agentId)) {
|
|
150
|
+
this.agentUsage.set(agentId, {
|
|
151
|
+
files_touched: 0,
|
|
152
|
+
bytes_changed: 0,
|
|
153
|
+
tool_ops: 0,
|
|
154
|
+
runtime_ms: 0,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Set rate limit
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} rateLimit - Rate limit configuration
|
|
163
|
+
*/
|
|
164
|
+
setRateLimit(rateLimit) {
|
|
165
|
+
this.rateLimit = RateLimitSchema.parse(rateLimit);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if operation exceeds soft limit (warning threshold)
|
|
170
|
+
*
|
|
171
|
+
* @param {string} metric - Metric name (files, bytes, ops, runtime, rewrites)
|
|
172
|
+
* @param {number} current - Current usage
|
|
173
|
+
* @param {number} delta - Operation delta
|
|
174
|
+
* @param {number} limit - Hard limit
|
|
175
|
+
* @param {number} threshold - Warning threshold (0.0-1.0)
|
|
176
|
+
* @returns {Object|null} Warning receipt if threshold exceeded
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
179
|
+
_checkSoftLimit(metric, current, delta, limit, threshold) {
|
|
180
|
+
if (!limit || !threshold) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const newUsage = current + delta;
|
|
185
|
+
const percentage = newUsage / limit;
|
|
186
|
+
|
|
187
|
+
// Error when exceeding limit (>100%)
|
|
188
|
+
if (newUsage > limit) {
|
|
189
|
+
return {
|
|
190
|
+
type: 'error',
|
|
191
|
+
metric,
|
|
192
|
+
current: newUsage,
|
|
193
|
+
limit,
|
|
194
|
+
percentage: Math.round(percentage * 100),
|
|
195
|
+
message: `Exceeded ${metric} limit: ${newUsage}/${limit}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Warning at threshold (default 80%) but strictly below 100%
|
|
200
|
+
if (percentage >= threshold && percentage < 1.0) {
|
|
201
|
+
return {
|
|
202
|
+
type: 'warning',
|
|
203
|
+
metric,
|
|
204
|
+
current: newUsage,
|
|
205
|
+
limit,
|
|
206
|
+
percentage: Math.round(percentage * 100),
|
|
207
|
+
message: `Approaching ${metric} limit: ${Math.round(percentage * 100)}% used (${newUsage}/${limit})`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check rate limit for agent
|
|
216
|
+
*
|
|
217
|
+
* @param {string} agentId - Agent identifier
|
|
218
|
+
* @returns {Object|null} Error if rate limit exceeded
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
_checkRateLimit(agentId) {
|
|
222
|
+
if (!this.rateLimit) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
const windowSize = this.rateLimit.windowSizeMs;
|
|
228
|
+
const maxOps = this.rateLimit.maxOpsPerSecond;
|
|
229
|
+
|
|
230
|
+
// Get or create window for agent
|
|
231
|
+
if (!this.rateLimitWindows.has(agentId)) {
|
|
232
|
+
this.rateLimitWindows.set(agentId, []);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const window = this.rateLimitWindows.get(agentId);
|
|
236
|
+
|
|
237
|
+
// Remove timestamps outside window
|
|
238
|
+
const cutoff = now - windowSize;
|
|
239
|
+
const activeOps = window.filter((timestamp) => timestamp > cutoff);
|
|
240
|
+
this.rateLimitWindows.set(agentId, activeOps);
|
|
241
|
+
|
|
242
|
+
// Check if rate limit exceeded
|
|
243
|
+
if (activeOps.length >= maxOps) {
|
|
244
|
+
return {
|
|
245
|
+
type: 'error',
|
|
246
|
+
metric: 'rate_limit',
|
|
247
|
+
current: activeOps.length,
|
|
248
|
+
limit: maxOps,
|
|
249
|
+
message: `Rate limit exceeded: ${activeOps.length} ops in ${windowSize}ms (max ${maxOps})`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Add current operation
|
|
254
|
+
activeOps.push(now);
|
|
255
|
+
this.rateLimitWindows.set(agentId, activeOps);
|
|
256
|
+
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check per-agent quota
|
|
262
|
+
*
|
|
263
|
+
* @param {string} agentId - Agent identifier
|
|
264
|
+
* @param {Object} operation - Operation to check
|
|
265
|
+
* @returns {Object|null} Error if quota exceeded
|
|
266
|
+
* @private
|
|
267
|
+
*/
|
|
268
|
+
_checkAgentQuota(agentId, operation) {
|
|
269
|
+
const quota = this.agentQuotas.get(agentId);
|
|
270
|
+
if (!quota) {
|
|
271
|
+
return null; // No quota set for this agent
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const usage = this.agentUsage.get(agentId) || {
|
|
275
|
+
files_touched: 0,
|
|
276
|
+
bytes_changed: 0,
|
|
277
|
+
tool_ops: 0,
|
|
278
|
+
runtime_ms: 0,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Check files quota
|
|
282
|
+
if (quota.max_files && operation.files) {
|
|
283
|
+
if (usage.files_touched + operation.files > quota.max_files) {
|
|
284
|
+
return {
|
|
285
|
+
type: 'error',
|
|
286
|
+
metric: 'agent_quota_files',
|
|
287
|
+
agentId,
|
|
288
|
+
current: usage.files_touched + operation.files,
|
|
289
|
+
limit: quota.max_files,
|
|
290
|
+
message: `Agent ${agentId} exceeded files quota: ${usage.files_touched + operation.files}/${quota.max_files}`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check bytes quota
|
|
296
|
+
if (quota.max_bytes && operation.bytes) {
|
|
297
|
+
if (usage.bytes_changed + operation.bytes > quota.max_bytes) {
|
|
298
|
+
return {
|
|
299
|
+
type: 'error',
|
|
300
|
+
metric: 'agent_quota_bytes',
|
|
301
|
+
agentId,
|
|
302
|
+
current: usage.bytes_changed + operation.bytes,
|
|
303
|
+
limit: quota.max_bytes,
|
|
304
|
+
message: `Agent ${agentId} exceeded bytes quota: ${usage.bytes_changed + operation.bytes}/${quota.max_bytes}`,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check ops quota
|
|
310
|
+
if (quota.max_ops && operation.ops) {
|
|
311
|
+
if (usage.tool_ops + operation.ops > quota.max_ops) {
|
|
312
|
+
return {
|
|
313
|
+
type: 'error',
|
|
314
|
+
metric: 'agent_quota_ops',
|
|
315
|
+
agentId,
|
|
316
|
+
current: usage.tool_ops + operation.ops,
|
|
317
|
+
limit: quota.max_ops,
|
|
318
|
+
message: `Agent ${agentId} exceeded ops quota: ${usage.tool_ops + operation.ops}/${quota.max_ops}`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check runtime quota
|
|
324
|
+
if (quota.max_runtime_ms && operation.ms) {
|
|
325
|
+
if (usage.runtime_ms + operation.ms > quota.max_runtime_ms) {
|
|
326
|
+
return {
|
|
327
|
+
type: 'error',
|
|
328
|
+
metric: 'agent_quota_runtime',
|
|
329
|
+
agentId,
|
|
330
|
+
current: usage.runtime_ms + operation.ms,
|
|
331
|
+
limit: quota.max_runtime_ms,
|
|
332
|
+
message: `Agent ${agentId} exceeded runtime quota: ${usage.runtime_ms + operation.ms}/${quota.max_runtime_ms}`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Check and record operation with comprehensive validation
|
|
342
|
+
*
|
|
343
|
+
* @param {Object} operation - Operation to check
|
|
344
|
+
* @param {string} [agentId] - Agent identifier
|
|
345
|
+
* @returns {Object} Receipt with admission decision and warnings
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* const receipt = await checker.checkAndRecord({
|
|
349
|
+
* type: 'file_write',
|
|
350
|
+
* files: 1,
|
|
351
|
+
* bytes: 1024,
|
|
352
|
+
* ops: 1
|
|
353
|
+
* }, 'agent:backend-dev');
|
|
354
|
+
*
|
|
355
|
+
* // Receipt structure:
|
|
356
|
+
* // {
|
|
357
|
+
* // admit: true/false,
|
|
358
|
+
* // receipt_id: 'uuid',
|
|
359
|
+
* // timestamp: 123456789,
|
|
360
|
+
* // warnings: [...],
|
|
361
|
+
* // errors: [...]
|
|
362
|
+
* // }
|
|
363
|
+
*/
|
|
364
|
+
async checkAndRecord(operation, agentId = 'system') {
|
|
365
|
+
const receipt = {
|
|
366
|
+
receipt_id: randomUUID(),
|
|
367
|
+
timestamp: Date.now(),
|
|
368
|
+
agentId,
|
|
369
|
+
operation: operation.type,
|
|
370
|
+
warnings: [],
|
|
371
|
+
errors: [],
|
|
372
|
+
admit: true,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// Check rate limit
|
|
376
|
+
const rateLimitError = this._checkRateLimit(agentId);
|
|
377
|
+
if (rateLimitError) {
|
|
378
|
+
receipt.errors.push(rateLimitError);
|
|
379
|
+
receipt.admit = false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check agent quota
|
|
383
|
+
const quotaError = this._checkAgentQuota(agentId, operation);
|
|
384
|
+
if (quotaError) {
|
|
385
|
+
receipt.errors.push(quotaError);
|
|
386
|
+
receipt.admit = false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Check soft limits for each metric
|
|
390
|
+
if (operation.files !== undefined) {
|
|
391
|
+
const check = this._checkSoftLimit(
|
|
392
|
+
'files',
|
|
393
|
+
this.usage.files_touched,
|
|
394
|
+
operation.files,
|
|
395
|
+
this.bounds.max_files_touched,
|
|
396
|
+
this.warnings.filesThreshold
|
|
397
|
+
);
|
|
398
|
+
if (check) {
|
|
399
|
+
if (check.type === 'warning') {
|
|
400
|
+
receipt.warnings.push(check);
|
|
401
|
+
} else {
|
|
402
|
+
receipt.errors.push(check);
|
|
403
|
+
receipt.admit = false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (operation.bytes !== undefined) {
|
|
409
|
+
const check = this._checkSoftLimit(
|
|
410
|
+
'bytes',
|
|
411
|
+
this.usage.bytes_changed,
|
|
412
|
+
operation.bytes,
|
|
413
|
+
this.bounds.max_bytes_changed,
|
|
414
|
+
this.warnings.bytesThreshold
|
|
415
|
+
);
|
|
416
|
+
if (check) {
|
|
417
|
+
if (check.type === 'warning') {
|
|
418
|
+
receipt.warnings.push(check);
|
|
419
|
+
} else {
|
|
420
|
+
receipt.errors.push(check);
|
|
421
|
+
receipt.admit = false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (operation.ops !== undefined) {
|
|
427
|
+
const check = this._checkSoftLimit(
|
|
428
|
+
'ops',
|
|
429
|
+
this.usage.tool_ops,
|
|
430
|
+
operation.ops,
|
|
431
|
+
this.bounds.max_tool_ops,
|
|
432
|
+
this.warnings.opsThreshold
|
|
433
|
+
);
|
|
434
|
+
if (check) {
|
|
435
|
+
if (check.type === 'warning') {
|
|
436
|
+
receipt.warnings.push(check);
|
|
437
|
+
} else {
|
|
438
|
+
receipt.errors.push(check);
|
|
439
|
+
receipt.admit = false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (operation.ms !== undefined) {
|
|
445
|
+
const check = this._checkSoftLimit(
|
|
446
|
+
'runtime',
|
|
447
|
+
this.usage.runtime_ms,
|
|
448
|
+
operation.ms,
|
|
449
|
+
this.bounds.max_runtime_ms,
|
|
450
|
+
this.warnings.runtimeThreshold
|
|
451
|
+
);
|
|
452
|
+
if (check) {
|
|
453
|
+
if (check.type === 'warning') {
|
|
454
|
+
receipt.warnings.push(check);
|
|
455
|
+
} else {
|
|
456
|
+
receipt.errors.push(check);
|
|
457
|
+
receipt.admit = false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (operation.rewrites !== undefined) {
|
|
463
|
+
const check = this._checkSoftLimit(
|
|
464
|
+
'rewrites',
|
|
465
|
+
this.usage.graph_rewrites,
|
|
466
|
+
operation.rewrites,
|
|
467
|
+
this.bounds.max_graph_rewrites,
|
|
468
|
+
this.warnings.graphRewritesThreshold
|
|
469
|
+
);
|
|
470
|
+
if (check) {
|
|
471
|
+
if (check.type === 'warning') {
|
|
472
|
+
receipt.warnings.push(check);
|
|
473
|
+
} else {
|
|
474
|
+
receipt.errors.push(check);
|
|
475
|
+
receipt.admit = false;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// If admitted, record usage
|
|
481
|
+
if (receipt.admit) {
|
|
482
|
+
this._recordUsage(operation, agentId);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Store receipt
|
|
486
|
+
this.receipts.push(receipt);
|
|
487
|
+
|
|
488
|
+
return receipt;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Record operation usage
|
|
493
|
+
*
|
|
494
|
+
* @param {Object} operation - Operation to record
|
|
495
|
+
* @param {string} agentId - Agent identifier
|
|
496
|
+
* @private
|
|
497
|
+
*/
|
|
498
|
+
_recordUsage(operation, agentId) {
|
|
499
|
+
// Update global usage
|
|
500
|
+
if (operation.files !== undefined) {
|
|
501
|
+
this.usage.files_touched += operation.files;
|
|
502
|
+
}
|
|
503
|
+
if (operation.bytes !== undefined) {
|
|
504
|
+
this.usage.bytes_changed += operation.bytes;
|
|
505
|
+
}
|
|
506
|
+
if (operation.ops !== undefined) {
|
|
507
|
+
this.usage.tool_ops += operation.ops;
|
|
508
|
+
}
|
|
509
|
+
if (operation.ms !== undefined) {
|
|
510
|
+
this.usage.runtime_ms += operation.ms;
|
|
511
|
+
}
|
|
512
|
+
if (operation.rewrites !== undefined) {
|
|
513
|
+
this.usage.graph_rewrites += operation.rewrites;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Update agent usage
|
|
517
|
+
if (!this.agentUsage.has(agentId)) {
|
|
518
|
+
this.agentUsage.set(agentId, {
|
|
519
|
+
files_touched: 0,
|
|
520
|
+
bytes_changed: 0,
|
|
521
|
+
tool_ops: 0,
|
|
522
|
+
runtime_ms: 0,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const agentUsageData = this.agentUsage.get(agentId);
|
|
527
|
+
if (operation.files !== undefined) {
|
|
528
|
+
agentUsageData.files_touched += operation.files;
|
|
529
|
+
}
|
|
530
|
+
if (operation.bytes !== undefined) {
|
|
531
|
+
agentUsageData.bytes_changed += operation.bytes;
|
|
532
|
+
}
|
|
533
|
+
if (operation.ops !== undefined) {
|
|
534
|
+
agentUsageData.tool_ops += operation.ops;
|
|
535
|
+
}
|
|
536
|
+
if (operation.ms !== undefined) {
|
|
537
|
+
agentUsageData.runtime_ms += operation.ms;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Get current usage
|
|
543
|
+
*
|
|
544
|
+
* @returns {Object} Global usage
|
|
545
|
+
*/
|
|
546
|
+
getUsage() {
|
|
547
|
+
return { ...this.usage };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Get agent usage
|
|
552
|
+
*
|
|
553
|
+
* @param {string} agentId - Agent identifier
|
|
554
|
+
* @returns {Object|null} Agent usage or null
|
|
555
|
+
*/
|
|
556
|
+
getAgentUsage(agentId) {
|
|
557
|
+
const usage = this.agentUsage.get(agentId);
|
|
558
|
+
return usage ? { ...usage } : null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Get all receipts
|
|
563
|
+
*
|
|
564
|
+
* @returns {Array<Object>} Receipt history
|
|
565
|
+
*/
|
|
566
|
+
getReceipts() {
|
|
567
|
+
return [...this.receipts];
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Get receipts by type
|
|
572
|
+
*
|
|
573
|
+
* @param {string} type - Receipt type ('warning' or 'error')
|
|
574
|
+
* @returns {Array<Object>} Filtered receipts
|
|
575
|
+
*/
|
|
576
|
+
getReceiptsByType(type) {
|
|
577
|
+
return this.receipts.filter((receipt) => {
|
|
578
|
+
if (type === 'warning') {
|
|
579
|
+
return receipt.warnings.length > 0;
|
|
580
|
+
}
|
|
581
|
+
if (type === 'error') {
|
|
582
|
+
return receipt.errors.length > 0;
|
|
583
|
+
}
|
|
584
|
+
return false;
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Reset usage (for testing)
|
|
590
|
+
*/
|
|
591
|
+
reset() {
|
|
592
|
+
this.usage = {
|
|
593
|
+
files_touched: 0,
|
|
594
|
+
bytes_changed: 0,
|
|
595
|
+
tool_ops: 0,
|
|
596
|
+
runtime_ms: 0,
|
|
597
|
+
graph_rewrites: 0,
|
|
598
|
+
};
|
|
599
|
+
this.agentUsage.clear();
|
|
600
|
+
this.rateLimitWindows.clear();
|
|
601
|
+
this.receipts = [];
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// =============================================================================
|
|
606
|
+
// Module Exports
|
|
607
|
+
// =============================================================================
|
|
608
|
+
|
|
609
|
+
export default {
|
|
610
|
+
EnhancedBoundsChecker,
|
|
611
|
+
EnhancedBoundsSchema,
|
|
612
|
+
AgentQuotaSchema,
|
|
613
|
+
RateLimitSchema,
|
|
614
|
+
};
|
package/src/executor.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Receipt-based operation executor
|
|
3
|
+
* Ensures all operations are deterministic and produce verifiable receipts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { generateReceipt } from './receipt.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Execute an operation with receipt generation
|
|
10
|
+
* @template T
|
|
11
|
+
* @param {string} operation - Operation name
|
|
12
|
+
* @param {Record<string, any>} inputs - Operation inputs
|
|
13
|
+
* @param {() => Promise<T>} fn - Operation function
|
|
14
|
+
* @param {string} [parentHash] - Parent receipt hash
|
|
15
|
+
* @returns {Promise<{result: T, receipt: import('./receipt.mjs').Receipt}>}
|
|
16
|
+
*/
|
|
17
|
+
export async function executeWithReceipt(operation, inputs, fn, parentHash) {
|
|
18
|
+
try {
|
|
19
|
+
const result = await fn();
|
|
20
|
+
|
|
21
|
+
const receipt = await generateReceipt(
|
|
22
|
+
operation,
|
|
23
|
+
inputs,
|
|
24
|
+
{ result, success: true },
|
|
25
|
+
parentHash
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return { result, receipt };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const receipt = await generateReceipt(
|
|
31
|
+
operation,
|
|
32
|
+
inputs,
|
|
33
|
+
{
|
|
34
|
+
error: error.message,
|
|
35
|
+
stack: error.stack,
|
|
36
|
+
success: false
|
|
37
|
+
},
|
|
38
|
+
parentHash
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
result: null,
|
|
43
|
+
receipt,
|
|
44
|
+
error
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Batch executor for multiple operations
|
|
51
|
+
* @param {Array<{operation: string, inputs: Record<string, any>, fn: () => Promise<any>}>} operations
|
|
52
|
+
* @returns {Promise<{results: any[], receipts: import('./receipt.mjs').Receipt[]}>}
|
|
53
|
+
*/
|
|
54
|
+
export async function executeBatch(operations) {
|
|
55
|
+
const results = [];
|
|
56
|
+
const receipts = [];
|
|
57
|
+
let lastHash;
|
|
58
|
+
|
|
59
|
+
for (const op of operations) {
|
|
60
|
+
const { result, receipt } = await executeWithReceipt(
|
|
61
|
+
op.operation,
|
|
62
|
+
op.inputs,
|
|
63
|
+
op.fn,
|
|
64
|
+
lastHash
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
results.push(result);
|
|
68
|
+
receipts.push(receipt);
|
|
69
|
+
lastHash = receipt.hash;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { results, receipts };
|
|
73
|
+
}
|