log-llm-config 1.3.5 → 1.3.7

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 { pathToFileURL } from 'node:url';
11
11
  import { resolve } from 'node:path';
12
12
  import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
13
- import { hookLogSessionBanner } from './log_config_files/runtime/hook_logger.js';
13
+ import { hookLogSessionBanner, hookRunLog } 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='));
@@ -111,7 +111,18 @@ export async function runCompliancePromptGate() {
111
111
  }
112
112
  const recheck = runLocalRemediationComplianceCheck();
113
113
  const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
114
- if (deferredSqlitePending || recheckOk) {
114
+ // Cursor: tolerate a failing recheck only when SQLite updates are deferred (apply after restart).
115
+ // Claude Code: JSON remediations are written immediately; merge/verify timing can still leave the
116
+ // in-process recheck red for the same UUID — allow in that case only for --ide=claude.
117
+ const appliedUuids = new Set(appliedViolations.map((v) => v.uuid));
118
+ const claudeRecheckStaleAfterImmediateApply = ide === 'claude' &&
119
+ !recheckOk &&
120
+ recheck.violations.length > 0 &&
121
+ recheck.violations.every((v) => appliedUuids.has(v.uuid));
122
+ if (claudeRecheckStaleAfterImmediateApply) {
123
+ hookRunLog('compliance_prompt_gate: Claude — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)');
124
+ }
125
+ if (deferredSqlitePending || recheckOk || claudeRecheckStaleAfterImmediateApply) {
115
126
  const autofixMessage = `Optimus Security auto-fixed ${fixed} policy violation(s):\n${appliedViolations.map((v) => autofixDialogLine(v)).join('\n')}`;
116
127
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
117
128
  if (restartCommands.length > 0)
@@ -1,6 +1,15 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { execSync } from 'node:child_process';
3
3
  import { readFileCollectionVscdbContract, writeFileCollectionVscdbContract, } from '../runtime/management_storage.js';
4
+ /**
5
+ * ItemTable keys that store a bare JSON boolean; compliance paths use `${key}.${field}`.
6
+ * Kept in sync with `coerceItemTableValueToObjectRoot` / `serializeItemTableValueForWrite` in remediation_sync.
7
+ */
8
+ export const CURSOR_SCALAR_ITEMTABLE_FIELDS = {
9
+ 'cursor/thirdPartyExtensibilityEnabled': 'thirdPartyExtensibilityEnabled',
10
+ 'cursorai/donotchange/privacyMode': 'privacyMode',
11
+ 'cursor/autoOpenLocalhostUrls': 'autoOpenLocalhostUrls',
12
+ };
4
13
  function querySqlite(dbPath, key) {
5
14
  const safe = key.replace(/'/g, "''");
6
15
  return execSync(`sqlite3 "${dbPath}" "SELECT value FROM ItemTable WHERE key='${safe}'"`, {
@@ -90,8 +99,14 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
90
99
  if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
91
100
  return { [itemKey]: parsed };
92
101
  }
93
- // Bare JSON primitives (e.g. cursor/thirdPartyExtensibilityEnabled stores `true`/`false`).
102
+ // Bare JSON primitives. Scalar toggles must be wrapped so getByPath(key.field) works in compliance checks.
94
103
  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) {
107
+ return { [itemKey]: { [field]: parsed } };
108
+ }
109
+ }
95
110
  return { [itemKey]: parsed };
96
111
  }
97
112
  return { [itemKey]: {} };
@@ -5,12 +5,15 @@
5
5
  * ComplianceStatus; the prompt gate and tests use that return value. Autofix may write config files
6
6
  * and remediation_instructions.json via applyAutofixViolations / enforceRemediation.
7
7
  *
8
- * Prompt gate: compliance_prompt_gate runs runLocalRemediationComplianceCheck (local files only).
9
- * Background: compliance_check_runner runs syncRemediations (network) then the same local check.
8
+ * Prompt gate: compliance_prompt_gate runs runLocalRemediationComplianceCheck then applyAutofixViolations.
9
+ * Background: compliance_check_runner runs syncRemediations (network), the same local check, then
10
+ * applyAutofixViolations + executeTrustedRestartCommands so remediations apply even when only the
11
+ * background path runs (sqlite/vscdb updates remain Cursor-specific; restarts use the shared allowlist).
10
12
  */
11
- import { existsSync, readFileSync } from 'node:fs';
13
+ import { existsSync, readFileSync, statSync } from 'node:fs';
12
14
  import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
13
- import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
15
+ import { getRemediationInstructionsPath, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
16
+ import { executeTrustedRestartCommands } from './trusted_restarts.js';
14
17
  import { complianceRunnerDiag, hookRunLog } from './hook_logger.js';
15
18
  import { loadEndpointBase } from '../sender/endpoint_config.js';
16
19
  import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
@@ -20,6 +23,14 @@ import { readStoredAuthKey } from '../auth/auth_key_store.js';
20
23
  // ---------------------------------------------------------------------------
21
24
  // Helpers
22
25
  // ---------------------------------------------------------------------------
26
+ /**
27
+ * ItemTable keys use slashes (e.g. cursorai/donotchange/privacyMode) and do not contain dots.
28
+ * Compliance setting_path is `${itemKey}.${nested.path}` — take the segment before the first `.`.
29
+ */
30
+ export function itemTableKeyFromSettingPath(settingPath) {
31
+ const i = settingPath.indexOf('.');
32
+ return i === -1 ? settingPath : settingPath.slice(0, i);
33
+ }
23
34
  /** Traverse a JSON object using dot-notation path. Returns undefined if any segment is missing. */
24
35
  export function getByPath(obj, path) {
25
36
  const parts = path.split('.');
@@ -85,20 +96,25 @@ function verifyOpsApplied(configJson, settingPath, ops) {
85
96
  }
86
97
  return { ok: true, expected: null };
87
98
  }
88
- /** Plain JSON file or virtual `…/state.vscdb#composerState` path for ItemTable-backed settings. */
89
- function loadRemediationConfigJson(configFilePath) {
99
+ /** Plain JSON file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
100
+ function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
90
101
  const hashIdx = configFilePath.indexOf('#');
91
102
  if (hashIdx >= 0) {
92
103
  const dbPath = configFilePath.slice(0, hashIdx);
93
- const itemKey = configFilePath.slice(hashIdx + 1).trim();
94
- if (!itemKey)
104
+ const itemKeyFromPath = configFilePath.slice(hashIdx + 1).trim();
105
+ if (!itemKeyFromPath)
95
106
  return { ok: false, reason: 'empty_vscdb_key' };
96
107
  if (!existsSync(dbPath))
97
108
  return { ok: false, reason: 'db_not_found' };
98
- const wrapped = readVscdbItemTableJson(dbPath, itemKey);
99
- if (wrapped === null)
100
- return { ok: false, reason: 'vscdb_read_failed' };
101
- return { ok: true, json: wrapped };
109
+ const keys = new Set([itemKeyFromPath, ...checkSettingPaths.map(itemTableKeyFromSettingPath)].filter(Boolean));
110
+ const merged = {};
111
+ for (const k of keys) {
112
+ const wrapped = readVscdbItemTableJson(dbPath, k);
113
+ if (wrapped === null)
114
+ return { ok: false, reason: 'vscdb_read_failed' };
115
+ Object.assign(merged, wrapped);
116
+ }
117
+ return { ok: true, json: merged };
102
118
  }
103
119
  if (!existsSync(configFilePath))
104
120
  return { ok: false, reason: 'file_not_found' };
@@ -137,7 +153,7 @@ export function runLocalRemediationComplianceCheck() {
137
153
  const checks = compliance.checks ?? [];
138
154
  if (checks.length === 0)
139
155
  continue;
140
- const loaded = loadRemediationConfigJson(entry.config_file_path);
156
+ const loaded = loadRemediationConfigJson(entry.config_file_path, checks.map((c) => c.setting_path));
141
157
  if (!loaded.ok) {
142
158
  const msg = loaded.reason === 'file_not_found'
143
159
  ? `compliance_check: config file not found, skipping uuid=${entry.uuid}`
@@ -374,7 +390,7 @@ export function pruneSatisfiedOneTimeRemediations() {
374
390
  remaining.push(raw);
375
391
  continue;
376
392
  }
377
- const prLoaded = loadRemediationConfigJson(inst.config_file_path);
393
+ const prLoaded = loadRemediationConfigJson(inst.config_file_path, checks.map((c) => c.setting_path));
378
394
  if (!prLoaded.ok) {
379
395
  remaining.push(raw);
380
396
  continue;
@@ -418,9 +434,22 @@ export function pruneSatisfiedOneTimeRemediations() {
418
434
  }
419
435
  return { removed, reportPromises };
420
436
  }
437
+ /** Same staleness window as compliance_prompt_gate: avoid applying very old local manifests. */
438
+ const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
439
+ function getManifestStalenessMs() {
440
+ try {
441
+ const p = getRemediationInstructionsPath();
442
+ if (!existsSync(p))
443
+ return null;
444
+ return Date.now() - statSync(p).mtimeMs;
445
+ }
446
+ catch {
447
+ return null;
448
+ }
449
+ }
421
450
  /**
422
- * Background refresh: server sync for latest instructions, then the same local evaluation as the hook.
423
- * Does not persist compliance state to disk.
451
+ * Background refresh: server sync, local evaluation, then autofix + trusted restarts (same enforcement
452
+ * as the prompt gate, without blocking stdin/stdout for the IDE).
424
453
  */
425
454
  export async function runComplianceCheck() {
426
455
  try {
@@ -429,5 +458,37 @@ export async function runComplianceCheck() {
429
458
  catch (err) {
430
459
  hookRunLog(`compliance_check: remediation_sync unexpected error: ${err instanceof Error ? err.message : String(err)}`);
431
460
  }
432
- runLocalRemediationComplianceCheck();
461
+ const status = runLocalRemediationComplianceCheck();
462
+ if (status.status === 'ok' || status.violations.length === 0) {
463
+ const pruned = pruneSatisfiedOneTimeRemediations();
464
+ if (pruned.removed > 0) {
465
+ await Promise.allSettled(pruned.reportPromises);
466
+ }
467
+ return;
468
+ }
469
+ const staleMs = getManifestStalenessMs();
470
+ if (staleMs !== null && staleMs > MANIFEST_STALE_MS) {
471
+ const staleDays = Math.floor(staleMs / (24 * 60 * 60 * 1000));
472
+ hookRunLog(`compliance_check_runner: skip autofix — local remediation manifest is stale (${staleDays} days old)`);
473
+ return;
474
+ }
475
+ const { fixed, restartCommands, failedViolations, reportPromises } = applyAutofixViolations(status.violations);
476
+ if (fixed === 0) {
477
+ if (failedViolations.length > 0) {
478
+ hookRunLog(`compliance_check_runner: autofix failed for ${failedViolations.length} violation(s) (see autofix logs above)`);
479
+ }
480
+ return;
481
+ }
482
+ await Promise.allSettled(reportPromises);
483
+ if (failedViolations.length > 0) {
484
+ hookRunLog(`compliance_check_runner: autofix partial failure — ${failedViolations.length} violation(s) still unresolved`);
485
+ }
486
+ if (restartCommands.length > 0) {
487
+ hookRunLog(`compliance_check_runner: executing ${restartCommands.length} trusted restart command(s)`);
488
+ executeTrustedRestartCommands(restartCommands);
489
+ }
490
+ const pruned = pruneSatisfiedOneTimeRemediations();
491
+ if (pruned.removed > 0) {
492
+ await Promise.allSettled(pruned.reportPromises);
493
+ }
433
494
  }
@@ -8,7 +8,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
10
  import { tryResolveHardwareUuid } from './hardware_uuid.js';
11
- import { persistVscdbComposerContractFromPatternsResponse, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
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
14
  function reactiveStorageItemKeyFromContract() {
@@ -410,12 +410,6 @@ function resolveCursorComposerSqliteOp(dbPath, sqliteOp) {
410
410
  return sqliteOp;
411
411
  }
412
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
413
  function coerceScalarForItemTableField(parsed) {
420
414
  if (typeof parsed === 'boolean')
421
415
  return parsed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {