log-llm-config 1.3.0 → 1.3.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.
@@ -5,9 +5,16 @@
5
5
  import { applyAutofixViolations, pruneSatisfiedOneTimeRemediations, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
6
6
  import { existsSync, statSync } from 'node:fs';
7
7
  import { spawn } from 'node:child_process';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { resolve } from 'node:path';
8
10
  import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
9
11
  import { hookLogSessionBanner, hookRunLog } from './log_config_files/runtime/hook_logger.js';
10
12
  const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
13
+ /** Set by optimus-compliance-check.sh only for Claude Desktop (dumb TERM). Hook runs allowlisted restart after osascript. */
14
+ function claudeDesktopHookRunsRestart() {
15
+ const v = process.env.OPTIMUS_CLAUDE_DESKTOP_DEFER_GATE_RESTART?.trim().toLowerCase();
16
+ return v === '1' || v === 'true' || v === 'yes';
17
+ }
11
18
  function parseIde() {
12
19
  const eq = process.argv.find((a) => a.startsWith('--ide='));
13
20
  if (eq) {
@@ -63,7 +70,17 @@ function getManifestStalenessMs() {
63
70
  return null;
64
71
  }
65
72
  }
66
- const ide = parseIde();
73
+ function isRunAsCliModule() {
74
+ const entry = process.argv[1];
75
+ if (!entry)
76
+ return false;
77
+ try {
78
+ return import.meta.url === pathToFileURL(resolve(entry)).href;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
67
84
  /** Short line for success dialog: finding title/sentence from manifest, not per-check remediation technical text. */
68
85
  function autofixDialogLine(v) {
69
86
  const title = v.finding_title?.trim();
@@ -76,7 +93,9 @@ function autofixDialogLine(v) {
76
93
  const short = d.length > 160 ? `${d.slice(0, 157)}…` : d;
77
94
  return `• [${v.finding_formatted_id}] ${short}`;
78
95
  }
79
- async function run() {
96
+ /** Exported for tests; CLI invokes this only when {@link isRunAsCliModule} is true. */
97
+ export async function runCompliancePromptGate() {
98
+ const ide = parseIde();
80
99
  hookLogSessionBanner('compliance_prompt_gate (before submit)');
81
100
  const status = runLocalRemediationComplianceCheck();
82
101
  if (status.status === 'fail' && status.violations.length > 0) {
@@ -88,27 +107,31 @@ async function run() {
88
107
  printAllowWithAdvisory(ide, advisory);
89
108
  return;
90
109
  }
91
- const { fixed, restartCommands, failedViolations, reportPromises, deferredSqlitePending } = applyAutofixViolations(status.violations);
110
+ const { fixed, appliedViolations = [], restartCommands, failedViolations, reportPromises, deferredSqlitePending, } = applyAutofixViolations(status.violations);
92
111
  if (fixed > 0) {
93
112
  // Wait for all server reports before exiting so the POST lands.
94
113
  await Promise.allSettled(reportPromises);
114
+ // Deferred SQLite can leave recheck failing until restart; that must not hide a separate
115
+ // failed autofix (e.g. JSON remediation failed while vscdb was only queued).
116
+ if (failedViolations.length > 0) {
117
+ const ids = failedViolations.map((v) => `[${v.finding_formatted_id}]`).join(', ');
118
+ const msg = `Auto-fix failed for ${ids} — please fix manually or contact your security team.\n\n${failedViolations[0]?.message ?? ''}`;
119
+ console.log(blockPayload(ide, msg));
120
+ return;
121
+ }
95
122
  const recheck = runLocalRemediationComplianceCheck();
96
123
  const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
97
124
  if (deferredSqlitePending || recheckOk) {
98
- const fixedViolations = status.violations.filter((v) => v.autofix_allowed);
99
- const byUuid = new Map();
100
- for (const v of fixedViolations) {
101
- if (!byUuid.has(v.uuid))
102
- byUuid.set(v.uuid, v);
103
- }
104
- const deduped = [...byUuid.values()];
105
- const autofixMessage = `Optimus Security auto-fixed ${deduped.length} policy violation(s):\n${deduped.map((v) => autofixDialogLine(v)).join('\n')}`;
125
+ const autofixMessage = `Optimus Security auto-fixed ${fixed} policy violation(s):\n${appliedViolations.map((v) => autofixDialogLine(v)).join('\n')}`;
106
126
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
107
127
  if (restartCommands.length > 0)
108
128
  payload.restart_commands = restartCommands;
109
129
  console.log(JSON.stringify(payload));
110
130
  // Cursor: .cursor/hooks runs restart after the osascript dialog — avoid double SIGKILL here.
111
- if (ide !== 'cursor')
131
+ // Claude Desktop: same — optimus-compliance-check.sh shows the dialog then evals allowlisted restarts.
132
+ // Claude Code (CLI): no shell restart path; gate must spawn here.
133
+ const fireInGate = ide !== 'cursor' && !(ide === 'claude' && claudeDesktopHookRunsRestart());
134
+ if (fireInGate)
112
135
  fireRestartCommands(restartCommands);
113
136
  return;
114
137
  }
@@ -133,4 +156,6 @@ async function run() {
133
156
  }
134
157
  printAllow(ide);
135
158
  }
136
- run().catch(() => printAllow(ide)).finally(() => process.exit(0));
159
+ if (isRunAsCliModule()) {
160
+ runCompliancePromptGate().catch(() => printAllow(parseIde())).finally(() => process.exit(0));
161
+ }
@@ -13,8 +13,8 @@ import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
13
13
  import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
14
14
  import { complianceRunnerDiag, hookRunLog } from './hook_logger.js';
15
15
  import { loadEndpointBase } from '../sender/endpoint_config.js';
16
- import { resolveHardwareUuid } from './hardware_uuid.js';
17
- import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
16
+ import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
17
+ import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
18
18
  import { sendConfigFile } from '../sender/batch_sender.js';
19
19
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
20
20
  // ---------------------------------------------------------------------------
@@ -221,10 +221,17 @@ export function runLocalRemediationComplianceCheck() {
221
221
  export function applyAutofixViolations(violations) {
222
222
  const autofixable = violations.filter((v) => v.autofix_allowed);
223
223
  if (autofixable.length === 0)
224
- return { fixed: 0, restartCommands: [], failedViolations: [], reportPromises: [] };
224
+ return {
225
+ fixed: 0,
226
+ appliedViolations: [],
227
+ restartCommands: [],
228
+ failedViolations: [],
229
+ reportPromises: [],
230
+ };
225
231
  const { remediations } = readRemediationInstructionsFile();
226
232
  const byUuid = new Map(remediations.map((r) => [r.uuid, r]));
227
233
  let fixed = 0;
234
+ const appliedViolations = [];
228
235
  const seen = new Set();
229
236
  const restartCommands = [];
230
237
  const failedViolations = [];
@@ -251,6 +258,7 @@ export function applyAutofixViolations(violations) {
251
258
  deferredSqlitePending = true;
252
259
  seen.add(violation.uuid);
253
260
  fixed++;
261
+ appliedViolations.push(violation);
254
262
  hookRunLog(`autofix: applied uuid=${inst.uuid} path=${inst.config_file_path}`);
255
263
  reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
256
264
  const authKey = readStoredAuthKey();
@@ -278,9 +286,15 @@ export function applyAutofixViolations(violations) {
278
286
  if (updatedContent !== undefined) {
279
287
  const fileType = (inst.file_type ?? '').trim();
280
288
  if (fileType) {
281
- reportPromises.push(sendConfigFile({ file_type: fileType, file_path: inst.config_file_path, raw_content: updatedContent }, resolveHardwareUuid(), authKey).then((sentOk) => {
282
- hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${inst.config_file_path} ok=${sentOk}`);
283
- }));
289
+ const hw = tryResolveHardwareUuid();
290
+ if (hw) {
291
+ reportPromises.push(sendConfigFile({ file_type: fileType, file_path: inst.config_file_path, raw_content: updatedContent }, hw, authKey).then((sentOk) => {
292
+ hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${inst.config_file_path} ok=${sentOk}`);
293
+ }));
294
+ }
295
+ else {
296
+ hookRunLog(`autofix: skip upload uuid=${inst.uuid} (hardware UUID unavailable)`);
297
+ }
284
298
  }
285
299
  else {
286
300
  hookRunLog(`autofix: skip upload uuid=${inst.uuid} — remediation_instructions.json missing file_type (re-sync manifest)`);
@@ -294,8 +308,13 @@ export function applyAutofixViolations(violations) {
294
308
  const spec = remediationFixSpec(inst);
295
309
  if (spec?.restart_required && spec.restart_command) {
296
310
  if (!er.deferredSqlite) {
297
- hookRunLog(`autofix: restart required uuid=${inst.uuid} command=${spec.restart_command}`);
298
- restartCommands.push(spec.restart_command);
311
+ if (isTrustedRestartCommandForAutofix(spec.restart_command)) {
312
+ hookRunLog(`autofix: restart required uuid=${inst.uuid} command=${spec.restart_command}`);
313
+ restartCommands.push(spec.restart_command);
314
+ }
315
+ else {
316
+ hookRunLog(`autofix: restart command rejected (not an allowlisted template) uuid=${inst.uuid}`);
317
+ }
299
318
  }
300
319
  }
301
320
  if (!inst.is_enforced) {
@@ -314,14 +333,20 @@ export function applyAutofixViolations(violations) {
314
333
  // Send a post-autofix heartbeat so the server sees the updated (reduced) UUID set immediately,
315
334
  // without waiting for the background runner (which may be locked out).
316
335
  const remainingUuids = remaining.map((r) => r.uuid);
317
- reportPromises.push(fetchSync(loadEndpointBase(), resolveHardwareUuid(), remainingUuids)
318
- .then(() => hookRunLog(`autofix: post-autofix heartbeat sent uuids=${remainingUuids.length}`))
319
- .catch(() => undefined));
336
+ const hwHeartbeat = tryResolveHardwareUuid();
337
+ if (hwHeartbeat) {
338
+ reportPromises.push(fetchSync(loadEndpointBase(), hwHeartbeat, remainingUuids)
339
+ .then(() => hookRunLog(`autofix: post-autofix heartbeat sent uuids=${remainingUuids.length}`))
340
+ .catch(() => undefined));
341
+ }
342
+ else {
343
+ hookRunLog('autofix: skip post-autofix heartbeat (hardware UUID unavailable)');
344
+ }
320
345
  }
321
346
  if (fixed > 0) {
322
347
  hookRunLog(`autofix: total_applied=${fixed}`);
323
348
  }
324
- return { fixed, restartCommands, failedViolations, reportPromises, deferredSqlitePending };
349
+ return { fixed, appliedViolations, restartCommands, failedViolations, reportPromises, deferredSqlitePending };
325
350
  }
326
351
  /**
327
352
  * Remove satisfied one-time remediations from local remediation_instructions.json.
@@ -381,11 +406,16 @@ export function pruneSatisfiedOneTimeRemediations() {
381
406
  writeRemediationInstructionsFile({ remediations: remaining });
382
407
  hookRunLog(`remediation_prune: removed=${removed} remaining=${remaining.length}`);
383
408
  const remainingUuids = remaining.map((r) => r.uuid);
384
- const reportPromises = [
385
- fetchSync(loadEndpointBase(), resolveHardwareUuid(), remainingUuids)
409
+ const hw = tryResolveHardwareUuid();
410
+ const reportPromises = [];
411
+ if (hw) {
412
+ reportPromises.push(fetchSync(loadEndpointBase(), hw, remainingUuids)
386
413
  .then(() => hookRunLog(`remediation_prune: post-prune heartbeat sent uuids=${remainingUuids.length}`))
387
- .catch(() => undefined),
388
- ];
414
+ .catch(() => undefined));
415
+ }
416
+ else {
417
+ hookRunLog('remediation_prune: skip post-prune heartbeat (hardware UUID unavailable)');
418
+ }
389
419
  return { removed, reportPromises };
390
420
  }
391
421
  /**
@@ -24,4 +24,13 @@ function resolveHardwareUuid() {
24
24
  }
25
25
  throw new Error('Unable to determine hardware UUID via ioreg or system_profiler.');
26
26
  }
27
+ /** Same as {@link resolveHardwareUuid} but returns null when the host exposes no stable ID (e.g. Linux CI). */
28
+ export function tryResolveHardwareUuid() {
29
+ try {
30
+ return resolveHardwareUuid();
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
27
36
  export { resolveHardwareUuid };
@@ -7,7 +7,7 @@ import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContr
7
7
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
8
8
  import { createSignature } from '../sender/signing.js';
9
9
  import { loadEndpointBase } from '../sender/endpoint_config.js';
10
- import { resolveHardwareUuid } from './hardware_uuid.js';
10
+ import { tryResolveHardwareUuid } from './hardware_uuid.js';
11
11
  import { 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';
@@ -319,6 +319,42 @@ function assertSqlite3Available() {
319
319
  return false;
320
320
  }
321
321
  }
322
+ /**
323
+ * Unquoted SQLite identifiers must be [A-Za-z_][A-Za-z0-9_]* so values read from
324
+ * deferred_vscdb_apply.json cannot inject SQL via bogus table/column names.
325
+ */
326
+ function isSafeSqliteIdentifier(ident) {
327
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(ident);
328
+ }
329
+ function assertSafeSqliteIdentifiersForItemTable(table, keyColumn, valueColumn) {
330
+ if (isSafeSqliteIdentifier(table) && isSafeSqliteIdentifier(keyColumn) && isSafeSqliteIdentifier(valueColumn)) {
331
+ return true;
332
+ }
333
+ hookRunLog(`sqlite_update: rejected unsafe SQL identifier(s) table=${table} key_column=${keyColumn} value_column=${valueColumn}`);
334
+ complianceRunnerDiag('sqlite_update: rejected unsafe SQL identifier(s)');
335
+ return false;
336
+ }
337
+ /**
338
+ * Autofix restart_command allowlist: manifest strings are attacker-controlled if JSON is tampered.
339
+ * Deferred vscdb path always uses buildDeferredCursorRestartCommand(); this guards non-deferred restarts.
340
+ */
341
+ export function isTrustedRestartCommandForAutofix(cmd) {
342
+ const t = cmd.trim();
343
+ if (!t)
344
+ return false;
345
+ const deferredPrefix = 'REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export REPO_ROOT CURSOR_PROJECT="$REPO_ROOT" && ';
346
+ const legacyCursorPrefix = 'CURSOR_PROJECT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export CURSOR_PROJECT && ';
347
+ const legacyCursorSnippet = 'nohup bash -c \'sleep 2 && open -a Cursor "$CURSOR_PROJECT"\'';
348
+ const deferred = t.startsWith(deferredPrefix) &&
349
+ (t.includes('apply_deferred_vscdb') || t.includes('apply-deferred-vscdb')) &&
350
+ t.includes('killall -9 Cursor') &&
351
+ t.includes('open -a Cursor');
352
+ const legacyCursor = t.startsWith(legacyCursorPrefix) &&
353
+ t.includes(legacyCursorSnippet) &&
354
+ t.includes('killall -9 Cursor');
355
+ const claude = t.startsWith("nohup bash -c 'sleep 2 && open -a Claude'") && t.includes("pkill -x 'Claude'");
356
+ return deferred || legacyCursor || claude;
357
+ }
322
358
  /** Legacy Cursor: dedicated ItemTable row `composerState`. Current Cursor: nested under reactive `applicationUser` blob. */
323
359
  function cursorVscdbHasUsableComposerStateRow(dbPath, sqliteOp) {
324
360
  const raw = sqliteSelectValueCell(dbPath, sqliteOp.table, sqliteOp.key_column, sqliteOp.value_column, 'composerState').trim();
@@ -374,6 +410,25 @@ function resolveCursorComposerSqliteOp(dbPath, sqliteOp) {
374
410
  return sqliteOp;
375
411
  }
376
412
  /** Apply sqlite merge: dot-path, or array match where `json_path` is `…container.arrayKey` (e.g. `modes4` or `composerState.modes4`). */
413
+ /** ItemTable keys that store a JSON primitive; map to one field for merge + serialize. */
414
+ const CURSOR_SCALAR_ITEMTABLE_FIELDS = {
415
+ 'cursor/thirdPartyExtensibilityEnabled': 'thirdPartyExtensibilityEnabled',
416
+ 'cursorai/donotchange/privacyMode': 'privacyMode',
417
+ 'cursor/autoOpenLocalhostUrls': 'autoOpenLocalhostUrls',
418
+ };
419
+ function coerceScalarForItemTableField(parsed) {
420
+ if (typeof parsed === 'boolean')
421
+ return parsed;
422
+ if (typeof parsed === 'string') {
423
+ const lower = parsed.trim().toLowerCase();
424
+ if (lower === 'true' || lower === '1' || lower === 'yes')
425
+ return true;
426
+ return false;
427
+ }
428
+ if (typeof parsed === 'number' && !Number.isNaN(parsed))
429
+ return parsed !== 0;
430
+ return undefined;
431
+ }
377
432
  /**
378
433
  * ItemTable values are sometimes bare JSON booleans for toggle keys; sqlite merge expects an object root.
379
434
  * Maps known scalar roots to the policy-shaped object before applying updates.
@@ -382,44 +437,11 @@ function coerceItemTableValueToObjectRoot(targetKey, parsed) {
382
437
  if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
383
438
  return parsed;
384
439
  }
385
- if (targetKey === 'cursor/thirdPartyExtensibilityEnabled') {
386
- if (typeof parsed === 'boolean') {
387
- return { thirdPartyExtensibilityEnabled: parsed };
388
- }
389
- if (typeof parsed === 'string') {
390
- const lower = parsed.trim().toLowerCase();
391
- const b = lower === 'true' || lower === '1' || lower === 'yes';
392
- return { thirdPartyExtensibilityEnabled: b };
393
- }
394
- if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
395
- return { thirdPartyExtensibilityEnabled: parsed !== 0 };
396
- }
397
- }
398
- if (targetKey === 'cursorai/donotchange/privacyMode') {
399
- if (typeof parsed === 'boolean') {
400
- return { privacyMode: parsed };
401
- }
402
- if (typeof parsed === 'string') {
403
- const lower = parsed.trim().toLowerCase();
404
- const b = lower === 'true' || lower === '1' || lower === 'yes';
405
- return { privacyMode: b };
406
- }
407
- if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
408
- return { privacyMode: parsed !== 0 };
409
- }
410
- }
411
- if (targetKey === 'cursor/autoOpenLocalhostUrls') {
412
- if (typeof parsed === 'boolean') {
413
- return { autoOpenLocalhostUrls: parsed };
414
- }
415
- if (typeof parsed === 'string') {
416
- const lower = parsed.trim().toLowerCase();
417
- const b = lower === 'true' || lower === '1' || lower === 'yes';
418
- return { autoOpenLocalhostUrls: b };
419
- }
420
- if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
421
- return { autoOpenLocalhostUrls: parsed !== 0 };
422
- }
440
+ const field = CURSOR_SCALAR_ITEMTABLE_FIELDS[targetKey];
441
+ if (field) {
442
+ const b = coerceScalarForItemTableField(parsed);
443
+ if (b !== undefined)
444
+ return { [field]: b };
423
445
  }
424
446
  return {};
425
447
  }
@@ -428,30 +450,11 @@ function coerceItemTableValueToObjectRoot(targetKey, parsed) {
428
450
  * object can be ignored or overwritten on launch; match the native primitive shape when disabling.
429
451
  */
430
452
  function serializeItemTableValueForWrite(targetKey, root) {
431
- if (targetKey === 'cursor/thirdPartyExtensibilityEnabled') {
453
+ const field = CURSOR_SCALAR_ITEMTABLE_FIELDS[targetKey];
454
+ if (field) {
432
455
  const keys = Object.keys(root);
433
- if (keys.length === 1 && keys[0] === 'thirdPartyExtensibilityEnabled') {
434
- const v = root.thirdPartyExtensibilityEnabled;
435
- if (typeof v === 'boolean')
436
- return v ? 'true' : 'false';
437
- if (typeof v === 'number' && !Number.isNaN(v))
438
- return v !== 0 ? 'true' : 'false';
439
- }
440
- }
441
- if (targetKey === 'cursorai/donotchange/privacyMode') {
442
- const keys = Object.keys(root);
443
- if (keys.length === 1 && keys[0] === 'privacyMode') {
444
- const v = root.privacyMode;
445
- if (typeof v === 'boolean')
446
- return v ? 'true' : 'false';
447
- if (typeof v === 'number' && !Number.isNaN(v))
448
- return v !== 0 ? 'true' : 'false';
449
- }
450
- }
451
- if (targetKey === 'cursor/autoOpenLocalhostUrls') {
452
- const keys = Object.keys(root);
453
- if (keys.length === 1 && keys[0] === 'autoOpenLocalhostUrls') {
454
- const v = root.autoOpenLocalhostUrls;
456
+ if (keys.length === 1 && keys[0] === field) {
457
+ const v = root[field];
455
458
  if (typeof v === 'boolean')
456
459
  return v ? 'true' : 'false';
457
460
  if (typeof v === 'number' && !Number.isNaN(v))
@@ -580,6 +583,9 @@ function mergeJsonAtSqlitePath(currentJson, json_path, updates) {
580
583
  }
581
584
  }
582
585
  function sqliteSelectValueCell(dbPath, table, key_column, value_column, target_key) {
586
+ if (!assertSafeSqliteIdentifiersForItemTable(table, key_column, value_column)) {
587
+ return '';
588
+ }
583
589
  const safeName = target_key.replace(/'/g, "''");
584
590
  const script = `.timeout 60000\nSELECT ${value_column} FROM ${table} WHERE ${key_column}='${safeName}';\n`;
585
591
  return execFileSync('sqlite3', ['-noheader', dbPath], {
@@ -663,6 +669,9 @@ export async function applyDeferredVscdbFromDisk() {
663
669
  hookRunLog(`deferred_vscdb: database missing ${it.dbPath}`);
664
670
  return false;
665
671
  }
672
+ if (!assertSafeSqliteIdentifiersForItemTable(it.table, it.key_column, it.value_column)) {
673
+ return false;
674
+ }
666
675
  const safeJson = it.new_value_json.replace(/'/g, "''");
667
676
  const safeName = it.target_key.replace(/'/g, "''");
668
677
  const sql = `UPDATE ${it.table} SET ${it.value_column}='${safeJson}' WHERE ${it.key_column}='${safeName}';`;
@@ -691,7 +700,12 @@ export async function applyDeferredVscdbFromDisk() {
691
700
  hookRunLog(`deferred_vscdb: post-apply read failed path=${u.file_path}`);
692
701
  continue;
693
702
  }
694
- const sent = await sendConfigFile({ file_type: u.file_type, file_path: u.file_path, raw_content: rawContent }, resolveHardwareUuid(), authKey);
703
+ const hw = tryResolveHardwareUuid();
704
+ if (!hw) {
705
+ hookRunLog(`deferred_vscdb: skip post-apply upload (hardware UUID unavailable) path=${u.file_path}`);
706
+ continue;
707
+ }
708
+ const sent = await sendConfigFile({ file_type: u.file_type, file_path: u.file_path, raw_content: rawContent }, hw, authKey);
695
709
  hookRunLog(`deferred_vscdb: post-apply upload path=${u.file_path} ok=${sent}`);
696
710
  }
697
711
  unlinkSync(path);
@@ -702,7 +716,13 @@ export async function applyDeferredVscdbFromDisk() {
702
716
  return false;
703
717
  }
704
718
  }
705
- /** macOS Cursor: SIGKILL, apply queued vscdb writes, reopen project. */
719
+ /**
720
+ * macOS Cursor: SIGKILL, apply queued vscdb writes, reopen project.
721
+ *
722
+ * When `restart_required` implies deferred state.vscdb, `applyAutofixViolations` replaces any
723
+ * `restart_command` from remediation specs with this string (see compliance_check.ts). Spec JSON
724
+ * still documents a simpler Cursor reopen for non-code readers; that template is not executed on the deferred path.
725
+ */
706
726
  export function buildDeferredCursorRestartCommand() {
707
727
  // Prefer monorepo path when hooks run from optimus-secure-fdn; otherwise `npx -p log-llm-config apply-deferred-vscdb`
708
728
  // (package bin) so published installs work without a local npx_packages copy.
@@ -798,6 +818,9 @@ function applyOrQueueSqliteJsonUpdate(configPath, sqliteOp, deferred) {
798
818
  return false;
799
819
  sqliteOp = resolveCursorComposerSqliteOp(dbPath, sqliteOp);
800
820
  const { table, key_column, value_column, target_key } = sqliteOp;
821
+ if (!assertSafeSqliteIdentifiersForItemTable(table, key_column, value_column)) {
822
+ return false;
823
+ }
801
824
  const safeName = target_key.replace(/'/g, "''");
802
825
  let currentJson = {};
803
826
  try {
@@ -932,7 +955,11 @@ export function reportAutofixApplied(remediationUuid, result) {
932
955
  hookRunLog(`autofix_report: no auth key available, skipping report for uuid=${remediationUuid}`);
933
956
  return Promise.resolve();
934
957
  }
935
- const hardwareUuid = resolveHardwareUuid();
958
+ const hardwareUuid = tryResolveHardwareUuid();
959
+ if (!hardwareUuid) {
960
+ hookRunLog(`autofix_report: hardware UUID unavailable, skipping report for uuid=${remediationUuid}`);
961
+ return Promise.resolve();
962
+ }
936
963
  const endpointBase = loadEndpointBase();
937
964
  const url = `${resolveOrigin(endpointBase)}/endpoint_security/api/autofix-applied/`;
938
965
  const payload = { hardware_uuid: hardwareUuid, remediation_uuid: remediationUuid, result };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "dev": "ts-node src/cli.ts",
16
16
  "lint": "eslint \"src/**/*.ts\"",
17
17
  "prepare": "npm run build",
18
+ "pretest": "npm run build",
18
19
  "test": "vitest run",
19
20
  "test:watch": "vitest"
20
21
  },
@@ -44,11 +45,11 @@
44
45
  },
45
46
  "devDependencies": {
46
47
  "@types/node": "^24.10.1",
47
- "@vitest/coverage-v8": "^4.1.1",
48
- "@vitest/ui": "^4.0.15",
48
+ "@vitest/coverage-v8": "^3.2.4",
49
+ "@vitest/ui": "^3.2.4",
49
50
  "ts-node": "^10.9.2",
50
51
  "typescript": "^5.4.5",
51
- "vitest": "^4.0.15"
52
+ "vitest": "^3.2.4"
52
53
  },
53
54
  "dependencies": {
54
55
  "axios": "^1.13.5",