log-llm-config 1.2.7 → 1.3.0

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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Runs after Cursor SIGKILL: applies queued state.vscdb patches from
3
+ * ~/opt-ai-sec/management/optimus_deferred_vscdb_apply.json (see remediation_sync).
4
+ */
5
+ import { applyDeferredVscdbFromDisk } from './log_config_files/runtime/remediation_sync.js';
6
+ applyDeferredVscdbFromDisk()
7
+ .then((ok) => process.exit(ok ? 0 : 1))
8
+ .catch(() => process.exit(1));
@@ -1,3 +1,9 @@
1
+ import { readFileCollectionVscdbContract } from '../log_config_files/runtime/management_storage.js';
2
+ /** Reactive ItemTable key from backend-derived cache (written on log-config); same path as remediations. */
3
+ function readReactiveStorageItemKeyFromDisk() {
4
+ const k = readFileCollectionVscdbContract()?.reactive_storage_item_key;
5
+ return typeof k === 'string' && k.trim() !== '' ? k.trim() : null;
6
+ }
1
7
  const fileCategories = [
2
8
  { label: 'Cursor: mcp.json', targets: ['./.cursor/mcp.json', '$HOME/.cursor/mcp.json'] },
3
9
  { label: 'Claude: .mcp.json', targets: ['./mcp.json', './.mcp.json', '/Library/Application Support/ClaudeCode/managed-mcp.json'] },
@@ -5,15 +11,20 @@ const fileCategories = [
5
11
  { label: 'Cursor: hooks.json', targets: ['./.cursor/hooks.json', '$HOME/.cursor/hooks.json', '/Library/Application Support/Cursor/hooks.json'] },
6
12
  { label: 'Cursor: User/settings.json (user-level)', targets: ['$HOME/Library/Application Support/Cursor/User/settings.json'] },
7
13
  ];
8
- const sqliteCategories = [
9
- {
10
- label: 'Cursor: state.vscdb (composerState)',
11
- dbPath: '$HOME/Library/Application Support/Cursor/User/globalStorage/state.vscdb',
12
- table: 'ItemTable',
13
- key: 'src.vs.platform.reactivestorage.browser.reactiveStorageServiceImpl.persistentStorage.applicationUser',
14
- jsonPaths: [['composerState'], []],
15
- },
16
- ];
14
+ function buildSqliteCategories() {
15
+ const key = readReactiveStorageItemKeyFromDisk();
16
+ if (!key)
17
+ return [];
18
+ return [
19
+ {
20
+ label: 'Cursor: state.vscdb (reactive blob / composerState)',
21
+ dbPath: '$HOME/Library/Application Support/Cursor/User/globalStorage/state.vscdb',
22
+ table: 'ItemTable',
23
+ key,
24
+ jsonPaths: [['composerState'], []],
25
+ },
26
+ ];
27
+ }
17
28
  function buildFileCategoryLines(category) {
18
29
  return [
19
30
  `echo "===== ${category.label} ====="`,
@@ -38,12 +49,13 @@ function buildFileCategoryLines(category) {
38
49
  }
39
50
  function buildSqliteCategoryLines(category) {
40
51
  const jsonPathsLiteral = JSON.stringify(category.jsonPaths);
52
+ const safeSqlKey = category.key.replace(/'/g, "''");
41
53
  return [
42
54
  `echo "===== ${category.label} ====="`, `db_path="${category.dbPath}"`,
43
55
  'expanded=$(eval echo "$db_path")',
44
56
  'if [ -f "$expanded" ]; then', ' if command -v sqlite3 >/dev/null 2>&1; then',
45
57
  ' echo "===== $expanded ====="',
46
- ` query_result=$(sqlite3 "$expanded" "SELECT value FROM ${category.table} WHERE key='${category.key}'")`,
58
+ ` query_result=$(sqlite3 "$expanded" "SELECT value FROM ${category.table} WHERE key='${safeSqlKey}'")`,
47
59
  ' if [ -n "$query_result" ]; then',
48
60
  ` echo "===== key: ${category.key} ====="`,
49
61
  ` LOG_CONFIG_JSON_VALUE="$query_result" python3 - <<'PY'`,
@@ -76,7 +88,7 @@ function renderBashScript() {
76
88
  ' local suffix="${path#"$repo_root"}"', ' if [ -z "$suffix" ]; then', ' echo "<project_root>"',
77
89
  ' else', ' echo "<project_root>${suffix}"', ' fi', ' else', ' echo "$path"', ' fi', '}', '',
78
90
  ...fileCategories.flatMap(buildFileCategoryLines),
79
- ...sqliteCategories.flatMap(buildSqliteCategoryLines),
91
+ ...buildSqliteCategories().flatMap(buildSqliteCategoryLines),
80
92
  ];
81
93
  return scriptLines.join('\n');
82
94
  }
@@ -1,9 +1,40 @@
1
+ import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { OPT_AI_SEC_MANAGEMENT_REL } from './bootstrap_constants.js';
5
+ import { hookLogSessionBanner } from './log_config_files/runtime/hook_logger.js';
1
6
  import { runComplianceCheck } from './log_config_files/runtime/compliance_check.js';
7
+ /** Append-only log for compliance runner lifecycle; hook_log.txt is also append-only (session banners). */
8
+ function runnerFileLog(message) {
9
+ try {
10
+ const dir = join(homedir(), OPT_AI_SEC_MANAGEMENT_REL);
11
+ if (!existsSync(dir))
12
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
13
+ const path = join(dir, 'compliance_runner.log');
14
+ appendFileSync(path, `${new Date().toISOString()} ${message}\n`, 'utf8');
15
+ }
16
+ catch {
17
+ /* ignore */
18
+ }
19
+ }
2
20
  (async () => {
21
+ hookLogSessionBanner('compliance_check_runner (background sync + check)');
22
+ runnerFileLog('compliance_check_runner: start');
3
23
  try {
4
24
  await runComplianceCheck();
25
+ runnerFileLog('compliance_check_runner: finished ok');
5
26
  }
6
27
  catch (err) {
28
+ const detail = err instanceof Error ? err.stack ?? err.message : String(err);
29
+ runnerFileLog('compliance_check_runner: uncaught error');
30
+ try {
31
+ const dir = join(homedir(), OPT_AI_SEC_MANAGEMENT_REL);
32
+ const path = join(dir, 'compliance_runner.log');
33
+ appendFileSync(path, `${detail}\n\n`, 'utf8');
34
+ }
35
+ catch {
36
+ /* ignore */
37
+ }
7
38
  process.stderr.write(`compliance_check_runner error: ${err instanceof Error ? err.message : String(err)}\n`);
8
39
  process.exit(1);
9
40
  }
@@ -4,7 +4,9 @@
4
4
  */
5
5
  import { applyAutofixViolations, pruneSatisfiedOneTimeRemediations, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
6
6
  import { existsSync, statSync } from 'node:fs';
7
+ import { spawn } from 'node:child_process';
7
8
  import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
9
+ import { hookLogSessionBanner, hookRunLog } from './log_config_files/runtime/hook_logger.js';
8
10
  const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
9
11
  function parseIde() {
10
12
  const eq = process.argv.find((a) => a.startsWith('--ide='));
@@ -42,6 +44,13 @@ function blockPayload(ide, violationMessage) {
42
44
  }
43
45
  return JSON.stringify({ continue: false, user_message: text });
44
46
  }
47
+ function fireRestartCommands(commands) {
48
+ for (const cmd of commands) {
49
+ hookRunLog(`restart: firing command="${cmd}"`);
50
+ const child = spawn('sh', ['-c', cmd], { detached: true, stdio: 'ignore' });
51
+ child.unref();
52
+ }
53
+ }
45
54
  function getManifestStalenessMs() {
46
55
  try {
47
56
  const path = getRemediationInstructionsPath();
@@ -55,7 +64,20 @@ function getManifestStalenessMs() {
55
64
  }
56
65
  }
57
66
  const ide = parseIde();
67
+ /** Short line for success dialog: finding title/sentence from manifest, not per-check remediation technical text. */
68
+ function autofixDialogLine(v) {
69
+ const title = v.finding_title?.trim();
70
+ if (title)
71
+ return `• [${v.finding_formatted_id}] ${title}`;
72
+ const fd = v.finding_description?.trim();
73
+ if (fd)
74
+ return `• [${v.finding_formatted_id}] ${fd}`;
75
+ const d = v.description.trim();
76
+ const short = d.length > 160 ? `${d.slice(0, 157)}…` : d;
77
+ return `• [${v.finding_formatted_id}] ${short}`;
78
+ }
58
79
  async function run() {
80
+ hookLogSessionBanner('compliance_prompt_gate (before submit)');
59
81
  const status = runLocalRemediationComplianceCheck();
60
82
  if (status.status === 'fail' && status.violations.length > 0) {
61
83
  const staleMs = getManifestStalenessMs();
@@ -66,19 +88,28 @@ async function run() {
66
88
  printAllowWithAdvisory(ide, advisory);
67
89
  return;
68
90
  }
69
- const { fixed, restartCommands, failedViolations, reportPromises } = applyAutofixViolations(status.violations);
91
+ const { fixed, restartCommands, failedViolations, reportPromises, deferredSqlitePending } = applyAutofixViolations(status.violations);
70
92
  if (fixed > 0) {
71
93
  // Wait for all server reports before exiting so the POST lands.
72
94
  await Promise.allSettled(reportPromises);
73
95
  const recheck = runLocalRemediationComplianceCheck();
74
- if (recheck.status === 'ok' || recheck.violations.length === 0) {
96
+ const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
97
+ if (deferredSqlitePending || recheckOk) {
75
98
  const fixedViolations = status.violations.filter((v) => v.autofix_allowed);
76
- const lines = fixedViolations.map((v) => `• [${v.finding_formatted_id}] ${v.description}`);
77
- const autofixMessage = `Optimus Security auto-fixed ${fixed} policy violation(s):\n${lines.join('\n')}`;
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')}`;
78
106
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
79
107
  if (restartCommands.length > 0)
80
108
  payload.restart_commands = restartCommands;
81
109
  console.log(JSON.stringify(payload));
110
+ // Cursor: .cursor/hooks runs restart after the osascript dialog — avoid double SIGKILL here.
111
+ if (ide !== 'cursor')
112
+ fireRestartCommands(restartCommands);
82
113
  return;
83
114
  }
84
115
  const msg = recheck.violations[0]?.message ?? 'A security policy violation has been detected.';
@@ -81,8 +81,12 @@ function buildVscdbRawContentFromSpec(state, spec) {
81
81
  if (!Array.isArray(value) || value.length === 0)
82
82
  return null;
83
83
  }
84
- if (spec.value_constraint === 'boolean' && typeof value !== 'boolean')
85
- return null;
84
+ if (spec.value_constraint === 'boolean') {
85
+ const okBool = typeof value === 'boolean';
86
+ const okSqliteInt = typeof value === 'number' && (value === 0 || value === 1);
87
+ if (!okBool && !okSqliteInt)
88
+ return null;
89
+ }
86
90
  return buildRawContent(state, spec.state_key, value, spec.include_keys, new Date().toISOString());
87
91
  }
88
92
  /**
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { execSync } from 'node:child_process';
3
+ import { readFileCollectionVscdbContract, writeFileCollectionVscdbContract, } from '../runtime/management_storage.js';
3
4
  function querySqlite(dbPath, key) {
4
5
  const safe = key.replace(/'/g, "''");
5
6
  return execSync(`sqlite3 "${dbPath}" "SELECT value FROM ItemTable WHERE key='${safe}'"`, {
@@ -7,6 +8,98 @@ function querySqlite(dbPath, key) {
7
8
  stdio: ['ignore', 'pipe', 'pipe'],
8
9
  }).trim();
9
10
  }
11
+ /**
12
+ * Fallback if the API omits vscdb_composer_contract (older server): derive from vscdb_read_queries.
13
+ */
14
+ export function parseVscdbComposerContractFromReadQueries(queries) {
15
+ const empty = {
16
+ version: 1,
17
+ reactive_storage_item_key: undefined,
18
+ composer_shadow_keys: [],
19
+ };
20
+ if (!queries?.length)
21
+ return empty;
22
+ for (const step of queries) {
23
+ if (step.value_kind === 'composer_with_include' && step.state_key === 'composerState') {
24
+ const keys = step.item_table_keys ?? [];
25
+ return {
26
+ version: 1,
27
+ reactive_storage_item_key: keys[0],
28
+ composer_shadow_keys: [...(step.include_keys ?? [])],
29
+ };
30
+ }
31
+ }
32
+ return empty;
33
+ }
34
+ export function normalizeVscdbComposerContractFromPatternsResponse(resp) {
35
+ const c = resp.vscdb_composer_contract;
36
+ const hasKey = c != null &&
37
+ ((typeof c.reactive_storage_item_key === 'string' && c.reactive_storage_item_key.trim() !== '') ||
38
+ (c.composer_shadow_keys?.length ?? 0) > 0);
39
+ if (hasKey && c) {
40
+ return {
41
+ version: 1,
42
+ reactive_storage_item_key: c.reactive_storage_item_key ?? undefined,
43
+ composer_shadow_keys: [...(c.composer_shadow_keys ?? [])],
44
+ };
45
+ }
46
+ return parseVscdbComposerContractFromReadQueries(resp.vscdb_read_queries);
47
+ }
48
+ /** Persist backend-derived contract next to remediation_instructions.json. */
49
+ export function persistVscdbComposerContractFromPatternsResponse(resp) {
50
+ const norm = normalizeVscdbComposerContractFromPatternsResponse(resp);
51
+ if (norm.reactive_storage_item_key || norm.composer_shadow_keys.length > 0) {
52
+ writeFileCollectionVscdbContract(norm);
53
+ }
54
+ }
55
+ /**
56
+ * Read one ItemTable JSON blob (e.g. key `composerState`) and return `{ [itemKey]: object }` so
57
+ * dot-paths like `composerState.modes4.agent.autoRun` (or legacy numeric `modes4.0`) work in compliance checks. Empty / missing value
58
+ * yields `{ [itemKey]: {} }`. Returns null if the DB is missing, sqlite3 is unavailable, or JSON parse fails.
59
+ *
60
+ * When `file_collection_vscdb_contract.json` lists `reactive_storage_item_key`, a missing legacy `composerState` row
61
+ * falls back to nested `composerState` inside that reactive blob (paths from the backend API only).
62
+ */
63
+ export function readVscdbItemTableJson(dbPath, itemKey) {
64
+ try {
65
+ if (!dbPath || !existsSync(dbPath))
66
+ return null;
67
+ execSync('which sqlite3', { stdio: 'ignore' });
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ try {
73
+ const raw = querySqlite(dbPath, itemKey);
74
+ const contract = readFileCollectionVscdbContract();
75
+ const reactiveKey = typeof contract?.reactive_storage_item_key === 'string' ? contract.reactive_storage_item_key.trim() : '';
76
+ if (itemKey === 'composerState' && (!raw || raw === '{}') && reactiveKey) {
77
+ const reactive = querySqlite(dbPath, reactiveKey);
78
+ if (reactive) {
79
+ const root = JSON.parse(reactive);
80
+ const cs = root.composerState;
81
+ if (cs !== null && typeof cs === 'object' && !Array.isArray(cs)) {
82
+ return { composerState: cs };
83
+ }
84
+ }
85
+ }
86
+ if (!raw) {
87
+ return { [itemKey]: {} };
88
+ }
89
+ const parsed = JSON.parse(raw);
90
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
91
+ return { [itemKey]: parsed };
92
+ }
93
+ // Bare JSON primitives (e.g. cursor/thirdPartyExtensibilityEnabled stores `true`/`false`).
94
+ if (typeof parsed === 'boolean' || typeof parsed === 'number' || typeof parsed === 'string') {
95
+ return { [itemKey]: parsed };
96
+ }
97
+ return { [itemKey]: {} };
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
10
103
  function setNested(obj, dotPath, value) {
11
104
  const parts = dotPath.split('.');
12
105
  const safe = (k) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype';
@@ -42,9 +135,17 @@ function runOneStep(dbPath, stateData, step) {
42
135
  stateData.composerState = composerState;
43
136
  }
44
137
  if (step.include_keys?.length) {
138
+ const nested = composerState && typeof composerState === 'object'
139
+ ? composerState
140
+ : undefined;
45
141
  for (const k of step.include_keys) {
46
- if (k in obj && obj[k] !== undefined)
142
+ // Prefer the value inside composerState when present; blob root can be stale vs the UI.
143
+ if (nested && k in nested && nested[k] !== undefined) {
144
+ stateData[k] = nested[k];
145
+ }
146
+ else if (k in obj && obj[k] !== undefined) {
47
147
  stateData[k] = obj[k];
148
+ }
48
149
  }
49
150
  }
50
151
  return;
@@ -9,23 +9,32 @@
9
9
  * Background: compliance_check_runner runs syncRemediations (network) then the same local check.
10
10
  */
11
11
  import { existsSync, readFileSync } from 'node:fs';
12
+ import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
12
13
  import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
13
- import { hookRunLog } from './hook_logger.js';
14
+ import { complianceRunnerDiag, hookRunLog } from './hook_logger.js';
14
15
  import { loadEndpointBase } from '../sender/endpoint_config.js';
15
16
  import { resolveHardwareUuid } from './hardware_uuid.js';
16
- import { enforceRemediation, fetchSync, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
17
+ import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
17
18
  import { sendConfigFile } from '../sender/batch_sender.js';
18
19
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
19
20
  // ---------------------------------------------------------------------------
20
21
  // Helpers
21
22
  // ---------------------------------------------------------------------------
22
23
  /** Traverse a JSON object using dot-notation path. Returns undefined if any segment is missing. */
23
- function getByPath(obj, path) {
24
+ export function getByPath(obj, path) {
24
25
  const parts = path.split('.');
25
26
  let current = obj;
26
27
  for (const part of parts) {
27
28
  if (current == null || typeof current !== 'object')
28
29
  return undefined;
30
+ // Cursor composerState.modes4: non-numeric segment indexes by mode id (agent row is not always [0]).
31
+ if (Array.isArray(current) && !/^\d+$/.test(part)) {
32
+ const idx = current.findIndex((item) => item !== null &&
33
+ typeof item === 'object' &&
34
+ item.id === part);
35
+ current = idx >= 0 ? current[idx] : undefined;
36
+ continue;
37
+ }
29
38
  current = current[part];
30
39
  }
31
40
  return current;
@@ -76,6 +85,30 @@ function verifyOpsApplied(configJson, settingPath, ops) {
76
85
  }
77
86
  return { ok: true, expected: null };
78
87
  }
88
+ /** Plain JSON file or virtual `…/state.vscdb#composerState` path for ItemTable-backed settings. */
89
+ function loadRemediationConfigJson(configFilePath) {
90
+ const hashIdx = configFilePath.indexOf('#');
91
+ if (hashIdx >= 0) {
92
+ const dbPath = configFilePath.slice(0, hashIdx);
93
+ const itemKey = configFilePath.slice(hashIdx + 1).trim();
94
+ if (!itemKey)
95
+ return { ok: false, reason: 'empty_vscdb_key' };
96
+ if (!existsSync(dbPath))
97
+ 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 };
102
+ }
103
+ if (!existsSync(configFilePath))
104
+ return { ok: false, reason: 'file_not_found' };
105
+ try {
106
+ return { ok: true, json: JSON.parse(readFileSync(configFilePath, 'utf8')) };
107
+ }
108
+ catch {
109
+ return { ok: false, reason: 'parse_error' };
110
+ }
111
+ }
79
112
  // ---------------------------------------------------------------------------
80
113
  // Check runner — Section 6: real per-check evaluation
81
114
  // ---------------------------------------------------------------------------
@@ -104,18 +137,21 @@ export function runLocalRemediationComplianceCheck() {
104
137
  const checks = compliance.checks ?? [];
105
138
  if (checks.length === 0)
106
139
  continue;
107
- if (!existsSync(entry.config_file_path)) {
108
- hookRunLog(`compliance_check: config file not found, skipping uuid=${entry.uuid}`);
109
- continue;
110
- }
111
- let configJson;
112
- try {
113
- configJson = JSON.parse(readFileSync(entry.config_file_path, 'utf8'));
114
- }
115
- catch {
116
- hookRunLog(`compliance_check: could not parse config file, skipping uuid=${entry.uuid}`);
140
+ const loaded = loadRemediationConfigJson(entry.config_file_path);
141
+ if (!loaded.ok) {
142
+ const msg = loaded.reason === 'file_not_found'
143
+ ? `compliance_check: config file not found, skipping uuid=${entry.uuid}`
144
+ : loaded.reason === 'db_not_found'
145
+ ? `compliance_check: vscdb file not found, skipping uuid=${entry.uuid}`
146
+ : loaded.reason === 'vscdb_read_failed'
147
+ ? `compliance_check: could not read vscdb (sqlite3 missing or invalid JSON?), skipping uuid=${entry.uuid}`
148
+ : loaded.reason === 'empty_vscdb_key'
149
+ ? `compliance_check: invalid vscdb path (empty # key), skipping uuid=${entry.uuid}`
150
+ : `compliance_check: could not parse config file, skipping uuid=${entry.uuid}`;
151
+ hookRunLog(msg);
117
152
  continue;
118
153
  }
154
+ const configJson = loaded.json;
119
155
  for (const check of checks) {
120
156
  // Prefer ops-based verification (matches delta apply semantics; doesn't require full after snapshot).
121
157
  if (check.ops) {
@@ -127,6 +163,8 @@ export function runLocalRemediationComplianceCheck() {
127
163
  finding_formatted_id: compliance.finding_formatted_id,
128
164
  setting_path: check.setting_path,
129
165
  description: check.description,
166
+ finding_title: entry.finding_title,
167
+ finding_description: entry.finding_description,
130
168
  severity: compliance.severity,
131
169
  autofix_allowed: compliance.autofix_allowed,
132
170
  config_file_path: entry.config_file_path,
@@ -149,6 +187,8 @@ export function runLocalRemediationComplianceCheck() {
149
187
  finding_formatted_id: compliance.finding_formatted_id,
150
188
  setting_path: check.setting_path,
151
189
  description: check.description,
190
+ finding_title: entry.finding_title,
191
+ finding_description: entry.finding_description,
152
192
  severity: compliance.severity,
153
193
  autofix_allowed: compliance.autofix_allowed,
154
194
  config_file_path: entry.config_file_path,
@@ -190,6 +230,7 @@ export function applyAutofixViolations(violations) {
190
230
  const failedViolations = [];
191
231
  const reportPromises = [];
192
232
  const oneTimeAppliedUuids = new Set();
233
+ let deferredSqlitePending = false;
193
234
  for (const violation of autofixable) {
194
235
  if (seen.has(violation.uuid))
195
236
  continue;
@@ -200,20 +241,39 @@ export function applyAutofixViolations(violations) {
200
241
  continue;
201
242
  }
202
243
  const inst = instruction;
203
- const ok = enforceRemediation(inst);
204
- if (ok) {
205
- seen.add(violation.uuid);
206
- fixed++;
207
- hookRunLog(`autofix: applied uuid=${inst.uuid} path=${inst.config_file_path}`);
208
- reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
209
- const authKey = readStoredAuthKey();
210
- if (authKey) {
244
+ complianceRunnerDiag(`autofix: calling enforceRemediation uuid=${inst.uuid} path=${inst.config_file_path}`);
245
+ const er = enforceRemediation(inst);
246
+ if (!er.ok) {
247
+ failedViolations.push(violation);
248
+ continue;
249
+ }
250
+ if (er.deferredSqlite)
251
+ deferredSqlitePending = true;
252
+ seen.add(violation.uuid);
253
+ fixed++;
254
+ hookRunLog(`autofix: applied uuid=${inst.uuid} path=${inst.config_file_path}`);
255
+ reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
256
+ const authKey = readStoredAuthKey();
257
+ if (authKey) {
258
+ if (er.deferredSqlite && inst.config_file_path.includes('#')) {
259
+ hookRunLog(`autofix: skip immediate vscdb upload (deferred until after restart) uuid=${inst.uuid}`);
260
+ }
261
+ else {
211
262
  let updatedContent;
212
- try {
213
- updatedContent = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
263
+ if (inst.config_file_path.includes('#')) {
264
+ const hi = inst.config_file_path.indexOf('#');
265
+ const dbPath = inst.config_file_path.slice(0, hi);
266
+ const itemKey = inst.config_file_path.slice(hi + 1).trim();
267
+ updatedContent =
268
+ itemKey ? (readVscdbItemTableJson(dbPath, itemKey) ?? undefined) : undefined;
214
269
  }
215
- catch {
216
- updatedContent = undefined;
270
+ else {
271
+ try {
272
+ updatedContent = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
273
+ }
274
+ catch {
275
+ updatedContent = undefined;
276
+ }
217
277
  }
218
278
  if (updatedContent !== undefined) {
219
279
  const fileType = (inst.file_type ?? '').trim();
@@ -227,22 +287,26 @@ export function applyAutofixViolations(violations) {
227
287
  }
228
288
  }
229
289
  }
230
- else {
231
- hookRunLog(`autofix: skip re-upload uuid=${inst.uuid} (no stored auth key)`);
232
- }
233
- const spec = remediationFixSpec(inst);
234
- if (spec?.restart_required && spec.restart_command) {
290
+ }
291
+ else {
292
+ hookRunLog(`autofix: skip re-upload uuid=${inst.uuid} (no stored auth key)`);
293
+ }
294
+ const spec = remediationFixSpec(inst);
295
+ if (spec?.restart_required && spec.restart_command) {
296
+ if (!er.deferredSqlite) {
235
297
  hookRunLog(`autofix: restart required uuid=${inst.uuid} command=${spec.restart_command}`);
236
298
  restartCommands.push(spec.restart_command);
237
299
  }
238
- if (!inst.is_enforced) {
239
- oneTimeAppliedUuids.add(inst.uuid);
240
- }
241
300
  }
242
- else {
243
- failedViolations.push(violation);
301
+ if (!inst.is_enforced) {
302
+ oneTimeAppliedUuids.add(inst.uuid);
244
303
  }
245
304
  }
305
+ if (deferredSqlitePending) {
306
+ restartCommands.length = 0;
307
+ restartCommands.push(buildDeferredCursorRestartCommand());
308
+ hookRunLog('autofix: deferred vscdb — restart command runs apply_deferred_vscdb.js then open -a Cursor');
309
+ }
246
310
  if (oneTimeAppliedUuids.size > 0) {
247
311
  const remaining = remediations.filter((r) => !oneTimeAppliedUuids.has(r.uuid));
248
312
  writeRemediationInstructionsFile({ remediations: remaining });
@@ -257,7 +321,7 @@ export function applyAutofixViolations(violations) {
257
321
  if (fixed > 0) {
258
322
  hookRunLog(`autofix: total_applied=${fixed}`);
259
323
  }
260
- return { fixed, restartCommands, failedViolations, reportPromises };
324
+ return { fixed, restartCommands, failedViolations, reportPromises, deferredSqlitePending };
261
325
  }
262
326
  /**
263
327
  * Remove satisfied one-time remediations from local remediation_instructions.json.
@@ -285,18 +349,12 @@ export function pruneSatisfiedOneTimeRemediations() {
285
349
  remaining.push(raw);
286
350
  continue;
287
351
  }
288
- if (!existsSync(inst.config_file_path)) {
289
- remaining.push(raw);
290
- continue;
291
- }
292
- let configJson;
293
- try {
294
- configJson = JSON.parse(readFileSync(inst.config_file_path, 'utf8'));
295
- }
296
- catch {
352
+ const prLoaded = loadRemediationConfigJson(inst.config_file_path);
353
+ if (!prLoaded.ok) {
297
354
  remaining.push(raw);
298
355
  continue;
299
356
  }
357
+ const configJson = prLoaded.json;
300
358
  // Only prune when every check is ops-based and currently satisfied.
301
359
  let okAll = true;
302
360
  for (const check of checks) {
@@ -1,15 +1,40 @@
1
- import { existsSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
2
2
  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
+ const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
5
6
  function getHookLogPath() {
6
7
  const homeDir = process.env.HOME || process.env.USERPROFILE;
7
8
  if (!homeDir)
8
9
  return null;
9
10
  return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, HOOK_LOG_FILENAME);
10
11
  }
11
- /** Replace hook log file at the start of a run so it doesn't grow unboundedly. */
12
- function hookLogReplace() {
12
+ function getComplianceRunnerLogPath() {
13
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
14
+ if (!homeDir)
15
+ return null;
16
+ return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_RUNNER_LOG_FILENAME);
17
+ }
18
+ /**
19
+ * Append-only diagnostics (not truncated by main_runner). Use for remediation sync / compliance.
20
+ */
21
+ function complianceRunnerDiag(message) {
22
+ const logPath = getComplianceRunnerLogPath();
23
+ if (!logPath)
24
+ return;
25
+ try {
26
+ const dir = path.dirname(logPath);
27
+ if (!existsSync(dir))
28
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
29
+ const ts = new Date().toISOString();
30
+ appendFileSync(logPath, `${ts} ${message}\n`, 'utf8');
31
+ }
32
+ catch {
33
+ // best-effort
34
+ }
35
+ }
36
+ /** Visible delimiter between hook_log.txt sessions (compliance gate, upload, etc.). */
37
+ function hookLogSessionBanner(label) {
13
38
  const logPath = getHookLogPath();
14
39
  if (!logPath)
15
40
  return;
@@ -17,12 +42,20 @@ function hookLogReplace() {
17
42
  const dir = path.dirname(logPath);
18
43
  if (!existsSync(dir))
19
44
  mkdirSync(dir, { recursive: true, mode: 0o700 });
20
- writeFileSync(logPath, '', 'utf8');
45
+ const ts = new Date().toISOString();
46
+ const banner = `\n${'='.repeat(72)}\n${ts} session: ${label}\n${'='.repeat(72)}\n`;
47
+ appendFileSync(logPath, banner, 'utf8');
21
48
  }
22
49
  catch {
23
50
  // best-effort
24
51
  }
25
52
  }
53
+ /**
54
+ * @deprecated Name kept for callers: begins a log_config_files upload section (append-only, does not truncate).
55
+ */
56
+ function hookLogReplace() {
57
+ hookLogSessionBanner('log_config_files (config upload)');
58
+ }
26
59
  /** Append a timestamped line to hook_log.txt. */
27
60
  function hookRunLog(message) {
28
61
  const logPath = getHookLogPath();
@@ -48,4 +81,4 @@ function hookLogLine(message) {
48
81
  // best-effort
49
82
  }
50
83
  }
51
- export { getHookLogPath, hookLogReplace, hookRunLog, hookLogLine };
84
+ export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookRunLog, hookLogLine, complianceRunnerDiag, };