@optimuslabs/harness-map-staging 1.5.16

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 (88) hide show
  1. package/README.md +46 -0
  2. package/dist/apply_deferred_vscdb.js +38 -0
  3. package/dist/bootstrap_constants.js +15 -0
  4. package/dist/cli/bash_script_generator.js +95 -0
  5. package/dist/cli.js +144 -0
  6. package/dist/cli_invocation_match.js +30 -0
  7. package/dist/compliance_check_runner.js +57 -0
  8. package/dist/compliance_prompt_gate.js +396 -0
  9. package/dist/dialog_prefs_cli.js +109 -0
  10. package/dist/endpoint_client/compress_payload.js +25 -0
  11. package/dist/endpoint_client/http_transport.js +90 -0
  12. package/dist/endpoint_client/index.js +3 -0
  13. package/dist/endpoint_client/registry_api.js +41 -0
  14. package/dist/endpoint_client/startup_api.js +44 -0
  15. package/dist/endpoint_client/types.js +4 -0
  16. package/dist/execute_trusted_restarts.js +60 -0
  17. package/dist/log_config_files/auth/auth_flow.js +22 -0
  18. package/dist/log_config_files/auth/auth_key_store.js +14 -0
  19. package/dist/log_config_files/collection/claude_desktop_extensions_collector.js +248 -0
  20. package/dist/log_config_files/collection/claude_known_projects_collector.js +42 -0
  21. package/dist/log_config_files/collection/claude_token_usage_collector.js +203 -0
  22. package/dist/log_config_files/collection/codex_token_usage_collector.js +342 -0
  23. package/dist/log_config_files/collection/config_collector.js +245 -0
  24. package/dist/log_config_files/collection/copilot_token_usage_collector.js +167 -0
  25. package/dist/log_config_files/collection/cowork_desktop_version_collector.js +60 -0
  26. package/dist/log_config_files/collection/cowork_session_whitelist.js +89 -0
  27. package/dist/log_config_files/collection/cursor_project_mcp_collector.js +230 -0
  28. package/dist/log_config_files/collection/cursor_project_workspace_activity.js +73 -0
  29. package/dist/log_config_files/collection/cursor_token_usage_collector.js +192 -0
  30. package/dist/log_config_files/collection/directory_collector.js +126 -0
  31. package/dist/log_config_files/collection/enrichment_helpers.js +53 -0
  32. package/dist/log_config_files/collection/ensure_cursor_user_settings_snapshot.js +50 -0
  33. package/dist/log_config_files/collection/file_type_rules.js +47 -0
  34. package/dist/log_config_files/collection/grok_cli_version_collector.js +53 -0
  35. package/dist/log_config_files/collection/hermes_token_usage_collector.js +241 -0
  36. package/dist/log_config_files/collection/mcp_tool_collector.js +106 -0
  37. package/dist/log_config_files/collection/metadata_merge.js +43 -0
  38. package/dist/log_config_files/collection/openclaw_helpers.js +55 -0
  39. package/dist/log_config_files/collection/openclaw_token_usage_collector.js +223 -0
  40. package/dist/log_config_files/collection/opencode_token_usage_collector.js +177 -0
  41. package/dist/log_config_files/collection/pi_token_usage_collector.js +239 -0
  42. package/dist/log_config_files/collection/plugin_collector.js +223 -0
  43. package/dist/log_config_files/collection/plugin_version_helpers.js +37 -0
  44. package/dist/log_config_files/collection/skills_cli_collector.js +162 -0
  45. package/dist/log_config_files/index.js +19 -0
  46. package/dist/log_config_files/paths/path_constants_helpers.js +71 -0
  47. package/dist/log_config_files/paths/pattern_resolver.js +326 -0
  48. package/dist/log_config_files/readers/composer_shadow_merge.js +67 -0
  49. package/dist/log_config_files/readers/cursor_shadow_merge_ruleset.js +46 -0
  50. package/dist/log_config_files/readers/file_readers.js +196 -0
  51. package/dist/log_config_files/readers/vscdb_config_builder.js +148 -0
  52. package/dist/log_config_files/readers/vscdb_reactive_storage.js +62 -0
  53. package/dist/log_config_files/readers/vscdb_reader.js +337 -0
  54. package/dist/log_config_files/runtime/client_event_reporter.js +347 -0
  55. package/dist/log_config_files/runtime/compliance_check.js +1013 -0
  56. package/dist/log_config_files/runtime/compliance_session_log.js +378 -0
  57. package/dist/log_config_files/runtime/dialog_preferences.js +31 -0
  58. package/dist/log_config_files/runtime/hardware_uuid.js +36 -0
  59. package/dist/log_config_files/runtime/hook_logger.js +267 -0
  60. package/dist/log_config_files/runtime/hook_type_for_request.js +30 -0
  61. package/dist/log_config_files/runtime/log_metadata.js +122 -0
  62. package/dist/log_config_files/runtime/main_runner.js +376 -0
  63. package/dist/log_config_files/runtime/management_storage.js +144 -0
  64. package/dist/log_config_files/runtime/ops_target_path.js +18 -0
  65. package/dist/log_config_files/runtime/remediation_apply_tracking.js +152 -0
  66. package/dist/log_config_files/runtime/remediation_config_path.js +140 -0
  67. package/dist/log_config_files/runtime/remediation_sync.js +1705 -0
  68. package/dist/log_config_files/runtime/secret_regex_scan.js +126 -0
  69. package/dist/log_config_files/runtime/sqlite_binary.js +124 -0
  70. package/dist/log_config_files/runtime/trusted_restarts.js +113 -0
  71. package/dist/log_config_files/runtime/workspace_repo.js +170 -0
  72. package/dist/log_config_files/runtime/worktree_absent.js +50 -0
  73. package/dist/log_config_files/runtime/worktree_scanner.js +137 -0
  74. package/dist/log_config_files/sender/batch_sender.js +283 -0
  75. package/dist/log_config_files/sender/endpoint_config.js +24 -0
  76. package/dist/log_config_files/sender/signing.js +1 -0
  77. package/dist/log_sensitive_paths_audit.js +89 -0
  78. package/dist/log_uuid/auth_key_store.js +37 -0
  79. package/dist/log_uuid/hardware_uuid.js +35 -0
  80. package/dist/log_uuid/index.js +11 -0
  81. package/dist/log_uuid/log_uuid_helper.js +30 -0
  82. package/dist/log_uuid/startup_sender.js +131 -0
  83. package/dist/log_uuid/user_profile.js +192 -0
  84. package/dist/remediation_change_preview.js +182 -0
  85. package/dist/tofu.js +22 -0
  86. package/dist/tofu_environment.js +36 -0
  87. package/dist/types/config_file_types.js +1 -0
  88. package/package.json +67 -0
@@ -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,124 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ let cached;
5
+ /**
6
+ * Resolve the sqlite3 CLI for remediations and vscdb reads.
7
+ *
8
+ * Cursor/IDE-launched hooks often inherit a minimal PATH (no Homebrew), while macOS still ships
9
+ * `/usr/bin/sqlite3`. Prefer explicit paths before `which` / bare `sqlite3`.
10
+ *
11
+ * Override: `OPTIMUS_SQLITE3`, `SQLITE3_PATH`, or `SQLITE3_BIN` = absolute path to the binary.
12
+ */
13
+ export function resolveSqlite3Binary() {
14
+ if (cached !== undefined)
15
+ return cached;
16
+ for (const envName of ['OPTIMUS_SQLITE3', 'SQLITE3_PATH', 'SQLITE3_BIN']) {
17
+ const v = process.env[envName]?.trim();
18
+ if (v && existsSync(v)) {
19
+ cached = v;
20
+ return cached;
21
+ }
22
+ }
23
+ const candidates = [];
24
+ if (process.platform === 'darwin') {
25
+ candidates.push('/usr/bin/sqlite3', '/opt/homebrew/bin/sqlite3', '/usr/local/bin/sqlite3', '/opt/local/bin/sqlite3');
26
+ }
27
+ else if (process.platform === 'linux') {
28
+ candidates.push('/usr/bin/sqlite3', '/usr/local/bin/sqlite3');
29
+ }
30
+ else if (process.platform === 'win32') {
31
+ const pf = process.env.ProgramFiles;
32
+ if (pf) {
33
+ candidates.push(join(pf, 'Git', 'usr', 'bin', 'sqlite3.exe'));
34
+ }
35
+ }
36
+ for (const p of candidates) {
37
+ if (p && existsSync(p)) {
38
+ cached = p;
39
+ return cached;
40
+ }
41
+ }
42
+ try {
43
+ if (process.platform === 'win32') {
44
+ const out = execFileSync('where.exe', ['sqlite3'], {
45
+ encoding: 'utf8',
46
+ stdio: ['ignore', 'pipe', 'ignore'],
47
+ }).trim();
48
+ const first = out.split(/\r?\n/).find((line) => {
49
+ const t = line.trim();
50
+ return t.length > 0 && existsSync(t);
51
+ });
52
+ if (first) {
53
+ cached = first.trim();
54
+ return cached;
55
+ }
56
+ }
57
+ else {
58
+ for (const whichBin of ['which', '/usr/bin/which']) {
59
+ try {
60
+ const out = execFileSync(whichBin, ['sqlite3'], {
61
+ encoding: 'utf8',
62
+ stdio: ['ignore', 'pipe', 'ignore'],
63
+ }).trim();
64
+ const first = out.split(/\n/)[0]?.trim();
65
+ if (first && existsSync(first)) {
66
+ cached = first;
67
+ return cached;
68
+ }
69
+ }
70
+ catch {
71
+ /* try next */
72
+ }
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ /* fall through */
78
+ }
79
+ try {
80
+ execFileSync('sqlite3', ['--version'], { stdio: 'ignore' });
81
+ cached = 'sqlite3';
82
+ return cached;
83
+ }
84
+ catch {
85
+ cached = null;
86
+ return null;
87
+ }
88
+ }
89
+ /** Test-only: reset memoized path after env/path changes. */
90
+ export function clearSqlite3BinaryCacheForTests() {
91
+ cached = undefined;
92
+ }
93
+ /**
94
+ * Run the sqlite3 CLI with `-escape off` inserted before the trailing dbPath argument, so raw
95
+ * control-byte output (our char(30)/char(31) row/field separators, or any control byte in a
96
+ * stored value) survives instead of being rendered as printable caret-notation text — the
97
+ * default in sqlite3 CLI builds that support `-escape` (added alongside the `-escape symbol`
98
+ * default; confirmed present in a macOS-bundled 3.51.0 from 2025-06).
99
+ *
100
+ * Falls back to the same call without `-escape off` if the flag itself isn't recognized (an
101
+ * older CLI predating the flag also predates the `-escape symbol` default that motivates it, so
102
+ * dropping the flag there is safe, not a silent behavior change). `argsWithDbPathLast`'s last
103
+ * element must be the database path.
104
+ */
105
+ export function execSqlite3(sqlite3, argsWithDbPathLast, input) {
106
+ const dbPath = argsWithDbPathLast[argsWithDbPathLast.length - 1];
107
+ const leadingArgs = argsWithDbPathLast.slice(0, -1);
108
+ const options = {
109
+ input,
110
+ encoding: 'utf8',
111
+ stdio: ['pipe', 'pipe', 'pipe'],
112
+ };
113
+ try {
114
+ return execFileSync(sqlite3, [...leadingArgs, '-escape', 'off', dbPath], options);
115
+ }
116
+ catch (err) {
117
+ try {
118
+ return execFileSync(sqlite3, argsWithDbPathLast, options);
119
+ }
120
+ catch {
121
+ throw err;
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Execute autofix restart_command strings only if allowlisted (same rules as gate enqueue).
3
+ * Used by the compliance shell hook via execute_trusted_restarts CLI — single place for allowlist + spawn.
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+ import { appendComplianceRunnerLine, hookRunLog } from './hook_logger.js';
7
+ import { getActiveComplianceLogPath } from './compliance_session_log.js';
8
+ import { getDeferredVscdbRestartLogPath } from './management_storage.js';
9
+ import { normalizeAgentToken } from './compliance_check.js';
10
+ import { isClaudeRestartCommand, isCursorRestartCommand, isTrustedRestartCommandForAutofix } from './remediation_sync.js';
11
+ import { resolveProjectRoot } from './workspace_repo.js';
12
+ const FALLBACK_PATH = '/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin';
13
+ /** Vars set by `npx --package=...` that break unrelated scoped `npx @scope/pkg` after Cursor reopen. */
14
+ const NPM_RESTART_POLLUTION = [
15
+ 'npm_config_package',
16
+ 'npm_lifecycle_event',
17
+ 'npm_lifecycle_script',
18
+ 'npm_config_local_prefix',
19
+ ];
20
+ function envForRestartSpawn(base) {
21
+ const env = { ...base };
22
+ for (const key of NPM_RESTART_POLLUTION) {
23
+ delete env[key];
24
+ }
25
+ return env;
26
+ }
27
+ function isDeferredVscdbRestart(cmd) {
28
+ return cmd.includes('OPTIMUS_DEFERRED_LOG') || cmd.includes('apply_deferred_vscdb');
29
+ }
30
+ function currentAgentFromEnv() {
31
+ // Keep restart scoping consistent with remediation scoping:
32
+ // prefer OPTIMUS_AGENT override, then fall back to OPTIMUS_HOOK_TYPE.
33
+ const override = normalizeAgentToken(process.env.OPTIMUS_AGENT);
34
+ if (override === 'cursor')
35
+ return 'cursor';
36
+ if (override === 'copilot')
37
+ return 'copilot';
38
+ if (override === 'opencode')
39
+ return 'opencode';
40
+ if (override === 'codex')
41
+ return 'codex';
42
+ if (override === 'claude' || override === 'claude_desktop')
43
+ return 'claude';
44
+ const hookType = normalizeAgentToken(process.env.OPTIMUS_HOOK_TYPE);
45
+ if (hookType === 'cursor')
46
+ return 'cursor';
47
+ if (hookType === 'copilot')
48
+ return 'copilot';
49
+ if (hookType === 'opencode')
50
+ return 'opencode';
51
+ if (hookType === 'codex')
52
+ return 'codex';
53
+ return 'claude';
54
+ }
55
+ /** Spawn each trusted command detached (same pattern as former compliance_prompt_gate fireRestartCommands). */
56
+ export function executeTrustedRestartCommands(commands) {
57
+ const agent = currentAgentFromEnv();
58
+ for (const cmd of commands) {
59
+ const t = cmd.trim();
60
+ if (!t)
61
+ continue;
62
+ if (!isTrustedRestartCommandForAutofix(cmd)) {
63
+ const msg = `rejected_not_allowlisted prefix=${t.slice(0, 200)}`;
64
+ hookRunLog(`execute_trusted_restarts: ${msg}`);
65
+ appendComplianceRunnerLine('RESTART', msg);
66
+ continue;
67
+ }
68
+ // Safety: never restart the wrong app for the current hook type.
69
+ if (agent === 'cursor' && isClaudeRestartCommand(t)) {
70
+ const msg = 'rejected_claude_restart_in_cursor_hook';
71
+ hookRunLog(`execute_trusted_restarts: ${msg}`);
72
+ appendComplianceRunnerLine('RESTART', msg);
73
+ continue;
74
+ }
75
+ if (agent === 'claude' && isCursorRestartCommand(t)) {
76
+ const msg = 'rejected_cursor_restart_in_claude_hook';
77
+ hookRunLog(`execute_trusted_restarts: ${msg}`);
78
+ appendComplianceRunnerLine('RESTART', msg);
79
+ continue;
80
+ }
81
+ const deferred = isDeferredVscdbRestart(t);
82
+ const detailPath = getDeferredVscdbRestartLogPath();
83
+ if (deferred) {
84
+ hookRunLog(`execute_trusted_restarts: spawning deferred state.vscdb restart (killall Cursor, then apply + reopen). Step-by-step log → ${detailPath}`);
85
+ appendComplianceRunnerLine('RESTART', `spawn deferred_vscdb_restart sh -c (detail_log=${detailPath}) cwd_inherited_from_node`);
86
+ }
87
+ else {
88
+ hookRunLog('execute_trusted_restarts: spawning trusted restart (see compliance session log [RESTART])');
89
+ appendComplianceRunnerLine('RESTART', 'spawn trusted_restart sh -c (non-deferred)');
90
+ }
91
+ const projectRoot = resolveProjectRoot();
92
+ const complianceLogPath = getActiveComplianceLogPath();
93
+ const child = spawn('sh', ['-c', t], {
94
+ detached: true,
95
+ stdio: 'ignore',
96
+ cwd: projectRoot,
97
+ env: envForRestartSpawn({
98
+ ...process.env,
99
+ CURSOR_PROJECT_DIR: process.env.CURSOR_PROJECT_DIR || projectRoot,
100
+ ...(complianceLogPath ? { OPTIMUS_COMPLIANCE_LOG: complianceLogPath } : {}),
101
+ PATH: process.env.PATH && String(process.env.PATH).trim().length > 0
102
+ ? process.env.PATH
103
+ : FALLBACK_PATH,
104
+ }),
105
+ });
106
+ child.on('error', (err) => {
107
+ const emsg = err instanceof Error ? err.message : String(err);
108
+ hookRunLog(`execute_trusted_restarts: spawn failed ${emsg}`);
109
+ appendComplianceRunnerLine('RESTART', `spawn_error ${emsg}`);
110
+ });
111
+ child.unref();
112
+ }
113
+ }
@@ -0,0 +1,170 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { hookRunLog } from './hook_logger.js';
6
+ const GIT_TIMEOUT_MS = 3000;
7
+ const MAX_WALK_UP = 25;
8
+ function gitRemoteInDir(dir) {
9
+ try {
10
+ return execFileSync('git', ['remote', '-v'], {
11
+ cwd: dir,
12
+ timeout: GIT_TIMEOUT_MS,
13
+ encoding: 'utf8',
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ }).trim();
16
+ }
17
+ catch {
18
+ return '';
19
+ }
20
+ }
21
+ function gitToplevelFromDir(dir) {
22
+ try {
23
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
24
+ cwd: dir,
25
+ timeout: GIT_TIMEOUT_MS,
26
+ encoding: 'utf8',
27
+ stdio: ['pipe', 'pipe', 'pipe'],
28
+ }).trim();
29
+ }
30
+ catch {
31
+ return '';
32
+ }
33
+ }
34
+ export function resolveProjectDirFromEnv() {
35
+ for (const candidate of [
36
+ process.env.OPTIMUS_PROJECT_DIR,
37
+ process.env.CLAUDE_PROJECT_DIR,
38
+ process.env.CURSOR_PROJECT_DIR,
39
+ ]) {
40
+ const dir = (candidate || '').trim();
41
+ if (dir && fs.existsSync(dir))
42
+ return dir;
43
+ }
44
+ return '';
45
+ }
46
+ export function resolveWorkspaceRepoFromEnv() {
47
+ for (const candidate of [
48
+ process.env.OPTIMUS_WORKSPACE_REPO,
49
+ process.env.GITHUB_REPOSITORY,
50
+ process.env.GH_REPOSITORY,
51
+ ]) {
52
+ const value = (candidate || '').trim();
53
+ if (value)
54
+ return value;
55
+ }
56
+ return '';
57
+ }
58
+ function expandManifestPath(filePath) {
59
+ const trimmed = (filePath || '').trim();
60
+ if (!trimmed)
61
+ return '';
62
+ if (trimmed.startsWith('/'))
63
+ return trimmed;
64
+ if (/^Users-[^/]+/.test(trimmed)) {
65
+ return path.join(os.homedir(), '.cursor', 'projects', trimmed);
66
+ }
67
+ return path.resolve(trimmed);
68
+ }
69
+ export function resolveRepoFromPath(filePath) {
70
+ const remote = gitRemoteFromPathWalk(filePath);
71
+ return remote || resolveWorkspaceRepoFromEnv();
72
+ }
73
+ function gitRemoteFromPathWalk(filePath) {
74
+ let dir = expandManifestPath(filePath);
75
+ if (!dir)
76
+ return '';
77
+ try {
78
+ if (fs.existsSync(dir) && fs.statSync(dir).isFile())
79
+ dir = path.dirname(dir);
80
+ else if (!fs.existsSync(dir))
81
+ dir = path.dirname(dir);
82
+ }
83
+ catch {
84
+ dir = path.dirname(dir);
85
+ }
86
+ for (let i = 0; i < MAX_WALK_UP; i++) {
87
+ const remote = gitRemoteInDir(dir);
88
+ if (remote)
89
+ return remote;
90
+ const parent = path.dirname(dir);
91
+ if (parent === dir)
92
+ break;
93
+ dir = parent;
94
+ }
95
+ return '';
96
+ }
97
+ function manifestPathPriority(filePath) {
98
+ if (filePath.startsWith('/Users/') || filePath.startsWith('/home/'))
99
+ return 0;
100
+ if (filePath.startsWith('/'))
101
+ return 1;
102
+ if (/^Users-[^/]+/.test(filePath))
103
+ return 2;
104
+ return 3;
105
+ }
106
+ export function resolveWorkspaceRepoFromManifest(manifest) {
107
+ if (!manifest.length)
108
+ return '';
109
+ const candidates = [...manifest]
110
+ .map((entry) => (entry || '').trim())
111
+ .filter(Boolean)
112
+ .sort((a, b) => manifestPathPriority(a) - manifestPathPriority(b));
113
+ for (const candidate of candidates) {
114
+ const remote = gitRemoteFromPathWalk(candidate);
115
+ if (remote)
116
+ return remote;
117
+ }
118
+ return '';
119
+ }
120
+ export function resolveWorkspaceRepo(manifest) {
121
+ const fromEnv = resolveWorkspaceRepoFromEnv();
122
+ if (fromEnv) {
123
+ hookRunLog(`workspace_repo: env (${fromEnv.slice(0, 80)}${fromEnv.length > 80 ? '…' : ''})`);
124
+ return fromEnv;
125
+ }
126
+ const projectDir = resolveProjectDirFromEnv();
127
+ if (projectDir) {
128
+ const remote = gitRemoteInDir(projectDir);
129
+ if (remote) {
130
+ hookRunLog(`workspace_repo: project_dir (${projectDir})`);
131
+ return remote;
132
+ }
133
+ }
134
+ const cwdRemote = gitRemoteInDir(process.cwd());
135
+ if (cwdRemote) {
136
+ hookRunLog(`workspace_repo: cwd (${process.cwd()})`);
137
+ return cwdRemote;
138
+ }
139
+ if (manifest?.length) {
140
+ const fromManifest = resolveWorkspaceRepoFromManifest(manifest);
141
+ if (fromManifest) {
142
+ hookRunLog('workspace_repo: manifest');
143
+ return fromManifest;
144
+ }
145
+ }
146
+ hookRunLog(`workspace_repo: unresolved (project_dir=${projectDir || 'unset'} cwd=${process.cwd()})`);
147
+ return '';
148
+ }
149
+ export function resolveProjectRoot() {
150
+ const projectDir = resolveProjectDirFromEnv();
151
+ if (projectDir) {
152
+ const top = gitToplevelFromDir(projectDir);
153
+ if (top)
154
+ return top;
155
+ return projectDir;
156
+ }
157
+ const cwdTop = gitToplevelFromDir(process.cwd());
158
+ if (cwdTop)
159
+ return cwdTop;
160
+ return process.cwd();
161
+ }
162
+ export function ensureWorkspaceRepoEnv(manifest) {
163
+ const resolved = resolveWorkspaceRepo(manifest);
164
+ if (resolved) {
165
+ process.env.OPTIMUS_WORKSPACE_REPO = resolved;
166
+ if (!process.env.GITHUB_REPOSITORY)
167
+ process.env.GITHUB_REPOSITORY = resolved;
168
+ }
169
+ return resolved;
170
+ }
@@ -0,0 +1,50 @@
1
+ import { executeBody } from '../../endpoint_client/http_transport.js';
2
+ import { buildApiUrl } from '../../endpoint_client/registry_api.js';
3
+ import { loadEndpointBase } from '../sender/endpoint_config.js';
4
+ import { createSignature } from '../sender/signing.js';
5
+ import { readStoredAuthKey } from '../auth/auth_key_store.js';
6
+ import { tryResolveHardwareUuid } from './hardware_uuid.js';
7
+ import { hookRunLog } from './hook_logger.js';
8
+ import { normalizePathForWorktree } from './worktree_scanner.js';
9
+ /**
10
+ * Notify server that worktree directories are gone so inventory/findings are pruned.
11
+ */
12
+ export function reportAbsentWorktrees(entries) {
13
+ const authKey = readStoredAuthKey();
14
+ if (!authKey) {
15
+ hookRunLog('worktree_absent: no auth key, skipping');
16
+ return Promise.resolve();
17
+ }
18
+ const hardwareUuid = tryResolveHardwareUuid();
19
+ if (!hardwareUuid) {
20
+ hookRunLog('worktree_absent: no hardware uuid, skipping');
21
+ return Promise.resolve();
22
+ }
23
+ const absent_worktrees = entries
24
+ .map((e) => ({
25
+ worktree_root: normalizePathForWorktree(e.worktree_root).replace(/\/+$/, ''),
26
+ reason: (e.reason || 'path_missing').slice(0, 64),
27
+ }))
28
+ .filter((e) => e.worktree_root)
29
+ .sort((a, b) => a.worktree_root.localeCompare(b.worktree_root));
30
+ if (absent_worktrees.length === 0) {
31
+ return Promise.resolve();
32
+ }
33
+ const endpointBase = loadEndpointBase();
34
+ const url = buildApiUrl(endpointBase, '/endpoint_security/worktree-absent/');
35
+ const payload = { hardware_uuid: hardwareUuid, absent_worktrees };
36
+ const signature = createSignature(payload, authKey.key);
37
+ const body = JSON.stringify({ ...payload, signature, key_id: authKey.key_id || '' });
38
+ return executeBody(url, 'POST', body, 8000)
39
+ .then(({ statusCode }) => {
40
+ if (statusCode !== 200) {
41
+ hookRunLog(`worktree_absent: server returned ${statusCode}`);
42
+ }
43
+ else {
44
+ hookRunLog(`worktree_absent: reported ${absent_worktrees.length} root(s)`);
45
+ }
46
+ })
47
+ .catch((err) => {
48
+ hookRunLog(`worktree_absent: request failed: ${err instanceof Error ? err.message : String(err)}`);
49
+ });
50
+ }