log-llm-config 1.3.6 → 1.3.8

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.
@@ -2,9 +2,9 @@ import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { OPT_AI_SEC_MANAGEMENT_REL } from './bootstrap_constants.js';
5
- import { hookLogSessionBanner } from './log_config_files/runtime/hook_logger.js';
5
+ import { hookLogAppendSection } from './log_config_files/runtime/hook_logger.js';
6
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). */
7
+ /** Append-only log for compliance runner lifecycle; hook_log.txt uses a fresh session from the gate then this section. */
8
8
  function runnerFileLog(message) {
9
9
  try {
10
10
  const dir = join(homedir(), OPT_AI_SEC_MANAGEMENT_REL);
@@ -18,7 +18,7 @@ function runnerFileLog(message) {
18
18
  }
19
19
  }
20
20
  (async () => {
21
- hookLogSessionBanner('compliance_check_runner (background sync + check)');
21
+ hookLogAppendSection('compliance_check_runner (background sync + check)');
22
22
  runnerFileLog('compliance_check_runner: start');
23
23
  try {
24
24
  await runComplianceCheck();
@@ -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,12 +99,13 @@ 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') {
95
- const leaf = itemKey.split('/').pop() ?? '';
96
- // Legacy cursorai/donotchange/privacyMode row is bare true/false; compliance paths use …/privacyMode.privacyMode.
97
- if (typeof parsed === 'boolean' && leaf === 'privacyMode') {
98
- return { [itemKey]: { privacyMode: parsed } };
104
+ if (typeof parsed === 'boolean') {
105
+ const field = CURSOR_SCALAR_ITEMTABLE_FIELDS[itemKey];
106
+ if (field) {
107
+ return { [itemKey]: { [field]: parsed } };
108
+ }
99
109
  }
100
110
  return { [itemKey]: parsed };
101
111
  }
@@ -5,8 +5,10 @@
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).
8
+ * Prompt gate: compliance_prompt_gate runs runLocalRemediationComplianceCheck then applyAutofixViolations.
9
9
  * Background: compliance_check_runner runs syncRemediations (network) then the same local check.
10
+ * Apply (autofix) is intentionally left to the gate on the next prompt — the background pass only
11
+ * downloads the latest manifest so the gate has fresh data to act on.
10
12
  */
11
13
  import { existsSync, readFileSync } from 'node:fs';
12
14
  import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
@@ -433,7 +435,8 @@ export function pruneSatisfiedOneTimeRemediations() {
433
435
  }
434
436
  /**
435
437
  * Background refresh: server sync for latest instructions, then the same local evaluation as the hook.
436
- * Does not persist compliance state to disk.
438
+ * Apply (autofix) is intentionally deferred to the gate on the next prompt — this pass only downloads
439
+ * a fresh manifest so the gate has up-to-date data when it runs.
437
440
  */
438
441
  export async function runComplianceCheck() {
439
442
  try {
@@ -442,5 +445,11 @@ export async function runComplianceCheck() {
442
445
  catch (err) {
443
446
  hookRunLog(`compliance_check: remediation_sync unexpected error: ${err instanceof Error ? err.message : String(err)}`);
444
447
  }
445
- runLocalRemediationComplianceCheck();
448
+ const status = runLocalRemediationComplianceCheck();
449
+ if (status.status === 'ok' || status.violations.length === 0) {
450
+ const pruned = pruneSatisfiedOneTimeRemediations();
451
+ if (pruned.removed > 0) {
452
+ await Promise.allSettled(pruned.reportPromises);
453
+ }
454
+ }
446
455
  }
@@ -1,8 +1,10 @@
1
- import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, appendFileSync, writeFileSync, statSync } 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
5
  const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
6
+ /** Hard cap so a single upload/sync session cannot grow hook_log.txt without bound. */
7
+ const MAX_HOOK_LOG_BYTES = 2 * 1024 * 1024;
6
8
  function getHookLogPath() {
7
9
  const homeDir = process.env.HOME || process.env.USERPROFILE;
8
10
  if (!homeDir)
@@ -33,7 +35,24 @@ function complianceRunnerDiag(message) {
33
35
  // best-effort
34
36
  }
35
37
  }
36
- /** Visible delimiter between hook_log.txt sessions (compliance gate, upload, etc.). */
38
+ function ensureHookLogUnderCap() {
39
+ const logPath = getHookLogPath();
40
+ if (!logPath || !existsSync(logPath))
41
+ return;
42
+ try {
43
+ if (statSync(logPath).size <= MAX_HOOK_LOG_BYTES)
44
+ return;
45
+ const ts = new Date().toISOString();
46
+ writeFileSync(logPath, `${'='.repeat(72)}\n${ts} hook_log.txt truncated (exceeded ${MAX_HOOK_LOG_BYTES} bytes)\n${'='.repeat(72)}\n`, 'utf8');
47
+ }
48
+ catch {
49
+ // best-effort
50
+ }
51
+ }
52
+ /**
53
+ * Start a new hook_log.txt session (replaces the file). Use once per logical run
54
+ * (e.g. compliance_prompt_gate, log_config_files upload via hookLogReplace).
55
+ */
37
56
  function hookLogSessionBanner(label) {
38
57
  const logPath = getHookLogPath();
39
58
  if (!logPath)
@@ -44,15 +63,34 @@ function hookLogSessionBanner(label) {
44
63
  mkdirSync(dir, { recursive: true, mode: 0o700 });
45
64
  const ts = new Date().toISOString();
46
65
  const banner = `\n${'='.repeat(72)}\n${ts} session: ${label}\n${'='.repeat(72)}\n`;
47
- appendFileSync(logPath, banner, 'utf8');
66
+ writeFileSync(logPath, banner, 'utf8');
48
67
  }
49
68
  catch {
50
69
  // best-effort
51
70
  }
52
71
  }
53
72
  /**
54
- * @deprecated Name kept for callers: begins a log_config_files upload section (append-only, does not truncate).
73
+ * Append a subsection after an existing session (e.g. compliance_check_runner after the gate)
74
+ * without replacing hook_log.txt.
55
75
  */
76
+ function hookLogAppendSection(label) {
77
+ const logPath = getHookLogPath();
78
+ if (!logPath)
79
+ return;
80
+ try {
81
+ ensureHookLogUnderCap();
82
+ const dir = path.dirname(logPath);
83
+ if (!existsSync(dir))
84
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
85
+ const ts = new Date().toISOString();
86
+ const banner = `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
87
+ appendFileSync(logPath, banner, 'utf8');
88
+ }
89
+ catch {
90
+ // best-effort
91
+ }
92
+ }
93
+ /** Begins a log_config_files upload session (replaces hook_log.txt, same as hookLogSessionBanner). */
56
94
  function hookLogReplace() {
57
95
  hookLogSessionBanner('log_config_files (config upload)');
58
96
  }
@@ -62,6 +100,7 @@ function hookRunLog(message) {
62
100
  if (!logPath)
63
101
  return;
64
102
  try {
103
+ ensureHookLogUnderCap();
65
104
  const ts = new Date().toISOString();
66
105
  appendFileSync(logPath, `${ts} ${message}\n`, 'utf8');
67
106
  }
@@ -75,10 +114,11 @@ function hookLogLine(message) {
75
114
  if (!logPath)
76
115
  return;
77
116
  try {
117
+ ensureHookLogUnderCap();
78
118
  appendFileSync(logPath, `${message}\n`, 'utf8');
79
119
  }
80
120
  catch {
81
121
  // best-effort
82
122
  }
83
123
  }
84
- export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookRunLog, hookLogLine, complianceRunnerDiag, };
124
+ export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookLogAppendSection, hookRunLog, hookLogLine, complianceRunnerDiag, };
@@ -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.6",
3
+ "version": "1.3.8",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {