log-llm-config 1.5.0 → 1.5.2

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.
@@ -6,7 +6,7 @@
6
6
  * When autofix returns restart_commands, this process does not spawn them — the shell hook pipes
7
7
  * the same JSON line to execute_trusted_restarts (TS allowlist + spawn).
8
8
  */
9
- import { applyAutofixViolations, confirmAppliedAutofixVerified, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, reportPostRestartVerificationOutcomes, runLocalRemediationComplianceCheck, uploadSatisfiedManifestConfigs, } from './log_config_files/runtime/compliance_check.js';
9
+ import { applyAutofixViolations, confirmAppliedAutofixVerified, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, reportCompliantRemediationVerifiedStatus, reportPostRestartVerificationOutcomes, runLocalRemediationComplianceCheck, uploadSatisfiedManifestConfigs, } from './log_config_files/runtime/compliance_check.js';
10
10
  import { isRemediationQuarantined } from './log_config_files/runtime/remediation_apply_tracking.js';
11
11
  import { existsSync, readFileSync, statSync } from 'node:fs';
12
12
  import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
@@ -16,8 +16,10 @@ import { finalizeAndUploadComplianceSessionLog, initComplianceSessionForGate, pa
16
16
  import { isThisCliModule } from './cli_invocation_match.js';
17
17
  import { ensureAuthentication } from './log_config_files/auth/auth_flow.js';
18
18
  import { sendConfigFile } from './log_config_files/sender/batch_sender.js';
19
+ import { parseJsonWithJsoncFallback } from './log_config_files/readers/file_readers.js';
20
+ import { PORTABLE_CURSOR_USER_SETTINGS, resolveRemediationConfigPath, resolveRemediationUploadFileType, } from './log_config_files/runtime/remediation_config_path.js';
19
21
  import { tryResolveHardwareUuid } from './log_config_files/runtime/hardware_uuid.js';
20
- import { syncRemediations } from './log_config_files/runtime/remediation_sync.js';
22
+ import { cursorAutofixDefersInlineVerify, syncRemediations, } from './log_config_files/runtime/remediation_sync.js';
21
23
  import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
22
24
  import { formatRemediationChangePreviewForApplied } from './remediation_change_preview.js';
23
25
  const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
@@ -147,9 +149,9 @@ export function formatCopilotAutofixDialog(appliedViolations) {
147
149
  export function formatOpenCodeAutofixDialog(appliedViolations) {
148
150
  return formatPreventiveAutofixDialog(appliedViolations, 'OpenCode will now apply this policy to your environment.');
149
151
  }
150
- /** Cursor restart dialog after enforced/preventive remediation is applied locally. */
152
+ /** Cursor restart after local autofix (all JSON settings + vscdb paths use restart_commands). */
151
153
  export function formatCursorRestartAutofixDialog(appliedViolations) {
152
- return formatPreventiveAutofixDialog(appliedViolations, 'Cursor will now restart to apply this policy, and your context will be retained.');
154
+ return formatPreventiveAutofixDialog(appliedViolations, 'Cursor will restart so the running app reloads policy from disk. Your context will be retained.');
153
155
  }
154
156
  /**
155
157
  * Upload a secondary compliance file to the backend so the server can resolve the finding.
@@ -157,16 +159,23 @@ export function formatCursorRestartAutofixDialog(appliedViolations) {
157
159
  */
158
160
  async function _uploadSecondaryFile(entry) {
159
161
  const { uuid, config_file_path, file_type } = entry;
160
- if (!file_type) {
162
+ const uploadFileType = resolveRemediationUploadFileType(config_file_path, file_type ?? undefined);
163
+ if (!uploadFileType) {
161
164
  hookRunLog(`secondary_upload: skipping uuid=${uuid} — no file_type on secondary group`);
162
165
  return;
163
166
  }
167
+ const diskPath = resolveRemediationConfigPath(config_file_path);
164
168
  let rawContent;
165
169
  try {
166
- rawContent = JSON.parse(readFileSync(config_file_path, 'utf8'));
170
+ const parsed = parseJsonWithJsoncFallback(readFileSync(diskPath, 'utf8'));
171
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
172
+ hookRunLog(`secondary_upload: invalid JSON uuid=${uuid} path=${diskPath}`);
173
+ return;
174
+ }
175
+ rawContent = parsed;
167
176
  }
168
177
  catch {
169
- hookRunLog(`secondary_upload: could not read file uuid=${uuid} path=${config_file_path}`);
178
+ hookRunLog(`secondary_upload: could not read file uuid=${uuid} path=${diskPath}`);
170
179
  return;
171
180
  }
172
181
  const hw = tryResolveHardwareUuid();
@@ -174,13 +183,16 @@ async function _uploadSecondaryFile(entry) {
174
183
  hookRunLog(`secondary_upload: hardware UUID unavailable, skipping uuid=${uuid}`);
175
184
  return;
176
185
  }
186
+ const portablePath = config_file_path.includes('Cursor/User/settings.json')
187
+ ? PORTABLE_CURSOR_USER_SETTINGS
188
+ : config_file_path.trim() || diskPath;
177
189
  try {
178
190
  const authKey = await ensureAuthentication(hw);
179
- const sent = await sendConfigFile({ file_type, file_path: config_file_path, raw_content: rawContent }, hw, authKey);
180
- hookRunLog(`secondary_upload: uuid=${uuid} path=${config_file_path} sent=${sent}`);
191
+ const sent = await sendConfigFile({ file_type: uploadFileType, file_path: portablePath, raw_content: rawContent }, hw, authKey);
192
+ hookRunLog(`secondary_upload: uuid=${uuid} path=${portablePath} sent=${sent}`);
181
193
  }
182
194
  catch (err) {
183
- hookRunLog(`secondary_upload: upload failed uuid=${uuid} path=${config_file_path} err=${err instanceof Error ? err.message : String(err)}`);
195
+ hookRunLog(`secondary_upload: upload failed uuid=${uuid} path=${portablePath} err=${err instanceof Error ? err.message : String(err)}`);
184
196
  }
185
197
  }
186
198
  /**
@@ -218,8 +230,6 @@ export async function runCompliancePromptGate() {
218
230
  hookLogSessionBanner('compliance_prompt_gate (before submit)');
219
231
  }
220
232
  let status = runLocalRemediationComplianceCheck(agent);
221
- // Post-restart verify + server reports BEFORE sync so a manifest UUID swap cannot delay
222
- // verified / activity-log until the following prompt.
223
233
  const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
224
234
  if (postRestartVerify.outcomes.length > 0) {
225
235
  await Promise.allSettled(postRestartVerify.reportPromises);
@@ -306,12 +316,14 @@ export async function runCompliancePromptGate() {
306
316
  // disk synchronously; a restart_command, if present, only reloads the already-compliant file
307
317
  // into the running app — it is not the mechanism that makes the fix take effect. So report
308
318
  // "verified" now instead of leaving the finding "pending" in the UI until the next prompt.
309
- // Cursor's restart can carry a deferred state.vscdb write that only lands post-restart, so
310
- // Cursor still verifies inline only when no restart is pending.
319
+ // Cursor JSON: disk recheck OK → verified before restart. Cursor deferred vscdb verify after restart.
311
320
  const immediateJsonAgent = ide === 'claude' || ide === 'copilot' || ide === 'opencode';
321
+ const diskProvenCompliant = recheckOk || claudeRecheckStaleAfterImmediateApply;
322
+ const cursorRestartVerifyPending = cursorAutofixDefersInlineVerify(ide, restartCommands.length, deferredSqlitePending === true, diskProvenCompliant);
312
323
  const immediateVerified = !deferredSqlitePending &&
313
- (recheckOk || claudeRecheckStaleAfterImmediateApply) &&
314
- (restartCommands.length === 0 || immediateJsonAgent);
324
+ !cursorRestartVerifyPending &&
325
+ diskProvenCompliant &&
326
+ (restartCommands.length === 0 || immediateJsonAgent || ide === 'cursor');
315
327
  if (immediateVerified) {
316
328
  confirmAppliedAutofixVerified(appliedViolations, reportPromises);
317
329
  await Promise.allSettled(reportPromises);
@@ -376,6 +388,7 @@ export async function runCompliancePromptGate() {
376
388
  await Promise.allSettled(pruned.reportPromises);
377
389
  }
378
390
  await Promise.allSettled(uploadSatisfiedManifestConfigs(agent));
391
+ await Promise.allSettled(reportCompliantRemediationVerifiedStatus(agent));
379
392
  printAllow(ide);
380
393
  }
381
394
  if (isRunAsCliModule()) {
@@ -86,6 +86,12 @@ function readContentByFormat(path, format) {
86
86
  // json (default) — fall back to markdown for .md files from agents not yet annotated
87
87
  return readJSONFile(path) ?? readMCPConfig(path) ?? (path.endsWith('.md') ? readMarkdownFile(path) : null);
88
88
  }
89
+ function isPortableCursorUserSettingsTarget(t) {
90
+ const p = (t.logicalFilePath ?? t.path).replace(/\\/g, '/').toLowerCase();
91
+ return (p === 'cursor/user/settings.json' ||
92
+ p.endsWith('/cursor/user/settings.json') ||
93
+ p.includes('application support/cursor/user/settings.json'));
94
+ }
89
95
  function collectRegularFileEntry(t, enrichByFileType) {
90
96
  const format = t.content_format || 'json';
91
97
  let content = readContentByFormat(t.path, format);
@@ -120,6 +126,12 @@ function collectRegularFileEntry(t, enrichByFileType) {
120
126
  return null;
121
127
  raw = filtered;
122
128
  }
129
+ if (t.file_type === 'vscode_settings' &&
130
+ isPortableCursorUserSettingsTarget(t) &&
131
+ Object.keys(raw).length === 0) {
132
+ console.warn(`Skipping empty Cursor User/settings.json upload (path=${t.path}); global ignore would be invisible to the server.`);
133
+ return null;
134
+ }
123
135
  return { file_type: t.file_type, file_path: t.logicalFilePath ?? t.path, raw_content: raw };
124
136
  }
125
137
  function collectMetadataEntry(t) {
@@ -0,0 +1,50 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readJSONFile } from '../readers/file_readers.js';
3
+ import { PORTABLE_CURSOR_USER_SETTINGS, cursorUserSettingsAbsolutePaths, } from '../runtime/remediation_config_path.js';
4
+ export function isPortableCursorUserSettingsUploadPath(filePath) {
5
+ const p = filePath.replace(/\\/g, '/').toLowerCase();
6
+ return (p === 'cursor/user/settings.json' ||
7
+ p.endsWith('/cursor/user/settings.json') ||
8
+ p.includes('application support/cursor/user/settings.json'));
9
+ }
10
+ function settingsPayloadHasGlobalIgnoreListKey(raw) {
11
+ if (!raw || typeof raw !== 'object')
12
+ return false;
13
+ const nested = raw.cursor
14
+ ?.general?.globalCursorIgnoreList;
15
+ if (Array.isArray(nested))
16
+ return true;
17
+ const flat = raw['cursor.general.globalCursorIgnoreList'];
18
+ return Array.isArray(flat);
19
+ }
20
+ /**
21
+ * Sensitive Directories scans need full User/settings.json (globalCursorIgnoreList).
22
+ * Pattern collection can omit the key or send an empty object; merge disk when the batch
23
+ * lacks an explicit globalCursorIgnoreList array (including []).
24
+ */
25
+ export function ensureCursorUserSettingsSnapshotInBatch(configFiles) {
26
+ const hasIgnoreKeyInBatch = configFiles.some((c) => c.file_type === 'vscode_settings' &&
27
+ isPortableCursorUserSettingsUploadPath(c.file_path) &&
28
+ settingsPayloadHasGlobalIgnoreListKey(c.raw_content));
29
+ if (hasIgnoreKeyInBatch)
30
+ return;
31
+ for (const abs of cursorUserSettingsAbsolutePaths()) {
32
+ if (!existsSync(abs))
33
+ continue;
34
+ const raw = readJSONFile(abs);
35
+ if (!raw || Object.keys(raw).length === 0)
36
+ continue;
37
+ const entry = {
38
+ file_type: 'vscode_settings',
39
+ file_path: PORTABLE_CURSOR_USER_SETTINGS,
40
+ raw_content: raw,
41
+ };
42
+ const idx = configFiles.findIndex((c) => c.file_type === 'vscode_settings' &&
43
+ isPortableCursorUserSettingsUploadPath(c.file_path));
44
+ if (idx >= 0)
45
+ configFiles[idx] = entry;
46
+ else
47
+ configFiles.push(entry);
48
+ return;
49
+ }
50
+ }
@@ -9,7 +9,7 @@ function normalizePathSkipPrefixes(prefixes) {
9
9
  return prefixes.filter((p) => typeof p === 'string' && p.length > 0);
10
10
  }
11
11
  /**
12
- * Expands glob patterns containing double-asterisk (recursive directory traversal).
12
+ * Expands glob patterns with double-asterisk (recursive directory traversal).
13
13
  *
14
14
  * Example: ~/.cursor/plugins/cache/ ** /skills/ recursively finds all skills
15
15
  * directories under the cache folder, up to RECURSIVE_GLOB_MAX_DEPTH.
@@ -31,12 +31,22 @@ function bufferPath() {
31
31
  return null;
32
32
  return path.join(home, OPT_AI_SEC_MANAGEMENT_REL, 'telemetry', BUFFER_BASENAME);
33
33
  }
34
+ /** Keep filename-safe chars only — must match optimus_sanitize_id in optimus-hook-common.sh. */
35
+ function sanitizeId(v) {
36
+ return v.replace(/[^A-Za-z0-9._-]/g, '');
37
+ }
34
38
  /** File start-every-prompt overwrites each submit with this prompt's story id (short-lived handoff). */
35
39
  function storyFilePath() {
36
40
  const home = process.env.HOME || process.env.USERPROFILE;
37
41
  if (!home)
38
42
  return null;
39
- return path.join(home, OPT_AI_SEC_MANAGEMENT_REL, 'telemetry', 'current_story.id');
43
+ const telemetryDir = path.join(home, OPT_AI_SEC_MANAGEMENT_REL, 'telemetry');
44
+ const session = sanitizeId((process.env.OPTIMUS_SESSION_ID || '').trim());
45
+ if (session) {
46
+ const agent = sanitizeId((process.env.OPTIMUS_HOOK_TYPE || process.env.OPTIMUS_AGENT || 'claude').trim()) || 'claude';
47
+ return path.join(telemetryDir, `current_story.${agent}.${session}.id`);
48
+ }
49
+ return path.join(telemetryDir, 'current_story.id');
40
50
  }
41
51
  /** Match shell OPTIMUS_STORY_FILE_MAX_AGE_SEC — ignore stale ids from a prior prompt. */
42
52
  const STORY_FILE_MAX_AGE_MS = 180_000;
@@ -16,14 +16,14 @@ import { join } from 'node:path';
16
16
  import { parseJsonWithJsoncFallback } from '../readers/file_readers.js';
17
17
  import { mergeComposerShadowKeysFromReactiveBlob, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
18
18
  import { readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
19
- import { resolveRemediationConfigPath } from './remediation_config_path.js';
19
+ import { resolveRemediationConfigPath, resolveRemediationUploadFileType } from './remediation_config_path.js';
20
20
  import { resolveOpsTargetPath } from './ops_target_path.js';
21
21
  import { isRemediationQuarantined, markRemediationApplyPendingVerification, markRemediationApplyVerified, processPendingPostRestartVerifications, readRemediationApplyTrackingFile, writeRemediationApplyTrackingFile, } from './remediation_apply_tracking.js';
22
22
  import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
23
23
  import { isEnvBackedSecretValue, scanJsonForHardcodedSecrets, } from './secret_regex_scan.js';
24
24
  import { loadEndpointBase } from '../sender/endpoint_config.js';
25
25
  import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
26
- import { buildDeferredCursorRestartCommand, discoverAllWorkspaceVscdbs, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
26
+ import { buildDeferredCursorRestartCommand, discoverAllWorkspaceVscdbs, enforceRemediation, fetchSync, globalCursorIgnoreListFromSettings, GLOBAL_CURSOR_IGNORE_SETTING_PATH, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
27
27
  import { sendConfigFile } from '../sender/batch_sender.js';
28
28
  import { ensureAuthentication } from '../auth/auth_flow.js';
29
29
  /** Normalize manifest/env/CLI agent tokens to a known Agent, or '' if unrecognized. */
@@ -220,6 +220,16 @@ function violationFromSecretScanFinding(entry, compliance, finding) {
220
220
  message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Remove the hardcoded ${finding.secretType} from ${finding.path} in ${entry.config_file_path}`,
221
221
  };
222
222
  }
223
+ function currentArrayAtComplianceTarget(configJson, targetPath) {
224
+ if (configJson &&
225
+ typeof configJson === 'object' &&
226
+ !Array.isArray(configJson) &&
227
+ targetPath === GLOBAL_CURSOR_IGNORE_SETTING_PATH) {
228
+ return globalCursorIgnoreListFromSettings(configJson);
229
+ }
230
+ const cur = getByPath(configJson, targetPath);
231
+ return Array.isArray(cur) ? cur : [];
232
+ }
223
233
  function verifyOpsApplied(configJson, settingPath, ops) {
224
234
  const parts = settingPath.split('.');
225
235
  const leafKey = parts[parts.length - 1] ?? '';
@@ -231,15 +241,16 @@ function verifyOpsApplied(configJson, settingPath, ops) {
231
241
  for (const k of keys) {
232
242
  const targetPath = resolveOpsTargetPath(settingPath, k);
233
243
  if (Object.prototype.hasOwnProperty.call(set, k)) {
234
- const cur = getByPath(configJson, targetPath);
235
244
  const expected = set[k];
236
- if (!remediationSetOpSatisfied(cur, expected)) {
245
+ const currentForSet = targetPath === GLOBAL_CURSOR_IGNORE_SETTING_PATH
246
+ ? globalCursorIgnoreListFromSettings(configJson)
247
+ : getByPath(configJson, targetPath);
248
+ if (!remediationSetOpSatisfied(currentForSet, expected)) {
237
249
  return { ok: false, expected: { op: 'set', path: targetPath, value: expected } };
238
250
  }
239
251
  continue;
240
252
  }
241
- const cur = getByPath(configJson, targetPath);
242
- const curArr = Array.isArray(cur) ? cur : [];
253
+ const curArr = currentArrayAtComplianceTarget(configJson, targetPath);
243
254
  const toAdd = add[k] ?? [];
244
255
  const toRemove = remove[k] ?? [];
245
256
  const isYoloAllowlist = leafKey === 'yoloCommandAllowlist' || targetPath.endsWith('.yoloCommandAllowlist');
@@ -565,9 +576,8 @@ export function reportPostRestartVerificationOutcomes(violations) {
565
576
  return { outcomes, reportPromises };
566
577
  }
567
578
  /**
568
- * Run immediately after a deferred state.vscdb write lands on disk (post-restart, before the
569
- * user's next prompt) so `pending_post_restart_verify` remediations are confirmed and reported
570
- * to the server right away — the UI does not need to wait for another gate invocation.
579
+ * After deferred vscdb apply (`apply_deferred_vscdb.ts`): confirm pending remediations and POST
580
+ * outcomes before the user's next prompt.
571
581
  */
572
582
  export async function runPostApplyVerification(agent = 'cursor') {
573
583
  const status = runLocalRemediationComplianceCheck(agent);
@@ -683,7 +693,7 @@ export function applyAutofixViolations(violations, agent = 'cursor') {
683
693
  parseJsonWithJsoncFallback(readFileSync(configPathForDisk, 'utf8')) ?? undefined;
684
694
  }
685
695
  if (updatedContent !== undefined) {
686
- const fileType = (inst.file_type ?? '').trim();
696
+ const fileType = resolveRemediationUploadFileType(inst.config_file_path, inst.file_type ?? undefined);
687
697
  if (fileType) {
688
698
  const hw = tryResolveHardwareUuid();
689
699
  if (hw) {
@@ -888,8 +898,8 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
888
898
  if (violations.length > 0)
889
899
  continue;
890
900
  const inst = entry;
891
- const fileType = (inst.file_type ?? '').trim();
892
- if (!fileType)
901
+ const uploadFileType = resolveRemediationUploadFileType(entry.config_file_path, inst.file_type ?? undefined);
902
+ if (!uploadFileType)
893
903
  continue;
894
904
  const diskPath = resolveRemediationConfigPath(entry.config_file_path);
895
905
  if (diskPath.includes('#'))
@@ -914,16 +924,24 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
914
924
  continue;
915
925
  }
916
926
  const uploadPath = entry.config_file_path.trim() || diskPath;
927
+ const shouldReportVerified = !prev?.pending_post_restart_verify && !prev?.server_verified_at;
917
928
  promises.push(ensureAuthentication(hw)
918
- .then((authKey) => sendConfigFile({ file_type: fileType, file_path: uploadPath, raw_content: rawContent }, hw, authKey))
919
- .then((sentOk) => {
929
+ .then((authKey) => sendConfigFile({ file_type: uploadFileType, file_path: uploadPath, raw_content: rawContent }, hw, authKey))
930
+ .then(async (sentOk) => {
920
931
  hookRunLog(`satisfied_upload: uuid=${entry.uuid} path=${uploadPath} ok=${sentOk}`);
921
- if (sentOk) {
922
- const file = readRemediationApplyTrackingFile();
923
- const ent = file.entries[entry.uuid] ?? { consecutive_verify_failures: 0, quarantined: false };
924
- ent.last_satisfied_upload_at = new Date().toISOString();
925
- file.entries[entry.uuid] = ent;
926
- writeRemediationApplyTrackingFile(file);
932
+ if (!sentOk)
933
+ return;
934
+ const file = readRemediationApplyTrackingFile();
935
+ const ent = file.entries[entry.uuid] ?? { consecutive_verify_failures: 0, quarantined: false };
936
+ ent.last_satisfied_upload_at = new Date().toISOString();
937
+ file.entries[entry.uuid] = ent;
938
+ writeRemediationApplyTrackingFile(file);
939
+ if (shouldReportVerified) {
940
+ await reportAutofixApplied(entry.uuid, 'verified', {
941
+ config_snapshot_after: rawContent,
942
+ });
943
+ markRemediationApplyVerified(entry.uuid);
944
+ hookRunLog(`satisfied_upload: reported verified uuid=${entry.uuid}`);
927
945
  }
928
946
  })
929
947
  .catch((err) => {
@@ -932,6 +950,35 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
932
950
  }
933
951
  return promises;
934
952
  }
953
+ /**
954
+ * When disk already matches remediation ops but autofix never ran, POST verified so the UI
955
+ * leaves Pending (EnforcementLog) even if satisfied_upload is throttled.
956
+ */
957
+ export function reportCompliantRemediationVerifiedStatus(agent = 'cursor') {
958
+ const { remediations } = readRemediationInstructionsFile();
959
+ const entries = remediations.filter((e) => targetsCurrentAgent(e, agent));
960
+ const promises = [];
961
+ for (const entry of entries) {
962
+ const { violations } = evaluateManifestEntryCompliance(entry);
963
+ if (violations.length > 0)
964
+ continue;
965
+ const tracking = readRemediationApplyTrackingFile();
966
+ const prev = tracking.entries[entry.uuid];
967
+ if (prev?.pending_post_restart_verify || prev?.server_verified_at)
968
+ continue;
969
+ const diskPath = resolveRemediationConfigPath(entry.config_file_path);
970
+ if (diskPath.includes('#'))
971
+ continue;
972
+ const rawContent = parseJsonWithJsoncFallback(readFileSync(diskPath, 'utf8'));
973
+ if (rawContent === null)
974
+ continue;
975
+ promises.push(reportAutofixApplied(entry.uuid, 'verified', { config_snapshot_after: rawContent }).then(() => {
976
+ markRemediationApplyVerified(entry.uuid);
977
+ hookRunLog(`compliance_check: reported verified (already compliant) uuid=${entry.uuid}`);
978
+ }));
979
+ }
980
+ return promises;
981
+ }
935
982
  /**
936
983
  * Background refresh: server sync for latest instructions, then the same local evaluation as the hook.
937
984
  * Apply (autofix) is intentionally deferred to the gate on the next prompt — this pass only downloads
@@ -15,6 +15,7 @@ import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
15
15
  import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
16
16
  import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
17
17
  import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles, collectMcpFromClaudeJsonProjects, collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, determineFileTypeFromPath, } from '../collection/config_collector.js';
18
+ import { ensureCursorUserSettingsSnapshotInBatch } from '../collection/ensure_cursor_user_settings_snapshot.js';
18
19
  import { collectSkillsCliInstalled } from '../collection/skills_cli_collector.js';
19
20
  import { collectWorkspaceVscdbs } from '../collection/mcp_tool_collector.js';
20
21
  import { collectCursorProjectWorkspaceMcpConfigs } from '../collection/cursor_project_mcp_collector.js';
@@ -227,6 +228,7 @@ async function main() {
227
228
  throw err;
228
229
  }
229
230
  await addSensitivePathsAudit(endpointBase, configFiles);
231
+ ensureCursorUserSettingsSnapshotInBatch(configFiles);
230
232
  const fileTypes = [...new Set(configFiles.map((c) => c.file_type))].join(', ');
231
233
  hookRunLog(`collected ${configFiles.length} config file(s) file_types=${fileTypes}`);
232
234
  for (const c of configFiles) {
@@ -69,6 +69,7 @@ export function markRemediationApplyPendingVerification(uuid) {
69
69
  ...prev,
70
70
  pending_post_restart_verify: true,
71
71
  last_apply_at: new Date().toISOString(),
72
+ server_verified_at: undefined,
72
73
  };
73
74
  writeRemediationApplyTrackingFile(file);
74
75
  hookRunLog(`remediation_tracking: pending_post_restart_verify uuid=${uuid}`);
@@ -83,6 +84,7 @@ export function markRemediationApplyVerified(uuid) {
83
84
  quarantined: false,
84
85
  last_failure_reason: undefined,
85
86
  quarantined_at: undefined,
87
+ server_verified_at: new Date().toISOString(),
86
88
  };
87
89
  file.entries[uuid] = next;
88
90
  writeRemediationApplyTrackingFile(file);
@@ -15,7 +15,7 @@ function expandLeadingTilde(configFilePath) {
15
15
  }
16
16
  return configFilePath;
17
17
  }
18
- function cursorUserSettingsAbsolutePaths() {
18
+ export function cursorUserSettingsAbsolutePaths() {
19
19
  const h = homedir();
20
20
  if (process.platform === 'darwin') {
21
21
  const support = join(h, 'Library', 'Application Support');
@@ -127,3 +127,14 @@ export function resolveRemediationConfigPath(configFilePath) {
127
127
  }
128
128
  return `${candidates[0]}${frag}`;
129
129
  }
130
+ /** Correct log-config file_type when manifest rows target Cursor User/settings.json. */
131
+ export function resolveRemediationUploadFileType(configFilePath, manifestFileType) {
132
+ const t = normalizeSlashes(configFilePath.trim());
133
+ const portableSettings = PORTABLE_CURSOR_USER_SETTINGS;
134
+ if (t === portableSettings ||
135
+ t.endsWith(`/${portableSettings}`) ||
136
+ t.toLowerCase().includes('cursor/user/settings.json')) {
137
+ return 'vscode_settings';
138
+ }
139
+ return (manifestFileType ?? '').trim();
140
+ }
@@ -355,6 +355,50 @@ function applyStringArrayDelta(current, before, after) {
355
355
  }
356
356
  return next;
357
357
  }
358
+ export const GLOBAL_CURSOR_IGNORE_SETTING_PATH = 'cursor.general.globalCursorIgnoreList';
359
+ export function isGlobalCursorIgnoreRemediationViolation(v) {
360
+ return (v.setting_path ?? '').trim() === GLOBAL_CURSOR_IGNORE_SETTING_PATH;
361
+ }
362
+ /** Match server scan: flat VS Code key wins when present (including []). */
363
+ export function globalCursorIgnoreListFromSettings(configJson) {
364
+ const flat = configJson[GLOBAL_CURSOR_IGNORE_SETTING_PATH];
365
+ if (Array.isArray(flat))
366
+ return flat;
367
+ const nested = getByPath(configJson, GLOBAL_CURSOR_IGNORE_SETTING_PATH);
368
+ if (Array.isArray(nested))
369
+ return nested;
370
+ return [];
371
+ }
372
+ export function persistGlobalCursorIgnoreList(configJson, list) {
373
+ configJson[GLOBAL_CURSOR_IGNORE_SETTING_PATH] = list;
374
+ setByPathNested(configJson, GLOBAL_CURSOR_IGNORE_SETTING_PATH, list);
375
+ }
376
+ /** Keep flat and nested global ignore lists aligned; prefer flat when it is an array. */
377
+ function syncFlatGlobalCursorIgnoreListKey(configJson) {
378
+ const flat = configJson[GLOBAL_CURSOR_IGNORE_SETTING_PATH];
379
+ if (Array.isArray(flat)) {
380
+ setByPathNested(configJson, GLOBAL_CURSOR_IGNORE_SETTING_PATH, flat);
381
+ return;
382
+ }
383
+ const nested = getByPath(configJson, GLOBAL_CURSOR_IGNORE_SETTING_PATH);
384
+ if (Array.isArray(nested)) {
385
+ configJson[GLOBAL_CURSOR_IGNORE_SETTING_PATH] = nested;
386
+ }
387
+ }
388
+ function listForComplianceTarget(configJson, targetPath) {
389
+ if (targetPath === GLOBAL_CURSOR_IGNORE_SETTING_PATH) {
390
+ return [...globalCursorIgnoreListFromSettings(configJson)];
391
+ }
392
+ const curVal = getByPath(configJson, targetPath);
393
+ return Array.isArray(curVal) ? [...curVal] : [];
394
+ }
395
+ function writeListAtComplianceTarget(configJson, targetPath, next) {
396
+ if (targetPath === GLOBAL_CURSOR_IGNORE_SETTING_PATH) {
397
+ persistGlobalCursorIgnoreList(configJson, next);
398
+ return;
399
+ }
400
+ setByPath(configJson, targetPath, next);
401
+ }
358
402
  function applyCheck(configJson, check) {
359
403
  const parts = check.setting_path.split('.');
360
404
  const leafKey = parts[parts.length - 1] ?? '';
@@ -370,11 +414,16 @@ function applyCheck(configJson, check) {
370
414
  for (const k of keys) {
371
415
  const targetPath = resolveOpsTargetPath(check.setting_path, k);
372
416
  if (set && Object.prototype.hasOwnProperty.call(set, k)) {
373
- setByPath(configJson, targetPath, set[k]);
417
+ const value = set[k];
418
+ if (targetPath === GLOBAL_CURSOR_IGNORE_SETTING_PATH && Array.isArray(value)) {
419
+ persistGlobalCursorIgnoreList(configJson, value);
420
+ }
421
+ else {
422
+ setByPath(configJson, targetPath, value);
423
+ }
374
424
  continue;
375
425
  }
376
- const curVal = getByPath(configJson, targetPath);
377
- const cur = Array.isArray(curVal) ? [...curVal] : [];
426
+ const cur = listForComplianceTarget(configJson, targetPath);
378
427
  const toRemove = (remove && remove[k]) ?? [];
379
428
  const toAdd = (add && add[k]) ?? [];
380
429
  const next = cur.filter((x) => !toRemove.some((item) => deepEqual(item, x)));
@@ -383,7 +432,7 @@ function applyCheck(configJson, check) {
383
432
  next.push(x);
384
433
  }
385
434
  }
386
- setByPath(configJson, targetPath, next);
435
+ writeListAtComplianceTarget(configJson, targetPath, next);
387
436
  }
388
437
  return;
389
438
  }
@@ -1051,8 +1100,8 @@ export async function applyDeferredVscdbFromDisk() {
1051
1100
  * macOS Cursor: after deferred **SQLite / state.vscdb** autofix — apply queued writes, SIGKILL, reopen project.
1052
1101
  *
1053
1102
  * When autofix used the deferred vscdb path, `applyAutofixViolations` replaces any manifest `restart_command`
1054
- * with this string (see compliance_check.ts). For **JSON settings-file** remediations only, the trusted template
1055
- * is the JSON-settings-only Cursor template (kill + reopen, no `apply_deferred_vscdb`).
1103
+ * with this string (see compliance_check.ts). For **JSON settings-file** remediations, use
1104
+ * {@link buildCursorJsonSettingsRestartCommand} (kill + reopen).
1056
1105
  */
1057
1106
  export function buildDeferredCursorRestartCommand() {
1058
1107
  // Prefer monorepo path when hooks run from optimus-secure-fdn; otherwise `npx --yes log-llm-config@latest apply-deferred-vscdb`
@@ -1062,6 +1111,20 @@ export function buildDeferredCursorRestartCommand() {
1062
1111
  export function buildCursorJsonSettingsRestartCommand() {
1063
1112
  return TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND;
1064
1113
  }
1114
+ /**
1115
+ * Cursor JSON settings are written synchronously; when local recheck passes, report verified before
1116
+ * restart (restart only reloads disk into the running app). Deferred state.vscdb must wait until
1117
+ * apply_deferred_vscdb runs in the trusted restart script, then the next compliance gate.
1118
+ */
1119
+ export function cursorAutofixDefersInlineVerify(ide, restartCommandCount, deferredSqlitePending, recheckOk) {
1120
+ if (ide !== 'cursor' || restartCommandCount === 0)
1121
+ return false;
1122
+ if (deferredSqlitePending)
1123
+ return true;
1124
+ if (recheckOk)
1125
+ return false;
1126
+ return true;
1127
+ }
1065
1128
  function sqliteRowGroupKey(dbPath, op) {
1066
1129
  return `${dbPath}|${op.table}|${op.key_column}|${op.value_column}|${op.target_key}`;
1067
1130
  }
@@ -1413,6 +1476,7 @@ export function enforceRemediation(instruction) {
1413
1476
  for (const check of checks) {
1414
1477
  applyCheck(configJson, check);
1415
1478
  }
1479
+ syncFlatGlobalCursorIgnoreListKey(configJson);
1416
1480
  const content = JSON.stringify(configJson, null, 2);
1417
1481
  const tmp = `${inst.config_file_path}.tmp`;
1418
1482
  writeFileSync(tmp, content, 'utf8');
@@ -1463,8 +1527,12 @@ export function reportAutofixApplied(remediationUuid, result, details) {
1463
1527
  payload.failure_reason = details.failure_reason.slice(0, 2000);
1464
1528
  if (details?.consecutive_failures != null)
1465
1529
  payload.consecutive_failures = details.consecutive_failures;
1530
+ const bodyPayload = { ...payload, signature: '' };
1531
+ if (details?.config_snapshot_after && typeof details.config_snapshot_after === 'object') {
1532
+ bodyPayload.config_snapshot_after = details.config_snapshot_after;
1533
+ }
1466
1534
  const signature = createSignature(payload, authKey.key);
1467
- const body = JSON.stringify({ ...payload, signature });
1535
+ const body = JSON.stringify({ ...bodyPayload, signature });
1468
1536
  return executeBody(url, 'POST', body, 8000)
1469
1537
  .then(({ statusCode }) => {
1470
1538
  if (statusCode === 200) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {