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.
- package/dist/apply_deferred_vscdb.js +8 -0
- package/dist/cli/bash_script_generator.js +23 -11
- package/dist/compliance_check_runner.js +31 -0
- package/dist/compliance_prompt_gate.js +35 -4
- package/dist/log_config_files/readers/vscdb_config_builder.js +6 -2
- package/dist/log_config_files/readers/vscdb_reader.js +102 -1
- package/dist/log_config_files/runtime/compliance_check.js +103 -45
- package/dist/log_config_files/runtime/hook_logger.js +38 -5
- package/dist/log_config_files/runtime/main_runner.js +2 -0
- package/dist/log_config_files/runtime/management_storage.js +31 -0
- package/dist/log_config_files/runtime/remediation_sync.js +680 -17
- package/package.json +4 -3
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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='${
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
77
|
-
const
|
|
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'
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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,
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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, };
|