log-llm-config-staging 1.3.83 → 1.3.84
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/cli.js +6 -0
- package/dist/compliance_prompt_gate.js +26 -15
- package/dist/dialog_prefs_cli.js +105 -0
- package/dist/log_config_files/runtime/compliance_check.js +66 -7
- package/dist/log_config_files/runtime/dialog_preferences.js +31 -0
- package/dist/log_config_files/runtime/management_storage.js +5 -0
- package/dist/log_config_files/runtime/ops_target_path.js +18 -0
- package/dist/log_config_files/runtime/remediation_config_path.js +36 -2
- package/dist/log_config_files/runtime/remediation_sync.js +87 -23
- package/dist/log_config_files/runtime/trusted_restarts.js +4 -0
- package/dist/remediation_change_preview.js +182 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -38,6 +38,12 @@ const main = async () => {
|
|
|
38
38
|
await import('./compliance_check_runner.js');
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
+
if (args[0] === 'dialog_prefs') {
|
|
42
|
+
const { runDialogPrefsCli } = await import('./dialog_prefs_cli.js');
|
|
43
|
+
await runDialogPrefsCli();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
41
47
|
// `npx log-llm-config@latest <name>` runs this file (default bin) with args[0] set — not the named bin file.
|
|
42
48
|
if (args[0] === 'execute-trusted-restarts') {
|
|
43
49
|
const { runExecuteTrustedRestartsFromStdin } = await import('./execute_trusted_restarts.js');
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* When autofix returns restart_commands, this process does not spawn them — the shell hook pipes
|
|
7
7
|
* the same JSON line to execute_trusted_restarts (TS allowlist + spawn).
|
|
8
8
|
*/
|
|
9
|
-
import { applyAutofixViolations, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, reportPostRestartVerificationOutcomes, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
|
|
10
|
-
import { isRemediationQuarantined } from './log_config_files/runtime/remediation_apply_tracking.js';
|
|
9
|
+
import { applyAutofixViolations, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, reportPostRestartVerificationOutcomes, runLocalRemediationComplianceCheck, uploadSatisfiedManifestConfigs, } from './log_config_files/runtime/compliance_check.js';
|
|
10
|
+
import { isRemediationQuarantined, markRemediationApplyVerified, } from './log_config_files/runtime/remediation_apply_tracking.js';
|
|
11
11
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
12
|
-
import { getRemediationInstructionsPath
|
|
12
|
+
import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
|
|
13
13
|
import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
|
|
14
14
|
import { isThisCliModule } from './cli_invocation_match.js';
|
|
15
15
|
import { ensureAuthentication } from './log_config_files/auth/auth_flow.js';
|
|
@@ -17,6 +17,7 @@ import { sendConfigFile } from './log_config_files/sender/batch_sender.js';
|
|
|
17
17
|
import { tryResolveHardwareUuid } from './log_config_files/runtime/hardware_uuid.js';
|
|
18
18
|
import { syncRemediations } from './log_config_files/runtime/remediation_sync.js';
|
|
19
19
|
import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
|
|
20
|
+
import { formatRemediationChangePreviewForApplied } from './remediation_change_preview.js';
|
|
20
21
|
const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
21
22
|
function parseIde() {
|
|
22
23
|
const eq = process.argv.find((a) => a.startsWith('--ide='));
|
|
@@ -103,8 +104,11 @@ function uniquePolicyLabelsFromViolations(appliedViolations) {
|
|
|
103
104
|
return policyNames.length > 0 ? policyNames.join('\n\n') : 'Agent security policy';
|
|
104
105
|
}
|
|
105
106
|
function formatPreventiveAutofixDialog(appliedViolations, applyLine) {
|
|
107
|
+
const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
|
|
108
|
+
const changePreviewBlock = changePreview ? `${changePreview}\n\n` : '';
|
|
106
109
|
return ('Optimus Labs enforced a preventive agent security policy as determined by your security team:\n\n' +
|
|
107
110
|
`${uniquePolicyLabelsFromViolations(appliedViolations)}\n\n` +
|
|
111
|
+
changePreviewBlock +
|
|
108
112
|
`${applyLine}\n\n` +
|
|
109
113
|
'Click OK to continue');
|
|
110
114
|
}
|
|
@@ -166,17 +170,14 @@ export async function runCompliancePromptGate() {
|
|
|
166
170
|
// unavailable server never delays the gate significantly.
|
|
167
171
|
const hw = tryResolveHardwareUuid();
|
|
168
172
|
if (hw) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
catch {
|
|
178
|
-
// Network or auth failure — fall back to local file state.
|
|
179
|
-
}
|
|
173
|
+
try {
|
|
174
|
+
await Promise.race([
|
|
175
|
+
syncRemediations(loadEndpointBase(), hw),
|
|
176
|
+
new Promise((resolve) => setTimeout(resolve, 3000)),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Network or auth failure — fall back to local file state.
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
const status = runLocalRemediationComplianceCheck(agent);
|
|
@@ -248,13 +249,22 @@ export async function runCompliancePromptGate() {
|
|
|
248
249
|
hookRunLog('compliance_prompt_gate: Claude — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)');
|
|
249
250
|
}
|
|
250
251
|
if (deferredSqlitePending || recheckOk || claudeRecheckStaleAfterImmediateApply) {
|
|
252
|
+
// For immediate (non-restart, non-deferred) fixes that the inline recheck confirmed,
|
|
253
|
+
// mark verified now so the next hook run does not false-quarantine due to pending flag.
|
|
254
|
+
if (recheckOk && restartCommands.length === 0 && !deferredSqlitePending) {
|
|
255
|
+
for (const v of appliedViolations) {
|
|
256
|
+
markRemediationApplyVerified(v.uuid);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
|
|
260
|
+
const changePreviewSuffix = changePreview ? `\n\n${changePreview}` : '';
|
|
251
261
|
const autofixMessage = ide === 'cursor' && restartCommands.length > 0
|
|
252
262
|
? formatCursorRestartAutofixDialog(appliedViolations)
|
|
253
263
|
: ide === 'claude'
|
|
254
264
|
? formatClaudeAutofixDialog(appliedViolations)
|
|
255
265
|
: `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
|
|
256
266
|
.map((v) => autofixDialogLine(v))
|
|
257
|
-
.join('\n')}`;
|
|
267
|
+
.join('\n')}${changePreviewSuffix}`;
|
|
258
268
|
const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
|
|
259
269
|
if (restartCommands.length > 0)
|
|
260
270
|
payload.restart_commands = restartCommands;
|
|
@@ -300,6 +310,7 @@ export async function runCompliancePromptGate() {
|
|
|
300
310
|
if (pruned.removed > 0) {
|
|
301
311
|
await Promise.allSettled(pruned.reportPromises);
|
|
302
312
|
}
|
|
313
|
+
await Promise.allSettled(uploadSatisfiedManifestConfigs(agent));
|
|
303
314
|
printAllow(ide);
|
|
304
315
|
}
|
|
305
316
|
if (isRunAsCliModule()) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI for macOS non-restart autofix dialogs and dialog preferences in ~/opt-ai-sec/management.
|
|
4
|
+
* Invoked from optimus-compliance-check.sh hooks.
|
|
5
|
+
*/
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { isNonRestartAutofixDialogSuppressed, setSuppressNonRestartAutofixDialogs, } from './log_config_files/runtime/dialog_preferences.js';
|
|
9
|
+
export const STOP_NOTIFICATIONS_BUTTON = 'Mute Alerts';
|
|
10
|
+
export const OK_BUTTON = 'OK';
|
|
11
|
+
function cliParts() {
|
|
12
|
+
const a = process.argv;
|
|
13
|
+
if (a[2] === 'dialog_prefs')
|
|
14
|
+
return a.slice(2);
|
|
15
|
+
return a.slice(2);
|
|
16
|
+
}
|
|
17
|
+
function parseArgValue(parts, flag) {
|
|
18
|
+
const eq = parts.find((p) => p.startsWith(`${flag}=`));
|
|
19
|
+
if (eq)
|
|
20
|
+
return eq.slice(flag.length + 1);
|
|
21
|
+
const idx = parts.indexOf(flag);
|
|
22
|
+
if (idx >= 0 && parts[idx + 1])
|
|
23
|
+
return parts[idx + 1];
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
function escapeAppleScriptString(text) {
|
|
27
|
+
return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
28
|
+
}
|
|
29
|
+
function icnsIconClause() {
|
|
30
|
+
const p = (process.env.OPTIMUS_DIALOG_ICNS ?? '').trim();
|
|
31
|
+
if (!p || !existsSync(p))
|
|
32
|
+
return 'with icon caution';
|
|
33
|
+
try {
|
|
34
|
+
const buf = readFileSync(p);
|
|
35
|
+
if (buf.length < 8 || buf.subarray(0, 4).toString('ascii') !== 'icns') {
|
|
36
|
+
return 'with icon caution';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'with icon caution';
|
|
41
|
+
}
|
|
42
|
+
return `with icon POSIX file "${escapeAppleScriptString(p)}"`;
|
|
43
|
+
}
|
|
44
|
+
export function buildNonRestartAutofixDialogScript(message, target) {
|
|
45
|
+
const msg = escapeAppleScriptString(message);
|
|
46
|
+
const icon = icnsIconClause();
|
|
47
|
+
const mute = escapeAppleScriptString(STOP_NOTIFICATIONS_BUTTON);
|
|
48
|
+
const body = `display dialog "${msg}" with title "Optimus Labs" buttons {"${mute}", "${OK_BUTTON}"} default button "${OK_BUTTON}" ${icon}`;
|
|
49
|
+
if (target === 'cursor') {
|
|
50
|
+
return `tell application "Cursor" to ${body}`;
|
|
51
|
+
}
|
|
52
|
+
if (target === 'claude-app') {
|
|
53
|
+
return `tell application "Claude" to ${body}`;
|
|
54
|
+
}
|
|
55
|
+
return body;
|
|
56
|
+
}
|
|
57
|
+
export function showNonRestartAutofixDialog(message, target) {
|
|
58
|
+
const script = buildNonRestartAutofixDialogScript(message, target);
|
|
59
|
+
let out = '';
|
|
60
|
+
try {
|
|
61
|
+
out = execFileSync('osascript', ['-'], { input: script, encoding: 'utf8' });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const e = err;
|
|
65
|
+
out = typeof e.stdout === 'string' ? e.stdout : '';
|
|
66
|
+
// User dismissed or osascript failed — do not block callers; preference only set on explicit Stop.
|
|
67
|
+
if (!out.includes(STOP_NOTIFICATIONS_BUTTON)) {
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (out.includes(STOP_NOTIFICATIONS_BUTTON)) {
|
|
72
|
+
setSuppressNonRestartAutofixDialogs(true);
|
|
73
|
+
return 'stop_notifications';
|
|
74
|
+
}
|
|
75
|
+
return 'ok';
|
|
76
|
+
}
|
|
77
|
+
function readMessageFile(path) {
|
|
78
|
+
if (!path || !existsSync(path)) {
|
|
79
|
+
throw new Error(`message file not found: ${path}`);
|
|
80
|
+
}
|
|
81
|
+
return readFileSync(path, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
export async function runDialogPrefsCli() {
|
|
84
|
+
const parts = cliParts();
|
|
85
|
+
const sub = parts[1];
|
|
86
|
+
if (sub === 'is_suppressed') {
|
|
87
|
+
console.log(isNonRestartAutofixDialogSuppressed() ? 'true' : 'false');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (sub === 'set_suppress') {
|
|
91
|
+
setSuppressNonRestartAutofixDialogs(true);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (sub === 'show') {
|
|
95
|
+
const messageFile = parseArgValue(parts, '--message-file');
|
|
96
|
+
const targetRaw = parseArgValue(parts, '--target') ?? 'terminal';
|
|
97
|
+
if (targetRaw !== 'cursor' && targetRaw !== 'claude-app' && targetRaw !== 'terminal') {
|
|
98
|
+
throw new Error(`invalid --target: ${targetRaw}`);
|
|
99
|
+
}
|
|
100
|
+
const message = readMessageFile(messageFile ?? '');
|
|
101
|
+
showNonRestartAutofixDialog(message, targetRaw);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`unknown dialog_prefs subcommand: ${sub ?? '(none)'}`);
|
|
105
|
+
}
|
|
@@ -16,7 +16,8 @@ import { join } from 'node:path';
|
|
|
16
16
|
import { mergeComposerShadowKeysFromReactiveBlob, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
|
|
17
17
|
import { readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
18
18
|
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
19
|
-
import {
|
|
19
|
+
import { resolveOpsTargetPath } from './ops_target_path.js';
|
|
20
|
+
import { isRemediationQuarantined, markRemediationApplyPendingVerification, processPendingPostRestartVerifications, readRemediationApplyTrackingFile, writeRemediationApplyTrackingFile, } from './remediation_apply_tracking.js';
|
|
20
21
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
21
22
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
22
23
|
import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
@@ -166,13 +167,8 @@ function verifyOpsApplied(configJson, settingPath, ops) {
|
|
|
166
167
|
const add = ops.add ?? {};
|
|
167
168
|
const remove = ops.remove ?? {};
|
|
168
169
|
const keys = new Set([...Object.keys(set), ...Object.keys(add), ...Object.keys(remove)]);
|
|
169
|
-
// If ops targets a single leaf key, check at settingPath; otherwise treat keys as subkeys under parent.
|
|
170
170
|
for (const k of keys) {
|
|
171
|
-
const targetPath =
|
|
172
|
-
? settingPath
|
|
173
|
-
: parentPath
|
|
174
|
-
? `${parentPath}.${k}`
|
|
175
|
-
: k;
|
|
171
|
+
const targetPath = resolveOpsTargetPath(settingPath, k);
|
|
176
172
|
if (Object.prototype.hasOwnProperty.call(set, k)) {
|
|
177
173
|
const cur = getByPath(configJson, targetPath);
|
|
178
174
|
const expected = set[k];
|
|
@@ -770,6 +766,68 @@ export function pruneSatisfiedOneTimeRemediations(agent = 'cursor') {
|
|
|
770
766
|
}
|
|
771
767
|
return { removed, reportPromises };
|
|
772
768
|
}
|
|
769
|
+
/** Throttle satisfied-manifest uploads (disk already matches ops; autofix did not run). */
|
|
770
|
+
const SATISFIED_MANIFEST_UPLOAD_COOLDOWN_MS = 5 * 60 * 1000;
|
|
771
|
+
/**
|
|
772
|
+
* When local compliance already passes, autofix is skipped — so settings never reach the server.
|
|
773
|
+
* Upload satisfied JSON remediations (throttled) so scan can resolve findings.
|
|
774
|
+
*/
|
|
775
|
+
export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
|
|
776
|
+
const { remediations } = readRemediationInstructionsFile();
|
|
777
|
+
const entries = remediations.filter((e) => targetsCurrentAgent(e, agent));
|
|
778
|
+
const promises = [];
|
|
779
|
+
for (const entry of entries) {
|
|
780
|
+
const { violations } = evaluateManifestEntryCompliance(entry);
|
|
781
|
+
if (violations.length > 0)
|
|
782
|
+
continue;
|
|
783
|
+
const inst = entry;
|
|
784
|
+
const fileType = (inst.file_type ?? '').trim();
|
|
785
|
+
if (!fileType)
|
|
786
|
+
continue;
|
|
787
|
+
const diskPath = resolveRemediationConfigPath(entry.config_file_path);
|
|
788
|
+
if (diskPath.includes('#'))
|
|
789
|
+
continue;
|
|
790
|
+
const tracking = readRemediationApplyTrackingFile();
|
|
791
|
+
const prev = tracking.entries[entry.uuid];
|
|
792
|
+
const lastUpload = prev?.last_satisfied_upload_at;
|
|
793
|
+
if (lastUpload) {
|
|
794
|
+
const elapsed = Date.now() - Date.parse(lastUpload);
|
|
795
|
+
if (!Number.isNaN(elapsed) && elapsed < SATISFIED_MANIFEST_UPLOAD_COOLDOWN_MS) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
let rawContent;
|
|
800
|
+
try {
|
|
801
|
+
rawContent = JSON.parse(readFileSync(diskPath, 'utf8'));
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
hookRunLog(`satisfied_upload: could not read path=${diskPath} uuid=${entry.uuid}`);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const hw = tryResolveHardwareUuid();
|
|
808
|
+
if (!hw) {
|
|
809
|
+
hookRunLog(`satisfied_upload: skip uuid=${entry.uuid} (hardware UUID unavailable)`);
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const uploadPath = entry.config_file_path.trim() || diskPath;
|
|
813
|
+
promises.push(ensureAuthentication(hw)
|
|
814
|
+
.then((authKey) => sendConfigFile({ file_type: fileType, file_path: uploadPath, raw_content: rawContent }, hw, authKey))
|
|
815
|
+
.then((sentOk) => {
|
|
816
|
+
hookRunLog(`satisfied_upload: uuid=${entry.uuid} path=${uploadPath} ok=${sentOk}`);
|
|
817
|
+
if (sentOk) {
|
|
818
|
+
const file = readRemediationApplyTrackingFile();
|
|
819
|
+
const ent = file.entries[entry.uuid] ?? { consecutive_verify_failures: 0, quarantined: false };
|
|
820
|
+
ent.last_satisfied_upload_at = new Date().toISOString();
|
|
821
|
+
file.entries[entry.uuid] = ent;
|
|
822
|
+
writeRemediationApplyTrackingFile(file);
|
|
823
|
+
}
|
|
824
|
+
})
|
|
825
|
+
.catch((err) => {
|
|
826
|
+
hookRunLog(`satisfied_upload: failed uuid=${entry.uuid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
return promises;
|
|
830
|
+
}
|
|
773
831
|
/**
|
|
774
832
|
* Background refresh: server sync for latest instructions, then the same local evaluation as the hook.
|
|
775
833
|
* Apply (autofix) is intentionally deferred to the gate on the next prompt — this pass only downloads
|
|
@@ -789,5 +847,6 @@ export async function runComplianceCheck() {
|
|
|
789
847
|
if (pruned.removed > 0) {
|
|
790
848
|
await Promise.allSettled(pruned.reportPromises);
|
|
791
849
|
}
|
|
850
|
+
await Promise.allSettled(uploadSatisfiedManifestConfigs(agent));
|
|
792
851
|
}
|
|
793
852
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { atomicWriteJson, getDialogPreferencesPath, } from './management_storage.js';
|
|
3
|
+
export function readDialogPreferences() {
|
|
4
|
+
const path = getDialogPreferencesPath();
|
|
5
|
+
if (!existsSync(path)) {
|
|
6
|
+
return { version: 1, suppress_non_restart_autofix_dialogs: false };
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
10
|
+
if (parsed?.version !== 1)
|
|
11
|
+
return { version: 1, suppress_non_restart_autofix_dialogs: false };
|
|
12
|
+
return {
|
|
13
|
+
version: 1,
|
|
14
|
+
suppress_non_restart_autofix_dialogs: Boolean(parsed.suppress_non_restart_autofix_dialogs),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { version: 1, suppress_non_restart_autofix_dialogs: false };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function isNonRestartAutofixDialogSuppressed() {
|
|
22
|
+
return readDialogPreferences().suppress_non_restart_autofix_dialogs === true;
|
|
23
|
+
}
|
|
24
|
+
export function setSuppressNonRestartAutofixDialogs(suppress) {
|
|
25
|
+
const next = {
|
|
26
|
+
version: 1,
|
|
27
|
+
suppress_non_restart_autofix_dialogs: suppress,
|
|
28
|
+
updated_at: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
atomicWriteJson(getDialogPreferencesPath(), next);
|
|
31
|
+
}
|
|
@@ -17,6 +17,8 @@ export const DEFERRED_VSCDB_RESTART_LOG_BASENAME = 'deferred_vscdb_restart.log';
|
|
|
17
17
|
* of hardcoded Cursor paths.
|
|
18
18
|
*/
|
|
19
19
|
export const FILE_COLLECTION_VSCDB_CONTRACT_BASENAME = 'file_collection_vscdb_contract.json';
|
|
20
|
+
/** User preferences for Optimus macOS dialogs (hook-driven). */
|
|
21
|
+
export const DIALOG_PREFERENCES_BASENAME = 'optimus_dialog_preferences.json';
|
|
20
22
|
export function sanitizeReactiveStorageItemKey(raw) {
|
|
21
23
|
if (typeof raw !== 'string')
|
|
22
24
|
return undefined;
|
|
@@ -59,6 +61,9 @@ export function getDeferredVscdbRestartLogPath() {
|
|
|
59
61
|
export function getFileCollectionVscdbContractPath() {
|
|
60
62
|
return join(getManagementDir(), FILE_COLLECTION_VSCDB_CONTRACT_BASENAME);
|
|
61
63
|
}
|
|
64
|
+
export function getDialogPreferencesPath() {
|
|
65
|
+
return join(getManagementDir(), DIALOG_PREFERENCES_BASENAME);
|
|
66
|
+
}
|
|
62
67
|
export function readFileCollectionVscdbContract() {
|
|
63
68
|
const path = getFileCollectionVscdbContractPath();
|
|
64
69
|
if (!existsSync(path))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map ops.add/set/remove keys to JSON dot-paths for apply + verify.
|
|
3
|
+
*
|
|
4
|
+
* Server may use either the leaf key (e.g. `deny`) or the full setting_path
|
|
5
|
+
* (e.g. `cursor.general.globalCursorIgnoreList`) as the op map key.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveOpsTargetPath(settingPath, opKey) {
|
|
8
|
+
const parts = settingPath.split('.');
|
|
9
|
+
const leafKey = parts[parts.length - 1] ?? '';
|
|
10
|
+
const parentPath = parts.slice(0, -1).join('.');
|
|
11
|
+
if (opKey === settingPath || opKey === leafKey) {
|
|
12
|
+
return settingPath;
|
|
13
|
+
}
|
|
14
|
+
if (opKey.includes('.')) {
|
|
15
|
+
return opKey;
|
|
16
|
+
}
|
|
17
|
+
return parentPath ? `${parentPath}.${opKey}` : opKey;
|
|
18
|
+
}
|
|
@@ -6,6 +6,30 @@ import { join } from 'node:path';
|
|
|
6
6
|
* (see optimus_security/endpoint/log_config_file/handler._canonical_cursor_user_state_vscdb_path).
|
|
7
7
|
*/
|
|
8
8
|
export const PORTABLE_CURSOR_USER_STATE_VSCDB = 'Cursor/User/globalStorage/state.vscdb';
|
|
9
|
+
/** Portable key for Cursor User settings.json (globalCursorIgnoreList). */
|
|
10
|
+
export const PORTABLE_CURSOR_USER_SETTINGS = 'Cursor/User/settings.json';
|
|
11
|
+
function expandLeadingTilde(configFilePath) {
|
|
12
|
+
const t = configFilePath.trim();
|
|
13
|
+
if (t.startsWith('~/')) {
|
|
14
|
+
return join(homedir(), t.slice(2));
|
|
15
|
+
}
|
|
16
|
+
return configFilePath;
|
|
17
|
+
}
|
|
18
|
+
function cursorUserSettingsAbsolutePaths() {
|
|
19
|
+
const h = homedir();
|
|
20
|
+
if (process.platform === 'darwin') {
|
|
21
|
+
const support = join(h, 'Library', 'Application Support');
|
|
22
|
+
const variants = ['Cursor', 'Cursor - Insiders', 'Cursor Nightly', 'Cursor Next'];
|
|
23
|
+
return variants.map((name) => join(support, name, 'User', 'settings.json'));
|
|
24
|
+
}
|
|
25
|
+
if (process.platform === 'win32') {
|
|
26
|
+
const appData = process.env.APPDATA;
|
|
27
|
+
if (appData)
|
|
28
|
+
return [join(appData, 'Cursor', 'User', 'settings.json')];
|
|
29
|
+
return [join(h, 'AppData', 'Roaming', 'Cursor', 'User', 'settings.json')];
|
|
30
|
+
}
|
|
31
|
+
return [join(h, '.config', 'Cursor', 'User', 'settings.json')];
|
|
32
|
+
}
|
|
9
33
|
function normalizeSlashes(p) {
|
|
10
34
|
return p.trim().replace(/\\/g, '/');
|
|
11
35
|
}
|
|
@@ -74,13 +98,23 @@ function cursorStateVscdbAbsoluteBasePaths() {
|
|
|
74
98
|
* No-op for absolute paths and for relative paths that are not the portable vscdb key.
|
|
75
99
|
*/
|
|
76
100
|
export function resolveRemediationConfigPath(configFilePath) {
|
|
77
|
-
const
|
|
101
|
+
const expandedTilde = expandLeadingTilde(configFilePath);
|
|
102
|
+
const t = normalizeSlashes(expandedTilde);
|
|
78
103
|
const hash = t.indexOf('#');
|
|
79
104
|
const base = hash >= 0 ? t.slice(0, hash) : t;
|
|
80
105
|
const frag = hash >= 0 ? t.slice(hash) : '';
|
|
81
106
|
const baseNorm = base.replace(/\/+$/, '');
|
|
82
107
|
if (baseNorm.startsWith('/') || /^[a-zA-Z]:\//.test(baseNorm)) {
|
|
83
|
-
return
|
|
108
|
+
return expandedTilde;
|
|
109
|
+
}
|
|
110
|
+
const portableSettings = PORTABLE_CURSOR_USER_SETTINGS;
|
|
111
|
+
if (baseNorm === portableSettings || baseNorm.endsWith('/' + portableSettings)) {
|
|
112
|
+
const candidates = cursorUserSettingsAbsolutePaths();
|
|
113
|
+
for (const abs of candidates) {
|
|
114
|
+
if (existsSync(abs))
|
|
115
|
+
return abs;
|
|
116
|
+
}
|
|
117
|
+
return candidates[0];
|
|
84
118
|
}
|
|
85
119
|
const portable = PORTABLE_CURSOR_USER_STATE_VSCDB;
|
|
86
120
|
if (baseNorm !== portable && !baseNorm.endsWith('/' + portable)) {
|
|
@@ -2,10 +2,11 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, rename
|
|
|
2
2
|
import { delimiter, dirname, join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import {
|
|
5
|
+
import { executeBody } from '../../endpoint_client/http_transport.js';
|
|
6
6
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
7
7
|
import { atomicWriteJson, getDeferredVscdbApplyPath, getFileCollectionVscdbContractPath, getRemediationInstructionsPath, readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
8
8
|
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
9
|
+
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
9
10
|
import { createSignature } from '../sender/signing.js';
|
|
10
11
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
11
12
|
import { tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
@@ -16,6 +17,7 @@ import { parseWorktreeRootFromPath } from './worktree_scanner.js';
|
|
|
16
17
|
import { reportAbsentWorktrees } from './worktree_absent.js';
|
|
17
18
|
import { buildApiUrl, getFileCollectionPatterns } from '../../endpoint_client/registry_api.js';
|
|
18
19
|
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
20
|
+
import { resolveOpsTargetPath } from './ops_target_path.js';
|
|
19
21
|
import { resolveSqlite3Binary } from './sqlite_binary.js';
|
|
20
22
|
import { CURSOR_COMPOSER_SHADOW_KEYS } from '../readers/cursor_shadow_merge_policy.js';
|
|
21
23
|
import { REACTIVE_STORAGE_ITEM_KEY_SUFFIX, composerShadowKeysFromContract, reactiveStorageItemKeyForVscdb, } from '../readers/vscdb_reactive_storage.js';
|
|
@@ -156,38 +158,95 @@ function transferQuarantineForRetiredUuids(removed, added, retiredInstructions,
|
|
|
156
158
|
if (changed)
|
|
157
159
|
writeRemediationApplyTrackingFile(file);
|
|
158
160
|
}
|
|
161
|
+
async function ensureAuthKeyForMachine(machineUuid) {
|
|
162
|
+
const stored = readStoredAuthKey();
|
|
163
|
+
if (stored?.key)
|
|
164
|
+
return stored;
|
|
165
|
+
try {
|
|
166
|
+
return await ensureAuthentication(machineUuid);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
hookRunLog(`remediation_sync: ensureAuthentication failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
170
|
+
complianceRunnerDiag(`remediation_sync: ensureAuthentication failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function buildSignedMachinePostBody(machineUuid, fields, authKey) {
|
|
175
|
+
const payload = { machine_uuid: machineUuid, ...fields };
|
|
176
|
+
const signature = createSignature(payload, authKey.key);
|
|
177
|
+
return JSON.stringify({ ...payload, signature });
|
|
178
|
+
}
|
|
159
179
|
export async function fetchSync(endpointBase, machineUuid, activeUuids, timeoutMs = 8000) {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
const sortedUuids = [...new Set(activeUuids.map((u) => u.trim()).filter(Boolean))].sort();
|
|
181
|
+
const authKey = await ensureAuthKeyForMachine(machineUuid);
|
|
182
|
+
if (!authKey) {
|
|
183
|
+
hookRunLog('remediation_sync_post: no auth key, skipping');
|
|
184
|
+
complianceRunnerDiag('remediation_sync_post: no auth key');
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const url = buildApiUrl(endpointBase, '/api/findings/remediations/sync/');
|
|
188
|
+
const body = buildSignedMachinePostBody(machineUuid, { active_uuids: sortedUuids }, authKey);
|
|
189
|
+
let statusCode = 0;
|
|
190
|
+
let responseBody = '';
|
|
191
|
+
try {
|
|
192
|
+
const result = await executeBody(url, 'POST', body, timeoutMs);
|
|
193
|
+
statusCode = result.statusCode;
|
|
194
|
+
responseBody = result.body;
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
const line = `remediation_sync_post: url=${url} error=${err instanceof Error ? err.message : String(err)}`;
|
|
198
|
+
hookRunLog(line);
|
|
199
|
+
complianceRunnerDiag(line);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (statusCode !== 200 || !responseBody) {
|
|
203
|
+
const line = `remediation_sync_post: url=${url} status=${statusCode} bytes=${responseBody?.length ?? 0}`;
|
|
165
204
|
hookRunLog(line);
|
|
166
205
|
complianceRunnerDiag(line);
|
|
167
206
|
return null;
|
|
168
207
|
}
|
|
169
208
|
try {
|
|
170
|
-
return JSON.parse(
|
|
209
|
+
return JSON.parse(responseBody);
|
|
171
210
|
}
|
|
172
211
|
catch {
|
|
173
|
-
hookRunLog('
|
|
174
|
-
complianceRunnerDiag('
|
|
212
|
+
hookRunLog('remediation_sync_post: invalid JSON body');
|
|
213
|
+
complianceRunnerDiag('remediation_sync_post: invalid JSON body');
|
|
175
214
|
return null;
|
|
176
215
|
}
|
|
177
216
|
}
|
|
178
217
|
async function fetchManifest(endpointBase, machineUuid, uuids, timeoutMs = 8000) {
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
|
|
218
|
+
const sortedUuids = [...new Set(uuids.map((u) => u.trim()).filter(Boolean))].sort();
|
|
219
|
+
const authKey = await ensureAuthKeyForMachine(machineUuid);
|
|
220
|
+
if (!authKey) {
|
|
221
|
+
hookRunLog('remediation_manifest_post: no auth key, skipping');
|
|
222
|
+
complianceRunnerDiag('remediation_manifest_post: no auth key');
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const url = buildApiUrl(endpointBase, '/api/findings/remediations/manifest/');
|
|
226
|
+
const body = buildSignedMachinePostBody(machineUuid, { uuids: sortedUuids }, authKey);
|
|
227
|
+
let statusCode = 0;
|
|
228
|
+
let responseBody = '';
|
|
229
|
+
try {
|
|
230
|
+
const result = await executeBody(url, 'POST', body, timeoutMs);
|
|
231
|
+
statusCode = result.statusCode;
|
|
232
|
+
responseBody = result.body;
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
const line = `remediation_manifest_post: url=${url} error=${err instanceof Error ? err.message : String(err)}`;
|
|
236
|
+
hookRunLog(line);
|
|
237
|
+
complianceRunnerDiag(line);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (statusCode !== 200 || !responseBody) {
|
|
241
|
+
const line = `remediation_manifest_post: url=${url} status=${statusCode} bytes=${responseBody?.length ?? 0}`;
|
|
183
242
|
hookRunLog(line);
|
|
184
243
|
complianceRunnerDiag(line);
|
|
185
244
|
return null;
|
|
186
245
|
}
|
|
187
246
|
try {
|
|
188
|
-
const parsed = JSON.parse(
|
|
247
|
+
const parsed = JSON.parse(responseBody);
|
|
189
248
|
if (!Array.isArray(parsed.remediations)) {
|
|
190
|
-
complianceRunnerDiag('
|
|
249
|
+
complianceRunnerDiag('remediation_manifest_post: JSON remediations is not an array');
|
|
191
250
|
return null;
|
|
192
251
|
}
|
|
193
252
|
return parsed.remediations.map((raw) => {
|
|
@@ -197,7 +256,7 @@ async function fetchManifest(endpointBase, machineUuid, uuids, timeoutMs = 8000)
|
|
|
197
256
|
});
|
|
198
257
|
}
|
|
199
258
|
catch {
|
|
200
|
-
complianceRunnerDiag('
|
|
259
|
+
complianceRunnerDiag('remediation_manifest_post: invalid JSON body');
|
|
201
260
|
return null;
|
|
202
261
|
}
|
|
203
262
|
}
|
|
@@ -308,11 +367,7 @@ function applyCheck(configJson, check) {
|
|
|
308
367
|
...Object.keys(remove ?? {}),
|
|
309
368
|
]);
|
|
310
369
|
for (const k of keys) {
|
|
311
|
-
const targetPath =
|
|
312
|
-
? check.setting_path
|
|
313
|
-
: parentPath
|
|
314
|
-
? `${parentPath}.${k}`
|
|
315
|
-
: k;
|
|
370
|
+
const targetPath = resolveOpsTargetPath(check.setting_path, k);
|
|
316
371
|
if (set && Object.prototype.hasOwnProperty.call(set, k)) {
|
|
317
372
|
setByPath(configJson, targetPath, set[k]);
|
|
318
373
|
continue;
|
|
@@ -431,7 +486,11 @@ const TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND = 'REPO_ROOT=$(git rev-pars
|
|
|
431
486
|
'CURSOR_PROJECT="${CURSOR_PROJECT_DIR:-$REPO_ROOT}" && export CURSOR_PROJECT && ' +
|
|
432
487
|
'OPTIMUS_DEFERRED_LOG="${HOME}/opt-ai-sec/management/deferred_vscdb_restart.log" && mkdir -p "$(dirname "$OPTIMUS_DEFERRED_LOG")" && export OPTIMUS_DEFERRED_LOG && ' +
|
|
433
488
|
"nohup bash -c 'exec >>\"\$OPTIMUS_DEFERRED_LOG\" 2>&1; echo deferred_restart:begin ts=\$(date -u +%Y-%m-%dT%H:%M:%SZ) REPO_ROOT=\"\$REPO_ROOT\" CURSOR_PROJECT=\"\$CURSOR_PROJECT\"; sleep 2; STATIC_DEV_ENV=\"\$HOME/opt-ai-sec/management/optimus_dev.env\"; ENV_LABEL=\"\"; if [ -f \"\$STATIC_DEV_ENV\" ]; then ENV_LABEL=\$(grep -E \"^(environment|ENVIRONMENT|OPTIMUS_ENVIRONMENT)=\" \"\$STATIC_DEV_ENV\" 2>/dev/null | head -1 | cut -d\"=\" -f2- | tr \"[:upper:]\" \"[:lower:]\" | xargs); fi; if [ -z \"\$ENV_LABEL\" ] && [ -n \"\${OPTIMUS_ENVIRONMENT:-}\" ]; then ENV_LABEL=\$(printf \"%s\" \"\$OPTIMUS_ENVIRONMENT\" | tr \"[:upper:]\" \"[:lower:]\" | xargs); fi; [ -z \"\$ENV_LABEL\" ] && ENV_LABEL=\"production\"; NPX_BASE=\"\"; if [ -f \"\$STATIC_DEV_ENV\" ]; then _nb=\$(grep -E \"^npx=\" \"\$STATIC_DEV_ENV\" 2>/dev/null | head -1 | cut -d\"=\" -f2- | xargs); [ -n \"\$_nb\" ] && [ -d \"\$_nb\" ] && NPX_BASE=\"\$_nb\"; fi; p=\"\${npm_config_prefix:-\${NPM_CONFIG_PREFIX:-}}\"; gp=\"\${npm_config_global_prefix:-}\"; gc=\"\${npm_config_globalconfig:-}\"; if [[ \"\$p\" == *\"/Applications/Cursor.app/\"* ]] || [[ \"\$gp\" == *\"/Applications/Cursor.app/\"* ]] || [[ \"\$gc\" == *\"/Applications/Cursor.app/\"* ]]; then unset npm_config_prefix NPM_CONFIG_PREFIX npm_config_global_prefix npm_config_globalconfig npm_node_execpath 2>/dev/null; fi; APPLY_EC=0; if [ -f \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\" ]; then echo deferred_restart:apply_via_dev_npx_packages; node \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\"; APPLY_EC=\$?; elif [ \"\$ENV_LABEL\" = \"development\" ] && [ -n \"\$NPX_BASE\" ] && [ -f \"\$NPX_BASE/log-llm-config/dist/apply_deferred_vscdb.js\" ]; then echo deferred_restart:apply_via_optimus_npx_base; node \"\$NPX_BASE/log-llm-config/dist/apply_deferred_vscdb.js\"; APPLY_EC=\$?; elif command -v npx >/dev/null 2>&1; then echo deferred_restart:apply_via_npx env=\"\$ENV_LABEL\"; cd \"\$REPO_ROOT\" || true; if [ \"\$ENV_LABEL\" = \"staging\" ]; then npx --yes --package=log-llm-config-staging@latest apply-deferred-vscdb-staging; APPLY_EC=\$?; else npx --yes --package=log-llm-config@latest apply-deferred-vscdb; APPLY_EC=\$?; fi; else echo deferred_restart:no_npx; APPLY_EC=127; fi; echo deferred_restart:apply_exit=\$APPLY_EC; if [ \$APPLY_EC -ne 0 ]; then echo deferred_restart:APPLY_FAILED_see_messages_above; fi; echo deferred_restart:open_cursor; env -u npm_config_package -u npm_lifecycle_event -u npm_lifecycle_script -u npm_config_local_prefix open -a Cursor \"\$CURSOR_PROJECT\"; echo deferred_restart:open_exit=\$?; echo deferred_restart:end ts=\$(date -u +%Y-%m-%dT%H:%M:%SZ)' >/dev/null 2>&1 & killall -9 Cursor";
|
|
434
|
-
|
|
489
|
+
/** Legacy manifests; hooks may run with cwd under `.cursor` where `pwd` is wrong. */
|
|
490
|
+
const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY = 'CURSOR_PROJECT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export CURSOR_PROJECT && nohup bash -c \'sleep 2 && open -a Cursor "$CURSOR_PROJECT"\' >/dev/null 2>&1 & killall -9 Cursor';
|
|
491
|
+
const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND = 'REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) && export REPO_ROOT && ' +
|
|
492
|
+
'CURSOR_PROJECT="${CURSOR_PROJECT_DIR:-$REPO_ROOT}" && export CURSOR_PROJECT && ' +
|
|
493
|
+
"nohup bash -c 'sleep 2 && open -a Cursor \"$CURSOR_PROJECT\"' >/dev/null 2>&1 & killall -9 Cursor";
|
|
435
494
|
const TRUSTED_CLAUDE_RESTART_COMMAND = "nohup bash -c 'sleep 2 && open -a Claude' >/dev/null 2>&1 & pkill -x 'Claude'";
|
|
436
495
|
export function isClaudeRestartCommand(cmd) {
|
|
437
496
|
return cmd.trim() === TRUSTED_CLAUDE_RESTART_COMMAND;
|
|
@@ -439,7 +498,8 @@ export function isClaudeRestartCommand(cmd) {
|
|
|
439
498
|
export function isCursorRestartCommand(cmd) {
|
|
440
499
|
const t = cmd.trim();
|
|
441
500
|
return (t === TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND ||
|
|
442
|
-
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND
|
|
501
|
+
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND ||
|
|
502
|
+
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY);
|
|
443
503
|
}
|
|
444
504
|
/**
|
|
445
505
|
* Autofix restart_command allowlist: manifest strings are attacker-controlled if JSON is tampered.
|
|
@@ -452,6 +512,7 @@ export function isTrustedRestartCommandForAutofix(cmd) {
|
|
|
452
512
|
return false;
|
|
453
513
|
return (t === TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND ||
|
|
454
514
|
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND ||
|
|
515
|
+
t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND_LEGACY ||
|
|
455
516
|
t === TRUSTED_CLAUDE_RESTART_COMMAND);
|
|
456
517
|
}
|
|
457
518
|
/** Legacy Cursor: dedicated ItemTable row `composerState`. Current Cursor: nested under reactive `applicationUser` blob. */
|
|
@@ -927,6 +988,9 @@ export function buildDeferredCursorRestartCommand() {
|
|
|
927
988
|
// (package bin) so published installs work without a local dev_npx_packages copy.
|
|
928
989
|
return TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND;
|
|
929
990
|
}
|
|
991
|
+
export function buildCursorJsonSettingsRestartCommand() {
|
|
992
|
+
return TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND;
|
|
993
|
+
}
|
|
930
994
|
function sqliteRowGroupKey(dbPath, op) {
|
|
931
995
|
return `${dbPath}|${op.table}|${op.key_column}|${op.value_column}|${op.target_key}`;
|
|
932
996
|
}
|
|
@@ -7,6 +7,7 @@ import { appendComplianceRunnerLine, hookRunLog } from './hook_logger.js';
|
|
|
7
7
|
import { getDeferredVscdbRestartLogPath } from './management_storage.js';
|
|
8
8
|
import { normalizeAgentToken } from './compliance_check.js';
|
|
9
9
|
import { isClaudeRestartCommand, isCursorRestartCommand, isTrustedRestartCommandForAutofix } from './remediation_sync.js';
|
|
10
|
+
import { resolveProjectRoot } from './workspace_repo.js';
|
|
10
11
|
const FALLBACK_PATH = '/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin';
|
|
11
12
|
/** Vars set by `npx --package=...` that break unrelated scoped `npx @scope/pkg` after Cursor reopen. */
|
|
12
13
|
const NPM_RESTART_POLLUTION = [
|
|
@@ -72,11 +73,14 @@ export function executeTrustedRestartCommands(commands) {
|
|
|
72
73
|
hookRunLog('execute_trusted_restarts: spawning trusted restart (see compliance_runner.log [RESTART])');
|
|
73
74
|
appendComplianceRunnerLine('RESTART', 'spawn trusted_restart sh -c (non-deferred)');
|
|
74
75
|
}
|
|
76
|
+
const projectRoot = resolveProjectRoot();
|
|
75
77
|
const child = spawn('sh', ['-c', t], {
|
|
76
78
|
detached: true,
|
|
77
79
|
stdio: 'ignore',
|
|
80
|
+
cwd: projectRoot,
|
|
78
81
|
env: envForRestartSpawn({
|
|
79
82
|
...process.env,
|
|
83
|
+
CURSOR_PROJECT_DIR: process.env.CURSOR_PROJECT_DIR || projectRoot,
|
|
80
84
|
PATH: process.env.PATH && String(process.env.PATH).trim().length > 0
|
|
81
85
|
? process.env.PATH
|
|
82
86
|
: FALLBACK_PATH,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { readRemediationInstructionsFile } from './log_config_files/runtime/management_storage.js';
|
|
2
|
+
import { remediationFixSpec } from './log_config_files/runtime/remediation_sync.js';
|
|
3
|
+
export const MAX_PREVIEW_LINES = 5;
|
|
4
|
+
export const MAX_PREVIEW_LINE_CHARS = 72;
|
|
5
|
+
function truncateLine(text, max = MAX_PREVIEW_LINE_CHARS) {
|
|
6
|
+
const trimmed = text.trim();
|
|
7
|
+
if (trimmed.length <= max)
|
|
8
|
+
return trimmed;
|
|
9
|
+
return `${trimmed.slice(0, max - 1)}…`;
|
|
10
|
+
}
|
|
11
|
+
function formatScalar(value) {
|
|
12
|
+
if (value === null)
|
|
13
|
+
return 'null';
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return 'undefined';
|
|
16
|
+
if (typeof value === 'string')
|
|
17
|
+
return `"${value}"`;
|
|
18
|
+
if (typeof value === 'boolean' || typeof value === 'number')
|
|
19
|
+
return String(value);
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
if (value.length === 0)
|
|
22
|
+
return '[]';
|
|
23
|
+
const parts = value.slice(0, 2).map((entry) => formatScalar(entry));
|
|
24
|
+
if (value.length > 2)
|
|
25
|
+
return `${parts.join(', ')} …+${value.length - 2}`;
|
|
26
|
+
return parts.join(', ');
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'object') {
|
|
29
|
+
const record = value;
|
|
30
|
+
if (typeof record.serverName === 'string')
|
|
31
|
+
return `"${record.serverName}"`;
|
|
32
|
+
const keys = Object.keys(record);
|
|
33
|
+
if (keys.length === 1 && keys[0])
|
|
34
|
+
return formatScalar(record[keys[0]]);
|
|
35
|
+
if (keys.length <= 3)
|
|
36
|
+
return `{${keys.join(', ')}}`;
|
|
37
|
+
return `{${keys.slice(0, 2).join(', ')}, …}`;
|
|
38
|
+
}
|
|
39
|
+
return String(value);
|
|
40
|
+
}
|
|
41
|
+
function formatListDeltaItems(items) {
|
|
42
|
+
if (items.length === 0)
|
|
43
|
+
return '—';
|
|
44
|
+
const shown = items.slice(0, 2).map((entry) => formatScalar(entry));
|
|
45
|
+
if (items.length > 2)
|
|
46
|
+
return `${shown.join(', ')} …+${items.length - 2}`;
|
|
47
|
+
return shown.join(', ');
|
|
48
|
+
}
|
|
49
|
+
function leafFromSettingPath(settingPath, opKey) {
|
|
50
|
+
const path = settingPath.trim();
|
|
51
|
+
if (!path)
|
|
52
|
+
return opKey;
|
|
53
|
+
if (path.endsWith(`.${opKey}`))
|
|
54
|
+
return opKey;
|
|
55
|
+
const last = path.split('.').pop();
|
|
56
|
+
return last && last.length > 0 ? last : opKey;
|
|
57
|
+
}
|
|
58
|
+
function linesFromOps(check) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
const settingPath = check.setting_path ?? '';
|
|
61
|
+
const ops = check.ops;
|
|
62
|
+
if (!ops || typeof ops !== 'object')
|
|
63
|
+
return lines;
|
|
64
|
+
const setOps = ops.set;
|
|
65
|
+
if (setOps && typeof setOps === 'object') {
|
|
66
|
+
for (const [key, value] of Object.entries(setOps)) {
|
|
67
|
+
const leaf = Object.keys(setOps).length === 1 ? leafFromSettingPath(settingPath, key) : key;
|
|
68
|
+
lines.push(`Set ${leaf} → ${formatScalar(value)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const addOps = ops.add;
|
|
72
|
+
if (addOps && typeof addOps === 'object') {
|
|
73
|
+
for (const [key, items] of Object.entries(addOps)) {
|
|
74
|
+
if (!Array.isArray(items))
|
|
75
|
+
continue;
|
|
76
|
+
lines.push(`Add to ${key}: ${formatListDeltaItems(items)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const removeOps = ops.remove;
|
|
80
|
+
if (removeOps && typeof removeOps === 'object') {
|
|
81
|
+
for (const [key, items] of Object.entries(removeOps)) {
|
|
82
|
+
if (Array.isArray(items)) {
|
|
83
|
+
lines.push(`Remove from ${key}: ${formatListDeltaItems(items)}`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (items && typeof items === 'object') {
|
|
87
|
+
lines.push(`Remove from ${key}: ${formatListDeltaItems(Object.keys(items))}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
function linesFromSqliteOp(check) {
|
|
94
|
+
const sqliteOp = check.sqlite_op;
|
|
95
|
+
if (!sqliteOp || typeof sqliteOp !== 'object')
|
|
96
|
+
return [];
|
|
97
|
+
const lines = [];
|
|
98
|
+
const settingPath = check.setting_path ?? '';
|
|
99
|
+
const updates = sqliteOp.updates;
|
|
100
|
+
if (updates && typeof updates === 'object') {
|
|
101
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
102
|
+
lines.push(`Set ${leafFromSettingPath(settingPath, key)} → ${formatScalar(value)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const unionAdd = sqliteOp.array_union_add;
|
|
106
|
+
if (Array.isArray(unionAdd) && unionAdd.length > 0) {
|
|
107
|
+
const target = sqliteOp.target_key ?? 'list';
|
|
108
|
+
lines.push(`Add to ${target}: ${formatListDeltaItems(unionAdd)}`);
|
|
109
|
+
}
|
|
110
|
+
return lines;
|
|
111
|
+
}
|
|
112
|
+
function linesFromCheck(check) {
|
|
113
|
+
const fromOps = linesFromOps(check);
|
|
114
|
+
if (fromOps.length > 0)
|
|
115
|
+
return fromOps;
|
|
116
|
+
return linesFromSqliteOp(check);
|
|
117
|
+
}
|
|
118
|
+
function linesFromFixSpec(spec) {
|
|
119
|
+
if (!spec)
|
|
120
|
+
return [];
|
|
121
|
+
if (Array.isArray(spec.checks) && spec.checks.length > 0) {
|
|
122
|
+
return spec.checks.flatMap((check) => linesFromCheck(check));
|
|
123
|
+
}
|
|
124
|
+
if (spec.setting_path || spec.ops) {
|
|
125
|
+
return linesFromCheck({ setting_path: spec.setting_path, ops: spec.ops });
|
|
126
|
+
}
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
/** Basename for macOS dialog copy (vscdb paths shortened). */
|
|
130
|
+
export function previewFileLabel(configFilePath) {
|
|
131
|
+
if (!configFilePath?.trim())
|
|
132
|
+
return 'config file';
|
|
133
|
+
const normalized = configFilePath.replace(/\\/g, '/');
|
|
134
|
+
const hashIdx = normalized.indexOf('#');
|
|
135
|
+
const pathPart = hashIdx >= 0 ? normalized.slice(0, hashIdx) : normalized;
|
|
136
|
+
const fragment = hashIdx >= 0 ? normalized.slice(hashIdx + 1) : '';
|
|
137
|
+
const segments = pathPart.split('/').filter(Boolean);
|
|
138
|
+
const base = segments[segments.length - 1] ?? pathPart;
|
|
139
|
+
if (base.endsWith('.vscdb') || base === 'state.vscdb') {
|
|
140
|
+
if (fragment && fragment !== 'composerState') {
|
|
141
|
+
const short = fragment.length > 28
|
|
142
|
+
? fragment.includes('/')
|
|
143
|
+
? (fragment.split('/').pop() ?? fragment)
|
|
144
|
+
: fragment
|
|
145
|
+
: fragment;
|
|
146
|
+
return `state.vscdb (${short})`;
|
|
147
|
+
}
|
|
148
|
+
return 'state.vscdb';
|
|
149
|
+
}
|
|
150
|
+
return base || 'config file';
|
|
151
|
+
}
|
|
152
|
+
export function collectRemediationPreviewLines(appliedViolations, remediations, maxLines = MAX_PREVIEW_LINES) {
|
|
153
|
+
const byUuid = new Map(remediations.map((row) => [row.uuid, row]));
|
|
154
|
+
const fileLabels = new Set();
|
|
155
|
+
const allLines = [];
|
|
156
|
+
for (const violation of appliedViolations) {
|
|
157
|
+
const instruction = byUuid.get(violation.uuid);
|
|
158
|
+
const spec = instruction ? remediationFixSpec(instruction) : null;
|
|
159
|
+
fileLabels.add(previewFileLabel(instruction?.config_file_path ?? violation.config_file_path ?? ''));
|
|
160
|
+
allLines.push(...linesFromFixSpec(spec));
|
|
161
|
+
}
|
|
162
|
+
const lines = allLines.slice(0, maxLines);
|
|
163
|
+
const hiddenCount = Math.max(0, allLines.length - lines.length);
|
|
164
|
+
return { fileLabels: [...fileLabels], lines, hiddenCount };
|
|
165
|
+
}
|
|
166
|
+
export function formatRemediationChangePreviewSection(appliedViolations, remediations) {
|
|
167
|
+
const { fileLabels, lines, hiddenCount } = collectRemediationPreviewLines(appliedViolations, remediations);
|
|
168
|
+
if (lines.length === 0)
|
|
169
|
+
return '';
|
|
170
|
+
const filePart = fileLabels.length === 1
|
|
171
|
+
? `Changes in ${fileLabels[0]}:`
|
|
172
|
+
: `Changes in ${fileLabels.slice(0, 2).join(', ')}${fileLabels.length > 2 ? ', …' : ''}:`;
|
|
173
|
+
const body = lines.map((line) => ` • ${truncateLine(line)}`).join('\n');
|
|
174
|
+
const tail = hiddenCount > 0
|
|
175
|
+
? `\n … and ${hiddenCount} more change${hiddenCount === 1 ? '' : 's'}`
|
|
176
|
+
: '';
|
|
177
|
+
return `${filePart}\n${body}${tail}`;
|
|
178
|
+
}
|
|
179
|
+
export function formatRemediationChangePreviewForApplied(appliedViolations) {
|
|
180
|
+
const { remediations } = readRemediationInstructionsFile();
|
|
181
|
+
return formatRemediationChangePreviewSection(appliedViolations, remediations);
|
|
182
|
+
}
|