log-llm-config 1.3.82 → 1.3.84
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/cli.js +6 -0
- package/dist/compliance_prompt_gate.js +43 -15
- package/dist/dialog_prefs_cli.js +105 -0
- package/dist/log_config_files/readers/composer_shadow_merge.js +67 -0
- package/dist/log_config_files/readers/cursor_shadow_merge_policy.js +46 -0
- package/dist/log_config_files/readers/vscdb_config_builder.js +3 -1
- package/dist/log_config_files/readers/vscdb_reactive_storage.js +62 -0
- package/dist/log_config_files/readers/vscdb_reader.js +77 -12
- package/dist/log_config_files/runtime/compliance_check.js +242 -162
- package/dist/log_config_files/runtime/dialog_preferences.js +31 -0
- package/dist/log_config_files/runtime/management_storage.js +5 -0
- package/dist/log_config_files/runtime/ops_target_path.js +18 -0
- package/dist/log_config_files/runtime/remediation_apply_tracking.js +138 -0
- package/dist/log_config_files/runtime/remediation_config_path.js +36 -2
- package/dist/log_config_files/runtime/remediation_sync.js +166 -45
- package/dist/log_config_files/runtime/trusted_restarts.js +4 -0
- package/dist/log_config_files/runtime/workspace_repo.js +1 -1
- package/dist/log_uuid/startup_sender.js +0 -2
- package/dist/remediation_change_preview.js +182 -0
- package/dist/tofu.js +5 -32
- package/dist/tofu_environment.js +32 -0
- package/package.json +1 -1
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
-
import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
|
|
17
|
-
import {
|
|
16
|
+
import { mergeComposerShadowKeysFromReactiveBlob, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
|
|
17
|
+
import { readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
18
18
|
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
19
|
+
import { resolveOpsTargetPath } from './ops_target_path.js';
|
|
20
|
+
import { isRemediationQuarantined, markRemediationApplyPendingVerification, processPendingPostRestartVerifications, readRemediationApplyTrackingFile, writeRemediationApplyTrackingFile, } from './remediation_apply_tracking.js';
|
|
19
21
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
20
22
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
21
23
|
import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
@@ -84,6 +86,28 @@ export function itemTableKeyFromSettingPath(settingPath) {
|
|
|
84
86
|
const i = settingPath.indexOf('.');
|
|
85
87
|
return i === -1 ? settingPath : settingPath.slice(0, i);
|
|
86
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Compliance reads the live config at `config_file_path` (#itemKey), but sqlite apply may target
|
|
91
|
+
* a different ItemTable row. Verify against the manifest path's item key, not the apply target.
|
|
92
|
+
*/
|
|
93
|
+
export function canonicalComplianceSettingPath(configFilePath, check) {
|
|
94
|
+
const settingPath = check.setting_path;
|
|
95
|
+
const applyKey = check.sqlite_op?.target_key?.trim();
|
|
96
|
+
if (!applyKey || applyKey === settingPath)
|
|
97
|
+
return settingPath;
|
|
98
|
+
const hashIdx = configFilePath.indexOf('#');
|
|
99
|
+
if (hashIdx < 0)
|
|
100
|
+
return settingPath;
|
|
101
|
+
const verifyKey = configFilePath.slice(hashIdx + 1).trim();
|
|
102
|
+
if (!verifyKey || applyKey === verifyKey)
|
|
103
|
+
return settingPath;
|
|
104
|
+
const prefix = `${applyKey}.`;
|
|
105
|
+
if (settingPath === applyKey)
|
|
106
|
+
return verifyKey;
|
|
107
|
+
if (settingPath.startsWith(prefix))
|
|
108
|
+
return `${verifyKey}${settingPath.slice(applyKey.length)}`;
|
|
109
|
+
return settingPath;
|
|
110
|
+
}
|
|
87
111
|
/** Traverse a JSON object using dot-notation path. Returns undefined if any segment is missing. */
|
|
88
112
|
export function getByPath(obj, path) {
|
|
89
113
|
const parts = path.split('.');
|
|
@@ -143,13 +167,8 @@ function verifyOpsApplied(configJson, settingPath, ops) {
|
|
|
143
167
|
const add = ops.add ?? {};
|
|
144
168
|
const remove = ops.remove ?? {};
|
|
145
169
|
const keys = new Set([...Object.keys(set), ...Object.keys(add), ...Object.keys(remove)]);
|
|
146
|
-
// If ops targets a single leaf key, check at settingPath; otherwise treat keys as subkeys under parent.
|
|
147
170
|
for (const k of keys) {
|
|
148
|
-
const targetPath =
|
|
149
|
-
? settingPath
|
|
150
|
-
: parentPath
|
|
151
|
-
? `${parentPath}.${k}`
|
|
152
|
-
: k;
|
|
171
|
+
const targetPath = resolveOpsTargetPath(settingPath, k);
|
|
153
172
|
if (Object.prototype.hasOwnProperty.call(set, k)) {
|
|
154
173
|
const cur = getByPath(configJson, targetPath);
|
|
155
174
|
const expected = set[k];
|
|
@@ -179,42 +198,8 @@ function shouldMergeComposerShadowKeys(itemKeyFromPath, checkSettingPaths) {
|
|
|
179
198
|
return true;
|
|
180
199
|
return checkSettingPaths.some((p) => p === 'composerState' || p.startsWith('composerState.'));
|
|
181
200
|
}
|
|
182
|
-
/**
|
|
183
|
-
|
|
184
|
-
* Policy scan merges root shadow keys into composerState; compliance must match or autofix
|
|
185
|
-
* never runs when nested values are stale (e.g. lastBrowserConnectionMode none vs root editor).
|
|
186
|
-
*/
|
|
187
|
-
export function mergeComposerShadowKeysFromReactiveBlob(dbPath, merged) {
|
|
188
|
-
const contract = readFileCollectionVscdbContract();
|
|
189
|
-
const reactiveKey = contract?.reactive_storage_item_key?.trim();
|
|
190
|
-
const shadowKeys = contract?.composer_shadow_keys ?? [];
|
|
191
|
-
if (!reactiveKey || shadowKeys.length === 0)
|
|
192
|
-
return;
|
|
193
|
-
const reactiveWrapped = readVscdbItemTableJson(dbPath, reactiveKey);
|
|
194
|
-
if (reactiveWrapped === null)
|
|
195
|
-
return;
|
|
196
|
-
const blob = reactiveWrapped[reactiveKey];
|
|
197
|
-
if (!blob || typeof blob !== 'object' || Array.isArray(blob))
|
|
198
|
-
return;
|
|
199
|
-
const root = blob;
|
|
200
|
-
let cs = merged.composerState;
|
|
201
|
-
if (!cs || typeof cs !== 'object' || Array.isArray(cs)) {
|
|
202
|
-
const nested = root.composerState;
|
|
203
|
-
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
204
|
-
cs = { ...nested };
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
cs = {};
|
|
208
|
-
}
|
|
209
|
-
merged.composerState = cs;
|
|
210
|
-
}
|
|
211
|
-
const composerState = cs;
|
|
212
|
-
for (const key of shadowKeys) {
|
|
213
|
-
if (Object.prototype.hasOwnProperty.call(root, key) && root[key] !== undefined) {
|
|
214
|
-
composerState[key] = root[key];
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
201
|
+
/** @deprecated Import from vscdb_reader — re-export for existing tests. */
|
|
202
|
+
export { mergeComposerShadowKeysFromReactiveBlob } from '../readers/vscdb_reader.js';
|
|
218
203
|
/** Plain JSON file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
|
|
219
204
|
function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
|
|
220
205
|
const resolvedPath = resolveRemediationConfigPath(configFilePath);
|
|
@@ -270,6 +255,101 @@ function evaluateSecondaryGroup(group) {
|
|
|
270
255
|
// ---------------------------------------------------------------------------
|
|
271
256
|
// Check runner — Section 6: real per-check evaluation
|
|
272
257
|
// ---------------------------------------------------------------------------
|
|
258
|
+
function violationFromCheck(entry, compliance, check, expected) {
|
|
259
|
+
return {
|
|
260
|
+
uuid: entry.uuid,
|
|
261
|
+
finding_formatted_id: compliance.finding_formatted_id,
|
|
262
|
+
setting_path: check.setting_path,
|
|
263
|
+
description: check.description,
|
|
264
|
+
finding_title: entry.finding_title,
|
|
265
|
+
finding_description: entry.finding_description,
|
|
266
|
+
policy_name: entry.policy_name,
|
|
267
|
+
severity: compliance.severity,
|
|
268
|
+
autofix_allowed: compliance.autofix_allowed,
|
|
269
|
+
config_file_path: entry.config_file_path,
|
|
270
|
+
expected_value: expected,
|
|
271
|
+
message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Apply remediation ops for ${check.setting_path} in ${entry.config_file_path}`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Evaluate one manifest row against on-disk config (used by gate + post-restart verify). */
|
|
275
|
+
export function evaluateManifestEntryCompliance(entry) {
|
|
276
|
+
const compliance = entry.fix ?? entry.compliance;
|
|
277
|
+
if (!compliance || compliance.file_format !== 'json')
|
|
278
|
+
return { violations: [] };
|
|
279
|
+
const checks = compliance.checks ?? [];
|
|
280
|
+
if (checks.length === 0)
|
|
281
|
+
return { violations: [] };
|
|
282
|
+
const loaded = loadRemediationConfigJson(entry.config_file_path, checks.map((c) => c.setting_path));
|
|
283
|
+
if (!loaded.ok)
|
|
284
|
+
return { violations: [] };
|
|
285
|
+
const configJson = loaded.json;
|
|
286
|
+
const entryViolations = [];
|
|
287
|
+
for (const check of checks) {
|
|
288
|
+
const effectivePath = canonicalComplianceSettingPath(entry.config_file_path, check);
|
|
289
|
+
if (check.sqlite_op?.apply_to_all_workspaces && check.ops) {
|
|
290
|
+
const segments = check.sqlite_op.workspace_storage_path_segments ?? [
|
|
291
|
+
'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage',
|
|
292
|
+
];
|
|
293
|
+
const wsPath = join(homedir(), ...segments);
|
|
294
|
+
const vscdbPaths = discoverAllWorkspaceVscdbs(wsPath);
|
|
295
|
+
if (vscdbPaths.length === 0)
|
|
296
|
+
continue;
|
|
297
|
+
let violated = false;
|
|
298
|
+
let expectedForViolation = null;
|
|
299
|
+
for (const dbPath of vscdbPaths) {
|
|
300
|
+
const wrapped = readVscdbItemTableJson(dbPath, check.sqlite_op.target_key ?? '');
|
|
301
|
+
if (wrapped === null) {
|
|
302
|
+
violated = true;
|
|
303
|
+
expectedForViolation = { op: 'read_failed', db: dbPath };
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
const { ok, expected } = verifyOpsApplied(wrapped, check.setting_path, check.ops);
|
|
307
|
+
if (!ok) {
|
|
308
|
+
violated = true;
|
|
309
|
+
expectedForViolation = expected;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (violated) {
|
|
314
|
+
entryViolations.push(violationFromCheck(entry, compliance, check, expectedForViolation));
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (check.ops) {
|
|
319
|
+
const { ok, expected } = verifyOpsApplied(configJson, effectivePath, check.ops);
|
|
320
|
+
if (!ok) {
|
|
321
|
+
entryViolations.push(violationFromCheck(entry, compliance, { ...check, setting_path: effectivePath }, expected));
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const currentValue = getByPath(configJson, effectivePath);
|
|
326
|
+
const leafKey = effectivePath.split('.').pop();
|
|
327
|
+
const expectedValue = check.after?.[leafKey];
|
|
328
|
+
if (expectedValue === undefined)
|
|
329
|
+
continue;
|
|
330
|
+
if (!deepEqual(currentValue, expectedValue)) {
|
|
331
|
+
entryViolations.push(violationFromCheck(entry, compliance, { ...check, setting_path: effectivePath }, expectedValue));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (entryViolations.length === 0)
|
|
335
|
+
return { violations: [] };
|
|
336
|
+
const secondaryGroups = compliance.secondary_checks ?? [];
|
|
337
|
+
const passingGroup = secondaryGroups.length > 0 ? secondaryGroups.find((g) => evaluateSecondaryGroup(g)) : undefined;
|
|
338
|
+
if (passingGroup) {
|
|
339
|
+
return {
|
|
340
|
+
violations: [],
|
|
341
|
+
secondarySatisfied: {
|
|
342
|
+
uuid: entry.uuid,
|
|
343
|
+
config_file_path: passingGroup.config_file_path,
|
|
344
|
+
file_type: passingGroup.file_type,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return { violations: entryViolations };
|
|
349
|
+
}
|
|
350
|
+
export function collectManifestEntryViolations(entry) {
|
|
351
|
+
return evaluateManifestEntryCompliance(entry).violations;
|
|
352
|
+
}
|
|
273
353
|
/**
|
|
274
354
|
* Evaluate current on-disk configs against remediation_instructions.json only (no server).
|
|
275
355
|
* Returns status for prompt gating / callers; does not persist compliance.json.
|
|
@@ -300,12 +380,11 @@ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
|
|
|
300
380
|
hookRunLog(`compliance_check: skipping non-json entry uuid=${entry.uuid}`);
|
|
301
381
|
continue;
|
|
302
382
|
}
|
|
303
|
-
|
|
304
|
-
if (checks.length === 0) {
|
|
383
|
+
if ((compliance.checks ?? []).length === 0) {
|
|
305
384
|
skippedNoChecks++;
|
|
306
385
|
continue;
|
|
307
386
|
}
|
|
308
|
-
const loaded = loadRemediationConfigJson(entry.config_file_path, checks.map((c) => c.setting_path));
|
|
387
|
+
const loaded = loadRemediationConfigJson(entry.config_file_path, (compliance.checks ?? []).map((c) => c.setting_path));
|
|
309
388
|
if (!loaded.ok) {
|
|
310
389
|
skippedUnreadable++;
|
|
311
390
|
const msg = loaded.reason === 'file_not_found'
|
|
@@ -327,121 +406,16 @@ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
|
|
|
327
406
|
});
|
|
328
407
|
continue;
|
|
329
408
|
}
|
|
330
|
-
const
|
|
331
|
-
const entryViolations
|
|
332
|
-
|
|
333
|
-
// When the check carries a sqlite_op with apply_to_all_workspaces, the authoritative data
|
|
334
|
-
// lives in workspace state.vscdb files — not in config_file_path (which may be mcp.json in
|
|
335
|
-
// the fallback case where no workspace vscdb has been collected yet). Verify directly against
|
|
336
|
-
// every discovered workspace vscdb; fail as a violation if any is missing the required entry.
|
|
337
|
-
if (check.sqlite_op?.apply_to_all_workspaces && check.ops) {
|
|
338
|
-
const segments = check.sqlite_op.workspace_storage_path_segments ?? [
|
|
339
|
-
'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage',
|
|
340
|
-
];
|
|
341
|
-
const wsPath = join(homedir(), ...segments);
|
|
342
|
-
const vscdbPaths = discoverAllWorkspaceVscdbs(wsPath);
|
|
343
|
-
if (vscdbPaths.length === 0) {
|
|
344
|
-
hookRunLog(`compliance_check: apply_to_all_workspaces — no workspace vscdbs found at ${wsPath}, skipping uuid=${entry.uuid}`);
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
let violated = false;
|
|
348
|
-
let expectedForViolation = null;
|
|
349
|
-
for (const dbPath of vscdbPaths) {
|
|
350
|
-
const wrapped = readVscdbItemTableJson(dbPath, check.sqlite_op.target_key ?? '');
|
|
351
|
-
if (wrapped === null) {
|
|
352
|
-
violated = true;
|
|
353
|
-
expectedForViolation = { op: 'read_failed', db: dbPath };
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
const { ok, expected } = verifyOpsApplied(wrapped, check.setting_path, check.ops);
|
|
357
|
-
if (!ok) {
|
|
358
|
-
violated = true;
|
|
359
|
-
expectedForViolation = expected;
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
if (violated) {
|
|
364
|
-
hookRunLog(`compliance_check: VIOLATION (vscdb) uuid=${entry.uuid} path=${check.setting_path} expected=${JSON.stringify(expectedForViolation)}`);
|
|
365
|
-
entryViolations.push({
|
|
366
|
-
uuid: entry.uuid,
|
|
367
|
-
finding_formatted_id: compliance.finding_formatted_id,
|
|
368
|
-
setting_path: check.setting_path,
|
|
369
|
-
description: check.description,
|
|
370
|
-
finding_title: entry.finding_title,
|
|
371
|
-
finding_description: entry.finding_description,
|
|
372
|
-
policy_name: entry.policy_name,
|
|
373
|
-
severity: compliance.severity,
|
|
374
|
-
autofix_allowed: compliance.autofix_allowed,
|
|
375
|
-
config_file_path: entry.config_file_path,
|
|
376
|
-
expected_value: expectedForViolation,
|
|
377
|
-
message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Apply remediation ops for ${check.setting_path} in workspace state.vscdb files`,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
// Prefer ops-based verification (matches delta apply semantics; doesn't require full after snapshot).
|
|
384
|
-
if (check.ops) {
|
|
385
|
-
const { ok, expected } = verifyOpsApplied(configJson, check.setting_path, check.ops);
|
|
386
|
-
if (!ok) {
|
|
387
|
-
hookRunLog(`compliance_check: VIOLATION uuid=${entry.uuid} path=${check.setting_path} expected=${JSON.stringify(expected)}`);
|
|
388
|
-
entryViolations.push({
|
|
389
|
-
uuid: entry.uuid,
|
|
390
|
-
finding_formatted_id: compliance.finding_formatted_id,
|
|
391
|
-
setting_path: check.setting_path,
|
|
392
|
-
description: check.description,
|
|
393
|
-
finding_title: entry.finding_title,
|
|
394
|
-
finding_description: entry.finding_description,
|
|
395
|
-
policy_name: entry.policy_name,
|
|
396
|
-
severity: compliance.severity,
|
|
397
|
-
autofix_allowed: compliance.autofix_allowed,
|
|
398
|
-
config_file_path: entry.config_file_path,
|
|
399
|
-
expected_value: expected,
|
|
400
|
-
message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Apply remediation ops for ${check.setting_path} in ${entry.config_file_path}`,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
// Backwards compat: old local files may still carry after snapshots.
|
|
406
|
-
const currentValue = getByPath(configJson, check.setting_path);
|
|
407
|
-
const leafKey = check.setting_path.split('.').pop();
|
|
408
|
-
const expectedValue = check.after?.[leafKey];
|
|
409
|
-
if (expectedValue === undefined)
|
|
410
|
-
continue;
|
|
411
|
-
if (!deepEqual(currentValue, expectedValue)) {
|
|
412
|
-
hookRunLog(`compliance_check: VIOLATION uuid=${entry.uuid} path=${check.setting_path} current=${JSON.stringify(currentValue)} expected=${JSON.stringify(expectedValue)}`);
|
|
413
|
-
entryViolations.push({
|
|
414
|
-
uuid: entry.uuid,
|
|
415
|
-
finding_formatted_id: compliance.finding_formatted_id,
|
|
416
|
-
setting_path: check.setting_path,
|
|
417
|
-
description: check.description,
|
|
418
|
-
finding_title: entry.finding_title,
|
|
419
|
-
finding_description: entry.finding_description,
|
|
420
|
-
policy_name: entry.policy_name,
|
|
421
|
-
severity: compliance.severity,
|
|
422
|
-
autofix_allowed: compliance.autofix_allowed,
|
|
423
|
-
config_file_path: entry.config_file_path,
|
|
424
|
-
expected_value: expectedValue,
|
|
425
|
-
message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Set ${check.setting_path} to ${JSON.stringify(expectedValue)} in ${entry.config_file_path}`,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
409
|
+
const { violations: entryViolations, secondarySatisfied: entrySecondary } = evaluateManifestEntryCompliance(entry);
|
|
410
|
+
for (const v of entryViolations) {
|
|
411
|
+
hookRunLog(`compliance_check: VIOLATION uuid=${entry.uuid} path=${v.setting_path} expected=${JSON.stringify(v.expected_value)}`);
|
|
428
412
|
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
hookRunLog(`compliance_check: secondary check satisfied uuid=${entry.uuid} — skipping ${entryViolations.length} primary violation(s)`);
|
|
436
|
-
secondarySatisfied.push({
|
|
437
|
-
uuid: entry.uuid,
|
|
438
|
-
config_file_path: passingGroup.config_file_path,
|
|
439
|
-
file_type: passingGroup.file_type,
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
else {
|
|
443
|
-
violations.push(...entryViolations);
|
|
444
|
-
}
|
|
413
|
+
if (entrySecondary) {
|
|
414
|
+
hookRunLog(`compliance_check: secondary check satisfied uuid=${entry.uuid} — skipping primary violation(s)`);
|
|
415
|
+
secondarySatisfied.push(entrySecondary);
|
|
416
|
+
}
|
|
417
|
+
else if (entryViolations.length > 0) {
|
|
418
|
+
violations.push(...entryViolations);
|
|
445
419
|
}
|
|
446
420
|
}
|
|
447
421
|
const status = {
|
|
@@ -485,6 +459,34 @@ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
|
|
|
485
459
|
return fallback;
|
|
486
460
|
}
|
|
487
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* After restart, verify pending remediations and report outcomes to the server.
|
|
464
|
+
*/
|
|
465
|
+
export function reportPostRestartVerificationOutcomes(violations) {
|
|
466
|
+
const { remediations } = readRemediationInstructionsFile();
|
|
467
|
+
const entriesByUuid = new Map(remediations.map((entry) => [entry.uuid, entry]));
|
|
468
|
+
const outcomes = processPendingPostRestartVerifications((uuid) => {
|
|
469
|
+
const entry = entriesByUuid.get(uuid);
|
|
470
|
+
if (entry && collectManifestEntryViolations(entry).length > 0)
|
|
471
|
+
return true;
|
|
472
|
+
return violations.some((v) => v.uuid === uuid);
|
|
473
|
+
});
|
|
474
|
+
const reportPromises = outcomes.map((o) => {
|
|
475
|
+
if (o.status === 'quarantined' || o.status === 'verification_failed') {
|
|
476
|
+
logRemediationApplyFailure('post_restart_verification_failed', {
|
|
477
|
+
uuid: o.uuid,
|
|
478
|
+
reason: o.reason ?? 'Setting unchanged after apply',
|
|
479
|
+
status: o.status,
|
|
480
|
+
consecutive_failures: o.consecutive_failures,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return reportAutofixApplied(o.uuid, o.status, {
|
|
484
|
+
failure_reason: o.reason,
|
|
485
|
+
consecutive_failures: o.consecutive_failures,
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
return { outcomes, reportPromises };
|
|
489
|
+
}
|
|
488
490
|
export function applyAutofixViolations(violations, agent = 'cursor') {
|
|
489
491
|
for (const v of violations) {
|
|
490
492
|
if (!v.autofix_allowed) {
|
|
@@ -520,6 +522,18 @@ export function applyAutofixViolations(violations, agent = 'cursor') {
|
|
|
520
522
|
for (const violation of autofixable) {
|
|
521
523
|
if (seen.has(violation.uuid))
|
|
522
524
|
continue;
|
|
525
|
+
if (isRemediationQuarantined(violation.uuid)) {
|
|
526
|
+
hookRunLog(`autofix: skipped quarantined uuid=${violation.uuid}`);
|
|
527
|
+
logRemediationApplyFailure('autofix_skipped_quarantined', {
|
|
528
|
+
uuid: violation.uuid,
|
|
529
|
+
finding_formatted_id: violation.finding_formatted_id,
|
|
530
|
+
config_file_path: violation.config_file_path,
|
|
531
|
+
setting_path: violation.setting_path,
|
|
532
|
+
reason: 'Auto-fix paused after repeated post-restart verification failures — agent config may have changed',
|
|
533
|
+
});
|
|
534
|
+
failedViolations.push(violation);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
523
537
|
const instruction = byUuid.get(violation.uuid);
|
|
524
538
|
if (!instruction) {
|
|
525
539
|
hookRunLog(`autofix: no instruction found for uuid=${violation.uuid}, skipping`);
|
|
@@ -548,6 +562,10 @@ export function applyAutofixViolations(violations, agent = 'cursor') {
|
|
|
548
562
|
appliedViolations.push(violation);
|
|
549
563
|
hookRunLog(`autofix: applied uuid=${inst.uuid} path=${configPathForDisk}`);
|
|
550
564
|
reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
|
|
565
|
+
// Every successful autofix (Cursor + Claude, restart or immediate JSON) awaits verification on
|
|
566
|
+
// the next compliance check so we can quarantine stuck applies and stop restart/retry loops.
|
|
567
|
+
markRemediationApplyPendingVerification(inst.uuid);
|
|
568
|
+
const spec = remediationFixSpec(inst);
|
|
551
569
|
if (er.deferredSqlite && configPathForDisk.includes('#')) {
|
|
552
570
|
hookRunLog(`autofix: skip immediate vscdb upload (deferred until after restart) uuid=${inst.uuid}`);
|
|
553
571
|
}
|
|
@@ -597,7 +615,6 @@ export function applyAutofixViolations(violations, agent = 'cursor') {
|
|
|
597
615
|
}
|
|
598
616
|
}
|
|
599
617
|
}
|
|
600
|
-
const spec = remediationFixSpec(inst);
|
|
601
618
|
if (spec?.restart_required && spec.restart_command) {
|
|
602
619
|
if (!er.deferredSqlite) {
|
|
603
620
|
if (isTrustedRestartCommandForAutofix(spec.restart_command)) {
|
|
@@ -749,6 +766,68 @@ export function pruneSatisfiedOneTimeRemediations(agent = 'cursor') {
|
|
|
749
766
|
}
|
|
750
767
|
return { removed, reportPromises };
|
|
751
768
|
}
|
|
769
|
+
/** Throttle satisfied-manifest uploads (disk already matches ops; autofix did not run). */
|
|
770
|
+
const SATISFIED_MANIFEST_UPLOAD_COOLDOWN_MS = 5 * 60 * 1000;
|
|
771
|
+
/**
|
|
772
|
+
* When local compliance already passes, autofix is skipped — so settings never reach the server.
|
|
773
|
+
* Upload satisfied JSON remediations (throttled) so scan can resolve findings.
|
|
774
|
+
*/
|
|
775
|
+
export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
|
|
776
|
+
const { remediations } = readRemediationInstructionsFile();
|
|
777
|
+
const entries = remediations.filter((e) => targetsCurrentAgent(e, agent));
|
|
778
|
+
const promises = [];
|
|
779
|
+
for (const entry of entries) {
|
|
780
|
+
const { violations } = evaluateManifestEntryCompliance(entry);
|
|
781
|
+
if (violations.length > 0)
|
|
782
|
+
continue;
|
|
783
|
+
const inst = entry;
|
|
784
|
+
const fileType = (inst.file_type ?? '').trim();
|
|
785
|
+
if (!fileType)
|
|
786
|
+
continue;
|
|
787
|
+
const diskPath = resolveRemediationConfigPath(entry.config_file_path);
|
|
788
|
+
if (diskPath.includes('#'))
|
|
789
|
+
continue;
|
|
790
|
+
const tracking = readRemediationApplyTrackingFile();
|
|
791
|
+
const prev = tracking.entries[entry.uuid];
|
|
792
|
+
const lastUpload = prev?.last_satisfied_upload_at;
|
|
793
|
+
if (lastUpload) {
|
|
794
|
+
const elapsed = Date.now() - Date.parse(lastUpload);
|
|
795
|
+
if (!Number.isNaN(elapsed) && elapsed < SATISFIED_MANIFEST_UPLOAD_COOLDOWN_MS) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
let rawContent;
|
|
800
|
+
try {
|
|
801
|
+
rawContent = JSON.parse(readFileSync(diskPath, 'utf8'));
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
hookRunLog(`satisfied_upload: could not read path=${diskPath} uuid=${entry.uuid}`);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const hw = tryResolveHardwareUuid();
|
|
808
|
+
if (!hw) {
|
|
809
|
+
hookRunLog(`satisfied_upload: skip uuid=${entry.uuid} (hardware UUID unavailable)`);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const uploadPath = entry.config_file_path.trim() || diskPath;
|
|
813
|
+
promises.push(ensureAuthentication(hw)
|
|
814
|
+
.then((authKey) => sendConfigFile({ file_type: fileType, file_path: uploadPath, raw_content: rawContent }, hw, authKey))
|
|
815
|
+
.then((sentOk) => {
|
|
816
|
+
hookRunLog(`satisfied_upload: uuid=${entry.uuid} path=${uploadPath} ok=${sentOk}`);
|
|
817
|
+
if (sentOk) {
|
|
818
|
+
const file = readRemediationApplyTrackingFile();
|
|
819
|
+
const ent = file.entries[entry.uuid] ?? { consecutive_verify_failures: 0, quarantined: false };
|
|
820
|
+
ent.last_satisfied_upload_at = new Date().toISOString();
|
|
821
|
+
file.entries[entry.uuid] = ent;
|
|
822
|
+
writeRemediationApplyTrackingFile(file);
|
|
823
|
+
}
|
|
824
|
+
})
|
|
825
|
+
.catch((err) => {
|
|
826
|
+
hookRunLog(`satisfied_upload: failed uuid=${entry.uuid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
return promises;
|
|
830
|
+
}
|
|
752
831
|
/**
|
|
753
832
|
* Background refresh: server sync for latest instructions, then the same local evaluation as the hook.
|
|
754
833
|
* Apply (autofix) is intentionally deferred to the gate on the next prompt — this pass only downloads
|
|
@@ -768,5 +847,6 @@ export async function runComplianceCheck() {
|
|
|
768
847
|
if (pruned.removed > 0) {
|
|
769
848
|
await Promise.allSettled(pruned.reportPromises);
|
|
770
849
|
}
|
|
850
|
+
await Promise.allSettled(uploadSatisfiedManifestConfigs(agent));
|
|
771
851
|
}
|
|
772
852
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { atomicWriteJson, getDialogPreferencesPath, } from './management_storage.js';
|
|
3
|
+
export function readDialogPreferences() {
|
|
4
|
+
const path = getDialogPreferencesPath();
|
|
5
|
+
if (!existsSync(path)) {
|
|
6
|
+
return { version: 1, suppress_non_restart_autofix_dialogs: false };
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
10
|
+
if (parsed?.version !== 1)
|
|
11
|
+
return { version: 1, suppress_non_restart_autofix_dialogs: false };
|
|
12
|
+
return {
|
|
13
|
+
version: 1,
|
|
14
|
+
suppress_non_restart_autofix_dialogs: Boolean(parsed.suppress_non_restart_autofix_dialogs),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { version: 1, suppress_non_restart_autofix_dialogs: false };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function isNonRestartAutofixDialogSuppressed() {
|
|
22
|
+
return readDialogPreferences().suppress_non_restart_autofix_dialogs === true;
|
|
23
|
+
}
|
|
24
|
+
export function setSuppressNonRestartAutofixDialogs(suppress) {
|
|
25
|
+
const next = {
|
|
26
|
+
version: 1,
|
|
27
|
+
suppress_non_restart_autofix_dialogs: suppress,
|
|
28
|
+
updated_at: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
atomicWriteJson(getDialogPreferencesPath(), next);
|
|
31
|
+
}
|
|
@@ -17,6 +17,8 @@ export const DEFERRED_VSCDB_RESTART_LOG_BASENAME = 'deferred_vscdb_restart.log';
|
|
|
17
17
|
* of hardcoded Cursor paths.
|
|
18
18
|
*/
|
|
19
19
|
export const FILE_COLLECTION_VSCDB_CONTRACT_BASENAME = 'file_collection_vscdb_contract.json';
|
|
20
|
+
/** User preferences for Optimus macOS dialogs (hook-driven). */
|
|
21
|
+
export const DIALOG_PREFERENCES_BASENAME = 'optimus_dialog_preferences.json';
|
|
20
22
|
export function sanitizeReactiveStorageItemKey(raw) {
|
|
21
23
|
if (typeof raw !== 'string')
|
|
22
24
|
return undefined;
|
|
@@ -59,6 +61,9 @@ export function getDeferredVscdbRestartLogPath() {
|
|
|
59
61
|
export function getFileCollectionVscdbContractPath() {
|
|
60
62
|
return join(getManagementDir(), FILE_COLLECTION_VSCDB_CONTRACT_BASENAME);
|
|
61
63
|
}
|
|
64
|
+
export function getDialogPreferencesPath() {
|
|
65
|
+
return join(getManagementDir(), DIALOG_PREFERENCES_BASENAME);
|
|
66
|
+
}
|
|
62
67
|
export function readFileCollectionVscdbContract() {
|
|
63
68
|
const path = getFileCollectionVscdbContractPath();
|
|
64
69
|
if (!existsSync(path))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map ops.add/set/remove keys to JSON dot-paths for apply + verify.
|
|
3
|
+
*
|
|
4
|
+
* Server may use either the leaf key (e.g. `deny`) or the full setting_path
|
|
5
|
+
* (e.g. `cursor.general.globalCursorIgnoreList`) as the op map key.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveOpsTargetPath(settingPath, opKey) {
|
|
8
|
+
const parts = settingPath.split('.');
|
|
9
|
+
const leafKey = parts[parts.length - 1] ?? '';
|
|
10
|
+
const parentPath = parts.slice(0, -1).join('.');
|
|
11
|
+
if (opKey === settingPath || opKey === leafKey) {
|
|
12
|
+
return settingPath;
|
|
13
|
+
}
|
|
14
|
+
if (opKey.includes('.')) {
|
|
15
|
+
return opKey;
|
|
16
|
+
}
|
|
17
|
+
return parentPath ? `${parentPath}.${opKey}` : opKey;
|
|
18
|
+
}
|