log-llm-config-staging 1.4.6 → 1.4.9

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.
@@ -1,9 +1,38 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Runs after Cursor SIGKILL: applies queued state.vscdb patches from
4
- * ~/opt-ai-sec/management/optimus_deferred_vscdb_apply.json (see remediation_sync).
5
- */
2
+ // Runs after Cursor SIGKILL: applies queued state.vscdb patches, then immediately runs
3
+ // post-restart verification so the server gets `verified` without waiting for the next prompt.
6
4
  import { applyDeferredVscdbFromDisk } from './log_config_files/runtime/remediation_sync.js';
5
+ import { runPostApplyVerification } from './log_config_files/runtime/compliance_check.js';
6
+ import { hookRunLog } from './log_config_files/runtime/hook_logger.js';
7
+ import { finalizeAndUploadComplianceSessionLog, setActiveComplianceLogPath, } from './log_config_files/runtime/compliance_session_log.js';
8
+ const complianceLogPath = process.env.OPTIMUS_COMPLIANCE_LOG?.trim() || null;
9
+ if (complianceLogPath) {
10
+ setActiveComplianceLogPath(complianceLogPath);
11
+ }
7
12
  applyDeferredVscdbFromDisk()
8
- .then((ok) => process.exit(ok ? 0 : 1))
13
+ .then(async (ok) => {
14
+ if (ok) {
15
+ try {
16
+ const outcomes = await runPostApplyVerification();
17
+ if (outcomes.length > 0) {
18
+ hookRunLog(`apply_deferred_vscdb: post_apply_verification count=${outcomes.length} ` +
19
+ `verified=${outcomes.filter((o) => o.status === 'verified').length}`);
20
+ }
21
+ if (complianceLogPath) {
22
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath);
23
+ }
24
+ }
25
+ catch (e) {
26
+ hookRunLog(`apply_deferred_vscdb: post_apply_verification error: ${e instanceof Error ? e.message : String(e)}`);
27
+ if (complianceLogPath) {
28
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
29
+ }
30
+ }
31
+ }
32
+ else if (complianceLogPath) {
33
+ hookRunLog('apply_deferred_vscdb: apply failed');
34
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
35
+ }
36
+ process.exit(ok ? 0 : 1);
37
+ })
9
38
  .catch(() => process.exit(1));
@@ -1,11 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import { complianceRunnerRunnerLine, hookLogAppendSection } from './log_config_files/runtime/hook_logger.js';
3
- import { runComplianceCheck } from './log_config_files/runtime/compliance_check.js';
3
+ import { normalizeAgentToken, runComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
4
+ import { finalizeAndUploadComplianceSessionLog, initComplianceSessionForRunner, isFinalizeComplianceSessionArgv, isInflightComplianceLogPath, parseComplianceLogArg, resolveComplianceSessionLogPath, } from './log_config_files/runtime/compliance_session_log.js';
5
+ import { existsSync } from 'node:fs';
6
+ function parseAgentArg() {
7
+ const eq = process.argv.find((a) => a.startsWith('--agent='));
8
+ if (!eq)
9
+ return undefined;
10
+ const normalized = normalizeAgentToken(eq.slice('--agent='.length));
11
+ return normalized ? normalized : undefined;
12
+ }
4
13
  (async () => {
5
- hookLogAppendSection('compliance_check_runner (background sync + check)');
14
+ let complianceLogPath = parseComplianceLogArg(process.argv);
15
+ if (isFinalizeComplianceSessionArgv(process.argv)) {
16
+ if (complianceLogPath) {
17
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath);
18
+ }
19
+ process.exit(0);
20
+ }
21
+ if (complianceLogPath) {
22
+ const resolved = resolveComplianceSessionLogPath(complianceLogPath);
23
+ if (existsSync(resolved) && isInflightComplianceLogPath(resolved)) {
24
+ complianceLogPath = initComplianceSessionForRunner(resolved);
25
+ }
26
+ else {
27
+ complianceLogPath = null;
28
+ }
29
+ }
30
+ else {
31
+ hookLogAppendSection('compliance_check_runner (background sync + check)');
32
+ }
6
33
  complianceRunnerRunnerLine('compliance_check_runner: start');
7
34
  try {
8
- await runComplianceCheck();
35
+ await runComplianceCheck(parseAgentArg());
9
36
  complianceRunnerRunnerLine('compliance_check_runner: finished ok');
10
37
  }
11
38
  catch (err) {
@@ -13,6 +40,12 @@ import { runComplianceCheck } from './log_config_files/runtime/compliance_check.
13
40
  complianceRunnerRunnerLine(`compliance_check_runner: uncaught error — ${err instanceof Error ? err.message : String(err)}`);
14
41
  complianceRunnerRunnerLine(`compliance_check_runner: stack_or_detail ${detail.slice(0, 4000)}`);
15
42
  process.stderr.write(`compliance_check_runner error: ${err instanceof Error ? err.message : String(err)}\n`);
43
+ if (complianceLogPath) {
44
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
45
+ }
16
46
  process.exit(1);
17
47
  }
48
+ if (complianceLogPath) {
49
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath);
50
+ }
18
51
  })();
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Synchronous pre-prompt gate: local compliance only (stdout = single JSON line for IDE hooks).
4
- * stderr must stay clean; logs go via hookRunLog (file).
4
+ * stderr must stay clean; logs go via hookRunLog (compliance session file when --compliance-log is set).
5
5
  *
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).
@@ -11,6 +11,7 @@ import { isRemediationQuarantined } from './log_config_files/runtime/remediation
11
11
  import { existsSync, readFileSync, statSync } from 'node:fs';
12
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
+ import { finalizeAndUploadComplianceSessionLog, initComplianceSessionForGate, parseComplianceLogArg, } from './log_config_files/runtime/compliance_session_log.js';
14
15
  import { isThisCliModule } from './cli_invocation_match.js';
15
16
  import { ensureAuthentication } from './log_config_files/auth/auth_flow.js';
16
17
  import { sendConfigFile } from './log_config_files/sender/batch_sender.js';
@@ -19,17 +20,34 @@ import { syncRemediations } from './log_config_files/runtime/remediation_sync.js
19
20
  import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
20
21
  import { formatRemediationChangePreviewForApplied } from './remediation_change_preview.js';
21
22
  const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
23
+ /** After verified (or failed verify), push enforcement + session log immediately — do not wait for background runner. */
24
+ async function flushRemediationOutcomeToServer(complianceLogPath) {
25
+ if (!complianceLogPath)
26
+ return;
27
+ try {
28
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath);
29
+ }
30
+ catch (err) {
31
+ hookRunLog(`compliance_prompt_gate: session finalize/upload failed: ${err instanceof Error ? err.message : String(err)}`);
32
+ }
33
+ }
22
34
  function parseIde() {
23
35
  const eq = process.argv.find((a) => a.startsWith('--ide='));
24
36
  if (eq) {
25
37
  const v = eq.slice('--ide='.length).toLowerCase();
26
- return v === 'claude' ? 'claude' : 'cursor';
38
+ if (v === 'claude')
39
+ return 'claude';
40
+ if (v === 'copilot')
41
+ return 'copilot';
42
+ return 'cursor';
27
43
  }
28
44
  if (process.argv.includes('--claude'))
29
45
  return 'claude';
30
46
  return 'cursor';
31
47
  }
32
48
  function defaultAgentFromIde(ide) {
49
+ if (ide === 'copilot')
50
+ return 'copilot';
33
51
  return ide === 'claude' ? 'claude' : 'cursor';
34
52
  }
35
53
  function parseAgent(ide) {
@@ -42,13 +60,13 @@ function parseAgent(ide) {
42
60
  return defaultAgentFromIde(ide);
43
61
  }
44
62
  function printAllow(ide) {
45
- if (ide === 'claude')
63
+ if (ide === 'claude' || ide === 'copilot')
46
64
  console.log('{}');
47
65
  else
48
66
  console.log(JSON.stringify({ continue: true }));
49
67
  }
50
68
  function printAllowWithAdvisory(ide, advisoryMessage) {
51
- if (ide === 'claude') {
69
+ if (ide === 'claude' || ide === 'copilot') {
52
70
  console.log(JSON.stringify({ __optimus_advisory: true, advisory_message: advisoryMessage }));
53
71
  }
54
72
  else {
@@ -62,7 +80,7 @@ function printAllowWithAdvisory(ide, advisoryMessage) {
62
80
  function blockPayload(ide, violationMessage) {
63
81
  const prefix = 'Prompt blocked by Optimus: ';
64
82
  const text = prefix + violationMessage;
65
- if (ide === 'claude') {
83
+ if (ide === 'claude' || ide === 'copilot') {
66
84
  return JSON.stringify({ decision: 'block', reason: text, systemMessage: text });
67
85
  }
68
86
  return JSON.stringify({ continue: false, user_message: text });
@@ -116,6 +134,10 @@ function formatPreventiveAutofixDialog(appliedViolations, applyLine) {
116
134
  export function formatClaudeAutofixDialog(appliedViolations) {
117
135
  return formatPreventiveAutofixDialog(appliedViolations, 'Claude will now apply this policy to your environment.');
118
136
  }
137
+ /** Copilot dialog after enforced/preventive remediation is applied locally (no restart). */
138
+ export function formatCopilotAutofixDialog(appliedViolations) {
139
+ return formatPreventiveAutofixDialog(appliedViolations, 'Copilot will now apply this policy to your environment.');
140
+ }
119
141
  /** Cursor restart dialog after enforced/preventive remediation is applied locally. */
120
142
  export function formatCursorRestartAutofixDialog(appliedViolations) {
121
143
  return formatPreventiveAutofixDialog(appliedViolations, 'Cursor will now restart to apply this policy, and your context will be retained.');
@@ -164,10 +186,21 @@ export async function runCompliancePromptGateCli() {
164
186
  export async function runCompliancePromptGate() {
165
187
  const ide = parseIde();
166
188
  const agent = parseAgent(ide);
167
- hookLogSessionBanner('compliance_prompt_gate (before submit)');
168
- // Sync before checking so server-side changes (enforce/unenforce) propagate on every prompt
169
- // without waiting for the background runner. Race against a 3 s timeout so a slow or
170
- // unavailable server never delays the gate significantly.
189
+ let complianceLogPath = parseComplianceLogArg(process.argv);
190
+ if (complianceLogPath) {
191
+ complianceLogPath = initComplianceSessionForGate(complianceLogPath);
192
+ }
193
+ else {
194
+ hookLogSessionBanner('compliance_prompt_gate (before submit)');
195
+ }
196
+ let status = runLocalRemediationComplianceCheck(agent);
197
+ const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
198
+ if (postRestartVerify.outcomes.length > 0) {
199
+ await Promise.allSettled(postRestartVerify.reportPromises);
200
+ await flushRemediationOutcomeToServer(complianceLogPath);
201
+ hookRunLog(`compliance_prompt_gate: post_restart_verification count=${postRestartVerify.outcomes.length} quarantined=${postRestartVerify.outcomes.filter((o) => o.status === 'quarantined').length}`);
202
+ status = runLocalRemediationComplianceCheck(agent);
203
+ }
171
204
  const hw = tryResolveHardwareUuid();
172
205
  if (hw) {
173
206
  try {
@@ -180,13 +213,7 @@ export async function runCompliancePromptGate() {
180
213
  // Network or auth failure — fall back to local file state.
181
214
  }
182
215
  }
183
- const status = runLocalRemediationComplianceCheck(agent);
184
- // Always process pending post-restart rows (even when compliance is ok — apply may have stuck).
185
- const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
186
- if (postRestartVerify.outcomes.length > 0) {
187
- await Promise.allSettled(postRestartVerify.reportPromises);
188
- hookRunLog(`compliance_prompt_gate: post_restart_verification count=${postRestartVerify.outcomes.length} quarantined=${postRestartVerify.outcomes.filter((o) => o.status === 'quarantined').length}`);
189
- }
216
+ status = runLocalRemediationComplianceCheck(agent);
190
217
  // Secondary-satisfied: primary checks failed but settings.json (or equiv) has the fix.
191
218
  // Upload those files fire-and-forget so the backend can resolve the finding immediately.
192
219
  for (const e of status.secondarySatisfied ?? []) {
@@ -238,15 +265,15 @@ export async function runCompliancePromptGate() {
238
265
  const recheck = runLocalRemediationComplianceCheck(agent);
239
266
  const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
240
267
  // Cursor: tolerate a failing recheck only when SQLite updates are deferred (apply after restart).
241
- // Claude Code: JSON remediations are written immediately; merge/verify timing can still leave the
242
- // in-process recheck red for the same UUID — allow in that case only for --ide=claude.
268
+ // Claude / Copilot: JSON remediations are written immediately; merge/verify timing can still leave
269
+ // the in-process recheck red for the same UUID — allow in that case for immediate JSON agents.
243
270
  const appliedUuids = new Set(appliedViolations.map((v) => v.uuid));
244
- const claudeRecheckStaleAfterImmediateApply = ide === 'claude' &&
271
+ const claudeRecheckStaleAfterImmediateApply = (ide === 'claude' || ide === 'copilot') &&
245
272
  !recheckOk &&
246
273
  recheck.violations.length > 0 &&
247
274
  recheck.violations.every((v) => appliedUuids.has(v.uuid));
248
275
  if (claudeRecheckStaleAfterImmediateApply) {
249
- hookRunLog('compliance_prompt_gate: Claude — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)');
276
+ hookRunLog(`compliance_prompt_gate: ${ide} — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)`);
250
277
  }
251
278
  if (deferredSqlitePending || recheckOk || claudeRecheckStaleAfterImmediateApply) {
252
279
  const immediateVerified = restartCommands.length === 0 &&
@@ -255,6 +282,7 @@ export async function runCompliancePromptGate() {
255
282
  if (immediateVerified) {
256
283
  confirmAppliedAutofixVerified(appliedViolations, reportPromises);
257
284
  await Promise.allSettled(reportPromises);
285
+ await flushRemediationOutcomeToServer(complianceLogPath);
258
286
  }
259
287
  const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
260
288
  const changePreviewSuffix = changePreview ? `\n\n${changePreview}` : '';
@@ -262,9 +290,11 @@ export async function runCompliancePromptGate() {
262
290
  ? formatCursorRestartAutofixDialog(appliedViolations)
263
291
  : ide === 'claude'
264
292
  ? formatClaudeAutofixDialog(appliedViolations)
265
- : `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
266
- .map((v) => autofixDialogLine(v))
267
- .join('\n')}${changePreviewSuffix}`;
293
+ : ide === 'copilot'
294
+ ? formatCopilotAutofixDialog(appliedViolations)
295
+ : `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
296
+ .map((v) => autofixDialogLine(v))
297
+ .join('\n')}${changePreviewSuffix}`;
268
298
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
269
299
  if (restartCommands.length > 0)
270
300
  payload.restart_commands = restartCommands;
@@ -7,9 +7,14 @@
7
7
  import { readFileSync } from 'node:fs';
8
8
  import { isThisCliModule } from './cli_invocation_match.js';
9
9
  import { appendComplianceRunnerLine, hookRunLog } from './log_config_files/runtime/hook_logger.js';
10
+ import { setActiveComplianceLogPath } from './log_config_files/runtime/compliance_session_log.js';
10
11
  import { executeTrustedRestartCommands } from './log_config_files/runtime/trusted_restarts.js';
11
12
  /** Invoked by dist entrypoint or by `npx log-llm-config@latest execute-trusted-restarts` (cli.js dispatches here). */
12
13
  export function runExecuteTrustedRestartsFromStdin() {
14
+ const complianceLogPath = process.env.OPTIMUS_COMPLIANCE_LOG?.trim();
15
+ if (complianceLogPath) {
16
+ setActiveComplianceLogPath(complianceLogPath);
17
+ }
13
18
  let raw;
14
19
  try {
15
20
  raw = readFileSync(0, 'utf8');
@@ -293,6 +293,14 @@ function resolvePatternToTargets(pathPattern, pathType, fileType, projectRoot, h
293
293
  targets.push({ path: basePath + suffix, file_type: fileType, content_format: contentFormat ?? 'extensions_cache' });
294
294
  return targets;
295
295
  }
296
+ if (spec?.type === 'env_dir_file') {
297
+ const configuredDir = process.env[spec.env_var]?.trim();
298
+ const baseDir = configuredDir
299
+ ? configuredDir.replace(/^~\//, `${home}/`)
300
+ : join(home, spec.fallback_under_home.replace(/^~\//, ''));
301
+ pushTargetPaths(targets, join(baseDir, spec.filename), fileType, false, collectStyle, contentFormat, dirGlob);
302
+ return targets;
303
+ }
296
304
  if (norm.includes('**'))
297
305
  return expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentFormat, dirGlob, homeRecurseSkipDirs);
298
306
  if (norm.includes('*'))
@@ -19,6 +19,7 @@ import { resolveRemediationConfigPath } from './remediation_config_path.js';
19
19
  import { resolveOpsTargetPath } from './ops_target_path.js';
20
20
  import { isRemediationQuarantined, markRemediationApplyPendingVerification, markRemediationApplyVerified, processPendingPostRestartVerifications, readRemediationApplyTrackingFile, writeRemediationApplyTrackingFile, } from './remediation_apply_tracking.js';
21
21
  import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
22
+ import { isEnvBackedSecretValue, scanJsonForHardcodedSecrets, } from './secret_regex_scan.js';
22
23
  import { loadEndpointBase } from '../sender/endpoint_config.js';
23
24
  import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
24
25
  import { buildDeferredCursorRestartCommand, discoverAllWorkspaceVscdbs, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
@@ -188,6 +189,36 @@ function allowlistEntryMatchesRemoveToken(entry, token) {
188
189
  const pattern = new RegExp(`(^|[^a-z0-9_|])${escapeRegExp(t)}([^a-z0-9_|]|$)`);
189
190
  return pattern.test(valStr);
190
191
  }
192
+ /**
193
+ * MCP secret remediations deploy ops.set to ${env:KEY}. Local compliance passes when the
194
+ * user picks any env reference, not only the exact string from the manifest.
195
+ */
196
+ function remediationSetOpSatisfied(current, expected) {
197
+ if (deepEqual(current, expected))
198
+ return true;
199
+ if (typeof expected === 'string' &&
200
+ isEnvBackedSecretValue(expected) &&
201
+ isEnvBackedSecretValue(current)) {
202
+ return true;
203
+ }
204
+ return false;
205
+ }
206
+ function violationFromSecretScanFinding(entry, compliance, finding) {
207
+ return {
208
+ uuid: entry.uuid,
209
+ finding_formatted_id: compliance.finding_formatted_id,
210
+ setting_path: finding.path,
211
+ description: compliance.description,
212
+ finding_title: entry.finding_title,
213
+ finding_description: entry.finding_description,
214
+ policy_name: entry.policy_name,
215
+ severity: compliance.severity,
216
+ autofix_allowed: compliance.autofix_allowed,
217
+ config_file_path: entry.config_file_path,
218
+ expected_value: { op: 'secret_scan', path: finding.path, secret_type: finding.secretType },
219
+ message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Remove the hardcoded ${finding.secretType} from ${finding.path} in ${entry.config_file_path}`,
220
+ };
221
+ }
191
222
  function verifyOpsApplied(configJson, settingPath, ops) {
192
223
  const parts = settingPath.split('.');
193
224
  const leafKey = parts[parts.length - 1] ?? '';
@@ -201,8 +232,9 @@ function verifyOpsApplied(configJson, settingPath, ops) {
201
232
  if (Object.prototype.hasOwnProperty.call(set, k)) {
202
233
  const cur = getByPath(configJson, targetPath);
203
234
  const expected = set[k];
204
- if (!deepEqual(cur, expected))
235
+ if (!remediationSetOpSatisfied(cur, expected)) {
205
236
  return { ok: false, expected: { op: 'set', path: targetPath, value: expected } };
237
+ }
206
238
  continue;
207
239
  }
208
240
  const cur = getByPath(configJson, targetPath);
@@ -319,6 +351,14 @@ export function evaluateManifestEntryCompliance(entry) {
319
351
  if (!loaded.ok)
320
352
  return { violations: [] };
321
353
  const configJson = loaded.json;
354
+ if (compliance.requires_secret_scan === true) {
355
+ const secretFindings = scanJsonForHardcodedSecrets(configJson);
356
+ return secretFindings.length === 0
357
+ ? { violations: [] }
358
+ : {
359
+ violations: secretFindings.map((f) => violationFromSecretScanFinding(entry, compliance, f)),
360
+ };
361
+ }
322
362
  const entryViolations = [];
323
363
  for (const check of checks) {
324
364
  const effectivePath = canonicalComplianceSettingPath(entry.config_file_path, check);
@@ -523,6 +563,19 @@ export function reportPostRestartVerificationOutcomes(violations) {
523
563
  });
524
564
  return { outcomes, reportPromises };
525
565
  }
566
+ /**
567
+ * Run immediately after a deferred state.vscdb write lands on disk (post-restart, before the
568
+ * user's next prompt) so `pending_post_restart_verify` remediations are confirmed and reported
569
+ * to the server right away — the UI does not need to wait for another gate invocation.
570
+ */
571
+ export async function runPostApplyVerification(agent = 'cursor') {
572
+ const status = runLocalRemediationComplianceCheck(agent);
573
+ const { outcomes, reportPromises } = reportPostRestartVerificationOutcomes(status.violations);
574
+ if (outcomes.length > 0) {
575
+ await Promise.allSettled(reportPromises);
576
+ }
577
+ return outcomes;
578
+ }
526
579
  /**
527
580
  * Immediate autofix succeeded (inline recheck OK or Claude stale-recheck tolerance).
528
581
  * Clear pending verification locally and report verified so the next prompt does not POST
@@ -746,6 +799,16 @@ export function pruneSatisfiedOneTimeRemediations(agent = 'cursor') {
746
799
  continue;
747
800
  }
748
801
  const configJson = prLoaded.json;
802
+ if (spec?.requires_secret_scan === true) {
803
+ if (scanJsonForHardcodedSecrets(configJson).length === 0) {
804
+ removed++;
805
+ hookRunLog(`remediation_prune: satisfied secret-scan uuid=${inst.uuid} path=${inst.config_file_path}`);
806
+ }
807
+ else {
808
+ remaining.push(raw);
809
+ }
810
+ continue;
811
+ }
749
812
  // Only prune when every check is ops-based and currently satisfied.
750
813
  let okAll = true;
751
814
  for (const check of checks) {
@@ -880,8 +943,8 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
880
943
  * Apply (autofix) is intentionally deferred to the gate on the next prompt — this pass only downloads
881
944
  * a fresh manifest so the gate has up-to-date data when it runs.
882
945
  */
883
- export async function runComplianceCheck() {
884
- const agent = currentAgentFromEnv();
946
+ export async function runComplianceCheck(agentOverride) {
947
+ const agent = agentOverride ?? currentAgentFromEnv();
885
948
  try {
886
949
  await syncRemediations(loadEndpointBase(), resolveHardwareUuid());
887
950
  }
@@ -0,0 +1,372 @@
1
+ import { existsSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, appendFileSync, readFileSync, } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
5
+ const COMPLIANCE_SUBDIR = 'compliance';
6
+ const TIER_NOOP = 'noop';
7
+ const TIER_SUCCESS = 'success';
8
+ const TIER_ERROR = 'error';
9
+ const TIER_IN_PROCESS = 'in_process';
10
+ const TIER_DIRS = [TIER_NOOP, TIER_SUCCESS, TIER_ERROR];
11
+ const NOOP_MAX_AGE_MS = 30 * 60 * 1000;
12
+ const SUCCESS_ERROR_MAX_AGE_MS = 24 * 60 * 60 * 1000;
13
+ /** In-flight logs in compliance/in_process/ before finalize. */
14
+ const INFLIGHT_MAX_AGE_MS = 2 * 60 * 60 * 1000;
15
+ const LATEST_POINTER = 'latest';
16
+ const MAX_SESSION_LOG_UPLOAD_BYTES = 5 * 1024 * 1024;
17
+ let activeComplianceLogPath = null;
18
+ export function parseComplianceLogArg(argv) {
19
+ const eq = argv.find((a) => a.startsWith('--compliance-log='));
20
+ if (eq) {
21
+ const v = eq.slice('--compliance-log='.length).trim();
22
+ return v || null;
23
+ }
24
+ const idx = argv.indexOf('--compliance-log');
25
+ if (idx >= 0 && argv[idx + 1]) {
26
+ const v = argv[idx + 1].trim();
27
+ return v || null;
28
+ }
29
+ return null;
30
+ }
31
+ export function isFinalizeComplianceSessionArgv(argv) {
32
+ return argv.includes('--finalize-compliance-session');
33
+ }
34
+ export function getComplianceLogDir() {
35
+ return path.join(homedir(), OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_SUBDIR);
36
+ }
37
+ export function getActiveComplianceLogPath() {
38
+ return activeComplianceLogPath;
39
+ }
40
+ export function setActiveComplianceLogPath(logPath) {
41
+ activeComplianceLogPath = logPath ? path.resolve(logPath) : null;
42
+ }
43
+ function isTierDirName(name) {
44
+ return TIER_DIRS.includes(name);
45
+ }
46
+ function isManagedDirName(name) {
47
+ return isTierDirName(name) || name === TIER_IN_PROCESS;
48
+ }
49
+ function getInProcessComplianceLogDir() {
50
+ return path.join(getComplianceLogDir(), TIER_IN_PROCESS);
51
+ }
52
+ /**
53
+ * Convert legacy/root session paths into the explicit in_process/ location.
54
+ *
55
+ * Hooks may still pass ~/.../compliance/<timestamp>.log; future writes should live
56
+ * at ~/.../compliance/in_process/<timestamp>.log until finalize classifies them.
57
+ */
58
+ export function resolveComplianceSessionLogPath(logPath) {
59
+ const resolved = path.resolve(logPath);
60
+ if (path.dirname(resolved) === getComplianceLogDir()) {
61
+ return path.join(getInProcessComplianceLogDir(), path.basename(resolved));
62
+ }
63
+ return resolved;
64
+ }
65
+ /** Session log still in compliance/in_process/ (not yet finalized into a tier folder). */
66
+ export function isInflightComplianceLogPath(logPath) {
67
+ const resolved = resolveComplianceSessionLogPath(logPath);
68
+ return path.dirname(resolved) === getInProcessComplianceLogDir();
69
+ }
70
+ export function readLatestComplianceLogPointer() {
71
+ const pointer = path.join(getComplianceLogDir(), LATEST_POINTER);
72
+ if (!existsSync(pointer))
73
+ return null;
74
+ try {
75
+ const p = readFileSync(pointer, 'utf8').trim();
76
+ return p || null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function pruneLogFilesInDir(dir, maxAgeMs, exceptPath) {
83
+ if (!existsSync(dir))
84
+ return 0;
85
+ const cutoff = Date.now() - maxAgeMs;
86
+ let removed = 0;
87
+ for (const name of readdirSync(dir)) {
88
+ if (!name.endsWith('.log'))
89
+ continue;
90
+ const full = path.join(dir, name);
91
+ if (exceptPath && full === exceptPath)
92
+ continue;
93
+ try {
94
+ const st = statSync(full);
95
+ if (st.mtimeMs < cutoff) {
96
+ unlinkSync(full);
97
+ removed += 1;
98
+ }
99
+ }
100
+ catch {
101
+ // best-effort
102
+ }
103
+ }
104
+ return removed;
105
+ }
106
+ export function pruneComplianceSessionLogs(exceptPath) {
107
+ const base = getComplianceLogDir();
108
+ const keep = exceptPath ? path.resolve(exceptPath) : null;
109
+ let removed = 0;
110
+ removed += pruneLogFilesInDir(path.join(base, TIER_NOOP), NOOP_MAX_AGE_MS, keep);
111
+ removed += pruneLogFilesInDir(path.join(base, TIER_SUCCESS), SUCCESS_ERROR_MAX_AGE_MS, keep);
112
+ removed += pruneLogFilesInDir(path.join(base, TIER_ERROR), SUCCESS_ERROR_MAX_AGE_MS, keep);
113
+ removed += pruneLogFilesInDir(path.join(base, TIER_IN_PROCESS), INFLIGHT_MAX_AGE_MS, keep);
114
+ if (!existsSync(base))
115
+ return removed;
116
+ for (const name of readdirSync(base)) {
117
+ if (name === LATEST_POINTER || isManagedDirName(name))
118
+ continue;
119
+ if (!name.endsWith('.log'))
120
+ continue;
121
+ const full = path.join(base, name);
122
+ if (keep && full === keep)
123
+ continue;
124
+ try {
125
+ const st = statSync(full);
126
+ if (st.mtimeMs < Date.now() - INFLIGHT_MAX_AGE_MS) {
127
+ unlinkSync(full);
128
+ removed += 1;
129
+ }
130
+ }
131
+ catch {
132
+ // best-effort
133
+ }
134
+ }
135
+ return removed;
136
+ }
137
+ /**
138
+ * Classify a compliance session for finalize + optional server upload.
139
+ *
140
+ * Only two outcomes are uploaded (success / error): remediation **succeeded** or **failed**.
141
+ * Policy violations, in-flight restart, and sync-only sessions are noop (local retention only).
142
+ */
143
+ export function classifyComplianceSessionLog(content) {
144
+ const text = content.trim();
145
+ if (!text)
146
+ return TIER_NOOP;
147
+ if (/remediation_tracking: verified uuid=/.test(text)) {
148
+ return TIER_SUCCESS;
149
+ }
150
+ const remediationFailedPatterns = [
151
+ /REMEDIATION APPLY FAILURE/,
152
+ /compliance_check_runner: uncaught error/,
153
+ /remediation_tracking: verification_failed uuid=/,
154
+ /post_restart_verification count=\d+ quarantined=[1-9]/,
155
+ /enforceRemediation.*failed/i,
156
+ ];
157
+ if (remediationFailedPatterns.some((p) => p.test(text))) {
158
+ return TIER_ERROR;
159
+ }
160
+ return TIER_NOOP;
161
+ }
162
+ /** Keep at compliance/ root until verify or remediation failure is recorded in the session. */
163
+ export function isRemediationCycleInflight(content) {
164
+ if (/remediation_tracking: verified uuid=/.test(content))
165
+ return false;
166
+ if (/remediation_tracking: verification_failed uuid=/.test(content))
167
+ return false;
168
+ if (/REMEDIATION APPLY FAILURE/.test(content))
169
+ return false;
170
+ if (/post_restart_verification count=\d+ quarantined=[1-9]/.test(content))
171
+ return false;
172
+ return /remediation_tracking: pending_post_restart_verify uuid=/.test(content);
173
+ }
174
+ function ensureComplianceLogDir() {
175
+ const dir = getComplianceLogDir();
176
+ if (!existsSync(dir))
177
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
178
+ for (const tier of [...TIER_DIRS, TIER_IN_PROCESS]) {
179
+ const sub = path.join(dir, tier);
180
+ if (!existsSync(sub))
181
+ mkdirSync(sub, { recursive: true, mode: 0o700 });
182
+ }
183
+ }
184
+ function writeLatestPointer(logPath) {
185
+ try {
186
+ ensureComplianceLogDir();
187
+ writeFileSync(path.join(getComplianceLogDir(), LATEST_POINTER), `${logPath}\n`, 'utf8');
188
+ }
189
+ catch {
190
+ // best-effort
191
+ }
192
+ }
193
+ function appendComplianceLine(logPath, line) {
194
+ try {
195
+ const dir = path.dirname(logPath);
196
+ if (!existsSync(dir))
197
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
198
+ appendFileSync(logPath, line, 'utf8');
199
+ }
200
+ catch {
201
+ // best-effort
202
+ }
203
+ }
204
+ export function complianceLogSessionBanner(logPath, label, mode) {
205
+ const resolved = path.resolve(logPath);
206
+ const ts = new Date().toISOString();
207
+ const banner = mode === 'replace'
208
+ ? `\n${'='.repeat(72)}\n${ts} session: ${label}\n${'='.repeat(72)}\n`
209
+ : `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
210
+ try {
211
+ const dir = path.dirname(resolved);
212
+ if (!existsSync(dir))
213
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
214
+ if (mode === 'replace') {
215
+ writeFileSync(resolved, banner, 'utf8');
216
+ }
217
+ else {
218
+ appendFileSync(resolved, banner, 'utf8');
219
+ }
220
+ writeLatestPointer(resolved);
221
+ }
222
+ catch {
223
+ // best-effort
224
+ }
225
+ }
226
+ /** Gate: prune old files, bind path, start or resume an inflight session file. */
227
+ export function initComplianceSessionForGate(logPath) {
228
+ const resolved = resolveComplianceSessionLogPath(logPath);
229
+ ensureComplianceLogDir();
230
+ const removed = pruneComplianceSessionLogs(resolved);
231
+ setActiveComplianceLogPath(resolved);
232
+ const resume = existsSync(resolved) && statSync(resolved).size > 0 && isInflightComplianceLogPath(resolved);
233
+ complianceLogSessionBanner(resolved, 'compliance_prompt_gate (before submit)', resume ? 'append' : 'replace');
234
+ if (!resume) {
235
+ writeLatestPointer(resolved);
236
+ }
237
+ if (removed > 0) {
238
+ appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/error 24h)\n`);
239
+ }
240
+ return resolved;
241
+ }
242
+ /** Runner: prune old files, bind path, append runner section to the gate session file. */
243
+ export function initComplianceSessionForRunner(logPath) {
244
+ const resolved = resolveComplianceSessionLogPath(logPath);
245
+ ensureComplianceLogDir();
246
+ const removed = pruneComplianceSessionLogs(resolved);
247
+ setActiveComplianceLogPath(resolved);
248
+ complianceLogSessionBanner(resolved, 'compliance_check_runner (background sync + check)', 'append');
249
+ if (removed > 0) {
250
+ appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/error 24h)\n`);
251
+ }
252
+ return resolved;
253
+ }
254
+ /**
255
+ * Move session log from compliance/ root into noop|success|error/ and prune tiers.
256
+ * No-op if file missing or already finalized.
257
+ */
258
+ export function finalizeComplianceSessionLog(logPath, forcedTier) {
259
+ const resolved = resolveComplianceSessionLogPath(logPath);
260
+ if (!existsSync(resolved))
261
+ return null;
262
+ const parentName = path.basename(path.dirname(resolved));
263
+ if (isTierDirName(parentName)) {
264
+ return { tier: parentName, logPath: resolved, moved: false };
265
+ }
266
+ const content = readFileSync(resolved, 'utf8');
267
+ if (isRemediationCycleInflight(content)) {
268
+ return null;
269
+ }
270
+ const tier = forcedTier ?? classifyComplianceSessionLog(content);
271
+ ensureComplianceLogDir();
272
+ const dest = path.join(getComplianceLogDir(), tier, path.basename(resolved));
273
+ if (resolved !== dest) {
274
+ if (existsSync(dest))
275
+ unlinkSync(dest);
276
+ renameSync(resolved, dest);
277
+ }
278
+ setActiveComplianceLogPath(null);
279
+ writeLatestPointer(dest);
280
+ pruneComplianceSessionLogs(dest);
281
+ return { tier, logPath: dest, moved: true };
282
+ }
283
+ export function appendToActiveComplianceLog(message) {
284
+ const logPath = activeComplianceLogPath;
285
+ if (!logPath)
286
+ return;
287
+ try {
288
+ appendComplianceLine(logPath, `${new Date().toISOString()} ${message}\n`);
289
+ }
290
+ catch {
291
+ // best-effort
292
+ }
293
+ }
294
+ export function readComplianceSessionLog(logPath) {
295
+ try {
296
+ if (!existsSync(logPath))
297
+ return '';
298
+ return readFileSync(logPath, 'utf8');
299
+ }
300
+ catch {
301
+ return '';
302
+ }
303
+ }
304
+ function appendLineToSessionLogFile(logPath, message) {
305
+ try {
306
+ appendComplianceLine(logPath, `${new Date().toISOString()} ${message}\n`);
307
+ }
308
+ catch {
309
+ // best-effort
310
+ }
311
+ }
312
+ /**
313
+ * Upload full session log to the server (success and error tiers only, first finalize only).
314
+ */
315
+ export async function uploadComplianceSessionLogFromFinalize(result) {
316
+ if (result.tier !== TIER_SUCCESS && result.tier !== TIER_ERROR)
317
+ return;
318
+ if (!result.moved)
319
+ return;
320
+ const logText = readComplianceSessionLog(result.logPath);
321
+ if (!logText.trim())
322
+ return;
323
+ const logBytes = Buffer.byteLength(logText, 'utf8');
324
+ if (logBytes > MAX_SESSION_LOG_UPLOAD_BYTES) {
325
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: skipped (${logBytes} bytes exceeds ${MAX_SESSION_LOG_UPLOAD_BYTES})`);
326
+ return;
327
+ }
328
+ const { readStoredAuthKey } = await import('../auth/auth_key_store.js');
329
+ const { tryResolveHardwareUuid } = await import('./hardware_uuid.js');
330
+ const { loadEndpointBase } = await import('../sender/endpoint_config.js');
331
+ const { createSignature } = await import('../sender/signing.js');
332
+ const { buildApiUrl } = await import('../../endpoint_client/registry_api.js');
333
+ const { executeBody } = await import('../../endpoint_client/http_transport.js');
334
+ const authKey = readStoredAuthKey();
335
+ const hardwareUuid = tryResolveHardwareUuid();
336
+ if (!authKey || !hardwareUuid) {
337
+ appendLineToSessionLogFile(result.logPath, 'compliance_session_upload: skipped (auth key or hardware UUID unavailable)');
338
+ return;
339
+ }
340
+ const sessionFilename = path.basename(result.logPath);
341
+ const outcome = result.tier;
342
+ const signingPayload = {
343
+ hardware_uuid: hardwareUuid,
344
+ outcome,
345
+ session_filename: sessionFilename,
346
+ log_text: logText,
347
+ };
348
+ const signature = createSignature(signingPayload, authKey.key);
349
+ const body = JSON.stringify({ ...signingPayload, signature });
350
+ const url = buildApiUrl(loadEndpointBase(), '/endpoint_security/api/remediation-activity-log/');
351
+ try {
352
+ const { statusCode } = await executeBody(url, 'POST', body, 30_000);
353
+ if (statusCode === 200) {
354
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: ok outcome=${outcome} bytes=${logBytes}`);
355
+ }
356
+ else {
357
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: server status=${statusCode}`);
358
+ }
359
+ }
360
+ catch (err) {
361
+ const msg = err instanceof Error ? err.message : String(err);
362
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: failed ${msg}`);
363
+ }
364
+ }
365
+ /** Finalize session file into success|error|noop tier, then upload when success or error. */
366
+ export async function finalizeAndUploadComplianceSessionLog(logPath, forcedTier) {
367
+ const result = finalizeComplianceSessionLog(logPath, forcedTier);
368
+ if (result) {
369
+ await uploadComplianceSessionLogFromFinalize(result);
370
+ }
371
+ return result;
372
+ }
@@ -1,8 +1,11 @@
1
1
  import { existsSync, mkdirSync, appendFileSync, writeFileSync, statSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
4
+ import { getActiveComplianceLogPath } from './compliance_session_log.js';
4
5
  const HOOK_LOG_FILENAME = 'hook_log.txt';
5
6
  const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
7
+ const COMPLIANCE_SUBDIR = 'compliance';
8
+ const COMPLIANCE_FALLBACK_TIER = 'noop';
6
9
  /** Append-only: remediation verify/apply failures (not cleared per hook session). */
7
10
  const REMEDIATION_APPLY_FAILURES_FILENAME = 'remediation_apply_failures.log';
8
11
  /** Hard cap so a single upload/sync session cannot grow hook_log.txt without bound. */
@@ -17,7 +20,7 @@ function getComplianceRunnerLogPath() {
17
20
  const homeDir = process.env.HOME || process.env.USERPROFILE;
18
21
  if (!homeDir)
19
22
  return null;
20
- return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_RUNNER_LOG_FILENAME);
23
+ return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_SUBDIR, COMPLIANCE_FALLBACK_TIER, COMPLIANCE_RUNNER_LOG_FILENAME);
21
24
  }
22
25
  function getRemediationApplyFailuresLogPath() {
23
26
  const homeDir = process.env.HOME || process.env.USERPROFILE;
@@ -26,10 +29,21 @@ function getRemediationApplyFailuresLogPath() {
26
29
  return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, REMEDIATION_APPLY_FAILURES_FILENAME);
27
30
  }
28
31
  /**
29
- * Append one ISO-timestamped line to compliance_runner.log.
32
+ * Append one ISO-timestamped line to the active compliance session log.
33
+ * If no session is active, use compliance/noop/compliance_runner.log as a legacy fallback.
30
34
  * `level` examples: INFO, RUNNER, RESTART, FAIL
31
35
  */
32
36
  function appendComplianceRunnerLine(level, message) {
37
+ const sessionPath = getActiveComplianceLogPath();
38
+ if (sessionPath) {
39
+ try {
40
+ appendFileSync(sessionPath, `${new Date().toISOString()} [${level}] pid=${process.pid} ${message}\n`, 'utf8');
41
+ }
42
+ catch {
43
+ // best-effort
44
+ }
45
+ return;
46
+ }
33
47
  const logPath = getComplianceRunnerLogPath();
34
48
  if (!logPath)
35
49
  return;
@@ -58,7 +72,7 @@ function complianceRunnerRunnerLine(message) {
58
72
  }
59
73
  /**
60
74
  * Loud, append-only record when a remediation cannot be verified or applied. Writes
61
- * {@link REMEDIATION_APPLY_FAILURES_FILENAME} and mirrors the same block to compliance_runner.log;
75
+ * {@link REMEDIATION_APPLY_FAILURES_FILENAME} and mirrors the same block to the compliance log;
62
76
  * also appends a single summary line to hook_log.txt for the current session.
63
77
  */
64
78
  function logRemediationApplyFailure(context, fields) {
@@ -89,12 +103,18 @@ function logRemediationApplyFailure(context, fields) {
89
103
  const failPath = getRemediationApplyFailuresLogPath();
90
104
  if (failPath)
91
105
  writeFailAppend(failPath);
92
- const crPath = getComplianceRunnerLogPath();
93
- if (crPath) {
94
- const dir = path.dirname(crPath);
95
- if (!existsSync(dir))
96
- mkdirSync(dir, { recursive: true, mode: 0o700 });
97
- appendFileSync(crPath, block, 'utf8');
106
+ const activeCompliancePath = getActiveComplianceLogPath();
107
+ if (activeCompliancePath) {
108
+ writeFailAppend(activeCompliancePath);
109
+ }
110
+ else {
111
+ const crPath = getComplianceRunnerLogPath();
112
+ if (crPath) {
113
+ const dir = path.dirname(crPath);
114
+ if (!existsSync(dir))
115
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
116
+ appendFileSync(crPath, block, 'utf8');
117
+ }
98
118
  }
99
119
  }
100
120
  catch {
@@ -147,6 +167,18 @@ function hookLogSessionBanner(label) {
147
167
  * without replacing hook_log.txt.
148
168
  */
149
169
  function hookLogAppendSection(label) {
170
+ const compliancePath = getActiveComplianceLogPath();
171
+ if (compliancePath) {
172
+ try {
173
+ const ts = new Date().toISOString();
174
+ const banner = `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
175
+ appendFileSync(compliancePath, banner, 'utf8');
176
+ }
177
+ catch {
178
+ // best-effort
179
+ }
180
+ return;
181
+ }
150
182
  const logPath = getHookLogPath();
151
183
  if (!logPath)
152
184
  return;
@@ -167,8 +199,19 @@ function hookLogAppendSection(label) {
167
199
  function hookLogReplace() {
168
200
  hookLogSessionBanner('log_config_files (config upload)');
169
201
  }
170
- /** Append a timestamped line to hook_log.txt. */
202
+ /** Append a timestamped line to hook_log.txt or the active compliance session log. */
171
203
  function hookRunLog(message) {
204
+ const compliancePath = getActiveComplianceLogPath();
205
+ if (compliancePath) {
206
+ try {
207
+ const ts = new Date().toISOString();
208
+ appendFileSync(compliancePath, `${ts} ${message}\n`, 'utf8');
209
+ }
210
+ catch {
211
+ // best-effort
212
+ }
213
+ return;
214
+ }
172
215
  const logPath = getHookLogPath();
173
216
  if (!logPath)
174
217
  return;
@@ -58,6 +58,10 @@ export function clearRemediationApplyQuarantine(uuid) {
58
58
  writeRemediationApplyTrackingFile(file);
59
59
  hookRunLog(`remediation_tracking: quarantine_cleared uuid=${uuid}`);
60
60
  }
61
+ export function hasPendingPostRestartVerification() {
62
+ const file = readRemediationApplyTrackingFile();
63
+ return Object.values(file.entries).some((e) => e.pending_post_restart_verify === true);
64
+ }
61
65
  export function markRemediationApplyPendingVerification(uuid) {
62
66
  const file = readRemediationApplyTrackingFile();
63
67
  const prev = file.entries[uuid] ?? defaultEntry();
@@ -1460,7 +1460,10 @@ export function reportAutofixApplied(remediationUuid, result, details) {
1460
1460
  const body = JSON.stringify({ ...payload, signature });
1461
1461
  return executeBody(url, 'POST', body, 8000)
1462
1462
  .then(({ statusCode }) => {
1463
- if (statusCode !== 200) {
1463
+ if (statusCode === 200) {
1464
+ hookRunLog(`autofix_report: ok result=${result} uuid=${remediationUuid}`);
1465
+ }
1466
+ else {
1464
1467
  hookRunLog(`autofix_report: server returned ${statusCode} for uuid=${remediationUuid}`);
1465
1468
  }
1466
1469
  })
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Lightweight hardcoded-secret detection for local compliance (requires_secret_scan rows).
3
+ * Regex set aligned with policy_engine HardcodedSecretRule — not a full Secretlint/Gitleaks port.
4
+ */
5
+ /** Values that reference env vars are never treated as hardcoded secrets. */
6
+ export function isEnvBackedSecretValue(value) {
7
+ if (typeof value !== 'string')
8
+ return false;
9
+ const s = value.trim();
10
+ if (!s)
11
+ return false;
12
+ return s.includes('${env:') || s.includes('$env:') || s.includes('process.env.');
13
+ }
14
+ /** Provider / format patterns (order matters — first match wins). */
15
+ const KNOWN_SECRET_PATTERNS = [
16
+ { re: /sk-proj-[a-zA-Z0-9_-]{20,}/i, label: 'OpenAI API key' },
17
+ { re: /sk-ant-[a-zA-Z0-9_-]{20,}/i, label: 'Anthropic API key' },
18
+ { re: /sk-[a-zA-Z0-9]{20,}/i, label: 'API key (sk-...)' },
19
+ { re: /sk_(?:live|test|prod)_[a-zA-Z0-9]{20,}/i, label: 'Stripe secret key' },
20
+ { re: /pk_(?:live|test)_[a-zA-Z0-9]{20,}/i, label: 'Stripe publishable key' },
21
+ { re: /pk_[a-zA-Z0-9_]{20,}/i, label: 'API key (pk_...)' },
22
+ { re: /whsec_[a-zA-Z0-9]{20,}/i, label: 'Stripe webhook secret' },
23
+ { re: /rk_(?:live|test)_[a-zA-Z0-9]{20,}/i, label: 'Stripe restricted key' },
24
+ { re: /pplx-[a-zA-Z0-9-]{20,}/i, label: 'Perplexity API key' },
25
+ { re: /ghp_[a-zA-Z0-9]{36}/i, label: 'GitHub personal access token' },
26
+ { re: /gho_[a-zA-Z0-9]{36}/i, label: 'GitHub OAuth token' },
27
+ { re: /ghu_[a-zA-Z0-9]{36}/i, label: 'GitHub user-to-server token' },
28
+ { re: /ghs_[a-zA-Z0-9]{36}/i, label: 'GitHub server-to-server token' },
29
+ { re: /ghr_[a-zA-Z0-9]{76}/i, label: 'GitHub refresh token' },
30
+ { re: /github_pat_[a-zA-Z0-9_]{20,}/i, label: 'GitHub fine-grained PAT' },
31
+ { re: /glpat-[a-zA-Z0-9_-]{20,}/i, label: 'GitLab personal access token' },
32
+ { re: /glcbt-[a-zA-Z0-9_-]{20,}/i, label: 'GitLab CI job token' },
33
+ { re: /xoxb-[a-zA-Z0-9-]+/i, label: 'Slack bot token' },
34
+ { re: /xoxp-[a-zA-Z0-9-]+/i, label: 'Slack user token' },
35
+ { re: /xoxa-[a-zA-Z0-9-]+/i, label: 'Slack app-level token' },
36
+ { re: /xapp-[a-zA-Z0-9-]+/i, label: 'Slack app token' },
37
+ { re: /AKIA[0-9A-Z]{16}/i, label: 'AWS access key ID' },
38
+ { re: /ASIA[0-9A-Z]{16}/i, label: 'AWS temporary access key ID' },
39
+ { re: /AIza[0-9A-Za-z_-]{35}/i, label: 'Google API key' },
40
+ { re: /ya29\.[0-9A-Za-z_-]+/i, label: 'Google OAuth access token' },
41
+ { re: /AC[a-z0-9]{32}/i, label: 'Twilio account SID' },
42
+ { re: /SK[a-z0-9]{32}/i, label: 'Twilio API key' },
43
+ { re: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/i, label: 'SendGrid API key' },
44
+ { re: /key-[a-f0-9]{32}/i, label: 'Mailgun API key' },
45
+ { re: /npm_[a-zA-Z0-9]{36}/i, label: 'npm access token' },
46
+ { re: /pypi-[a-zA-Z0-9_-]{50,}/i, label: 'PyPI API token' },
47
+ { re: /discord(?:app)?\.com\/api\/webhooks\/\d+\/[a-zA-Z0-9_-]+/i, label: 'Discord webhook URL' },
48
+ { re: /hooks\.slack\.com\/services\/T[a-zA-Z0-9_]+\/B[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+/i, label: 'Slack webhook URL' },
49
+ { re: /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/i, label: 'JWT (JSON Web Token)' },
50
+ { re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/i, label: 'Private key block' },
51
+ { re: /sq0[a-zA-Z0-9_-]{20,}/i, label: 'Square access token' },
52
+ { re: /shpat_[a-fA-F0-9]{32}/i, label: 'Shopify access token' },
53
+ { re: /shpca_[a-fA-F0-9]{32}/i, label: 'Shopify custom app token' },
54
+ { re: /shpss_[a-fA-F0-9]{32}/i, label: 'Shopify shared secret' },
55
+ { re: /dop_v1_[a-f0-9]{64}/i, label: 'DigitalOcean personal access token' },
56
+ { re: /pat_[a-zA-Z0-9]{22}_[a-fA-F0-9]{59}/i, label: 'Atlassian API token' },
57
+ { re: /Bearer\s+[a-zA-Z0-9\-_.]{20,}/i, label: 'Bearer token' },
58
+ ];
59
+ const BASIC_AUTH_IN_URL = /[a-zA-Z0-9._%+-]+:[a-zA-Z0-9._%+-]+@/gi;
60
+ const GENERIC_TOKEN = /\b[a-zA-Z0-9]{32,}\b/g;
61
+ const GENERIC_KEY_HINT = /(?:api[_-]?key|app[_-]?key|secret|token|password|passwd|pass\b|(?<!o)auth|credential|private[_-]?key|access[_-]?key|master[_-]?key)/i;
62
+ function basicAuthMatchIsCredential(value, matchIndex) {
63
+ const before = value.slice(0, matchIndex);
64
+ const schemeIdx = before.lastIndexOf('://');
65
+ if (schemeIdx === -1)
66
+ return true;
67
+ const between = value.slice(schemeIdx + 3, matchIndex);
68
+ return !between.includes('/');
69
+ }
70
+ function looksLikeUrlContext(value) {
71
+ return value.includes(':') && (value.toLowerCase().includes('http') || value.includes('://'));
72
+ }
73
+ /**
74
+ * Scan one scalar config value. Returns secret type label or null if clean / env-backed.
75
+ */
76
+ export function scanScalarForHardcodedSecret(value, keyName) {
77
+ if (value == null)
78
+ return null;
79
+ const valueStr = String(value);
80
+ if (isEnvBackedSecretValue(valueStr))
81
+ return null;
82
+ for (const { re, label } of KNOWN_SECRET_PATTERNS) {
83
+ if (re.test(valueStr))
84
+ return label;
85
+ }
86
+ BASIC_AUTH_IN_URL.lastIndex = 0;
87
+ for (const match of valueStr.matchAll(BASIC_AUTH_IN_URL)) {
88
+ if (basicAuthMatchIsCredential(valueStr, match.index ?? 0)) {
89
+ return 'Basic authentication credentials';
90
+ }
91
+ }
92
+ if (!GENERIC_KEY_HINT.test(keyName) || looksLikeUrlContext(valueStr)) {
93
+ return null;
94
+ }
95
+ const genericMatches = valueStr.match(GENERIC_TOKEN) ?? [];
96
+ if (genericMatches.some((token) => token.length >= 40)) {
97
+ return 'Potential token/secret';
98
+ }
99
+ return null;
100
+ }
101
+ function collectScalarLeaves(value, path = '') {
102
+ if (value == null)
103
+ return [];
104
+ if (Array.isArray(value)) {
105
+ return value.flatMap((item, idx) => collectScalarLeaves(item, path ? `${path}.${idx}` : String(idx)));
106
+ }
107
+ if (typeof value === 'object') {
108
+ return Object.entries(value).flatMap(([key, nested]) => collectScalarLeaves(nested, path ? `${path}.${key}` : key));
109
+ }
110
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
111
+ return [];
112
+ }
113
+ const key = path.split('.').pop() ?? path;
114
+ return [{ path: path || '.', key, value }];
115
+ }
116
+ /** Walk parsed JSON and return all hardcoded-secret hits (JSON dot paths). */
117
+ export function scanJsonForHardcodedSecrets(configJson) {
118
+ const findings = [];
119
+ for (const leaf of collectScalarLeaves(configJson)) {
120
+ const secretType = scanScalarForHardcodedSecret(leaf.value, leaf.key);
121
+ if (secretType) {
122
+ findings.push({ path: leaf.path, key: leaf.key, secretType });
123
+ }
124
+ }
125
+ return findings;
126
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * In-process Secretlint scan for compliance rows with requires_secret_scan.
3
+ * Bundled as log-llm-config dependencies — no separate user install.
4
+ */
5
+ import { lintSource } from '@secretlint/core';
6
+ import { loadPackagesFromConfigDescriptor } from '@secretlint/config-loader';
7
+ let secretlintConfigPromise = null;
8
+ async function getSecretlintConfig() {
9
+ if (!secretlintConfigPromise) {
10
+ secretlintConfigPromise = loadPackagesFromConfigDescriptor({
11
+ configDescriptor: {
12
+ rules: [{ id: '@secretlint/secretlint-rule-preset-recommend' }],
13
+ },
14
+ }).then((loaded) => {
15
+ if (!loaded.ok) {
16
+ throw new Error('Failed to load Secretlint config');
17
+ }
18
+ return loaded.config;
19
+ });
20
+ }
21
+ return secretlintConfigPromise;
22
+ }
23
+ function collectStringLeaves(value, path = '') {
24
+ if (value == null)
25
+ return [];
26
+ if (Array.isArray(value)) {
27
+ return value.flatMap((item, idx) => collectStringLeaves(item, path ? `${path}.${idx}` : String(idx)));
28
+ }
29
+ if (typeof value === 'object') {
30
+ return Object.entries(value).flatMap(([key, nested]) => collectStringLeaves(nested, path ? `${path}.${key}` : key));
31
+ }
32
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
33
+ return [];
34
+ }
35
+ const key = path.split('.').pop() ?? path;
36
+ return [{ path: path || '.', key, value: String(value) }];
37
+ }
38
+ function secretTypeFromMessage(ruleId, message) {
39
+ const trimmed = message.trim();
40
+ return trimmed || ruleId || 'Secretlint detection';
41
+ }
42
+ /**
43
+ * Scan parsed JSON config for secrets using Secretlint (preset-recommend).
44
+ */
45
+ export async function scanJsonForSecretsWithSecretlint(configJson) {
46
+ const config = await getSecretlintConfig();
47
+ const leaves = collectStringLeaves(configJson);
48
+ const findings = [];
49
+ for (const leaf of leaves) {
50
+ const result = await lintSource({
51
+ source: {
52
+ filePath: `${leaf.path.replace(/\./g, '/')}.json`,
53
+ content: leaf.value,
54
+ ext: '.json',
55
+ contentType: 'text',
56
+ },
57
+ options: { config },
58
+ });
59
+ if (result.messages.length === 0)
60
+ continue;
61
+ const first = result.messages[0];
62
+ findings.push({
63
+ path: leaf.path,
64
+ key: leaf.key,
65
+ secretType: secretTypeFromMessage(first.ruleId ?? 'secret', String(first.message ?? '')),
66
+ });
67
+ }
68
+ return findings;
69
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
6
  import { appendComplianceRunnerLine, hookRunLog } from './hook_logger.js';
7
+ import { getActiveComplianceLogPath } from './compliance_session_log.js';
7
8
  import { getDeferredVscdbRestartLogPath } from './management_storage.js';
8
9
  import { normalizeAgentToken } from './compliance_check.js';
9
10
  import { isClaudeRestartCommand, isCursorRestartCommand, isTrustedRestartCommandForAutofix } from './remediation_sync.js';
@@ -84,10 +85,11 @@ export function executeTrustedRestartCommands(commands) {
84
85
  appendComplianceRunnerLine('RESTART', `spawn deferred_vscdb_restart sh -c (detail_log=${detailPath}) cwd_inherited_from_node`);
85
86
  }
86
87
  else {
87
- hookRunLog('execute_trusted_restarts: spawning trusted restart (see compliance_runner.log [RESTART])');
88
+ hookRunLog('execute_trusted_restarts: spawning trusted restart (see compliance session log [RESTART])');
88
89
  appendComplianceRunnerLine('RESTART', 'spawn trusted_restart sh -c (non-deferred)');
89
90
  }
90
91
  const projectRoot = resolveProjectRoot();
92
+ const complianceLogPath = getActiveComplianceLogPath();
91
93
  const child = spawn('sh', ['-c', t], {
92
94
  detached: true,
93
95
  stdio: 'ignore',
@@ -95,6 +97,7 @@ export function executeTrustedRestartCommands(commands) {
95
97
  env: envForRestartSpawn({
96
98
  ...process.env,
97
99
  CURSOR_PROJECT_DIR: process.env.CURSOR_PROJECT_DIR || projectRoot,
100
+ ...(complianceLogPath ? { OPTIMUS_COMPLIANCE_LOG: complianceLogPath } : {}),
98
101
  PATH: process.env.PATH && String(process.env.PATH).trim().length > 0
99
102
  ? process.env.PATH
100
103
  : FALLBACK_PATH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config-staging",
3
- "version": "1.4.6",
3
+ "version": "1.4.9",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {