log-llm-config 1.4.4 → 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,39 @@
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
+ // Gate may have finalized+moved the session before this background runner started.
28
+ complianceLogPath = null;
29
+ }
30
+ }
31
+ else {
32
+ hookLogAppendSection('compliance_check_runner (background sync + check)');
33
+ }
6
34
  complianceRunnerRunnerLine('compliance_check_runner: start');
7
35
  try {
8
- await runComplianceCheck();
36
+ await runComplianceCheck(parseAgentArg());
9
37
  complianceRunnerRunnerLine('compliance_check_runner: finished ok');
10
38
  }
11
39
  catch (err) {
@@ -13,6 +41,12 @@ import { runComplianceCheck } from './log_config_files/runtime/compliance_check.
13
41
  complianceRunnerRunnerLine(`compliance_check_runner: uncaught error — ${err instanceof Error ? err.message : String(err)}`);
14
42
  complianceRunnerRunnerLine(`compliance_check_runner: stack_or_detail ${detail.slice(0, 4000)}`);
15
43
  process.stderr.write(`compliance_check_runner error: ${err instanceof Error ? err.message : String(err)}\n`);
44
+ if (complianceLogPath) {
45
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
46
+ }
16
47
  process.exit(1);
17
48
  }
49
+ if (complianceLogPath) {
50
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath);
51
+ }
18
52
  })();
@@ -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,23 @@ 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
+ // Post-restart verify + server reports BEFORE sync so a manifest UUID swap cannot delay
198
+ // verified / activity-log until the following prompt.
199
+ const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
200
+ if (postRestartVerify.outcomes.length > 0) {
201
+ await Promise.allSettled(postRestartVerify.reportPromises);
202
+ await flushRemediationOutcomeToServer(complianceLogPath);
203
+ hookRunLog(`compliance_prompt_gate: post_restart_verification count=${postRestartVerify.outcomes.length} quarantined=${postRestartVerify.outcomes.filter((o) => o.status === 'quarantined').length}`);
204
+ status = runLocalRemediationComplianceCheck(agent);
205
+ }
171
206
  const hw = tryResolveHardwareUuid();
172
207
  if (hw) {
173
208
  try {
@@ -180,13 +215,7 @@ export async function runCompliancePromptGate() {
180
215
  // Network or auth failure — fall back to local file state.
181
216
  }
182
217
  }
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
- }
218
+ status = runLocalRemediationComplianceCheck(agent);
190
219
  // Secondary-satisfied: primary checks failed but settings.json (or equiv) has the fix.
191
220
  // Upload those files fire-and-forget so the backend can resolve the finding immediately.
192
221
  for (const e of status.secondarySatisfied ?? []) {
@@ -238,15 +267,15 @@ export async function runCompliancePromptGate() {
238
267
  const recheck = runLocalRemediationComplianceCheck(agent);
239
268
  const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
240
269
  // 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.
270
+ // Claude / Copilot: JSON remediations are written immediately; merge/verify timing can still leave
271
+ // the in-process recheck red for the same UUID — allow in that case for immediate JSON agents.
243
272
  const appliedUuids = new Set(appliedViolations.map((v) => v.uuid));
244
- const claudeRecheckStaleAfterImmediateApply = ide === 'claude' &&
273
+ const claudeRecheckStaleAfterImmediateApply = (ide === 'claude' || ide === 'copilot') &&
245
274
  !recheckOk &&
246
275
  recheck.violations.length > 0 &&
247
276
  recheck.violations.every((v) => appliedUuids.has(v.uuid));
248
277
  if (claudeRecheckStaleAfterImmediateApply) {
249
- hookRunLog('compliance_prompt_gate: Claude — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)');
278
+ hookRunLog(`compliance_prompt_gate: ${ide} — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)`);
250
279
  }
251
280
  if (deferredSqlitePending || recheckOk || claudeRecheckStaleAfterImmediateApply) {
252
281
  const immediateVerified = restartCommands.length === 0 &&
@@ -255,6 +284,7 @@ export async function runCompliancePromptGate() {
255
284
  if (immediateVerified) {
256
285
  confirmAppliedAutofixVerified(appliedViolations, reportPromises);
257
286
  await Promise.allSettled(reportPromises);
287
+ await flushRemediationOutcomeToServer(complianceLogPath);
258
288
  }
259
289
  const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
260
290
  const changePreviewSuffix = changePreview ? `\n\n${changePreview}` : '';
@@ -262,9 +292,11 @@ export async function runCompliancePromptGate() {
262
292
  ? formatCursorRestartAutofixDialog(appliedViolations)
263
293
  : ide === 'claude'
264
294
  ? 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}`;
295
+ : ide === 'copilot'
296
+ ? formatCopilotAutofixDialog(appliedViolations)
297
+ : `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
298
+ .map((v) => autofixDialogLine(v))
299
+ .join('\n')}${changePreviewSuffix}`;
268
300
  const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
269
301
  if (restartCommands.length > 0)
270
302
  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');
@@ -1,11 +1,14 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { homedir } from 'node:os';
1
+ import { spawnSync } from 'node:child_process';
2
+ import { closeSync, mkdtempSync, openSync, readFileSync, rmSync } from 'node:fs';
3
+ import { homedir, tmpdir } from 'node:os';
3
4
  import { join } from 'node:path';
4
5
  export const SKILLS_CLI_FILE_TYPE = 'skills_cli_installed';
5
6
  export const SKILLS_CLI_INSTALLED_PATH = join(homedir(), '.agents', '.skills-cli-installed.json');
6
7
  /** Override the skills package spec, e.g. `skills@1.5.10`. Default uses whatever is on the machine. */
7
8
  export const SKILLS_CLI_NPX_PACKAGE_ENV = 'SKILLS_CLI_NPX_PACKAGE';
8
9
  const LIST_TIMEOUT_MS = 120_000;
10
+ /** stderr from npx/npm only; stdout is streamed to a temp file (avoids pipe/maxBuffer truncation). */
11
+ const LIST_STDERR_MAX_BUFFER = 16 * 1024 * 1024;
9
12
  function listExecEnv() {
10
13
  return {
11
14
  ...process.env,
@@ -16,20 +19,61 @@ function npxPackageSpec() {
16
19
  const fromEnv = (process.env[SKILLS_CLI_NPX_PACKAGE_ENV] || '').trim();
17
20
  return fromEnv || 'skills';
18
21
  }
19
- export function runSkillsListJson(args, cwd) {
20
- const out = execFileSync('npx', [npxPackageSpec(), 'list', ...args, '--json'], {
21
- encoding: 'utf8',
22
- cwd,
23
- timeout: LIST_TIMEOUT_MS,
24
- env: listExecEnv(),
25
- });
26
- const trimmed = out.trim();
22
+ /** Strip leading npx/npm noise and parse the skills `list --json` array payload. */
23
+ export function parseSkillsListJsonStdout(stdout) {
24
+ const trimmed = stdout.trim();
27
25
  if (!trimmed)
28
26
  return [];
29
- const parsed = JSON.parse(trimmed);
30
- if (!Array.isArray(parsed))
31
- return [];
32
- return parsed;
27
+ const start = trimmed.indexOf('[');
28
+ if (start < 0) {
29
+ throw new SyntaxError('skills list --json: no JSON array in stdout');
30
+ }
31
+ let end = trimmed.lastIndexOf(']');
32
+ let lastErr;
33
+ while (end > start) {
34
+ const slice = trimmed.slice(start, end + 1);
35
+ try {
36
+ const parsed = JSON.parse(slice);
37
+ if (!Array.isArray(parsed))
38
+ return [];
39
+ return parsed;
40
+ }
41
+ catch (err) {
42
+ lastErr = err instanceof SyntaxError ? err : new SyntaxError(String(err));
43
+ end = trimmed.lastIndexOf(']', end - 1);
44
+ }
45
+ }
46
+ throw lastErr ?? new SyntaxError('skills list --json: could not parse stdout');
47
+ }
48
+ export function runSkillsListJson(args, cwd) {
49
+ const tmpDir = mkdtempSync(join(tmpdir(), 'optimus-skills-list-'));
50
+ const outPath = join(tmpDir, 'stdout.json');
51
+ const outFd = openSync(outPath, 'w');
52
+ try {
53
+ const child = spawnSync('npx', [npxPackageSpec(), 'list', ...args, '--json'], {
54
+ cwd,
55
+ timeout: LIST_TIMEOUT_MS,
56
+ env: listExecEnv(),
57
+ stdio: ['ignore', outFd, 'pipe'],
58
+ maxBuffer: LIST_STDERR_MAX_BUFFER,
59
+ });
60
+ if (child.error)
61
+ throw child.error;
62
+ if (child.status !== 0) {
63
+ const stderr = (child.stderr ?? '').toString().trim();
64
+ throw new Error(stderr || `npx skills list exited ${child.status ?? 'unknown'}`);
65
+ }
66
+ }
67
+ finally {
68
+ closeSync(outFd);
69
+ }
70
+ try {
71
+ const out = readFileSync(outPath, 'utf8');
72
+ return parseSkillsListJsonStdout(out);
73
+ }
74
+ finally {
75
+ rmSync(tmpDir, { recursive: true, force: true });
76
+ }
33
77
  }
34
78
  function normalizeListRows(rows, scope) {
35
79
  const out = [];
@@ -60,39 +104,43 @@ export function formatSkillsListScopeForHookLog(scopeLabel, entries) {
60
104
  .join(' | ');
61
105
  return `skills_cli list ${scopeLabel}: ${entries.length} skill(s) — ${detail}`;
62
106
  }
107
+ function runSkillsListForScope(scopeLabel, args, projectRoot, logLine) {
108
+ try {
109
+ return runSkillsListJson(args, projectRoot);
110
+ }
111
+ catch (err) {
112
+ const msg = err instanceof Error ? err.message : String(err);
113
+ logLine(`skills_cli: list ${scopeLabel} --json failed: ${msg}`);
114
+ return [];
115
+ }
116
+ }
63
117
  export function collectSkillsCliInstalled(projectRoot, log) {
64
118
  const logLine = (message) => {
65
119
  log?.(message);
66
120
  };
67
- try {
68
- const packageSpec = npxPackageSpec();
69
- logLine(`skills_cli: npx ${packageSpec} list -g --json && list --json (projectRoot=${projectRoot})`);
70
- const globalRows = runSkillsListJson(['-g'], projectRoot);
71
- const projectRows = runSkillsListJson([], projectRoot);
72
- const global = normalizeListRows(globalRows, 'global');
73
- const project = normalizeListRows(projectRows, 'project');
74
- logLine(formatSkillsListScopeForHookLog('-g', global));
75
- logLine(formatSkillsListScopeForHookLog('project', project));
76
- const payload = {
77
- version: 1,
78
- skills_cli_version: packageSpec,
79
- generated_at: new Date().toISOString(),
80
- global,
81
- project,
82
- };
83
- if (payload.global.length === 0 && payload.project.length === 0) {
84
- logLine('skills_cli_installed: not uploaded (no global or project skills)');
85
- return null;
86
- }
87
- logLine(`skills_cli_installed: upload ${SKILLS_CLI_INSTALLED_PATH} global=${global.length} project=${project.length}`);
88
- return {
89
- file_type: SKILLS_CLI_FILE_TYPE,
90
- file_path: SKILLS_CLI_INSTALLED_PATH,
91
- raw_content: payload,
92
- };
93
- }
94
- catch (err) {
95
- logLine(`skills_cli: list --json failed: ${err instanceof Error ? err.message : String(err)}`);
121
+ const packageSpec = npxPackageSpec();
122
+ logLine(`skills_cli: npx ${packageSpec} list -g --json && list --json (projectRoot=${projectRoot})`);
123
+ const globalRows = runSkillsListForScope('-g', ['-g'], projectRoot, logLine);
124
+ const projectRows = runSkillsListForScope('project', [], projectRoot, logLine);
125
+ const global = normalizeListRows(globalRows, 'global');
126
+ const project = normalizeListRows(projectRows, 'project');
127
+ logLine(formatSkillsListScopeForHookLog('-g', global));
128
+ logLine(formatSkillsListScopeForHookLog('project', project));
129
+ const payload = {
130
+ version: 1,
131
+ skills_cli_version: packageSpec,
132
+ generated_at: new Date().toISOString(),
133
+ global,
134
+ project,
135
+ };
136
+ if (payload.global.length === 0 && payload.project.length === 0) {
137
+ logLine('skills_cli_installed: not uploaded (no global or project skills)');
96
138
  return null;
97
139
  }
140
+ logLine(`skills_cli_installed: upload ${SKILLS_CLI_INSTALLED_PATH} global=${global.length} project=${project.length}`);
141
+ return {
142
+ file_type: SKILLS_CLI_FILE_TYPE,
143
+ file_path: SKILLS_CLI_INSTALLED_PATH,
144
+ raw_content: payload,
145
+ };
98
146
  }
@@ -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
  }