log-llm-config 1.2.8 → 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.
@@ -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,8 +4,17 @@
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';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { resolve } from 'node:path';
7
10
  import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
11
+ import { hookLogSessionBanner, hookRunLog } from './log_config_files/runtime/hook_logger.js';
8
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
+ }
9
18
  function parseIde() {
10
19
  const eq = process.argv.find((a) => a.startsWith('--ide='));
11
20
  if (eq) {
@@ -42,6 +51,13 @@ function blockPayload(ide, violationMessage) {
42
51
  }
43
52
  return JSON.stringify({ continue: false, user_message: text });
44
53
  }
54
+ function fireRestartCommands(commands) {
55
+ for (const cmd of commands) {
56
+ hookRunLog(`restart: firing command="${cmd}"`);
57
+ const child = spawn('sh', ['-c', cmd], { detached: true, stdio: 'ignore' });
58
+ child.unref();
59
+ }
60
+ }
45
61
  function getManifestStalenessMs() {
46
62
  try {
47
63
  const path = getRemediationInstructionsPath();
@@ -54,8 +70,33 @@ function getManifestStalenessMs() {
54
70
  return null;
55
71
  }
56
72
  }
57
- const ide = parseIde();
58
- async function run() {
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
+ }
84
+ /** Short line for success dialog: finding title/sentence from manifest, not per-check remediation technical text. */
85
+ function autofixDialogLine(v) {
86
+ const title = v.finding_title?.trim();
87
+ if (title)
88
+ return `• [${v.finding_formatted_id}] ${title}`;
89
+ const fd = v.finding_description?.trim();
90
+ if (fd)
91
+ return `• [${v.finding_formatted_id}] ${fd}`;
92
+ const d = v.description.trim();
93
+ const short = d.length > 160 ? `${d.slice(0, 157)}…` : d;
94
+ return `• [${v.finding_formatted_id}] ${short}`;
95
+ }
96
+ /** Exported for tests; CLI invokes this only when {@link isRunAsCliModule} is true. */
97
+ export async function runCompliancePromptGate() {
98
+ const ide = parseIde();
99
+ hookLogSessionBanner('compliance_prompt_gate (before submit)');
59
100
  const status = runLocalRemediationComplianceCheck();
60
101
  if (status.status === 'fail' && status.violations.length > 0) {
61
102
  const staleMs = getManifestStalenessMs();
@@ -66,19 +107,32 @@ async function run() {
66
107
  printAllowWithAdvisory(ide, advisory);
67
108
  return;
68
109
  }
69
- const { fixed, restartCommands, failedViolations, reportPromises } = applyAutofixViolations(status.violations);
110
+ const { fixed, appliedViolations = [], restartCommands, failedViolations, reportPromises, deferredSqlitePending, } = applyAutofixViolations(status.violations);
70
111
  if (fixed > 0) {
71
112
  // Wait for all server reports before exiting so the POST lands.
72
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
+ }
73
122
  const recheck = runLocalRemediationComplianceCheck();
74
- if (recheck.status === 'ok' || recheck.violations.length === 0) {
75
- 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')}`;
123
+ const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
124
+ if (deferredSqlitePending || recheckOk) {
125
+ const autofixMessage = `Optimus Security auto-fixed ${fixed} policy violation(s):\n${appliedViolations.map((v) => autofixDialogLine(v)).join('\n')}`;
78
126
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
79
127
  if (restartCommands.length > 0)
80
128
  payload.restart_commands = restartCommands;
81
129
  console.log(JSON.stringify(payload));
130
+ // Cursor: .cursor/hooks runs restart after the osascript dialog — avoid double SIGKILL here.
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)
135
+ fireRestartCommands(restartCommands);
82
136
  return;
83
137
  }
84
138
  const msg = recheck.violations[0]?.message ?? 'A security policy violation has been detected.';
@@ -102,4 +156,6 @@ async function run() {
102
156
  }
103
157
  printAllow(ide);
104
158
  }
105
- run().catch(() => printAllow(ide)).finally(() => process.exit(0));
159
+ if (isRunAsCliModule()) {
160
+ runCompliancePromptGate().catch(() => printAllow(parseIde())).finally(() => process.exit(0));
161
+ }
@@ -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;