log-llm-config 1.3.16 → 1.3.18

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.
@@ -10,7 +10,7 @@ import { existsSync, statSync } from 'node:fs';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { basename } from 'node:path';
12
12
  import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
13
- import { hookLogSessionBanner, hookRunLog } from './log_config_files/runtime/hook_logger.js';
13
+ import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
14
14
  const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
15
15
  function parseIde() {
16
16
  const eq = process.argv.find((a) => a.startsWith('--ide='));
@@ -101,6 +101,11 @@ export async function runCompliancePromptGate() {
101
101
  const staleDays = Math.floor(staleMs / (24 * 60 * 60 * 1000));
102
102
  const advisory = `Remediation enforcement suspended: local remediation manifest is stale (${staleDays} days old). ` +
103
103
  'Please reconnect to sync latest policy state.';
104
+ logRemediationApplyFailure('prompt_gate_stale_manifest_advisory', {
105
+ reason: advisory,
106
+ stale_days: staleDays,
107
+ violation_count: status.violations.length,
108
+ });
104
109
  printAllowWithAdvisory(ide, advisory);
105
110
  return;
106
111
  }
@@ -113,6 +118,12 @@ export async function runCompliancePromptGate() {
113
118
  if (failedViolations.length > 0) {
114
119
  const ids = failedViolations.map((v) => `[${v.finding_formatted_id}]`).join(', ');
115
120
  const msg = `Auto-fix failed for ${ids} — please fix manually or contact your security team.\n\n${failedViolations[0]?.message ?? ''}`;
121
+ logRemediationApplyFailure('prompt_gate_block_autofix_failed', {
122
+ reason: 'autofix had fixed>0 path but failedViolations non-empty after apply',
123
+ ide,
124
+ failed_formatted_ids: failedViolations.map((v) => v.finding_formatted_id).join(', '),
125
+ detail: failedViolations[0]?.message ?? '',
126
+ });
116
127
  console.log(blockPayload(ide, msg));
117
128
  return;
118
129
  }
@@ -139,16 +150,35 @@ export async function runCompliancePromptGate() {
139
150
  return;
140
151
  }
141
152
  const msg = recheck.violations[0]?.message ?? 'A security policy violation has been detected.';
153
+ logRemediationApplyFailure('prompt_gate_block_recheck_failed', {
154
+ reason: 'after autofix, local compliance recheck still reports violations',
155
+ ide,
156
+ deferred_sqlite_pending: deferredSqlitePending,
157
+ first_uuid: recheck.violations[0]?.uuid ?? '',
158
+ message: msg,
159
+ });
142
160
  console.log(blockPayload(ide, msg));
143
161
  return;
144
162
  }
145
163
  if (failedViolations.length > 0) {
146
164
  const ids = failedViolations.map((v) => `[${v.finding_formatted_id}]`).join(', ');
147
165
  const msg = `Auto-fix failed for ${ids} — please fix manually or contact your security team.\n\n${failedViolations[0]?.message ?? ''}`;
166
+ logRemediationApplyFailure('prompt_gate_block_autofix_failed', {
167
+ reason: 'autofix failed (fixed=0 branch or partial failure)',
168
+ ide,
169
+ failed_formatted_ids: failedViolations.map((v) => v.finding_formatted_id).join(', '),
170
+ detail: failedViolations[0]?.message ?? '',
171
+ });
148
172
  console.log(blockPayload(ide, msg));
149
173
  return;
150
174
  }
151
175
  const msg = status.violations[0]?.message ?? 'A security policy violation has been detected.';
176
+ logRemediationApplyFailure('prompt_gate_block_violations_no_autofix_applied', {
177
+ reason: 'violations on submit but autofix did not succeed — see autofix_skipped_not_allowed, enforceRemediation, or compliance_check entries',
178
+ ide,
179
+ first_uuid: status.violations[0]?.uuid ?? '',
180
+ finding_formatted_id: status.violations[0]?.finding_formatted_id ?? '',
181
+ });
152
182
  console.log(blockPayload(ide, msg));
153
183
  return;
154
184
  }
@@ -2,6 +2,7 @@ import { homedir } from 'node:os';
2
2
  import { readVSCDBState } from './vscdb_reader.js';
3
3
  import { getVscdbPath } from '../paths/path_constants_helpers.js';
4
4
  import { getByPath } from '../collection/enrichment_helpers.js';
5
+ import { hookRunLog } from '../runtime/hook_logger.js';
5
6
  /** True if path is a vscdb virtual path (db file or db#fragment). Uses backend vscdb_basename and vscdb_entry_specs only. */
6
7
  export function isVscdbVirtualPath(filePath, response) {
7
8
  if (!response?.vscdb_entry_specs?.length)
@@ -93,17 +94,47 @@ function buildVscdbRawContentFromSpec(state, spec) {
93
94
  * Build config file entries from state.vscdb using backend-driven specs and read queries.
94
95
  * No hardcoded file_type, path fragments, or ItemTable keys; all from API.
95
96
  */
97
+ function summarizeComposerStateValue(composerState) {
98
+ if (composerState === null || composerState === undefined || typeof composerState !== 'object' || Array.isArray(composerState)) {
99
+ return `composerState_type=${composerState === null ? 'null' : typeof composerState}`;
100
+ }
101
+ const o = composerState;
102
+ const m4 = o.modes4;
103
+ if (!Array.isArray(m4))
104
+ return `modes4=${m4 === undefined ? 'absent' : typeof m4}`;
105
+ const agent = m4.find((x) => x !== null && typeof x === 'object' && !Array.isArray(x) && x.id === 'agent');
106
+ if (!agent)
107
+ return `modes4_len=${m4.length} agent_row=missing`;
108
+ return `modes4_len=${m4.length} agent_autoRun=${JSON.stringify(agent.autoRun)} agent_fullAutoRun=${JSON.stringify(agent.fullAutoRun)}`;
109
+ }
110
+ /** Exported for main_runner: one-line summary of uploaded #composerState payload (policy full-auto-run input). */
111
+ export function summarizeComposerPayloadForDiagnostics(raw) {
112
+ const inner = summarizeComposerStateValue(raw.composerState);
113
+ const rawKeys = Object.keys(raw)
114
+ .filter((k) => k !== 'source' && k !== 'extracted_at')
115
+ .slice(0, 16);
116
+ return `${inner} payload_keys=[${rawKeys.join(',')}]`;
117
+ }
96
118
  function collectVscdbEntries(vscdbPath, specs, readQueries, mergeFromComposerState) {
97
119
  if (!specs?.length)
98
120
  return [];
99
121
  const state = readVSCDBState(vscdbPath, readQueries, mergeFromComposerState);
100
- if (!state || typeof state !== 'object')
122
+ if (!state || typeof state !== 'object') {
123
+ hookRunLog(`diag vscdb collect: readVSCDBState returned null/invalid vscdb=${vscdbPath}`);
101
124
  return [];
125
+ }
126
+ const sk = Object.keys(state);
127
+ hookRunLog(`diag vscdb collect: readVSCDBState ok vscdb=${vscdbPath} top_keys=[${sk.slice(0, 24).join(',')}] ${summarizeComposerStateValue(state.composerState)}`);
102
128
  const entries = [];
103
129
  for (const spec of specs) {
130
+ const atPath = getByPath(state, spec.state_key);
104
131
  const rawContent = buildVscdbRawContentFromSpec(state, spec);
105
- if (!rawContent)
132
+ if (!rawContent) {
133
+ if (spec.state_key === 'composerState' || spec.path_suffix === '#composerState') {
134
+ hookRunLog(`diag vscdb collect: skipped composer emit value_constraint=${spec.value_constraint ?? 'none'} at_path=${atPath === undefined ? 'undefined' : atPath === null ? 'null' : typeof atPath} path_suffix=${spec.path_suffix}`);
135
+ }
106
136
  continue;
137
+ }
107
138
  entries.push({
108
139
  file_type: spec.file_type,
109
140
  file_path: `${vscdbPath}${spec.path_suffix}`,
@@ -100,12 +100,16 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
100
100
  return { [itemKey]: parsed };
101
101
  }
102
102
  // Bare JSON primitives. Scalar toggles must be wrapped so getByPath(key.field) works in compliance checks.
103
+ // Match coerceScalarForItemTableField in remediation_sync: Cursor may store toggles as JSON 0/1, not only booleans.
103
104
  if (typeof parsed === 'boolean' || typeof parsed === 'number' || typeof parsed === 'string') {
104
- if (typeof parsed === 'boolean') {
105
- const field = CURSOR_SCALAR_ITEMTABLE_FIELDS[itemKey];
106
- if (field) {
105
+ const field = CURSOR_SCALAR_ITEMTABLE_FIELDS[itemKey];
106
+ if (field) {
107
+ if (typeof parsed === 'boolean') {
107
108
  return { [itemKey]: { [field]: parsed } };
108
109
  }
110
+ if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
111
+ return { [itemKey]: { [field]: parsed !== 0 } };
112
+ }
109
113
  }
110
114
  return { [itemKey]: parsed };
111
115
  }
@@ -13,7 +13,8 @@
13
13
  import { existsSync, readFileSync } from 'node:fs';
14
14
  import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
15
15
  import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
16
- import { complianceRunnerDiag, hookRunLog } from './hook_logger.js';
16
+ import { resolveRemediationConfigPath } from './remediation_config_path.js';
17
+ import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
17
18
  import { loadEndpointBase } from '../sender/endpoint_config.js';
18
19
  import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
19
20
  import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
@@ -97,10 +98,11 @@ function verifyOpsApplied(configJson, settingPath, ops) {
97
98
  }
98
99
  /** Plain JSON file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
99
100
  function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
100
- const hashIdx = configFilePath.indexOf('#');
101
+ const resolvedPath = resolveRemediationConfigPath(configFilePath);
102
+ const hashIdx = resolvedPath.indexOf('#');
101
103
  if (hashIdx >= 0) {
102
- const dbPath = configFilePath.slice(0, hashIdx);
103
- const itemKeyFromPath = configFilePath.slice(hashIdx + 1).trim();
104
+ const dbPath = resolvedPath.slice(0, hashIdx);
105
+ const itemKeyFromPath = resolvedPath.slice(hashIdx + 1).trim();
104
106
  if (!itemKeyFromPath)
105
107
  return { ok: false, reason: 'empty_vscdb_key' };
106
108
  if (!existsSync(dbPath))
@@ -115,10 +117,10 @@ function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
115
117
  }
116
118
  return { ok: true, json: merged };
117
119
  }
118
- if (!existsSync(configFilePath))
120
+ if (!existsSync(resolvedPath))
119
121
  return { ok: false, reason: 'file_not_found' };
120
122
  try {
121
- return { ok: true, json: JSON.parse(readFileSync(configFilePath, 'utf8')) };
123
+ return { ok: true, json: JSON.parse(readFileSync(resolvedPath, 'utf8')) };
122
124
  }
123
125
  catch {
124
126
  return { ok: false, reason: 'parse_error' };
@@ -164,6 +166,13 @@ export function runLocalRemediationComplianceCheck() {
164
166
  ? `compliance_check: invalid vscdb path (empty # key), skipping uuid=${entry.uuid}`
165
167
  : `compliance_check: could not parse config file, skipping uuid=${entry.uuid}`;
166
168
  hookRunLog(msg);
169
+ logRemediationApplyFailure('compliance_check_config_unreadable', {
170
+ uuid: entry.uuid,
171
+ config_file_path: entry.config_file_path,
172
+ finding_formatted_id: compliance.finding_formatted_id,
173
+ load_reason: loaded.reason,
174
+ reason: 'cannot read config for compliance verify — remediation may not run until path/db is valid',
175
+ });
167
176
  continue;
168
177
  }
169
178
  const configJson = loaded.json;
@@ -223,7 +232,14 @@ export function runLocalRemediationComplianceCheck() {
223
232
  return status;
224
233
  }
225
234
  catch (err) {
226
- hookRunLog(`compliance_check: unexpected error: ${err instanceof Error ? err.message : String(err)}`);
235
+ const emsg = err instanceof Error ? err.message : String(err);
236
+ const stack = err instanceof Error ? err.stack : undefined;
237
+ hookRunLog(`compliance_check: unexpected error: ${emsg}`);
238
+ logRemediationApplyFailure('compliance_check_exception', {
239
+ reason: 'runLocalRemediationComplianceCheck threw',
240
+ error: emsg,
241
+ stack: stack ?? '',
242
+ });
227
243
  const fallback = {
228
244
  status: 'ok',
229
245
  checked_at: new Date().toISOString(),
@@ -234,6 +250,17 @@ export function runLocalRemediationComplianceCheck() {
234
250
  }
235
251
  }
236
252
  export function applyAutofixViolations(violations) {
253
+ for (const v of violations) {
254
+ if (!v.autofix_allowed) {
255
+ logRemediationApplyFailure('autofix_skipped_not_allowed', {
256
+ uuid: v.uuid,
257
+ finding_formatted_id: v.finding_formatted_id,
258
+ config_file_path: v.config_file_path,
259
+ setting_path: v.setting_path,
260
+ reason: 'autofix_allowed is false — policy/manual fix required',
261
+ });
262
+ }
263
+ }
237
264
  const autofixable = violations.filter((v) => v.autofix_allowed);
238
265
  if (autofixable.length === 0)
239
266
  return {
@@ -259,11 +286,19 @@ export function applyAutofixViolations(violations) {
259
286
  const instruction = byUuid.get(violation.uuid);
260
287
  if (!instruction) {
261
288
  hookRunLog(`autofix: no instruction found for uuid=${violation.uuid}, skipping`);
289
+ logRemediationApplyFailure('autofix_no_local_instruction', {
290
+ uuid: violation.uuid,
291
+ finding_formatted_id: violation.finding_formatted_id,
292
+ config_file_path: violation.config_file_path,
293
+ setting_path: violation.setting_path,
294
+ reason: 'violation UUID not present in remediation_instructions.json remediations[]',
295
+ });
262
296
  failedViolations.push(violation);
263
297
  continue;
264
298
  }
265
299
  const inst = instruction;
266
- complianceRunnerDiag(`autofix: calling enforceRemediation uuid=${inst.uuid} path=${inst.config_file_path}`);
300
+ const configPathForDisk = resolveRemediationConfigPath(inst.config_file_path);
301
+ complianceRunnerDiag(`autofix: calling enforceRemediation uuid=${inst.uuid} path=${configPathForDisk}`);
267
302
  const er = enforceRemediation(inst);
268
303
  if (!er.ok) {
269
304
  failedViolations.push(violation);
@@ -274,25 +309,25 @@ export function applyAutofixViolations(violations) {
274
309
  seen.add(violation.uuid);
275
310
  fixed++;
276
311
  appliedViolations.push(violation);
277
- hookRunLog(`autofix: applied uuid=${inst.uuid} path=${inst.config_file_path}`);
312
+ hookRunLog(`autofix: applied uuid=${inst.uuid} path=${configPathForDisk}`);
278
313
  reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
279
314
  const authKey = readStoredAuthKey();
280
315
  if (authKey) {
281
- if (er.deferredSqlite && inst.config_file_path.includes('#')) {
316
+ if (er.deferredSqlite && configPathForDisk.includes('#')) {
282
317
  hookRunLog(`autofix: skip immediate vscdb upload (deferred until after restart) uuid=${inst.uuid}`);
283
318
  }
284
319
  else {
285
320
  let updatedContent;
286
- if (inst.config_file_path.includes('#')) {
287
- const hi = inst.config_file_path.indexOf('#');
288
- const dbPath = inst.config_file_path.slice(0, hi);
289
- const itemKey = inst.config_file_path.slice(hi + 1).trim();
321
+ if (configPathForDisk.includes('#')) {
322
+ const hi = configPathForDisk.indexOf('#');
323
+ const dbPath = configPathForDisk.slice(0, hi);
324
+ const itemKey = configPathForDisk.slice(hi + 1).trim();
290
325
  updatedContent =
291
326
  itemKey ? (readVscdbItemTableJson(dbPath, itemKey) ?? undefined) : undefined;
292
327
  }
293
328
  else {
294
329
  try {
295
- updatedContent = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
330
+ updatedContent = JSON.parse(readFileSync(configPathForDisk, 'utf8'));
296
331
  }
297
332
  catch {
298
333
  updatedContent = undefined;
@@ -303,8 +338,8 @@ export function applyAutofixViolations(violations) {
303
338
  if (fileType) {
304
339
  const hw = tryResolveHardwareUuid();
305
340
  if (hw) {
306
- reportPromises.push(sendConfigFile({ file_type: fileType, file_path: inst.config_file_path, raw_content: updatedContent }, hw, authKey).then((sentOk) => {
307
- hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${inst.config_file_path} ok=${sentOk}`);
341
+ reportPromises.push(sendConfigFile({ file_type: fileType, file_path: configPathForDisk, raw_content: updatedContent }, hw, authKey).then((sentOk) => {
342
+ hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${configPathForDisk} ok=${sentOk}`);
308
343
  }));
309
344
  }
310
345
  else {
@@ -313,6 +348,11 @@ export function applyAutofixViolations(violations) {
313
348
  }
314
349
  else {
315
350
  hookRunLog(`autofix: skip upload uuid=${inst.uuid} — remediation_instructions.json missing file_type (re-sync manifest)`);
351
+ logRemediationApplyFailure('autofix_post_apply_upload_skipped', {
352
+ uuid: inst.uuid,
353
+ config_file_path: inst.config_file_path,
354
+ reason: 'file_type missing on instruction — server sync/manifest will not see applied file',
355
+ });
316
356
  }
317
357
  }
318
358
  }
@@ -3,6 +3,8 @@ import path from 'node:path';
3
3
  import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
4
4
  const HOOK_LOG_FILENAME = 'hook_log.txt';
5
5
  const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
6
+ /** Append-only: remediation verify/apply failures (not cleared per hook session). */
7
+ const REMEDIATION_APPLY_FAILURES_FILENAME = 'remediation_apply_failures.log';
6
8
  /** Hard cap so a single upload/sync session cannot grow hook_log.txt without bound. */
7
9
  const MAX_HOOK_LOG_BYTES = 2 * 1024 * 1024;
8
10
  function getHookLogPath() {
@@ -17,6 +19,12 @@ function getComplianceRunnerLogPath() {
17
19
  return null;
18
20
  return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_RUNNER_LOG_FILENAME);
19
21
  }
22
+ function getRemediationApplyFailuresLogPath() {
23
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
24
+ if (!homeDir)
25
+ return null;
26
+ return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, REMEDIATION_APPLY_FAILURES_FILENAME);
27
+ }
20
28
  /**
21
29
  * Append-only diagnostics (not truncated by main_runner). Use for remediation sync / compliance.
22
30
  */
@@ -35,6 +43,53 @@ function complianceRunnerDiag(message) {
35
43
  // best-effort
36
44
  }
37
45
  }
46
+ /**
47
+ * Loud, append-only record when a remediation cannot be verified or applied. Writes
48
+ * {@link REMEDIATION_APPLY_FAILURES_FILENAME} and mirrors the same block to compliance_runner.log;
49
+ * also appends a single summary line to hook_log.txt for the current session.
50
+ */
51
+ function logRemediationApplyFailure(context, fields) {
52
+ const ts = new Date().toISOString();
53
+ const bodyLines = Object.entries(fields)
54
+ .filter(([, v]) => v !== undefined && v !== null && String(v) !== '')
55
+ .map(([k, v]) => ` ${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`);
56
+ const block = [
57
+ '',
58
+ '='.repeat(72),
59
+ `${ts} REMEDIATION APPLY FAILURE — ${context}`,
60
+ ...bodyLines,
61
+ '='.repeat(72),
62
+ '',
63
+ ].join('\n');
64
+ const summary = fields.reason != null
65
+ ? String(fields.reason)
66
+ : fields.message != null
67
+ ? String(fields.message)
68
+ : 'see fields above';
69
+ const writeAppend = (filePath) => {
70
+ const dir = path.dirname(filePath);
71
+ if (!existsSync(dir))
72
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
73
+ appendFileSync(filePath, block, 'utf8');
74
+ };
75
+ try {
76
+ const failPath = getRemediationApplyFailuresLogPath();
77
+ if (failPath)
78
+ writeAppend(failPath);
79
+ const crPath = getComplianceRunnerLogPath();
80
+ if (crPath)
81
+ writeAppend(crPath);
82
+ }
83
+ catch {
84
+ /* best-effort */
85
+ }
86
+ try {
87
+ hookRunLog(`REMEDIATION_APPLY_FAILURE [${context}] ${fields.uuid != null ? `uuid=${fields.uuid} ` : ''}${summary}`);
88
+ }
89
+ catch {
90
+ /* best-effort */
91
+ }
92
+ }
38
93
  function ensureHookLogUnderCap() {
39
94
  const logPath = getHookLogPath();
40
95
  if (!logPath || !existsSync(logPath))
@@ -121,4 +176,4 @@ function hookLogLine(message) {
121
176
  // best-effort
122
177
  }
123
178
  }
124
- export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookLogAppendSection, hookRunLog, hookLogLine, complianceRunnerDiag, };
179
+ export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookLogAppendSection, hookRunLog, hookLogLine, complianceRunnerDiag, logRemediationApplyFailure, };
@@ -9,7 +9,7 @@ import { hookLogReplace, hookRunLog } from './hook_logger.js';
9
9
  import { resolveHardwareUuid } from './hardware_uuid.js';
10
10
  import { ensureAuthentication } from '../auth/auth_flow.js';
11
11
  import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
12
- import { isVscdbVirtualPath, tryReadVscdbVirtualFile } from '../readers/vscdb_config_builder.js';
12
+ import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
13
13
  import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
14
14
  import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, determineFileTypeFromPath } from '../collection/config_collector.js';
15
15
  import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
@@ -126,6 +126,12 @@ async function main() {
126
126
  await addSensitivePathsAudit(endpointBase, configFiles);
127
127
  const fileTypes = [...new Set(configFiles.map((c) => c.file_type))].join(', ');
128
128
  hookRunLog(`collected ${configFiles.length} config file(s) file_types=${fileTypes}`);
129
+ for (const c of configFiles) {
130
+ if (!c.file_path.replace(/\\/g, '/').includes('#composerState'))
131
+ continue;
132
+ const tail = c.file_path.length > 120 ? `…${c.file_path.slice(-120)}` : c.file_path;
133
+ hookRunLog(`diag pre-upload #composerState ${summarizeComposerPayloadForDiagnostics(c.raw_content)} path=${tail}`);
134
+ }
129
135
  const claudeSettings = configFiles.filter((c) => c.file_type === 'claude_settings');
130
136
  if (claudeSettings.length > 0)
131
137
  hookRunLog(`claude_settings in batch: ${claudeSettings.length} path(s): ${claudeSettings.map((c) => c.file_path).join(', ')}`);
@@ -0,0 +1,48 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Portable key stored by the server for Cursor User globalStorage state.vscdb
6
+ * (see optimus_security/endpoint/log_config_file/handler._canonical_cursor_user_state_vscdb_path).
7
+ */
8
+ export const PORTABLE_CURSOR_USER_STATE_VSCDB = 'Cursor/User/globalStorage/state.vscdb';
9
+ function normalizeSlashes(p) {
10
+ return p.trim().replace(/\\/g, '/');
11
+ }
12
+ function cursorStateVscdbAbsoluteBasePaths() {
13
+ const h = homedir();
14
+ if (process.platform === 'darwin') {
15
+ return [join(h, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
16
+ }
17
+ if (process.platform === 'win32') {
18
+ const appData = process.env.APPDATA;
19
+ if (appData)
20
+ return [join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
21
+ return [join(h, 'AppData', 'Roaming', 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
22
+ }
23
+ return [join(h, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
24
+ }
25
+ /**
26
+ * Expand server-side portable Cursor state.vscdb paths to a local absolute path.
27
+ * No-op for absolute paths and for relative paths that are not the portable vscdb key.
28
+ */
29
+ export function resolveRemediationConfigPath(configFilePath) {
30
+ const t = normalizeSlashes(configFilePath);
31
+ const hash = t.indexOf('#');
32
+ const base = hash >= 0 ? t.slice(0, hash) : t;
33
+ const frag = hash >= 0 ? t.slice(hash) : '';
34
+ const baseNorm = base.replace(/\/+$/, '');
35
+ if (baseNorm.startsWith('/') || /^[a-zA-Z]:\//.test(baseNorm)) {
36
+ return configFilePath;
37
+ }
38
+ const portable = PORTABLE_CURSOR_USER_STATE_VSCDB;
39
+ if (baseNorm !== portable && !baseNorm.endsWith('/' + portable)) {
40
+ return configFilePath;
41
+ }
42
+ const candidates = cursorStateVscdbAbsoluteBasePaths();
43
+ for (const abs of candidates) {
44
+ if (existsSync(abs))
45
+ return `${abs}${frag}`;
46
+ }
47
+ return `${candidates[0]}${frag}`;
48
+ }
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkS
2
2
  import { dirname } from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { executeGet, executeBody } from '../../endpoint_client/http_transport.js';
5
- import { complianceRunnerDiag, hookRunLog } from './hook_logger.js';
5
+ import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
6
6
  import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readFileCollectionVscdbContract, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
7
7
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
8
8
  import { createSignature } from '../sender/signing.js';
@@ -11,6 +11,7 @@ import { tryResolveHardwareUuid } from './hardware_uuid.js';
11
11
  import { CURSOR_SCALAR_ITEMTABLE_FIELDS, persistVscdbComposerContractFromPatternsResponse, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
12
12
  import { sendConfigFile } from '../sender/batch_sender.js';
13
13
  import { getFileCollectionPatterns } from '../../endpoint_client/registry_api.js';
14
+ import { resolveRemediationConfigPath } from './remediation_config_path.js';
14
15
  function reactiveStorageItemKeyFromContract() {
15
16
  const k = readFileCollectionVscdbContract()?.reactive_storage_item_key;
16
17
  return typeof k === 'string' && k.trim() !== '' ? k.trim() : undefined;
@@ -735,6 +736,14 @@ function sqliteRowGroupKey(dbPath, op) {
735
736
  * entry per row. Without this, two ops on the same `composerState` row each read the stale DB and queue
736
737
  * two full JSON blobs — the second UPDATE overwrites the first (e.g. only one of autoRun/fullAutoRun sticks).
737
738
  */
739
+ /** ItemTable keys must not contain SQL-quote garbage (bad vscdb #fragment or corrupted manifest). */
740
+ function assertSafeDeferredItemTableKey(targetKey) {
741
+ if (!targetKey || targetKey.length > 512)
742
+ return false;
743
+ if (/['"\\]/.test(targetKey))
744
+ return false;
745
+ return true;
746
+ }
738
747
  function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
739
748
  const dbPath = configPath.split('#')[0];
740
749
  if (!existsSync(dbPath)) {
@@ -758,6 +767,12 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
758
767
  try {
759
768
  for (const ops of groups.values()) {
760
769
  const first = ops[0];
770
+ if (!assertSafeDeferredItemTableKey(first.target_key)) {
771
+ const line = `sqlite_update: rejected unsafe or empty target_key for deferred queue (refusing to write state.vscdb)`;
772
+ hookRunLog(line);
773
+ complianceRunnerDiag(`${line} target_key_preview=${first.target_key.slice(0, 80)}`);
774
+ return false;
775
+ }
761
776
  complianceRunnerDiag(`sqlite_update: deferred merge db=${dbPath} target_key=${first.target_key} operations=${ops.length}`);
762
777
  let currentJson = {};
763
778
  try {
@@ -778,6 +793,12 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
778
793
  }
779
794
  repairComposerStateEmptySegmentBug(currentJson);
780
795
  const updatedJson = serializeItemTableValueForWrite(first.target_key, currentJson);
796
+ if (updatedJson === '{}' && Object.keys(currentJson).length === 0) {
797
+ const line = `sqlite_update: deferred merge produced empty JSON — refusing to queue (would wipe ItemTable row)`;
798
+ hookRunLog(line);
799
+ complianceRunnerDiag(line);
800
+ return false;
801
+ }
781
802
  queueDeferredVscdbItem({
782
803
  dbPath,
783
804
  table: first.table,
@@ -875,67 +896,99 @@ function applyOrQueueSqliteJsonUpdate(configPath, sqliteOp, deferred) {
875
896
  }
876
897
  }
877
898
  export function enforceRemediation(instruction) {
899
+ const resolvedPath = resolveRemediationConfigPath(instruction.config_file_path);
900
+ const inst = resolvedPath === instruction.config_file_path
901
+ ? instruction
902
+ : { ...instruction, config_file_path: resolvedPath };
903
+ const fail = (reason, extra) => {
904
+ const spec = remediationFixSpec(inst);
905
+ logRemediationApplyFailure('enforceRemediation', {
906
+ uuid: inst.uuid,
907
+ config_file_path: inst.config_file_path,
908
+ file_type: inst.file_type ?? '',
909
+ finding_formatted_id: spec?.finding_formatted_id ?? '',
910
+ reason,
911
+ ...extra,
912
+ });
913
+ hookRunLog(`remediation_enforce: failed uuid=${inst.uuid} reason=${reason}`);
914
+ return { ok: false, failureReason: reason };
915
+ };
878
916
  try {
879
- const fixSpec = remediationFixSpec(instruction);
917
+ const fixSpec = remediationFixSpec(inst);
880
918
  const checks = fixSpec?.checks ?? [];
881
919
  if (checks.length === 0) {
882
- hookRunLog(`remediation_enforce: no checks to apply uuid=${instruction.uuid}`);
883
- return { ok: false };
920
+ return fail('no checks in fix spec (empty or missing compliance checks)');
884
921
  }
885
922
  const sqliteOps = checks.filter((c) => {
886
923
  const raw = c;
887
924
  return raw.sqlite_op !== undefined;
888
925
  });
889
926
  if (sqliteOps.length > 0) {
890
- complianceRunnerDiag(`remediation_enforce: sqlite path uuid=${instruction.uuid} checks_with_sqlite=${sqliteOps.length}`);
927
+ complianceRunnerDiag(`remediation_enforce: sqlite path uuid=${inst.uuid} checks_with_sqlite=${sqliteOps.length}`);
891
928
  const restartRequired = !!fixSpec?.restart_required;
892
929
  if (restartRequired) {
893
930
  const ops = sqliteOps.map((c) => c.sqlite_op);
894
- const ft = instruction.file_type?.trim();
895
- const postApplyUpload = ft && instruction.config_file_path.includes('#')
896
- ? { file_path: instruction.config_file_path, file_type: ft }
931
+ const ft = inst.file_type?.trim();
932
+ const postApplyUpload = ft && inst.config_file_path.includes('#')
933
+ ? { file_path: inst.config_file_path, file_type: ft }
897
934
  : undefined;
898
- const ok = queueDeferredSqliteOpsMerged(instruction.config_file_path, ops, postApplyUpload);
935
+ const ok = queueDeferredSqliteOpsMerged(inst.config_file_path, ops, postApplyUpload);
936
+ if (!ok) {
937
+ return fail('deferred state.vscdb queue failed (database missing, sqlite3 unavailable, or read/merge error — see sqlite_update lines above in hook_log)', { config_file_path: inst.config_file_path });
938
+ }
899
939
  return { ok, deferredSqlite: ok };
900
940
  }
901
941
  let allSuccess = true;
902
942
  for (const check of sqliteOps) {
903
943
  const raw = check;
904
944
  const sqliteOp = raw.sqlite_op;
905
- const rowOk = applyOrQueueSqliteJsonUpdate(instruction.config_file_path, sqliteOp, false);
945
+ const rowOk = applyOrQueueSqliteJsonUpdate(inst.config_file_path, sqliteOp, false);
906
946
  if (!rowOk)
907
947
  allSuccess = false;
908
948
  }
909
- return { ok: allSuccess, deferredSqlite: false };
949
+ if (!allSuccess) {
950
+ return fail('immediate sqlite apply failed for one or more checks');
951
+ }
952
+ return { ok: true, deferredSqlite: false };
910
953
  }
911
954
  if (fixSpec?.file_format !== 'json') {
912
- hookRunLog(`remediation_enforce: unsupported file format ${fixSpec?.file_format} uuid=${instruction.uuid}`);
913
- return { ok: false };
955
+ return fail(`unsupported file format: ${String(fixSpec?.file_format ?? 'undefined')}`);
914
956
  }
915
- const dir = dirname(instruction.config_file_path);
957
+ const dir = dirname(inst.config_file_path);
916
958
  if (!existsSync(dir))
917
959
  mkdirSync(dir, { recursive: true, mode: 0o700 });
918
960
  let configJson = {};
919
- if (existsSync(instruction.config_file_path)) {
961
+ if (existsSync(inst.config_file_path)) {
920
962
  try {
921
- configJson = JSON.parse(readFileSync(instruction.config_file_path, 'utf8'));
963
+ configJson = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
922
964
  }
923
965
  catch {
924
- hookRunLog(`remediation_enforce: could not parse existing file, starting fresh uuid=${instruction.uuid}`);
966
+ hookRunLog(`remediation_enforce: could not parse existing file, starting fresh uuid=${inst.uuid}`);
925
967
  }
926
968
  }
927
969
  for (const check of checks) {
928
970
  applyCheck(configJson, check);
929
971
  }
930
972
  const content = JSON.stringify(configJson, null, 2);
931
- const tmp = `${instruction.config_file_path}.tmp`;
973
+ const tmp = `${inst.config_file_path}.tmp`;
932
974
  writeFileSync(tmp, content, 'utf8');
933
- renameSync(tmp, instruction.config_file_path);
975
+ renameSync(tmp, inst.config_file_path);
934
976
  return { ok: true };
935
977
  }
936
978
  catch (err) {
937
- hookRunLog(`remediation_enforce_error: uuid=${instruction.uuid} err=${err instanceof Error ? err.message : String(err)}`);
938
- return { ok: false };
979
+ const msg = err instanceof Error ? err.message : String(err);
980
+ const stack = err instanceof Error ? err.stack : undefined;
981
+ logRemediationApplyFailure('enforceRemediation', {
982
+ uuid: inst.uuid,
983
+ config_file_path: inst.config_file_path,
984
+ file_type: inst.file_type ?? '',
985
+ finding_formatted_id: remediationFixSpec(inst)?.finding_formatted_id ?? '',
986
+ reason: 'exception during enforce',
987
+ error: msg,
988
+ stack: stack ?? '',
989
+ });
990
+ hookRunLog(`remediation_enforce_error: uuid=${inst.uuid} err=${msg}`);
991
+ return { ok: false, failureReason: `exception: ${msg}` };
939
992
  }
940
993
  }
941
994
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.3.16",
3
+ "version": "1.3.18",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {