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
|
-
|
|
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 (
|
|
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
|
|
9
|
-
* Background: compliance_check_runner runs syncRemediations (network)
|
|
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#
|
|
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
|
|
94
|
-
if (!
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
423
|
-
*
|
|
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;
|