@peac/adapter-openclaw 0.10.9 → 0.10.10

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/dist/index.mjs ADDED
@@ -0,0 +1,1273 @@
1
+ import { canonicalize, base64urlDecode, sign } from '@peac/crypto';
2
+ import { toInteractionEvidence, createHasher, createCaptureSession } from '@peac/capture-core';
3
+
4
+ // src/types.ts
5
+ var OPENCLAW_EXTENSION_KEYS = {
6
+ /** OpenClaw context extension */
7
+ CONTEXT: "org.openclaw/context",
8
+ /** Security audit digest */
9
+ AUDIT_DIGEST: "org.openclaw/audit_digest"
10
+ };
11
+ var OPENCLAW_ERROR_CODES = {
12
+ /** Missing required field in OpenClaw event */
13
+ MISSING_FIELD: "E_OPENCLAW_MISSING_FIELD",
14
+ /** Invalid field value in OpenClaw event */
15
+ INVALID_FIELD: "E_OPENCLAW_INVALID_FIELD",
16
+ /** Payload serialization failed */
17
+ SERIALIZATION_FAILED: "E_OPENCLAW_SERIALIZATION_FAILED",
18
+ /** Signing failed */
19
+ SIGNING_FAILED: "E_OPENCLAW_SIGNING_FAILED",
20
+ /** Emitter not configured */
21
+ EMITTER_NOT_CONFIGURED: "E_OPENCLAW_EMITTER_NOT_CONFIGURED"
22
+ };
23
+ var OPENCLAW_WARNING_CODES = {
24
+ /** Payload exceeded size limit and was truncated for hashing */
25
+ PAYLOAD_TRUNCATED: "W_OPENCLAW_PAYLOAD_TRUNCATED",
26
+ /** Payload serialization failed (circular refs, BigInt, etc.) */
27
+ SERIALIZATION_FAILED: "W_OPENCLAW_SERIALIZATION_FAILED",
28
+ /** Optional field missing (non-critical) */
29
+ OPTIONAL_FIELD_MISSING: "W_OPENCLAW_OPTIONAL_FIELD_MISSING",
30
+ /** Unknown tool provider */
31
+ UNKNOWN_PROVIDER: "W_OPENCLAW_UNKNOWN_PROVIDER",
32
+ /** Payload dropped due to redaction policy (hash_only mode for non-allowlisted tool) */
33
+ PAYLOAD_REDACTED: "W_OPENCLAW_PAYLOAD_REDACTED"
34
+ };
35
+ var DEFAULT_PLATFORM = "openclaw";
36
+ var DEFAULT_KIND = "tool.call";
37
+ function mapToolCallEvent(event, config) {
38
+ const warnings = [];
39
+ const validationError = validateRequiredFields(event);
40
+ if (validationError) {
41
+ return {
42
+ success: false,
43
+ error_code: validationError.code,
44
+ error_message: validationError.message
45
+ };
46
+ }
47
+ const interactionId = buildInteractionId(event);
48
+ const captureMode = config?.capture?.mode ?? "hash_only";
49
+ const allowlist = config?.capture?.allowlist ?? [];
50
+ const maxPayloadSize = config?.capture?.max_payload_size ?? 1024 * 1024;
51
+ const isAllowlisted = captureMode === "allowlist" && allowlist.includes(event.tool_name);
52
+ const inputResult = serializePayloadWithPolicy(
53
+ event.input,
54
+ maxPayloadSize,
55
+ isAllowlisted,
56
+ "input"
57
+ );
58
+ const outputResult = event.output !== void 0 ? serializePayloadWithPolicy(event.output, maxPayloadSize, isAllowlisted, "output") : void 0;
59
+ if (inputResult.warning) {
60
+ warnings.push(inputResult.warning);
61
+ }
62
+ if (outputResult?.warning) {
63
+ warnings.push(outputResult.warning);
64
+ }
65
+ const inputBytes = inputResult.bytes;
66
+ const outputBytes = outputResult?.bytes;
67
+ const platform = config?.platform ?? DEFAULT_PLATFORM;
68
+ const platformVersion = config?.platform_version;
69
+ const pluginId = config?.plugin_id;
70
+ const extensions = buildExtensions(event);
71
+ const action = {
72
+ id: interactionId,
73
+ kind: DEFAULT_KIND,
74
+ platform,
75
+ started_at: event.started_at,
76
+ completed_at: event.completed_at,
77
+ tool_name: event.tool_name,
78
+ tool_provider: event.tool_provider,
79
+ input_bytes: inputBytes,
80
+ output_bytes: outputBytes,
81
+ status: event.status,
82
+ error_code: event.error_code,
83
+ retryable: event.retryable,
84
+ metadata: {
85
+ // Platform metadata
86
+ ...platformVersion && { platform_version: platformVersion },
87
+ ...pluginId && { plugin_id: pluginId },
88
+ // Tool version (not a standard field, stored in metadata)
89
+ ...event.tool_version && { tool_version: event.tool_version },
90
+ // Policy context
91
+ ...event.policy && {
92
+ policy_decision: event.policy.decision,
93
+ sandbox_enabled: event.policy.sandbox_enabled,
94
+ elevated: event.policy.elevated,
95
+ policy_hash: event.policy.policy_hash
96
+ },
97
+ // OpenClaw extensions (will be mapped to interaction.extensions)
98
+ ...extensions
99
+ }
100
+ };
101
+ return {
102
+ success: true,
103
+ action,
104
+ warnings: warnings.length > 0 ? warnings : void 0
105
+ };
106
+ }
107
+ function mapToolCallEventBatch(events, config) {
108
+ return events.map((event) => mapToolCallEvent(event, config));
109
+ }
110
+ function validateRequiredFields(event) {
111
+ if (!event.tool_call_id) {
112
+ return {
113
+ code: OPENCLAW_ERROR_CODES.MISSING_FIELD,
114
+ message: "Missing required field: tool_call_id"
115
+ };
116
+ }
117
+ if (!event.run_id) {
118
+ return {
119
+ code: OPENCLAW_ERROR_CODES.MISSING_FIELD,
120
+ message: "Missing required field: run_id"
121
+ };
122
+ }
123
+ if (!event.tool_name) {
124
+ return {
125
+ code: OPENCLAW_ERROR_CODES.MISSING_FIELD,
126
+ message: "Missing required field: tool_name"
127
+ };
128
+ }
129
+ if (!event.started_at) {
130
+ return {
131
+ code: OPENCLAW_ERROR_CODES.MISSING_FIELD,
132
+ message: "Missing required field: started_at"
133
+ };
134
+ }
135
+ if (!event.status) {
136
+ return {
137
+ code: OPENCLAW_ERROR_CODES.MISSING_FIELD,
138
+ message: "Missing required field: status"
139
+ };
140
+ }
141
+ const validStatuses = ["ok", "error", "timeout", "canceled"];
142
+ if (!validStatuses.includes(event.status)) {
143
+ return {
144
+ code: OPENCLAW_ERROR_CODES.INVALID_FIELD,
145
+ message: `Invalid status: ${event.status}. Expected one of: ${validStatuses.join(", ")}`
146
+ };
147
+ }
148
+ if (!isValidRfc3339(event.started_at)) {
149
+ return {
150
+ code: OPENCLAW_ERROR_CODES.INVALID_FIELD,
151
+ message: `Invalid started_at timestamp: ${event.started_at}. Expected RFC 3339 format.`
152
+ };
153
+ }
154
+ if (event.completed_at && !isValidRfc3339(event.completed_at)) {
155
+ return {
156
+ code: OPENCLAW_ERROR_CODES.INVALID_FIELD,
157
+ message: `Invalid completed_at timestamp: ${event.completed_at}. Expected RFC 3339 format.`
158
+ };
159
+ }
160
+ return null;
161
+ }
162
+ function buildInteractionId(event) {
163
+ const runIdEncoded = base64urlEncode(event.run_id);
164
+ const toolCallIdEncoded = base64urlEncode(event.tool_call_id);
165
+ return `openclaw/${runIdEncoded}/${toolCallIdEncoded}`;
166
+ }
167
+ function base64urlEncode(str) {
168
+ const bytes = new TextEncoder().encode(str);
169
+ const base64 = btoa(String.fromCharCode(...bytes));
170
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
171
+ }
172
+ function serializePayloadWithPolicy(payload, maxPayloadSize, isAllowlisted, fieldName) {
173
+ if (payload === void 0) {
174
+ return {};
175
+ }
176
+ let bytes;
177
+ try {
178
+ const json = canonicalize(payload);
179
+ bytes = new TextEncoder().encode(json);
180
+ } catch {
181
+ return {
182
+ warning: {
183
+ code: OPENCLAW_WARNING_CODES.SERIALIZATION_FAILED,
184
+ message: `${fieldName} payload could not be serialized (circular ref, BigInt, etc.)`,
185
+ field: fieldName
186
+ }
187
+ };
188
+ }
189
+ let truncated = false;
190
+ if (bytes.length > maxPayloadSize) {
191
+ bytes = bytes.slice(0, maxPayloadSize);
192
+ truncated = true;
193
+ }
194
+ const result = { bytes, truncated };
195
+ if (truncated) {
196
+ result.warning = {
197
+ code: OPENCLAW_WARNING_CODES.PAYLOAD_TRUNCATED,
198
+ message: `${fieldName} payload exceeded ${maxPayloadSize} bytes and was truncated`,
199
+ field: fieldName
200
+ };
201
+ }
202
+ return result;
203
+ }
204
+ function buildExtensions(event) {
205
+ const extensions = {};
206
+ if (event.context) {
207
+ const contextExt = {
208
+ ...event.context.channel && { channel: event.context.channel },
209
+ ...event.context.workspace_digest && { workspace_digest: event.context.workspace_digest },
210
+ ...event.context.gateway_version && { gateway_version: event.context.gateway_version },
211
+ tool_call_id: event.tool_call_id
212
+ };
213
+ if (Object.keys(contextExt).length > 0) {
214
+ extensions[OPENCLAW_EXTENSION_KEYS.CONTEXT] = contextExt;
215
+ }
216
+ } else {
217
+ extensions[OPENCLAW_EXTENSION_KEYS.CONTEXT] = {
218
+ tool_call_id: event.tool_call_id
219
+ };
220
+ }
221
+ if (event.context?.audit_digest) {
222
+ extensions[OPENCLAW_EXTENSION_KEYS.AUDIT_DIGEST] = event.context.audit_digest;
223
+ }
224
+ return extensions;
225
+ }
226
+ function isValidRfc3339(timestamp) {
227
+ const rfc3339Pattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
228
+ if (!rfc3339Pattern.test(timestamp)) {
229
+ return false;
230
+ }
231
+ const date = new Date(timestamp);
232
+ return !isNaN(date.getTime());
233
+ }
234
+ function extractWorkflowId(event) {
235
+ return event.session_key;
236
+ }
237
+ function buildWorkflowContext(event) {
238
+ if (!event.session_key) {
239
+ return void 0;
240
+ }
241
+ return {
242
+ workflow_id: event.session_key,
243
+ step_id: event.tool_call_id,
244
+ tool_name: event.tool_name,
245
+ framework: "openclaw"
246
+ };
247
+ }
248
+
249
+ // src/hooks.ts
250
+ function createHookHandler(handlerConfig) {
251
+ const { session, config, onCapture, onMappingError } = handlerConfig;
252
+ return {
253
+ async afterToolCall(event) {
254
+ const mappingResult = mapToolCallEvent(event, config);
255
+ if (!mappingResult.success || !mappingResult.action) {
256
+ if (onMappingError) {
257
+ onMappingError(mappingResult, event);
258
+ }
259
+ return {
260
+ success: false,
261
+ code: mappingResult.error_code ?? "E_OPENCLAW_MAPPING_FAILED",
262
+ message: mappingResult.error_message ?? "Failed to map OpenClaw event"
263
+ };
264
+ }
265
+ const captureResult = await session.capture(mappingResult.action);
266
+ const hookResult = captureResult.success ? { success: true, entry: captureResult.entry } : { success: false, code: captureResult.code, message: captureResult.message };
267
+ if (onCapture) {
268
+ onCapture(hookResult, event);
269
+ }
270
+ return hookResult;
271
+ },
272
+ getSession() {
273
+ return session;
274
+ },
275
+ async close() {
276
+ await session.close();
277
+ }
278
+ };
279
+ }
280
+ async function captureBatch(handler, events) {
281
+ const results = [];
282
+ for (const event of events) {
283
+ const result = await handler.afterToolCall(event);
284
+ results.push(result);
285
+ }
286
+ return results;
287
+ }
288
+ async function captureParallel(handler, events) {
289
+ return Promise.all(events.map((event) => handler.afterToolCall(event)));
290
+ }
291
+ function createSessionHistoryTailer(tailerConfig) {
292
+ const { handler, sessionId, pollIntervalMs = 1e3, fetchHistory, onError } = tailerConfig;
293
+ let running = false;
294
+ let inFlight = false;
295
+ let lastEventId;
296
+ let intervalHandle;
297
+ const poll = async () => {
298
+ if (inFlight) return;
299
+ inFlight = true;
300
+ try {
301
+ const events = await fetchHistory(sessionId, lastEventId);
302
+ for (const event of events) {
303
+ if (!running) break;
304
+ await handler.afterToolCall(event);
305
+ lastEventId = event.tool_call_id;
306
+ }
307
+ } catch (error) {
308
+ if (onError) {
309
+ onError(error instanceof Error ? error : new Error(String(error)));
310
+ }
311
+ } finally {
312
+ inFlight = false;
313
+ }
314
+ };
315
+ return {
316
+ start() {
317
+ if (running) return;
318
+ running = true;
319
+ intervalHandle = setInterval(poll, pollIntervalMs);
320
+ poll();
321
+ },
322
+ stop() {
323
+ if (!running) return;
324
+ running = false;
325
+ if (intervalHandle) {
326
+ clearInterval(intervalHandle);
327
+ intervalHandle = void 0;
328
+ }
329
+ },
330
+ getLastEventId() {
331
+ return lastEventId;
332
+ },
333
+ isRunning() {
334
+ return running;
335
+ }
336
+ };
337
+ }
338
+ function createReceiptEmitter(emitterConfig) {
339
+ const { signer, writer, onEmit, onError } = emitterConfig;
340
+ let closed = false;
341
+ return {
342
+ async emit(entry) {
343
+ if (closed) {
344
+ return {
345
+ success: false,
346
+ error_code: "E_EMITTER_CLOSED",
347
+ error_message: "Emitter has been closed"
348
+ };
349
+ }
350
+ try {
351
+ const interaction = toInteractionEvidence(entry);
352
+ const receiptPayload = buildReceiptPayload(interaction, entry, signer);
353
+ const jws = await signer.sign(receiptPayload);
354
+ const signedReceipt = {
355
+ rid: receiptPayload.rid,
356
+ jws,
357
+ interaction_id: entry.action.id,
358
+ entry_digest: entry.entry_digest
359
+ };
360
+ const receiptPath = await writer.write(signedReceipt);
361
+ const result = {
362
+ success: true,
363
+ receipt_path: receiptPath,
364
+ receipt_id: receiptPayload.rid
365
+ };
366
+ if (onEmit) {
367
+ onEmit(result, entry);
368
+ }
369
+ return result;
370
+ } catch (error) {
371
+ const errorMessage = error instanceof Error ? error.message : String(error);
372
+ if (onError) {
373
+ onError(error instanceof Error ? error : new Error(errorMessage), entry);
374
+ }
375
+ return {
376
+ success: false,
377
+ error_code: OPENCLAW_ERROR_CODES.SIGNING_FAILED,
378
+ error_message: `Failed to emit receipt: ${errorMessage}`
379
+ };
380
+ }
381
+ },
382
+ async flush() {
383
+ },
384
+ async close() {
385
+ closed = true;
386
+ await writer.close();
387
+ }
388
+ };
389
+ }
390
+ function buildReceiptPayload(interaction, entry, signer) {
391
+ const now = Math.floor(Date.now() / 1e3);
392
+ const rid = generateReceiptId(entry.entry_digest);
393
+ const payload = {
394
+ iss: signer.getIssuer(),
395
+ iat: now,
396
+ rid,
397
+ evidence: {
398
+ extensions: {
399
+ "org.peacprotocol/interaction@0.1": interaction
400
+ }
401
+ }
402
+ };
403
+ const audience = signer.getAudience();
404
+ if (audience) {
405
+ payload.aud = audience;
406
+ }
407
+ return payload;
408
+ }
409
+ function generateReceiptId(entryDigest) {
410
+ return `r_${entryDigest.slice(0, 32)}`;
411
+ }
412
+ function createBackgroundService(serviceConfig) {
413
+ const {
414
+ emitter,
415
+ getPendingEntries,
416
+ markEmitted,
417
+ drainIntervalMs = 1e3,
418
+ onError
419
+ } = serviceConfig;
420
+ let running = false;
421
+ let stopping = false;
422
+ let inFlight = false;
423
+ let intervalHandle;
424
+ const stats = {
425
+ emitted: 0,
426
+ failed: 0
427
+ };
428
+ const drainOnce = async () => {
429
+ if (inFlight) return;
430
+ inFlight = true;
431
+ try {
432
+ const entries = await getPendingEntries();
433
+ for (const entry of entries) {
434
+ if (stopping) break;
435
+ const result = await emitter.emit(entry);
436
+ if (result.success) {
437
+ await markEmitted(entry.entry_digest);
438
+ stats.emitted++;
439
+ stats.lastEmitTime = /* @__PURE__ */ new Date();
440
+ } else {
441
+ stats.failed++;
442
+ stats.lastError = result.error_message;
443
+ }
444
+ }
445
+ } catch (error) {
446
+ stats.lastError = error instanceof Error ? error.message : String(error);
447
+ if (onError) {
448
+ onError(error instanceof Error ? error : new Error(String(error)));
449
+ }
450
+ } finally {
451
+ inFlight = false;
452
+ }
453
+ };
454
+ return {
455
+ start() {
456
+ if (running) return;
457
+ running = true;
458
+ intervalHandle = setInterval(drainOnce, drainIntervalMs);
459
+ drainOnce();
460
+ },
461
+ stop() {
462
+ if (!running) return;
463
+ stopping = true;
464
+ running = false;
465
+ if (intervalHandle) {
466
+ clearInterval(intervalHandle);
467
+ intervalHandle = void 0;
468
+ }
469
+ },
470
+ async drain() {
471
+ await drainOnce();
472
+ },
473
+ isRunning() {
474
+ return running;
475
+ },
476
+ getStats() {
477
+ return { ...stats };
478
+ }
479
+ };
480
+ }
481
+ function createJwkSigner(jwk, issuer, audience) {
482
+ const kid = jwk.kid || generateKeyId(jwk);
483
+ if (!jwk.d) {
484
+ throw new Error("JWK must include private key (d parameter)");
485
+ }
486
+ const privateKeyBytes = base64urlDecode(jwk.d);
487
+ return {
488
+ async sign(payload) {
489
+ return sign(payload, privateKeyBytes, kid);
490
+ },
491
+ getKeyId() {
492
+ return kid;
493
+ },
494
+ getIssuer() {
495
+ return issuer;
496
+ },
497
+ getAudience() {
498
+ return audience;
499
+ }
500
+ };
501
+ }
502
+ async function resolveSigner(keyRef, issuer, audience) {
503
+ const colonIndex = keyRef.indexOf(":");
504
+ if (colonIndex === -1) {
505
+ throw new Error(`Invalid key reference format: ${keyRef}. Expected scheme:value`);
506
+ }
507
+ const scheme = keyRef.slice(0, colonIndex);
508
+ const value = keyRef.slice(colonIndex + 1);
509
+ switch (scheme) {
510
+ case "env": {
511
+ const keyData = process.env[value];
512
+ if (!keyData) {
513
+ throw new Error(`Environment variable ${value} not set for signing key`);
514
+ }
515
+ const jwk = JSON.parse(keyData);
516
+ return createJwkSigner(jwk, issuer, audience);
517
+ }
518
+ case "file": {
519
+ const fs = await import('fs');
520
+ const keyData = await fs.promises.readFile(value, "utf-8");
521
+ const jwk = JSON.parse(keyData);
522
+ return createJwkSigner(jwk, issuer, audience);
523
+ }
524
+ case "keychain":
525
+ throw new Error("Keychain signing not yet implemented. Use env: for development.");
526
+ case "sidecar":
527
+ throw new Error("Sidecar signing not yet implemented. Use env: for development.");
528
+ default:
529
+ throw new Error(`Unknown key reference scheme: ${scheme}. Use env: or file:`);
530
+ }
531
+ }
532
+ function generateKeyId(jwk) {
533
+ const publicPart = jwk.x || "";
534
+ let hash = 0;
535
+ for (let i = 0; i < publicPart.length; i++) {
536
+ const char = publicPart.charCodeAt(i);
537
+ hash = (hash << 5) - hash + char;
538
+ hash = hash & hash;
539
+ }
540
+ return `k_${Math.abs(hash).toString(16).padStart(8, "0")}`;
541
+ }
542
+ async function createFileReceiptWriter(outputDir) {
543
+ const fs = await import('fs');
544
+ const pathModule = await import('path');
545
+ const resolvedOutputDir = pathModule.resolve(outputDir);
546
+ await fs.promises.mkdir(resolvedOutputDir, { recursive: true });
547
+ return {
548
+ async write(receipt) {
549
+ const filename = `${receipt.rid}.peac.json`;
550
+ const filepath = pathModule.join(resolvedOutputDir, filename);
551
+ const tempPath = `${filepath}.tmp`;
552
+ const content = JSON.stringify(
553
+ {
554
+ rid: receipt.rid,
555
+ interaction_id: receipt.interaction_id,
556
+ entry_digest: receipt.entry_digest,
557
+ _jws: receipt.jws
558
+ },
559
+ null,
560
+ 2
561
+ );
562
+ await fs.promises.writeFile(tempPath, content, "utf-8");
563
+ const fd = await fs.promises.open(tempPath, "r");
564
+ await fd.sync();
565
+ await fd.close();
566
+ await fs.promises.rename(tempPath, filepath);
567
+ try {
568
+ const dirFd = await fs.promises.open(resolvedOutputDir, "r");
569
+ await dirFd.sync();
570
+ await dirFd.close();
571
+ } catch {
572
+ }
573
+ return filepath;
574
+ },
575
+ async close() {
576
+ }
577
+ };
578
+ }
579
+ async function createPluginInstance(options) {
580
+ const {
581
+ signer,
582
+ writer,
583
+ adapterConfig,
584
+ store,
585
+ dedupe,
586
+ drainIntervalMs = 1e3,
587
+ batchSize = 100,
588
+ onError
589
+ } = options;
590
+ const hasher = await createHasher();
591
+ const session = createCaptureSession({
592
+ store,
593
+ dedupe,
594
+ hasher
595
+ });
596
+ const stats = {
597
+ totalCaptured: 0,
598
+ duplicatesSkipped: 0,
599
+ pendingCount: 0,
600
+ totalEmitted: 0,
601
+ totalErrors: 0,
602
+ keyId: signer.getKeyId(),
603
+ isRunning: false
604
+ };
605
+ const hookHandler = createHookHandler({
606
+ session,
607
+ config: adapterConfig,
608
+ onCapture: (result) => {
609
+ if (result.success) {
610
+ stats.totalCaptured++;
611
+ stats.pendingCount++;
612
+ } else {
613
+ stats.duplicatesSkipped++;
614
+ }
615
+ }
616
+ });
617
+ const emitter = createReceiptEmitter({
618
+ signer,
619
+ writer,
620
+ onEmit: (result) => {
621
+ if (result.success) {
622
+ stats.totalEmitted++;
623
+ stats.pendingCount = Math.max(0, stats.pendingCount - 1);
624
+ }
625
+ },
626
+ onError: (error) => {
627
+ stats.totalErrors++;
628
+ if (onError) onError(error);
629
+ }
630
+ });
631
+ let lastEmittedSequence = 0;
632
+ let drainInProgress = false;
633
+ const backgroundService = createBackgroundService({
634
+ emitter,
635
+ drainIntervalMs,
636
+ getPendingEntries: async () => {
637
+ if (drainInProgress) return [];
638
+ drainInProgress = true;
639
+ try {
640
+ const currentSequence = await store.getSequence();
641
+ if (currentSequence <= lastEmittedSequence) return [];
642
+ const entries = await store.read(lastEmittedSequence, batchSize);
643
+ const pending = [];
644
+ for (const entry of entries) {
645
+ if (entry.sequence <= lastEmittedSequence) continue;
646
+ const dedupeEntry = await dedupe.get(entry.action.id);
647
+ if (dedupeEntry?.emitted) continue;
648
+ pending.push(entry);
649
+ }
650
+ return pending;
651
+ } finally {
652
+ drainInProgress = false;
653
+ }
654
+ },
655
+ markEmitted: async (digest) => {
656
+ const entries = await store.read(lastEmittedSequence, batchSize);
657
+ const entry = entries.find((e) => e.entry_digest === digest);
658
+ if (entry) {
659
+ if (entry.sequence > lastEmittedSequence) {
660
+ lastEmittedSequence = entry.sequence;
661
+ }
662
+ await dedupe.markEmitted(entry.action.id);
663
+ }
664
+ },
665
+ onError
666
+ });
667
+ return {
668
+ session,
669
+ hookHandler,
670
+ backgroundService,
671
+ start() {
672
+ stats.isRunning = true;
673
+ backgroundService.start();
674
+ },
675
+ async stop() {
676
+ backgroundService.stop();
677
+ await backgroundService.drain();
678
+ await hookHandler.close();
679
+ stats.isRunning = false;
680
+ },
681
+ getStats() {
682
+ return { ...stats };
683
+ }
684
+ };
685
+ }
686
+
687
+ // src/tools.ts
688
+ var MAX_STATUS_FILE_SCAN = 1e3;
689
+ var MAX_QUERY_FILE_PARSE = 1e4;
690
+ var MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
691
+ var ALLOWED_ALGORITHMS = ["EdDSA", "ES256", "ES384", "ES512", "RS256", "RS384", "RS512"];
692
+ var ALG_KEY_TYPE_MAP = {
693
+ EdDSA: { kty: "OKP", crv: ["Ed25519", "Ed448"] },
694
+ ES256: { kty: "EC", crv: ["P-256"] },
695
+ ES384: { kty: "EC", crv: ["P-384"] },
696
+ ES512: { kty: "EC", crv: ["P-521"] },
697
+ RS256: { kty: "RSA" },
698
+ RS384: { kty: "RSA" },
699
+ RS512: { kty: "RSA" }
700
+ };
701
+ function createStatusTool(stats, outputDir) {
702
+ return {
703
+ name: "peac_receipts.status",
704
+ description: "Show PEAC receipts status: spool size, last receipt time, config summary",
705
+ parameters: {},
706
+ async execute() {
707
+ const fs = await import('fs');
708
+ const pathModule = await import('path');
709
+ const resolvedOutputDir = pathModule.resolve(outputDir);
710
+ let receiptCount = 0;
711
+ let lastReceiptTime = null;
712
+ let oldestReceiptTime = null;
713
+ let countApproximate = false;
714
+ try {
715
+ const files = await fs.promises.readdir(resolvedOutputDir);
716
+ const receiptFiles = files.filter((f) => f.endsWith(".peac.json"));
717
+ receiptCount = receiptFiles.length;
718
+ const filesToScan = receiptFiles.slice(0, MAX_STATUS_FILE_SCAN);
719
+ countApproximate = receiptFiles.length > MAX_STATUS_FILE_SCAN;
720
+ if (filesToScan.length > 0) {
721
+ const fileStats = await Promise.all(
722
+ filesToScan.map(async (f) => {
723
+ const stat = await fs.promises.stat(pathModule.join(resolvedOutputDir, f));
724
+ return { file: f, mtime: stat.mtime };
725
+ })
726
+ );
727
+ fileStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
728
+ lastReceiptTime = fileStats[0].mtime.toISOString();
729
+ oldestReceiptTime = fileStats[fileStats.length - 1].mtime.toISOString();
730
+ }
731
+ } catch {
732
+ }
733
+ return {
734
+ status: "ok",
735
+ spool: {
736
+ pending_entries: stats.pendingCount,
737
+ total_captured: stats.totalCaptured,
738
+ duplicates_skipped: stats.duplicatesSkipped
739
+ },
740
+ receipts: {
741
+ count: receiptCount,
742
+ count_approximate: countApproximate,
743
+ output_dir: resolvedOutputDir,
744
+ last_receipt_time: lastReceiptTime,
745
+ oldest_receipt_time: oldestReceiptTime
746
+ },
747
+ emitter: {
748
+ total_emitted: stats.totalEmitted,
749
+ total_errors: stats.totalErrors,
750
+ is_running: stats.isRunning,
751
+ key_id: stats.keyId
752
+ }
753
+ };
754
+ }
755
+ };
756
+ }
757
+ function createExportBundleTool(outputDir, logger) {
758
+ return {
759
+ name: "peac_receipts.export_bundle",
760
+ description: "Export receipts as a bundle directory for audit (manifest.json + receipts/)",
761
+ parameters: {
762
+ type: "object",
763
+ properties: {
764
+ workflow_id: {
765
+ type: "string",
766
+ description: "Filter by workflow ID"
767
+ },
768
+ since: {
769
+ type: "string",
770
+ description: "Include receipts since this RFC 3339 timestamp"
771
+ },
772
+ until: {
773
+ type: "string",
774
+ description: "Include receipts until this RFC 3339 timestamp"
775
+ },
776
+ output_path: {
777
+ type: "string",
778
+ description: "Output path for bundle directory (default: peac-bundle-{timestamp})"
779
+ }
780
+ }
781
+ },
782
+ async execute(params) {
783
+ const fs = await import('fs');
784
+ const pathModule = await import('path');
785
+ const resolvedOutputDir = pathModule.resolve(outputDir);
786
+ try {
787
+ const files = await fs.promises.readdir(resolvedOutputDir);
788
+ const receiptFiles = files.filter((f) => f.endsWith(".peac.json"));
789
+ if (receiptFiles.length === 0) {
790
+ return {
791
+ status: "ok",
792
+ message: "No receipts to export",
793
+ receipt_count: 0
794
+ };
795
+ }
796
+ const receipts = [];
797
+ for (const file of receiptFiles) {
798
+ const filePath = pathModule.join(resolvedOutputDir, file);
799
+ let stat;
800
+ try {
801
+ stat = await fs.promises.stat(filePath);
802
+ } catch {
803
+ continue;
804
+ }
805
+ if (params.since && stat.mtime < new Date(params.since)) {
806
+ continue;
807
+ }
808
+ if (params.until && stat.mtime > new Date(params.until)) {
809
+ continue;
810
+ }
811
+ let content;
812
+ try {
813
+ content = JSON.parse(await fs.promises.readFile(filePath, "utf-8"));
814
+ } catch {
815
+ logger.warn(`Skipping invalid JSON in ${file}`);
816
+ continue;
817
+ }
818
+ if (params.workflow_id) {
819
+ const workflowExt = content?.auth?.extensions?.["org.peacprotocol/workflow"];
820
+ if (workflowExt?.workflow_id !== params.workflow_id) {
821
+ continue;
822
+ }
823
+ }
824
+ receipts.push({ file, content, mtime: stat.mtime });
825
+ }
826
+ if (receipts.length === 0) {
827
+ return {
828
+ status: "ok",
829
+ message: "No receipts match the filter criteria",
830
+ receipt_count: 0
831
+ };
832
+ }
833
+ const manifest = {
834
+ version: "1.0",
835
+ format: "peac-bundle-directory",
836
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
837
+ receipt_count: receipts.length,
838
+ filters: {
839
+ workflow_id: params.workflow_id || null,
840
+ since: params.since || null,
841
+ until: params.until || null
842
+ },
843
+ receipts: receipts.map((r) => ({
844
+ file: r.file,
845
+ mtime: r.mtime.toISOString()
846
+ }))
847
+ };
848
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
849
+ const bundleDir = params.output_path ? pathModule.resolve(params.output_path) : pathModule.join(resolvedOutputDir, `peac-bundle-${timestamp}`);
850
+ await fs.promises.mkdir(bundleDir, { recursive: true });
851
+ await fs.promises.writeFile(
852
+ pathModule.join(bundleDir, "manifest.json"),
853
+ JSON.stringify(manifest, null, 2)
854
+ );
855
+ const receiptsDir = pathModule.join(bundleDir, "receipts");
856
+ await fs.promises.mkdir(receiptsDir, { recursive: true });
857
+ for (const receipt of receipts) {
858
+ await fs.promises.writeFile(
859
+ pathModule.join(receiptsDir, receipt.file),
860
+ JSON.stringify(receipt.content, null, 2)
861
+ );
862
+ }
863
+ logger.info(`Exported ${receipts.length} receipts to ${bundleDir}`);
864
+ return {
865
+ status: "ok",
866
+ message: `Exported ${receipts.length} receipts to directory`,
867
+ receipt_count: receipts.length,
868
+ bundle_path: bundleDir
869
+ };
870
+ } catch (error) {
871
+ logger.error("Export bundle failed:", error);
872
+ return {
873
+ status: "error",
874
+ message: error instanceof Error ? error.message : "Unknown error",
875
+ receipt_count: 0
876
+ };
877
+ }
878
+ }
879
+ };
880
+ }
881
+ function createVerifyTool(logger) {
882
+ return {
883
+ name: "peac_receipts.verify",
884
+ description: "Verify a receipt or bundle offline",
885
+ parameters: {
886
+ type: "object",
887
+ properties: {
888
+ path: {
889
+ type: "string",
890
+ description: "Path to receipt file or bundle directory"
891
+ },
892
+ jwks_path: {
893
+ type: "string",
894
+ description: "Path to JWKS file for key verification (optional)"
895
+ }
896
+ },
897
+ required: ["path"]
898
+ },
899
+ async execute(params) {
900
+ const verifyParams = params;
901
+ const fs = await import('fs');
902
+ const pathModule = await import('path');
903
+ try {
904
+ const stat = await fs.promises.stat(verifyParams.path);
905
+ if (stat.isDirectory()) {
906
+ return await verifyBundle(
907
+ verifyParams.path,
908
+ verifyParams.jwks_path,
909
+ fs,
910
+ pathModule,
911
+ logger
912
+ );
913
+ } else {
914
+ return await verifySingleReceipt(verifyParams.path, verifyParams.jwks_path, fs, logger);
915
+ }
916
+ } catch (error) {
917
+ logger.error("Verification failed:", error);
918
+ return {
919
+ status: "error",
920
+ valid: false,
921
+ message: error instanceof Error ? error.message : "Unknown error",
922
+ errors: [error instanceof Error ? error.message : "Unknown error"]
923
+ };
924
+ }
925
+ }
926
+ };
927
+ }
928
+ async function verifySingleReceipt(receiptPath, jwksPath, fs, logger) {
929
+ const stat = await fs.promises.stat(receiptPath);
930
+ if (stat.size > MAX_FILE_SIZE_BYTES) {
931
+ return {
932
+ status: "error",
933
+ valid: false,
934
+ message: `File too large: ${stat.size} bytes exceeds limit of ${MAX_FILE_SIZE_BYTES} bytes`,
935
+ errors: [`File size ${stat.size} bytes exceeds ${MAX_FILE_SIZE_BYTES} byte limit`]
936
+ };
937
+ }
938
+ const content = await fs.promises.readFile(receiptPath, "utf-8");
939
+ const receipt = JSON.parse(content);
940
+ const errors = [];
941
+ const warnings = [];
942
+ if (!receipt.auth) {
943
+ errors.push("Missing auth block");
944
+ }
945
+ if (!receipt.evidence) {
946
+ errors.push("Missing evidence block");
947
+ }
948
+ const interaction = receipt.evidence?.extensions?.["org.peacprotocol/interaction@0.1"];
949
+ if (interaction) {
950
+ if (!interaction.interaction_id) {
951
+ errors.push("Missing interaction_id");
952
+ }
953
+ if (!interaction.kind) {
954
+ errors.push("Missing kind");
955
+ }
956
+ if (!interaction.executor?.platform) {
957
+ errors.push("Missing executor.platform");
958
+ }
959
+ if (!interaction.started_at) {
960
+ errors.push("Missing started_at");
961
+ }
962
+ if (interaction.completed_at && interaction.started_at) {
963
+ if (new Date(interaction.completed_at) < new Date(interaction.started_at)) {
964
+ errors.push("completed_at is before started_at");
965
+ }
966
+ }
967
+ if (interaction.output && !interaction.result?.status) {
968
+ errors.push("output present but result.status missing");
969
+ }
970
+ } else {
971
+ warnings.push("No interaction evidence found");
972
+ }
973
+ if (jwksPath && receipt._jws) {
974
+ try {
975
+ const { verify, base64urlDecode: base64urlDecode2, base64urlDecodeString } = await import('@peac/crypto');
976
+ const jwks = JSON.parse(await fs.promises.readFile(jwksPath, "utf-8"));
977
+ const jwsParts = receipt._jws.split(".");
978
+ if (jwsParts.length !== 3) {
979
+ errors.push("Invalid JWS format (expected 3 parts)");
980
+ } else {
981
+ const headerJson = base64urlDecodeString(jwsParts[0]);
982
+ const header = JSON.parse(headerJson);
983
+ if (header.alg === "none") {
984
+ errors.push('Algorithm "none" is not allowed');
985
+ }
986
+ if (header.alg && !ALLOWED_ALGORITHMS.includes(header.alg)) {
987
+ errors.push(
988
+ `Algorithm "${header.alg}" is not in allowed list: ${ALLOWED_ALGORITHMS.join(", ")}`
989
+ );
990
+ }
991
+ if (header.crit && header.crit.length > 0) {
992
+ const headerObj = header;
993
+ const missingCrit = header.crit.filter((h) => !(h in headerObj));
994
+ if (missingCrit.length > 0) {
995
+ errors.push(`Critical headers declared but missing: ${missingCrit.join(", ")}`);
996
+ }
997
+ errors.push(`Unsupported critical headers: ${header.crit.join(", ")}`);
998
+ }
999
+ const targetKid = header.kid;
1000
+ let keyJwk;
1001
+ if (!Array.isArray(jwks.keys) || jwks.keys.length === 0) {
1002
+ errors.push("JWKS has no keys");
1003
+ } else if (targetKid) {
1004
+ keyJwk = jwks.keys.find((k) => k.kid === targetKid);
1005
+ if (!keyJwk) {
1006
+ errors.push(`Key with kid "${targetKid}" not found in JWKS`);
1007
+ }
1008
+ } else if (jwks.keys.length === 1) {
1009
+ keyJwk = jwks.keys[0];
1010
+ warnings.push("JWS has no kid - using only key from JWKS");
1011
+ } else {
1012
+ errors.push(`JWS missing kid but JWKS has ${jwks.keys.length} keys - cannot select`);
1013
+ }
1014
+ if (keyJwk && header.alg && errors.length === 0) {
1015
+ const expectedKeyType = ALG_KEY_TYPE_MAP[header.alg];
1016
+ if (expectedKeyType) {
1017
+ if (keyJwk.kty !== expectedKeyType.kty) {
1018
+ errors.push(
1019
+ `Algorithm "${header.alg}" requires key type "${expectedKeyType.kty}" but key has "${keyJwk.kty}"`
1020
+ );
1021
+ } else if (expectedKeyType.crv && keyJwk.crv && !expectedKeyType.crv.includes(keyJwk.crv)) {
1022
+ errors.push(
1023
+ `Algorithm "${header.alg}" requires curve ${expectedKeyType.crv.join(" or ")} but key has "${keyJwk.crv}"`
1024
+ );
1025
+ }
1026
+ }
1027
+ if (keyJwk.use && keyJwk.use !== "sig") {
1028
+ errors.push(
1029
+ `Key use "${keyJwk.use}" is not valid for signature verification (expected "sig")`
1030
+ );
1031
+ }
1032
+ if (keyJwk.key_ops && !keyJwk.key_ops.includes("verify")) {
1033
+ errors.push(`Key operations ${JSON.stringify(keyJwk.key_ops)} do not include "verify"`);
1034
+ }
1035
+ }
1036
+ if (keyJwk && errors.length === 0) {
1037
+ const publicKeyParam = keyJwk.x || keyJwk.n;
1038
+ if (!publicKeyParam) {
1039
+ errors.push("Selected key missing public key (x or n parameter)");
1040
+ } else {
1041
+ const publicKeyBytes = base64urlDecode2(publicKeyParam);
1042
+ await verify(receipt._jws, publicKeyBytes);
1043
+ logger.info(`Signature verified with key ${keyJwk.kid || "anonymous"}`);
1044
+ }
1045
+ }
1046
+ }
1047
+ } catch (error) {
1048
+ errors.push(
1049
+ `Signature verification failed: ${error instanceof Error ? error.message : "Unknown"}`
1050
+ );
1051
+ }
1052
+ } else if (!jwksPath) {
1053
+ warnings.push("No JWKS provided - signature not verified");
1054
+ }
1055
+ return {
1056
+ status: errors.length === 0 ? "ok" : "error",
1057
+ valid: errors.length === 0,
1058
+ message: errors.length === 0 ? "Receipt is valid" : "Receipt has validation errors",
1059
+ errors: errors.length > 0 ? errors : void 0,
1060
+ warnings: warnings.length > 0 ? warnings : void 0,
1061
+ receipt_id: receipt.auth?.rid,
1062
+ interaction_id: interaction?.interaction_id
1063
+ };
1064
+ }
1065
+ async function verifyBundle(bundlePath, jwksPath, fs, pathModule, logger) {
1066
+ const manifestPath = pathModule.join(bundlePath, "manifest.json");
1067
+ const manifest = JSON.parse(await fs.promises.readFile(manifestPath, "utf-8"));
1068
+ const receiptsDir = pathModule.join(bundlePath, "receipts");
1069
+ const files = await fs.promises.readdir(receiptsDir);
1070
+ let validCount = 0;
1071
+ let invalidCount = 0;
1072
+ const allErrors = [];
1073
+ const allWarnings = [];
1074
+ for (const file of files) {
1075
+ if (!file.endsWith(".peac.json")) continue;
1076
+ const result = await verifySingleReceipt(
1077
+ pathModule.join(receiptsDir, file),
1078
+ jwksPath,
1079
+ fs,
1080
+ logger
1081
+ );
1082
+ if (result.valid) {
1083
+ validCount++;
1084
+ } else {
1085
+ invalidCount++;
1086
+ if (result.errors) {
1087
+ allErrors.push(...result.errors.map((e) => `${file}: ${e}`));
1088
+ }
1089
+ }
1090
+ if (result.warnings) {
1091
+ allWarnings.push(...result.warnings.map((w) => `${file}: ${w}`));
1092
+ }
1093
+ }
1094
+ return {
1095
+ status: invalidCount === 0 ? "ok" : "error",
1096
+ valid: invalidCount === 0,
1097
+ message: `Bundle verification: ${validCount} valid, ${invalidCount} invalid`,
1098
+ errors: allErrors.length > 0 ? allErrors : void 0,
1099
+ warnings: allWarnings.length > 0 ? allWarnings : void 0,
1100
+ bundle_stats: {
1101
+ total: validCount + invalidCount,
1102
+ valid: validCount,
1103
+ invalid: invalidCount,
1104
+ manifest_receipt_count: manifest.receipt_count
1105
+ }
1106
+ };
1107
+ }
1108
+ function createQueryTool(outputDir, logger) {
1109
+ return {
1110
+ name: "peac_receipts.query",
1111
+ description: "Query receipts by workflow_id, tool name, or time range",
1112
+ parameters: {
1113
+ type: "object",
1114
+ properties: {
1115
+ workflow_id: {
1116
+ type: "string",
1117
+ description: "Filter by workflow ID"
1118
+ },
1119
+ tool_name: {
1120
+ type: "string",
1121
+ description: "Filter by tool name"
1122
+ },
1123
+ since: {
1124
+ type: "string",
1125
+ description: "Include receipts since this RFC 3339 timestamp"
1126
+ },
1127
+ until: {
1128
+ type: "string",
1129
+ description: "Include receipts until this RFC 3339 timestamp"
1130
+ },
1131
+ status: {
1132
+ type: "string",
1133
+ enum: ["ok", "error", "timeout", "canceled"],
1134
+ description: "Filter by result status"
1135
+ },
1136
+ limit: {
1137
+ type: "integer",
1138
+ description: "Maximum number of results (default: 100)"
1139
+ },
1140
+ offset: {
1141
+ type: "integer",
1142
+ description: "Skip this many results (for pagination)"
1143
+ }
1144
+ }
1145
+ },
1146
+ async execute(params) {
1147
+ const fs = await import('fs');
1148
+ const pathModule = await import('path');
1149
+ const resolvedOutputDir = pathModule.resolve(outputDir);
1150
+ try {
1151
+ const files = await fs.promises.readdir(resolvedOutputDir);
1152
+ const receiptFiles = files.filter((f) => f.endsWith(".peac.json"));
1153
+ const limit = Math.min(params.limit || 100, MAX_QUERY_FILE_PARSE);
1154
+ const offset = params.offset || 0;
1155
+ const fileInfos = [];
1156
+ for (const file of receiptFiles.slice(0, MAX_QUERY_FILE_PARSE)) {
1157
+ try {
1158
+ const filePath = pathModule.join(resolvedOutputDir, file);
1159
+ const stat = await fs.promises.stat(filePath);
1160
+ if (params.since && stat.mtime < new Date(params.since)) {
1161
+ continue;
1162
+ }
1163
+ if (params.until && stat.mtime > new Date(params.until)) {
1164
+ continue;
1165
+ }
1166
+ fileInfos.push({ file, mtime: stat.mtime });
1167
+ } catch {
1168
+ continue;
1169
+ }
1170
+ }
1171
+ fileInfos.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1172
+ const matches = [];
1173
+ let skippedForFilters = 0;
1174
+ let filesSkippedForSize = 0;
1175
+ let filesSkippedForInvalidJson = 0;
1176
+ for (const { file } of fileInfos) {
1177
+ const filePath = pathModule.join(resolvedOutputDir, file);
1178
+ try {
1179
+ const fileStat = await fs.promises.stat(filePath);
1180
+ if (fileStat.size > MAX_FILE_SIZE_BYTES) {
1181
+ filesSkippedForSize++;
1182
+ continue;
1183
+ }
1184
+ } catch {
1185
+ continue;
1186
+ }
1187
+ let content;
1188
+ try {
1189
+ content = JSON.parse(await fs.promises.readFile(filePath, "utf-8"));
1190
+ } catch {
1191
+ filesSkippedForInvalidJson++;
1192
+ continue;
1193
+ }
1194
+ const interaction = content?.evidence?.extensions?.["org.peacprotocol/interaction@0.1"];
1195
+ const workflow = content?.auth?.extensions?.["org.peacprotocol/workflow"];
1196
+ if (params.workflow_id && workflow?.workflow_id !== params.workflow_id) {
1197
+ skippedForFilters++;
1198
+ continue;
1199
+ }
1200
+ if (params.tool_name && interaction?.tool?.name !== params.tool_name) {
1201
+ skippedForFilters++;
1202
+ continue;
1203
+ }
1204
+ if (params.status && interaction?.result?.status !== params.status) {
1205
+ skippedForFilters++;
1206
+ continue;
1207
+ }
1208
+ matches.push({
1209
+ file,
1210
+ receipt_id: content.auth?.rid,
1211
+ interaction_id: interaction?.interaction_id,
1212
+ workflow_id: workflow?.workflow_id,
1213
+ tool_name: interaction?.tool?.name,
1214
+ status: interaction?.result?.status,
1215
+ started_at: interaction?.started_at,
1216
+ completed_at: interaction?.completed_at
1217
+ });
1218
+ if (matches.length >= offset + limit + 100) {
1219
+ break;
1220
+ }
1221
+ }
1222
+ matches.sort((a, b) => {
1223
+ const aTime = a.started_at ? new Date(a.started_at).getTime() : 0;
1224
+ const bTime = b.started_at ? new Date(b.started_at).getTime() : 0;
1225
+ if (bTime !== aTime) return bTime - aTime;
1226
+ return a.file.localeCompare(b.file);
1227
+ });
1228
+ const paginated = matches.slice(offset, offset + limit);
1229
+ const warnings = [];
1230
+ if (receiptFiles.length > MAX_QUERY_FILE_PARSE) {
1231
+ warnings.push(`Results capped at ${MAX_QUERY_FILE_PARSE} files`);
1232
+ }
1233
+ if (filesSkippedForSize > 0) {
1234
+ warnings.push(
1235
+ `${filesSkippedForSize} files skipped (exceeded ${MAX_FILE_SIZE_BYTES} byte limit)`
1236
+ );
1237
+ }
1238
+ if (filesSkippedForInvalidJson > 0) {
1239
+ warnings.push(`${filesSkippedForInvalidJson} files skipped (invalid JSON)`);
1240
+ }
1241
+ const hasTruncation = receiptFiles.length > MAX_QUERY_FILE_PARSE || filesSkippedForSize > 0 || filesSkippedForInvalidJson > 0;
1242
+ return {
1243
+ status: "ok",
1244
+ total: matches.length,
1245
+ offset,
1246
+ limit,
1247
+ results: paginated,
1248
+ truncated: hasTruncation,
1249
+ skipped: hasTruncation ? {
1250
+ too_large: filesSkippedForSize,
1251
+ invalid_json: filesSkippedForInvalidJson,
1252
+ capped: receiptFiles.length > MAX_QUERY_FILE_PARSE ? receiptFiles.length - MAX_QUERY_FILE_PARSE : 0
1253
+ } : void 0,
1254
+ ...warnings.length > 0 && { warning: warnings.join("; ") }
1255
+ };
1256
+ } catch (error) {
1257
+ logger.error("Query failed:", error);
1258
+ return {
1259
+ status: "error",
1260
+ total: 0,
1261
+ offset: 0,
1262
+ limit: params.limit || 100,
1263
+ results: [],
1264
+ error: error instanceof Error ? error.message : "Unknown error"
1265
+ };
1266
+ }
1267
+ }
1268
+ };
1269
+ }
1270
+
1271
+ export { OPENCLAW_ERROR_CODES, OPENCLAW_EXTENSION_KEYS, OPENCLAW_WARNING_CODES, buildWorkflowContext, captureBatch, captureParallel, createBackgroundService, createExportBundleTool, createFileReceiptWriter, createHookHandler, createJwkSigner, createPluginInstance, createQueryTool, createReceiptEmitter, createSessionHistoryTailer, createStatusTool, createVerifyTool, extractWorkflowId, generateKeyId, mapToolCallEvent, mapToolCallEventBatch, resolveSigner };
1272
+ //# sourceMappingURL=index.mjs.map
1273
+ //# sourceMappingURL=index.mjs.map