log-llm-config-staging 1.3.48 → 1.3.51

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,10 +184,10 @@ 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
- const entries = rawEntries;
190
+ const entries = rawEntries.filter((e) => targetsCurrentAgent(e, agent));
140
191
  if (entries.length === 0) {
141
192
  hookRunLog('compliance_check: no remediation instructions present');
142
193
  return { status: 'ok', checked_at: new Date().toISOString(), manifest_uuids: [], violations: [] };
@@ -272,7 +323,7 @@ export function runLocalRemediationComplianceCheck() {
272
323
  return fallback;
273
324
  }
274
325
  }
275
- export function applyAutofixViolations(violations) {
326
+ export function applyAutofixViolations(violations, agent = 'cursor') {
276
327
  for (const v of violations) {
277
328
  if (!v.autofix_allowed) {
278
329
  logRemediationApplyFailure('autofix_skipped_not_allowed', {
@@ -294,7 +345,8 @@ export function applyAutofixViolations(violations) {
294
345
  reportPromises: [],
295
346
  };
296
347
  const { remediations } = readRemediationInstructionsFile();
297
- const byUuid = new Map(remediations.map((r) => [r.uuid, r]));
348
+ const scopedRemediations = remediations.filter((e) => targetsCurrentAgent(e, agent));
349
+ const byUuid = new Map(scopedRemediations.map((r) => [r.uuid, r]));
298
350
  let fixed = 0;
299
351
  const appliedViolations = [];
300
352
  const seen = new Set();
@@ -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;
@@ -502,15 +564,16 @@ export function pruneSatisfiedOneTimeRemediations() {
502
564
  * a fresh manifest so the gate has up-to-date data when it runs.
503
565
  */
504
566
  export async function runComplianceCheck() {
567
+ const agent = currentAgentFromEnv();
505
568
  try {
506
569
  await syncRemediations(loadEndpointBase(), resolveHardwareUuid());
507
570
  }
508
571
  catch (err) {
509
572
  hookRunLog(`compliance_check: remediation_sync unexpected error: ${err instanceof Error ? err.message : String(err)}`);
510
573
  }
511
- const status = runLocalRemediationComplianceCheck();
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-staging",
3
- "version": "1.3.48",
3
+ "version": "1.3.51",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {