@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.
- package/README.md +12 -1
- package/dist/commands/runtime-doctor.d.ts.map +1 -1
- package/dist/commands/runtime-doctor.js +17 -2
- package/dist/commands/runtime-doctor.js.map +1 -1
- package/dist/commands/runtime-sync.d.ts.map +1 -1
- package/dist/commands/runtime-sync.js +9 -5
- package/dist/commands/runtime-sync.js.map +1 -1
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/runtime.js +42 -0
- package/dist/commands/runtime.js.map +1 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +4 -0
- package/dist/commands/session.js.map +1 -1
- package/dist/runtime-build.json +4 -4
- package/dist/utils/runtime-live.d.ts.map +1 -1
- package/dist/utils/runtime-live.js +12 -3
- package/dist/utils/runtime-live.js.map +1 -1
- package/dist/utils/runtime-outbox.d.ts +23 -1
- package/dist/utils/runtime-outbox.d.ts.map +1 -1
- package/dist/utils/runtime-outbox.js +569 -14
- package/dist/utils/runtime-outbox.js.map +1 -1
- package/dist/utils/runtime-privacy.d.ts +11 -0
- package/dist/utils/runtime-privacy.d.ts.map +1 -0
- package/dist/utils/runtime-privacy.js +321 -0
- package/dist/utils/runtime-privacy.js.map +1 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
214
|
-
? String(
|
|
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
|
|
218
|
-
&&
|
|
219
|
-
&& typeof
|
|
220
|
-
?
|
|
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(
|
|
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
|