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.
- package/dist/compliance_prompt_gate.js +29 -16
- package/dist/log_config_files/collection/config_collector.js +12 -0
- package/dist/log_config_files/collection/ensure_cursor_user_settings_snapshot.js +50 -0
- package/dist/log_config_files/paths/pattern_resolver.js +1 -1
- package/dist/log_config_files/runtime/client_event_reporter.js +11 -1
- package/dist/log_config_files/runtime/compliance_check.js +67 -20
- package/dist/log_config_files/runtime/main_runner.js +2 -0
- package/dist/log_config_files/runtime/remediation_apply_tracking.js +2 -0
- package/dist/log_config_files/runtime/remediation_config_path.js +12 -1
- package/dist/log_config_files/runtime/remediation_sync.js +75 -7
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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=${
|
|
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:
|
|
180
|
-
hookRunLog(`secondary_upload: uuid=${uuid} path=${
|
|
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=${
|
|
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
|
|
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
|
-
|
|
314
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
569
|
-
* user's next prompt
|
|
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 ??
|
|
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
|
|
892
|
-
if (!
|
|
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:
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1055
|
-
*
|
|
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({ ...
|
|
1535
|
+
const body = JSON.stringify({ ...bodyPayload, signature });
|
|
1468
1536
|
return executeBody(url, 'POST', body, 8000)
|
|
1469
1537
|
.then(({ statusCode }) => {
|
|
1470
1538
|
if (statusCode === 200) {
|