@neurcode-ai/cli 0.19.3 → 0.19.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MAX_RUNTIME_DELIVERY_ATTEMPTS = exports.MAX_RUNTIME_DEAD_LETTER_EVENTS = exports.MAX_RUNTIME_OUTBOX_EVENTS = exports.RUNTIME_DELIVERY_SCHEMA_VERSION = exports.RUNTIME_OUTBOX_SCHEMA_VERSION = void 0;
3
+ exports.MAX_RUNTIME_DELIVERY_ATTEMPTS = exports.MAX_RUNTIME_DEAD_LETTER_EVENTS = exports.MAX_RUNTIME_OUTBOX_EVENTS = exports.RUNTIME_PRIVACY_AUDIT_SCHEMA_VERSION = exports.RUNTIME_DELIVERY_SCHEMA_VERSION = exports.LEGACY_RUNTIME_OUTBOX_SCHEMA_VERSION = exports.RUNTIME_OUTBOX_SCHEMA_VERSION = void 0;
4
4
  exports.runtimeOutboxPath = runtimeOutboxPath;
5
5
  exports.enqueueRuntimeSessionSnapshot = enqueueRuntimeSessionSnapshot;
6
6
  exports.enqueueRuntimeApprovalAck = enqueueRuntimeApprovalAck;
@@ -11,14 +11,21 @@ exports.markRuntimeOutboxDelivered = markRuntimeOutboxDelivered;
11
11
  exports.markRuntimeOutboxFailed = markRuntimeOutboxFailed;
12
12
  exports.retryRuntimeDeadLetters = retryRuntimeDeadLetters;
13
13
  exports.inspectRuntimeOutbox = inspectRuntimeOutbox;
14
+ exports.auditRuntimePrivacy = auditRuntimePrivacy;
14
15
  const crypto_1 = require("crypto");
15
16
  const fs_1 = require("fs");
16
17
  const path_1 = require("path");
18
+ const governance_runtime_1 = require("@neurcode-ai/governance-runtime");
17
19
  const gitignore_1 = require("./gitignore");
18
- exports.RUNTIME_OUTBOX_SCHEMA_VERSION = 'neurcode.runtime-outbox.v1';
20
+ const runtime_privacy_1 = require("./runtime-privacy");
21
+ exports.RUNTIME_OUTBOX_SCHEMA_VERSION = 'neurcode.runtime-outbox.v2';
22
+ exports.LEGACY_RUNTIME_OUTBOX_SCHEMA_VERSION = 'neurcode.runtime-outbox.v1';
19
23
  exports.RUNTIME_DELIVERY_SCHEMA_VERSION = 'neurcode.runtime-delivery.v1';
24
+ exports.RUNTIME_PRIVACY_AUDIT_SCHEMA_VERSION = 'neurcode.runtime-privacy-audit.v1';
20
25
  const OUTBOX_FILE = 'runtime-outbox.json';
21
26
  const OUTBOX_LOCK = 'runtime-outbox.lock';
27
+ const OUTBOX_QUARANTINE_FILE = 'runtime-outbox-quarantine.json';
28
+ const OUTBOX_BACKUP_DIR = 'runtime-outbox-backups';
22
29
  exports.MAX_RUNTIME_OUTBOX_EVENTS = 1_000;
23
30
  exports.MAX_RUNTIME_DEAD_LETTER_EVENTS = 100;
24
31
  exports.MAX_RUNTIME_DELIVERY_ATTEMPTS = 5;
@@ -42,6 +49,7 @@ function emptyOutbox() {
42
49
  nextSequenceBySession: {},
43
50
  events: [],
44
51
  deadLetters: [],
52
+ quarantined: [],
45
53
  state: {
46
54
  lastEnqueuedAt: null,
47
55
  lastAttemptAt: null,
@@ -52,6 +60,9 @@ function emptyOutbox() {
52
60
  lastDeadLetteredEventId: null,
53
61
  lastDeadLetterError: null,
54
62
  lastRecoveredAt: null,
63
+ lastPrivacyScanAt: null,
64
+ lastPrivacyMigrationAt: null,
65
+ lastPrivacyQuarantineAt: null,
55
66
  },
56
67
  };
57
68
  }
@@ -102,7 +113,18 @@ function withOutboxLock(repoRoot, action) {
102
113
  }
103
114
  }
104
115
  function stablePayloadHash(payload) {
105
- return (0, crypto_1.createHash)('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 32);
116
+ const stable = (value) => {
117
+ if (Array.isArray(value))
118
+ return `[${value.map(stable).join(',')}]`;
119
+ if (value && typeof value === 'object') {
120
+ return `{${Object.entries(value)
121
+ .sort(([left], [right]) => left.localeCompare(right))
122
+ .map(([key, child]) => `${JSON.stringify(key)}:${stable(child)}`)
123
+ .join(',')}}`;
124
+ }
125
+ return JSON.stringify(value);
126
+ };
127
+ return (0, crypto_1.createHash)('sha256').update(stable(payload)).digest('hex').slice(0, 32);
106
128
  }
107
129
  function assertSourceFree(value, path = 'payload') {
108
130
  if (Array.isArray(value)) {
@@ -140,7 +162,8 @@ function normalizeOutbox(value) {
140
162
  if (!value || typeof value !== 'object' || Array.isArray(value))
141
163
  return emptyOutbox();
142
164
  const input = value;
143
- if (input.schemaVersion !== exports.RUNTIME_OUTBOX_SCHEMA_VERSION)
165
+ if (input.schemaVersion !== exports.RUNTIME_OUTBOX_SCHEMA_VERSION
166
+ && input.schemaVersion !== exports.LEGACY_RUNTIME_OUTBOX_SCHEMA_VERSION)
144
167
  return emptyOutbox();
145
168
  return {
146
169
  schemaVersion: exports.RUNTIME_OUTBOX_SCHEMA_VERSION,
@@ -149,6 +172,13 @@ function normalizeOutbox(value) {
149
172
  : {},
150
173
  events: Array.isArray(input.events) ? input.events.filter(isRuntimeOutboxEvent) : [],
151
174
  deadLetters: Array.isArray(input.deadLetters) ? input.deadLetters.filter(isRuntimeDeadLetterEvent) : [],
175
+ quarantined: Array.isArray(input.quarantined)
176
+ ? input.quarantined.filter((entry) => Boolean(entry)
177
+ && typeof entry === 'object'
178
+ && typeof entry.eventId === 'string'
179
+ && typeof entry.sessionId === 'string'
180
+ && typeof entry.quarantinedAt === 'string')
181
+ : [],
152
182
  state: {
153
183
  lastEnqueuedAt: input.state?.lastEnqueuedAt || null,
154
184
  lastAttemptAt: input.state?.lastAttemptAt || null,
@@ -159,6 +189,9 @@ function normalizeOutbox(value) {
159
189
  lastDeadLetteredEventId: input.state?.lastDeadLetteredEventId || null,
160
190
  lastDeadLetterError: input.state?.lastDeadLetterError || null,
161
191
  lastRecoveredAt: input.state?.lastRecoveredAt || null,
192
+ lastPrivacyScanAt: input.state?.lastPrivacyScanAt || null,
193
+ lastPrivacyMigrationAt: input.state?.lastPrivacyMigrationAt || null,
194
+ lastPrivacyQuarantineAt: input.state?.lastPrivacyQuarantineAt || null,
162
195
  },
163
196
  };
164
197
  }
@@ -181,6 +214,355 @@ function writeOutbox(repoRoot, outbox) {
181
214
  (0, fs_1.writeFileSync)(tmp, JSON.stringify(outbox, null, 2) + '\n', 'utf8');
182
215
  (0, fs_1.renameSync)(tmp, path);
183
216
  }
217
+ function quarantinePath(repoRoot) {
218
+ return (0, path_1.join)(repoRoot, '.neurcode', OUTBOX_QUARANTINE_FILE);
219
+ }
220
+ function readQuarantine(repoRoot) {
221
+ try {
222
+ const path = quarantinePath(repoRoot);
223
+ if (!(0, fs_1.existsSync)(path)) {
224
+ return {
225
+ schemaVersion: 'neurcode.runtime-outbox-quarantine.v1',
226
+ classification: 'local_private',
227
+ entries: [],
228
+ };
229
+ }
230
+ const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
231
+ return {
232
+ schemaVersion: 'neurcode.runtime-outbox-quarantine.v1',
233
+ classification: 'local_private',
234
+ entries: Array.isArray(parsed.entries) ? parsed.entries.slice(-exports.MAX_RUNTIME_DEAD_LETTER_EVENTS) : [],
235
+ };
236
+ }
237
+ catch {
238
+ return {
239
+ schemaVersion: 'neurcode.runtime-outbox-quarantine.v1',
240
+ classification: 'local_private',
241
+ entries: [],
242
+ };
243
+ }
244
+ }
245
+ function writeRestrictedJson(path, value) {
246
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true, mode: 0o700 });
247
+ const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
248
+ (0, fs_1.writeFileSync)(tmp, JSON.stringify(value, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
249
+ (0, fs_1.chmodSync)(tmp, 0o600);
250
+ (0, fs_1.renameSync)(tmp, path);
251
+ (0, fs_1.chmodSync)(path, 0o600);
252
+ }
253
+ function writeQuarantine(repoRoot, entries) {
254
+ const current = readQuarantine(repoRoot);
255
+ const byEventId = new Map(current.entries.map((entry) => [entry.event.eventId, entry]));
256
+ entries.forEach((entry) => byEventId.set(entry.event.eventId, entry));
257
+ writeRestrictedJson(quarantinePath(repoRoot), {
258
+ schemaVersion: 'neurcode.runtime-outbox-quarantine.v1',
259
+ classification: 'local_private',
260
+ entries: Array.from(byEventId.values())
261
+ .sort((left, right) => left.quarantinedAt.localeCompare(right.quarantinedAt))
262
+ .slice(-exports.MAX_RUNTIME_DEAD_LETTER_EVENTS),
263
+ });
264
+ }
265
+ function createRestrictedBackup(repoRoot, outbox) {
266
+ if (outbox.events.length === 0 && outbox.deadLetters.length === 0)
267
+ return false;
268
+ const backupPath = (0, path_1.join)(repoRoot, '.neurcode', OUTBOX_BACKUP_DIR, `runtime-outbox.${Date.now()}.json`);
269
+ writeRestrictedJson(backupPath, {
270
+ classification: 'local_private',
271
+ reason: 'intent_privacy_migration_backup',
272
+ outbox,
273
+ });
274
+ return true;
275
+ }
276
+ function preparePayload(eventType, payload) {
277
+ assertSourceFree(payload);
278
+ const rawValidation = (0, governance_runtime_1.validatePrivacySafeCloudPayload)(payload);
279
+ const semanticPathIssues = rawValidation.issues.filter((issue) => /\.(?:path|filePath|paths|pathTokens|expectedFiles|expectedGlobs|allowedGlobs|sensitiveGlobs|approvalRequiredGlobs|approvedPaths|safeSupportGlobs|ignoredGlobs|expectedPathGlobs|supportPathGlobs|outOfScopeGlobs|addedFiles|addedGlobs|blockedPath|suggestedApprovalPath|requiredPath|appliedPath)(?:\[|\.|$)/i
280
+ .test(issue.fieldPath));
281
+ if (semanticPathIssues.length > 0) {
282
+ const description = semanticPathIssues
283
+ .slice(0, 12)
284
+ .map((issue) => `${issue.fieldPath}:${issue.reasonCode}`)
285
+ .join(', ');
286
+ throw new Error(`intent privacy validation failed (${description})`);
287
+ }
288
+ const projected = eventType === 'session_snapshot'
289
+ ? (0, runtime_privacy_1.projectRuntimePayloadForCloud)(payload)
290
+ : payload;
291
+ assertRuntimeCloudPayloadShape(eventType, projected);
292
+ assertSourceFree(projected);
293
+ (0, governance_runtime_1.assertPrivacySafeCloudPayload)(projected);
294
+ return projected;
295
+ }
296
+ function recordValue(value) {
297
+ return value && typeof value === 'object' && !Array.isArray(value)
298
+ ? value
299
+ : null;
300
+ }
301
+ function assertAllowedKeys(value, allowed, path) {
302
+ const record = recordValue(value);
303
+ if (!record)
304
+ throw new Error(`intent privacy validation failed (${path}:invalid_schema)`);
305
+ const allowedKeys = new Set(allowed);
306
+ const unknown = Object.keys(record).find((key) => !allowedKeys.has(key));
307
+ if (unknown) {
308
+ throw new Error(`intent privacy validation failed (${path}.${unknown}:forbidden_field)`);
309
+ }
310
+ return record;
311
+ }
312
+ function assertRuntimeSessionShape(value) {
313
+ const session = assertAllowedKeys(value, [
314
+ 'schemaVersion',
315
+ 'cloudSchemaVersion',
316
+ 'runtimeLiveSchemaVersion',
317
+ 'sessionId',
318
+ 'repoName',
319
+ 'profileHash',
320
+ 'status',
321
+ 'startedAt',
322
+ 'finishedAt',
323
+ 'replayHash',
324
+ 'intentSummary',
325
+ 'contract',
326
+ 'events',
327
+ 'livePayload',
328
+ 'privacy',
329
+ ], 'payload.session');
330
+ const summary = assertAllowedKeys(session.intentSummary, [
331
+ 'schemaVersion',
332
+ 'policyVersion',
333
+ 'intentHash',
334
+ 'categories',
335
+ 'domains',
336
+ 'paths',
337
+ 'planRevision',
338
+ 'scopeMode',
339
+ 'ruleIds',
340
+ 'counts',
341
+ 'actorType',
342
+ 'createdAt',
343
+ 'updatedAt',
344
+ 'redaction',
345
+ 'provenance',
346
+ 'contentAvailable',
347
+ ], 'payload.session.intentSummary');
348
+ assertAllowedKeys(summary.counts, [
349
+ 'characters',
350
+ 'lines',
351
+ 'paths',
352
+ 'planSteps',
353
+ 'events',
354
+ ], 'payload.session.intentSummary.counts');
355
+ assertAllowedKeys(summary.redaction, [
356
+ 'status',
357
+ 'reasonCodes',
358
+ ], 'payload.session.intentSummary.redaction');
359
+ assertAllowedKeys(summary.provenance, [
360
+ 'classification',
361
+ 'source',
362
+ ], 'payload.session.intentSummary.provenance');
363
+ assertAllowedKeys(session.contract, [
364
+ 'scopeMode',
365
+ 'allowedGlobs',
366
+ 'sensitiveGlobs',
367
+ 'approvalRequiredGlobs',
368
+ 'approvedPaths',
369
+ 'planRevision',
370
+ 'planVersionCount',
371
+ 'pendingPlanAmendmentCount',
372
+ 'architecture',
373
+ 'ruleIds',
374
+ ], 'payload.session.contract');
375
+ const contract = recordValue(session.contract);
376
+ assertAllowedKeys(contract?.architecture, [
377
+ 'total',
378
+ 'pending',
379
+ 'satisfied',
380
+ 'waived',
381
+ 'criticalPending',
382
+ ], 'payload.session.contract.architecture');
383
+ if (!Array.isArray(session.events)) {
384
+ throw new Error('intent privacy validation failed (payload.session.events:invalid_schema)');
385
+ }
386
+ session.events.forEach((event, index) => {
387
+ const eventRecord = assertAllowedKeys(event, [
388
+ 'type',
389
+ 'ts',
390
+ 'filePath',
391
+ 'verdict',
392
+ 'decision',
393
+ 'reasonCodes',
394
+ 'detail',
395
+ ], `payload.session.events[${index}]`);
396
+ if (eventRecord.detail !== undefined) {
397
+ assertAllowedKeys(eventRecord.detail, [
398
+ 'boundaryVerdict',
399
+ 'blockType',
400
+ 'filePath',
401
+ 'operatorActionKind',
402
+ 'suggestedApprovalPath',
403
+ 'blockedPath',
404
+ 'owners',
405
+ 'planRevision',
406
+ 'planAmendmentStatus',
407
+ 'architectureStatus',
408
+ 'obligationId',
409
+ ], `payload.session.events[${index}].detail`);
410
+ }
411
+ });
412
+ assertAllowedKeys(session.livePayload, [
413
+ 'schemaVersion',
414
+ 'compacted',
415
+ 'originalEventCount',
416
+ 'includedEventCount',
417
+ 'rawIntentIncluded',
418
+ 'rawPlanIncluded',
419
+ 'rawChatIncluded',
420
+ 'localStateClassification',
421
+ ], 'payload.session.livePayload');
422
+ assertAllowedKeys(session.privacy, [
423
+ 'policyVersion',
424
+ 'classification',
425
+ 'sourceIncluded',
426
+ 'diffIncluded',
427
+ 'promptIncluded',
428
+ 'chatIncluded',
429
+ 'planProseIncluded',
430
+ 'contentUnavailableByDesign',
431
+ ], 'payload.session.privacy');
432
+ }
433
+ function assertRuntimeCloudPayloadShape(eventType, payload) {
434
+ if (eventType === 'approval_ack') {
435
+ const ack = assertAllowedKeys(payload, ['approvalId', 'body'], 'payload');
436
+ assertAllowedKeys(ack.body, [
437
+ 'status',
438
+ 'appliedPath',
439
+ 'expiresAt',
440
+ 'reasonCode',
441
+ ], 'payload.body');
442
+ return;
443
+ }
444
+ if (eventType === 'scope_amendment_ack') {
445
+ const ack = assertAllowedKeys(payload, ['amendmentId', 'body'], 'payload');
446
+ assertAllowedKeys(ack.body, [
447
+ 'status',
448
+ 'appliedRevision',
449
+ 'reasonCode',
450
+ ], 'payload.body');
451
+ return;
452
+ }
453
+ const envelope = assertAllowedKeys(payload, [
454
+ 'repo',
455
+ 'generatedAt',
456
+ 'session',
457
+ 'migration',
458
+ ], 'payload');
459
+ if (envelope.repo !== undefined) {
460
+ const repo = assertAllowedKeys(envelope.repo, [
461
+ 'name',
462
+ 'repoKey',
463
+ 'rootHash',
464
+ 'remoteHash',
465
+ 'profileHash',
466
+ 'topologyHash',
467
+ 'profileFreshness',
468
+ 'source',
469
+ ], 'payload.repo');
470
+ if (repo.profileFreshness !== undefined) {
471
+ assertAllowedKeys(repo.profileFreshness, [
472
+ 'status',
473
+ 'refreshed',
474
+ 'action',
475
+ 'sessionCompatibility',
476
+ 'checkedAt',
477
+ 'profilePath',
478
+ 'reasons',
479
+ 'cachedProfileHash',
480
+ 'cachedTopologyHash',
481
+ 'sessionProfileHash',
482
+ 'currentProfileHash',
483
+ 'currentTopologyHash',
484
+ 'trackedFileCount',
485
+ 'recoveryReason',
486
+ 'recoveryCommand',
487
+ 'unresolvedHumanDecisions',
488
+ ], 'payload.repo.profileFreshness');
489
+ }
490
+ }
491
+ assertRuntimeSessionShape(envelope.session);
492
+ if (envelope.migration !== undefined) {
493
+ assertAllowedKeys(envelope.migration, [
494
+ 'from',
495
+ 'to',
496
+ 'reasonCodes',
497
+ ], 'payload.migration');
498
+ }
499
+ }
500
+ function payloadChanged(left, right) {
501
+ return stablePayloadHash(left) !== stablePayloadHash(right);
502
+ }
503
+ function incrementReasonCounts(counts, reasons) {
504
+ for (const reason of reasons)
505
+ counts[reason] = (counts[reason] || 0) + 1;
506
+ }
507
+ function prepareRuntimeOutboxForDelivery(repoRoot, options = {}) {
508
+ return withOutboxLock(repoRoot, () => {
509
+ const outbox = readOutbox(repoRoot);
510
+ const now = new Date().toISOString();
511
+ const retained = [];
512
+ const quarantineEntries = [];
513
+ const quarantineMetadata = [...outbox.quarantined];
514
+ const reasonCodeCounts = {};
515
+ let migrated = 0;
516
+ let quarantined = 0;
517
+ let backupCreated = false;
518
+ for (const event of outbox.events) {
519
+ try {
520
+ const prepared = preparePayload(event.eventType, event.payload);
521
+ if (payloadChanged(prepared, event.payload)) {
522
+ migrated += 1;
523
+ retained.push({
524
+ ...event,
525
+ payload: prepared,
526
+ payloadHash: stablePayloadHash(prepared),
527
+ lastError: null,
528
+ nextAttemptAt: null,
529
+ });
530
+ }
531
+ else {
532
+ retained.push(event);
533
+ }
534
+ }
535
+ catch (error) {
536
+ const reasons = (0, runtime_privacy_1.privacyReasonCodesFromError)(error);
537
+ incrementReasonCounts(reasonCodeCounts, reasons);
538
+ quarantined += 1;
539
+ quarantineEntries.push({ event, quarantinedAt: now, reasonCodes: reasons });
540
+ quarantineMetadata.push({
541
+ eventId: event.eventId,
542
+ sessionId: event.sessionId,
543
+ eventType: event.eventType,
544
+ quarantinedAt: now,
545
+ reasonCodes: reasons,
546
+ });
547
+ }
548
+ }
549
+ if (migrated > 0 || quarantined > 0) {
550
+ if (options.createBackup !== false)
551
+ backupCreated = createRestrictedBackup(repoRoot, outbox);
552
+ if (quarantineEntries.length > 0)
553
+ writeQuarantine(repoRoot, quarantineEntries);
554
+ outbox.events = retained;
555
+ outbox.quarantined = Array.from(new Map(quarantineMetadata.map((entry) => [entry.eventId, entry])).values())
556
+ .sort((left, right) => left.quarantinedAt.localeCompare(right.quarantinedAt))
557
+ .slice(-exports.MAX_RUNTIME_DEAD_LETTER_EVENTS);
558
+ outbox.state.lastPrivacyMigrationAt = migrated > 0 ? now : outbox.state.lastPrivacyMigrationAt;
559
+ outbox.state.lastPrivacyQuarantineAt = quarantined > 0 ? now : outbox.state.lastPrivacyQuarantineAt;
560
+ }
561
+ outbox.state.lastPrivacyScanAt = now;
562
+ writeOutbox(repoRoot, outbox);
563
+ return { migrated, quarantined, backupCreated, reasonCodeCounts };
564
+ });
565
+ }
184
566
  function nextSequence(outbox, sessionId) {
185
567
  const sequence = Math.max(0, Number(outbox.nextSequenceBySession[sessionId] || 0)) + 1;
186
568
  outbox.nextSequenceBySession[sessionId] = sequence;
@@ -206,18 +588,18 @@ function trimDeadLetters(events) {
206
588
  .slice(-exports.MAX_RUNTIME_DEAD_LETTER_EVENTS);
207
589
  }
208
590
  function enqueue(repoRoot, sessionId, eventType, payload) {
209
- assertSourceFree(payload);
591
+ const preparedPayload = preparePayload(eventType, payload);
210
592
  return withOutboxLock(repoRoot, () => {
211
593
  const outbox = readOutbox(repoRoot);
212
594
  const actionId = (eventType === 'approval_ack' || eventType === 'scope_amendment_ack')
213
- && (typeof payload.approvalId === 'string' || typeof payload.amendmentId === 'string')
214
- ? String(payload.approvalId || payload.amendmentId)
595
+ && (typeof preparedPayload.approvalId === 'string' || typeof preparedPayload.amendmentId === 'string')
596
+ ? String(preparedPayload.approvalId || preparedPayload.amendmentId)
215
597
  : null;
216
598
  const actionStatus = (eventType === 'approval_ack' || eventType === 'scope_amendment_ack')
217
- && typeof payload.body === 'object'
218
- && payload.body !== null
219
- && typeof payload.body.status === 'string'
220
- ? payload.body.status
599
+ && typeof preparedPayload.body === 'object'
600
+ && preparedPayload.body !== null
601
+ && typeof preparedPayload.body.status === 'string'
602
+ ? preparedPayload.body.status
221
603
  : null;
222
604
  if (actionId) {
223
605
  const existing = outbox.events.find((event) => event.eventType === eventType
@@ -238,8 +620,8 @@ function enqueue(repoRoot, sessionId, eventType, payload) {
238
620
  sequence: nextSequence(outbox, sessionId),
239
621
  eventType,
240
622
  generatedAt,
241
- payloadHash: stablePayloadHash(payload),
242
- payload,
623
+ payloadHash: stablePayloadHash(preparedPayload),
624
+ payload: preparedPayload,
243
625
  attemptCount: 0,
244
626
  nextAttemptAt: null,
245
627
  lastAttemptAt: null,
@@ -275,6 +657,7 @@ function runtimeDeliveryEnvelope(event) {
275
657
  };
276
658
  }
277
659
  function pendingRuntimeOutboxEvents(repoRoot, options = {}) {
660
+ prepareRuntimeOutboxForDelivery(repoRoot);
278
661
  const nowMs = options.nowMs ?? Date.now();
279
662
  const limit = Math.max(1, Math.min(options.limit ?? 10, 100));
280
663
  return readOutbox(repoRoot).events
@@ -385,7 +768,7 @@ function inspectRuntimeOutbox(repoRoot) {
385
768
  .sort();
386
769
  return {
387
770
  schemaVersion: exports.RUNTIME_OUTBOX_SCHEMA_VERSION,
388
- health: deadLetters.length > 0
771
+ health: deadLetters.length > 0 || outbox.quarantined.length > 0
389
772
  ? 'degraded'
390
773
  : retryingEvents.length > 0
391
774
  ? 'retrying'
@@ -397,6 +780,7 @@ function inspectRuntimeOutbox(repoRoot) {
397
780
  pendingApprovalAcks: sorted.filter((event) => event.eventType === 'approval_ack' || event.eventType === 'scope_amendment_ack').length,
398
781
  retryingEvents: retryingEvents.length,
399
782
  deadLetterEvents: deadLetters.length,
783
+ quarantinedEvents: outbox.quarantined.length,
400
784
  deadLetterSessionSnapshots: deadLetters.filter((event) => event.eventType === 'session_snapshot').length,
401
785
  deadLetterApprovalAcks: deadLetters.filter((event) => event.eventType === 'approval_ack' || event.eventType === 'scope_amendment_ack').length,
402
786
  oldestPendingAt: sorted[0]?.generatedAt || null,
@@ -412,4 +796,175 @@ function inspectRuntimeOutbox(repoRoot) {
412
796
  lastRecoveredAt: outbox.state.lastRecoveredAt,
413
797
  };
414
798
  }
799
+ function scanSensitiveStrings(value, reasonCounts) {
800
+ let found = false;
801
+ const visit = (entry) => {
802
+ if (typeof entry === 'string') {
803
+ const sanitized = (0, governance_runtime_1.sanitizeLocalPrivateText)(entry, Math.max(entry.length, 1));
804
+ if (sanitized.redacted) {
805
+ found = true;
806
+ incrementReasonCounts(reasonCounts, sanitized.reasonCodes);
807
+ }
808
+ return;
809
+ }
810
+ if (Array.isArray(entry)) {
811
+ entry.forEach(visit);
812
+ return;
813
+ }
814
+ if (!entry || typeof entry !== 'object')
815
+ return;
816
+ Object.values(entry).forEach(visit);
817
+ };
818
+ visit(value);
819
+ return found;
820
+ }
821
+ function isPrivacySafeAIChangeRecord(value) {
822
+ const envelopeRecord = value.record && typeof value.record === 'object' && !Array.isArray(value.record)
823
+ ? value.record
824
+ : value;
825
+ const session = envelopeRecord.session && typeof envelopeRecord.session === 'object' && !Array.isArray(envelopeRecord.session)
826
+ ? envelopeRecord.session
827
+ : {};
828
+ const intent = envelopeRecord.intent && typeof envelopeRecord.intent === 'object' && !Array.isArray(envelopeRecord.intent)
829
+ ? envelopeRecord.intent
830
+ : {};
831
+ const summary = intent.summary;
832
+ if (!(0, governance_runtime_1.isIntentSummaryV1)(summary))
833
+ return false;
834
+ const safeIntentLabel = `intent-${summary.intentHash.slice(0, 12)}`;
835
+ if (session.goal !== safeIntentLabel || intent.userGoal !== safeIntentLabel)
836
+ return false;
837
+ const plan = envelopeRecord.plan && typeof envelopeRecord.plan === 'object' && !Array.isArray(envelopeRecord.plan)
838
+ ? envelopeRecord.plan
839
+ : {};
840
+ if (plan.activeSummary !== null && plan.activeSummary !== undefined)
841
+ return false;
842
+ const timeline = Array.isArray(plan.timeline) ? plan.timeline : [];
843
+ if (timeline.some((entry) => {
844
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
845
+ return true;
846
+ const item = entry;
847
+ return (item.summary !== null && item.summary !== undefined)
848
+ || (item.reason !== null && item.reason !== undefined);
849
+ }))
850
+ return false;
851
+ (0, governance_runtime_1.assertSourceFreeAIChangeRecordPayload)(envelopeRecord);
852
+ return true;
853
+ }
854
+ function auditRuntimePrivacy(repoRoot, options = {}) {
855
+ const repaired = options.repair === true
856
+ ? prepareRuntimeOutboxForDelivery(repoRoot, { createBackup: true })
857
+ : null;
858
+ const outbox = readOutbox(repoRoot);
859
+ const reasonCodeCounts = {};
860
+ let safe = 0;
861
+ let migrated = repaired?.migrated ?? 0;
862
+ let pendingOutboxMigrations = 0;
863
+ let pendingUnsafe = 0;
864
+ let rejected = 0;
865
+ let entriesScanned = outbox.quarantined.length;
866
+ let filesScanned = (0, fs_1.existsSync)(runtimeOutboxPath(repoRoot)) ? 1 : 0;
867
+ if ((0, fs_1.existsSync)(quarantinePath(repoRoot)))
868
+ filesScanned += 1;
869
+ for (const entry of outbox.quarantined) {
870
+ incrementReasonCounts(reasonCodeCounts, entry.reasonCodes);
871
+ }
872
+ for (const event of [...outbox.events, ...outbox.deadLetters]) {
873
+ entriesScanned += 1;
874
+ try {
875
+ const prepared = preparePayload(event.eventType, event.payload);
876
+ if (payloadChanged(prepared, event.payload)) {
877
+ migrated += 1;
878
+ pendingOutboxMigrations += 1;
879
+ }
880
+ else
881
+ safe += 1;
882
+ }
883
+ catch (error) {
884
+ pendingUnsafe += 1;
885
+ incrementReasonCounts(reasonCodeCounts, (0, runtime_privacy_1.privacyReasonCodesFromError)(error));
886
+ }
887
+ }
888
+ const sessionDir = (0, path_1.join)(repoRoot, '.neurcode', 'sessions');
889
+ if ((0, fs_1.existsSync)(sessionDir)) {
890
+ for (const file of (0, fs_1.readdirSync)(sessionDir).filter((name) => name.endsWith('.json')).sort()) {
891
+ filesScanned += 1;
892
+ entriesScanned += 1;
893
+ try {
894
+ const value = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(sessionDir, file), 'utf8'));
895
+ if (file.endsWith('.change-record.json')) {
896
+ const hasSensitiveText = scanSensitiveStrings(value, reasonCodeCounts);
897
+ if (hasSensitiveText) {
898
+ rejected += 1;
899
+ continue;
900
+ }
901
+ try {
902
+ if (isPrivacySafeAIChangeRecord(value))
903
+ safe += 1;
904
+ else {
905
+ migrated += 1;
906
+ reasonCodeCounts.legacy_raw_intent = (reasonCodeCounts.legacy_raw_intent || 0) + 1;
907
+ }
908
+ }
909
+ catch {
910
+ migrated += 1;
911
+ reasonCodeCounts.legacy_raw_intent = (reasonCodeCounts.legacy_raw_intent || 0) + 1;
912
+ }
913
+ continue;
914
+ }
915
+ const privacy = value.privacy && typeof value.privacy === 'object'
916
+ ? value.privacy
917
+ : null;
918
+ const hasSensitiveText = scanSensitiveStrings(value, reasonCodeCounts);
919
+ if (hasSensitiveText) {
920
+ rejected += 1;
921
+ }
922
+ else if (privacy?.classification === 'local_private'
923
+ && privacy?.policyVersion === 'neurcode.intent-privacy.v1') {
924
+ safe += 1;
925
+ }
926
+ else {
927
+ migrated += 1;
928
+ reasonCodeCounts.legacy_raw_intent = (reasonCodeCounts.legacy_raw_intent || 0) + 1;
929
+ }
930
+ }
931
+ catch {
932
+ rejected += 1;
933
+ reasonCodeCounts.invalid_schema = (reasonCodeCounts.invalid_schema || 0) + 1;
934
+ }
935
+ }
936
+ }
937
+ const quarantinedTotal = outbox.quarantined.length;
938
+ const quarantinedThisRun = repaired?.quarantined ?? 0;
939
+ const quarantined = quarantinedTotal + pendingUnsafe;
940
+ const backupCreated = repaired?.backupCreated ?? false;
941
+ return {
942
+ schemaVersion: exports.RUNTIME_PRIVACY_AUDIT_SCHEMA_VERSION,
943
+ filesScanned,
944
+ entriesScanned,
945
+ safe,
946
+ migrated,
947
+ quarantined,
948
+ quarantinedThisRun,
949
+ quarantinedTotal,
950
+ rejected,
951
+ schemaVersions: Array.from(new Set([
952
+ exports.RUNTIME_OUTBOX_SCHEMA_VERSION,
953
+ exports.RUNTIME_DELIVERY_SCHEMA_VERSION,
954
+ ...(0, runtime_privacy_1.runtimePrivacySchemaVersions)(),
955
+ ])).sort(),
956
+ reasonCodeCounts: Object.fromEntries(Object.entries(reasonCodeCounts).sort(([left], [right]) => left.localeCompare(right))),
957
+ repairApplied: options.repair === true,
958
+ backupCreated,
959
+ nextRecoveryAction: quarantined > 0
960
+ ? 'Keep quarantined entries local; generate a new safe session snapshot before retrying delivery.'
961
+ : rejected > 0
962
+ ? 'Leave finalized legacy evidence unchanged; generate new privacy-safe evidence for cloud synchronization.'
963
+ : pendingOutboxMigrations > 0 && options.repair !== true
964
+ ? 'Run `neurcode runtime privacy-audit --repair` to rewrite only pending outbox entries with a restricted backup.'
965
+ : migrated > 0
966
+ ? 'No pending outbox repair is required; legacy local records remain unchanged and are projected safely when read.'
967
+ : 'No privacy recovery action is required.',
968
+ };
969
+ }
415
970
  //# sourceMappingURL=runtime-outbox.js.map