wendkeep 0.87.0 → 0.89.0

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 (65) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.en.md +3 -2
  3. package/README.md +3 -2
  4. package/bin/wendkeep.mjs +1 -0
  5. package/docs/en/commands/ecosystem-bridges.md +172 -0
  6. package/docs/en/commands/observer-security.md +154 -0
  7. package/docs/en/commands/observer.md +30 -12
  8. package/docs/en/commands/verify.md +6 -0
  9. package/docs/pt-BR/commands/ecosystem-bridges.md +169 -0
  10. package/docs/pt-BR/commands/observer-security.md +154 -0
  11. package/docs/pt-BR/commands/observer.md +30 -12
  12. package/docs/pt-BR/commands/verify.md +6 -0
  13. package/hooks/observer-publish.mjs +3 -1
  14. package/package.json +2 -1
  15. package/packages/cli/src/index.mjs +10 -1
  16. package/packages/harness/src/sensors-core.mjs +49 -3
  17. package/packages/integrations/src/bridge-config.mjs +139 -0
  18. package/packages/integrations/src/bridge-contract.mjs +316 -0
  19. package/packages/integrations/src/bridge-diagnostics.mjs +45 -0
  20. package/packages/integrations/src/canonical-bridge-authority.mjs +32 -0
  21. package/packages/integrations/src/capabilities.mjs +34 -0
  22. package/packages/integrations/src/ecosystem-bridge.mjs +82 -0
  23. package/packages/integrations/src/index.mjs +6 -0
  24. package/packages/integrations/src/spec-kit-adapter.mjs +259 -0
  25. package/packages/integrations/src/superpowers-adapter.mjs +269 -0
  26. package/packages/mcp/src/executor.mjs +35 -2
  27. package/packages/observer/package.json +16 -0
  28. package/packages/observer/src/audit.mjs +1 -0
  29. package/packages/observer/src/authz.mjs +38 -0
  30. package/packages/observer/src/encryption.mjs +75 -0
  31. package/packages/observer/src/index.mjs +7 -0
  32. package/packages/observer/src/policy.mjs +305 -0
  33. package/packages/observer/src/purge.mjs +100 -0
  34. package/packages/observer/src/redaction.mjs +54 -0
  35. package/packages/observer/src/retention.mjs +39 -0
  36. package/packages/observer/src/token-registry.mjs +122 -0
  37. package/schema/ecosystem-bridge-artifact-manifest-v1.schema.json +30 -0
  38. package/schema/ecosystem-bridge-v1.schema.json +65 -0
  39. package/schema/observer/006-observer-security.sql +64 -0
  40. package/schema/observer-policy-v1.schema.json +63 -0
  41. package/schema/sync-event-v1.schema.json +10 -0
  42. package/schema/wendkeep.evidence-envelope-v2.schema.json +39 -0
  43. package/schema/wendkeep.sensors.schema.json +14 -0
  44. package/src/doctor.mjs +6 -1
  45. package/src/ecosystem-bridge-artifact-collector.mjs +111 -0
  46. package/src/ecosystem-bridge-baseline.mjs +58 -0
  47. package/src/ecosystem-bridge-proof.mjs +97 -0
  48. package/src/ecosystem-bridges.mjs +227 -0
  49. package/src/evidence-envelope.mjs +2 -0
  50. package/src/observer-auth.mjs +8 -0
  51. package/src/observer-privacy.mjs +7 -3
  52. package/src/observer-publish.mjs +31 -0
  53. package/src/observer-server.mjs +179 -20
  54. package/src/observer-sql-migrate.mjs +5 -2
  55. package/src/observer-sql-publish.mjs +114 -39
  56. package/src/observer-sql-store.mjs +299 -45
  57. package/src/observer-transcript-store.mjs +23 -8
  58. package/src/observer.mjs +145 -12
  59. package/src/sync-protocol.mjs +20 -0
  60. package/src/task-contracts.mjs +19 -0
  61. package/src/task.mjs +82 -0
  62. package/src/verify.mjs +9 -0
  63. package/web/observer/app.mjs +107 -31
  64. package/web/observer/index.html +7 -0
  65. package/web/observer/styles.css +5 -0
@@ -17,6 +17,8 @@ import { parseSessionCost, } from './cost.mjs';
17
17
  import { buildSessionIdentityMap, listMigrationDocuments, parseFrontmatter, sessionEvents } from './observer-sql-migrate.mjs';
18
18
  import { observerAuthHeaders } from './observer-auth.mjs';
19
19
  import { sanitizeObserverContent, sanitizeObserverMetadata } from './observer-privacy.mjs';
20
+ import { decryptObserverValue, encryptObserverValue } from '../packages/observer/src/encryption.mjs';
21
+ import { createObserverPolicy, protectObserverEvent } from '../packages/observer/src/policy.mjs';
20
22
 
21
23
  export const SQL_OUTBOX_REL = '.brain/observer-sql-outbox';
22
24
  export const SQL_STATE_REL = '.brain/observer-sql-state.json';
@@ -57,6 +59,18 @@ function isoNow(value) {
57
59
  if (Number.isNaN(date.getTime())) throw new Error('occurred_at inválido.');
58
60
  return date.toISOString();
59
61
  }
62
+ function transcriptOccurredAt(value, fallback) {
63
+ const candidate = value === null || value === undefined
64
+ || (typeof value === 'string' && !value.trim())
65
+ ? fallback
66
+ : value;
67
+ if (!(candidate instanceof Date) && typeof candidate !== 'string' && typeof candidate !== 'number') {
68
+ throw new Error('occurred_at inválido.');
69
+ }
70
+ const date = candidate instanceof Date ? candidate : new Date(candidate);
71
+ if (Number.isNaN(date.getTime())) throw new Error('occurred_at inválido.');
72
+ return date.toISOString();
73
+ }
60
74
  function readJson(path, fallback) {
61
75
  if (!existsSync(path)) return fallback;
62
76
  try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return fallback; }
@@ -167,12 +181,13 @@ function transcriptCalls({ projectId, sessionId, agentId, role, provider, modelF
167
181
  ? 'complete'
168
182
  : hash({ prompt, response, tokens, status: turn.status }).slice(0, 16);
169
183
  const callId = eventId('call', projectId, `${transcriptId}:${agentId}:${stableTurn}:${turnRevision}`);
184
+ const callOccurredAt = transcriptOccurredAt(turn.timestamp, occurredAt);
170
185
  return [{
171
186
  schema_version: 1,
172
187
  event_id: eventId('call-event', projectId, callId),
173
188
  kind: 'llm_call',
174
189
  project_id: projectId,
175
- occurred_at: text(turn.timestamp, occurredAt),
190
+ occurred_at: callOccurredAt,
176
191
  payload: {
177
192
  call_id: callId,
178
193
  session_id: sessionId,
@@ -183,7 +198,7 @@ function transcriptCalls({ projectId, sessionId, agentId, role, provider, modelF
183
198
  model,
184
199
  effort: '',
185
200
  sequence: index + 1,
186
- occurred_at: text(turn.timestamp, occurredAt),
201
+ occurred_at: callOccurredAt,
187
202
  tokens,
188
203
  cost_usd: 0,
189
204
  cost_status: 'unknown',
@@ -223,7 +238,7 @@ function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider,
223
238
  ? agentEvent({ projectId, sessionId, agentId, parentAgentId: mainAgentId, role: 'subagent', provider, model, input: source.agentInput, occurredAt: now })
224
239
  : null;
225
240
  const fingerprint = hash(content);
226
- const transcript = captureLevel === 'full-transcript' ? {
241
+ const transcript = {
227
242
  schema_version: 1,
228
243
  event_id: eventId('transcript', projectId, `${source.transcriptId}:${fingerprint}`),
229
244
  kind: 'transcript.upsert',
@@ -238,7 +253,7 @@ function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider,
238
253
  source: 'hook-transcript',
239
254
  metadata: { source_label: basename(source.path), source_hash: hash(normalizePath(source.path)) },
240
255
  },
241
- } : null;
256
+ };
242
257
  const calls = transcriptCalls({
243
258
  projectId,
244
259
  sessionId,
@@ -249,7 +264,7 @@ function completeTranscriptEvents({ projectId, sessionId, mainAgentId, provider,
249
264
  transcriptId: source.transcriptId,
250
265
  content,
251
266
  occurredAt: now,
252
- includeMessages: captureLevel !== 'metadata',
267
+ includeMessages: true,
253
268
  });
254
269
  return { events: [agent, transcript, ...calls].filter(Boolean), fingerprint, transcriptId: source.transcriptId };
255
270
  }
@@ -263,10 +278,31 @@ function dedupeEvents(events) {
263
278
  });
264
279
  }
265
280
 
266
- export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, now = new Date(), state = readState(vaultBase), remoteDocuments = {}, captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata', forceFull = false } = {}) {
281
+ function applyObserverPolicy(events, policy) {
282
+ return events.map((event) => protectObserverEvent(event, { policy })).filter(Boolean);
283
+ }
284
+
285
+ function publisherPolicy(policy, captureLevel) {
286
+ if (policy) return createObserverPolicy(policy);
287
+ const legacy = normalizeObserverCaptureLevel(captureLevel);
288
+ return createObserverPolicy({
289
+ document_capture: 'full',
290
+ transcript_capture: legacy === 'full-transcript' ? 'full' : legacy,
291
+ prompt_capture: legacy === 'metadata' ? 'none' : 'full',
292
+ response_capture: legacy === 'metadata' ? 'none' : 'full',
293
+ usage_capture: 'calls',
294
+ });
295
+ }
296
+
297
+ function publisherCoverage(policy) {
298
+ return `policy:${policy.transcript_capture}`;
299
+ }
300
+
301
+ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, now = new Date(), state = readState(vaultBase), remoteDocuments = {}, captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata', forceFull = false, policy = null } = {}) {
267
302
  if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
268
303
  const occurredAt = isoNow(now);
269
- const resolvedCaptureLevel = normalizeObserverCaptureLevel(captureLevel);
304
+ const effectivePolicy = publisherPolicy(policy, captureLevel);
305
+ const resolvedCaptureLevel = publisherCoverage(effectivePolicy);
270
306
  const nextState = { schema_version: SQL_SCHEMA_VERSION, files: { ...(state.files || {}) }, transcripts: { ...(state.transcripts || {}) } };
271
307
  const events = [];
272
308
  const sessionContexts = [];
@@ -326,7 +362,7 @@ export function buildObserverSqlEventBatch({ vaultBase, projectId, input = {}, n
326
362
  nextState.transcripts[context.fm.observability_transcript_id] ||= { content_hash: '', coverage: 'summary_only' };
327
363
  }
328
364
  }
329
- return { events: dedupeEvents(events), nextState, scanned: Object.keys(nextState.files).length, changed };
365
+ return { events: applyObserverPolicy(dedupeEvents(events), effectivePolicy), nextState, scanned: Object.keys(nextState.files).length, changed };
330
366
  }
331
367
 
332
368
  function hookEventName(input = {}) {
@@ -366,6 +402,7 @@ export function buildObserverSqlIncrementalBatch({
366
402
  now = new Date(),
367
403
  state = readState(vaultBase),
368
404
  captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata',
405
+ policy = null,
369
406
  } = {}) {
370
407
  if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
371
408
  const eventName = hookEventName(input);
@@ -381,7 +418,8 @@ export function buildObserverSqlIncrementalBatch({
381
418
  if (!context) return { events: [], nextState, scanned: 0, changed: 0, scope: 'drain-only' };
382
419
 
383
420
  const occurredAt = isoNow(now);
384
- const resolvedCaptureLevel = normalizeObserverCaptureLevel(captureLevel);
421
+ const effectivePolicy = publisherPolicy(policy, captureLevel);
422
+ const resolvedCaptureLevel = publisherCoverage(effectivePolicy);
385
423
  const events = [];
386
424
  let changed = 0;
387
425
  const safeContent = sanitizeObserverContent(context.content);
@@ -451,7 +489,7 @@ export function buildObserverSqlIncrementalBatch({
451
489
  changed += 1;
452
490
  }
453
491
  return {
454
- events: dedupeEvents(events),
492
+ events: applyObserverPolicy(dedupeEvents(events), effectivePolicy),
455
493
  nextState,
456
494
  scanned: 1,
457
495
  changed,
@@ -459,10 +497,30 @@ export function buildObserverSqlIncrementalBatch({
459
497
  };
460
498
  }
461
499
 
462
- function queueOutbox(vaultBase, batch) {
500
+ function outboxAad(path) { return `observer-outbox:${basename(path)}`; }
501
+
502
+ function writeOutbox(path, batch, encryption = null) {
503
+ if (!encryption) return atomicJson(path, batch);
504
+ return atomicJson(path, {
505
+ schema_version: 1,
506
+ protected: true,
507
+ envelope: encryptObserverValue(encryption, JSON.stringify(batch), { aad: outboxAad(path) }),
508
+ });
509
+ }
510
+
511
+ function readOutbox(path, fallback = null, encryption = null) {
512
+ const stored = readJson(path, fallback);
513
+ if (!stored?.protected) return stored;
514
+ if (!encryption) {
515
+ throw Object.assign(new Error('Chave da outbox protegida indisponível.'), { code: 'observer_encryption_key_unavailable' });
516
+ }
517
+ return JSON.parse(decryptObserverValue(encryption, stored.envelope, { aad: outboxAad(path) }));
518
+ }
519
+
520
+ function queueOutbox(vaultBase, batch, encryption = null) {
463
521
  mkdirSync(outboxDir(vaultBase), { recursive: true });
464
522
  const path = outboxPath(vaultBase, batch);
465
- if (!existsSync(path)) atomicJson(path, batch);
523
+ if (!existsSync(path)) writeOutbox(path, batch, encryption);
466
524
  return path;
467
525
  }
468
526
 
@@ -521,29 +579,32 @@ function releaseBatchFileLease(lease) {
521
579
  }
522
580
 
523
581
  /** Queue a precise writer batch and replace older pending state for the same logical scope. */
524
- export function enqueueObserverSqlBatch(vaultBase, batch, { scope = 'incremental', now = new Date() } = {}) {
582
+ export function enqueueObserverSqlBatch(vaultBase, batch, { scope = 'incremental', now = new Date(), encryption = null, policy = null } = {}) {
525
583
  if (!batch?.project_id || !Array.isArray(batch.events)) throw new Error('Batch incremental do Observer inválido.');
584
+ if (policy?.encryption_required && !encryption) {
585
+ throw Object.assign(new Error('A policy exige criptografia antes de enfileirar eventos do Observer.'), { code: 'observer_encryption_required' });
586
+ }
526
587
  if (!batch.events.length) return { queued: false, events: 0, path: '' };
527
588
  mkdirSync(outboxDir(vaultBase), { recursive: true });
528
589
  const path = join(outboxDir(vaultBase), `sql-live-${hash(`${batch.project_id}:${scope}`).slice(0, 24)}.json`);
529
590
  const lease = acquireBatchFileLease(path, 200);
530
591
  if (!lease) {
531
- const fallback = queueOutbox(vaultBase, { ...batch, enqueued_at: isoNow(now), scope });
592
+ const fallback = queueOutbox(vaultBase, { ...batch, enqueued_at: isoNow(now), scope }, encryption);
532
593
  return { queued: true, events: batch.events.length, path: fallback, coalesced: false };
533
594
  }
534
595
  try {
535
- const existing = readJson(path, { events: [] });
596
+ const existing = existsSync(path) ? readOutbox(path, { events: [] }, encryption) : { events: [] };
536
597
  const merged = new Map();
537
598
  for (const event of [...(existing.events || []), ...batch.events]) merged.set(eventCoalesceKey(event), event);
538
599
  const timestamp = isoNow(now);
539
- atomicJson(path, {
600
+ writeOutbox(path, {
540
601
  schema_version: SQL_SCHEMA_VERSION,
541
602
  project_id: batch.project_id,
542
603
  scope,
543
604
  enqueued_at: existing.enqueued_at || timestamp,
544
605
  updated_at: timestamp,
545
606
  events: [...merged.values()],
546
- });
607
+ }, encryption);
547
608
  return { queued: true, events: merged.size, path, coalesced: true };
548
609
  } finally {
549
610
  releaseBatchFileLease(lease);
@@ -557,6 +618,7 @@ export function enqueueObserverDocumentChange({
557
618
  logicalPath,
558
619
  deleted = false,
559
620
  now = new Date(),
621
+ outboxEncryption = null,
560
622
  } = {}) {
561
623
  const normalized = normalizePath(logicalPath);
562
624
  if (!vaultBase || !projectId || !normalized || normalized.startsWith('/') || normalized.includes('..')) {
@@ -599,21 +661,26 @@ export function enqueueObserverDocumentChange({
599
661
  schema_version: SQL_SCHEMA_VERSION,
600
662
  project_id: projectId,
601
663
  events: [event],
602
- }, { scope: `document:${normalized}`, now });
664
+ }, { scope: `document:${normalized}`, now, encryption: outboxEncryption });
603
665
  atomicJson(statePath(vaultBase), nextState);
604
666
  return { ...queued, event_id: event.event_id, deleted };
605
667
  }
606
668
 
607
- export function listSqlOutbox(vaultBase) {
669
+ export function listSqlOutbox(vaultBase, { encryption = null } = {}) {
608
670
  const dir = outboxDir(vaultBase);
609
671
  if (!existsSync(dir)) return [];
610
672
  return readdirSync(dir).filter((name) => /^sql-(?:live-)?[a-f0-9]{24}\.json$/.test(name)).sort().flatMap((name) => {
611
- try { return [{ path: join(dir, name), ...JSON.parse(readFileSync(join(dir, name), 'utf8')) }]; } catch { return []; }
673
+ const path = join(dir, name);
674
+ try { return [{ path, ...readOutbox(path, null, encryption) }]; }
675
+ catch (error) {
676
+ if (error?.code === 'observer_encryption_key_unavailable' || error?.code === 'observer_decryption_failed') throw error;
677
+ return [];
678
+ }
612
679
  });
613
680
  }
614
681
 
615
- export function inspectObserverSqlOutbox(vaultBase, currentTime = Date.now()) {
616
- const batches = listSqlOutbox(vaultBase);
682
+ export function inspectObserverSqlOutbox(vaultBase, currentTime = Date.now(), { encryption = null } = {}) {
683
+ const batches = listSqlOutbox(vaultBase, { encryption });
617
684
  let bytes = 0;
618
685
  let oldestAt = '';
619
686
  let events = 0;
@@ -720,8 +787,8 @@ async function postSqlBatch({ url, projectId, events, fetchImpl = globalThis.fet
720
787
  return aggregate;
721
788
  }
722
789
 
723
- export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '' } = {}) {
724
- const pending = listSqlOutbox(vaultBase);
790
+ export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '', outboxEncryption = null } = {}) {
791
+ const pending = listSqlOutbox(vaultBase, { encryption: outboxEncryption });
725
792
  if (!url) return { attempted: 0, confirmed: 0, pending: pending.length };
726
793
  const lease = acquirePublisherLease(vaultBase);
727
794
  if (!lease) return { attempted: 0, confirmed: 0, pending: pending.length, busy: true };
@@ -734,7 +801,7 @@ export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchI
734
801
  if (!batchLease) continue;
735
802
  attempted += 1;
736
803
  try {
737
- const current = readJson(batch.path, null);
804
+ const current = readOutbox(batch.path, null, outboxEncryption);
738
805
  if (!current?.events?.length) continue;
739
806
  await postSqlBatch({ url, projectId, events: current.events, fetchImpl, token });
740
807
  if (existsSync(batch.path)) unlinkSync(batch.path);
@@ -742,7 +809,7 @@ export async function retryObserverSqlOutbox({ vaultBase, projectId, url, fetchI
742
809
  } catch { break; }
743
810
  finally { releaseBatchFileLease(batchLease); }
744
811
  }
745
- return { attempted, confirmed, pending: listSqlOutbox(vaultBase).length, busy: false };
812
+ return { attempted, confirmed, pending: listSqlOutbox(vaultBase, { encryption: outboxEncryption }).length, busy: false };
746
813
  } finally {
747
814
  releasePublisherLease(lease);
748
815
  }
@@ -758,20 +825,25 @@ export async function publishObserverSqlIncremental({
758
825
  fetchImpl = globalThis.fetch,
759
826
  token = process.env.WENDKEEP_OBSERVER_TOKEN || '',
760
827
  captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata',
828
+ outboxEncryption = null,
829
+ policy = null,
761
830
  } = {}) {
831
+ if (policy?.encryption_required && !outboxEncryption) {
832
+ throw Object.assign(new Error('A policy exige criptografia da outbox do Observer.'), { code: 'observer_encryption_required' });
833
+ }
762
834
  if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
763
835
  const state = readState(vaultBase);
764
836
  const batch = buildObserverSqlIncrementalBatch({
765
- vaultBase, projectId, input, now, state, captureLevel,
837
+ vaultBase, projectId, input, now, state, captureLevel, policy,
766
838
  });
767
839
  const queued = enqueueObserverSqlBatch(vaultBase, {
768
840
  schema_version: SQL_SCHEMA_VERSION,
769
841
  project_id: projectId,
770
842
  events: batch.events,
771
- }, { scope: batch.scope, now });
843
+ }, { scope: batch.scope, now, encryption: outboxEncryption, policy });
772
844
  if (batch.events.length) atomicJson(statePath(vaultBase), batch.nextState);
773
- const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl, token });
774
- const pending = listSqlOutbox(vaultBase).length;
845
+ const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl, token, outboxEncryption });
846
+ const pending = listSqlOutbox(vaultBase, { encryption: outboxEncryption }).length;
775
847
  return {
776
848
  ok: pending === 0,
777
849
  queued: pending > 0,
@@ -784,30 +856,33 @@ export async function publishObserverSqlIncremental({
784
856
  };
785
857
  }
786
858
 
787
- export async function publishObserverSql({ vaultBase, projectId, url = process.env.WENDKEEP_OBSERVER_URL || '', input = {}, now = new Date(), fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '', captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata', forceFull = false } = {}) {
859
+ export async function publishObserverSql({ vaultBase, projectId, url = process.env.WENDKEEP_OBSERVER_URL || '', input = {}, now = new Date(), fetchImpl = globalThis.fetch, token = process.env.WENDKEEP_OBSERVER_TOKEN || '', captureLevel = process.env.WENDKEEP_OBSERVER_CAPTURE_LEVEL || 'metadata', forceFull = false, outboxEncryption = null, policy = null } = {}) {
860
+ if (policy?.encryption_required && !outboxEncryption) {
861
+ throw Object.assign(new Error('A policy exige criptografia da outbox do Observer.'), { code: 'observer_encryption_required' });
862
+ }
788
863
  if (!vaultBase || !projectId) throw new Error('vaultBase e projectId são obrigatórios.');
789
- const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl, token });
864
+ const replay = await retryObserverSqlOutbox({ vaultBase, projectId, url, fetchImpl, token, outboxEncryption });
790
865
  const state = readState(vaultBase);
791
866
  const remoteDocuments = forceFull || Object.keys(state.files || {}).length === 0
792
867
  ? await readRemoteDocuments({ url, projectId, fetchImpl, token })
793
868
  : {};
794
- const batch = buildObserverSqlEventBatch({ vaultBase, projectId, input, now, state, remoteDocuments, captureLevel, forceFull });
869
+ const batch = buildObserverSqlEventBatch({ vaultBase, projectId, input, now, state, remoteDocuments, captureLevel, forceFull, policy });
795
870
  if (!batch.events.length) {
796
871
  atomicJson(statePath(vaultBase), batch.nextState);
797
- return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay };
872
+ return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase, { encryption: outboxEncryption }).length, replay };
798
873
  }
799
874
  if (!url) {
800
- queueOutbox(vaultBase, { schema_version: SQL_SCHEMA_VERSION, project_id: projectId, events: batch.events });
875
+ queueOutbox(vaultBase, { schema_version: SQL_SCHEMA_VERSION, project_id: projectId, events: batch.events }, outboxEncryption);
801
876
  atomicJson(statePath(vaultBase), batch.nextState);
802
- return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, hookExitCode: 0 };
877
+ return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase, { encryption: outboxEncryption }).length, replay, hookExitCode: 0 };
803
878
  }
804
879
  try {
805
880
  const response = await postSqlBatch({ url, projectId, events: batch.events, fetchImpl, token });
806
881
  atomicJson(statePath(vaultBase), batch.nextState);
807
- return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, response };
882
+ return { ok: true, queued: false, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase, { encryption: outboxEncryption }).length, replay, response };
808
883
  } catch (error) {
809
- queueOutbox(vaultBase, { schema_version: SQL_SCHEMA_VERSION, project_id: projectId, events: batch.events });
884
+ queueOutbox(vaultBase, { schema_version: SQL_SCHEMA_VERSION, project_id: projectId, events: batch.events }, outboxEncryption);
810
885
  atomicJson(statePath(vaultBase), batch.nextState);
811
- return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase).length, replay, hookExitCode: 0, error: error.message };
886
+ return { ok: false, queued: true, scanned: batch.scanned, changed: batch.changed, pending: listSqlOutbox(vaultBase, { encryption: outboxEncryption }).length, replay, hookExitCode: 0, error: error.message };
812
887
  }
813
888
  }