log-llm-config 1.3.51 → 1.3.52

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.
@@ -6,7 +6,7 @@
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, pruneSatisfiedOneTimeRemediations, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
9
+ import { applyAutofixViolations, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
10
10
  import { existsSync, statSync } from 'node:fs';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { basename } from 'node:path';
@@ -23,6 +23,18 @@ function parseIde() {
23
23
  return 'claude';
24
24
  return 'cursor';
25
25
  }
26
+ function defaultAgentFromIde(ide) {
27
+ return ide === 'claude' ? 'claude' : 'cursor';
28
+ }
29
+ function parseAgent(ide) {
30
+ const eq = process.argv.find((a) => a.startsWith('--agent='));
31
+ if (eq) {
32
+ const normalized = normalizeAgentToken(eq.slice('--agent='.length));
33
+ if (normalized)
34
+ return normalized;
35
+ }
36
+ return defaultAgentFromIde(ide);
37
+ }
26
38
  function printAllow(ide) {
27
39
  if (ide === 'claude')
28
40
  console.log('{}');
@@ -94,8 +106,9 @@ export async function runCompliancePromptGateCli() {
94
106
  /** Exported for tests; runs when `dist/compliance_prompt_gate.js` is the process entry (argv[1]). */
95
107
  export async function runCompliancePromptGate() {
96
108
  const ide = parseIde();
109
+ const agent = parseAgent(ide);
97
110
  hookLogSessionBanner('compliance_prompt_gate (before submit)');
98
- const status = runLocalRemediationComplianceCheck();
111
+ const status = runLocalRemediationComplianceCheck(agent);
99
112
  if (status.status === 'fail' && status.violations.length > 0) {
100
113
  const staleMs = getManifestStalenessMs();
101
114
  if (staleMs !== null && staleMs > MANIFEST_STALE_MS) {
@@ -110,7 +123,7 @@ export async function runCompliancePromptGate() {
110
123
  printAllowWithAdvisory(ide, advisory);
111
124
  return;
112
125
  }
113
- const { fixed, appliedViolations = [], restartCommands, failedViolations, reportPromises, deferredSqlitePending, } = applyAutofixViolations(status.violations);
126
+ const { fixed, appliedViolations = [], restartCommands, failedViolations, reportPromises, deferredSqlitePending, } = applyAutofixViolations(status.violations, agent);
114
127
  hookRunLog(`compliance_prompt_gate: autofix result ide=${ide} fixed=${fixed} applied=${appliedViolations.length} failed=${failedViolations.length} restart_commands=${restartCommands.length} deferred_sqlite_pending=${deferredSqlitePending}`);
115
128
  if (fixed > 0) {
116
129
  // Wait for all server reports before exiting so the POST lands.
@@ -129,7 +142,7 @@ export async function runCompliancePromptGate() {
129
142
  console.log(blockPayload(ide, msg));
130
143
  return;
131
144
  }
132
- const recheck = runLocalRemediationComplianceCheck();
145
+ const recheck = runLocalRemediationComplianceCheck(agent);
133
146
  const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
134
147
  // Cursor: tolerate a failing recheck only when SQLite updates are deferred (apply after restart).
135
148
  // Claude Code: JSON remediations are written immediately; merge/verify timing can still leave the
@@ -188,7 +201,7 @@ export async function runCompliancePromptGate() {
188
201
  return;
189
202
  }
190
203
  // No violations: clean up satisfied one-time remediations so they don't linger locally forever.
191
- const pruned = pruneSatisfiedOneTimeRemediations();
204
+ const pruned = pruneSatisfiedOneTimeRemediations(agent);
192
205
  if (pruned.removed > 0) {
193
206
  await Promise.allSettled(pruned.reportPromises);
194
207
  }
@@ -20,6 +20,57 @@ import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js'
20
20
  import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
21
21
  import { sendConfigFile } from '../sender/batch_sender.js';
22
22
  import { readStoredAuthKey } from '../auth/auth_key_store.js';
23
+ /** Normalize manifest/env/CLI agent tokens to a known Agent, or '' if unrecognized. */
24
+ export function normalizeAgentToken(raw) {
25
+ if (typeof raw !== 'string')
26
+ return '';
27
+ const s = raw.trim().toLowerCase();
28
+ if (!s)
29
+ return '';
30
+ if (s === 'claude-desktop')
31
+ return 'claude_desktop';
32
+ if (s === 'claude_desktop')
33
+ return 'claude_desktop';
34
+ if (s === 'claude')
35
+ return 'claude';
36
+ if (s === 'cursor')
37
+ return 'cursor';
38
+ return '';
39
+ }
40
+ function currentAgentFromEnv() {
41
+ // Allow explicit override for debugging / special launchers.
42
+ const override = normalizeAgentToken(process.env.OPTIMUS_AGENT);
43
+ if (override)
44
+ return override;
45
+ // Backwards-compatible: hook wrappers set OPTIMUS_HOOK_TYPE to cursor|claude.
46
+ const hookType = normalizeAgentToken(process.env.OPTIMUS_HOOK_TYPE);
47
+ return hookType === 'cursor' ? 'cursor' : 'claude';
48
+ }
49
+ function targetsCurrentAgent(entry, agent) {
50
+ // Prefer top-level manifest field; fall back to embedded fix payload for older local files.
51
+ const embedded = (entry.fix && typeof entry.fix === 'object'
52
+ ? entry.fix.target_agent
53
+ : null) ??
54
+ (entry.compliance && typeof entry.compliance === 'object'
55
+ ? entry.compliance.target_agent
56
+ : null);
57
+ const t = entry.target_agent ?? embedded;
58
+ // Backwards compat: missing/empty target_agent means "applies to all agents".
59
+ if (t === null || t === undefined)
60
+ return true;
61
+ if (typeof t !== 'string') {
62
+ complianceRunnerDiag(`Ignoring remediation with non-string target_agent: ${String(t)}`);
63
+ return false;
64
+ }
65
+ if (!t.trim())
66
+ return true;
67
+ const normalized = normalizeAgentToken(t);
68
+ if (!normalized) {
69
+ complianceRunnerDiag(`Ignoring remediation with unknown target_agent: ${t}`);
70
+ return false;
71
+ }
72
+ return normalized === agent;
73
+ }
23
74
  // ---------------------------------------------------------------------------
24
75
  // Helpers
25
76
  // ---------------------------------------------------------------------------
@@ -133,21 +184,22 @@ function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
133
184
  * Evaluate current on-disk configs against remediation_instructions.json only (no server).
134
185
  * Returns status for prompt gating / callers; does not persist compliance.json.
135
186
  */
136
- export function runLocalRemediationComplianceCheck() {
187
+ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
137
188
  try {
138
189
  const { remediations: rawEntries } = readRemediationInstructionsFile();
139
190
  const entries = rawEntries;
140
- if (entries.length === 0) {
191
+ const scoped = entries.filter((e) => targetsCurrentAgent(e, agent));
192
+ if (scoped.length === 0) {
141
193
  hookRunLog('compliance_check: no remediation instructions present');
142
194
  return { status: 'ok', checked_at: new Date().toISOString(), manifest_uuids: [], violations: [] };
143
195
  }
144
- const uuids = entries.map((e) => e.uuid);
196
+ const uuids = scoped.map((e) => e.uuid);
145
197
  const violations = [];
146
198
  let skippedNoCompliance = 0;
147
199
  let skippedNonJson = 0;
148
200
  let skippedNoChecks = 0;
149
201
  let skippedUnreadable = 0;
150
- for (const entry of entries) {
202
+ for (const entry of scoped) {
151
203
  const compliance = entry.fix ?? entry.compliance;
152
204
  if (!compliance) {
153
205
  skippedNoCompliance++;
@@ -272,7 +324,7 @@ export function runLocalRemediationComplianceCheck() {
272
324
  return fallback;
273
325
  }
274
326
  }
275
- export function applyAutofixViolations(violations) {
327
+ export function applyAutofixViolations(violations, agent = 'cursor') {
276
328
  for (const v of violations) {
277
329
  if (!v.autofix_allowed) {
278
330
  logRemediationApplyFailure('autofix_skipped_not_allowed', {
@@ -405,11 +457,17 @@ export function applyAutofixViolations(violations) {
405
457
  hookRunLog('autofix: deferred vscdb — restart command runs apply_deferred_vscdb.js then open -a Cursor');
406
458
  }
407
459
  if (oneTimeAppliedUuids.size > 0) {
408
- const remaining = remediations.filter((r) => !oneTimeAppliedUuids.has(r.uuid));
460
+ // Only prune one-time remediations for this agent; keep other-agent remediations intact.
461
+ const remaining = remediations.filter((r) => {
462
+ if (!targetsCurrentAgent(r, agent))
463
+ return true;
464
+ return !oneTimeAppliedUuids.has(r.uuid);
465
+ });
409
466
  writeRemediationInstructionsFile({ remediations: remaining });
410
467
  hookRunLog(`autofix: removed ${oneTimeAppliedUuids.size} one-time remediation(s) from local store`);
411
468
  // Send a post-autofix heartbeat so the server sees the updated (reduced) UUID set immediately,
412
469
  // without waiting for the background runner (which may be locked out).
470
+ // Heartbeat should reflect the full local file state (including other-agent remediations that remain).
413
471
  const remainingUuids = remaining.map((r) => r.uuid);
414
472
  const hwHeartbeat = tryResolveHardwareUuid();
415
473
  if (hwHeartbeat) {
@@ -434,7 +492,7 @@ export function applyAutofixViolations(violations) {
434
492
  *
435
493
  * Returns number removed and any async report promises (heartbeat).
436
494
  */
437
- export function pruneSatisfiedOneTimeRemediations() {
495
+ export function pruneSatisfiedOneTimeRemediations(agent = 'cursor') {
438
496
  const { remediations } = readRemediationInstructionsFile();
439
497
  if (!Array.isArray(remediations) || remediations.length === 0)
440
498
  return { removed: 0, reportPromises: [] };
@@ -442,6 +500,10 @@ export function pruneSatisfiedOneTimeRemediations() {
442
500
  let removed = 0;
443
501
  for (const raw of remediations) {
444
502
  const inst = raw;
503
+ if (!targetsCurrentAgent(inst, agent)) {
504
+ remaining.push(raw);
505
+ continue;
506
+ }
445
507
  if (inst.is_enforced) {
446
508
  remaining.push(raw);
447
509
  continue;
@@ -508,9 +570,10 @@ export async function runComplianceCheck() {
508
570
  catch (err) {
509
571
  hookRunLog(`compliance_check: remediation_sync unexpected error: ${err instanceof Error ? err.message : String(err)}`);
510
572
  }
511
- const status = runLocalRemediationComplianceCheck();
573
+ const agent = currentAgentFromEnv();
574
+ const status = runLocalRemediationComplianceCheck(agent);
512
575
  if (status.status === 'ok' || status.violations.length === 0) {
513
- const pruned = pruneSatisfiedOneTimeRemediations();
576
+ const pruned = pruneSatisfiedOneTimeRemediations(agent);
514
577
  if (pruned.removed > 0) {
515
578
  await Promise.allSettled(pruned.reportPromises);
516
579
  }
@@ -368,6 +368,14 @@ const TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND = 'REPO_ROOT=$(git rev-pars
368
368
  "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; if [ -f \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\" ]; then echo deferred_restart:apply_via_monorepo_node; node \"\$REPO_ROOT/dev_npx_packages/log-llm-config/dist/apply_deferred_vscdb.js\"; APPLY_EC=\$?; else echo deferred_restart:apply_via_npx; cd \"\$REPO_ROOT\" && npx --yes log-llm-config@latest apply-deferred-vscdb; APPLY_EC=\$?; 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; 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";
369
369
  const TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND = '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';
370
370
  const TRUSTED_CLAUDE_RESTART_COMMAND = "nohup bash -c 'sleep 2 && open -a Claude' >/dev/null 2>&1 & pkill -x 'Claude'";
371
+ export function isClaudeRestartCommand(cmd) {
372
+ return cmd.trim() === TRUSTED_CLAUDE_RESTART_COMMAND;
373
+ }
374
+ export function isCursorRestartCommand(cmd) {
375
+ const t = cmd.trim();
376
+ return (t === TRUSTED_CURSOR_SQLITE_DEFERRED_RESTART_COMMAND ||
377
+ t === TRUSTED_CURSOR_JSON_SETTINGS_RESTART_COMMAND);
378
+ }
371
379
  /**
372
380
  * Autofix restart_command allowlist: manifest strings are attacker-controlled if JSON is tampered.
373
381
  * SQLite-deferred Cursor path always uses {@link buildDeferredCursorRestartCommand}; manifests may still
@@ -5,13 +5,26 @@
5
5
  import { spawn } from 'node:child_process';
6
6
  import { appendComplianceRunnerLine, hookRunLog } from './hook_logger.js';
7
7
  import { getDeferredVscdbRestartLogPath } from './management_storage.js';
8
- import { isTrustedRestartCommandForAutofix } from './remediation_sync.js';
8
+ import { normalizeAgentToken } from './compliance_check.js';
9
+ import { isClaudeRestartCommand, isCursorRestartCommand, isTrustedRestartCommandForAutofix } from './remediation_sync.js';
9
10
  const FALLBACK_PATH = '/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin';
10
11
  function isDeferredVscdbRestart(cmd) {
11
12
  return cmd.includes('OPTIMUS_DEFERRED_LOG') || cmd.includes('apply_deferred_vscdb');
12
13
  }
14
+ function currentAgentFromEnv() {
15
+ // Keep restart scoping consistent with remediation scoping:
16
+ // prefer OPTIMUS_AGENT override, then fall back to OPTIMUS_HOOK_TYPE.
17
+ const override = normalizeAgentToken(process.env.OPTIMUS_AGENT);
18
+ if (override === 'cursor')
19
+ return 'cursor';
20
+ if (override === 'claude' || override === 'claude_desktop')
21
+ return 'claude';
22
+ const hookType = normalizeAgentToken(process.env.OPTIMUS_HOOK_TYPE);
23
+ return hookType === 'cursor' ? 'cursor' : 'claude';
24
+ }
13
25
  /** Spawn each trusted command detached (same pattern as former compliance_prompt_gate fireRestartCommands). */
14
26
  export function executeTrustedRestartCommands(commands) {
27
+ const agent = currentAgentFromEnv();
15
28
  for (const cmd of commands) {
16
29
  const t = cmd.trim();
17
30
  if (!t)
@@ -22,6 +35,19 @@ export function executeTrustedRestartCommands(commands) {
22
35
  appendComplianceRunnerLine('RESTART', msg);
23
36
  continue;
24
37
  }
38
+ // Safety: never restart the wrong app for the current hook type.
39
+ if (agent === 'cursor' && isClaudeRestartCommand(t)) {
40
+ const msg = 'rejected_claude_restart_in_cursor_hook';
41
+ hookRunLog(`execute_trusted_restarts: ${msg}`);
42
+ appendComplianceRunnerLine('RESTART', msg);
43
+ continue;
44
+ }
45
+ if (agent === 'claude' && isCursorRestartCommand(t)) {
46
+ const msg = 'rejected_cursor_restart_in_claude_hook';
47
+ hookRunLog(`execute_trusted_restarts: ${msg}`);
48
+ appendComplianceRunnerLine('RESTART', msg);
49
+ continue;
50
+ }
25
51
  const deferred = isDeferredVscdbRestart(t);
26
52
  const detailPath = getDeferredVscdbRestartLogPath();
27
53
  if (deferred) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config",
3
- "version": "1.3.51",
3
+ "version": "1.3.52",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {