log-llm-config 1.3.0 → 1.3.5
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/compliance_prompt_gate.js +34 -24
- package/dist/execute_trusted_restarts.js +56 -0
- package/dist/log_config_files/runtime/compliance_check.js +46 -16
- package/dist/log_config_files/runtime/hardware_uuid.js +9 -0
- package/dist/log_config_files/runtime/remediation_sync.js +92 -65
- package/dist/log_config_files/runtime/trusted_restarts.js +22 -0
- package/package.json +7 -5
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Synchronous pre-prompt gate: local compliance only (stdout = single JSON line for IDE hooks).
|
|
3
3
|
* stderr must stay clean; logs go via hookRunLog (file).
|
|
4
|
+
*
|
|
5
|
+
* When autofix returns restart_commands, this process does not spawn them — the shell hook pipes
|
|
6
|
+
* the same JSON line to execute_trusted_restarts (TS allowlist + spawn).
|
|
4
7
|
*/
|
|
5
8
|
import { applyAutofixViolations, pruneSatisfiedOneTimeRemediations, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
|
|
6
9
|
import { existsSync, statSync } from 'node:fs';
|
|
7
|
-
import {
|
|
10
|
+
import { pathToFileURL } from 'node:url';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
8
12
|
import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
|
|
9
|
-
import { hookLogSessionBanner
|
|
13
|
+
import { hookLogSessionBanner } from './log_config_files/runtime/hook_logger.js';
|
|
10
14
|
const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
15
|
function parseIde() {
|
|
12
16
|
const eq = process.argv.find((a) => a.startsWith('--ide='));
|
|
@@ -44,13 +48,6 @@ function blockPayload(ide, violationMessage) {
|
|
|
44
48
|
}
|
|
45
49
|
return JSON.stringify({ continue: false, user_message: text });
|
|
46
50
|
}
|
|
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
|
-
}
|
|
54
51
|
function getManifestStalenessMs() {
|
|
55
52
|
try {
|
|
56
53
|
const path = getRemediationInstructionsPath();
|
|
@@ -63,7 +60,17 @@ function getManifestStalenessMs() {
|
|
|
63
60
|
return null;
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
|
-
|
|
63
|
+
function isRunAsCliModule() {
|
|
64
|
+
const entry = process.argv[1];
|
|
65
|
+
if (!entry)
|
|
66
|
+
return false;
|
|
67
|
+
try {
|
|
68
|
+
return import.meta.url === pathToFileURL(resolve(entry)).href;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
67
74
|
/** Short line for success dialog: finding title/sentence from manifest, not per-check remediation technical text. */
|
|
68
75
|
function autofixDialogLine(v) {
|
|
69
76
|
const title = v.finding_title?.trim();
|
|
@@ -76,7 +83,9 @@ function autofixDialogLine(v) {
|
|
|
76
83
|
const short = d.length > 160 ? `${d.slice(0, 157)}…` : d;
|
|
77
84
|
return `• [${v.finding_formatted_id}] ${short}`;
|
|
78
85
|
}
|
|
79
|
-
|
|
86
|
+
/** Exported for tests; CLI invokes this only when {@link isRunAsCliModule} is true. */
|
|
87
|
+
export async function runCompliancePromptGate() {
|
|
88
|
+
const ide = parseIde();
|
|
80
89
|
hookLogSessionBanner('compliance_prompt_gate (before submit)');
|
|
81
90
|
const status = runLocalRemediationComplianceCheck();
|
|
82
91
|
if (status.status === 'fail' && status.violations.length > 0) {
|
|
@@ -88,28 +97,27 @@ async function run() {
|
|
|
88
97
|
printAllowWithAdvisory(ide, advisory);
|
|
89
98
|
return;
|
|
90
99
|
}
|
|
91
|
-
const { fixed, restartCommands, failedViolations, reportPromises, deferredSqlitePending } = applyAutofixViolations(status.violations);
|
|
100
|
+
const { fixed, appliedViolations = [], restartCommands, failedViolations, reportPromises, deferredSqlitePending, } = applyAutofixViolations(status.violations);
|
|
92
101
|
if (fixed > 0) {
|
|
93
102
|
// Wait for all server reports before exiting so the POST lands.
|
|
94
103
|
await Promise.allSettled(reportPromises);
|
|
104
|
+
// Deferred SQLite can leave recheck failing until restart; that must not hide a separate
|
|
105
|
+
// failed autofix (e.g. JSON remediation failed while vscdb was only queued).
|
|
106
|
+
if (failedViolations.length > 0) {
|
|
107
|
+
const ids = failedViolations.map((v) => `[${v.finding_formatted_id}]`).join(', ');
|
|
108
|
+
const msg = `Auto-fix failed for ${ids} — please fix manually or contact your security team.\n\n${failedViolations[0]?.message ?? ''}`;
|
|
109
|
+
console.log(blockPayload(ide, msg));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
95
112
|
const recheck = runLocalRemediationComplianceCheck();
|
|
96
113
|
const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
|
|
97
114
|
if (deferredSqlitePending || recheckOk) {
|
|
98
|
-
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')}`;
|
|
115
|
+
const autofixMessage = `Optimus Security auto-fixed ${fixed} policy violation(s):\n${appliedViolations.map((v) => autofixDialogLine(v)).join('\n')}`;
|
|
106
116
|
const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
|
|
107
117
|
if (restartCommands.length > 0)
|
|
108
118
|
payload.restart_commands = restartCommands;
|
|
109
119
|
console.log(JSON.stringify(payload));
|
|
110
|
-
//
|
|
111
|
-
if (ide !== 'cursor')
|
|
112
|
-
fireRestartCommands(restartCommands);
|
|
120
|
+
// Restarts: always executed by optimus-compliance-check.sh via execute_trusted_restarts (TS allowlist + spawn).
|
|
113
121
|
return;
|
|
114
122
|
}
|
|
115
123
|
const msg = recheck.violations[0]?.message ?? 'A security policy violation has been detected.';
|
|
@@ -133,4 +141,6 @@ async function run() {
|
|
|
133
141
|
}
|
|
134
142
|
printAllow(ide);
|
|
135
143
|
}
|
|
136
|
-
|
|
144
|
+
if (isRunAsCliModule()) {
|
|
145
|
+
runCompliancePromptGate().catch(() => printAllow(parseIde())).finally(() => process.exit(0));
|
|
146
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: read one JSON line from stdin (same shape as compliance_prompt_gate stdout for __optimus_autofix),
|
|
3
|
+
* parse restart_commands[], allowlist each in TS, spawn detached. Invoked by optimus-compliance-check.sh only.
|
|
4
|
+
* stderr stays quiet; failures logged via hookRunLog.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { hookRunLog } from './log_config_files/runtime/hook_logger.js';
|
|
10
|
+
import { executeTrustedRestartCommands } from './log_config_files/runtime/trusted_restarts.js';
|
|
11
|
+
function isRunAsCliModule() {
|
|
12
|
+
const entry = process.argv[1];
|
|
13
|
+
if (!entry)
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
return import.meta.url === pathToFileURL(resolve(entry)).href;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function main() {
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = readFileSync(0, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
hookRunLog('execute_trusted_restarts: could not read stdin');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const first = raw.trim().split(/\r?\n/)[0]?.trim() ?? '';
|
|
32
|
+
if (!first)
|
|
33
|
+
return;
|
|
34
|
+
let j;
|
|
35
|
+
try {
|
|
36
|
+
j = JSON.parse(first);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
hookRunLog('execute_trusted_restarts: stdin is not valid JSON');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const cmds = j.restart_commands;
|
|
43
|
+
if (!Array.isArray(cmds))
|
|
44
|
+
return;
|
|
45
|
+
const strings = cmds.filter((c) => typeof c === 'string');
|
|
46
|
+
executeTrustedRestartCommands(strings);
|
|
47
|
+
}
|
|
48
|
+
if (isRunAsCliModule()) {
|
|
49
|
+
try {
|
|
50
|
+
main();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
hookRunLog(`execute_trusted_restarts: ${e instanceof Error ? e.message : String(e)}`);
|
|
54
|
+
}
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
@@ -13,8 +13,8 @@ import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
|
|
|
13
13
|
import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
|
|
14
14
|
import { complianceRunnerDiag, hookRunLog } from './hook_logger.js';
|
|
15
15
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
16
|
-
import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
17
|
-
import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
|
|
16
|
+
import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
17
|
+
import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
|
|
18
18
|
import { sendConfigFile } from '../sender/batch_sender.js';
|
|
19
19
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
@@ -221,10 +221,17 @@ export function runLocalRemediationComplianceCheck() {
|
|
|
221
221
|
export function applyAutofixViolations(violations) {
|
|
222
222
|
const autofixable = violations.filter((v) => v.autofix_allowed);
|
|
223
223
|
if (autofixable.length === 0)
|
|
224
|
-
return {
|
|
224
|
+
return {
|
|
225
|
+
fixed: 0,
|
|
226
|
+
appliedViolations: [],
|
|
227
|
+
restartCommands: [],
|
|
228
|
+
failedViolations: [],
|
|
229
|
+
reportPromises: [],
|
|
230
|
+
};
|
|
225
231
|
const { remediations } = readRemediationInstructionsFile();
|
|
226
232
|
const byUuid = new Map(remediations.map((r) => [r.uuid, r]));
|
|
227
233
|
let fixed = 0;
|
|
234
|
+
const appliedViolations = [];
|
|
228
235
|
const seen = new Set();
|
|
229
236
|
const restartCommands = [];
|
|
230
237
|
const failedViolations = [];
|
|
@@ -251,6 +258,7 @@ export function applyAutofixViolations(violations) {
|
|
|
251
258
|
deferredSqlitePending = true;
|
|
252
259
|
seen.add(violation.uuid);
|
|
253
260
|
fixed++;
|
|
261
|
+
appliedViolations.push(violation);
|
|
254
262
|
hookRunLog(`autofix: applied uuid=${inst.uuid} path=${inst.config_file_path}`);
|
|
255
263
|
reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
|
|
256
264
|
const authKey = readStoredAuthKey();
|
|
@@ -278,9 +286,15 @@ export function applyAutofixViolations(violations) {
|
|
|
278
286
|
if (updatedContent !== undefined) {
|
|
279
287
|
const fileType = (inst.file_type ?? '').trim();
|
|
280
288
|
if (fileType) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
289
|
+
const hw = tryResolveHardwareUuid();
|
|
290
|
+
if (hw) {
|
|
291
|
+
reportPromises.push(sendConfigFile({ file_type: fileType, file_path: inst.config_file_path, raw_content: updatedContent }, hw, authKey).then((sentOk) => {
|
|
292
|
+
hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${inst.config_file_path} ok=${sentOk}`);
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
hookRunLog(`autofix: skip upload uuid=${inst.uuid} (hardware UUID unavailable)`);
|
|
297
|
+
}
|
|
284
298
|
}
|
|
285
299
|
else {
|
|
286
300
|
hookRunLog(`autofix: skip upload uuid=${inst.uuid} — remediation_instructions.json missing file_type (re-sync manifest)`);
|
|
@@ -294,8 +308,13 @@ export function applyAutofixViolations(violations) {
|
|
|
294
308
|
const spec = remediationFixSpec(inst);
|
|
295
309
|
if (spec?.restart_required && spec.restart_command) {
|
|
296
310
|
if (!er.deferredSqlite) {
|
|
297
|
-
|
|
298
|
-
|
|
311
|
+
if (isTrustedRestartCommandForAutofix(spec.restart_command)) {
|
|
312
|
+
hookRunLog(`autofix: restart required uuid=${inst.uuid} command=${spec.restart_command}`);
|
|
313
|
+
restartCommands.push(spec.restart_command);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
hookRunLog(`autofix: restart command rejected (not an allowlisted template) uuid=${inst.uuid}`);
|
|
317
|
+
}
|
|
299
318
|
}
|
|
300
319
|
}
|
|
301
320
|
if (!inst.is_enforced) {
|
|
@@ -314,14 +333,20 @@ export function applyAutofixViolations(violations) {
|
|
|
314
333
|
// Send a post-autofix heartbeat so the server sees the updated (reduced) UUID set immediately,
|
|
315
334
|
// without waiting for the background runner (which may be locked out).
|
|
316
335
|
const remainingUuids = remaining.map((r) => r.uuid);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
.
|
|
336
|
+
const hwHeartbeat = tryResolveHardwareUuid();
|
|
337
|
+
if (hwHeartbeat) {
|
|
338
|
+
reportPromises.push(fetchSync(loadEndpointBase(), hwHeartbeat, remainingUuids)
|
|
339
|
+
.then(() => hookRunLog(`autofix: post-autofix heartbeat sent uuids=${remainingUuids.length}`))
|
|
340
|
+
.catch(() => undefined));
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
hookRunLog('autofix: skip post-autofix heartbeat (hardware UUID unavailable)');
|
|
344
|
+
}
|
|
320
345
|
}
|
|
321
346
|
if (fixed > 0) {
|
|
322
347
|
hookRunLog(`autofix: total_applied=${fixed}`);
|
|
323
348
|
}
|
|
324
|
-
return { fixed, restartCommands, failedViolations, reportPromises, deferredSqlitePending };
|
|
349
|
+
return { fixed, appliedViolations, restartCommands, failedViolations, reportPromises, deferredSqlitePending };
|
|
325
350
|
}
|
|
326
351
|
/**
|
|
327
352
|
* Remove satisfied one-time remediations from local remediation_instructions.json.
|
|
@@ -381,11 +406,16 @@ export function pruneSatisfiedOneTimeRemediations() {
|
|
|
381
406
|
writeRemediationInstructionsFile({ remediations: remaining });
|
|
382
407
|
hookRunLog(`remediation_prune: removed=${removed} remaining=${remaining.length}`);
|
|
383
408
|
const remainingUuids = remaining.map((r) => r.uuid);
|
|
384
|
-
const
|
|
385
|
-
|
|
409
|
+
const hw = tryResolveHardwareUuid();
|
|
410
|
+
const reportPromises = [];
|
|
411
|
+
if (hw) {
|
|
412
|
+
reportPromises.push(fetchSync(loadEndpointBase(), hw, remainingUuids)
|
|
386
413
|
.then(() => hookRunLog(`remediation_prune: post-prune heartbeat sent uuids=${remainingUuids.length}`))
|
|
387
|
-
.catch(() => undefined)
|
|
388
|
-
|
|
414
|
+
.catch(() => undefined));
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
hookRunLog('remediation_prune: skip post-prune heartbeat (hardware UUID unavailable)');
|
|
418
|
+
}
|
|
389
419
|
return { removed, reportPromises };
|
|
390
420
|
}
|
|
391
421
|
/**
|
|
@@ -24,4 +24,13 @@ function resolveHardwareUuid() {
|
|
|
24
24
|
}
|
|
25
25
|
throw new Error('Unable to determine hardware UUID via ioreg or system_profiler.');
|
|
26
26
|
}
|
|
27
|
+
/** Same as {@link resolveHardwareUuid} but returns null when the host exposes no stable ID (e.g. Linux CI). */
|
|
28
|
+
export function tryResolveHardwareUuid() {
|
|
29
|
+
try {
|
|
30
|
+
return resolveHardwareUuid();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
27
36
|
export { resolveHardwareUuid };
|
|
@@ -7,7 +7,7 @@ import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContr
|
|
|
7
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
|
-
import {
|
|
10
|
+
import { tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
11
11
|
import { 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';
|
|
@@ -319,6 +319,42 @@ function assertSqlite3Available() {
|
|
|
319
319
|
return false;
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Unquoted SQLite identifiers must be [A-Za-z_][A-Za-z0-9_]* so values read from
|
|
324
|
+
* deferred_vscdb_apply.json cannot inject SQL via bogus table/column names.
|
|
325
|
+
*/
|
|
326
|
+
function isSafeSqliteIdentifier(ident) {
|
|
327
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(ident);
|
|
328
|
+
}
|
|
329
|
+
function assertSafeSqliteIdentifiersForItemTable(table, keyColumn, valueColumn) {
|
|
330
|
+
if (isSafeSqliteIdentifier(table) && isSafeSqliteIdentifier(keyColumn) && isSafeSqliteIdentifier(valueColumn)) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
hookRunLog(`sqlite_update: rejected unsafe SQL identifier(s) table=${table} key_column=${keyColumn} value_column=${valueColumn}`);
|
|
334
|
+
complianceRunnerDiag('sqlite_update: rejected unsafe SQL identifier(s)');
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Autofix restart_command allowlist: manifest strings are attacker-controlled if JSON is tampered.
|
|
339
|
+
* Deferred vscdb path always uses buildDeferredCursorRestartCommand(); this guards non-deferred restarts.
|
|
340
|
+
*/
|
|
341
|
+
export function isTrustedRestartCommandForAutofix(cmd) {
|
|
342
|
+
const t = cmd.trim();
|
|
343
|
+
if (!t)
|
|
344
|
+
return false;
|
|
345
|
+
const deferredPrefix = 'REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export REPO_ROOT CURSOR_PROJECT="$REPO_ROOT" && ';
|
|
346
|
+
const legacyCursorPrefix = 'CURSOR_PROJECT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export CURSOR_PROJECT && ';
|
|
347
|
+
const legacyCursorSnippet = 'nohup bash -c \'sleep 2 && open -a Cursor "$CURSOR_PROJECT"\'';
|
|
348
|
+
const deferred = t.startsWith(deferredPrefix) &&
|
|
349
|
+
(t.includes('apply_deferred_vscdb') || t.includes('apply-deferred-vscdb')) &&
|
|
350
|
+
t.includes('killall -9 Cursor') &&
|
|
351
|
+
t.includes('open -a Cursor');
|
|
352
|
+
const legacyCursor = t.startsWith(legacyCursorPrefix) &&
|
|
353
|
+
t.includes(legacyCursorSnippet) &&
|
|
354
|
+
t.includes('killall -9 Cursor');
|
|
355
|
+
const claude = t.startsWith("nohup bash -c 'sleep 2 && open -a Claude'") && t.includes("pkill -x 'Claude'");
|
|
356
|
+
return deferred || legacyCursor || claude;
|
|
357
|
+
}
|
|
322
358
|
/** Legacy Cursor: dedicated ItemTable row `composerState`. Current Cursor: nested under reactive `applicationUser` blob. */
|
|
323
359
|
function cursorVscdbHasUsableComposerStateRow(dbPath, sqliteOp) {
|
|
324
360
|
const raw = sqliteSelectValueCell(dbPath, sqliteOp.table, sqliteOp.key_column, sqliteOp.value_column, 'composerState').trim();
|
|
@@ -374,6 +410,25 @@ function resolveCursorComposerSqliteOp(dbPath, sqliteOp) {
|
|
|
374
410
|
return sqliteOp;
|
|
375
411
|
}
|
|
376
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
|
+
function coerceScalarForItemTableField(parsed) {
|
|
420
|
+
if (typeof parsed === 'boolean')
|
|
421
|
+
return parsed;
|
|
422
|
+
if (typeof parsed === 'string') {
|
|
423
|
+
const lower = parsed.trim().toLowerCase();
|
|
424
|
+
if (lower === 'true' || lower === '1' || lower === 'yes')
|
|
425
|
+
return true;
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
if (typeof parsed === 'number' && !Number.isNaN(parsed))
|
|
429
|
+
return parsed !== 0;
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
377
432
|
/**
|
|
378
433
|
* ItemTable values are sometimes bare JSON booleans for toggle keys; sqlite merge expects an object root.
|
|
379
434
|
* Maps known scalar roots to the policy-shaped object before applying updates.
|
|
@@ -382,44 +437,11 @@ function coerceItemTableValueToObjectRoot(targetKey, parsed) {
|
|
|
382
437
|
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
383
438
|
return parsed;
|
|
384
439
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const lower = parsed.trim().toLowerCase();
|
|
391
|
-
const b = lower === 'true' || lower === '1' || lower === 'yes';
|
|
392
|
-
return { thirdPartyExtensibilityEnabled: b };
|
|
393
|
-
}
|
|
394
|
-
if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
|
|
395
|
-
return { thirdPartyExtensibilityEnabled: parsed !== 0 };
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (targetKey === 'cursorai/donotchange/privacyMode') {
|
|
399
|
-
if (typeof parsed === 'boolean') {
|
|
400
|
-
return { privacyMode: parsed };
|
|
401
|
-
}
|
|
402
|
-
if (typeof parsed === 'string') {
|
|
403
|
-
const lower = parsed.trim().toLowerCase();
|
|
404
|
-
const b = lower === 'true' || lower === '1' || lower === 'yes';
|
|
405
|
-
return { privacyMode: b };
|
|
406
|
-
}
|
|
407
|
-
if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
|
|
408
|
-
return { privacyMode: parsed !== 0 };
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (targetKey === 'cursor/autoOpenLocalhostUrls') {
|
|
412
|
-
if (typeof parsed === 'boolean') {
|
|
413
|
-
return { autoOpenLocalhostUrls: parsed };
|
|
414
|
-
}
|
|
415
|
-
if (typeof parsed === 'string') {
|
|
416
|
-
const lower = parsed.trim().toLowerCase();
|
|
417
|
-
const b = lower === 'true' || lower === '1' || lower === 'yes';
|
|
418
|
-
return { autoOpenLocalhostUrls: b };
|
|
419
|
-
}
|
|
420
|
-
if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
|
|
421
|
-
return { autoOpenLocalhostUrls: parsed !== 0 };
|
|
422
|
-
}
|
|
440
|
+
const field = CURSOR_SCALAR_ITEMTABLE_FIELDS[targetKey];
|
|
441
|
+
if (field) {
|
|
442
|
+
const b = coerceScalarForItemTableField(parsed);
|
|
443
|
+
if (b !== undefined)
|
|
444
|
+
return { [field]: b };
|
|
423
445
|
}
|
|
424
446
|
return {};
|
|
425
447
|
}
|
|
@@ -428,30 +450,11 @@ function coerceItemTableValueToObjectRoot(targetKey, parsed) {
|
|
|
428
450
|
* object can be ignored or overwritten on launch; match the native primitive shape when disabling.
|
|
429
451
|
*/
|
|
430
452
|
function serializeItemTableValueForWrite(targetKey, root) {
|
|
431
|
-
|
|
453
|
+
const field = CURSOR_SCALAR_ITEMTABLE_FIELDS[targetKey];
|
|
454
|
+
if (field) {
|
|
432
455
|
const keys = Object.keys(root);
|
|
433
|
-
if (keys.length === 1 && keys[0] ===
|
|
434
|
-
const v = root
|
|
435
|
-
if (typeof v === 'boolean')
|
|
436
|
-
return v ? 'true' : 'false';
|
|
437
|
-
if (typeof v === 'number' && !Number.isNaN(v))
|
|
438
|
-
return v !== 0 ? 'true' : 'false';
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
if (targetKey === 'cursorai/donotchange/privacyMode') {
|
|
442
|
-
const keys = Object.keys(root);
|
|
443
|
-
if (keys.length === 1 && keys[0] === 'privacyMode') {
|
|
444
|
-
const v = root.privacyMode;
|
|
445
|
-
if (typeof v === 'boolean')
|
|
446
|
-
return v ? 'true' : 'false';
|
|
447
|
-
if (typeof v === 'number' && !Number.isNaN(v))
|
|
448
|
-
return v !== 0 ? 'true' : 'false';
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
if (targetKey === 'cursor/autoOpenLocalhostUrls') {
|
|
452
|
-
const keys = Object.keys(root);
|
|
453
|
-
if (keys.length === 1 && keys[0] === 'autoOpenLocalhostUrls') {
|
|
454
|
-
const v = root.autoOpenLocalhostUrls;
|
|
456
|
+
if (keys.length === 1 && keys[0] === field) {
|
|
457
|
+
const v = root[field];
|
|
455
458
|
if (typeof v === 'boolean')
|
|
456
459
|
return v ? 'true' : 'false';
|
|
457
460
|
if (typeof v === 'number' && !Number.isNaN(v))
|
|
@@ -580,6 +583,9 @@ function mergeJsonAtSqlitePath(currentJson, json_path, updates) {
|
|
|
580
583
|
}
|
|
581
584
|
}
|
|
582
585
|
function sqliteSelectValueCell(dbPath, table, key_column, value_column, target_key) {
|
|
586
|
+
if (!assertSafeSqliteIdentifiersForItemTable(table, key_column, value_column)) {
|
|
587
|
+
return '';
|
|
588
|
+
}
|
|
583
589
|
const safeName = target_key.replace(/'/g, "''");
|
|
584
590
|
const script = `.timeout 60000\nSELECT ${value_column} FROM ${table} WHERE ${key_column}='${safeName}';\n`;
|
|
585
591
|
return execFileSync('sqlite3', ['-noheader', dbPath], {
|
|
@@ -663,6 +669,9 @@ export async function applyDeferredVscdbFromDisk() {
|
|
|
663
669
|
hookRunLog(`deferred_vscdb: database missing ${it.dbPath}`);
|
|
664
670
|
return false;
|
|
665
671
|
}
|
|
672
|
+
if (!assertSafeSqliteIdentifiersForItemTable(it.table, it.key_column, it.value_column)) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
666
675
|
const safeJson = it.new_value_json.replace(/'/g, "''");
|
|
667
676
|
const safeName = it.target_key.replace(/'/g, "''");
|
|
668
677
|
const sql = `UPDATE ${it.table} SET ${it.value_column}='${safeJson}' WHERE ${it.key_column}='${safeName}';`;
|
|
@@ -691,7 +700,12 @@ export async function applyDeferredVscdbFromDisk() {
|
|
|
691
700
|
hookRunLog(`deferred_vscdb: post-apply read failed path=${u.file_path}`);
|
|
692
701
|
continue;
|
|
693
702
|
}
|
|
694
|
-
const
|
|
703
|
+
const hw = tryResolveHardwareUuid();
|
|
704
|
+
if (!hw) {
|
|
705
|
+
hookRunLog(`deferred_vscdb: skip post-apply upload (hardware UUID unavailable) path=${u.file_path}`);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const sent = await sendConfigFile({ file_type: u.file_type, file_path: u.file_path, raw_content: rawContent }, hw, authKey);
|
|
695
709
|
hookRunLog(`deferred_vscdb: post-apply upload path=${u.file_path} ok=${sent}`);
|
|
696
710
|
}
|
|
697
711
|
unlinkSync(path);
|
|
@@ -702,7 +716,13 @@ export async function applyDeferredVscdbFromDisk() {
|
|
|
702
716
|
return false;
|
|
703
717
|
}
|
|
704
718
|
}
|
|
705
|
-
/**
|
|
719
|
+
/**
|
|
720
|
+
* macOS Cursor: SIGKILL, apply queued vscdb writes, reopen project.
|
|
721
|
+
*
|
|
722
|
+
* When `restart_required` implies deferred state.vscdb, `applyAutofixViolations` replaces any
|
|
723
|
+
* `restart_command` from remediation specs with this string (see compliance_check.ts). Spec JSON
|
|
724
|
+
* still documents a simpler Cursor reopen for non-code readers; that template is not executed on the deferred path.
|
|
725
|
+
*/
|
|
706
726
|
export function buildDeferredCursorRestartCommand() {
|
|
707
727
|
// Prefer monorepo path when hooks run from optimus-secure-fdn; otherwise `npx -p log-llm-config apply-deferred-vscdb`
|
|
708
728
|
// (package bin) so published installs work without a local npx_packages copy.
|
|
@@ -798,6 +818,9 @@ function applyOrQueueSqliteJsonUpdate(configPath, sqliteOp, deferred) {
|
|
|
798
818
|
return false;
|
|
799
819
|
sqliteOp = resolveCursorComposerSqliteOp(dbPath, sqliteOp);
|
|
800
820
|
const { table, key_column, value_column, target_key } = sqliteOp;
|
|
821
|
+
if (!assertSafeSqliteIdentifiersForItemTable(table, key_column, value_column)) {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
801
824
|
const safeName = target_key.replace(/'/g, "''");
|
|
802
825
|
let currentJson = {};
|
|
803
826
|
try {
|
|
@@ -932,7 +955,11 @@ export function reportAutofixApplied(remediationUuid, result) {
|
|
|
932
955
|
hookRunLog(`autofix_report: no auth key available, skipping report for uuid=${remediationUuid}`);
|
|
933
956
|
return Promise.resolve();
|
|
934
957
|
}
|
|
935
|
-
const hardwareUuid =
|
|
958
|
+
const hardwareUuid = tryResolveHardwareUuid();
|
|
959
|
+
if (!hardwareUuid) {
|
|
960
|
+
hookRunLog(`autofix_report: hardware UUID unavailable, skipping report for uuid=${remediationUuid}`);
|
|
961
|
+
return Promise.resolve();
|
|
962
|
+
}
|
|
936
963
|
const endpointBase = loadEndpointBase();
|
|
937
964
|
const url = `${resolveOrigin(endpointBase)}/endpoint_security/api/autofix-applied/`;
|
|
938
965
|
const payload = { hardware_uuid: hardwareUuid, remediation_uuid: remediationUuid, result };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execute autofix restart_command strings only if allowlisted (same rules as gate enqueue).
|
|
3
|
+
* Used by the compliance shell hook via execute_trusted_restarts CLI — single place for allowlist + spawn.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { hookRunLog } from './hook_logger.js';
|
|
7
|
+
import { isTrustedRestartCommandForAutofix } from './remediation_sync.js';
|
|
8
|
+
/** Spawn each trusted command detached (same pattern as former compliance_prompt_gate fireRestartCommands). */
|
|
9
|
+
export function executeTrustedRestartCommands(commands) {
|
|
10
|
+
for (const cmd of commands) {
|
|
11
|
+
const t = cmd.trim();
|
|
12
|
+
if (!t)
|
|
13
|
+
continue;
|
|
14
|
+
if (!isTrustedRestartCommandForAutofix(cmd)) {
|
|
15
|
+
hookRunLog(`execute_trusted_restarts: rejected (not allowlisted) prefix=${t.slice(0, 120)}`);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
hookRunLog(`execute_trusted_restarts: firing command="${t}"`);
|
|
19
|
+
const child = spawn('sh', ['-c', t], { detached: true, stdio: 'ignore' });
|
|
20
|
+
child.unref();
|
|
21
|
+
}
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "log-llm-config",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
"log_uuid": "dist/log_uuid/index.js",
|
|
9
9
|
"log_config_files": "dist/log_config_files/index.js",
|
|
10
10
|
"log_sensitive_paths_audit": "dist/log_sensitive_paths_audit.js",
|
|
11
|
-
"apply-deferred-vscdb": "dist/apply_deferred_vscdb.js"
|
|
11
|
+
"apply-deferred-vscdb": "dist/apply_deferred_vscdb.js",
|
|
12
|
+
"execute-trusted-restarts": "dist/execute_trusted_restarts.js"
|
|
12
13
|
},
|
|
13
14
|
"scripts": {
|
|
14
15
|
"build": "tsc -p tsconfig.json",
|
|
15
16
|
"dev": "ts-node src/cli.ts",
|
|
16
17
|
"lint": "eslint \"src/**/*.ts\"",
|
|
17
18
|
"prepare": "npm run build",
|
|
19
|
+
"pretest": "npm run build",
|
|
18
20
|
"test": "vitest run",
|
|
19
21
|
"test:watch": "vitest"
|
|
20
22
|
},
|
|
@@ -44,11 +46,11 @@
|
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
48
|
"@types/node": "^24.10.1",
|
|
47
|
-
"@vitest/coverage-v8": "^
|
|
48
|
-
"@vitest/ui": "^
|
|
49
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
50
|
+
"@vitest/ui": "^3.2.4",
|
|
49
51
|
"ts-node": "^10.9.2",
|
|
50
52
|
"typescript": "^5.4.5",
|
|
51
|
-
"vitest": "^
|
|
53
|
+
"vitest": "^3.2.4"
|
|
52
54
|
},
|
|
53
55
|
"dependencies": {
|
|
54
56
|
"axios": "^1.13.5",
|