log-llm-config-staging 1.3.44

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.
Files changed (51) hide show
  1. package/README.md +46 -0
  2. package/dist/apply_deferred_vscdb.js +8 -0
  3. package/dist/bootstrap_constants.js +5 -0
  4. package/dist/cli/bash_script_generator.js +95 -0
  5. package/dist/cli.js +103 -0
  6. package/dist/cli_invocation_match.js +28 -0
  7. package/dist/compliance_check_runner.js +17 -0
  8. package/dist/compliance_prompt_gate.js +197 -0
  9. package/dist/endpoint_client/http_transport.js +88 -0
  10. package/dist/endpoint_client/index.js +3 -0
  11. package/dist/endpoint_client/registry_api.js +41 -0
  12. package/dist/endpoint_client/startup_api.js +43 -0
  13. package/dist/endpoint_client/types.js +4 -0
  14. package/dist/execute_trusted_restarts.js +54 -0
  15. package/dist/log_config_files/auth/auth_flow.js +22 -0
  16. package/dist/log_config_files/auth/auth_key_store.js +14 -0
  17. package/dist/log_config_files/collection/config_collector.js +160 -0
  18. package/dist/log_config_files/collection/directory_collector.js +96 -0
  19. package/dist/log_config_files/collection/enrichment_helpers.js +53 -0
  20. package/dist/log_config_files/collection/file_type_rules.js +47 -0
  21. package/dist/log_config_files/collection/mcp_tool_collector.js +37 -0
  22. package/dist/log_config_files/collection/openclaw_helpers.js +55 -0
  23. package/dist/log_config_files/collection/plugin_collector.js +89 -0
  24. package/dist/log_config_files/collection/plugin_version_helpers.js +37 -0
  25. package/dist/log_config_files/index.js +19 -0
  26. package/dist/log_config_files/paths/path_constants_helpers.js +71 -0
  27. package/dist/log_config_files/paths/pattern_resolver.js +227 -0
  28. package/dist/log_config_files/readers/file_readers.js +69 -0
  29. package/dist/log_config_files/readers/vscdb_config_builder.js +146 -0
  30. package/dist/log_config_files/readers/vscdb_reader.js +247 -0
  31. package/dist/log_config_files/runtime/compliance_check.js +518 -0
  32. package/dist/log_config_files/runtime/hardware_uuid.js +36 -0
  33. package/dist/log_config_files/runtime/hook_logger.js +197 -0
  34. package/dist/log_config_files/runtime/main_runner.js +192 -0
  35. package/dist/log_config_files/runtime/management_storage.js +82 -0
  36. package/dist/log_config_files/runtime/remediation_config_path.js +90 -0
  37. package/dist/log_config_files/runtime/remediation_sync.js +1290 -0
  38. package/dist/log_config_files/runtime/sqlite_binary.js +92 -0
  39. package/dist/log_config_files/runtime/trusted_restarts.js +52 -0
  40. package/dist/log_config_files/sender/batch_sender.js +220 -0
  41. package/dist/log_config_files/sender/endpoint_config.js +24 -0
  42. package/dist/log_config_files/sender/signing.js +1 -0
  43. package/dist/log_sensitive_paths_audit.js +97 -0
  44. package/dist/log_uuid/auth_key_store.js +71 -0
  45. package/dist/log_uuid/hardware_uuid.js +35 -0
  46. package/dist/log_uuid/index.js +11 -0
  47. package/dist/log_uuid/log_uuid_helper.js +30 -0
  48. package/dist/log_uuid/startup_sender.js +74 -0
  49. package/dist/log_uuid/user_profile.js +178 -0
  50. package/dist/types/config_file_types.js +1 -0
  51. package/package.json +62 -0
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ ## log-llm-config
2
+
3
+ CLI helpers for Optimus Security startup workflows.
4
+
5
+ ### Prerequisites
6
+
7
+ - Node.js 18+
8
+ - `OPTIMUS_ENDPOINT` defined in `optimus.env` (e.g. `http://localhost:8080/endpoint_security/`)
9
+
10
+ ### Commands
11
+
12
+ #### `log_uuid`
13
+
14
+ Collects the machine hardware UUID (or uses the `OPTIMUS_HARDWARE_UUID` env variable), prints it, and POSTs a startup payload to `OPTIMUS_ENDPOINT/startup/`. If the server responds with a key, it stores it at `~/opt-ai-sec/management/auth_key.txt`.
15
+
16
+ ```bash
17
+ OPTIMUS_ENDPOINT=http://localhost:8080/endpoint_security/ \
18
+ npx --yes log-llm-config@latest log_uuid
19
+ ```
20
+
21
+ - **Optional flags** (for git context, similar to `optimus-init`):
22
+ - `--uuid <hw_uuid>` – override hardware UUID
23
+ - `--user <git_user>` – e.g. `$(git config user.name)`
24
+ - `--repo <git_remote>` – e.g. `$(git remote -v)`
25
+ - `--branch <git_branch>` – e.g. `$(git branch --show-current)`
26
+ - `--agent <agent_name>` – e.g. `Cursor`
27
+
28
+ #### `log-llm-config`
29
+
30
+ Legacy helper that logs local Optimus/Cursor configuration files to `configs.txt`.
31
+
32
+ You can also pass the UUID explicitly:
33
+
34
+ ```bash
35
+ npx --yes log-llm-config@latest log_uuid --uuid "DUMMY-LOCAL-UUID"
36
+ ```
37
+
38
+ ### Development
39
+
40
+ ```bash
41
+ npm install
42
+ npm run build
43
+ ```
44
+
45
+ > Proprietary package. Do not publish to public registries.
46
+
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Runs after Cursor SIGKILL: applies queued state.vscdb patches from
3
+ * ~/opt-ai-sec/management/optimus_deferred_vscdb_apply.json (see remediation_sync).
4
+ */
5
+ import { applyDeferredVscdbFromDisk } from './log_config_files/runtime/remediation_sync.js';
6
+ applyDeferredVscdbFromDisk()
7
+ .then((ok) => process.exit(ok ? 0 : 1))
8
+ .catch(() => process.exit(1));
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Bootstrap-level path constants — known before the API response is available.
3
+ * Used by auth, hook logging, and audit output. Do not add agent-specific paths here.
4
+ */
5
+ export const OPT_AI_SEC_MANAGEMENT_REL = 'opt-ai-sec/management';
@@ -0,0 +1,95 @@
1
+ import { readFileCollectionVscdbContract } from '../log_config_files/runtime/management_storage.js';
2
+ /** Reactive ItemTable key from backend-derived cache (written on log-config); same path as remediations. */
3
+ function readReactiveStorageItemKeyFromDisk() {
4
+ const k = readFileCollectionVscdbContract()?.reactive_storage_item_key;
5
+ return typeof k === 'string' && k.trim() !== '' ? k.trim() : null;
6
+ }
7
+ const fileCategories = [
8
+ { label: 'Cursor: mcp.json', targets: ['./.cursor/mcp.json', '$HOME/.cursor/mcp.json'] },
9
+ { label: 'Claude: .mcp.json', targets: ['./mcp.json', './.mcp.json', '/Library/Application Support/ClaudeCode/managed-mcp.json'] },
10
+ { label: 'Claude: settings.json', targets: ['./.claude/settings.json', './.claude/settings.local.json', '$HOME/.claude/settings.json', '/Library/Application Support/ClaudeCode/managed-settings.json'] },
11
+ { label: 'Cursor: hooks.json', targets: ['./.cursor/hooks.json', '$HOME/.cursor/hooks.json', '/Library/Application Support/Cursor/hooks.json'] },
12
+ { label: 'Cursor: User/settings.json (user-level)', targets: ['$HOME/Library/Application Support/Cursor/User/settings.json'] },
13
+ ];
14
+ function buildSqliteCategories() {
15
+ const key = readReactiveStorageItemKeyFromDisk();
16
+ if (!key)
17
+ return [];
18
+ return [
19
+ {
20
+ label: 'Cursor: state.vscdb (reactive blob / composerState)',
21
+ dbPath: '$HOME/Library/Application Support/Cursor/User/globalStorage/state.vscdb',
22
+ table: 'ItemTable',
23
+ key,
24
+ jsonPaths: [['composerState'], []],
25
+ },
26
+ ];
27
+ }
28
+ function buildFileCategoryLines(category) {
29
+ return [
30
+ `echo "===== ${category.label} ====="`,
31
+ 'files=(', ...category.targets.map((t) => ` "${t}"`), ')',
32
+ 'expanded_files=()', 'paths=()', 'display_paths=()',
33
+ 'for f in "${files[@]}"; do',
34
+ ' expanded=$(eval echo "$f")',
35
+ ' expanded_dir=$(dirname "$expanded")', ' expanded_base=$(basename "$expanded")',
36
+ ' abs_dir=$(cd "$expanded_dir" 2>/dev/null && pwd || echo "$expanded_dir")',
37
+ ' abs_path="$abs_dir/$expanded_base"', ' expanded_files+=("$abs_path")',
38
+ ' relative_path="$(to_relative "$abs_path")"', ' paths+=("$relative_path")', ' display_paths+=("$relative_path")',
39
+ 'done', 'echo ""',
40
+ 'if [ ${#paths[@]} -gt 0 ]; then', ' echo "Paths:"',
41
+ ' printf "%s\\n" "${paths[@]}" | awk \'!seen[$0]++\' | while IFS= read -r dir; do', ' echo "$dir"', ' done',
42
+ ' echo ""', 'else', ' echo "Paths: (none)"', 'fi', 'echo ""',
43
+ 'for idx in "${!expanded_files[@]}"; do', ' expanded="${expanded_files[$idx]}"', ' display="${display_paths[$idx]}"',
44
+ ' if [ -f "$expanded" ]; then',
45
+ ` echo "===== ${category.label}: $display ====="`, ' cat "$expanded"',
46
+ ' else', ` echo "===== ${category.label}: missing: $display ====="`, ' fi',
47
+ 'done', 'echo ""',
48
+ ];
49
+ }
50
+ function buildSqliteCategoryLines(category) {
51
+ const jsonPathsLiteral = JSON.stringify(category.jsonPaths);
52
+ const safeSqlKey = category.key.replace(/'/g, "''");
53
+ return [
54
+ `echo "===== ${category.label} ====="`, `db_path="${category.dbPath}"`,
55
+ 'expanded=$(eval echo "$db_path")',
56
+ 'if [ -f "$expanded" ]; then', ' if command -v sqlite3 >/dev/null 2>&1; then',
57
+ ' echo "===== $expanded ====="',
58
+ ` query_result=$(sqlite3 "$expanded" "SELECT value FROM ${category.table} WHERE key='${safeSqlKey}'")`,
59
+ ' if [ -n "$query_result" ]; then',
60
+ ` echo "===== key: ${category.key} ====="`,
61
+ ` LOG_CONFIG_JSON_VALUE="$query_result" python3 - <<'PY'`,
62
+ 'import json', 'import os', '',
63
+ 'raw = os.environ.get("LOG_CONFIG_JSON_VALUE", "")',
64
+ 'if not raw:', ' print("{}")', ' raise SystemExit',
65
+ `paths = ${jsonPathsLiteral}`,
66
+ 'try:', ' data = json.loads(raw)',
67
+ 'except json.JSONDecodeError as exc:', ' print(f"failed to parse JSON: {exc}")', ' raise SystemExit',
68
+ 'def walk(obj, path):', ' cur = obj',
69
+ ' for segment in path:', ' if isinstance(cur, dict):', ' cur = cur.get(segment)', ' else:', ' return None',
70
+ ' return cur',
71
+ 'selected_value = None', 'selected_label = None',
72
+ 'for path in paths:', ' value = walk(data, path)',
73
+ ' if value is not None:', ' selected_value = value', ' selected_label = ".".join(path) if path else "root"', ' break',
74
+ 'if selected_value is None:', ' print("{}")',
75
+ 'else:', ' print(f"[path: {selected_label}]")', ' print(json.dumps(selected_value, indent=2, sort_keys=True))',
76
+ 'PY',
77
+ ' else', ` echo "===== key not found: ${category.key} ====="`, ' fi',
78
+ ' else', ' echo "===== sqlite3 not found; skipping ====="', ' fi',
79
+ 'else', ' echo "===== missing: $expanded ====="', 'fi', 'echo ""',
80
+ ];
81
+ }
82
+ function renderBashScript() {
83
+ const scriptLines = [
84
+ '#!/usr/bin/env bash', 'set -euo pipefail',
85
+ 'output_file="$(pwd)/configs.txt"', ': > "$output_file"', 'exec >"$output_file"', 'exec 2>&1', '',
86
+ 'repo_root="$(pwd)"',
87
+ 'to_relative() {', ' local path="$1"', ' if [[ "$path" == "$repo_root"* ]]; then',
88
+ ' local suffix="${path#"$repo_root"}"', ' if [ -z "$suffix" ]; then', ' echo "<project_root>"',
89
+ ' else', ' echo "<project_root>${suffix}"', ' fi', ' else', ' echo "$path"', ' fi', '}', '',
90
+ ...fileCategories.flatMap(buildFileCategoryLines),
91
+ ...buildSqliteCategories().flatMap(buildSqliteCategoryLines),
92
+ ];
93
+ return scriptLines.join('\n');
94
+ }
95
+ export { renderBashScript };
package/dist/cli.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { renderBashScript } from './cli/bash_script_generator.js';
3
+ const args = process.argv.slice(2);
4
+ const main = async () => {
5
+ if (args[0] === 'log_uuid') {
6
+ parseAndSetLogUuidEnvVars();
7
+ await import('./log_uuid/index.js');
8
+ return;
9
+ }
10
+ if (args[0] === 'log_config_files') {
11
+ const { main, logSingleFile } = await import('./log_config_files/index.js');
12
+ const filePathArg = args.find((arg, idx) => idx > 0 && !arg.startsWith('-') && (arg.includes('/') || arg.endsWith('.json') || arg.endsWith('.md')));
13
+ if (filePathArg) {
14
+ const success = await logSingleFile(filePathArg);
15
+ try {
16
+ process.exit(success ? 0 : 1);
17
+ }
18
+ catch {
19
+ // process.exit may be mocked in tests (throws instead of exiting); allow process to continue
20
+ }
21
+ return;
22
+ }
23
+ await main();
24
+ return;
25
+ }
26
+ if (args[0] === 'log_sensitive_paths_audit') {
27
+ const { main } = await import('./log_sensitive_paths_audit.js');
28
+ await main();
29
+ return;
30
+ }
31
+ if (args[0] === 'compliance_prompt_gate') {
32
+ const { runCompliancePromptGateCli } = await import('./compliance_prompt_gate.js');
33
+ await runCompliancePromptGateCli();
34
+ process.exit(0);
35
+ return;
36
+ }
37
+ if (args[0] === 'compliance_check_runner') {
38
+ await import('./compliance_check_runner.js');
39
+ return;
40
+ }
41
+ // `npx log-llm-config@latest <name>` runs this file (default bin) with args[0] set — not the named bin file.
42
+ if (args[0] === 'execute-trusted-restarts') {
43
+ const { runExecuteTrustedRestartsFromStdin } = await import('./execute_trusted_restarts.js');
44
+ try {
45
+ runExecuteTrustedRestartsFromStdin();
46
+ }
47
+ catch (e) {
48
+ console.error(e instanceof Error ? e.message : String(e));
49
+ process.exit(1);
50
+ }
51
+ process.exit(0);
52
+ return;
53
+ }
54
+ if (args[0] === 'apply-deferred-vscdb') {
55
+ await import('./apply_deferred_vscdb.js');
56
+ return;
57
+ }
58
+ // No command matched — output legacy bash script
59
+ console.log(renderBashScript());
60
+ };
61
+ function parseAndSetLogUuidEnvVars() {
62
+ let uuidArg;
63
+ let userArg;
64
+ let repoArg;
65
+ let branchArg;
66
+ let agentArg;
67
+ for (let i = 1; i < args.length; i++) {
68
+ const arg = args[i];
69
+ if (arg === '--uuid' || arg === '-u') {
70
+ uuidArg = args[++i];
71
+ continue;
72
+ }
73
+ if (arg === '--user') {
74
+ userArg = args[++i];
75
+ continue;
76
+ }
77
+ if (arg === '--repo') {
78
+ repoArg = args[++i];
79
+ continue;
80
+ }
81
+ if (arg === '--branch') {
82
+ branchArg = args[++i];
83
+ continue;
84
+ }
85
+ if (arg === '--agent') {
86
+ agentArg = args[++i];
87
+ continue;
88
+ }
89
+ if (!arg.startsWith('-') && !uuidArg)
90
+ uuidArg = arg;
91
+ }
92
+ if (uuidArg)
93
+ process.env.OPTIMUS_HARDWARE_UUID = uuidArg;
94
+ if (userArg)
95
+ process.env.GITHUB_USER = userArg;
96
+ if (repoArg)
97
+ process.env.GITHUB_REPOSITORY = repoArg;
98
+ if (branchArg)
99
+ process.env.GITHUB_REF_NAME = branchArg;
100
+ if (agentArg)
101
+ process.env.OPTIMUS_AGENT = agentArg;
102
+ }
103
+ void main();
@@ -0,0 +1,28 @@
1
+ import { basename } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ function toFsPath(scriptPathOrFileUrl) {
4
+ if (scriptPathOrFileUrl.startsWith('file://')) {
5
+ return fileURLToPath(scriptPathOrFileUrl);
6
+ }
7
+ return scriptPathOrFileUrl;
8
+ }
9
+ /**
10
+ * Basename of the invoked script, normalized so npm `bin` names (hyphens) match compiled
11
+ * artifacts (underscores), e.g. `execute-trusted-restarts` → `execute_trusted_restarts`.
12
+ */
13
+ export function normalizedCliScriptBasename(scriptPathOrFileUrl) {
14
+ return basename(toFsPath(scriptPathOrFileUrl))
15
+ .replace(/\.[cm]?js$/, '')
16
+ .replace(/-/g, '_');
17
+ }
18
+ /**
19
+ * True when this process was started as the given module's CLI entrypoint.
20
+ * Handles npx bin shims (symlink paths) and hyphen vs underscore bin/file naming.
21
+ */
22
+ export function isThisCliModule(argv1, thisImportMetaUrl) {
23
+ if (!argv1)
24
+ return false;
25
+ const entry = normalizedCliScriptBasename(argv1);
26
+ const self = normalizedCliScriptBasename(thisImportMetaUrl);
27
+ return entry !== '' && entry === self;
28
+ }
@@ -0,0 +1,17 @@
1
+ import { complianceRunnerRunnerLine, hookLogAppendSection } from './log_config_files/runtime/hook_logger.js';
2
+ import { runComplianceCheck } from './log_config_files/runtime/compliance_check.js';
3
+ (async () => {
4
+ hookLogAppendSection('compliance_check_runner (background sync + check)');
5
+ complianceRunnerRunnerLine('compliance_check_runner: start');
6
+ try {
7
+ await runComplianceCheck();
8
+ complianceRunnerRunnerLine('compliance_check_runner: finished ok');
9
+ }
10
+ catch (err) {
11
+ const detail = err instanceof Error ? err.stack ?? err.message : String(err);
12
+ complianceRunnerRunnerLine(`compliance_check_runner: uncaught error — ${err instanceof Error ? err.message : String(err)}`);
13
+ complianceRunnerRunnerLine(`compliance_check_runner: stack_or_detail ${detail.slice(0, 4000)}`);
14
+ process.stderr.write(`compliance_check_runner error: ${err instanceof Error ? err.message : String(err)}\n`);
15
+ process.exit(1);
16
+ }
17
+ })();
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Synchronous pre-prompt gate: local compliance only (stdout = single JSON line for IDE hooks).
3
+ * stderr must stay clean; logs go via hookRunLog (file).
4
+ *
5
+ * When autofix returns restart_commands, this process does not spawn them — the shell hook pipes
6
+ * the same JSON line to execute_trusted_restarts (TS allowlist + spawn).
7
+ */
8
+ import { applyAutofixViolations, pruneSatisfiedOneTimeRemediations, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
9
+ import { existsSync, statSync } from 'node:fs';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { basename } from 'node:path';
12
+ import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
13
+ import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
14
+ const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
15
+ function parseIde() {
16
+ const eq = process.argv.find((a) => a.startsWith('--ide='));
17
+ if (eq) {
18
+ const v = eq.slice('--ide='.length).toLowerCase();
19
+ return v === 'claude' ? 'claude' : 'cursor';
20
+ }
21
+ if (process.argv.includes('--claude'))
22
+ return 'claude';
23
+ return 'cursor';
24
+ }
25
+ function printAllow(ide) {
26
+ if (ide === 'claude')
27
+ console.log('{}');
28
+ else
29
+ console.log(JSON.stringify({ continue: true }));
30
+ }
31
+ function printAllowWithAdvisory(ide, advisoryMessage) {
32
+ if (ide === 'claude') {
33
+ console.log(JSON.stringify({ __optimus_advisory: true, advisory_message: advisoryMessage }));
34
+ }
35
+ else {
36
+ console.log(JSON.stringify({
37
+ continue: true,
38
+ __optimus_advisory: true,
39
+ advisory_message: advisoryMessage,
40
+ }));
41
+ }
42
+ }
43
+ function blockPayload(ide, violationMessage) {
44
+ const prefix = 'Prompt blocked by Optimus: ';
45
+ const text = prefix + violationMessage;
46
+ if (ide === 'claude') {
47
+ return JSON.stringify({ decision: 'block', reason: text, systemMessage: text });
48
+ }
49
+ return JSON.stringify({ continue: false, user_message: text });
50
+ }
51
+ function getManifestStalenessMs() {
52
+ try {
53
+ const path = getRemediationInstructionsPath();
54
+ if (!existsSync(path))
55
+ return null;
56
+ const st = statSync(path);
57
+ return Date.now() - st.mtimeMs;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function isRunAsCliModule() {
64
+ const entry = process.argv[1];
65
+ if (!entry)
66
+ return false;
67
+ // Compare by basename only: npx bins are symlinks (no .js extension) while import.meta.url
68
+ // is the real resolved path with extension — full-path comparison reliably fails across setups.
69
+ const entryBase = basename(entry).replace(/\.[cm]?js$/, '');
70
+ const selfBase = basename(fileURLToPath(import.meta.url)).replace(/\.[cm]?js$/, '');
71
+ return entryBase !== '' && entryBase === selfBase;
72
+ }
73
+ /** Short line for success dialog: finding title/sentence from manifest, not per-check remediation technical text. */
74
+ function autofixDialogLine(v) {
75
+ const title = v.finding_title?.trim();
76
+ if (title)
77
+ return `• [${v.finding_formatted_id}] ${title}`;
78
+ const fd = v.finding_description?.trim();
79
+ if (fd)
80
+ return `• [${v.finding_formatted_id}] ${fd}`;
81
+ const d = v.description.trim();
82
+ const short = d.length > 160 ? `${d.slice(0, 157)}…` : d;
83
+ return `• [${v.finding_formatted_id}] ${short}`;
84
+ }
85
+ /**
86
+ * Entry when the default npm bin (`log-llm-config` / cli.js) dispatches
87
+ * `compliance_prompt_gate` — npx does not execute compliance_prompt_gate.js as argv[1], so
88
+ * {@link isRunAsCliModule} is false and the gate must be invoked explicitly from cli.ts.
89
+ */
90
+ export async function runCompliancePromptGateCli() {
91
+ await runCompliancePromptGate().catch(() => printAllow(parseIde()));
92
+ }
93
+ /** Exported for tests; runs when `dist/compliance_prompt_gate.js` is the process entry (argv[1]). */
94
+ export async function runCompliancePromptGate() {
95
+ const ide = parseIde();
96
+ hookLogSessionBanner('compliance_prompt_gate (before submit)');
97
+ const status = runLocalRemediationComplianceCheck();
98
+ if (status.status === 'fail' && status.violations.length > 0) {
99
+ const staleMs = getManifestStalenessMs();
100
+ if (staleMs !== null && staleMs > MANIFEST_STALE_MS) {
101
+ const staleDays = Math.floor(staleMs / (24 * 60 * 60 * 1000));
102
+ const advisory = `Remediation enforcement suspended: local remediation manifest is stale (${staleDays} days old). ` +
103
+ 'Please reconnect to sync latest policy state.';
104
+ logRemediationApplyFailure('prompt_gate_stale_manifest_advisory', {
105
+ reason: advisory,
106
+ stale_days: staleDays,
107
+ violation_count: status.violations.length,
108
+ });
109
+ printAllowWithAdvisory(ide, advisory);
110
+ return;
111
+ }
112
+ const { fixed, appliedViolations = [], restartCommands, failedViolations, reportPromises, deferredSqlitePending, } = applyAutofixViolations(status.violations);
113
+ if (fixed > 0) {
114
+ // Wait for all server reports before exiting so the POST lands.
115
+ await Promise.allSettled(reportPromises);
116
+ // Deferred SQLite can leave recheck failing until restart; that must not hide a separate
117
+ // failed autofix (e.g. JSON remediation failed while vscdb was only queued).
118
+ if (failedViolations.length > 0) {
119
+ const ids = failedViolations.map((v) => `[${v.finding_formatted_id}]`).join(', ');
120
+ const msg = `Auto-fix failed for ${ids} — please fix manually or contact your security team.\n\n${failedViolations[0]?.message ?? ''}`;
121
+ logRemediationApplyFailure('prompt_gate_block_autofix_failed', {
122
+ reason: 'autofix had fixed>0 path but failedViolations non-empty after apply',
123
+ ide,
124
+ failed_formatted_ids: failedViolations.map((v) => v.finding_formatted_id).join(', '),
125
+ detail: failedViolations[0]?.message ?? '',
126
+ });
127
+ console.log(blockPayload(ide, msg));
128
+ return;
129
+ }
130
+ const recheck = runLocalRemediationComplianceCheck();
131
+ const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
132
+ // Cursor: tolerate a failing recheck only when SQLite updates are deferred (apply after restart).
133
+ // Claude Code: JSON remediations are written immediately; merge/verify timing can still leave the
134
+ // in-process recheck red for the same UUID — allow in that case only for --ide=claude.
135
+ const appliedUuids = new Set(appliedViolations.map((v) => v.uuid));
136
+ const claudeRecheckStaleAfterImmediateApply = ide === 'claude' &&
137
+ !recheckOk &&
138
+ recheck.violations.length > 0 &&
139
+ recheck.violations.every((v) => appliedUuids.has(v.uuid));
140
+ if (claudeRecheckStaleAfterImmediateApply) {
141
+ hookRunLog('compliance_prompt_gate: Claude — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)');
142
+ }
143
+ if (deferredSqlitePending || recheckOk || claudeRecheckStaleAfterImmediateApply) {
144
+ const violationLabel = fixed === 1 ? 'policy violation' : 'policy violations';
145
+ const autofixMessage = `Optimus Labs auto-fixed ${fixed} ${violationLabel}:\n\n${appliedViolations
146
+ .map((v) => autofixDialogLine(v))
147
+ .join('\n')}`;
148
+ const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
149
+ if (restartCommands.length > 0)
150
+ payload.restart_commands = restartCommands;
151
+ console.log(JSON.stringify(payload));
152
+ // Restarts: always executed by optimus-compliance-check.sh via execute_trusted_restarts (TS allowlist + spawn).
153
+ return;
154
+ }
155
+ const msg = recheck.violations[0]?.message ?? 'A security policy violation has been detected.';
156
+ logRemediationApplyFailure('prompt_gate_block_recheck_failed', {
157
+ reason: 'after autofix, local compliance recheck still reports violations',
158
+ ide,
159
+ deferred_sqlite_pending: deferredSqlitePending,
160
+ first_uuid: recheck.violations[0]?.uuid ?? '',
161
+ message: msg,
162
+ });
163
+ console.log(blockPayload(ide, msg));
164
+ return;
165
+ }
166
+ if (failedViolations.length > 0) {
167
+ const ids = failedViolations.map((v) => `[${v.finding_formatted_id}]`).join(', ');
168
+ const msg = `Auto-fix failed for ${ids} — please fix manually or contact your security team.\n\n${failedViolations[0]?.message ?? ''}`;
169
+ logRemediationApplyFailure('prompt_gate_block_autofix_failed', {
170
+ reason: 'autofix failed (fixed=0 branch or partial failure)',
171
+ ide,
172
+ failed_formatted_ids: failedViolations.map((v) => v.finding_formatted_id).join(', '),
173
+ detail: failedViolations[0]?.message ?? '',
174
+ });
175
+ console.log(blockPayload(ide, msg));
176
+ return;
177
+ }
178
+ const msg = status.violations[0]?.message ?? 'A security policy violation has been detected.';
179
+ logRemediationApplyFailure('prompt_gate_block_violations_no_autofix_applied', {
180
+ reason: 'violations on submit but autofix did not succeed — see autofix_skipped_not_allowed, enforceRemediation, or compliance_check entries',
181
+ ide,
182
+ first_uuid: status.violations[0]?.uuid ?? '',
183
+ finding_formatted_id: status.violations[0]?.finding_formatted_id ?? '',
184
+ });
185
+ console.log(blockPayload(ide, msg));
186
+ return;
187
+ }
188
+ // No violations: clean up satisfied one-time remediations so they don't linger locally forever.
189
+ const pruned = pruneSatisfiedOneTimeRemediations();
190
+ if (pruned.removed > 0) {
191
+ await Promise.allSettled(pruned.reportPromises);
192
+ }
193
+ printAllow(ide);
194
+ }
195
+ if (isRunAsCliModule()) {
196
+ runCompliancePromptGateCli().finally(() => process.exit(0));
197
+ }
@@ -0,0 +1,88 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import { URL } from 'node:url';
4
+ /** Normalize localhost/::1 to 127.0.0.1 to avoid IPv6 resolution issues. */
5
+ function resolveHostname(raw) {
6
+ return raw === 'localhost' || raw === '::1' ? '127.0.0.1' : raw;
7
+ }
8
+ function buildGetOptions(url, timeoutMs) {
9
+ const isHttps = url.protocol === 'https:';
10
+ return {
11
+ hostname: resolveHostname(url.hostname),
12
+ port: url.port || (isHttps ? 443 : 80),
13
+ path: url.pathname + url.search,
14
+ method: 'GET',
15
+ timeout: timeoutMs,
16
+ };
17
+ }
18
+ function buildBodyOptions(url, payload, method, timeoutMs) {
19
+ const isHttps = url.protocol === 'https:';
20
+ return {
21
+ hostname: resolveHostname(url.hostname),
22
+ port: url.port || (isHttps ? 443 : 80),
23
+ path: `${url.pathname}${url.search}`,
24
+ method,
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ 'Content-Length': Buffer.byteLength(payload).toString(),
28
+ },
29
+ timeout: timeoutMs,
30
+ };
31
+ }
32
+ /** Execute a GET request. Returns statusCode=0 and empty body on network/timeout failure. */
33
+ export function executeGet(urlStr, timeoutMs) {
34
+ const url = new URL(urlStr);
35
+ const options = buildGetOptions(url, timeoutMs);
36
+ const transport = url.protocol === 'https:' ? https.request : http.request;
37
+ return new Promise((resolve) => {
38
+ const req = transport(options, (res) => {
39
+ let body = '';
40
+ res.setEncoding('utf8');
41
+ res.on('data', (chunk) => { body += chunk; });
42
+ res.on('end', () => resolve({ statusCode: res.statusCode ?? 0, body }));
43
+ });
44
+ req.on('error', () => resolve({ statusCode: 0, body: '' }));
45
+ req.on('timeout', () => { req.destroy(); resolve({ statusCode: 0, body: '' }); });
46
+ req.end();
47
+ });
48
+ }
49
+ /** Execute a POST/PATCH request. Rejects on network error or timeout. */
50
+ export function executeBody(urlStr, method, payload, timeoutMs) {
51
+ const url = new URL(urlStr);
52
+ const options = buildBodyOptions(url, payload, method, timeoutMs);
53
+ const transport = url.protocol === 'https:' ? https.request : http.request;
54
+ return new Promise((resolve, reject) => {
55
+ let done = false;
56
+ const timer = setTimeout(() => {
57
+ if (!done) {
58
+ done = true;
59
+ req.destroy();
60
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
61
+ }
62
+ }, timeoutMs);
63
+ const req = transport(options, (res) => {
64
+ let body = '';
65
+ res.setEncoding('utf8');
66
+ res.on('data', (chunk) => { body += chunk; });
67
+ res.on('end', () => {
68
+ done = true;
69
+ clearTimeout(timer);
70
+ resolve({ statusCode: res.statusCode ?? 0, statusMessage: res.statusMessage ?? '', headers: res.headers, body });
71
+ });
72
+ });
73
+ req.on('error', (err) => {
74
+ done = true;
75
+ clearTimeout(timer);
76
+ console.error('Request error:', err.message, err.code);
77
+ reject(err);
78
+ });
79
+ req.on('timeout', () => {
80
+ done = true;
81
+ clearTimeout(timer);
82
+ req.destroy();
83
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
84
+ });
85
+ req.write(payload);
86
+ req.end();
87
+ });
88
+ }
@@ -0,0 +1,3 @@
1
+ export { FILE_PATH_REGISTRY_FILE_PATTERNS_PATH, FILE_PATH_REGISTRY_SENSITIVE_PATHS_PATH, } from './types.js';
2
+ export { buildApiUrl, getFileCollectionPatterns, getSensitivePathsAuditCandidates, } from './registry_api.js';
3
+ export { postStartupPayload, patchPayload, classifyEndpointResponse } from './startup_api.js';
@@ -0,0 +1,41 @@
1
+ import { URL } from 'node:url';
2
+ import { executeGet } from './http_transport.js';
3
+ import { FILE_PATH_REGISTRY_FILE_PATTERNS_PATH, FILE_PATH_REGISTRY_SENSITIVE_PATHS_PATH, } from './types.js';
4
+ /** Join API base (may include a path prefix, e.g. https://host/optimus/) with an absolute API path. */
5
+ export function buildApiUrl(base, path) {
6
+ const normalizedBase = `${base.replace(/\/+$/, '')}/`;
7
+ const relativePath = path.replace(/^\/+/, '');
8
+ return new URL(relativePath, normalizedBase).href;
9
+ }
10
+ /**
11
+ * GET file collection patterns from the backend (what to look for).
12
+ * No auth required. Returns the complete list of path + type + file_type.
13
+ */
14
+ export const getFileCollectionPatterns = async (apiBaseUrl, timeoutMs = 5000) => {
15
+ const { statusCode, body } = await executeGet(buildApiUrl(apiBaseUrl, FILE_PATH_REGISTRY_FILE_PATTERNS_PATH), timeoutMs);
16
+ if (statusCode !== 200 || !body)
17
+ return null;
18
+ try {
19
+ const parsed = JSON.parse(body);
20
+ return Array.isArray(parsed.patterns) && typeof parsed.count === 'number' ? parsed : null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ };
26
+ /**
27
+ * GET sensitive path audit candidates from the backend (concrete paths to check for existence).
28
+ * No auth required. Returns path templates (~ = home). Client checks existence only, never sends contents.
29
+ */
30
+ export const getSensitivePathsAuditCandidates = async (apiBaseUrl, timeoutMs = 5000) => {
31
+ const { statusCode, body } = await executeGet(buildApiUrl(apiBaseUrl, FILE_PATH_REGISTRY_SENSITIVE_PATHS_PATH), timeoutMs);
32
+ if (statusCode !== 200 || !body)
33
+ return null;
34
+ try {
35
+ const parsed = JSON.parse(body);
36
+ return Array.isArray(parsed.paths) ? parsed : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ };